![src]()
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 an 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 = "[email protected]"
}
});
// 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 = "[email protected]",
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.