Angular ASP

Introduction

As an ASP.NET developer, you’re likely accustomed to building robust backend systems using C#, SQL Server, and frameworks like ASP.NET MVC or WebForms. However, modern web development increasingly demands dynamic, client-side experiences, which is where Angular shines. In this comprehensive guide, we’ll build an Invoice Entry System, a real-world enterprise application, using Angular for the front-end, ASP.NET Core for the backend, SQL Server with stored procedures, Dapper for data access, and CQRS (Command Query Responsibility Segregation) for a clean architecture.

We’ll integrate Serilog for logging, implement error handling and database transactions, and compare this modern stack with traditional ASP.NET MVC, WebForms, and jQuery/AJAX approaches.This blog post is tailored for ASP.NET developers transitioning to full-stack development with Angular.

We’ll cover

1. Project Overview

Invoice Entry System

The Invoice Entry System is a full-stack application that allows users to:

Real-World Scenarios

This system mirrors common enterprise applications:

Technology Stack

2. Why Angular + ASP.NET Core?

As an ASP.NET developer, you’re familiar with C#, MVC, and structured development. Angular complements this with:

Comparison with MVC, WebForms, and jQuery

Aspect

Angular + ASP.NET Core

ASP.NET MVC

WebForms

jQuery/AJAX

Architecture

Component-based (Angular) + CQRS (API); separates client/server logic.

MVC pattern; server-side rendering with controllers and views.

Code-behind model; tightly coupled UI and logic.

No architecture; manual DOM manipulation.

Data Binding

Two-way binding in Angular; declarative UI updates.

Server-side model binding; Razor views.

Server-side controls; limited client-side dynamism.

Manual DOM updates via JavaScript.

Scalability

CQRS enables separate read/write scaling; Dapper is lightweight.

Scales well with EF Core but heavier than Dapper.

Poor scalability due to stateful nature.

Scales poorly for complex apps.

Maintainability

Modular with components, services, and CQRS; TypeScript ensures type safety.

Modular with controllers and services; less client-side structure.

Hard to maintain due to code-behind.

Spaghetti code in large apps.

Performance

Angular’s SPA with lazy loading; Dapper’s minimal overhead.

Server-side rendering can be slower for dynamic UIs.

Heavy server-side processing; ViewState bloats pages.

Fast for small scripts but inefficient for SPAs.

Learning Curve

Steep for Angular (TypeScript, RxJS); familiar for .NET developers.

Moderate; familiar to .NET developers.

Easier but outdated; limited modern use.

Easy for small tasks; complex for large apps.

Real-World Analogy

Building with Angular + ASP.NET Core is like constructing a modular house with a factory (components, services) and a robust foundation (CQRS, Dapper). MVC is like a custom-built house with blueprints (Razor views), WebForms is like an old, rigid structure, and jQuery is like assembling furniture manually without a plan.

3. Setting Up the Environment Prerequisites

