Thursday, August 7, 2025
0 comments

ASP.NET Core with Angular Invoice Management System

12:12 PM





ASP.NET Core with Angular Invoice Management System




This comprehensive guide will walk you through building a complete Invoice Management System using ASP.NET Core Web API with Angular, focusing on best practices for performance, security, maintainability, and clean architecture.

Table of Contents

  1. Technology Stack Selection

  2. System Architecture

  3. Database Design

  4. API Project Structure

  5. Implementing the Invoice Service

  6. Stored Procedure Implementation

  7. Dapper Integration

  8. JWT Authentication

  9. Error Handling

  10. Transaction Management

  11. Angular Implementation

  12. Deployment Considerations

Technology Stack Selection

Based on your requirements (performance, security, maintainability, DB-first approach), here's my recommendation:

  • Database AccessDapper (with raw SQL/Stored Procedures)

    • Why Dapper over EF Core?

      • Better performance for complex queries

      • More control over SQL

      • Easier to work with existing stored procedures

      • Less abstraction layer

    • Why not pure ADO.NET? Dapper provides a nice balance between raw performance and developer productivity

  • Database Approach: Stored Procedures for complex operations (like full CRUD in one proc)

    • Benefits:

      • Better security (SQL injection protection)

      • Performance optimization at DB level

      • DB-first friendly

      • Can change implementation without redeploying app

  • Authentication: JWT with refresh tokens

System Architecture

We'll use a Clean Architecture-inspired approach with these layers:

  1. API Layer: ASP.NET Core Web API (Controllers, DTOs)

  2. Application Layer: Services, Business Logic

  3. Infrastructure Layer: Data Access (Dapper), File Storage, etc.

  4. Domain Layer: Core Entities, Interfaces

text
InvoiceManagement/
├── API/                 # ASP.NET Core Web API
├── Application/         # Business logic, services
├── Domain/              # Entities, interfaces
├── Infrastructure/      # Data access, external services
└── AngularApp/          # Angular frontend

Database Design

For the Invoice Management system, we'll need these tables:

sql
CREATE TABLE Customers (
    CustomerId INT PRIMARY KEY IDENTITY(1,1),
    Name NVARCHAR(100) NOT NULL,
    Email NVARCHAR(100),
    Phone NVARCHAR(20),
    Address NVARCHAR(200),
    IsActive BIT DEFAULT 1,
    CreatedDate DATETIME DEFAULT GETDATE()
);

CREATE TABLE Products (
    ProductId INT PRIMARY KEY IDENTITY(1,1),
    Name NVARCHAR(100) NOT NULL,
    Description NVARCHAR(500),
    Price DECIMAL(18,2) NOT NULL,
    IsActive BIT DEFAULT 1,
    CreatedDate DATETIME DEFAULT GETDATE()
);

CREATE TABLE Invoices (
    InvoiceId INT PRIMARY KEY IDENTITY(1,1),
    CustomerId INT NOT NULL,
    InvoiceNumber NVARCHAR(20) NOT NULL UNIQUE,
    Date DATETIME NOT NULL DEFAULT GETDATE(),
    DueDate DATETIME NOT NULL,
    TotalAmount DECIMAL(18,2) NOT NULL,
    TaxAmount DECIMAL(18,2) NOT NULL,
    DiscountAmount DECIMAL(18,2) DEFAULT 0,
    Notes NVARCHAR(500),
    Status NVARCHAR(20) NOT NULL, -- Draft, Sent, Paid, Cancelled
    CreatedDate DATETIME DEFAULT GETDATE(),
    ModifiedDate DATETIME,
    FOREIGN KEY (CustomerId) REFERENCES Customers(CustomerId)
);

CREATE TABLE InvoiceItems (
    InvoiceItemId INT PRIMARY KEY IDENTITY(1,1),
    InvoiceId INT NOT NULL,
    ProductId INT NOT NULL,
    Quantity INT NOT NULL,
    UnitPrice DECIMAL(18,2) NOT NULL,
    Discount DECIMAL(18,2) DEFAULT 0,
    TaxRate DECIMAL(5,2) DEFAULT 0,
    Notes NVARCHAR(200),
    FOREIGN KEY (InvoiceId) REFERENCES Invoices(InvoiceId),
    FOREIGN KEY (ProductId) REFERENCES Products(ProductId)
);

API Project Structure

Let's start with the .NET Core API project structure:

1. Domain Layer

Entities/Invoice.cs

csharp
namespace InvoiceManagement.Domain.Entities
{
    public class Invoice
    {
        public int InvoiceId { get; set; }
        public int CustomerId { get; set; }
        public string InvoiceNumber { get; set; }
        public DateTime Date { get; set; }
        public DateTime DueDate { get; set; }
        public decimal TotalAmount { get; set; }
        public decimal TaxAmount { get; set; }
        public decimal DiscountAmount { get; set; }
        public string Notes { get; set; }
        public string Status { get; set; }
        public DateTime CreatedDate { get; set; }
        public DateTime? ModifiedDate { get; set; }
        public Customer Customer { get; set; }
        public List<InvoiceItem> Items { get; set; } = new List<InvoiceItem>();
    }

    public class InvoiceItem
    {
        public int InvoiceItemId { get; set; }
        public int InvoiceId { get; set; }
        public int ProductId { get; set; }
        public int Quantity { get; set; }
        public decimal UnitPrice { get; set; }
        public decimal Discount { get; set; }
        public decimal TaxRate { get; set; }
        public string Notes { get; set; }
        public Product Product { get; set; }
    }

    public class Customer
    {
        public int CustomerId { get; set; }
        public string Name { get; set; }
        public string Email { get; set; }
        public string Phone { get; set; }
        public string Address { get; set; }
        public bool IsActive { get; set; }
        public DateTime CreatedDate { get; set; }
    }

    public class Product
    {
        public int ProductId { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public decimal Price { get; set; }
        public bool IsActive { get; set; }
        public DateTime CreatedDate { get; set; }
    }
}

Interfaces/IRepository.cs

csharp
namespace InvoiceManagement.Domain.Interfaces
{
    public interface IRepository<T> where T : class
    {
        Task<T> GetByIdAsync(int id);
        Task<IEnumerable<T>> GetAllAsync();
        Task AddAsync(T entity);
        Task UpdateAsync(T entity);
        Task DeleteAsync(T entity);
    }

    public interface IInvoiceRepository : IRepository<Invoice>
    {
        Task<Invoice> GetInvoiceWithItemsAsync(int id);
        Task<IEnumerable<Invoice>> GetInvoicesByCustomerAsync(int customerId);
        Task<string> GenerateInvoiceNumberAsync();
    }
}

2. Infrastructure Layer

Data/InvoiceDbContext.cs

csharp
namespace InvoiceManagement.Infrastructure.Data
{
    public class InvoiceDbContext
    {
        private readonly string _connectionString;

