Enterprise ASP.NET Core Web API with Clean Architecture
I'll create a comprehensive solution for your requirements, focusing on scalability, large data handling, and clean architecture principles. Let's start with the project structure and then dive into implementation details.
Solution Architecture
Project Structure (Visual Studio 2022 Solution)
ECommerceSolution.sln ├───src │ ├───ECommerce.API (ASP.NET Core Web API - Presentation Layer) │ ├───ECommerce.Application (Application Layer - CQRS, MediatR) │ ├───ECommerce.Core (Domain Layer - Entities, Interfaces) │ ├───ECommerce.Infrastructure (Infrastructure Layer - Dapper, Repositories) │ ├───ECommerce.Database (Database Project - SQL Server Objects) ├───tests │ ├───ECommerce.UnitTests │ ├───ECommerce.IntegrationTests
1. Setting Up the Solution
1.1 Create Solution and Projects
First, let's create the solution and projects in Visual Studio 2022:
Create a new Blank Solution named "ECommerceSolution"
Add projects as shown above (Class Library projects for all except the API which is ASP.NET Core Web API)
Set up dependencies:
API depends on Application and Infrastructure
Application depends on Core
Infrastructure depends on Core
1.2 Configure Base Project Structure
ECommerce.Core (Domain Layer)
Core/ ├───Entities/ ├───Enums/ ├───Exceptions/ ├───Interfaces/ ├───Common/ └───Extensions/
ECommerce.Application (Application Layer)
Application/ ├───Features/ │ ├───Authentication/ │ │ ├───Commands/ │ │ ├───Queries/ │ │ └───Models/ │ ├───Invoices/ │ │ ├───Commands/ │ │ ├───Queries/ │ │ └───Models/ ├───Common/ │ ├───Behaviors/ │ ├───Interfaces/ │ └───Mappings/ └───Services/
ECommerce.Infrastructure (Infrastructure Layer)
Infrastructure/ ├───Data/ │ ├───Migrations/ │ ├───Repositories/ │ └───Dapper/ ├───Identity/ ├───Logging/ ├───Caching/ └───Services/
ECommerce.API (Presentation Layer)
API/ ├───Controllers/ ├───Middleware/ ├───Extensions/ ├───Filters/ └───wwwroot/
2. Database-First Approach with SQL Server
2.1 Database Project Setup
Create a SQL Server Database Project (ECommerce.Database) to manage all database objects:
Database/ ├───Tables/ ├───Stored Procedures/ │ ├───Authentication/ │ ├───Invoice/ │ └───Common/ ├───Functions/ ├───Views/ ├───Types/ └───Scripts/
2.2 Sample Database Schema
Let's create tables for User Management and Invoices:
-- Tables/Users.sql CREATE TABLE [dbo].[Users] ( [Id] UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(), [UserName] NVARCHAR(50) NOT NULL, [Email] NVARCHAR(100) NOT NULL, [PasswordHash] VARBINARY(MAX) NOT NULL, [PasswordSalt] VARBINARY(MAX) NOT NULL, [FirstName] NVARCHAR(50) NULL, [LastName] NVARCHAR(50) NULL, [IsActive] BIT NOT NULL DEFAULT 1, [CreatedDate] DATETIME2 NOT NULL DEFAULT GETUTCDATE(), [ModifiedDate] DATETIME2 NULL, CONSTRAINT [UK_Users_UserName] UNIQUE ([UserName]), CONSTRAINT [UK_Users_Email] UNIQUE ([Email]) ); -- Tables/Roles.sql CREATE TABLE [dbo].[Roles] ( [Id] UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(), [Name] NVARCHAR(50) NOT NULL, [Description] NVARCHAR(255) NULL, CONSTRAINT [UK_Roles_Name] UNIQUE ([Name]) ); -- Tables/UserRoles.sql CREATE TABLE [dbo].[UserRoles] ( [UserId] UNIQUEIDENTIFIER NOT NULL, [RoleId] UNIQUEIDENTIFIER NOT NULL, PRIMARY KEY ([UserId], [RoleId]), FOREIGN KEY ([UserId]) REFERENCES [dbo].[Users]([Id]), FOREIGN KEY ([RoleId]) REFERENCES [dbo].[Roles]([Id]) ); -- Tables/RefreshTokens.sql CREATE TABLE [dbo].[RefreshTokens] ( [Id] UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(), [UserId] UNIQUEIDENTIFIER NOT NULL, [Token] NVARCHAR(255) NOT NULL, [Expires] DATETIME2 NOT NULL, [Created] DATETIME2 NOT NULL DEFAULT GETUTCDATE(), [CreatedByIp] NVARCHAR(50) NULL, [Revoked] DATETIME2 NULL, [RevokedByIp] NVARCHAR(50) NULL, [ReplacedByToken] NVARCHAR(255) NULL, FOREIGN KEY ([UserId]) REFERENCES [dbo].[Users]([Id]) ); -- Tables/Invoices.sql CREATE TABLE [dbo].[Invoices] ( [Id] UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(), [InvoiceNumber] NVARCHAR(20) NOT NULL, [CustomerId] UNIQUEIDENTIFIER NOT NULL, [InvoiceDate] DATETIME2 NOT NULL, [DueDate] DATETIME2 NOT NULL, [TotalAmount] DECIMAL(18,2) NOT NULL, [Status] INT NOT NULL DEFAULT 0, [Notes] NVARCHAR(MAX) NULL, [CreatedBy] UNIQUEIDENTIFIER NOT NULL, [CreatedDate] DATETIME2 NOT NULL DEFAULT GETUTCDATE(), [ModifiedBy] UNIQUEIDENTIFIER NULL, [ModifiedDate] DATETIME2 NULL, CONSTRAINT [UK_Invoices_InvoiceNumber] UNIQUE ([InvoiceNumber]), FOREIGN KEY ([CreatedBy]) REFERENCES [dbo].[Users]([Id]), FOREIGN KEY ([ModifiedBy]) REFERENCES [dbo].[Users]([Id]) ); -- Tables/InvoiceItems.sql CREATE TABLE [dbo].[InvoiceItems] ( [Id] UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(), [InvoiceId] UNIQUEIDENTIFIER NOT NULL, [ItemName] NVARCHAR(100) NOT NULL, [Description] NVARCHAR(255) NULL, [Quantity] INT NOT NULL, [UnitPrice] DECIMAL(18,2) NOT NULL, [TaxRate] DECIMAL(5,2) NOT NULL DEFAULT 0, [Discount] DECIMAL(5,2) NOT NULL DEFAULT 0, [LineTotal] DECIMAL(18,2) NOT NULL, FOREIGN KEY ([InvoiceId]) REFERENCES [dbo].[Invoices]([Id]) );
2.3 Stored Procedures for User Management
-- Stored Procedures/Authentication/usp_UserLogin.sql CREATE PROCEDURE [dbo].[usp_UserLogin] @UserName NVARCHAR(50), @Password NVARCHAR(100) AS BEGIN SET NOCOUNT ON; DECLARE @UserId UNIQUEIDENTIFIER; DECLARE @PasswordHash VARBINARY(MAX); DECLARE @PasswordSalt VARBINARY(MAX); DECLARE @IsActive BIT; SELECT @UserId = Id, @PasswordHash = PasswordHash, @PasswordSalt = PasswordSalt, @IsActive = IsActive FROM [dbo].[Users] WHERE UserName = @UserName; IF @UserId IS NULL OR @IsActive = 0 BEGIN RETURN 0; -- User not found or inactive END -- Verify password (this would be done in application layer) -- For demo, we're just returning the user if found SELECT u.Id, u.UserName, u.Email, u.FirstName, u.LastName, STRING_AGG(r.Name, ',') AS Roles FROM [dbo].[Users] u LEFT JOIN [dbo].[UserRoles] ur ON u.Id = ur.UserId LEFT JOIN [dbo].[Roles] r ON ur.RoleId = r.Id WHERE u.Id = @UserId GROUP BY u.Id, u.UserName, u.Email, u.FirstName, u.LastName; RETURN 1; -- Success END
2.4 Stored Procedures for Invoice Operations
-- Stored Procedures/Invoice/usp_CreateInvoice.sql CREATE PROCEDURE [dbo].[usp_CreateInvoice] @InvoiceNumber NVARCHAR(20), @CustomerId UNIQUEIDENTIFIER, @InvoiceDate DATETIME2, @DueDate DATETIME2, @Notes NVARCHAR(MAX), @CreatedBy UNIQUEIDENTIFIER, @Items InvoiceItemsType READONLY AS BEGIN SET NOCOUNT ON; BEGIN TRY BEGIN TRANSACTION; DECLARE @InvoiceId UNIQUEIDENTIFIER = NEWID(); DECLARE @TotalAmount DECIMAL(18,2) = 0; -- Calculate total from items SELECT @TotalAmount = SUM((Quantity * UnitPrice) * (1 - Discount/100) * (1 + TaxRate/100)) FROM @Items; -- Insert invoice header INSERT INTO [dbo].[Invoices] ( Id, InvoiceNumber, CustomerId, InvoiceDate, DueDate, TotalAmount, Notes, CreatedBy ) VALUES ( @InvoiceId, @InvoiceNumber, @CustomerId, @InvoiceDate, @DueDate, @TotalAmount, @Notes, @CreatedBy ); -- Insert invoice items INSERT INTO [dbo].[InvoiceItems] ( Id, InvoiceId, ItemName, Description, Quantity, UnitPrice, TaxRate, Discount, LineTotal ) SELECT NEWID(), @InvoiceId, ItemName, Description, Quantity, UnitPrice, TaxRate, Discount, (Quantity * UnitPrice) * (1 - Discount/100) * (1 + TaxRate/100) AS LineTotal FROM @Items; COMMIT TRANSACTION; -- Return the created invoice EXEC [dbo].[usp_GetInvoiceById] @InvoiceId; END TRY BEGIN CATCH IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION; THROW; END CATCH END
3. Implementing Clean Architecture with ASP.NET Core
3.1 Core Layer (Domain)
Entities/User.cs
namespace ECommerce.Core.Entities { public class User : BaseEntity { public string UserName { get; set; } public string Email { get; set; } public byte[] PasswordHash { get; set; } public byte[] PasswordSalt { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public bool IsActive { get; set; } public DateTime CreatedDate { get; set; } public DateTime? ModifiedDate { get; set; } // Navigation properties public ICollection<UserRole> UserRoles { get; set; } public ICollection<RefreshToken> RefreshTokens { get; set; } } }
Interfaces/IRepository.cs
namespace ECommerce.Core.Interfaces { public interface IRepository<T> where T : BaseEntity { Task<T> GetByIdAsync(Guid id); Task<IReadOnlyList<T>> GetAllAsync(); Task<T> AddAsync(T entity); Task UpdateAsync(T entity); Task DeleteAsync(T entity); } }
3.2 Infrastructure Layer
Data/DapperContext.cs
namespace ECommerce.Infrastructure.Data { public class DapperContext : IDapperContext { private readonly IConfiguration _configuration; private readonly string _connectionString; public DapperContext(IConfiguration configuration) { _configuration = configuration; _connectionString = _configuration.GetConnectionString("DefaultConnection"); } public IDbConnection CreateConnection() => new SqlConnection(_connectionString); public async Task<T> WithConnection<T>(Func<IDbConnection, Task<T>> getData) { await using var connection = CreateConnection(); await connection.OpenAsync(); return await getData(connection); } public async Task WithConnection(Func<IDbConnection, Task> getData) { await using var connection = CreateConnection(); await connection.OpenAsync(); await getData(connection); } } }
Repositories/UserRepository.cs
namespace ECommerce.Infrastructure.Data.Repositories { public class UserRepository : IUserRepository { private readonly IDapperContext _context; private readonly ILogger<UserRepository> _logger; public UserRepository(IDapperContext context, ILogger<UserRepository> logger) { _context = context; _logger = logger; } public async Task<User> GetByIdAsync(Guid id) { const string sql = @"SELECT * FROM Users WHERE Id = @Id"; try { return await _context.WithConnection(async conn => { return await conn.QueryFirstOrDefaultAsync<User>(sql, new { Id = id }); }); } catch (Exception ex) { _logger.LogError(ex, "Error getting user by ID: {Id}", id); throw; } } public async Task<User> GetByUserNameAsync(string userName) { const string sql = @"SELECT * FROM Users WHERE UserName = @UserName"; try { return await _context.WithConnection(async conn => { return await conn.QueryFirstOrDefaultAsync<User>(sql, new { UserName = userName }); }); } catch (Exception ex) { _logger.LogError(ex, "Error getting user by username: {UserName}", userName); throw; } } } }
3.3 Application Layer
Features/Authentication/Commands/LoginCommand.cs
namespace ECommerce.Application.Features.Authentication.Commands { public class LoginCommand : IRequest<AuthenticationResponse> { public string UserName { get; set; } public string Password { get; set; } public string IpAddress { get; set; } } public class LoginCommandHandler : IRequestHandler<LoginCommand, AuthenticationResponse> { private readonly IUserRepository _userRepository; private readonly IJwtService _jwtService; private readonly IRefreshTokenService _refreshTokenService; private readonly IPasswordService _passwordService; public LoginCommandHandler( IUserRepository userRepository, IJwtService jwtService, IRefreshTokenService refreshTokenService, IPasswordService passwordService) { _userRepository = userRepository; _jwtService = jwtService; _refreshTokenService = refreshTokenService; _passwordService = passwordService; } public async Task<AuthenticationResponse> Handle(LoginCommand request, CancellationToken cancellationToken) { var user = await _userRepository.GetByUserNameAsync(request.UserName); if (user == null || !user.IsActive) throw new AuthenticationException("Invalid username or password"); if (!_passwordService.VerifyPasswordHash(request.Password, user.PasswordHash, user.PasswordSalt)) throw new AuthenticationException("Invalid username or password"); var jwtToken = _jwtService.GenerateJwtToken(user); var refreshToken = await _refreshTokenService.GenerateRefreshToken(user.Id, request.IpAddress); return new AuthenticationResponse { Id = user.Id, UserName = user.UserName, Email = user.Email, Token = jwtToken, RefreshToken = refreshToken.Token, Roles = await _userRepository.GetUserRolesAsync(user.Id) }; } } }
3.4 API Layer
Controllers/AuthenticationController.cs
namespace ECommerce.API.Controllers { [Route("api/[controller]")] [ApiController] public class AuthenticationController : ControllerBase { private readonly IMediator _mediator; public AuthenticationController(IMediator mediator) { _mediator = mediator; } [HttpPost("login")] public async Task<IActionResult> Login([FromBody] LoginRequest request) { var ipAddress = Request.HttpContext.Connection.RemoteIpAddress?.MapToIPv4().ToString(); var command = new LoginCommand { UserName = request.UserName, Password = request.Password, IpAddress = ipAddress }; var response = await _mediator.Send(command); setTokenCookie(response.RefreshToken); return Ok(response); } private void setTokenCookie(string token) { var cookieOptions = new CookieOptions { HttpOnly = true, Expires = DateTime.UtcNow.AddDays(7), Secure = true, SameSite = SameSiteMode.Strict }; Response.Cookies.Append("refreshToken", token, cookieOptions); } } }
4. Invoice Master-Detail Implementation
4.1 Database Table Type for Invoice Items
-- Types/InvoiceItemsType.sql CREATE TYPE [dbo].[InvoiceItemsType] AS TABLE ( [ItemName] NVARCHAR(100) NOT NULL, [Description] NVARCHAR(255) NULL, [Quantity] INT NOT NULL, [UnitPrice] DECIMAL(18,2) NOT NULL, [TaxRate] DECIMAL(5,2) NOT NULL, [Discount] DECIMAL(5,2) NOT NULL );
4.2 Application Layer - Invoice Commands
Features/Invoices/Commands/CreateInvoiceCommand.cs
namespace ECommerce.Application.Features.Invoices.Commands { public class CreateInvoiceCommand : IRequest<InvoiceDto> { public string InvoiceNumber { get; set; } public Guid CustomerId { get; set; } public DateTime InvoiceDate { get; set; } public DateTime DueDate { get; set; } public string Notes { get; set; } public List<InvoiceItemDto> Items { get; set; } public Guid CreatedBy { get; set; } } public class CreateInvoiceCommandHandler : IRequestHandler<CreateInvoiceCommand, InvoiceDto> { private readonly IInvoiceRepository _invoiceRepository; private readonly IMapper _mapper; private readonly ILogger<CreateInvoiceCommandHandler> _logger; public CreateInvoiceCommandHandler( IInvoiceRepository invoiceRepository, IMapper mapper, ILogger<CreateInvoiceCommandHandler> logger) { _invoiceRepository = invoiceRepository; _mapper = mapper; _logger = logger; } public async Task<InvoiceDto> Handle(CreateInvoiceCommand request, CancellationToken cancellationToken) { try { // Create DataTable for items var itemsTable = new DataTable(); itemsTable.Columns.Add("ItemName", typeof(string)); itemsTable.Columns.Add("Description", typeof(string)); itemsTable.Columns.Add("Quantity", typeof(int)); itemsTable.Columns.Add("UnitPrice", typeof(decimal)); itemsTable.Columns.Add("TaxRate", typeof(decimal)); itemsTable.Columns.Add("Discount", typeof(decimal)); foreach (var item in request.Items) { itemsTable.Rows.Add( item.ItemName, item.Description, item.Quantity, item.UnitPrice, item.TaxRate, item.Discount); } // Call stored procedure var invoice = await _invoiceRepository.CreateInvoiceAsync( request.InvoiceNumber, request.CustomerId, request.InvoiceDate, request.DueDate, request.Notes, request.CreatedBy, itemsTable); return _mapper.Map<InvoiceDto>(invoice); } catch (Exception ex) { _logger.LogError(ex, "Error creating invoice"); throw; } } } }
4.3 Infrastructure Layer - Invoice Repository
Repositories/InvoiceRepository.cs
namespace ECommerce.Infrastructure.Data.Repositories { public class InvoiceRepository : IInvoiceRepository { private readonly IDapperContext _context; private readonly ILogger<InvoiceRepository> _logger; public InvoiceRepository(IDapperContext context, ILogger<InvoiceRepository> logger) { _context = context; _logger = logger; } public async Task<Invoice> CreateInvoiceAsync( string invoiceNumber, Guid customerId, DateTime invoiceDate, DateTime dueDate, string notes, Guid createdBy, DataTable items) { const string sql = "EXEC [dbo].[usp_CreateInvoice] " + "@InvoiceNumber, @CustomerId, @InvoiceDate, @DueDate, " + "@Notes, @CreatedBy, @Items"; try { return await _context.WithConnection(async conn => { var parameters = new DynamicParameters(); parameters.Add("@InvoiceNumber", invoiceNumber); parameters.Add("@CustomerId", customerId); parameters.Add("@InvoiceDate", invoiceDate); parameters.Add("@DueDate", dueDate); parameters.Add("@Notes", notes); parameters.Add("@CreatedBy", createdBy); parameters.Add("@Items", items.AsTableValuedParameter("dbo.InvoiceItemsType")); return await conn.QueryFirstOrDefaultAsync<Invoice>(sql, parameters); }); } catch (Exception ex) { _logger.LogError(ex, "Error creating invoice"); throw; } } public async Task<Invoice> GetInvoiceByIdAsync(Guid id) { const string sql = "EXEC [dbo].[usp_GetInvoiceById] @Id"; try { return await _context.WithConnection(async conn => { var invoice = await conn.QueryFirstOrDefaultAsync<Invoice>(sql, new { Id = id }); if (invoice != null) { var items = await conn.QueryAsync<InvoiceItem>( "SELECT * FROM InvoiceItems WHERE InvoiceId = @Id", new { Id = id }); invoice.Items = items.ToList(); } return invoice; }); } catch (Exception ex) { _logger.LogError(ex, "Error getting invoice by ID: {Id}", id); throw; } } } }
4.4 API Controller for Invoices
Controllers/InvoicesController.cs
namespace ECommerce.API.Controllers { [Authorize] [Route("api/[controller]")] [ApiController] public class InvoicesController : ControllerBase { private readonly IMediator _mediator; public InvoicesController(IMediator mediator) { _mediator = mediator; } [HttpPost] public async Task<ActionResult<InvoiceDto>> Create([FromBody] CreateInvoiceRequest request) { var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); var command = new CreateInvoiceCommand { InvoiceNumber = request.InvoiceNumber, CustomerId = request.CustomerId, InvoiceDate = request.InvoiceDate, DueDate = request.DueDate, Notes = request.Notes, Items = request.Items, CreatedBy = Guid.Parse(userId) }; var result = await _mediator.Send(command); return CreatedAtAction(nameof(GetById), new { id = result.Id }, result); } [HttpGet("{id}")] public async Task<ActionResult<InvoiceDto>> GetById(Guid id) { var query = new GetInvoiceByIdQuery { Id = id }; var result = await _mediator.Send(query); if (result == null) return NotFound(); return Ok(result); } } }
5. Advanced Features Implementation
5.1 Global Error Handling Middleware
namespace ECommerce.API.Middleware { public class ErrorHandlingMiddleware { private readonly RequestDelegate _next; private readonly ILogger<ErrorHandlingMiddleware> _logger; public ErrorHandlingMiddleware(RequestDelegate next, ILogger<ErrorHandlingMiddleware> logger) { _next = next; _logger = logger; } public async Task Invoke(HttpContext context) { try { await _next(context); } catch (Exception ex) { await HandleExceptionAsync(context, ex); } } private Task HandleExceptionAsync(HttpContext context, Exception exception) { var code = HttpStatusCode.InternalServerError; var result = string.Empty; switch (exception) { case ValidationException validationException: code = HttpStatusCode.BadRequest; result = JsonSerializer.Serialize(validationException.Errors); break; case NotFoundException _: code = HttpStatusCode.NotFound; break; case AuthenticationException _: code = HttpStatusCode.Unauthorized; break; case ForbiddenException _: code = HttpStatusCode.Forbidden; break; default: _logger.LogError(exception, "An unhandled exception has occurred"); result = JsonSerializer.Serialize(new { error = "An unexpected error occurred" }); break; } context.Response.ContentType = "application/json"; context.Response.StatusCode = (int)code; if (string.IsNullOrEmpty(result)) { result = JsonSerializer.Serialize(new { error = exception.Message }); } return context.Response.WriteAsync(result); } } }
5.2 Logging Configuration
namespace ECommerce.API.Extensions { public static class LoggingExtensions { public static IServiceCollection AddCustomLogging(this IServiceCollection services, IConfiguration configuration) { services.AddLogging(loggingBuilder => { loggingBuilder.ClearProviders(); loggingBuilder.AddConfiguration(configuration.GetSection("Logging")); // Console logging for development loggingBuilder.AddConsole(); // Application Insights for production if (!string.IsNullOrEmpty(configuration["ApplicationInsights:InstrumentationKey"])) { loggingBuilder.AddApplicationInsights( configureTelemetryConfiguration: (config) => config.ConnectionString = configuration["ApplicationInsights:ConnectionString"], configureApplicationInsightsLoggerOptions: (options) => { }); } // File logging loggingBuilder.AddFile(configuration.GetSection("Logging:File")); }); return services; } } }
5.3 Performance Monitoring with MiniProfiler
namespace ECommerce.API.Extensions { public static class PerformanceExtensions { public static IServiceCollection AddPerformanceMonitoring(this IServiceCollection services) { services.AddMiniProfiler(options => { options.RouteBasePath = "/profiler"; options.ColorScheme = StackExchange.Profiling.ColorScheme.Auto; options.Storage = new SqlServerStorage(services.BuildServiceProvider() .GetRequiredService<IConfiguration>() .GetConnectionString("DefaultConnection")); options.TrackConnectionOpenClose = true; options.SqlFormatter = new StackExchange.Profiling.SqlFormatters.InlineFormatter(); }) .AddEntityFramework(); return services; } public static IApplicationBuilder UsePerformanceMonitoring(this IApplicationBuilder app) { app.UseMiniProfiler(); return app; } } }
6. JWT Authentication Implementation
6.1 JWT Service
namespace ECommerce.Infrastructure.Services { public class JwtService : IJwtService { private readonly IConfiguration _configuration; private readonly ILogger<JwtService> _logger; public JwtService(IConfiguration configuration, ILogger<JwtService> logger) { _configuration = configuration; _logger = logger; } public string GenerateJwtToken(User user) { try { var tokenHandler = new JwtSecurityTokenHandler(); var key = Encoding.ASCII.GetBytes(_configuration["Jwt:Secret"]); var claims = new List<Claim> { new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), new Claim(ClaimTypes.Name, user.UserName), new Claim(ClaimTypes.Email, user.Email) }; var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(claims), Expires = DateTime.UtcNow.AddMinutes(Convert.ToDouble(_configuration["Jwt:ExpiryMinutes"])), SigningCredentials = new SigningCredentials( new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) }; var token = tokenHandler.CreateToken(tokenDescriptor); return tokenHandler.WriteToken(token); } catch (Exception ex) { _logger.LogError(ex, "Error generating JWT token"); throw; } } public Guid? ValidateJwtToken(string token) { if (string.IsNullOrEmpty(token)) return null; var tokenHandler = new JwtSecurityTokenHandler(); var key = Encoding.ASCII.GetBytes(_configuration["Jwt:Secret"]); try { tokenHandler.ValidateToken(token, new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(key), ValidateIssuer = false, ValidateAudience = false, ClockSkew = TimeSpan.Zero }, out SecurityToken validatedToken); var jwtToken = (JwtSecurityToken)validatedToken; var userId = Guid.Parse(jwtToken.Claims.First(x => x.Type == ClaimTypes.NameIdentifier).Value); return userId; } catch { return null; } } } }
6.2 Refresh Token Service
namespace ECommerce.Infrastructure.Services { public class RefreshTokenService : IRefreshTokenService { private readonly IRefreshTokenRepository _refreshTokenRepository; private readonly IConfiguration _configuration; private readonly ILogger<RefreshTokenService> _logger; public RefreshTokenService( IRefreshTokenRepository refreshTokenRepository, IConfiguration configuration, ILogger<RefreshTokenService> logger) { _refreshTokenRepository = refreshTokenRepository; _configuration = configuration; _logger = logger; } public async Task<RefreshToken> GenerateRefreshToken(Guid userId, string ipAddress) { try { // Remove old refresh tokens await _refreshTokenRepository.RemoveOldRefreshTokensForUser(userId); // Generate new refresh token var refreshToken = new RefreshToken { UserId = userId, Token = generateRandomToken(), Expires = DateTime.UtcNow.AddDays(Convert.ToDouble(_configuration["Jwt:RefreshTokenExpiryDays"])), Created = DateTime.UtcNow, CreatedByIp = ipAddress }; await _refreshTokenRepository.AddAsync(refreshToken); return refreshToken; } catch (Exception ex) { _logger.LogError(ex, "Error generating refresh token"); throw; } } public async Task<RefreshToken> RotateRefreshToken(string token, string ipAddress) { try { var refreshToken = await _refreshTokenRepository.GetByTokenAsync(token); if (refreshToken == null || !refreshToken.IsActive) throw new AuthenticationException("Invalid token"); // Revoke current token refreshToken.Revoked = DateTime.UtcNow; refreshToken.RevokedByIp = ipAddress; // Generate new token var newRefreshToken = new RefreshToken { UserId = refreshToken.UserId, Token = generateRandomToken(), Expires = DateTime.UtcNow.AddDays(Convert.ToDouble(_configuration["Jwt:RefreshTokenExpiryDays"])), Created = DateTime.UtcNow, CreatedByIp = ipAddress, ReplacedByToken = refreshToken.Token }; await _refreshTokenRepository.UpdateAsync(refreshToken); await _refreshTokenRepository.AddAsync(newRefreshToken); return newRefreshToken; } catch (Exception ex) { _logger.LogError(ex, "Error rotating refresh token"); throw; } } private string generateRandomToken() { var randomNumber = new byte[32]; using var rng = RandomNumberGenerator.Create(); rng.GetBytes(randomNumber); return Convert.ToBase64String(randomNumber); } } }
7. Transaction Management
7.1 Transaction Behavior for MediatR
namespace ECommerce.Application.Common.Behaviors { public class TransactionBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse> { private readonly IDapperContext _context; private readonly ILogger<TransactionBehavior<TRequest, TResponse>> _logger; public TransactionBehavior( IDapperContext context, ILogger<TransactionBehavior<TRequest, TResponse>> logger) { _context = context; _logger = logger; } public async Task<TResponse> Handle( TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken) { if (request is not ITransactional) { return await next(); } await using var connection = _context.CreateConnection(); await connection.OpenAsync(cancellationToken); await using var transaction = await connection.BeginTransactionAsync(cancellationToken); try { _logger.LogInformation("Begin transaction for {RequestName}", typeof(TRequest).Name); var response = await next(); await transaction.CommitAsync(cancellationToken); _logger.LogInformation("Transaction committed for {RequestName}", typeof(TRequest).Name); return response; } catch (Exception ex) { _logger.LogError(ex, "Error handling transaction for {RequestName}", typeof(TRequest).Name); await transaction.RollbackAsync(cancellationToken); throw; } } } }
7.2 Using Transactions with Dapper
namespace ECommerce.Infrastructure.Data.Repositories { public class InvoiceRepository : IInvoiceRepository { // ... other methods public async Task<Invoice> CreateInvoiceWithTransactionAsync( string invoiceNumber, Guid customerId, DateTime invoiceDate, DateTime dueDate, string notes, Guid createdBy, DataTable items) { const string headerSql = @" INSERT INTO Invoices (...) VALUES (...); SELECT SCOPE_IDENTITY();"; const string itemsSql = @" INSERT INTO InvoiceItems (...) VALUES (...);"; try { return await _context.WithConnection(async conn => { await using var transaction = await conn.BeginTransactionAsync(); try { // Insert header var invoiceId = await conn.ExecuteScalarAsync<Guid>(headerSql, new { InvoiceNumber = invoiceNumber, CustomerId = customerId, // ... other parameters }, transaction: transaction); // Insert items foreach (DataRow row in items.Rows) { await conn.ExecuteAsync(itemsSql, new { InvoiceId = invoiceId, ItemName = row["ItemName"], // ... other parameters }, transaction: transaction); } await transaction.CommitAsync(); return await GetInvoiceByIdAsync(invoiceId); } catch { await transaction.RollbackAsync(); throw; } }); } catch (Exception ex) { _logger.LogError(ex, "Error creating invoice with transaction"); throw; } } } }
8. Large Data Handling
8.1 Pagination with Dapper
namespace ECommerce.Infrastructure.Data.Repositories { public class InvoiceRepository : IInvoiceRepository { // ... other methods public async Task<PagedList<Invoice>> GetInvoicesPagedAsync( int pageNumber, int pageSize, string searchTerm = null, string sortColumn = null, string sortOrder = "asc") { const string baseSql = @" SELECT * FROM Invoices WHERE (@SearchTerm IS NULL OR InvoiceNumber LIKE '%' + @SearchTerm + '%' OR Notes LIKE '%' + @SearchTerm + '%')"; const string countSql = @" SELECT COUNT(*) FROM Invoices WHERE (@SearchTerm IS NULL OR InvoiceNumber LIKE '%' + @SearchTerm + '%' OR Notes LIKE '%' + @SearchTerm + '%')"; var orderBy = !string.IsNullOrWhiteSpace(sortColumn) ? $"ORDER BY {sortColumn} {(sortOrder == "desc" ? "DESC" : "ASC")}" : "ORDER BY InvoiceDate DESC"; var sql = $@" {baseSql} {orderBy} OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY"; try { return await _context.WithConnection(async conn => { var offset = (pageNumber - 1) * pageSize; var parameters = new DynamicParameters(); parameters.Add("@SearchTerm", searchTerm); parameters.Add("@Offset", offset); parameters.Add("@PageSize", pageSize); var count = await conn.ExecuteScalarAsync<int>(countSql, parameters); var items = await conn.QueryAsync<Invoice>(sql, parameters); return new PagedList<Invoice>( items.ToList(), count, pageNumber, pageSize); }); } catch (Exception ex) { _logger.LogError(ex, "Error getting paged invoices"); throw; } } } }
8.2 Bulk Operations with Table-Valued Parameters
namespace ECommerce.Infrastructure.Data.Repositories { public class BulkOperationsRepository : IBulkOperationsRepository { private readonly IDapperContext _context; private readonly ILogger<BulkOperationsRepository> _logger; public BulkOperationsRepository( IDapperContext context, ILogger<BulkOperationsRepository> logger) { _context = context; _logger = logger; } public async Task BulkInsertInvoicesAsync(IEnumerable<Invoice> invoices) { const string sql = "EXEC [dbo].[usp_BulkInsertInvoices] @Invoices"; try { var invoiceTable = new DataTable(); invoiceTable.Columns.Add("InvoiceNumber", typeof(string)); invoiceTable.Columns.Add("CustomerId", typeof(Guid)); // ... other columns foreach (var invoice in invoices) { invoiceTable.Rows.Add( invoice.InvoiceNumber, invoice.CustomerId // ... other values ); } await _context.WithConnection(async conn => { var parameters = new DynamicParameters(); parameters.Add("@Invoices", invoiceTable.AsTableValuedParameter("dbo.InvoiceBulkType")); await conn.ExecuteAsync(sql, parameters); }); } catch (Exception ex) { _logger.LogError(ex, "Error in bulk insert of invoices"); throw; } } } }
9. Caching Strategy
9.1 Distributed Cache Implementation
namespace ECommerce.Infrastructure.Caching { public class DistributedCacheService : ICacheService { private readonly IDistributedCache _cache; private readonly ILogger<DistributedCacheService> _logger; private readonly DistributedCacheEntryOptions _defaultOptions; public DistributedCacheService( IDistributedCache cache, ILogger<DistributedCacheService> logger, IConfiguration configuration) { _cache = cache; _logger = logger; _defaultOptions = new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes( configuration.GetValue<int>("Cache:DefaultExpirationMinutes")) }; } public async Task<T> GetAsync<T>(string key, CancellationToken cancellationToken = default) { try { var cachedData = await _cache.GetStringAsync(key, cancellationToken); if (string.IsNullOrEmpty(cachedData)) return default; return JsonSerializer.Deserialize<T>(cachedData); } catch (Exception ex) { _logger.LogError(ex, "Error getting cached data for key {Key}", key); return default; } } public async Task SetAsync<T>( string key, T value, DistributedCacheEntryOptions options = null, CancellationToken cancellationToken = default) { try { var serializedValue = JsonSerializer.Serialize(value); await _cache.SetStringAsync( key, serializedValue, options ?? _defaultOptions, cancellationToken); } catch (Exception ex) { _logger.LogError(ex, "Error setting cached data for key {Key}", key); } } public async Task RemoveAsync(string key, CancellationToken cancellationToken = default) { try { await _cache.RemoveAsync(key, cancellationToken); } catch (Exception ex) { _logger.LogError(ex, "Error removing cached data for key {Key}", key); } } } }
9.2 Cache Invalidation with MediatR Notifications
namespace ECommerce.Application.Common.Behaviors { public class CacheInvalidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse> { private readonly ICacheService _cache; private readonly ILogger<CacheInvalidationBehavior<TRequest, TResponse>> _logger; public CacheInvalidationBehavior( ICacheService cache, ILogger<CacheInvalidationBehavior<TRequest, TResponse>> logger) { _cache = cache; _logger = logger; } public async Task<TResponse> Handle( TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken) { var response = await next(); if (request is ICacheInvalidator invalidator) { try { foreach (var key in invalidator.CacheKeys) { await _cache.RemoveAsync(key, cancellationToken); _logger.LogDebug("Cache invalidated for key {Key}", key); } } catch (Exception ex) { _logger.LogError(ex, "Error invalidating cache for request {Request}", typeof(TRequest).Name); } } return response; } } }
10. Swagger Configuration with JWT Support
namespace ECommerce.API.Extensions { public static class SwaggerExtensions { public static IServiceCollection AddCustomSwagger(this IServiceCollection services, IConfiguration configuration) { services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "ECommerce API", Version = "v1", Description = "ECommerce API with JWT Authentication", Contact = new OpenApiContact { Name = "Your Name", Email = "your.email@example.com" } }); // Add JWT Authentication support in Swagger var securityScheme = new OpenApiSecurityScheme { Name = "JWT Authentication", Description = "Enter JWT Bearer token **_only_**", In = ParameterLocation.Header, Type = SecuritySchemeType.Http, Scheme = "bearer", BearerFormat = "JWT", Reference = new OpenApiReference { Id = JwtBearerDefaults.AuthenticationScheme, Type = ReferenceType.SecurityScheme } }; c.AddSecurityDefinition(securityScheme.Reference.Id, securityScheme); c.AddSecurityRequirement(new OpenApiSecurityRequirement { { securityScheme, Array.Empty<string>() } }); // Enable annotations c.EnableAnnotations(); // Include XML comments var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); if (File.Exists(xmlPath)) { c.IncludeXmlComments(xmlPath); } }); return services; } public static IApplicationBuilder UseCustomSwagger(this IApplicationBuilder app) { app.UseSwagger(); app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "ECommerce API V1"); c.RoutePrefix = "api-docs"; c.DocumentTitle = "ECommerce API Documentation"; c.DisplayRequestDuration(); c.EnableDeepLinking(); c.DefaultModelsExpandDepth(-1); // Hide schemas by default }); return app; } } }
11. Complete Startup Configuration
namespace ECommerce.API { public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) { // Base services services.AddControllers() .AddJsonOptions(options => { options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase; options.JsonSerializerOptions.WriteIndented = true; }); // Database services.AddDatabase(Configuration); // Authentication services.AddAuthentication(Configuration); // Application services services.AddApplicationServices(); // Infrastructure services services.AddInfrastructureServices(Configuration); // Swagger services.AddCustomSwagger(Configuration); // Caching services.AddDistributedMemoryCache(); // Or Redis for production services.AddSingleton<ICacheService, DistributedCacheService>(); // Performance monitoring services.AddPerformanceMonitoring(); // Logging services.AddCustomLogging(Configuration); // Health checks services.AddHealthChecks() .AddSqlServer(Configuration.GetConnectionString("DefaultConnection")) .AddDbContextCheck<ApplicationDbContext>(); // Cross-Origin Resource Sharing (CORS) services.AddCors(options => { options.AddPolicy("AllowAll", builder => { builder.AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader(); }); }); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UsePerformanceMonitoring(); } else { app.UseExceptionHandler("/error"); app.UseHsts(); } app.UseMiddleware<ErrorHandlingMiddleware>(); app.UseHttpsRedirection(); app.UseRouting(); app.UseCors("AllowAll"); app.UseAuthentication(); app.UseAuthorization(); app.UseCustomSwagger(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); endpoints.MapHealthChecks("/health"); }); } } }
12. Deployment Considerations
12.1 Docker Configuration
# Dockerfile FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base WORKDIR /app EXPOSE 80 EXPOSE 443 FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build WORKDIR /src COPY ["ECommerce.API/ECommerce.API.csproj", "ECommerce.API/"] COPY ["ECommerce.Application/ECommerce.Application.csproj", "ECommerce.Application/"] COPY ["ECommerce.Core/ECommerce.Core.csproj", "ECommerce.Core/"] COPY ["ECommerce.Infrastructure/ECommerce.Infrastructure.csproj", "ECommerce.Infrastructure/"] RUN dotnet restore "ECommerce.API/ECommerce.API.csproj" COPY . . WORKDIR "/src/ECommerce.API" RUN dotnet build "ECommerce.API.csproj" -c Release -o /app/build FROM build AS publish RUN dotnet publish "ECommerce.API.csproj" -c Release -o /app/publish FROM base AS final WORKDIR /app COPY --from=publish /app/publish . ENTRYPOINT ["dotnet", "ECommerce.API.dll"]
12.2 Kubernetes Deployment
# deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: ecommerce-api spec: replicas: 3 selector: matchLabels: app: ecommerce-api template: metadata: labels: app: ecommerce-api spec: containers: - name: ecommerce-api image: yourregistry/ecommerce-api:latest ports: - containerPort: 80 envFrom: - configMapRef: name: ecommerce-config resources: requests: memory: "256Mi" cpu: "100m" limits: memory: "512Mi" cpu: "500m" readinessProbe: httpGet: path: /health port: 80 initialDelaySeconds: 10 periodSeconds: 5 livenessProbe: httpGet: path: /health port: 80 initialDelaySeconds: 30 periodSeconds: 10 --- # service.yaml apiVersion: v1 kind: Service metadata: name: ecommerce-api spec: selector: app: ecommerce-api ports: - protocol: TCP port: 80 targetPort: 80 type: LoadBalancer
13. Testing Strategy
13.1 Unit Tests Example
namespace ECommerce.UnitTests.Application.Features.Authentication.Commands { public class LoginCommandHandlerTests { private readonly Mock<IUserRepository> _userRepositoryMock; private readonly Mock<IJwtService> _jwtServiceMock; private readonly Mock<IRefreshTokenService> _refreshTokenServiceMock; private readonly Mock<IPasswordService> _passwordServiceMock; private readonly LoginCommandHandler _handler; public LoginCommandHandlerTests() { _userRepositoryMock = new Mock<IUserRepository>(); _jwtServiceMock = new Mock<IJwtService>(); _refreshTokenServiceMock = new Mock<IRefreshTokenService>(); _passwordServiceMock = new Mock<IPasswordService>(); _handler = new LoginCommandHandler( _userRepositoryMock.Object, _jwtServiceMock.Object, _refreshTokenServiceMock.Object, _passwordServiceMock.Object); } [Fact] public async Task Handle_WithValidCredentials_ReturnsAuthenticationResponse() { // Arrange var userId = Guid.NewGuid(); var password = "Test@123"; var passwordHash = new byte[] { 1, 2, 3 }; var passwordSalt = new byte[] { 4, 5, 6 }; var user = new User { Id = userId, UserName = "testuser", Email = "test@example.com", PasswordHash = passwordHash, PasswordSalt = passwordSalt, IsActive = true }; var command = new LoginCommand { UserName = "testuser", Password = password, IpAddress = "127.0.0.1" }; _userRepositoryMock.Setup(x => x.GetByUserNameAsync(command.UserName)) .ReturnsAsync(user); _passwordServiceMock.Setup(x => x.VerifyPasswordHash( command.Password, passwordHash, passwordSalt)) .Returns(true); _jwtServiceMock.Setup(x => x.GenerateJwtToken(user)) .Returns("jwt_token"); _refreshTokenServiceMock.Setup(x => x.GenerateRefreshToken( user.Id, command.IpAddress)) .ReturnsAsync(new RefreshToken { Token = "refresh_token" }); _userRepositoryMock.Setup(x => x.GetUserRolesAsync(user.Id)) .ReturnsAsync(new List<string> { "User" }); // Act var result = await _handler.Handle(command, CancellationToken.None); // Assert result.Should().NotBeNull(); result.Token.Should().Be("jwt_token"); result.RefreshToken.Should().Be("refresh_token"); result.Roles.Should().Contain("User"); } } }
13.2 Integration Tests Example
namespace ECommerce.IntegrationTests.Controllers { public class AuthenticationControllerTests : IClassFixture<CustomWebApplicationFactory<Startup>> { private readonly CustomWebApplicationFactory<Startup> _factory; private readonly HttpClient _client; public AuthenticationControllerTests(CustomWebApplicationFactory<Startup> factory) { _factory = factory; _client = _factory.CreateClient(); } [Fact] public async Task Login_WithValidCredentials_ReturnsToken() { // Arrange var request = new LoginRequest { UserName = "admin", Password = "Admin@123" }; var content = new StringContent( JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"); // Act var response = await _client.PostAsync("/api/authentication/login", content); // Assert response.EnsureSuccessStatusCode(); var responseString = await response.Content.ReadAsStringAsync(); var result = JsonSerializer.Deserialize<AuthenticationResponse>( responseString, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); result.Should().NotBeNull(); result.Token.Should().NotBeNullOrEmpty(); result.RefreshToken.Should().NotBeNullOrEmpty(); } } }
14. Monitoring and Observability
14.1 Application Insights Configuration
namespace ECommerce.API.Extensions { public static class MonitoringExtensions { public static IServiceCollection AddApplicationMonitoring(this IServiceCollection services, IConfiguration configuration) { if (!string.IsNullOrEmpty(configuration["ApplicationInsights:ConnectionString"])) { services.AddApplicationInsightsTelemetry(options => { options.ConnectionString = configuration["ApplicationInsights:ConnectionString"]; options.EnableAdaptiveSampling = false; }); services.AddApplicationInsightsKubernetesEnricher(); services.AddSingleton<ITelemetryInitializer, CloudRoleNameTelemetryInitializer>(); } return services; } } public class CloudRoleNameTelemetryInitializer : ITelemetryInitializer { public void Initialize(ITelemetry telemetry) { telemetry.Context.Cloud.RoleName = "ECommerce.API"; } } }
14.2 Health Checks with UI
namespace ECommerce.API.Extensions { public static class HealthCheckExtensions { public static IServiceCollection AddCustomHealthChecks(this IServiceCollection services, IConfiguration configuration) { services.AddHealthChecks() .AddSqlServer( configuration.GetConnectionString("DefaultConnection"), name: "SQL Server", tags: new[] { "database", "sql" }) .AddRedis( configuration.GetConnectionString("Redis"), name: "Redis", tags: new[] { "cache", "redis" }) .AddApplicationInsightsPublisher(); services.AddHealthChecksUI(setup => { setup.AddHealthCheckEndpoint("API", "/health"); setup.SetEvaluationTimeInSeconds(60); setup.SetMinimumSecondsBetweenFailureNotifications(300); }) .AddInMemoryStorage(); return services; } public static IApplicationBuilder UseCustomHealthChecks(this IApplicationBuilder app) { app.UseHealthChecks("/health", new HealthCheckOptions { Predicate = _ => true, ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse }); app.UseHealthChecksUI(options => { options.UIPath = "/health-ui"; options.ApiPath = "/health-api"; }); return app; } } }
15. Security Best Practices
15.1 Security Headers Middleware
namespace ECommerce.API.Middleware { public class SecurityHeadersMiddleware { private readonly RequestDelegate _next; public SecurityHeadersMiddleware(RequestDelegate next) { _next = next; } public async Task Invoke(HttpContext context) { // Add security headers context.Response.Headers.Add("X-Frame-Options", "DENY"); context.Response.Headers.Add("X-Content-Type-Options", "nosniff"); context.Response.Headers.Add("X-XSS-Protection", "1; mode=block"); context.Response.Headers.Add("Referrer-Policy", "no-referrer"); context.Response.Headers.Add("Content-Security-Policy", "default-src 'self'; " + "script-src 'self' 'unsafe-inline' 'unsafe-eval'; " + "style-src 'self' 'unsafe-inline'; " + "img-src 'self' data:; " + "font-src 'self'; " + "connect-src 'self'; " + "media-src 'self'; " + "object-src 'none'; " + "frame-src 'none'; " + "base-uri 'self'; " + "form-action 'self'; " + "frame-ancestors 'none';"); await _next(context); } } }
15.2 Rate Limiting
namespace ECommerce.API.Extensions { public static class RateLimitingExtensions { public static IServiceCollection AddCustomRateLimiting(this IServiceCollection services) { services.AddRateLimiter(options => { options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context => { return RateLimitPartition.GetFixedWindowLimiter( partitionKey: context.Request.Headers.Host.ToString(), factory: partition => new FixedWindowRateLimiterOptions { AutoReplenishment = true, PermitLimit = 100, QueueLimit = 10, Window = TimeSpan.FromMinutes(1) }); }); options.OnRejected = async (context, token) => { context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests; await context.HttpContext.Response.WriteAsync( "Too many requests. Please try again later.", cancellationToken: token); }; }); return services; } } }
Conclusion
This comprehensive architecture provides a solid foundation for building scalable ASP.NET Core Web APIs with:
Clean Architecture separation of concerns
Database-first approach with SQL Server stored procedures
Dapper for efficient data access
CQRS pattern with MediatR
JWT authentication with refresh tokens
Transaction management for data integrity
Large data handling techniques
Comprehensive logging and monitoring
Robust error handling
API documentation with Swagger
Security best practices
Testing strategies
The implementation demonstrates key scenarios like user authentication and master-detail invoice operations while following enterprise-grade patterns and practices.
To complete your solution, you would:
Implement the remaining repositories and services
Add more domain entities and features
Configure deployment pipelines
Set up monitoring dashboards
Implement frontend integration (Angular/React/Flutter)
This architecture is designed to handle large-scale applications with complex business requirements while maintaining performance, scalability, and maintainability.
0 comments:
Post a Comment