Step 1. Set Up SQL Server

  1. Create a database
    CREATE DATABASE InvoiceDb;
  2. Create tables
    USE InvoiceDb;
    
    CREATE TABLE Invoices (
        Id INT PRIMARY KEY IDENTITY(1,1),
        CustomerName NVARCHAR(100) NOT NULL,
        InvoiceDate DATE NOT NULL
    );
    
    CREATE TABLE InvoiceItems (
        Id INT PRIMARY KEY IDENTITY(1,1),
        InvoiceId INT NOT NULL,
        ProductName NVARCHAR(100) NOT NULL,
        Quantity INT NOT NULL,
        UnitPrice DECIMAL(18,2) NOT NULL,
        FOREIGN KEY (InvoiceId) REFERENCES Invoices(Id)
    );
  3. Create stored procedures
    • Create Invoice
      CREATE PROCEDURE sp_CreateInvoice
          @CustomerName NVARCHAR(100),
          @InvoiceDate DATE,
          @ItemsXml XML,
          @NewInvoiceId INT OUTPUT
      AS
      BEGIN
          SET NOCOUNT ON;
          BEGIN TRY
              BEGIN TRANSACTION;
      
              -- Insert Invoice
              INSERT INTO Invoices (CustomerName, InvoiceDate)
              VALUES (@CustomerName, @InvoiceDate);
              SET @NewInvoiceId = SCOPE_IDENTITY();
      
              -- Insert Items from XML
              INSERT INTO InvoiceItems (InvoiceId, ProductName, Quantity, UnitPrice)
              SELECT
                  @NewInvoiceId,
                  Item.value('(ProductName/text())[1]', 'NVARCHAR(100)'),
                  Item.value('(Quantity/text())[1]', 'INT'),
                  Item.value('(UnitPrice/text())[1]', 'DECIMAL(18,2)')
              FROM @ItemsXml.nodes('/Items/Item') AS T(Item);
      
              COMMIT TRANSACTION;
          END TRY
          BEGIN CATCH
              ROLLBACK TRANSACTION;
              DECLARE @ErrorMessage NVARCHAR(4000) = ERROR_MESSAGE();
              DECLARE @ErrorSeverity INT = ERROR_SEVERITY();
              DECLARE @ErrorState INT = ERROR_STATE();
              RAISERROR (@ErrorMessage, @ErrorSeverity, @ErrorState);
          END CATCH
      END;
    • Get Invoices
      CREATE PROCEDURE sp_GetInvoices
      AS
      BEGIN
          SET NOCOUNT ON;
          SELECT
              i.Id,
              i.CustomerName,
              i.InvoiceDate,
              (
                  SELECT
                      it.Id,
                      it.ProductName,
                      it.Quantity,
                      it.UnitPrice,
                      (it.Quantity * it.UnitPrice) AS Total
                  FROM InvoiceItems it
                  WHERE it.InvoiceId = i.Id
                  FOR JSON PATH
              ) AS Items
          FROM Invoices i
          FOR JSON PATH;
      END;

Step 2. Set Up ASP.NET Core API

  1. Create a new ASP.NET Core Web API project named InvoiceApi.
  2. Install NuGet packages:
    dotnet add package Dapper
    dotnet add package System.Data.SqlClient
    dotnet add package MediatR
    dotnet add package MediatR.Extensions.Microsoft.DependencyInjection
    dotnet add package Serilog.AspNetCore
    dotnet add package Serilog.Sinks.File
  3. Configure appsettings.json:
    {
      "ConnectionStrings": {
        "DefaultConnection": "Server=localhost;Database=InvoiceDb;Trusted_Connection=True;"
      },
      "Serilog": {
        "Using": [ "Serilog.Sinks.File" ],
        "MinimumLevel": {
          "Default": "Information",
          "Override": {
            "Microsoft": "Warning",
            "System": "Warning"
          }
        },
        "WriteTo": [
          {
            "Name": "File",
            "Args": {
              "path": "Logs/log-.txt",
              "rollingInterval": "Day",
              "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level}] {Message}{NewLine}{Exception}"
            }
          }
        ]
      }
    }
  4. Set up Serilog in Program.cs:
    using Microsoft.AspNetCore.Builder;
    using Microsoft.Extensions.DependencyInjection;
    using Serilog;
    using MediatR;
    
    var builder = WebApplication.CreateBuilder(args);
    
    // Configure Serilog
    builder.Host.UseSerilog((context, configuration) =>
    {
        configuration.ReadFrom.Configuration(context.Configuration);
    });
    
    builder.Services.AddControllers();
    builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));
    builder.Services.AddCors(options =>
    {
        options.AddPolicy("AllowAll", builder =>
        {
            builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader();
        });
    });
    
    var app = builder.Build();
    
    app.UseSerilogRequestLogging();
    app.UseCors("AllowAll");
    app.UseAuthorization();
    app.MapControllers();
    
    app.Run();