        public InvoiceDbContext(IConfiguration configuration)
        {
            _connectionString = configuration.GetConnectionString("DefaultConnection");
        }

        public IDbConnection CreateConnection() => new SqlConnection(_connectionString);
    }
}

Repositories/InvoiceRepository.cs

csharp
namespace InvoiceManagement.Infrastructure.Repositories
{
    public class InvoiceRepository : IInvoiceRepository
    {
        private readonly InvoiceDbContext _context;

        public InvoiceRepository(InvoiceDbContext context)
        {
            _context = context;
        }

        public async Task<Invoice> GetByIdAsync(int id)
        {
            using var connection = _context.CreateConnection();
            
            var sql = @"
                SELECT i.*, c.* 
                FROM Invoices i
                JOIN Customers c ON i.CustomerId = c.CustomerId
                WHERE i.InvoiceId = @Id";
                
            var invoice = await connection.QueryAsync<Invoice, Customer, Invoice>(
                sql,
                (invoice, customer) => {
                    invoice.Customer = customer;
                    return invoice;
                },
                new { Id = id },
                splitOn: "CustomerId");
                
            return invoice.FirstOrDefault();
        }

        public async Task<Invoice> GetInvoiceWithItemsAsync(int id)
        {
            using var connection = _context.CreateConnection();
            
            var sql = @"
                SELECT i.*, c.*, ii.*, p.* 
                FROM Invoices i
                JOIN Customers c ON i.CustomerId = c.CustomerId
                LEFT JOIN InvoiceItems ii ON i.InvoiceId = ii.InvoiceId
                LEFT JOIN Products p ON ii.ProductId = p.ProductId
                WHERE i.InvoiceId = @Id";
                
            var invoiceDict = new Dictionary<int, Invoice>();
            
            await connection.QueryAsync<Invoice, Customer, InvoiceItem, Product, Invoice>(
                sql,
                (invoice, customer, item, product) => {
                    if (!invoiceDict.TryGetValue(invoice.InvoiceId, out var currentInvoice))
                    {
                        currentInvoice = invoice;
                        currentInvoice.Customer = customer;
                        currentInvoice.Items = new List<InvoiceItem>();
                        invoiceDict.Add(currentInvoice.InvoiceId, currentInvoice);
                    }
                    
                    if (item != null)
                    {
                        item.Product = product;
                        currentInvoice.Items.Add(item);
                    }
                    
                    return currentInvoice;
                },
                new { Id = id },
                splitOn: "CustomerId,InvoiceItemId,ProductId");
                
            return invoiceDict.Values.FirstOrDefault();
        }

        public async Task<string> GenerateInvoiceNumberAsync()
        {
            using var connection = _context.CreateConnection();
            var result = await connection.ExecuteScalarAsync<string>(
                "SELECT 'INV-' + FORMAT(GETDATE(), 'yyyyMMdd') + '-' + RIGHT('0000' + CAST(ISNULL(MAX(CAST(RIGHT(InvoiceNumber, 4) AS INT)), 0) + 1 AS VARCHAR(4)), 4) FROM Invoices WHERE InvoiceNumber LIKE 'INV-' + FORMAT(GETDATE(), 'yyyyMMdd') + '%'");
            return result ?? $"INV-{DateTime.Now:yyyyMMdd}-0001";
        }

        // Other CRUD operations...
    }
}

3. Application Layer

Services/InvoiceService.cs

csharp
namespace InvoiceManagement.Application.Services
{
    public interface IInvoiceService
    {
        Task<InvoiceDto> GetInvoiceAsync(int id);
        Task<IEnumerable<InvoiceListDto>> GetInvoicesAsync();
        Task<InvoiceDto> CreateInvoiceAsync(InvoiceCreateDto invoiceDto);
        Task UpdateInvoiceAsync(InvoiceUpdateDto invoiceDto);
        Task DeleteInvoiceAsync(int id);
    }

    public class InvoiceService : IInvoiceService
    {
        private readonly IInvoiceRepository _invoiceRepository;
        private readonly IMapper _mapper;

        public InvoiceService(IInvoiceRepository invoiceRepository, IMapper mapper)
        {
            _invoiceRepository = invoiceRepository;
            _mapper = mapper;
        }

        public async Task<InvoiceDto> GetInvoiceAsync(int id)
        {
            var invoice = await _invoiceRepository.GetInvoiceWithItemsAsync(id);
            return _mapper.Map<InvoiceDto>(invoice);
        }

        public async Task<IEnumerable<InvoiceListDto>> GetInvoicesAsync()
        {
            var invoices = await _invoiceRepository.GetAllAsync();
            return _mapper.Map<IEnumerable<InvoiceListDto>>(invoices);
        }

        public async Task<InvoiceDto> CreateInvoiceAsync(InvoiceCreateDto invoiceDto)
        {
            var invoice = _mapper.Map<Invoice>(invoiceDto);
            invoice.InvoiceNumber = await _invoiceRepository.GenerateInvoiceNumberAsync();
            invoice.Status = "Draft";
            invoice.CreatedDate = DateTime.UtcNow;
            
            foreach (var item in invoice.Items)
            {
                item.UnitPrice = item.Product.Price;
            }
            
            await _invoiceRepository.AddAsync(invoice);
            return _mapper.Map<InvoiceDto>(invoice);
        }

        public async Task UpdateInvoiceAsync(InvoiceUpdateDto invoiceDto)
        {
            var existingInvoice = await _invoiceRepository.GetInvoiceWithItemsAsync(invoiceDto.InvoiceId);
            if (existingInvoice == null)
            {
                throw new NotFoundException("Invoice not found");
            }
            
            _mapper.Map(invoiceDto, existingInvoice);
            existingInvoice.ModifiedDate = DateTime.UtcNow;
            
            await _invoiceRepository.UpdateAsync(existingInvoice);
        }

        public async Task DeleteInvoiceAsync(int id)
        {
            var invoice = await _invoiceRepository.GetByIdAsync(id);
            if (invoice == null)
            {
                throw new NotFoundException("Invoice not found");
            }
            
            await _invoiceRepository.DeleteAsync(invoice);
        }
    }
}

4. API Layer

DTOs/InvoiceDtos.cs

csharp
namespace InvoiceManagement.API.DTOs
{
    public class InvoiceDto
    {
        public int InvoiceId { get; set; }
        public string InvoiceNumber { get; set; }
        public DateTime Date { get; set; }
        public DateTime DueDate { get; set; }
        public decimal TotalAmount { get; set; }
        public decimal TaxAmount { get; set; }
        public decimal DiscountAmount { get; set; }
        public string Notes { get; set; }
        public string Status { get; set; }
        public CustomerDto Customer { get; set; }
        public List<InvoiceItemDto> Items { get; set; } = new List<InvoiceItemDto>();
    }

