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:
1. Project Overview: Invoice Entry SystemThe Invoice Entry System is a full-stack application that allows users to:
2. Why Angular + ASP.NET Core?As an ASP.NET developer, you’re familiar with C#, MVC, and structured development. Angular complements this with:
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
4. Logging with SerilogSerilog provides structured logging, similar to logging in .NET applications. It captures:Best Practices for Logging:
5. Real-World ScenariosScenario 1: Retail Store
6. Pros and ConsAngular + ASP.NET Core with CQRS/DapperPros:
7. Best PracticesBackend (ASP.NET Core, Dapper, CQRS)
8. Output
9. jQuery/AJAX EquivalentFor comparison, here’s a jQuery version of the create invoice functionality:Drawbacks:
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.
- 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.
- 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.
- 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.
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. |
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.
- Create a database:sql
CREATE DATABASE InvoiceDb;
- 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) );
- 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;
- Create Invoice:
- Create a new ASP.NET Core Web API project named InvoiceApi.
- 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
- 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}" } } ] } }
- 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();
- 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; }
- 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; } }
- Create a query in Queries/GetInvoicesQuery.cs:csharp
using MediatR; using System.Collections.Generic; public class GetInvoicesQuery : IRequest<List<Invoice>> { }
- 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); } } }
- 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); } } }
- 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 }); } } }
- 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; }
- 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); } }
- 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.'); } } }
- 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).
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
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- Rapid Development: Server controls simplify UI creation.
- Familiar for Legacy: Common in older .NET systems.
- Outdated: Heavy ViewState and limited client-side support.
- Poor Scalability: Stateful nature hinders large apps.
- Maintenance: Code-behind leads to spaghetti code.
- Simple for Small Apps: Quick to implement for basic functionality.
- Lightweight: Minimal setup for small scripts.
- 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.
- 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.
- 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.
- 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>
- 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.
0 comments:
Post a Comment