Step 3. Implement CQRS with Dapper

  1. Create models in Models/Invoice.cs:
    public class Invoice
    {
        public int Id { get; set; }
        public string CustomerName { get; set; }
        public DateTime InvoiceDate { get; set; }
        public List<InvoiceItem> Items { get; set; }
        public decimal TotalAmount => Items?.Sum(item => item.Total) ?? 0;
    }
    
    public class InvoiceItem
    {
        public int Id { get; set; }
        public string ProductName { get; set; }
        public int Quantity { get; set; }
        public decimal UnitPrice { get; set; }
        public decimal Total => Quantity * UnitPrice;
    }
  2. Create a command in Commands/CreateInvoiceCommand.cs:
    using MediatR;
    
    public class CreateInvoiceCommand : IRequest<int>
    {
        public string CustomerName { get; set; }
        public DateTime InvoiceDate { get; set; }
        public List<InvoiceItemDto> Items { get; set; }
    }
    
    public class InvoiceItemDto
    {
        public string ProductName { get; set; }
        public int Quantity { get; set; }
        public decimal UnitPrice { get; set; }
    }
  3. Create a query in Queries/GetInvoicesQuery.cs:
    using MediatR;
    using System.Collections.Generic;
    
    public class GetInvoicesQuery : IRequest<List<Invoice>> { }
  4. Create a command handler in Handlers/CreateInvoiceCommandHandler.cs:
    using Dapper;
    using MediatR;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.Logging;
    using System;
    using System.Data.SqlClient;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Xml.Linq;
    
    public class CreateInvoiceCommandHandler : IRequestHandler<CreateInvoiceCommand, int>
    {
        private readonly string _connectionString;
        private readonly ILogger<CreateInvoiceCommandHandler> _logger;
    
        public CreateInvoiceCommandHandler(IConfiguration configuration, ILogger<CreateInvoiceCommandHandler> logger)
        {
            _connectionString = configuration.GetConnectionString("DefaultConnection");
            _logger = logger;
        }
    
        public async Task<int> Handle(CreateInvoiceCommand request, CancellationToken cancellationToken)
        {
            _logger.LogInformation("Creating invoice for customer: {CustomerName}", request.CustomerName);
    
            try
            {
                // Validate inputs
                if (string.IsNullOrWhiteSpace(request.CustomerName))
                {
                    _logger.LogWarning("Invalid customer name provided.");
                    throw new ArgumentException("Customer name is required.");
                }
                if (!request.Items.Any())
                {
                    _logger.LogWarning("No items provided for invoice.");
                    throw new ArgumentException("At least one item is required.");
                }
    
                // Create XML for items
                var itemsXml = new XElement("Items",
                    request.Items.Select(item => new XElement("Item",
                        new XElement("ProductName", item.ProductName),
                        new XElement("Quantity", item.Quantity),
                        new XElement("UnitPrice", item.UnitPrice))));
    
                using (var connection = new SqlConnection(_connectionString))
                {
                    await connection.OpenAsync(cancellationToken);
                    var newInvoiceId = await connection.ExecuteScalarAsync<int>(
                        "sp_CreateInvoice",
                        new
                        {
                            CustomerName = request.CustomerName,
                            InvoiceDate = request.InvoiceDate,
                            ItemsXml = itemsXml.ToString(),
                            NewInvoiceId = 0
                        },
                        commandType: System.Data.CommandType.StoredProcedure);
    
                    _logger.LogInformation("Invoice created successfully with ID: {InvoiceId}", newInvoiceId);
                    return newInvoiceId;
                }
            }
            catch (SqlException ex)
            {
                _logger.LogError(ex, "Database error while creating invoice for customer: {CustomerName}", request.CustomerName);
                throw new Exception($"Database error: {ex.Message}", ex);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error creating invoice for customer: {CustomerName}", request.CustomerName);
                throw new Exception($"Error creating invoice: {ex.Message}", ex);
            }
        }
    }
  5. Create a query handler in Handlers/GetInvoicesQueryHandler.cs:
    using Dapper;
    using MediatR;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.Logging;
    using System.Collections.Generic;
    using System.Data.SqlClient;
    using System.Text.Json;
    using System.Threading;
    using System.Threading.Tasks;
    
    public class GetInvoicesQueryHandler : IRequestHandler<GetInvoicesQuery, List<Invoice>>
    {
        private readonly string _connectionString;
        private readonly ILogger<GetInvoicesQueryHandler> _logger;
    
        public GetInvoicesQueryHandler(IConfiguration configuration, ILogger<GetInvoicesQueryHandler> logger)
        {
            _connectionString = configuration.GetConnectionString("DefaultConnection");
            _logger = logger;
        }
    
        public async Task<List<Invoice>> Handle(GetInvoicesQuery request, CancellationToken cancellationToken)
        {
            _logger.LogInformation("Retrieving all invoices.");
    
            try
            {
                using (var connection = new SqlConnection(_connectionString))
                {
                    var jsonResult = await connection.QuerySingleAsync<string>(
                        "sp_GetInvoices",
                        commandType: System.Data.CommandType.StoredProcedure);
    
                    var invoices = JsonSerializer.Deserialize<List<Invoice>>(jsonResult);
                    _logger.LogInformation("Retrieved {Count} invoices.", invoices.Count);
                    return invoices;
                }
            }
            catch (SqlException ex)
            {
                _logger.LogError(ex, "Database error while retrieving invoices.");
                throw new Exception($"Database error: {ex.Message}", ex);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error retrieving invoices.");
                throw new Exception($"Error retrieving invoices: {ex.Message}", ex);
            }
        }
    }
  6. Update the controller in Controllers/InvoicesController.cs:
    using MediatR;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.Extensions.Logging;
    using System.Threading.Tasks;
    
    [Route("api/[controller]")]
    [ApiController]
    public class InvoicesController : ControllerBase
    {
        private readonly IMediator _mediator;
        private readonly ILogger<InvoicesController> _logger;
    
        public InvoicesController(IMediator mediator, ILogger<InvoicesController> logger)
        {
            _mediator = mediator;
            _logger = logger;
        }
    
        [HttpGet]
        public async Task<IActionResult> GetInvoices()
        {
            _logger.LogInformation("API call: GetInvoices");
            try
            {
                var invoices = await _mediator.Send(new GetInvoicesQuery());
                return Ok(invoices);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error in GetInvoices API call.");
                return StatusCode(500, new { Message = ex.Message });
            }
        }
    
        [HttpPost]
        public async Task<IActionResult> CreateInvoice([FromBody] CreateInvoiceCommand command)
        {
            _logger.LogInformation("API call: CreateInvoice for customer: {CustomerName}", command.CustomerName);
            try
            {
                var newInvoiceId = await _mediator.Send(command);
                return Ok(new { Id = newInvoiceId });
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error in CreateInvoice API call for customer: {CustomerName}", command.CustomerName);
                return BadRequest(new { Message = ex.Message });
            }
        }
    }