    public class InvoiceListDto
    {
        public int InvoiceId { get; set; }
        public string InvoiceNumber { get; set; }
        public DateTime Date { get; set; }
        public DateTime DueDate { get; set; }
        public decimal TotalAmount { get; set; }
        public string Status { get; set; }
        public string CustomerName { get; set; }
    }

    public class InvoiceCreateDto
    {
        public int CustomerId { get; set; }
        public DateTime Date { get; set; } = DateTime.UtcNow;
        public DateTime DueDate { get; set; } = DateTime.UtcNow.AddDays(30);
        public decimal DiscountAmount { get; set; }
        public string Notes { get; set; }
        public List<InvoiceItemCreateDto> Items { get; set; } = new List<InvoiceItemCreateDto>();
    }

    public class InvoiceUpdateDto
    {
        public int InvoiceId { get; set; }
        public int CustomerId { get; set; }
        public DateTime Date { get; set; }
        public DateTime DueDate { get; set; }
        public decimal DiscountAmount { get; set; }
        public string Notes { get; set; }
        public string Status { get; set; }
        public List<InvoiceItemUpdateDto> Items { get; set; } = new List<InvoiceItemUpdateDto>();
    }

    // Other DTOs for Customer, Product, InvoiceItem...
}

Controllers/InvoicesController.cs

csharp
namespace InvoiceManagement.API.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    [Authorize]
    public class InvoicesController : ControllerBase
    {
        private readonly IInvoiceService _invoiceService;
        private readonly IMapper _mapper;

        public InvoicesController(IInvoiceService invoiceService, IMapper mapper)
        {
            _invoiceService = invoiceService;
            _mapper = mapper;
        }

        [HttpGet]
        public async Task<ActionResult<IEnumerable<InvoiceListDto>>> GetInvoices()
        {
            var invoices = await _invoiceService.GetInvoicesAsync();
            return Ok(invoices);
        }

        [HttpGet("{id}")]
        public async Task<ActionResult<InvoiceDto>> GetInvoice(int id)
        {
            var invoice = await _invoiceService.GetInvoiceAsync(id);
            if (invoice == null)
            {
                return NotFound();
            }
            return Ok(invoice);
        }

        [HttpPost]
        public async Task<ActionResult<InvoiceDto>> CreateInvoice(InvoiceCreateDto invoiceDto)
        {
            var invoice = await _invoiceService.CreateInvoiceAsync(invoiceDto);
            return CreatedAtAction(nameof(GetInvoice), new { id = invoice.InvoiceId }, invoice);
        }

        [HttpPut("{id}")]
        public async Task<IActionResult> UpdateInvoice(int id, InvoiceUpdateDto invoiceDto)
        {
            if (id != invoiceDto.InvoiceId)
            {
                return BadRequest();
            }
            
            await _invoiceService.UpdateInvoiceAsync(invoiceDto);
            return NoContent();
        }

        [HttpDelete("{id}")]
        public async Task<IActionResult> DeleteInvoice(int id)
        {
            await _invoiceService.DeleteInvoiceAsync(id);
            return NoContent();
        }
    }
}

Stored Procedure Implementation

For the full CRUD in a single stored procedure approach:

sql
CREATE PROCEDURE [dbo].[usp_Invoice_CRUD]
    @Action VARCHAR(10), -- 'CREATE', 'READ', 'UPDATE', 'DELETE'
    @InvoiceId INT = NULL,
    @CustomerId INT = NULL,
    @InvoiceNumber NVARCHAR(20) = NULL,
    @Date DATETIME = NULL,
    @DueDate DATETIME = NULL,
    @DiscountAmount DECIMAL(18,2) = 0,
    @Notes NVARCHAR(500) = NULL,
    @Status NVARCHAR(20) = NULL,
    @ItemsJson NVARCHAR(MAX) = NULL, -- JSON array of items
    @UserId INT = NULL -- For audit purposes
