ASP.NET Core  

Building an Invoice Entry System with Angular, ASP.NET Core, Dapper, CQRS, and SQL Server

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

  • Detailed Implementation: Step-by-step code for the Invoice Entry System.
  • Real-World Scenarios: Use cases in retail, finance, and ERP systems.
  • Pros and Cons: Comparing Angular + ASP.NET Core with MVC, WebForms, and jQuery.
  • Best Practices: Architecture, error handling, transactions, and logging.
  • Logging with Serilog: Capturing application events and errors.
  • Code Examples and Outputs: Practical, working code with expected results.

1. Project Overview

Invoice Entry System

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

  • Create Invoices: Enter customer details and multiple items (product, quantity, price) with dynamic total calculations.
  • View Invoices: Display a list of invoices with their items and totals.
  • Validate Inputs: Ensure required fields and valid data (e.g., positive quantities).
  • Log Operations: Track API calls, errors, and user actions using Serilog.
  • Use Transactions: Ensure atomicity when saving invoices and items.
  • Dynamic API URL: Configure the API endpoint for development and production.
  • Bootstrap Styling: Provide a responsive, polished UI.

Real-World Scenarios

This system mirrors common enterprise applications:

  • Retail: Creating invoices for customer purchases (e.g., at a supermarket).
  • Finance: Managing invoices for billing clients in accounting systems.
  • ERP Systems: Integrating with modules like SAP or Dynamics for inventory and sales.
  • E-Commerce: Generating invoices for online orders.

Technology Stack

  • Front-End: Angular with TypeScript, Bootstrap for styling.
  • Back-End: ASP.NET Core Web API, Dapper for data access, CQRS for architecture.
  • Database: SQL Server with stored procedures.
  • Logging: Serilog for structured logging.
  • Error Handling: Centralized in API and Angular.
  • Transactions: Managed in stored procedures and Dapper.

2. Why Angular + ASP.NET Core?

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

  • TypeScript: Similar to C# with static typing and classes.
  • Component-Based Architecture: Like MVC’s Views but client-side.
  • Dependency Injection (DI): Mirrors ASP.NET Core’s DI system.
  • Seamless API Integration: Angular’s HttpClient works well with ASP.NET Core APIs.
  • Enterprise-Ready: Angular’s modularity suits complex systems like those in .NET ecosystems.

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

  • SQL Server: Install SQL Server Express or Developer Edition.
  • Visual Studio: For ASP.NET Core development.
  • Node.js: LTS version (e.g., 18.x) from nodejs.org.
  • Angular CLI: npm install -g @angular/cli.
  • NuGet Packages: Dapper, Serilog, MediatR.
  • Bootstrap: For Angular UI styling.

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:

  • API Requests: Via UseSerilogRequestLogging.
  • Command/Query Execution: Errors and success in handlers.
  • Client-Side Actions: Console logs in Angular (replace with a proper logging service in production).

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

  • Structured Logging: Use Serilog’s properties (e.g., {CustomerName}) for searchable logs.
  • Log Levels: Use Information for normal operations, Warning for validation issues, Error for exceptions.
  • Sinks: Add sinks like Seq or Application Insights for centralized logging.
  • Client-Side Logging: Implement an Angular logging service to send logs to the server.

5. Real-World Scenarios

Scenario 1: Retail Store

  • Context: A supermarket needs to generate invoices for customer purchases.
  • Implementation: The cashier uses the Angular app to enter customer details and items (e.g., groceries). The ASP.NET Core API saves the invoice using sp_CreateInvoice, ensuring atomicity with transactions. Serilog logs each invoice creation for auditing.
  • Benefit: Angular’s reactive forms handle dynamic item entries, and CQRS allows fast invoice retrieval for reporting.

Scenario 2: Financial Billing

  • Context: An accounting firm bills clients for services.
  • Implementation: The app captures client details and service items (e.g., consulting hours). Dapper’s stored procedures ensure performance, and error handling catches invalid inputs (e.g., negative prices).
  • Benefit: TypeScript’s type safety and CQRS’s separation of concerns reduce errors and improve maintainability.

Scenario 3: ERP Integration

  • Context: An ERP system integrates invoicing with inventory and sales modules.
  • Implementation: The API uses CQRS to separate invoice queries (e.g., for dashboards) from commands (e.g., creating invoices). Transactions ensure inventory updates and invoice saves are atomic.
  • Benefit: Angular’s modularity and ASP.NET Core’s scalability suit complex ERP requirements.

6. Pros and Cons