Step 4. Update Angular Front-End

The Angular front-end remains similar to the previous example, but we’ll ensure compatibility with the updated API.

  1. Update src/app/models/invoice.ts:
    export interface Invoice {
      id: number;
      customerName: string;
      invoiceDate: string;
      items: InvoiceItem[];
      totalAmount?: number;
    }
    
    export interface InvoiceItem {
      id: number;
      productName: string;
      quantity: number;
      unitPrice: number;
      total?: number;
    }
  2. Update src/app/services/invoice.service.ts:
    import { Injectable } from '@angular/core';
    import { HttpClient } from '@angular/common/http';
    import { Observable } from 'rxjs';
    import { Invoice } from '../models/invoice';
    import { environment } from '../../environments/environment';
    
    @Injectable({
      providedIn: 'root'
    })
    export class InvoiceService {
      private apiUrl = `${environment.apiUrl}/invoices`;
    
      constructor(private http: HttpClient) {}
    
      getInvoices(): Observable<Invoice[]> {
        return this.http.get<Invoice[]>(this.apiUrl);
      }
    
      createInvoice(invoice: Invoice): Observable<{ id: number }> {
        return this.http.post<{ id: number }>(this.apiUrl, invoice);
      }
    }
  3. Update src/app/invoice-entry/invoice-entry.component.ts to log client-side actions:
    import { Component } from '@angular/core';
    import { InvoiceService } from '../services/invoice.service';
    import { Invoice, InvoiceItem } from '../models/invoice';
    import { FormBuilder, FormGroup, Validators, FormArray } from '@angular/forms';
    import { Router } from '@angular/router';
    
    @Component({
      selector: 'app-invoice-entry',
      templateUrl: './invoice-entry.component.html',
      styleUrls: ['./invoice-entry.component.css']
    })
    export class InvoiceEntryComponent {
      invoiceForm: FormGroup;
    
      constructor(
        private fb: FormBuilder,
        private invoiceService: InvoiceService,
        private router: Router
      ) {
        this.invoiceForm = this.fb.group({
          customerName: ['', Validators.required],
          invoiceDate: ['', Validators.required],
          items: this.fb.array([])
        });
      }
    
      get items(): FormArray {
        return this.invoiceForm.get('items') as FormArray;
      }
    
      addItem(): void {
        const itemForm = this.fb.group({
          productName: ['', Validators.required],
          quantity: [1, [Validators.required, Validators.min(1)]],
          unitPrice: [0, [Validators.required, Validators.min(0)]]
        });
        this.items.push(itemForm);
        console.log('Added new item to invoice form.');
      }
    
      removeItem(index: number): void {
        this.items.removeAt(index);
        console.log(`Removed item at index ${index}.`);
      }
    
      getItemTotal(index: number): number {
        const item = this.items.at(index);
        return item.get('quantity')?.value * item.get('unitPrice')?.value;
      }
    
      getGrandTotal(): number {
        return this.items.controls.reduce((total, item) => {
          return total + (item.get('quantity')?.value * item.get('unitPrice')?.value);
        }, 0);
      }
    
      onSubmit(): void {
        if (this.invoiceForm.valid) {
          const invoice: Invoice = {
            id: 0,
            customerName: this.invoiceForm.get('customerName')?.value,
            invoiceDate: this.invoiceForm.get('invoiceDate')?.value,
            items: this.items.controls.map((item, index) => ({
              id: index + 1,
              productName: item.get('productName')?.value,
              quantity: item.get('quantity')?.value,
              unitPrice: item.get('unitPrice')?.value,
              total: this.getItemTotal(index)
            }))
          };
          console.log('Submitting invoice:', invoice);
          this.invoiceService.createInvoice(invoice).subscribe({
            next: (response) => {
              console.log(`Invoice created with ID: ${response.id}`);
              this.router.navigate(['/invoices']);
            },
            error: (err) => {
              console.error('Error creating invoice:', err);
              alert('Failed to create invoice: ' + err.message);
            }
          });
        } else {
          console.warn('Invoice form is invalid.');
        }
      }
    }
  4. Reuse the previous invoice-list component and routing setup.