AS
BEGIN
    SET NOCOUNT ON;
    
    BEGIN TRY
        BEGIN TRANSACTION;
        
        -- CREATE action
        IF @Action = 'CREATE'
        BEGIN
            -- Generate invoice number if not provided
            IF @InvoiceNumber IS NULL
            BEGIN
                SET @InvoiceNumber = 'INV-' + FORMAT(GETDATE(), 'yyyyMMdd') + '-' + 
                    RIGHT('0000' + CAST(ISNULL((SELECT MAX(CAST(RIGHT(InvoiceNumber, 4) AS INT)) 
                    FROM Invoices WHERE InvoiceNumber LIKE 'INV-' + FORMAT(GETDATE(), 'yyyyMMdd') + '%'), 0) + 1 AS VARCHAR(4)), 4);
            END
            
            -- Insert invoice header
            INSERT INTO Invoices (CustomerId, InvoiceNumber, Date, DueDate, DiscountAmount, Notes, Status, CreatedDate)
            VALUES (@CustomerId, @InvoiceNumber, @Date, @DueDate, @DiscountAmount, @Notes, ISNULL(@Status, 'Draft'), GETDATE());
            
            SET @InvoiceId = SCOPE_IDENTITY();
            
            -- Insert items if provided
            IF @ItemsJson IS NOT NULL
            BEGIN
                INSERT INTO InvoiceItems (InvoiceId, ProductId, Quantity, UnitPrice, Discount, TaxRate, Notes)
                SELECT 
                    @InvoiceId,
                    ProductId,
                    Quantity,
                    (SELECT Price FROM Products WHERE ProductId = json.ProductId) AS UnitPrice,
                    ISNULL(Discount, 0),
                    ISNULL(TaxRate, 0),
                    Notes
                FROM OPENJSON(@ItemsJson)
                WITH (
                    ProductId INT '$.productId',
                    Quantity INT '$.quantity',
                    Discount DECIMAL(18,2) '$.discount',
                    TaxRate DECIMAL(5,2) '$.taxRate',
                    Notes NVARCHAR(200) '$.notes'
                ) AS json;
                
                -- Calculate totals
                DECLARE @TotalAmount DECIMAL(18,2) = 0;
                DECLARE @TaxAmount DECIMAL(18,2) = 0;
                
                SELECT 
                    @TotalAmount = SUM((ii.UnitPrice * ii.Quantity) * (1 - ISNULL(ii.Discount, 0))),
                    @TaxAmount = SUM((ii.UnitPrice * ii.Quantity) * (1 - ISNULL(ii.Discount, 0))) * (ISNULL(ii.TaxRate, 0)/100))
                FROM InvoiceItems ii
                WHERE ii.InvoiceId = @InvoiceId;
                
                -- Update invoice with totals
                UPDATE Invoices
                SET 
                    TotalAmount = @TotalAmount,
                    TaxAmount = @TaxAmount,
                    ModifiedDate = GETDATE()
                WHERE InvoiceId = @InvoiceId;
            END
            
            -- Return the created invoice
            SELECT @InvoiceId AS InvoiceId, @InvoiceNumber AS InvoiceNumber;
        END
        
        -- READ action
        ELSE IF @Action = 'READ'
        BEGIN
            -- Return invoice header
            SELECT 
                i.InvoiceId, i.InvoiceNumber, i.Date, i.DueDate, 
                i.TotalAmount, i.TaxAmount, i.DiscountAmount, i.Notes, i.Status,
                c.CustomerId, c.Name AS CustomerName, c.Email, c.Phone, c.Address
            FROM Invoices i
            JOIN Customers c ON i.CustomerId = c.CustomerId
            WHERE i.InvoiceId = @InvoiceId;
            
            -- Return invoice items
            SELECT 
                ii.InvoiceItemId, ii.ProductId, p.Name AS ProductName, p.Description, 
                ii.Quantity, ii.UnitPrice, ii.Discount, ii.TaxRate, ii.Notes
            FROM InvoiceItems ii
            JOIN Products p ON ii.ProductId = p.ProductId
            WHERE ii.InvoiceId = @InvoiceId;
        END
        
        -- UPDATE action
        ELSE IF @Action = 'UPDATE'
        BEGIN
            -- Update invoice header
            UPDATE Invoices
            SET 
                CustomerId = @CustomerId,
                Date = @Date,
                DueDate = @DueDate,
                DiscountAmount = @DiscountAmount,
                Notes = @Notes,
                Status = @Status,
                ModifiedDate = GETDATE()
            WHERE InvoiceId = @InvoiceId;
            
            -- Handle items (delete all and reinsert for simplicity)
            DELETE FROM InvoiceItems WHERE InvoiceId = @InvoiceId;
            
            IF @ItemsJson IS NOT NULL
            BEGIN
                INSERT INTO InvoiceItems (InvoiceId, ProductId, Quantity, UnitPrice, Discount, TaxRate, Notes)
                SELECT 
                    @InvoiceId,
                    ProductId,
                    Quantity,
                    (SELECT Price FROM Products WHERE ProductId = json.ProductId) AS UnitPrice,
                    ISNULL(Discount, 0),
                    ISNULL(TaxRate, 0),
                    Notes
                FROM OPENJSON(@ItemsJson)
                WITH (
                    ProductId INT '$.productId',
                    Quantity INT '$.quantity',
                    Discount DECIMAL(18,2) '$.discount',
                    TaxRate DECIMAL(5,2) '$.taxRate',
                    Notes NVARCHAR(200) '$.notes'
                ) AS json;
                
                -- Recalculate totals
                DECLARE @NewTotalAmount DECIMAL(18,2) = 0;
                DECLARE @NewTaxAmount DECIMAL(18,2) = 0;
                
                SELECT 
                    @NewTotalAmount = SUM((ii.UnitPrice * ii.Quantity) * (1 - ISNULL(ii.Discount, 0))),
                    @NewTaxAmount = SUM((ii.UnitPrice * ii.Quantity) * (1 - ISNULL(ii.Discount, 0))) * (ISNULL(ii.TaxRate, 0)/100))
                FROM InvoiceItems ii
                WHERE ii.InvoiceId = @InvoiceId;
                
                -- Update invoice with new totals
                UPDATE Invoices
                SET 
                    TotalAmount = @NewTotalAmount,
                    TaxAmount = @NewTaxAmount,
                    ModifiedDate = GETDATE()
                WHERE InvoiceId = @InvoiceId;
            END
        END
        
        -- DELETE action
        ELSE IF @Action = 'DELETE'
        BEGIN
            -- First delete items
            DELETE FROM InvoiceItems WHERE InvoiceId = @InvoiceId;
            
            -- Then delete invoice
            DELETE FROM Invoices WHERE InvoiceId = @InvoiceId;
        END
        
        COMMIT TRANSACTION;
    END TRY
    BEGIN CATCH
        IF @@TRANCOUNT > 0
            ROLLBACK TRANSACTION;
            
        THROW;
    END CATCH
END

Dapper Integration with Stored Procedure

Update the InvoiceRepository to use the stored procedure:

csharp
public async Task<Invoice> GetInvoiceWithItemsAsync(int id)
{
    using var connection = _context.CreateConnection();
    
    var parameters = new DynamicParameters();
    parameters.Add("Action", "READ");
    parameters.Add("InvoiceId", id);
    
    using var multi = await connection.QueryMultipleAsync(
        "usp_Invoice_CRUD", 
        parameters, 
        commandType: CommandType.StoredProcedure);
    
    var invoice = await multi.ReadSingleOrDefaultAsync<Invoice>();
    if (invoice != null)
    {
        invoice.Items = (await multi.ReadAsync<InvoiceItem>()).ToList();
    }
    
    return invoice;
}

public async Task<int> CreateInvoiceAsync(Invoice invoice)
{
    using var connection = _context.CreateConnection();
    
    var parameters = new DynamicParameters();
    parameters.Add("Action", "CREATE");
    parameters.Add("CustomerId", invoice.CustomerId);
    parameters.Add("Date", invoice.Date);
    parameters.Add("DueDate", invoice.DueDate);
    parameters.Add("DiscountAmount", invoice.DiscountAmount);
    parameters.Add("Notes", invoice.Notes);
    parameters.Add("Status", invoice.Status);
    parameters.Add("ItemsJson", JsonConvert.SerializeObject(invoice.Items.Select(i => new {
        productId = i.ProductId,
        quantity = i.Quantity,
        discount = i.Discount,
        taxRate = i.TaxRate,
        notes = i.Notes
    })));
    
    var result = await connection.QueryFirstOrDefaultAsync<dynamic>(
        "usp_Invoice_CRUD", 
        parameters, 
        commandType: CommandType.StoredProcedure);
    
    return result?.InvoiceId ?? 0;
}

// Similar methods for Update and Delete

JWT Authentication

Configuration in Program.cs

csharp
// Add JWT Authentication
var jwtSettings = builder.Configuration.GetSection("JwtSettings");
builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ValidIssuer = jwtSettings["validIssuer"],
        ValidAudience = jwtSettings["validAudience"],
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings["secretKey"]))
    };
});

// Add Authorization
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("RequireAdminRole", policy => policy.RequireRole("Admin"));
    options.AddPolicy("RequireUserRole", policy => policy.RequireRole("User"));
});

AuthController.cs

