![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
- Create a database
CREATE DATABASE InvoiceDb;
- 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)
);
- 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
- Create a new ASP.NET Core Web API project named InvoiceApi.
- 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
- 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}"
}
}
]
}
}
- 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
- 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;
}
- 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; }
}
- Create a query in Queries/GetInvoicesQuery.cs:
using MediatR;
using System.Collections.Generic;
public class GetInvoicesQuery : IRequest<List<Invoice>> { }
- 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);
}
}
}
- 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);
}
}
}
- 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.
- 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;
}
- 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);
}
}
- 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.');
}
}
}
- 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.