4. Logging with Serilog

Serilog provides structured logging, similar to logging in .NET applications. It captures:

Log Output Example (in Logs/log-20250808.txt)

2025-08-08 21:44:23 [Information] API call: GetInvoices
2025-08-08 21:44:23 [Information] Retrieving all invoices.
2025-08-08 21:44:23 [Information] Retrieved 1 invoices.
2025-08-08 21:44:30 [Information] API call: CreateInvoice for customer: Jane Smith
2025-08-08 21:44:30 [Information] Creating invoice for customer: Jane Smith
2025-08-08 21:44:30 [Information] Invoice created successfully with ID: 2

Best Practices for Logging

5. Real-World Scenarios

Scenario 1: Retail Store

Scenario 2: Financial Billing

Scenario 3: ERP Integration

6. Pros and Cons

Angular + ASP.NET Core with CQRS/Dapper Pros

Cons

ASP.NET MVC Pros

Cons

WebForms Pros

Cons

jQuery/AJAX Pros

Cons

7. Best Practices Backend (ASP.NET Core, Dapper, CQRS)

Database (SQL Server)

Front-End (Angular)

General

8. Output

9. jQuery/AJAX Equivalent

For comparison, here’s a jQuery version of the create invoice functionality:

<script>
function saveInvoice() {
    const customerName = $('#customerName').val();
    const invoiceDate = $('#invoiceDate').val();
    const items = [];
    $('.row').each(function(index) {
        items.push({
            ProductName: $(this).find('.productName').val(),
            Quantity: parseInt($(this).find('.quantity').val()) || 0,
            UnitPrice: parseFloat($(this).find('.unitPrice').val()) || 0
        });
    });

    const itemsXml = `<Items>${items.map(item => `
        <Item>
            <ProductName>${item.ProductName}</ProductName>
            <Quantity>${item.Quantity}</Quantity>
            <UnitPrice>${item.UnitPrice}</UnitPrice>
        </Item>`).join('')}</Items>`;

    $.ajax({
        url: 'http://localhost:5000/api/invoices',
        type: 'POST',
        contentType: 'application/json',
        data: JSON.stringify({ customerName, invoiceDate, items }),
        success: function(response) {
            console.log('Invoice created:', response);
            window.location.href = '/invoices.html';
        },
        error: function(xhr) {
            console.error('Error:', xhr.responseJSON.Message);
            alert('Failed to create invoice: ' + xhr.responseJSON.Message);
        }
    });
}
</script>

Drawbacks

10. Conclusion

The Invoice Entry System demonstrates how Angular and ASP.NET Core, combined with Dapper, CQRS, SQL Server stored procedures, and Serilog, create a robust, scalable, and maintainable full-stack application. For ASP.NET developers, Angular’s TypeScript and component-based architecture feel familiar, while CQRS and Dapper enhance backend performance and clarity. Compared to MVC, WebForms, and jQuery, this stack offers superior modularity, scalability, and client-side dynamism, making it ideal for enterprise applications.