csharp
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
    private readonly IAuthService _authService;
    
    public AuthController(IAuthService authService)
    {
        _authService = authService;
    }
    
    [HttpPost("login")]
    public async Task<IActionResult> Login(LoginDto loginDto)
    {
        var result = await _authService.LoginAsync(loginDto);
        if (!result.Success)
        {
            return Unauthorized(result.Message);
        }
        
        return Ok(new {
            token = result.Token,
            refreshToken = result.RefreshToken,
            expiresIn = result.ExpiresIn
        });
    }
    
    [HttpPost("refresh")]
    public async Task<IActionResult> RefreshToken(RefreshTokenDto refreshTokenDto)
    {
        var result = await _authService.RefreshTokenAsync(refreshTokenDto);
        if (!result.Success)
        {
            return Unauthorized(result.Message);
        }
        
        return Ok(new {
            token = result.Token,
            refreshToken = result.RefreshToken,
            expiresIn = result.ExpiresIn
        });
    }
    
    [HttpPost("register")]
    public async Task<IActionResult> Register(RegisterDto registerDto)
    {
        var result = await _authService.RegisterAsync(registerDto);
        if (!result.Success)
        {
            return BadRequest(result.Message);
        }
        
        return Ok(result);
    }
}

AuthService.cs

csharp
public class AuthService : IAuthService
{
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly IConfiguration _configuration;
    private readonly JwtSettings _jwtSettings;
    
    public AuthService(UserManager<ApplicationUser> userManager, IConfiguration configuration)
    {
        _userManager = userManager;
        _configuration = configuration;
        _jwtSettings = configuration.GetSection("JwtSettings").Get<JwtSettings>();
    }
    
    public async Task<AuthResult> LoginAsync(LoginDto loginDto)
    {
        var user = await _userManager.FindByEmailAsync(loginDto.Email);
        if (user == null)
        {
            return AuthResult.Failure("User not found");
        }
        
        if (!await _userManager.CheckPasswordAsync(user, loginDto.Password))
        {
            return AuthResult.Failure("Invalid credentials");
        }
        
        var token = GenerateJwtToken(user);
        var refreshToken = GenerateRefreshToken();
        
        user.RefreshToken = refreshToken;
        user.RefreshTokenExpiryTime = DateTime.UtcNow.AddDays(_jwtSettings.RefreshTokenExpiryInDays);
        await _userManager.UpdateAsync(user);
        
        return AuthResult.Success(token, refreshToken, _jwtSettings.TokenExpiryInMinutes * 60);
    }
    
    public async Task<AuthResult> RefreshTokenAsync(RefreshTokenDto refreshTokenDto)
    {
        var principal = GetPrincipalFromExpiredToken(refreshTokenDto.Token);
        var userId = principal.FindFirstValue(ClaimTypes.NameIdentifier);
        var user = await _userManager.FindByIdAsync(userId);
        
        if (user == null || user.RefreshToken != refreshTokenDto.RefreshToken || 
            user.RefreshTokenExpiryTime <= DateTime.UtcNow)
        {
            return AuthResult.Failure("Invalid refresh token");
        }
        
        var newToken = GenerateJwtToken(user);
        var newRefreshToken = GenerateRefreshToken();
        
        user.RefreshToken = newRefreshToken;
        user.RefreshTokenExpiryTime = DateTime.UtcNow.AddDays(_jwtSettings.RefreshTokenExpiryInDays);
        await _userManager.UpdateAsync(user);
        
        return AuthResult.Success(newToken, newRefreshToken, _jwtSettings.TokenExpiryInMinutes * 60);
    }
    
    private string GenerateJwtToken(ApplicationUser user)
    {
        var claims = new[]
        {
            new Claim(JwtRegisteredClaimNames.Sub, user.Id),
            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
            new Claim(JwtRegisteredClaimNames.Email, user.Email),
            new Claim(ClaimTypes.Name, user.UserName),
            new Claim(ClaimTypes.Role, "User") // Add actual roles from user
        };
        
        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.SecretKey));
        var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
        
        var token = new JwtSecurityToken(
            issuer: _jwtSettings.ValidIssuer,
            audience: _jwtSettings.ValidAudience,
            claims: claims,
            expires: DateTime.UtcNow.AddMinutes(_jwtSettings.TokenExpiryInMinutes),
            signingCredentials: creds);
            
        return new JwtSecurityTokenHandler().WriteToken(token);
    }
    
    private string GenerateRefreshToken()
    {
        var randomNumber = new byte[32];
        using var rng = RandomNumberGenerator.Create();
        rng.GetBytes(randomNumber);
        return Convert.ToBase64String(randomNumber);
    }
    
    private ClaimsPrincipal GetPrincipalFromExpiredToken(string token)
    {
        var tokenValidationParameters = new TokenValidationParameters
        {
            ValidateAudience = false,
            ValidateIssuer = false,
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.SecretKey)),
            ValidateLifetime = false
        };
        
        var tokenHandler = new JwtSecurityTokenHandler();
        var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out var securityToken);
        
        if (securityToken is not JwtSecurityToken jwtSecurityToken || 
            !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase))
        {
            throw new SecurityTokenException("Invalid token");
        }
        
        return principal;
    }
}

Error Handling

Global Exception Handling Middleware

csharp
public class ExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ExceptionMiddleware> _logger;
    
    public ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }
    
    public async Task InvokeAsync(HttpContext httpContext)
    {
        try
        {
            await _next(httpContext);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "An unhandled exception has occurred");
            await HandleExceptionAsync(httpContext, ex);
        }
    }
    
    private static Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        context.Response.ContentType = "application/json";
        
        var statusCode = exception switch
        {
            NotFoundException => StatusCodes.Status404NotFound,
            BadRequestException => StatusCodes.Status400BadRequest,
            UnauthorizedException => StatusCodes.Status401Unauthorized,
            _ => StatusCodes.Status500InternalServerError
        };
        
        context.Response.StatusCode = statusCode;
        
        return context.Response.WriteAsync(new ErrorDetails
        {
            StatusCode = context.Response.StatusCode,
            Message = exception.Message,
            StackTrace = context.Response.StatusCode == StatusCodes.Status500InternalServerError ? 
                exception.StackTrace : null
        }.ToString());
    }
}

public class ErrorDetails
{
    public int StatusCode { get; set; }
    public string Message { get; set; }
    public string StackTrace { get; set; }
    
    public override string ToString()
    {
        return JsonSerializer.Serialize(this);
    }
}

// Custom exceptions
public class NotFoundException : Exception
{
    public NotFoundException(string message) : base(message) { }
}

public class BadRequestException : Exception
{
    public BadRequestException(string message) : base(message) { }
}

public class UnauthorizedException : Exception
{
    public UnauthorizedException(string message) : base(message) { }
}

Register the middleware in Program.cs:

csharp
app.UseMiddleware<ExceptionMiddleware>();