Angular + ASP.NET Core with CQRS/Dapper Pros

  • Modularity: Angular components and CQRS handlers promote reusability.
  • Performance: Dapper and stored procedures are lightweight; Angular’s SPA is responsive.
  • Type Safety: TypeScript and C# reduce runtime errors.
  • Scalability: CQRS allows separate read/write scaling; Dapper minimizes overhead.
  • Error Handling: Centralized in API and Angular; Serilog provides detailed logs.
  • Transactions: Stored procedures ensure atomicity for complex operations.

Cons

  • Learning Curve: Angular’s TypeScript, RxJS, and CQRS require learning.
  • Complexity: CQRS adds overhead for small apps.
  • Setup Time: Configuring Dapper, MediatR, and Serilog takes effort.

ASP.NET MVC Pros

  • Familiarity: Razor views and controllers are intuitive for .NET developers.
  • Server-Side Rendering: Good for SEO and simpler initial loads.
  • Built-In Features: Model binding, validation, and routing are robust.

Cons

  • Less Dynamic: Server-side rendering limits client-side interactivity.
  • Heavier: EF Core and Razor can be slower than Dapper and Angular.
  • Tightly Coupled: Views and controllers are less modular than Angular components.

WebForms Pros

  • Rapid Development: Server controls simplify UI creation.
  • Familiar for Legacy: Common in older .NET systems.

Cons

  • Outdated: Heavy ViewState and limited client-side support.
  • Poor Scalability: Stateful nature hinders large apps.
  • Maintenance: Code-behind leads to spaghetti code.

jQuery/AJAX Pros

  • Simple for Small Apps: Quick to implement for basic functionality.
  • Lightweight: Minimal setup for small scripts.

Cons

  • No Structure: Leads to unmaintainable code in large apps.
  • Manual DOM Manipulation: Error-prone and time-consuming.
  • No Type Safety: JavaScript lacks compile-time checks.

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

  • Use Stored Procedures: Prevent SQL injection and improve performance.
  • Implement CQRS: Separate read/write logic for scalability and clarity.
  • Transactions: Use in stored procedures or Dapper for atomic operations.
  • Error Handling: Centralize in handlers; return meaningful HTTP responses (400, 500).
  • Logging: Use Serilog with structured data; log at appropriate levels (Info, Warning, Error).
  • Dependency Injection: Register services (e.g., MediatR, ILogger) in Program.cs.
  • Validation: Perform in command handlers before database operations.

Database (SQL Server)

  • Indexing: Add indexes on InvoiceItems.InvoiceId for faster joins.
  • Stored Procedures: Encapsulate complex logic; use XML for bulk inserts.
  • Transactions: Ensure atomicity for multi-table operations.
  • Error Handling: Use TRY-CATCH in stored procedures to rollback on errors.

Front-End (Angular)

  • Reactive Forms: Use for complex forms with validation.
  • Services: Encapsulate API calls and business logic.
  • TypeScript: Leverage interfaces for type safety.
  • Routing: Use Angular’s router for SPA navigation.
  • Error Handling: Handle API errors in subscribe or use interceptors.
  • Logging: Implement a client-side logging service for production.

General

  • Dynamic Configuration: Use environment.ts (Angular) and appsettings.json (ASP.NET Core).
  • Security: Add authentication (e.g., JWT) and input sanitization.
  • Testing: Write unit tests for Angular components and API handlers.

8. Output

  • Invoices List (/invoices)
    [Navbar: Invoices | Create Invoice]
    Invoices
    | ID | Customer   | Date       | Total   | Items                              |
    |----|------------|------------|---------|------------------------------------|
    | 1  | John Doe   | 8/8/2025   | $2100   | Laptop (Qty: 2, Price: $1000, Total: $2000) |
    |    |            |            |         | Mouse (Qty: 5, Price: $20, Total: $100)   |
  • Create Invoice (/create-invoice)
    [Navbar: Invoices | Create Invoice]
    Create Invoice
    Customer Name: [Jane Smith]
    Invoice Date: [2025-08-08]
    Items:
      [Keyboard] [Qty: 3] [Price: $50] [Total: $150.00] [Remove]
      [Monitor] [Qty: 1] [Price: $200] [Total: $200.00] [Remove]
    [Add Item]
    Grand Total: $350.00
    [Save Invoice]
  • Log File (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
  • Error Example
    • If customer name is empty: Angular shows “Customer name is required”; API logs warning and returns 400.
    • If database fails: API logs error and returns 500; Angular shows alert.

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

  • Manual XML construction is error-prone.
  • No type safety or structured validation.
  • Error handling is ad-hoc in callbacks.
  • No client-side logging framework.

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.