Friday, August 8, 2025
0 comments

Mastering Full-Stack Development: Building an Invoice Entry System with Angular, ASP.NET Core, Dapper, CQRS, and SQL Server

9:52 PM






Mastering Full-Stack Development: Building an Invoice Entry System with Angular, ASP.NET Core, Dapper, CQRS, and SQL Server

IntroductionAs 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 SystemThe 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 ScenariosThis 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 EnvironmentPrerequisites
  • 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:
    sql
    CREATE DATABASE InvoiceDb;
  2. Create tables:
    sql
    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:
      sql
      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:
      sql
      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:
    bash
    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:
    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:
    csharp
    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:
    csharp
    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:
    csharp
    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:
    csharp
    using MediatR;
    using System.Collections.Generic;
    
    public class GetInvoicesQuery : IRequest<List<Invoice>> { }
  4. Create a command handler in Handlers/CreateInvoiceCommandHandler.cs:
    csharp
    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:
    csharp
    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:
    csharp
    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-EndThe 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:
    typescript
    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:
    typescript
    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:
    typescript
    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 SerilogSerilog 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 ScenariosScenario 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 ConsAngular + ASP.NET Core with CQRS/DapperPros:
  • 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 MVCPros:
  • 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.
WebFormsPros:
  • 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/AJAXPros:
  • 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 PracticesBackend (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 EquivalentFor comparison, here’s a jQuery version of the create invoice functionality:
html
<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. ConclusionThe 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.
Next
This is the most recent post.
Older Post

0 comments:

 
Toggle Footer