Transaction Management

For complex operations that span multiple repositories or services:

csharp
public class UnitOfWork : IUnitOfWork
{
    private readonly InvoiceDbContext _context;
    private readonly IInvoiceRepository _invoiceRepository;
    private readonly ICustomerRepository _customerRepository;
    
    public UnitOfWork(InvoiceDbContext context, 
        IInvoiceRepository invoiceRepository, 
        ICustomerRepository customerRepository)
    {
        _context = context;
        _invoiceRepository = invoiceRepository;
        _customerRepository = customerRepository;
    }
    
    public IInvoiceRepository Invoices => _invoiceRepository;
    public ICustomerRepository Customers => _customerRepository;
    
    public async Task<bool> CommitAsync()
    {
        using var transaction = await _context.CreateConnection().BeginTransactionAsync();
        try
        {
            // If repositories have changes to save, they would do it here
            await transaction.CommitAsync();
            return true;
        }
        catch
        {
            await transaction.RollbackAsync();
            throw;
        }
    }
}

Example usage in a service:

csharp
public async Task<InvoiceDto> CreateInvoiceWithCustomerAsync(InvoiceCreateWithCustomerDto dto)
{
    using (var transaction = await _unitOfWork.BeginTransactionAsync())
    {
        try
        {
            // Create customer if not exists
            var customer = await _unitOfWork.Customers.GetByEmailAsync(dto.CustomerEmail);
            if (customer == null)
            {
                customer = new Customer
                {
                    Name = dto.CustomerName,
                    Email = dto.CustomerEmail,
                    Phone = dto.CustomerPhone,
                    Address = dto.CustomerAddress
                };
                await _unitOfWork.Customers.AddAsync(customer);
            }
            
            // Create invoice
            var invoice = _mapper.Map<Invoice>(dto.Invoice);
            invoice.CustomerId = customer.CustomerId;
            invoice.InvoiceNumber = await _unitOfWork.Invoices.GenerateInvoiceNumberAsync();
            
            await _unitOfWork.Invoices.AddAsync(invoice);
            await _unitOfWork.CommitAsync();
            
            return _mapper.Map<InvoiceDto>(invoice);
        }
        catch
        {
            await transaction.RollbackAsync();
            throw;
        }
    }
}

Angular Implementation

Project Structure

text
angular-app/
├── src/
│   ├── app/
│   │   ├── core/               # Core modules (auth, interceptors, guards)
│   │   ├── modules/            # Feature modules
│   │   │   ├── invoices/       # Invoice management
│   │   │   ├── customers/      # Customer management
│   │   │   └── products/       # Product management
│   │   ├── shared/             # Shared components, models, services
│   │   ├── app-routing.module.ts
│   │   └── app.component.ts
│   ├── assets/
│   └── environments/
└── angular.json

Core Services

auth.service.ts

typescript
@Injectable({
  providedIn: 'root'
})
export class AuthService {
  private readonly apiUrl = environment.apiUrl + '/auth';
  private currentUserSubject: BehaviorSubject<User>;
  public currentUser: Observable<User>;

  constructor(private http: HttpClient) {
    this.currentUserSubject = new BehaviorSubject<User>(
      JSON.parse(localStorage.getItem('currentUser'))
    );
    this.currentUser = this.currentUserSubject.asObservable();
  }

  public get currentUserValue(): User {
    return this.currentUserSubject.value;
  }

  login(email: string, password: string): Observable<AuthResponse> {
    return this.http.post<AuthResponse>(`${this.apiUrl}/login`, { email, password })
      .pipe(
        map(response => {
          if (response.token) {
            const user = this.decodeToken(response.token);
            user.token = response.token;
            user.refreshToken = response.refreshToken;
            localStorage.setItem('currentUser', JSON.stringify(user));
            this.currentUserSubject.next(user);
          }
          return response;
        })
      );
  }

  logout(): void {
    localStorage.removeItem('currentUser');
    this.currentUserSubject.next(null);
  }

  refreshToken(): Observable<AuthResponse> {
    const currentUser = this.currentUserValue;
    if (!currentUser || !currentUser.refreshToken) {
      return throwError('No refresh token available');
    }

    return this.http.post<AuthResponse>(`${this.apiUrl}/refresh`, {
      token: currentUser.token,
      refreshToken: currentUser.refreshToken
    }).pipe(
      map(response => {
        if (response.token) {
          const user = this.decodeToken(response.token);
          user.token = response.token;
          user.refreshToken = response.refreshToken;
          localStorage.setItem('currentUser', JSON.stringify(user));
          this.currentUserSubject.next(user);
        }
        return response;
      })
    );
  }

  private decodeToken(token: string): User {
    const decoded = jwtDecode(token);
    return {
      id: decoded.sub,
      email: decoded.email,
      name: decoded.name,
      roles: decoded.roles || [],
      token: token,
      refreshToken: null
    };
  }
}

http-interceptor.ts

typescript
@Injectable()
export class JwtInterceptor implements HttpInterceptor {
  private isRefreshing = false;
  private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);

  constructor(private authService: AuthService) {}

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // Add auth header with jwt if user is logged in
    const currentUser = this.authService.currentUserValue;
    if (currentUser && currentUser.token) {
      request = this.addToken(request, currentUser.token);
    }

    return next.handle(request).pipe(
      catchError(error => {
        if (error instanceof HttpErrorResponse && error.status === 401) {
          return this.handle401Error(request, next);
        } else {
          return throwError(error);
        }
      })
    );
  }

  private addToken(request: HttpRequest<any>, token: string) {
    return request.clone({
      setHeaders: {
        Authorization: `Bearer ${token}`
      }
    });
  }

  private handle401Error(request: HttpRequest<any>, next: HttpHandler) {
    if (!this.isRefreshing) {
      this.isRefreshing = true;
      this.refreshTokenSubject.next(null);

      return this.authService.refreshToken().pipe(
        switchMap((response: any) => {
          this.isRefreshing = false;
          this.refreshTokenSubject.next(response.token);
          return next.handle(this.addToken(request, response.token));
        }),
        catchError(err => {
          this.isRefreshing = false;
          this.authService.logout();
          return throwError(err);
        })
      );
    } else {
      return this.refreshTokenSubject.pipe(
        filter(token => token != null),
        take(1),
        switchMap(token => {
          return next.handle(this.addToken(request, token));
        })
      );
    }
  }
}

Invoice Module

invoice.service.ts

typescript
@Injectable({
  providedIn: 'root'
})
export class InvoiceService {
  private apiUrl = environment.apiUrl + '/invoices';

  constructor(private http: HttpClient) {}

  getInvoices(params?: any): Observable<InvoiceListDto[]> {
    return this.http.get<InvoiceListDto[]>(this.apiUrl, { params });
  }

