ASP.NET Core  

ASP.NET Core FluentValidation & AutoMapper Guide | Clean Data & Robust APIs (Part-38 of 40)

FluentValidation

Previous article: Master Repository & Unit of Work Patterns in ASP.NET Core (Part-37 of 40)

Table of Contents

  1. The Foundation of Clean Data in Web Applications

  2. FluentValidation Deep Dive

  3. Advanced Validation Scenarios

  4. AutoMapper Fundamentals

  5. Advanced Mapping Techniques

  6. Integration Patterns & Best Practices

  7. Real-World Enterprise Application

  8. Performance Optimization & Testing

  9. Error Handling & Localization

  10. 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

  • SQL injection attacks

  • Cross-site scripting (XSS)

  • Data corruption

  • Business logic bypass

User Experience Issues

  • Confusing error messages

  • Data loss

  • Inconsistent application behavior

  • Poor performance

Business Impact

  • Incorrect business decisions

  • Financial losses

  • Compliance violations

  • Damaged reputation

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.