![FluentValidation]()
Previous article: Master Repository & Unit of Work Patterns in ASP.NET Core (Part-37 of 40)
Table of Contents
The Foundation of Clean Data in Web Applications
FluentValidation Deep Dive
Advanced Validation Scenarios
AutoMapper Fundamentals
Advanced Mapping Techniques
Integration Patterns & Best Practices
Real-World Enterprise Application
Performance Optimization & Testing
Error Handling & Localization
Production Deployment & Monitoring
1. The Foundation of Clean Data in Web Applications
Why Data Validation and Mapping Matter
In modern web applications, data integrity is not just a feature—it's a fundamental requirement. Poor data validation can lead to:
Security Vulnerabilities
User Experience Issues
Business Impact
Real-World Data Disaster Stories
Case Study: Financial Institution
A banking application accepted negative transaction amounts due to missing validation, allowing attackers to create money through transaction reversals. Loss: $2.3 million.
Case Study: E-commerce Platform
Inadequate address validation caused 15% of international orders to be undeliverable, resulting in $500,000 annual losses in shipping and customer service costs.
The Validation & Mapping Ecosystem
// Traditional approach vs Modern approach
public class TraditionalValidation
{
// Problem: Validation logic scattered everywhere
public bool ValidateUser(User user)
{
if (string.IsNullOrEmpty(user.Name))
return false;
if (user.Age < 0 || user.Age > 150)
return false;
if (!Regex.IsMatch(user.Email, @"^[^@\s]+@[^@\s]+\.[^@\s]+$"))
return false;
// More scattered validation...
return true;
}
}
// Modern approach with FluentValidation
public class UserValidator : AbstractValidator<User>
{
public UserValidator()
{
RuleFor(x => x.Name).NotEmpty().Length(2, 100);
RuleFor(x => x.Age).InclusiveBetween(0, 150);
RuleFor(x => x.Email).EmailAddress();
// Clean, centralized validation rules
}
}
2. FluentValidation Deep Dive
Comprehensive Setup and Configuration
// Program.cs - FluentValidation Configuration
using FluentValidation;
using FluentValidation.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
// Add FluentValidation
builder.Services.AddFluentValidationAutoValidation()
.AddFluentValidationClientsideAdapters();
// Register validators
builder.Services.AddValidatorsFromAssemblyContaining<Program>();
// Alternative: Manual registration for better control
builder.Services.AddScoped<IValidator<UserCreateRequest>, UserCreateRequestValidator>();
builder.Services.AddScoped<IValidator<OrderCreateRequest>, OrderCreateRequestValidator>();
builder.Services.AddScoped<IValidator<ProductCreateRequest>, ProductCreateRequestValidator>();
// Configure validation behavior
builder.Services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressModelStateInvalidFilter = false; // Let FluentValidation handle model state
options.InvalidModelStateResponseFactory = context =>
{
var problems = new CustomProblemDetails
{
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1",
Title = "One or more validation errors occurred.",
Status = StatusCodes.Status400BadRequest,
Instance = context.HttpContext.Request.Path,
CorrelationId = context.HttpContext.TraceIdentifier
};
// Custom error response format
foreach (var error in context.ModelState)
{
if (error.Value.Errors.Count > 0)
{
problems.Errors[error.Key] = error.Value.Errors
.Select(e => e.ErrorMessage)
.ToArray();
}
}
return new BadRequestObjectResult(problems)
{
ContentTypes = { "application/problem+json" }
};
};
});
var app = builder.Build();
Basic Validator Implementations
// Models/Requests/UserCreateRequest.cs
public class UserCreateRequest
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public string ConfirmPassword { get; set; }
public DateTime DateOfBirth { get; set; }
public string PhoneNumber { get; set; }
public AddressCreateRequest Address { get; set; }
public List<string> Roles { get; set; } = new();
public Dictionary<string, string> Preferences { get; set; } = new();
}
public class AddressCreateRequest
{
public string Street { get; set; }
public string City { get; set; }
public string State { get; set; }
public string ZipCode { get; set; }
public string Country { get; set; }
}
// Validators/UserCreateRequestValidator.cs
public class UserCreateRequestValidator : AbstractValidator<UserCreateRequest>
{
public UserCreateRequestValidator()
{
// Name validation
RuleFor(x => x.FirstName)
.NotEmpty().WithMessage("First name is required")
.Length(2, 50).WithMessage("First name must be between 2 and 50 characters")
.Matches(@"^[a-zA-Z\s\-']+$").WithMessage("First name can only contain letters, spaces, hyphens, and apostrophes");
RuleFor(x => x.LastName)
.NotEmpty().WithMessage("Last name is required")
.Length(2, 50).WithMessage("Last name must be between 2 and 50 characters")
.Matches(@"^[a-zA-Z\s\-']+$").WithMessage("Last name can only contain letters, spaces, hyphens, and apostrophes");
// Email validation
RuleFor(x => x.Email)
.NotEmpty().WithMessage("Email address is required")
.EmailAddress().WithMessage("A valid email address is required")
.MaximumLength(254).WithMessage("Email address cannot exceed 254 characters")
.Must(BeUniqueEmail).WithMessage("Email address is already registered")
.When(x => !string.IsNullOrEmpty(x.Email));
// Password validation with strong requirements
RuleFor(x => x.Password)
.NotEmpty().WithMessage("Password is required")
.MinimumLength(8).WithMessage("Password must be at least 8 characters long")
.Matches(@"[A-Z]").WithMessage("Password must contain at least one uppercase letter")
.Matches(@"[a-z]").WithMessage("Password must contain at least one lowercase letter")
.Matches(@"\d").WithMessage("Password must contain at least one number")
.Matches(@"[!@#$%^&*()_+\-=\[\]{};':""\\|,.<>\/?]")
.WithMessage("Password must contain at least one special character")
.Equal(x => x.ConfirmPassword).WithMessage("Password and confirmation password do not match");
// Date of birth validation
RuleFor(x => x.DateOfBirth)
.NotEmpty().WithMessage("Date of birth is required")
.LessThan(DateTime.Today.AddYears(-13))
.WithMessage("You must be at least 13 years old to register")
.GreaterThan(DateTime.Today.AddYears(-120))
.WithMessage("Please enter a valid date of birth");
// Phone number validation
RuleFor(x => x.PhoneNumber)
.NotEmpty().WithMessage("Phone number is required")
.Matches(@"^\+?[1-9]\d{1,14}$").WithMessage("Please enter a valid phone number")
.When(x => !string.IsNullOrEmpty(x.PhoneNumber));
// Complex object validation
RuleFor(x => x.Address)
.NotNull().WithMessage("Address is required")
.SetValidator(new AddressCreateRequestValidator());
// Collection validation
RuleFor(x => x.Roles)
.NotEmpty().WithMessage("At least one role must be specified")
.Must(roles => roles.All(role => !string.IsNullOrWhiteSpace(role)))
.WithMessage("Roles cannot contain empty values")
.Must(roles => roles.Count <= 5).WithMessage("Cannot assign more than 5 roles");
// Dictionary validation
RuleFor(x => x.Preferences)
.Must(prefs => prefs.Count <= 20)
.WithMessage("Cannot have more than 20 preferences")
.Must(prefs => prefs.All(p => !string.IsNullOrWhiteSpace(p.Key) && p.Key.Length <= 50))
.WithMessage("Preference keys must be non-empty and less than 50 characters");
// Cross-property validation
RuleFor(x => x)
.Must(x => !x.Email.Equals("[email protected]", StringComparison.OrdinalIgnoreCase) ||
x.Roles.Contains("Administrator"))
.WithMessage("Admin email must have administrator role")
.OverridePropertyName("Email");
}
private bool BeUniqueEmail(string email)
{
// In real application, check against database
// This is simplified for example
var existingEmails = new[] { "[email protected]", "[email protected]" };
return !existingEmails.Contains(email?.ToLower());
}
}
// Validators/AddressCreateRequestValidator.cs
public class AddressCreateRequestValidator : AbstractValidator<AddressCreateRequest>
{
public AddressCreateRequestValidator()
{
RuleFor(x => x.Street)
.NotEmpty().WithMessage("Street address is required")
.MaximumLength(100).WithMessage("Street address cannot exceed 100 characters");
RuleFor(x => x.City)
.NotEmpty().WithMessage("City is required")
.Length(2, 50).WithMessage("City must be between 2 and 50 characters")
.Matches(@"^[a-zA-Z\s\-']+$").WithMessage("City can only contain letters, spaces, hyphens, and apostrophes");
RuleFor(x => x.State)
.NotEmpty().WithMessage("State is required")
.Length(2, 50).WithMessage("State must be between 2 and 50 characters");
RuleFor(x => x.ZipCode)
.NotEmpty().WithMessage("Zip code is required")
.Matches(@"^\d{5}(-\d{4})?$").WithMessage("Please enter a valid zip code");
RuleFor(x => x.Country)
.NotEmpty().WithMessage("Country is required")
.Length(2, 56).WithMessage("Please enter a valid country name");
}
}
Controller Implementation with Validation
// Controllers/UsersController.cs
[ApiController]
[Route("api/[controller]")]
[Produces("application/json")]
public class UsersController : ControllerBase
{
private readonly IUserService _userService;
private readonly IValidator<UserCreateRequest> _validator;
private readonly ILogger<UsersController> _logger;
public UsersController(
IUserService userService,
IValidator<UserCreateRequest> validator,
ILogger<UsersController> logger)
{
_userService = userService;
_validator = validator;
_logger = logger;
}
[HttpPost]
[ProducesResponseType(typeof(UserResponse), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<UserResponse>> CreateUser([FromBody] UserCreateRequest request)
{
try
{
_logger.LogInformation("Creating new user with email: {Email}", request.Email);
// Manual validation (if auto-validation is disabled)
var validationResult = await _validator.ValidateAsync(request);
if (!validationResult.IsValid)
{
var problemDetails = CreateValidationProblemDetails(validationResult);
return BadRequest(problemDetails);
}
var user = await _userService.CreateUserAsync(request);
_logger.LogInformation("User created successfully with ID: {UserId}", user.Id);
return CreatedAtAction(
nameof(GetUser),
new { id = user.Id },
user);
}
catch (ValidationException ex)
{
_logger.LogWarning(ex, "Validation failed for user creation request");
var problemDetails = CreateValidationProblemDetails(ex);
return BadRequest(problemDetails);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating user with email: {Email}", request.Email);
return Problem(
title: "An error occurred while creating the user",
statusCode: StatusCodes.Status500InternalServerError,
detail: "Please try again later");
}
}
[HttpGet("{id:guid}")]
[ProducesResponseType(typeof(UserResponse), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<ActionResult<UserResponse>> GetUser(Guid id)
{
var user = await _userService.GetUserByIdAsync(id);
if (user == null)
{
return NotFound(new ProblemDetails
{
Title = "User not found",
Status = StatusCodes.Status404NotFound,
Detail = $"User with ID {id} does not exist"
});
}
return Ok(user);
}
private ValidationProblemDetails CreateValidationProblemDetails(ValidationResult validationResult)
{
var errors = new Dictionary<string, string[]>();
foreach (var error in validationResult.Errors)
{
if (errors.ContainsKey(error.PropertyName))
{
var existingErrors = errors[error.PropertyName].ToList();
existingErrors.Add(error.ErrorMessage);
errors[error.PropertyName] = existingErrors.ToArray();
}
else
{
errors[error.PropertyName] = new[] { error.ErrorMessage };
}
}
return new ValidationProblemDetails(errors)
{
Title = "One or more validation errors occurred",
Status = StatusCodes.Status400BadRequest,
Instance = HttpContext.Request.Path,
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1"
};
}
private ValidationProblemDetails CreateValidationProblemDetails(ValidationException ex)
{
var errors = new Dictionary<string, string[]>();
foreach (var error in ex.Errors)
{
if (errors.ContainsKey(error.PropertyName))
{
var existingErrors = errors[error.PropertyName].ToList();
existingErrors.Add(error.ErrorMessage);
errors[error.PropertyName] = existingErrors.ToArray();
}
else
{
errors[error.PropertyName] = new[] { error.ErrorMessage };
}
}
return new ValidationProblemDetails(errors)
{
Title = "Validation failed",
Status = StatusCodes.Status400BadRequest,
Instance = HttpContext.Request.Path
};
}
}
3. Advanced Validation Scenarios
Conditional and Complex Validation
// Models/Requests/OrderCreateRequest.cs
public class OrderCreateRequest
{
public string OrderType { get; set; } // "Standard", "Express", "International"
public List<OrderItemRequest> Items { get; set; } = new();
public PaymentInfoRequest PaymentInfo { get; set; }
public ShippingInfoRequest ShippingInfo { get; set; }
public string DiscountCode { get; set; }
public bool RequiresInsurance { get; set; }
public decimal InsuranceValue { get; set; }
public DateTime? PreferredDeliveryDate { get; set; }
public string SpecialInstructions { get; set; }
}
public class OrderItemRequest
{
public Guid ProductId { get; set; }
public string ProductName { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
public List<string> SelectedOptions { get; set; } = new();
}
public class PaymentInfoRequest
{
public string PaymentMethod { get; set; } // "CreditCard", "PayPal", "BankTransfer"
public CreditCardInfoRequest CreditCardInfo { get; set; }
public PayPalInfoRequest PayPalInfo { get; set; }
public BankTransferInfoRequest BankTransferInfo { get; set; }
}
public class CreditCardInfoRequest
{
public string CardNumber { get; set; }
public string CardHolderName { get; set; }
public string ExpiryMonth { get; set; }
public string ExpiryYear { get; set; }
public string CVV { get; set; }
}
// Validators/OrderCreateRequestValidator.cs
public class OrderCreateRequestValidator : AbstractValidator<OrderCreateRequest>
{
public OrderCreateRequestValidator()
{
// Order type validation
RuleFor(x => x.OrderType)
.NotEmpty().WithMessage("Order type is required")
.Must(BeValidOrderType).WithMessage("Invalid order type specified");
// Items validation
RuleFor(x => x.Items)
.NotEmpty().WithMessage("Order must contain at least one item")
.Must(items => items.Sum(i => i.Quantity) <= 100)
.WithMessage("Total quantity cannot exceed 100 items")
.ForEach(itemRule =>
{
itemRule.SetValidator(new OrderItemRequestValidator());
});
// Payment info validation based on payment method
RuleFor(x => x.PaymentInfo)
.NotNull().WithMessage("Payment information is required")
.SetValidator(new PaymentInfoRequestValidator());
// Shipping info validation
RuleFor(x => x.ShippingInfo)
.NotNull().WithMessage("Shipping information is required")
.SetValidator(new ShippingInfoRequestValidator());
// Conditional validation for insurance
When(x => x.RequiresInsurance, () =>
{
RuleFor(x => x.InsuranceValue)
.GreaterThan(0).WithMessage("Insurance value must be greater than 0")
.LessThanOrEqualTo(10000).WithMessage("Insurance value cannot exceed $10,000");
});
// Conditional validation for express orders
When(x => x.OrderType == "Express", () =>
{
RuleFor(x => x.PreferredDeliveryDate)
.NotNull().WithMessage("Preferred delivery date is required for express orders")
.GreaterThan(DateTime.Today.AddDays(1))
.WithMessage("Express delivery must be at least 2 days in advance")
.LessThan(DateTime.Today.AddDays(14))
.WithMessage("Express delivery cannot be scheduled more than 14 days in advance");
});
// International order validation
When(x => x.OrderType == "International", () =>
{
RuleFor(x => x.ShippingInfo.Country)
.NotEmpty().WithMessage("Country is required for international orders")
.Must(BeSupportedCountry).WithMessage("International shipping not available to this country");
RuleFor(x => x.Items)
.Must(items => items.All(i => i.UnitPrice <= 5000))
.WithMessage("International orders cannot contain items over $5000")
.Must(items => items.Sum(i => i.Quantity * i.UnitPrice) <= 10000)
.WithMessage("International order total cannot exceed $10,000");
});
// Cross-property validation: Discount code and order total
RuleFor(x => x)
.Must(x => string.IsNullOrEmpty(x.DiscountCode) ||
CalculateOrderTotal(x.Items) >= 50)
.WithMessage("Discount codes can only be applied to orders of $50 or more")
.OverridePropertyName("DiscountCode");
// Special instructions length validation
RuleFor(x => x.SpecialInstructions)
.MaximumLength(500).WithMessage("Special instructions cannot exceed 500 characters")
.When(x => !string.IsNullOrEmpty(x.SpecialInstructions));
}
private bool BeValidOrderType(string orderType)
{
var validTypes = new[] { "Standard", "Express", "International" };
return validTypes.Contains(orderType);
}
private bool BeSupportedCountry(string country)
{
var supportedCountries = new[] { "USA", "Canada", "UK", "Germany", "France", "Australia", "Japan" };
return supportedCountries.Contains(country);
}
private decimal CalculateOrderTotal(List<OrderItemRequest> items)
{
return items.Sum(i => i.Quantity * i.UnitPrice);
}
}
// Validators/OrderItemRequestValidator.cs
public class OrderItemRequestValidator : AbstractValidator<OrderItemRequest>
{
public OrderItemRequestValidator()
{
RuleFor(x => x.ProductId)
.NotEmpty().WithMessage("Product ID is required");
RuleFor(x => x.ProductName)
.NotEmpty().WithMessage("Product name is required")
.MaximumLength(100).WithMessage("Product name cannot exceed 100 characters");
RuleFor(x => x.Quantity)
.GreaterThan(0).WithMessage("Quantity must be greater than 0")
.LessThanOrEqualTo(10).WithMessage("Quantity cannot exceed 10 per item");
RuleFor(x => x.UnitPrice)
.GreaterThan(0).WithMessage("Unit price must be greater than 0")
.LessThanOrEqualTo(10000).WithMessage("Unit price cannot exceed $10,000");
RuleFor(x => x.SelectedOptions)
.Must(options => options.Count <= 5)
.WithMessage("Cannot select more than 5 options per item")
.Must(options => options.All(option => !string.IsNullOrWhiteSpace(option)))
.WithMessage("Options cannot contain empty values");
}
}
// Validators/PaymentInfoRequestValidator.cs
public class PaymentInfoRequestValidator : AbstractValidator<PaymentInfoRequest>
{
public PaymentInfoRequestValidator()
{
RuleFor(x => x.PaymentMethod)
.NotEmpty().WithMessage("Payment method is required")
.Must(BeValidPaymentMethod).WithMessage("Invalid payment method");
// Conditional validation based on payment method
When(x => x.PaymentMethod == "CreditCard", () =>
{
RuleFor(x => x.CreditCardInfo)
.NotNull().WithMessage("Credit card information is required for credit card payments")
.SetValidator(new CreditCardInfoRequestValidator());
});
When(x => x.PaymentMethod == "PayPal", () =>
{
RuleFor(x => x.PayPalInfo)
.NotNull().WithMessage("PayPal information is required for PayPal payments")
.SetValidator(new PayPalInfoRequestValidator());
});
When(x => x.PaymentMethod == "BankTransfer", () =>
{
RuleFor(x => x.BankTransferInfo)
.NotNull().WithMessage("Bank transfer information is required for bank transfer payments")
.SetValidator(new BankTransferInfoRequestValidator());
});
}
private bool BeValidPaymentMethod(string paymentMethod)
{
var validMethods = new[] { "CreditCard", "PayPal", "BankTransfer" };
return validMethods.Contains(paymentMethod);
}
}
// Validators/CreditCardInfoRequestValidator.cs
public class CreditCardInfoRequestValidator : AbstractValidator<CreditCardInfoRequest>
{
public CreditCardInfoRequestValidator()
{
RuleFor(x => x.CardNumber)
.NotEmpty().WithMessage("Card number is required")
.CreditCard().WithMessage("Invalid credit card number")
.Must(BeSupportedCardType).WithMessage("Unsupported card type");
RuleFor(x => x.CardHolderName)
.NotEmpty().WithMessage("Card holder name is required")
.Matches(@"^[a-zA-Z\s\-']+$").WithMessage("Card holder name can only contain letters, spaces, hyphens, and apostrophes")
.MaximumLength(100).WithMessage("Card holder name cannot exceed 100 characters");
RuleFor(x => x.ExpiryMonth)
.NotEmpty().WithMessage("Expiry month is required")
.Matches(@"^(0[1-9]|1[0-2])$").WithMessage("Invalid expiry month");
RuleFor(x => x.ExpiryYear)
.NotEmpty().WithMessage("Expiry year is required")
.Matches(@"^\d{4}$").WithMessage("Invalid expiry year")
.Must(BeValidExpiryYear).WithMessage("Card has expired");
RuleFor(x => x.CVV)
.NotEmpty().WithMessage("CVV is required")
.Matches(@"^\d{3,4}$").WithMessage("Invalid CVV code");
// Cross-property validation: Expiry date
RuleFor(x => x)
.Must(x => BeValidExpiryDate(x.ExpiryMonth, x.ExpiryYear))
.WithMessage("Card has expired")
.OverridePropertyName("ExpiryMonth");
}
private bool BeSupportedCardType(string cardNumber)
{
// Simplified card type validation
if (cardNumber.StartsWith("4")) return true; // Visa
if (cardNumber.StartsWith("5")) return true; // MasterCard
if (cardNumber.StartsWith("34") || cardNumber.StartsWith("37")) return true; // American Express
return false;
}
private bool BeValidExpiryYear(string expiryYear)
{
if (!int.TryParse(expiryYear, out int year))
return false;
return year >= DateTime.Now.Year;
}
private bool BeValidExpiryDate(string expiryMonth, string expiryYear)
{
if (!int.TryParse(expiryMonth, out int month) ||
!int.TryParse(expiryYear, out int year))
return false;
var expiryDate = new DateTime(year, month, 1).AddMonths(1).AddDays(-1);
return expiryDate >= DateTime.Today;
}
}
Custom Validators and Rules
// Custom Validators/FileTypeValidator.cs
public class FileTypeValidator<T> : PropertyValidator<T, IFormFile>
{
private readonly string[] _allowedExtensions;
private readonly string[] _allowedMimeTypes;
public FileTypeValidator(string[] allowedExtensions, string[] allowedMimeTypes)
{
_allowedExtensions = allowedExtensions;
_allowedMimeTypes = allowedMimeTypes;
}
public override string Name => "FileTypeValidator";
public override bool IsValid(ValidationContext<T> context, IFormFile file)
{
if (file == null || file.Length == 0)
return true; // Let Required validator handle empty files
var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
var mimeType = file.ContentType.ToLowerInvariant();
var isValidExtension = _allowedExtensions.Contains(extension);
var isValidMimeType = _allowedMimeTypes.Contains(mimeType);
if (!isValidExtension || !isValidMimeType)
{
context.MessageFormatter
.AppendArgument("AllowedExtensions", string.Join(", ", _allowedExtensions))
.AppendArgument("AllowedMimeTypes", string.Join(", ", _allowedMimeTypes));
return false;
}
return true;
}
protected override string GetDefaultMessageTemplate(string errorCode)
=> "File type is not allowed. Allowed extensions: {AllowedExtensions}, Allowed MIME types: {AllowedMimeTypes}";
}
// Custom Validators/StrongPasswordValidator.cs
public class StrongPasswordValidator<T> : PropertyValidator<T, string>
{
public override string Name => "StrongPasswordValidator";
public override bool IsValid(ValidationContext<T> context, string password)
{
if (string.IsNullOrEmpty(password))
return true; // Let Required validator handle empty passwords
var hasMinimumLength = password.Length >= 8;
var hasUpperCase = password.Any(char.IsUpper);
var hasLowerCase = password.Any(char.IsLower);
var hasDigit = password.Any(char.IsDigit);
var hasSpecialChar = password.Any(ch => !char.IsLetterOrDigit(ch));
if (!hasMinimumLength || !hasUpperCase || !hasLowerCase || !hasDigit || !hasSpecialChar)
{
context.MessageFormatter
.AppendArgument("MinLength", 8)
.AppendArgument("HasUpperCase", hasUpperCase)
.AppendArgument("HasLowerCase", hasLowerCase)
.AppendArgument("HasDigit", hasDigit)
.AppendArgument("HasSpecialChar", hasSpecialChar);
return false;
}
return true;
}
protected override string GetDefaultMessageTemplate(string errorCode)
=> "Password must be at least {MinLength} characters long and contain at least one uppercase letter, one lowercase letter, one digit, and one special character.";
}
// Extension methods for custom validators
public static class CustomValidationExtensions
{
public static IRuleBuilderOptions<T, IFormFile> ValidFileType<T>(
this IRuleBuilder<T, IFormFile> ruleBuilder,
string[] allowedExtensions,
string[] allowedMimeTypes)
{
return ruleBuilder.SetValidator(new FileTypeValidator<T>(allowedExtensions, allowedMimeTypes));
}
public static IRuleBuilderOptions<T, string> StrongPassword<T>(
this IRuleBuilder<T, string> ruleBuilder)
{
return ruleBuilder.SetValidator(new StrongPasswordValidator<T>());
}
public static IRuleBuilderOptions<T, string> BeValidPhoneNumber<T>(
this IRuleBuilder<T, string> ruleBuilder)
{
return ruleBuilder.Must(phone => IsValidPhoneNumber(phone))
.WithMessage("Please enter a valid phone number");
}
public static IRuleBuilderOptions<T, string> BeFutureDate<T>(
this IRuleBuilder<T, string> ruleBuilder)
{
return ruleBuilder.Must(dateString =>
DateTime.TryParse(dateString, out var date) && date > DateTime.Now)
.WithMessage("Date must be in the future");
}
private static bool IsValidPhoneNumber(string phone)
{
if (string.IsNullOrWhiteSpace(phone))
return false;
// E.164 format validation
var pattern = @"^\+[1-9]\d{1,14}$";
return Regex.IsMatch(phone, pattern);
}
}
// Usage of custom validators
public class DocumentUploadRequestValidator : AbstractValidator<DocumentUploadRequest>
{
public DocumentUploadRequestValidator()
{
RuleFor(x => x.File)
.NotNull().WithMessage("File is required")
.ValidFileType(
new[] { ".pdf", ".doc", ".docx", ".jpg", ".png" },
new[] { "application/pdf", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "image/jpeg", "image/png" })
.Must(file => file.Length <= 10 * 1024 * 1024) // 10MB
.WithMessage("File size cannot exceed 10MB");
RuleFor(x => x.Password)
.NotEmpty().WithMessage("Password is required")
.StrongPassword();
}
}
4. AutoMapper Fundamentals
Comprehensive AutoMapper Setup
// Program.cs - AutoMapper Configuration
using AutoMapper;
var builder = WebApplication.CreateBuilder(args);
// Add AutoMapper
builder.Services.AddAutoMapper(typeof(Program));
// Alternative: Manual configuration for better control
builder.Services.AddSingleton<IMapper>(provider =>
{
var configuration = new MapperConfiguration(cfg =>
{
cfg.AddProfile<UserMappingProfile>();
cfg.AddProfile<OrderMappingProfile>();
cfg.AddProfile<ProductMappingProfile>();
// Advanced configuration
cfg.AllowNullCollections = true;
cfg.AllowNullDestinationValues = true;
cfg.ValidateInlineMaps = false;
// For performance-sensitive applications
cfg.Advanced.MaxExecutionPlanDepth = 1;
});
configuration.AssertConfigurationIsValid();
return configuration.CreateMapper();
});
var app = builder.Build();
Basic Mapping Profiles
// Models/Domain/User.cs
public class User
{
public Guid Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public DateTime DateOfBirth { get; set; }
public string PhoneNumber { get; set; }
public Address Address { get; set; }
public List<string> Roles { get; set; } = new();
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public bool IsActive { get; set; }
public Dictionary<string, string> Preferences { get; set; } = new();
}
public class Address
{
public string Street { get; set; }
public string City { get; set; }
public string State { get; set; }
public string ZipCode { get; set; }
public string Country { get; set; }
}
// Models/Responses/UserResponse.cs
public class UserResponse
{
public Guid Id { get; set; }
public string FullName { get; set; }
public string Email { get; set; }
public int Age { get; set; }
public string PhoneNumber { get; set; }
public AddressResponse Address { get; set; }
public List<string> Roles { get; set; } = new();
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public bool IsActive { get; set; }
public Dictionary<string, string> Preferences { get; set; } = new();
public string DisplayName { get; set; }
}
public class AddressResponse
{
public string Street { get; set; }
public string City { get; set; }
public string State { get; set; }
public string ZipCode { get; set; }
public string Country { get; set; }
public string FormattedAddress { get; set; }
}
// MappingProfiles/UserMappingProfile.cs
public class UserMappingProfile : Profile
{
public UserMappingProfile()
{
// Basic mapping from User to UserResponse
CreateMap<User, UserResponse>()
.ForMember(dest => dest.FullName, opt => opt.MapFrom(src => $"{src.FirstName} {src.LastName}"))
.ForMember(dest => dest.Age, opt => opt.MapFrom(src => CalculateAge(src.DateOfBirth)))
.ForMember(dest => dest.DisplayName, opt => opt.MapFrom(src => GenerateDisplayName(src)))
.ForMember(dest => dest.Address, opt => opt.MapFrom(src => src.Address))
.ReverseMap(); // Enable reverse mapping if needed
// Address mapping
CreateMap<Address, AddressResponse>()
.ForMember(dest => dest.FormattedAddress, opt => opt.MapFrom(src =>
$"{src.Street}, {src.City}, {src.State} {src.ZipCode}, {src.Country}"));
// Mapping from CreateRequest to User
CreateMap<UserCreateRequest, User>()
.ForMember(dest => dest.Id, opt => opt.Ignore())
.ForMember(dest => dest.CreatedAt, opt => opt.MapFrom(_ => DateTime.UtcNow))
.ForMember(dest => dest.UpdatedAt, opt => opt.MapFrom(_ => DateTime.UtcNow))
.ForMember(dest => dest.IsActive, opt => opt.MapFrom(_ => true))
.ForMember(dest => dest.Roles, opt => opt.MapFrom(src => src.Roles ?? new List<string>()))
.ForMember(dest => dest.Preferences, opt => opt.MapFrom(src => src.Preferences ?? new Dictionary<string, string>()))
.ForMember(dest => dest.Address, opt => opt.MapFrom(src => src.Address));
// Mapping from UpdateRequest to User (with conditional mapping)
CreateMap<UserUpdateRequest, User>()
.ForMember(dest => dest.UpdatedAt, opt => opt.MapFrom(_ => DateTime.UtcNow))
.ForAllMembers(opts => opts.Condition((src, dest, srcMember) =>
srcMember != null && !srcMember.Equals(GetDefaultValue(srcMember.GetType()))));
}
private static int CalculateAge(DateTime dateOfBirth)
{
var today = DateTime.Today;
var age = today.Year - dateOfBirth.Year;
if (dateOfBirth.Date > today.AddYears(-age)) age--;
return age;
}
private static string GenerateDisplayName(User user)
{
return $"{user.FirstName} {user.LastName.First()}.";
}
private static object GetDefaultValue(Type type)
{
return type.IsValueType ? Activator.CreateInstance(type) : null;
}
}
Service Layer with AutoMapper
// Services/UserService.cs
public interface IUserService
{
Task<UserResponse> CreateUserAsync(UserCreateRequest request);
Task<UserResponse> GetUserByIdAsync(Guid id);
Task<List<UserResponse>> GetUsersAsync(UserQuery query);
Task<UserResponse> UpdateUserAsync(Guid id, UserUpdateRequest request);
Task<bool> DeleteUserAsync(Guid id);
}
public class UserService : IUserService
{
private readonly IUserRepository _userRepository;
private readonly IMapper _mapper;
private readonly ILogger<UserService> _logger;
public UserService(IUserRepository userRepository, IMapper mapper, ILogger<UserService> logger)
{
_userRepository = userRepository;
_mapper = mapper;
_logger = logger;
}
public async Task<UserResponse> CreateUserAsync(UserCreateRequest request)
{
_logger.LogInformation("Creating new user with email: {Email}", request.Email);
// Map request to domain model
var user = _mapper.Map<User>(request);
// Additional business logic
user.Id = Guid.NewGuid();
user.CreatedAt = DateTime.UtcNow;
user.UpdatedAt = DateTime.UtcNow;
// Save to database
var createdUser = await _userRepository.AddAsync(user);
// Map domain model to response
var response = _mapper.Map<UserResponse>(createdUser);
_logger.LogInformation("User created successfully with ID: {UserId}", createdUser.Id);
return response;
}
public async Task<UserResponse> GetUserByIdAsync(Guid id)
{
_logger.LogDebug("Retrieving user with ID: {UserId}", id);
var user = await _userRepository.GetByIdAsync(id);
if (user == null)
{
_logger.LogWarning("User not found with ID: {UserId}", id);
return null;
}
var response = _mapper.Map<UserResponse>(user);
_logger.LogDebug("User retrieved successfully: {UserId}", id);
return response;
}
public async Task<List<UserResponse>> GetUsersAsync(UserQuery query)
{
_logger.LogDebug("Retrieving users with query: {@Query}", query);
var users = await _userRepository.GetAllAsync(query);
// Map list of domain models to list of responses
var responses = _mapper.Map<List<UserResponse>>(users);
_logger.LogDebug("Retrieved {UserCount} users", responses.Count);
return responses;
}
public async Task<UserResponse> UpdateUserAsync(Guid id, UserUpdateRequest request)
{
_logger.LogInformation("Updating user with ID: {UserId}", id);
var existingUser = await _userRepository.GetByIdAsync(id);
if (existingUser == null)
{
_logger.LogWarning("User not found for update: {UserId}", id);
return null;
}
// Map updates to existing user
_mapper.Map(request, existingUser);
existingUser.UpdatedAt = DateTime.UtcNow;
var updatedUser = await _userRepository.UpdateAsync(existingUser);
var response = _mapper.Map<UserResponse>(updatedUser);
_logger.LogInformation("User updated successfully: {UserId}", id);
return response;
}
public async Task<bool> DeleteUserAsync(Guid id)
{
_logger.LogInformation("Deleting user with ID: {UserId}", id);
var result = await _userRepository.DeleteAsync(id);
if (result)
{
_logger.LogInformation("User deleted successfully: {UserId}", id);
}
else
{
_logger.LogWarning("User not found for deletion: {UserId}", id);
}
return result;
}
}
5. Advanced Mapping Techniques
Complex Object Mapping with Custom Resolvers
// Models/Domain/Order.cs
public class Order
{
public Guid Id { get; set; }
public string OrderNumber { get; set; }
public Guid CustomerId { get; set; }
public User Customer { get; set; }
public List<OrderItem> Items { get; set; } = new();
public OrderStatus Status { get; set; }
public decimal TotalAmount { get; set; }
public decimal TaxAmount { get; set; }
public decimal DiscountAmount { get; set; }
public decimal FinalAmount { get; set; }
public Address ShippingAddress { get; set; }
public Address BillingAddress { get; set; }
public PaymentInfo PaymentInfo { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public DateTime? ShippedAt { get; set; }
public DateTime? DeliveredAt { get; set; }
public string Notes { get; set; }
}
public class OrderItem
{
public Guid Id { get; set; }
public Guid ProductId { get; set; }
public Product Product { get; set; }
public string ProductName { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
public decimal TotalPrice { get; set; }
public List<string> SelectedOptions { get; set; } = new();
}
public enum OrderStatus
{
Pending,
Confirmed,
Processing,
Shipped,
Delivered,
Cancelled,
Refunded
}
// Models/Responses/OrderResponse.cs
public class OrderResponse
{
public Guid Id { get; set; }
public string OrderNumber { get; set; }
public OrderCustomerResponse Customer { get; set; }
public List<OrderItemResponse> Items { get; set; } = new();
public string Status { get; set; }
public string StatusDescription { get; set; }
public decimal TotalAmount { get; set; }
public decimal TaxAmount { get; set; }
public decimal DiscountAmount { get; set; }
public decimal FinalAmount { get; set; }
public AddressResponse ShippingAddress { get; set; }
public AddressResponse BillingAddress { get; set; }
public PaymentInfoResponse PaymentInfo { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public DateTime? ShippedAt { get; set; }
public DateTime? DeliveredAt { get; set; }
public string Notes { get; set; }
public string EstimatedDelivery { get; set; }
public bool CanBeCancelled { get; set; }
public bool CanBeModified { get; set; }
}
public class OrderCustomerResponse
{
public Guid Id { get; set; }
public string FullName { get; set; }
public string Email { get; set; }
public string PhoneNumber { get; set; }
}
public class OrderItemResponse
{
public Guid Id { get; set; }
public Guid ProductId { get; set; }
public string ProductName { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
public decimal TotalPrice { get; set; }
public List<string> SelectedOptions { get; set; } = new();
public string ImageUrl { get; set; }
public string ProductCategory { get; set; }
}
// MappingProfiles/OrderMappingProfile.cs
public class OrderMappingProfile : Profile
{
public OrderMappingProfile()
{
// Complex order mapping with custom resolvers
CreateMap<Order, OrderResponse>()
.ForMember(dest => dest.Status, opt => opt.MapFrom(src => src.Status.ToString()))
.ForMember(dest => dest.StatusDescription, opt => opt.MapFrom<OrderStatusDescriptionResolver>())
.ForMember(dest => dest.Customer, opt => opt.MapFrom(src => src.Customer))
.ForMember(dest => dest.Items, opt => opt.MapFrom(src => src.Items))
.ForMember(dest => dest.ShippingAddress, opt => opt.MapFrom(src => src.ShippingAddress))
.ForMember(dest => dest.BillingAddress, opt => opt.MapFrom(src => src.BillingAddress))
.ForMember(dest => dest.PaymentInfo, opt => opt.MapFrom(src => src.PaymentInfo))
.ForMember(dest => dest.EstimatedDelivery, opt => opt.MapFrom<EstimatedDeliveryResolver>())
.ForMember(dest => dest.CanBeCancelled, opt => opt.MapFrom<OrderCancellationResolver>())
.ForMember(dest => dest.CanBeModified, opt => opt.MapFrom<OrderModificationResolver>());
// Customer mapping within order context
CreateMap<User, OrderCustomerResponse>()
.ForMember(dest => dest.FullName, opt => opt.MapFrom(src => $"{src.FirstName} {src.LastName}"));
// Order item mapping with additional data
CreateMap<OrderItem, OrderItemResponse>()
.ForMember(dest => dest.ImageUrl, opt => opt.MapFrom<OrderItemImageResolver>())
.ForMember(dest => dest.ProductCategory, opt => opt.MapFrom(src => src.Product.Category));
// Mapping from create request to order
CreateMap<OrderCreateRequest, Order>()
.ForMember(dest => dest.Id, opt => opt.Ignore())
.ForMember(dest => dest.OrderNumber, opt => opt.MapFrom(_ => GenerateOrderNumber()))
.ForMember(dest => dest.Status, opt => opt.MapFrom(_ => OrderStatus.Pending))
.ForMember(dest => dest.CreatedAt, opt => opt.MapFrom(_ => DateTime.UtcNow))
.ForMember(dest => dest.UpdatedAt, opt => opt.MapFrom(_ => DateTime.UtcNow))
.ForMember(dest => dest.Items, opt => opt.MapFrom(src => src.Items))
.ForMember(dest => dest.TotalAmount, opt => opt.MapFrom<OrderTotalAmountResolver>())
.ForMember(dest => dest.FinalAmount, opt => opt.MapFrom<OrderFinalAmountResolver>())
.AfterMap((src, dest) =>
{
// Post-mapping logic
foreach (var item in dest.Items)
{
item.TotalPrice = item.Quantity * item.UnitPrice;
}
});
}
private string GenerateOrderNumber()
{
return $"ORD-{DateTime.UtcNow:yyyyMMdd}-{Guid.NewGuid().ToString("N").Substring(0, 8).ToUpper()}";
}
}
// Custom Resolvers/OrderStatusDescriptionResolver.cs
public class OrderStatusDescriptionResolver : IValueResolver<Order, OrderResponse, string>
{
public string Resolve(Order source, OrderResponse destination, string destMember, ResolutionContext context)
{
return source.Status switch
{
OrderStatus.Pending => "Your order is being processed",
OrderStatus.Confirmed => "Your order has been confirmed",
OrderStatus.Processing => "We're preparing your order for shipment",
OrderStatus.Shipped => "Your order is on the way",
OrderStatus.Delivered => "Your order has been delivered",
OrderStatus.Cancelled => "Your order has been cancelled",
OrderStatus.Refunded => "Your order has been refunded",
_ => "Unknown status"
};
}
}
// Custom Resolvers/EstimatedDeliveryResolver.cs
public class EstimatedDeliveryResolver : IValueResolver<Order, OrderResponse, string>
{
public string Resolve(Order source, OrderResponse destination, string destMember, ResolutionContext context)
{
if (source.ShippedAt.HasValue)
{
var estimatedDelivery = source.ShippedAt.Value.AddDays(3); // Standard shipping time
return estimatedDelivery.ToString("MMMM dd, yyyy");
}
if (source.Status >= OrderStatus.Confirmed)
{
var estimatedShipDate = source.CreatedAt.AddDays(2); // Processing time
var estimatedDelivery = estimatedShipDate.AddDays(3); // Shipping time
return estimatedDelivery.ToString("MMMM dd, yyyy");
}
return "To be determined";
}
}
// Custom Resolvers/OrderCancellationResolver.cs
public class OrderCancellationResolver : IValueResolver<Order, OrderResponse, bool>
{
public bool Resolve(Order source, OrderResponse destination, string destMember, ResolutionContext context)
{
return source.Status switch
{
OrderStatus.Pending or OrderStatus.Confirmed => true,
_ => false
};
}
}
// Custom Resolvers/OrderModificationResolver.cs
public class OrderModificationResolver : IValueResolver<Order, OrderResponse, bool>
{
public bool Resolve(Order source, OrderResponse destination, string destMember, ResolutionContext context)
{
return source.Status == OrderStatus.Pending;
}
}
// Custom Resolvers/OrderItemImageResolver.cs
public class OrderItemImageResolver : IValueResolver<OrderItem, OrderItemResponse, string>
{
private readonly IProductImageService _imageService;
public OrderItemImageResolver(IProductImageService imageService)
{
_imageService = imageService;
}
public string Resolve(OrderItem source, OrderItemResponse destination, string destMember, ResolutionContext context)
{
return _imageService.GetProductImageUrl(source.ProductId);
}
}
// Custom Resolvers/OrderTotalAmountResolver.cs
public class OrderTotalAmountResolver : IValueResolver<OrderCreateRequest, Order, decimal>
{
public decimal Resolve(OrderCreateRequest source, Order destination, decimal destMember, ResolutionContext context)
{
return source.Items.Sum(item => item.Quantity * item.UnitPrice);
}
}
// Custom Resolvers/OrderFinalAmountResolver.cs
public class OrderFinalAmountResolver : IValueResolver<OrderCreateRequest, Order, decimal>
{
public decimal Resolve(OrderCreateRequest source, Order destination, decimal destMember, ResolutionContext context)
{
var totalAmount = source.Items.Sum(item => item.Quantity * item.UnitPrice);
var taxAmount = totalAmount * 0.1m; // 10% tax
var discountAmount = string.IsNullOrEmpty(source.DiscountCode) ? 0 : totalAmount * 0.1m; // 10% discount
return totalAmount + taxAmount - discountAmount;
}
}
Collection and Inheritance Mapping
// Models for inheritance example
public abstract class Notification
{
public Guid Id { get; set; }
public string Title { get; set; }
public string Message { get; set; }
public DateTime CreatedAt { get; set; }
public bool IsRead { get; set; }
}
public class EmailNotification : Notification
{
public string EmailAddress { get; set; }
public string Subject { get; set; }
public bool IsHtml { get; set; }
}
public class SmsNotification : Notification
{
public string PhoneNumber { get; set; }
public string SenderId { get; set; }
}
public class PushNotification : Notification
{
public string DeviceToken { get; set; }
public string Platform { get; set; }
}
// Response models
public class NotificationResponse
{
public Guid Id { get; set; }
public string Title { get; set; }
public string Message { get; set; }
public DateTime CreatedAt { get; set; }
public bool IsRead { get; set; }
public string Type { get; set; }
public Dictionary<string, object> AdditionalData { get; set; } = new();
}
// MappingProfiles/NotificationMappingProfile.cs
public class NotificationMappingProfile : Profile
{
public NotificationMappingProfile()
{
// Inheritance mapping using Include
CreateMap<Notification, NotificationResponse>()
.Include<EmailNotification, NotificationResponse>()
.Include<SmsNotification, NotificationResponse>()
.Include<PushNotification, NotificationResponse>()
.ForMember(dest => dest.Type, opt => opt.MapFrom(src => src.GetType().Name))
.ForMember(dest => dest.AdditionalData, opt => opt.Ignore());
CreateMap<EmailNotification, NotificationResponse>()
.ForMember(dest => dest.AdditionalData, opt => opt.MapFrom(src => new Dictionary<string, object>
{
["EmailAddress"] = src.EmailAddress,
["Subject"] = src.Subject,
["IsHtml"] = src.IsHtml
}));
CreateMap<SmsNotification, NotificationResponse>()
.ForMember(dest => dest.AdditionalData, opt => opt.MapFrom(src => new Dictionary<string, object>
{
["PhoneNumber"] = src.PhoneNumber,
["SenderId"] = src.SenderId
}));
CreateMap<PushNotification, NotificationResponse>()
.ForMember(dest => dest.AdditionalData, opt => opt.MapFrom(src => new Dictionary<string, object>
{
["DeviceToken"] = src.DeviceToken,
["Platform"] = src.Platform
}));
// Complex collection mapping
CreateMap<List<Notification>, NotificationSummaryResponse>()
.ForMember(dest => dest.TotalCount, opt => opt.MapFrom(src => src.Count))
.ForMember(dest => dest.UnreadCount, opt => opt.MapFrom(src => src.Count(n => !n.IsRead)))
.ForMember(dest => dest.NotificationsByType, opt => opt.MapFrom(src => src
.GroupBy(n => n.GetType().Name)
.ToDictionary(g => g.Key, g => g.Count())))
.ForMember(dest => dest.RecentNotifications, opt => opt.MapFrom(src => src
.OrderByDescending(n => n.CreatedAt)
.Take(5)
.ToList()));
}
}
6. Integration Patterns & Best Practices
Service Layer Integration
// Services/OrderService.cs with advanced mapping
public class OrderService : IOrderService
{
private readonly IOrderRepository _orderRepository;
private readonly IUserRepository _userRepository;
private readonly IProductRepository _productRepository;
private readonly IMapper _mapper;
private readonly ILogger<OrderService> _logger;
private readonly IValidator<OrderCreateRequest> _validator;
public OrderService(
IOrderRepository orderRepository,
IUserRepository userRepository,
IProductRepository productRepository,
IMapper mapper,
ILogger<OrderService> logger,
IValidator<OrderCreateRequest> validator)
{
_orderRepository = orderRepository;
_userRepository = userRepository;
_productRepository = productRepository;
_mapper = mapper;
_logger = logger;
_validator = validator;
}
public async Task<OrderResponse> CreateOrderAsync(OrderCreateRequest request)
{
_logger.LogInformation("Creating order for user: {UserId}", request.CustomerId);
// Validate request
var validationResult = await _validator.ValidateAsync(request);
if (!validationResult.IsValid)
{
throw new ValidationException(validationResult.Errors);
}
// Verify customer exists
var customer = await _userRepository.GetByIdAsync(request.CustomerId);
if (customer == null)
{
throw new ArgumentException($"Customer with ID {request.CustomerId} not found");
}
// Verify products exist and are available
await VerifyProductsAvailabilityAsync(request.Items);
// Map request to domain model with additional data
var order = _mapper.Map<Order>(request);
order.Customer = customer;
// Calculate additional order details
await CalculateOrderDetailsAsync(order);
// Save order
var createdOrder = await _orderRepository.AddAsync(order);
// Map to response with enriched data
var response = _mapper.Map<OrderResponse>(createdOrder);
_logger.LogInformation("Order created successfully: {OrderId}", createdOrder.Id);
return response;
}
public async Task<OrderResponse> GetOrderWithDetailsAsync(Guid orderId)
{
_logger.LogDebug("Retrieving order with details: {OrderId}", orderId);
var order = await _orderRepository.GetByIdWithDetailsAsync(orderId);
if (order == null)
{
_logger.LogWarning("Order not found: {OrderId}", orderId);
return null;
}
// Enrich order data before mapping
await EnrichOrderDataAsync(order);
var response = _mapper.Map<OrderResponse>(order);
_logger.LogDebug("Order retrieved successfully: {OrderId}", orderId);
return response;
}
public async Task<PagedResponse<OrderResponse>> GetOrdersAsync(OrderQuery query)
{
_logger.LogDebug("Retrieving orders with query: {@Query}", query);
var orders = await _orderRepository.GetPagedAsync(query);
// Enrich each order with additional data
foreach (var order in orders.Items)
{
await EnrichOrderDataAsync(order);
}
var responses = _mapper.Map<List<OrderResponse>>(orders.Items);
var pagedResponse = new PagedResponse<OrderResponse>
{
Items = responses,
TotalCount = orders.TotalCount,
PageNumber = orders.PageNumber,
PageSize = orders.PageSize
};
_logger.LogDebug("Retrieved {OrderCount} orders", responses.Count);
return pagedResponse;
}
private async Task VerifyProductsAvailabilityAsync(List<OrderItemRequest> items)
{
foreach (var item in items)
{
var product = await _productRepository.GetByIdAsync(item.ProductId);
if (product == null)
{
throw new ArgumentException($"Product with ID {item.ProductId} not found");
}
if (!product.IsActive)
{
throw new InvalidOperationException($"Product {product.Name} is not available");
}
if (product.StockQuantity < item.Quantity)
{
throw new InvalidOperationException($"Insufficient stock for product {product.Name}");
}
}
}
private async Task CalculateOrderDetailsAsync(Order order)
{
foreach (var item in order.Items)
{
var product = await _productRepository.GetByIdAsync(item.ProductId);
item.ProductName = product.Name;
item.UnitPrice = product.Price;
item.TotalPrice = item.Quantity * item.UnitPrice;
}
order.TotalAmount = order.Items.Sum(i => i.TotalPrice);
order.TaxAmount = order.TotalAmount * 0.1m; // 10% tax
// Apply discount logic
if (!string.IsNullOrEmpty(order.Notes) && order.Notes.Contains("DISCOUNT"))
{
order.DiscountAmount = order.TotalAmount * 0.1m; // 10% discount
}
order.FinalAmount = order.TotalAmount + order.TaxAmount - order.DiscountAmount;
}
private async Task EnrichOrderDataAsync(Order order)
{
// Add any additional data enrichment logic here
// This could include fetching related data, calculating derived properties, etc.
if (order.Customer == null)
{
order.Customer = await _userRepository.GetByIdAsync(order.CustomerId);
}
foreach (var item in order.Items.Where(i => i.Product == null))
{
item.Product = await _productRepository.GetByIdAsync(item.ProductId);
}
}
}
Advanced Validation Service
// Services/ValidationService.cs
public interface IValidationService
{
Task<ValidationResult> ValidateAsync<T>(T model);
Task ValidateAndThrowAsync<T>(T model);
bool IsValid<T>(T model);
List<string> GetValidationErrors<T>(T model);
}
public class ValidationService : IValidationService
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<ValidationService> _logger;
public ValidationService(IServiceProvider serviceProvider, ILogger<ValidationService> logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
}
public async Task<ValidationResult> ValidateAsync<T>(T model)
{
if (model == null)
{
throw new ArgumentNullException(nameof(model));
}
var validator = _serviceProvider.GetService<IValidator<T>>();
if (validator == null)
{
_logger.LogWarning("No validator found for type {Type}", typeof(T).Name);
return new ValidationResult(); // Return empty valid result
}
var result = await validator.ValidateAsync(model);
if (!result.IsValid)
{
_logger.LogDebug("Validation failed for {Type}: {ErrorCount} errors",
typeof(T).Name, result.Errors.Count);
}
return result;
}
public async Task ValidateAndThrowAsync<T>(T model)
{
var result = await ValidateAsync(model);
if (!result.IsValid)
{
throw new ValidationException(result.Errors);
}
}
public bool IsValid<T>(T model)
{
return ValidateAsync(model).GetAwaiter().GetResult().IsValid;
}
public List<string> GetValidationErrors<T>(T model)
{
var result = ValidateAsync(model).GetAwaiter().GetResult();
return result.Errors.Select(e => e.ErrorMessage).ToList();
}
}
// Custom exception for validation
public class CustomValidationException : Exception
{
public Dictionary<string, string[]> Errors { get; }
public CustomValidationException(Dictionary<string, string[]> errors)
: base("One or more validation errors occurred")
{
Errors = errors;
}
public CustomValidationException(ValidationResult validationResult)
: this(ConvertToDictionary(validationResult))
{
}
private static Dictionary<string, string[]> ConvertToDictionary(ValidationResult validationResult)
{
var errors = new Dictionary<string, string[]>();
foreach (var error in validationResult.Errors)
{
if (errors.ContainsKey(error.PropertyName))
{
var existingErrors = errors[error.PropertyName].ToList();
existingErrors.Add(error.ErrorMessage);
errors[error.PropertyName] = existingErrors.ToArray();
}
else
{
errors[error.PropertyName] = new[] { error.ErrorMessage };
}
}
return errors;
}
}
7. Real-World Enterprise Application
E-commerce Platform Implementation
// Complete e-commerce example
public class ECommerceService
{
private readonly IProductService _productService;
private readonly IOrderService _orderService;
private readonly IUserService _userService;
private readonly IValidationService _validationService;
private readonly IMapper _mapper;
private readonly ILogger<ECommerceService> _logger;
public ECommerceService(
IProductService productService,
IOrderService orderService,
IUserService userService,
IValidationService validationService,
IMapper mapper,
ILogger<ECommerceService> logger)
{
_productService = productService;
_orderService = orderService;
_userService = userService;
_validationService = validationService;
_mapper = mapper;
_logger = logger;
}
public async Task<OrderProcessResult> ProcessOrderAsync(OrderProcessRequest request)
{
using var activity = Activity.Current?.Source.StartActivity("ProcessOrder");
_logger.LogInformation("Processing order for user {UserId}", request.UserId);
try
{
// Step 1: Validate the request
await _validationService.ValidateAndThrowAsync(request);
// Step 2: Get user information
var user = await _userService.GetUserByIdAsync(request.UserId);
if (user == null)
{
throw new ArgumentException($"User with ID {request.UserId} not found");
}
// Step 3: Verify product availability and prices
var productVerification = await VerifyProductsAsync(request.Items);
if (!productVerification.IsSuccess)
{
return new OrderProcessResult
{
IsSuccess = false,
Errors = productVerification.Errors,
OrderId = null
};
}
// Step 4: Create order
var orderCreateRequest = _mapper.Map<OrderCreateRequest>(request);
orderCreateRequest.CustomerId = request.UserId;
orderCreateRequest.TotalAmount = productVerification.TotalAmount;
var order = await _orderService.CreateOrderAsync(orderCreateRequest);
// Step 5: Process payment
var paymentResult = await ProcessPaymentAsync(order, request.PaymentInfo);
if (!paymentResult.IsSuccess)
{
// Rollback order creation
await _orderService.CancelOrderAsync(order.Id);
return new OrderProcessResult
{
IsSuccess = false,
Errors = paymentResult.Errors,
OrderId = order.Id
};
}
// Step 6: Update inventory
await UpdateInventoryAsync(request.Items);
// Step 7: Send confirmation
await SendOrderConfirmationAsync(order, user);
_logger.LogInformation("Order processed successfully: {OrderId}", order.Id);
return new OrderProcessResult
{
IsSuccess = true,
OrderId = order.Id,
OrderNumber = order.OrderNumber,
TotalAmount = order.FinalAmount
};
}
catch (ValidationException ex)
{
_logger.LogWarning(ex, "Validation failed during order processing");
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing order for user {UserId}", request.UserId);
throw;
}
}
private async Task<ProductVerificationResult> VerifyProductsAsync(List<OrderItemRequest> items)
{
var result = new ProductVerificationResult();
var errors = new List<string>();
foreach (var item in items)
{
var product = await _productService.GetProductByIdAsync(item.ProductId);
if (product == null)
{
errors.Add($"Product with ID {item.ProductId} not found");
continue;
}
if (!product.IsActive)
{
errors.Add($"Product {product.Name} is not available");
continue;
}
if (product.StockQuantity < item.Quantity)
{
errors.Add($"Insufficient stock for product {product.Name}. Available: {product.StockQuantity}, Requested: {item.Quantity}");
continue;
}
// Verify price hasn't changed
if (product.Price != item.UnitPrice)
{
errors.Add($"Price for product {product.Name} has changed. New price: {product.Price:C}");
continue;
}
result.TotalAmount += item.Quantity * item.UnitPrice;
}
result.IsSuccess = !errors.Any();
result.Errors = errors;
return result;
}
private async Task<PaymentResult> ProcessPaymentAsync(OrderResponse order, PaymentInfoRequest paymentInfo)
{
// Payment processing logic
// This would integrate with payment gateways like Stripe, PayPal, etc.
try
{
// Simulate payment processing
await Task.Delay(1000);
return new PaymentResult
{
IsSuccess = true,
TransactionId = Guid.NewGuid().ToString(),
ProcessedAt = DateTime.UtcNow
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Payment processing failed for order {OrderId}", order.Id);
return new PaymentResult
{
IsSuccess = false,
Errors = new[] { "Payment processing failed. Please try again." }
};
}
}
private async Task UpdateInventoryAsync(List<OrderItemRequest> items)
{
foreach (var item in items)
{
await _productService.UpdateStockAsync(item.ProductId, -item.Quantity);
}
}
private async Task SendOrderConfirmationAsync(OrderResponse order, UserResponse user)
{
// Email sending logic
// This would integrate with email services like SendGrid, SMTP, etc.
try
{
var emailRequest = _mapper.Map<OrderConfirmationEmailRequest>(order);
emailRequest.RecipientEmail = user.Email;
emailRequest.RecipientName = user.FullName;
// Send email
await Task.Delay(500); // Simulate email sending
_logger.LogInformation("Order confirmation sent for order {OrderId}", order.Id);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to send order confirmation for order {OrderId}", order.Id);
// Don't throw - email failure shouldn't fail the entire order process
}
}
}
// Supporting classes
public class OrderProcessRequest
{
public Guid UserId { get; set; }
public List<OrderItemRequest> Items { get; set; } = new();
public PaymentInfoRequest PaymentInfo { get; set; }
public AddressCreateRequest ShippingAddress { get; set; }
public AddressCreateRequest BillingAddress { get; set; }
public string DiscountCode { get; set; }
public string Notes { get; set; }
}
public class OrderProcessResult
{
public bool IsSuccess { get; set; }
public Guid? OrderId { get; set; }
public string OrderNumber { get; set; }
public decimal TotalAmount { get; set; }
public List<string> Errors { get; set; } = new();
public string RedirectUrl { get; set; }
}
public class ProductVerificationResult
{
public bool IsSuccess { get; set; }
public decimal TotalAmount { get; set; }
public List<string> Errors { get; set; } = new();
}
public class PaymentResult
{
public bool IsSuccess { get; set; }
public string TransactionId { get; set; }
public DateTime ProcessedAt { get; set; }
public List<string> Errors { get; set; } = new();
}
8. Performance Optimization & Testing
Performance-Optimized Mapping
csharp
// Optimized mapping configurations
public class OptimizedMappingProfile : Profile
{
public OptimizedMappingProfile()
{
// Disable constructor mapping for better performance
this.DisableConstructorMapping();
// Optimized user mapping
CreateMap<User, UserResponse>()
.ForMember(dest => dest.FullName, opt => opt.MapFrom(src => $"{src.FirstName} {src.LastName}"))
.ForMember(dest => dest.Age, opt => opt.MapFrom(src => CalculateAge(src.DateOfBirth)))
.ForMember(dest => dest.DisplayName, opt => opt.MapFrom(src => $"{src.FirstName} {src.LastName.First()}."))
.ReverseMap()
.ForMember(dest => dest.CreatedAt, opt => opt.Ignore())
.ForMember(dest => dest.UpdatedAt, opt => opt.Ignore());
// Use fast expression compilation for critical paths
CreateMap<Order, OrderSummaryResponse>()
.ConvertUsing<OrderToSummaryConverter>();
}
private static int CalculateAge(DateTime dateOfBirth)
{
var today = DateTime.Today;
var age = today.Year - dateOfBirth.Year;
if (dateOfBirth.Date > today.AddYears(-age)) age--;
return age;
}
}
// Custom type converter for better performance
public class OrderToSummaryConverter : ITypeConverter<Order, OrderSummaryResponse>
{
public OrderSummaryResponse Convert(Order source, OrderSummaryResponse destination, ResolutionContext context)
{
return new OrderSummaryResponse
{
Id = source.Id,
OrderNumber = source.OrderNumber,
CustomerName = $"{source.Customer.FirstName} {source.Customer.LastName}",
TotalAmount = source.TotalAmount,
Status = source.Status.ToString(),
CreatedAt = source.CreatedAt,
ItemCount = source.Items.Sum(i => i.Quantity)
};
}
}
// Cached mapper for high-performance scenarios
public class CachedMappingService
{
private readonly IMapper _mapper;
private readonly MemoryCache _mappingCache;
private readonly TimeSpan _cacheDuration;
public CachedMappingService(IMapper mapper)
{
_mapper = mapper;
_mappingCache = new MemoryCache(new MemoryCacheOptions());
_cacheDuration = TimeSpan.FromMinutes(30);
}
public TDestination Map<TSource, TDestination>(TSource source)
{
var cacheKey = $"{typeof(TSource).Name}-{typeof(TDestination).Name}-{GetHashCode(source)}";
if (_mappingCache.TryGetValue<TDestination>(cacheKey, out var cachedResult))
{
return cachedResult;
}
var result = _mapper.Map<TDestination>(source);
var cacheOptions = new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = _cacheDuration,
Size = 1
};
_mappingCache.Set(cacheKey, result, cacheOptions);
return result;
}
public List<TDestination> MapList<TSource, TDestination>(List<TSource> sources)
{
var results = new List<TDestination>();
foreach (var source in sources)
{
results.Add(Map<TSource, TDestination>(source));
}
return results;
}
private string GetHashCode<T>(T obj)
{
if (obj == null) return "null";
// Simple hash code generation for caching
return JsonSerializer.Serialize(obj).GetHashCode().ToString();
}
}
Comprehensive Testing Suite
csharp
// Unit tests for validators
public class UserCreateRequestValidatorTests
{
private readonly UserCreateRequestValidator _validator;
public UserCreateRequestValidatorTests()
{
_validator = new UserCreateRequestValidator();
}
[Fact]
public async Task Validate_WithValidRequest_ShouldPass()
{
// Arrange
var request = new UserCreateRequest
{
FirstName = "John",
LastName = "Doe",
Email = "[email protected]",
Password = "SecurePassword123!",
ConfirmPassword = "SecurePassword123!",
DateOfBirth = new DateTime(1990, 1, 1),
PhoneNumber = "+1234567890",
Address = new AddressCreateRequest
{
Street = "123 Main St",
City = "New York",
State = "NY",
ZipCode = "10001",
Country = "USA"
},
Roles = new List<string> { "User" }
};
// Act
var result = await _validator.ValidateAsync(request);
// Assert
result.IsValid.Should().BeTrue();
}
[Theory]
[InlineData("")]
[InlineData(" ")]
[InlineData(null)]
public async Task Validate_WithInvalidFirstName_ShouldFail(string firstName)
{
// Arrange
var request = new UserCreateRequest
{
FirstName = firstName,
LastName = "Doe",
Email = "[email protected]",
Password = "Password123!",
ConfirmPassword = "Password123!"
};
// Act
var result = await _validator.ValidateAsync(request);
// Assert
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.PropertyName == "FirstName");
}
[Theory]
[InlineData("weak")]
[InlineData("password")]
[InlineData("12345678")]
[InlineData("Password")]
[InlineData("PASSWORD123")]
public async Task Validate_WithWeakPassword_ShouldFail(string password)
{
// Arrange
var request = new UserCreateRequest
{
FirstName = "John",
LastName = "Doe",
Email = "[email protected]",
Password = password,
ConfirmPassword = password
};
// Act
var result = await _validator.ValidateAsync(request);
// Assert
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.PropertyName == "Password");
}
[Fact]
public async Task Validate_WithUnderageUser_ShouldFail()
{
// Arrange
var request = new UserCreateRequest
{
FirstName = "John",
LastName = "Doe",
Email = "[email protected]",
Password = "Password123!",
ConfirmPassword = "Password123!",
DateOfBirth = DateTime.Today.AddYears(-12) // 12 years old
};
// Act
var result = await _validator.ValidateAsync(request);
// Assert
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.PropertyName == "DateOfBirth");
}
}
// Unit tests for mapping profiles
public class UserMappingProfileTests
{
private readonly IMapper _mapper;
public UserMappingProfileTests()
{
var configuration = new MapperConfiguration(cfg =>
{
cfg.AddProfile<UserMappingProfile>();
});
_mapper = configuration.CreateMapper();
}
[Fact]
public void Configuration_ShouldBeValid()
{
// Assert
_mapper.ConfigurationProvider.AssertConfigurationIsValid();
}
[Fact]
public void Map_FromUserToUserResponse_ShouldMapCorrectly()
{
// Arrange
var user = new User
{
Id = Guid.NewGuid(),
FirstName = "John",
LastName = "Doe",
Email = "[email protected]",
DateOfBirth = new DateTime(1990, 1, 1),
PhoneNumber = "+1234567890",
CreatedAt = DateTime.UtcNow,
IsActive = true
};
// Act
var result = _mapper.Map<UserResponse>(user);
// Assert
result.Should().NotBeNull();
result.Id.Should().Be(user.Id);
result.FullName.Should().Be("John Doe");
result.Age.Should().Be(DateTime.Now.Year - 1990);
result.Email.Should().Be(user.Email);
result.IsActive.Should().BeTrue();
}
[Fact]
public void Map_FromUserCreateRequestToUser_ShouldMapCorrectly()
{
// Arrange
var request = new UserCreateRequest
{
FirstName = "John",
LastName = "Doe",
Email = "[email protected]",
Password = "Password123!",
DateOfBirth = new DateTime(1990, 1, 1),
PhoneNumber = "+1234567890",
Address = new AddressCreateRequest
{
Street = "123 Main St",
City = "New York",
State = "NY",
ZipCode = "10001",
Country = "USA"
}
};
// Act
var result = _mapper.Map<User>(request);
// Assert
result.Should().NotBeNull();
result.FirstName.Should().Be(request.FirstName);
result.LastName.Should().Be(request.LastName);
result.Email.Should().Be(request.Email);
result.DateOfBirth.Should().Be(request.DateOfBirth);
result.PhoneNumber.Should().Be(request.PhoneNumber);
result.Address.Should().NotBeNull();
result.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
result.IsActive.Should().BeTrue();
}
}
// Integration tests
public class UserControllerIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
private readonly HttpClient _client;
public UserControllerIntegrationTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
_client = factory.CreateClient();
}
[Fact]
public async Task CreateUser_WithValidRequest_ShouldReturnCreated()
{
// Arrange
var request = new
{
firstName = "John",
lastName = "Doe",
email = "[email protected]",
password = "SecurePassword123!",
confirmPassword = "SecurePassword123!",
dateOfBirth = new DateTime(1990, 1, 1).ToString("yyyy-MM-dd"),
phoneNumber = "+1234567890",
address = new
{
street = "123 Main St",
city = "New York",
state = "NY",
zipCode = "10001",
country = "USA"
},
roles = new[] { "User" }
};
var content = new StringContent(
JsonSerializer.Serialize(request),
Encoding.UTF8,
"application/json");
// Act
var response = await _client.PostAsync("/api/users", content);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.Created);
var responseContent = await response.Content.ReadAsStringAsync();
var userResponse = JsonSerializer.Deserialize<UserResponse>(responseContent, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
userResponse.Should().NotBeNull();
userResponse.FullName.Should().Be("John Doe");
userResponse.Email.Should().Be("[email protected]");
}
[Fact]
public async Task CreateUser_WithInvalidRequest_ShouldReturnBadRequest()
{
// Arrange
var request = new
{
firstName = "", // Invalid: empty first name
lastName = "Doe",
email = "invalid-email", // Invalid email
password = "weak", // Weak password
confirmPassword = "different" // Mismatched passwords
};
var content = new StringContent(
JsonSerializer.Serialize(request),
Encoding.UTF8,
"application/json");
// Act
var response = await _client.PostAsync("/api/users", content);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
var responseContent = await response.Content.ReadAsStringAsync();
responseContent.Should().Contain("validation errors");
}
}
9. Error Handling & Localization
Advanced Error Handling
// Custom exception handling middleware
public class CustomExceptionHandlerMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<CustomExceptionHandlerMiddleware> _logger;
public CustomExceptionHandlerMiddleware(RequestDelegate next, ILogger<CustomExceptionHandlerMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (ValidationException ex)
{
_logger.LogWarning(ex, "Validation exception occurred");
await HandleValidationExceptionAsync(context, ex);
}
catch (AutoMapperMappingException ex)
{
_logger.LogError(ex, "AutoMapper mapping exception occurred");
await HandleMappingExceptionAsync(context, ex);
}
catch (Exception ex)
{
_logger.LogError(ex, "An unhandled exception occurred");
await HandleGenericExceptionAsync(context, ex);
}
}
private static async Task HandleValidationExceptionAsync(HttpContext context, ValidationException exception)
{
var problemDetails = new ValidationProblemDetails
{
Title = "One or more validation errors occurred",
Status = StatusCodes.Status400BadRequest,
Instance = context.Request.Path,
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1"
};
foreach (var error in exception.Errors)
{
if (problemDetails.Errors.ContainsKey(error.PropertyName))
{
var existingErrors = problemDetails.Errors[error.PropertyName].ToList();
existingErrors.Add(error.ErrorMessage);
problemDetails.Errors[error.PropertyName] = existingErrors.ToArray();
}
else
{
problemDetails.Errors[error.PropertyName] = new[] { error.ErrorMessage };
}
}
context.Response.StatusCode = StatusCodes.Status400BadRequest;
context.Response.ContentType = "application/problem+json";
await context.Response.WriteAsJsonAsync(problemDetails);
}
private static async Task HandleMappingExceptionAsync(HttpContext context, AutoMapperMappingException exception)
{
var problemDetails = new ProblemDetails
{
Title = "Data mapping error",
Status = StatusCodes.Status500InternalServerError,
Instance = context.Request.Path,
Type = "https://tools.ietf.org/html/rfc7231#section-6.6.1",
Detail = "An error occurred while processing your request. Please try again later."
};
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
context.Response.ContentType = "application/problem+json";
await context.Response.WriteAsJsonAsync(problemDetails);
}
private static async Task HandleGenericExceptionAsync(HttpContext context, Exception exception)
{
var problemDetails = new ProblemDetails
{
Title = "An error occurred",
Status = StatusCodes.Status500InternalServerError,
Instance = context.Request.Path,
Type = "https://tools.ietf.org/html/rfc7231#section-6.6.1",
Detail = "An unexpected error occurred. Please try again later."
};
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
context.Response.ContentType = "application/problem+json";
await context.Response.WriteAsJsonAsync(problemDetails);
}
}
// Localized validators
public class LocalizedUserCreateRequestValidator : AbstractValidator<UserCreateRequest>
{
public LocalizedUserCreateRequestValidator(IStringLocalizer<LocalizedUserCreateRequestValidator> localizer)
{
RuleFor(x => x.FirstName)
.NotEmpty().WithMessage(localizer["FirstNameRequired"])
.Length(2, 50).WithMessage(localizer["FirstNameLength", 2, 50])
.Matches(@"^[a-zA-Z\s\-']+$").WithMessage(localizer["FirstNameInvalidCharacters"]);
RuleFor(x => x.LastName)
.NotEmpty().WithMessage(localizer["LastNameRequired"])
.Length(2, 50).WithMessage(localizer["LastNameLength", 2, 50])
.Matches(@"^[a-zA-Z\s\-']+$").WithMessage(localizer["LastNameInvalidCharacters"]);
RuleFor(x => x.Email)
.NotEmpty().WithMessage(localizer["EmailRequired"])
.EmailAddress().WithMessage(localizer["EmailInvalid"])
.Must(BeUniqueEmail).WithMessage(localizer["EmailAlreadyExists"]);
RuleFor(x => x.Password)
.NotEmpty().WithMessage(localizer["PasswordRequired"])
.MinimumLength(8).WithMessage(localizer["PasswordMinLength", 8])
.Matches(@"[A-Z]").WithMessage(localizer["PasswordUppercaseRequired"])
.Matches(@"[a-z]").WithMessage(localizer["PasswordLowercaseRequired"])
.Matches(@"\d").WithMessage(localizer["PasswordDigitRequired"])
.Matches(@"[!@#$%^&*()_+\-=\[\]{};':""\\|,.<>\/?]")
.WithMessage(localizer["PasswordSpecialCharacterRequired"]);
}
private bool BeUniqueEmail(string email)
{
// Implementation for checking unique email
return true;
}
}
10. Production Deployment & Monitoring
Production Configuration
// Program.cs - Production setup
using FluentValidation;
using AutoMapper;
var builder = WebApplication.CreateBuilder(args);
// Configuration
builder.Configuration
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
// FluentValidation
builder.Services.AddFluentValidationAutoValidation();
builder.Services.AddFluentValidationClientsideAdapters();
builder.Services.AddValidatorsFromAssemblyContaining<Program>();
// AutoMapper
builder.Services.AddAutoMapper(typeof(Program));
// API Controllers
builder.Services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
options.SuppressModelStateInvalidFilter = true; // Let FluentValidation handle validation
options.SuppressMapClientErrors = true;
});
// Swagger/OpenAPI
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo
{
Title = "E-Commerce API",
Version = "v1",
Description = "E-Commerce API with FluentValidation and AutoMapper"
});
// Add validation support to Swagger
options.OperationFilter<ValidationOperationFilter>();
});
// Health checks
builder.Services.AddHealthChecks()
.AddDbContextCheck<ApplicationDbContext>()
.AddUrlGroup(new Uri("https://api.example.com/health"), "External API");
// Caching
builder.Services.AddMemoryCache();
// HTTP client
builder.Services.AddHttpClient();
var app = builder.Build();
// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
// Custom middleware
app.UseMiddleware<CustomExceptionHandlerMiddleware>();
app.UseMiddleware<RequestLoggingMiddleware>();
app.MapControllers();
app.MapHealthChecks("/health");
app.Run();
// Swagger operation filter for validation
public class ValidationOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var parameters = operation.Parameters;
if (parameters == null)
return;
foreach (var parameter in parameters)
{
var hasValidator = context.MethodInfo.GetParameters()
.Any(p => p.Name == parameter.Name &&
p.ParameterType.GetCustomAttributes(typeof(ValidatorAttribute), true).Any());
if (hasValidator)
{
parameter.Description = "This parameter is validated using FluentValidation rules";
}
}
}
}
Monitoring and Logging
// Request logging middleware
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestLoggingMiddleware> _logger;
public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
var stopwatch = Stopwatch.StartNew();
var correlationId = context.TraceIdentifier;
using (_logger.BeginScope(new Dictionary<string, object>
{
["CorrelationId"] = correlationId,
["RequestPath"] = context.Request.Path,
["RequestMethod"] = context.Request.Method
}))
{
_logger.LogInformation("Starting request {Method} {Path}",
context.Request.Method, context.Request.Path);
try
{
await _next(context);
stopwatch.Stop();
_logger.LogInformation("Completed request {Method} {Path} with status {StatusCode} in {ElapsedMs}ms",
context.Request.Method, context.Request.Path, context.Response.StatusCode, stopwatch.ElapsedMilliseconds);
}
catch (Exception ex)
{
stopwatch.Stop();
_logger.LogError(ex, "Request {Method} {Path} failed after {ElapsedMs}ms",
context.Request.Method, context.Request.Path, stopwatch.ElapsedMilliseconds);
throw;
}
}
}
}
// Performance monitoring
public class MappingPerformanceService
{
private readonly ILogger<MappingPerformanceService> _logger;
private readonly Stopwatch _stopwatch;
public MappingPerformanceService(ILogger<MappingPerformanceService> logger)
{
_logger = logger;
_stopwatch = new Stopwatch();
}
public TDestination MapWithPerformanceLogging<TSource, TDestination>(TSource source, IMapper mapper)
{
_stopwatch.Restart();
try
{
var result = mapper.Map<TDestination>(source);
return result;
}
finally
{
_stopwatch.Stop();
if (_stopwatch.ElapsedMilliseconds > 100) // Log slow mappings
{
_logger.LogWarning("Slow mapping detected: {SourceType} to {DestinationType} took {ElapsedMs}ms",
typeof(TSource).Name, typeof(TDestination).Name, _stopwatch.ElapsedMilliseconds);
}
}
}
}
This comprehensive guide provides everything needed to master FluentValidation and AutoMapper in ASP.NET Core applications. From basic setup to advanced enterprise patterns, you'll learn how to build robust, maintainable, and efficient applications with clean data validation and seamless object mapping. The real-world examples and best practices ensure your applications are production-ready and follow industry standards.