  getInvoice(id: number): Observable<InvoiceDto> {
    return this.http.get<InvoiceDto>(`${this.apiUrl}/${id}`);
  }

  createInvoice(invoice: InvoiceCreateDto): Observable<InvoiceDto> {
    return this.http.post<InvoiceDto>(this.apiUrl, invoice);
  }

  updateInvoice(id: number, invoice: InvoiceUpdateDto): Observable<void> {
    return this.http.put<void>(`${this.apiUrl}/${id}`, invoice);
  }

  deleteInvoice(id: number): Observable<void> {
    return this.http.delete<void>(`${this.apiUrl}/${id}`);
  }

  generatePdf(id: number): Observable<Blob> {
    return this.http.get(`${this.apiUrl}/${id}/pdf`, {
      responseType: 'blob'
    });
  }
}

invoice-list.component.ts

typescript
@Component({
  selector: 'app-invoice-list',
  templateUrl: './invoice-list.component.html',
  styleUrls: ['./invoice-list.component.scss']
})
export class InvoiceListComponent implements OnInit, OnDestroy {
  invoices: InvoiceListDto[] = [];
  isLoading = false;
  searchForm: FormGroup;
  private destroy$ = new Subject<void>();

  constructor(
    private invoiceService: InvoiceService,
    private fb: FormBuilder,
    private router: Router,
    private dialog: MatDialog
  ) {}

  ngOnInit(): void {
    this.initForm();
    this.loadInvoices();
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }

  private initForm(): void {
    this.searchForm = this.fb.group({
      search: [''],
      status: [''],
      fromDate: [''],
      toDate: ['']
    });

    this.searchForm.valueChanges.pipe(
      debounceTime(500),
      distinctUntilChanged(),
      takeUntil(this.destroy$)
    ).subscribe(() => this.loadInvoices());
  }

  private loadInvoices(): void {
    this.isLoading = true;
    const params = this.buildSearchParams();
    
    this.invoiceService.getInvoices(params).pipe(
      finalize(() => this.isLoading = false),
      takeUntil(this.destroy$)
    ).subscribe(
      invoices => this.invoices = invoices,
      error => console.error('Failed to load invoices', error)
    );
  }

  private buildSearchParams(): any {
    const formValue = this.searchForm.value;
    const params: any = {};

    if (formValue.search) params.search = formValue.search;
    if (formValue.status) params.status = formValue.status;
    if (formValue.fromDate) params.fromDate = formValue.fromDate;
    if (formValue.toDate) params.toDate = formValue.toDate;

    return params;
  }

  onEdit(invoiceId: number): void {
    this.router.navigate(['/invoices', invoiceId, 'edit']);
  }

  onView(invoiceId: number): void {
    this.router.navigate(['/invoices', invoiceId]);
  }

  onDelete(invoiceId: number): void {
    const dialogRef = this.dialog.open(ConfirmDialogComponent, {
      data: {
        title: 'Delete Invoice',
        message: 'Are you sure you want to delete this invoice?'
      }
    });

    dialogRef.afterClosed().pipe(
      filter(result => result),
      switchMap(() => this.invoiceService.deleteInvoice(invoiceId)),
      takeUntil(this.destroy$)
    ).subscribe(
      () => {
        this.loadInvoices();
        // Show success message
      },
      error => console.error('Failed to delete invoice', error)
    );
  }

  onCreate(): void {
    this.router.navigate(['/invoices', 'create']);
  }

  onExportPdf(invoiceId: number): void {
    this.invoiceService.generatePdf(invoiceId).pipe(
      takeUntil(this.destroy$)
    ).subscribe(blob => {
      saveAs(blob, `invoice_${invoiceId}.pdf`);
    });
  }
}

invoice-form.component.ts

typescript
@Component({
  selector: 'app-invoice-form',
  templateUrl: './invoice-form.component.html',
  styleUrls: ['./invoice-form.component.scss']
})
export class InvoiceFormComponent implements OnInit, OnDestroy {
  invoiceForm: FormGroup;
  isEditMode = false;
  isLoading = false;
  invoiceId: number;
  customers: CustomerDto[] = [];
  products: ProductDto[] = [];
  private destroy$ = new Subject<void>();

  constructor(
    private fb: FormBuilder,
    private invoiceService: InvoiceService,
    private customerService: CustomerService,
    private productService: ProductService,
    private route: ActivatedRoute,
    private router: Router,
    private snackBar: MatSnackBar
  ) {}

  ngOnInit(): void {
    this.initForm();
    this.loadCustomers();
    this.loadProducts();
    
    this.route.params.pipe(
      takeUntil(this.destroy$)
    ).subscribe(params => {
      if (params['id']) {
        this.isEditMode = true;
        this.invoiceId = +params['id'];
        this.loadInvoice(this.invoiceId);
      }
    });
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }

  private initForm(): void {
    this.invoiceForm = this.fb.group({
      customerId: ['', Validators.required],
      date: [new Date(), Validators.required],
      dueDate: [this.getDefaultDueDate(), Validators.required],
      discountAmount: [0, [Validators.min(0)]],
      notes: [''],
      status: ['Draft', Validators.required],
      items: this.fb.array([], Validators.minLength(1))
    });
  }

  private getDefaultDueDate(): Date {
    const date = new Date();
    date.setDate(date.getDate() + 30);
    return date;
  }

  get items(): FormArray {
    return this.invoiceForm.get('items') as FormArray;
  }

  addItem(product?: ProductDto): void {
    this.items.push(this.fb.group({
      productId: [product?.productId || '', Validators.required],
      quantity: [1, [Validators.required, Validators.min(1)]],
      unitPrice: [{value: product?.price || 0, disabled: true}, Validators.required],
      discount: [0, [Validators.min(0), Validators.max(100)]],
      taxRate: [product?.taxRate || 0, [Validators.min(0), Validators.max(100)]],
      notes: ['']
    }));
  }

  removeItem(index: number): void {
    this.items.removeAt(index);
  }

  private loadCustomers(): void {
    this.customerService.getCustomers().pipe(
      takeUntil(this.destroy$)
    ).subscribe(
      customers => this.customers = customers,
      error => console.error('Failed to load customers', error)
    );
  }

  private loadProducts(): void {
    this.productService.getProducts().pipe(
      takeUntil(this.destroy$)
    ).subscribe(
      products => this.products = products,
      error => console.error('Failed to load products', error)
    );
  }

  private loadInvoice(id: number): void {
    this.isLoading = true;
    this.invoiceService.getInvoice(id).pipe(
      finalize(() => this.isLoading = false),
      takeUntil(this.destroy$)
    ).subscribe(
      invoice => this.populateForm(invoice),
      error => console.error('Failed to load invoice', error)
    );
  }

  private populateForm(invoice: InvoiceDto): void {
    this.invoiceForm.patchValue({
      customerId: invoice.customer.customerId,
      date: new Date(invoice.date),
      dueDate: new Date(invoice.dueDate),
      discountAmount: invoice.discountAmount,
      notes: invoice.notes,
      status: invoice.status
    });

    this.items.clear();
    invoice.items.forEach(item => {
      const product = this.products.find(p => p.productId === item.product.productId);
      this.addItem(product);
      const lastItem = this.items.at(this.items.length - 1) as FormGroup;
      lastItem.patchValue({
        productId: item.product.productId,
        quantity: item.quantity,
        unitPrice: item.unitPrice,
        discount: item.discount,
        taxRate: item.taxRate,
        notes: item.notes
      });
    });
  }

  onSubmit(): void {
    if (this.invoiceForm.invalid) {
      return;
    }

    this.isLoading = true;
    const formValue = this.invoiceForm.getRawValue();
    const invoiceData = {
      ...formValue,
      items: formValue.items.map(item => ({
        productId: item.productId,
        quantity: item.quantity,
        discount: item.discount,
        taxRate: item.taxRate,
        notes: item.notes
      }))
    };

    const operation = this.isEditMode 
      ? this.invoiceService.updateInvoice(this.invoiceId, invoiceData)
      : this.invoiceService.createInvoice(invoiceData);

    operation.pipe(
      finalize(() => this.isLoading = false),
      takeUntil(this.destroy$)
    ).subscribe(
      () => {
        this.snackBar.open(`Invoice ${this.isEditMode ? 'updated' : 'created'} successfully`, 'Close', {
          duration: 3000
        });
        this.router.navigate(['/invoices']);
      },
      error => {
        console.error('Failed to save invoice', error);
        this.snackBar.open('Failed to save invoice', 'Close', {
          duration: 3000,
          panelClass: ['error-snackbar']
        });
      }
    );
  }

  onProductSelected(index: number): void {
    const itemGroup = this.items.at(index) as FormGroup;
    const productId = itemGroup.get('productId').value;
    const product = this.products.find(p => p.productId === productId);
    
    if (product) {
      itemGroup.patchValue({
        unitPrice: product.price,
        taxRate: product.taxRate || 0
      });
    }
  }

  calculateItemTotal(index: number): number {
    const item = this.items.at(index).value;
    const subtotal = item.quantity * item.unitPrice;
    const discountAmount = subtotal * (item.discount / 100);
    const taxableAmount = subtotal - discountAmount;
    const taxAmount = taxableAmount * (item.taxRate / 100);
    return taxableAmount + taxAmount;
  }

  calculateInvoiceTotal(): number {
    const itemsTotal = this.items.controls
      .map((_, index) => this.calculateItemTotal(index))
      .reduce((sum, current) => sum + current, 0);
      
    return itemsTotal - (this.invoiceForm.value.discountAmount || 0);
  }
}

Deployment Considerations

Backend (ASP.NET Core)

  1. Dockerfile

dockerfile
FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443

FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
WORKDIR /src
COPY ["InvoiceManagement.API/InvoiceManagement.API.csproj", "InvoiceManagement.API/"]
COPY ["InvoiceManagement.Application/InvoiceManagement.Application.csproj", "InvoiceManagement.Application/"]
COPY ["InvoiceManagement.Domain/InvoiceManagement.Domain.csproj", "InvoiceManagement.Domain/"]
COPY ["InvoiceManagement.Infrastructure/InvoiceManagement.Infrastructure.csproj", "InvoiceManagement.Infrastructure/"]
RUN dotnet restore "InvoiceManagement.API/InvoiceManagement.API.csproj"
COPY . .
WORKDIR "/src/InvoiceManagement.API"
RUN dotnet build "InvoiceManagement.API.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "InvoiceManagement.API.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "InvoiceManagement.API.dll"]
  1. Database Migrations
    For stored procedure approach, you'll need to:

  • Create a SQL migration script folder in your project

  • Use Flyway or DbUp for database versioning

  • Include all your stored procedures in the migration scripts

Frontend (Angular)

  1. Dockerfile

dockerfile
# Stage 1: Build the Angular app
FROM node:16 as build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build --prod

# Stage 2: Serve the app with Nginx
FROM nginx:alpine
COPY --from=build /app/dist/angular-app /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
  1. nginx.conf

nginx
server {
    listen 80;
    server_name localhost;
    
    location / {
        root /usr/share/nginx/html;
        index index.html index.htm;
        try_files $uri $uri/ /index.html;
    }
    
    location /api/ {
        proxy_pass http://api:80/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
  1. docker-compose.yml

yaml
version: '3.8'

services:
  api:
    build:
      context: .
      dockerfile: InvoiceManagement.API/Dockerfile
    ports:
      - "5000:80"
    environment:
      - ASPNETCORE_ENVIRONMENT=Production
      - ConnectionStrings__DefaultConnection=Server=db;Database=InvoiceManagement;User=sa;Password=YourStrong@Passw0rd;
    depends_on:
      - db

  db:
    image: mcr.microsoft.com/mssql/server:2019-latest
    environment:
      - ACCEPT_EULA=Y
      - SA_PASSWORD=YourStrong@Passw0rd
      - MSSQL_PID=Express
    ports:
      - "1433:1433"
    volumes:
      - sql_data:/var/opt/mssql

  web:
    build:
      context: .
      dockerfile: AngularApp/Dockerfile
    ports:
      - "4200:80"
    depends_on:
      - api

volumes:
  sql_data:

Performance Optimization

  1. Database Level:

    • Index critical columns (InvoiceNumber, CustomerId, Date)

    • Use table-valued parameters for bulk operations

    • Implement proper transaction isolation levels

  2. API Level:

    • Implement caching for frequently accessed data

    • Use pagination for large datasets

    • Compress responses

  3. Angular Level:

    • Lazy load modules

    • Use OnPush change detection strategy

    • Implement virtual scrolling for large lists

Security Best Practices

  1. API Security:

    • Always validate input parameters

    • Use parameterized queries (Dapper handles this)

    • Implement rate limiting

    • Use HTTPS in production

    • Sanitize all outputs

  2. Angular Security:

    • Sanitize all user inputs

    • Use Content Security Policy (CSP)

    • Implement XSS protection

    • Use HttpOnly cookies for tokens

  3. Database Security:

    • Least privilege principle for DB users

    • Encrypt sensitive data

    • Regular backups

    • Audit sensitive operations

This comprehensive guide provides a solid foundation for building an Invoice Management System with ASP.NET Core and Angular using best practices for performance, security, and maintainability. The architecture is designed to be flexible enough to accommodate future requirements while maintaining a clean separation of concerns.

0 comments:

 
Toggle Footer