Skip to content

C# Development Best Practices

Overview

This document provides comprehensive C# development best practices for the Dispatch Center Application, covering coding standards, security, logging, alerting, deployment, and integration with the overall system architecture.

Table of Contents

Coding Standards

Naming Conventions

Class and Method Naming

// ✅ Good - PascalCase for classes, methods, properties
public class ServiceRequestManager
{
    public async Task<ServiceRequest> CreateServiceRequestAsync(CreateServiceRequestCommand command)
    {
        // Implementation
    }

    public bool IsValidCustomer { get; set; }
}

// ✅ Good - camelCase for fields, local variables, parameters
private readonly ILogger<ServiceRequestManager> _logger;
private readonly IServiceRequestRepository _serviceRequestRepository;

public async Task ProcessRequestAsync(string customerId, int priority)
{
    var serviceRequest = new ServiceRequest();
    var validationResult = await ValidateRequestAsync(serviceRequest);
}

Interface and Abstract Class Conventions

// ✅ Good - Interface naming with 'I' prefix
public interface IServiceRequestRepository
{
    Task<ServiceRequest> GetByIdAsync(int id);
    Task<IEnumerable<ServiceRequest>> GetByCustomerIdAsync(string customerId);
}

// ✅ Good - Abstract base class naming
public abstract class BaseRepository<T> where T : class
{
    protected readonly DbContext Context;

    protected BaseRepository(DbContext context)
    {
        Context = context ?? throw new ArgumentNullException(nameof(context));
    }
}

Code Organization

File and Namespace Structure

// ✅ Good - Clear namespace hierarchy
namespace DispatchCenter.Core.Domain.ServiceRequests
{
    public class ServiceRequest
    {
        // Domain entity implementation
    }
}

namespace DispatchCenter.Infrastructure.Data.Repositories
{
    public class ServiceRequestRepository : IServiceRequestRepository
    {
        // Repository implementation
    }
}

namespace DispatchCenter.Api.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class ServiceRequestsController : ControllerBase
    {
        // API controller implementation
    }
}

Dependency Injection Registration

// ✅ Good - Organized service registration
public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddApplicationServices(this IServiceCollection services)
    {
        // Domain services
        services.AddScoped<IServiceRequestService, ServiceRequestService>();
        services.AddScoped<ITechnicianService, TechnicianService>();
        services.AddScoped<ICustomerService, CustomerService>();

        // Infrastructure services
        services.AddScoped<IServiceRequestRepository, ServiceRequestRepository>();
        services.AddScoped<ITechnicianRepository, TechnicianRepository>();

        // Integration services - see [Integration Patterns](./INTEGRATION_PATTERNS.md)
        services.AddHttpClient<IReachApiService, ReachApiService>();
        services.AddScoped<IGeoTabService, GeoTabService>();

        return services;
    }
}

Method Design Principles

Single Responsibility Principle

// ✅ Good - Single responsibility
public class ServiceRequestValidator
{
    public ValidationResult ValidateServiceRequest(ServiceRequest request)
    {
        var result = new ValidationResult();

        ValidateCustomerInformation(request.Customer, result);
        ValidateServiceDetails(request.ServiceDetails, result);
        ValidatePriority(request.Priority, result);

        return result;
    }

    private void ValidateCustomerInformation(Customer customer, ValidationResult result)
    {
        if (customer == null)
            result.AddError("Customer information is required");

        if (string.IsNullOrWhiteSpace(customer.Email))
            result.AddError("Customer email is required");
    }
}

Async/Await Best Practices

// ✅ Good - Proper async implementation
public class ServiceRequestService
{
    public async Task<ServiceRequest> CreateServiceRequestAsync(CreateServiceRequestCommand command)
    {
        // Validate input
        var validationResult = await ValidateCommandAsync(command);
        if (!validationResult.IsValid)
            throw new ValidationException(validationResult.Errors);

        // Create entity
        var serviceRequest = new ServiceRequest
        {
            CustomerId = command.CustomerId,
            Description = command.Description,
            Priority = command.Priority,
            CreatedDate = DateTime.UtcNow
        };

        // Save to database
        await _repository.AddAsync(serviceRequest);
        await _unitOfWork.SaveChangesAsync();

        // Publish event - see [Integration Patterns](./INTEGRATION_PATTERNS.md)
        await _eventPublisher.PublishAsync(new ServiceRequestCreatedEvent(serviceRequest.Id));

        return serviceRequest;
    }
}

// ❌ Bad - Blocking async calls
public ServiceRequest CreateServiceRequest(CreateServiceRequestCommand command)
{
    return CreateServiceRequestAsync(command).Result; // Don't do this!
}

Project Structure

DispatchCenter.sln
├── src/
│   ├── DispatchCenter.Domain/           # Domain entities and interfaces
│   ├── DispatchCenter.Application/      # Application services and DTOs
│   ├── DispatchCenter.Infrastructure/   # Data access and external services
│   ├── DispatchCenter.Api/             # Web API controllers
│   └── DispatchCenter.Web/             # MVC web application (if needed)
├── tests/
│   ├── DispatchCenter.UnitTests/       # Unit tests
│   ├── DispatchCenter.IntegrationTests/# Integration tests
│   └── DispatchCenter.EndToEndTests/   # E2E tests
├── docs/                               # Documentation
└── scripts/                            # Build and deployment scripts

Clean Architecture Implementation

// Domain Layer - Core business entities
namespace DispatchCenter.Domain.Entities
{
    public class ServiceRequest : BaseEntity
    {
        public string CustomerId { get; set; }
        public string Description { get; set; }
        public ServiceRequestStatus Status { get; set; }
        public Priority Priority { get; set; }
        public DateTime RequestedDate { get; set; }
        public DateTime? ScheduledDate { get; set; }
        public string AssignedTechnicianId { get; set; }

        // Domain methods
        public void AssignTechnician(string technicianId)
        {
            if (string.IsNullOrWhiteSpace(technicianId))
                throw new DomainException("Technician ID cannot be empty");

            AssignedTechnicianId = technicianId;
            Status = ServiceRequestStatus.Assigned;
            UpdatedDate = DateTime.UtcNow;
        }
    }
}

// Application Layer - Use cases and business logic
namespace DispatchCenter.Application.Services
{
    public class ServiceRequestService : IServiceRequestService
    {
        private readonly IServiceRequestRepository _repository;
        private readonly ILogger<ServiceRequestService> _logger;

        public ServiceRequestService(
            IServiceRequestRepository repository,
            ILogger<ServiceRequestService> logger)
        {
            _repository = repository;
            _logger = logger;
        }

        public async Task<ServiceRequestDto> AssignTechnicianAsync(int serviceRequestId, string technicianId)
        {
            using var activity = _logger.BeginScope("Assigning technician {TechnicianId} to service request {ServiceRequestId}", 
                technicianId, serviceRequestId);

            var serviceRequest = await _repository.GetByIdAsync(serviceRequestId);
            if (serviceRequest == null)
                throw new NotFoundException($"Service request {serviceRequestId} not found");

            serviceRequest.AssignTechnician(technicianId);
            await _repository.UpdateAsync(serviceRequest);

            _logger.LogInformation("Successfully assigned technician {TechnicianId} to service request {ServiceRequestId}", 
                technicianId, serviceRequestId);

            return _mapper.Map<ServiceRequestDto>(serviceRequest);
        }
    }
}

Security Best Practices

For comprehensive security guidelines, see Security Documentation

Input Validation and Sanitization

Data Transfer Objects (DTOs) with Validation

public class CreateServiceRequestDto
{
    [Required(ErrorMessage = "Customer ID is required")]
    [StringLength(50, ErrorMessage = "Customer ID cannot exceed 50 characters")]
    public string CustomerId { get; set; }

    [Required(ErrorMessage = "Description is required")]
    [StringLength(2000, MinimumLength = 10, ErrorMessage = "Description must be between 10 and 2000 characters")]
    [AntiXss] // Custom validation attribute
    public string Description { get; set; }

    [Range(1, 5, ErrorMessage = "Priority must be between 1 and 5")]
    public int Priority { get; set; }

    [EmailAddress(ErrorMessage = "Invalid email format")]
    public string ContactEmail { get; set; }

    [Phone(ErrorMessage = "Invalid phone number format")]
    public string ContactPhone { get; set; }
}

// Custom validation attribute for XSS prevention
public class AntiXssAttribute : ValidationAttribute
{
    public override bool IsValid(object value)
    {
        if (value is string stringValue)
        {
            return !ContainsMaliciousContent(stringValue);
        }
        return true;
    }

    private bool ContainsMaliciousContent(string input)
    {
        var maliciousPatterns = new[]
        {
            @"<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>",
            @"javascript:",
            @"vbscript:",
            @"onload\s*=",
            @"onerror\s*="
        };

        return maliciousPatterns.Any(pattern => 
            Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase));
    }
}

SQL Injection Prevention

// ✅ Good - Parameterized queries with Entity Framework
public async Task<IEnumerable<ServiceRequest>> GetServiceRequestsByCustomerAsync(string customerId)
{
    return await _context.ServiceRequests
        .Where(sr => sr.CustomerId == customerId)
        .Include(sr => sr.Customer)
        .Include(sr => sr.AssignedTechnician)
        .ToListAsync();
}

// ✅ Good - Raw SQL with parameters (when necessary)
public async Task<IEnumerable<ServiceRequest>> GetServiceRequestsByStatusAsync(string status)
{
    return await _context.ServiceRequests
        .FromSqlRaw("SELECT * FROM ServiceRequests WHERE Status = {0}", status)
        .ToListAsync();
}

// ❌ Bad - String concatenation (vulnerable to SQL injection)
public async Task<IEnumerable<ServiceRequest>> GetServiceRequestsByStatusBad(string status)
{
    var sql = $"SELECT * FROM ServiceRequests WHERE Status = '{status}'"; // Don't do this!
    return await _context.ServiceRequests.FromSqlRaw(sql).ToListAsync();
}

Authentication and Authorization

JWT Token Validation

[Authorize(Policy = "RequireDispatcherRole")]
[HttpPost("assign-technician")]
public async Task<ActionResult<ServiceRequestDto>> AssignTechnician(
    [FromBody] AssignTechnicianDto dto)
{
    var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
    var userRoles = User.FindAll(ClaimTypes.Role).Select(c => c.Value);

    _logger.LogInformation("User {UserId} with roles {Roles} attempting to assign technician", 
        userId, string.Join(",", userRoles));

    var result = await _serviceRequestService.AssignTechnicianAsync(dto.ServiceRequestId, dto.TechnicianId);
    return Ok(result);
}

// Policy configuration in Startup.cs
services.AddAuthorization(options =>
{
    options.AddPolicy("RequireDispatcherRole", policy =>
        policy.RequireRole("Dispatcher", "Admin"));

    options.AddPolicy("RequireServiceRequestAccess", policy =>
        policy.RequireAuthenticatedUser()
              .RequireAssertion(context =>
                  context.User.HasClaim("permission", "service_requests.read") ||
                  context.User.IsInRole("Admin")));
});

Secrets Management

Azure Key Vault Integration

public class KeyVaultConfiguration
{
    public static void ConfigureKeyVault(IConfigurationBuilder config, IHostEnvironment env)
    {
        if (!env.IsDevelopment())
        {
            var builtConfig = config.Build();
            var keyVaultName = builtConfig["KeyVaultName"];
            var keyVaultUri = $"https://{keyVaultName}.vault.azure.net/";

            config.AddAzureKeyVault(keyVaultUri, new DefaultAzureCredential());
        }
    }
}

// Service configuration with secrets
public class ExternalApiConfiguration
{
    [Required]
    public string ReachApiBaseUrl { get; set; }

    [Required]
    public string ReachApiKey { get; set; } // Retrieved from Key Vault

    [Required] 
    public string GeoTabUsername { get; set; } // Retrieved from Key Vault

    [Required]
    public string GeoTabPassword { get; set; } // Retrieved from Key Vault
}

Logging Implementation

For comprehensive logging guidelines, see Logging Documentation

Serilog Configuration

Structured Logging Setup

public static class LoggingConfiguration
{
    public static IHostBuilder ConfigureLogging(this IHostBuilder builder)
    {
        return builder.UseSerilog((context, configuration) =>
        {
            configuration
                .ReadFrom.Configuration(context.Configuration)
                .Enrich.FromLogContext()
                .Enrich.WithMachineName()
                .Enrich.WithEnvironmentName()
                .Enrich.WithProperty("Application", "DispatchCenter")
                .WriteTo.Console(new JsonFormatter())
                .WriteTo.ApplicationInsights(TelemetryConfiguration.Active, TelemetryConverter.Traces);

            if (context.HostingEnvironment.IsDevelopment())
            {
                configuration.WriteTo.Debug();
            }
        });
    }
}

Logging Best Practices

public class ServiceRequestService
{
    private readonly ILogger<ServiceRequestService> _logger;

    public async Task<ServiceRequest> CreateServiceRequestAsync(CreateServiceRequestCommand command)
    {
        using var scope = _logger.BeginScope("Creating service request for customer {CustomerId}", command.CustomerId);

        try
        {
            _logger.LogInformation("Starting service request creation with priority {Priority}", command.Priority);

            // Validate command
            var validationResult = await _validator.ValidateAsync(command);
            if (!validationResult.IsValid)
            {
                _logger.LogWarning("Service request validation failed for customer {CustomerId}: {Errors}", 
                    command.CustomerId, validationResult.Errors);
                throw new ValidationException(validationResult.Errors);
            }

            // Create service request
            var serviceRequest = _mapper.Map<ServiceRequest>(command);
            serviceRequest.Id = await _repository.AddAsync(serviceRequest);

            _logger.LogInformation("Successfully created service request {ServiceRequestId} for customer {CustomerId}", 
                serviceRequest.Id, command.CustomerId);

            // Publish domain event - see [Integration Patterns](./INTEGRATION_PATTERNS.md)
            await _eventPublisher.PublishAsync(new ServiceRequestCreatedEvent(serviceRequest));

            return serviceRequest;
        }
        catch (ValidationException ex)
        {
            _logger.LogWarning(ex, "Validation failed for service request creation");
            throw;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Unexpected error creating service request for customer {CustomerId}", command.CustomerId);
            throw;
        }
    }
}

Performance Logging

Method Execution Timing

public class PerformanceLoggingAttribute : ActionFilterAttribute
{
    private readonly ILogger<PerformanceLoggingAttribute> _logger;

    public PerformanceLoggingAttribute(ILogger<PerformanceLoggingAttribute> logger)
    {
        _logger = logger;
    }

    public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        var stopwatch = Stopwatch.StartNew();
        var actionName = context.ActionDescriptor.DisplayName;

        _logger.LogDebug("Starting execution of {ActionName}", actionName);

        var result = await next();

        stopwatch.Stop();

        if (result.Exception != null)
        {
            _logger.LogWarning("Action {ActionName} completed with exception in {ElapsedMs}ms", 
                actionName, stopwatch.ElapsedMilliseconds);
        }
        else
        {
            _logger.LogInformation("Action {ActionName} completed successfully in {ElapsedMs}ms", 
                actionName, stopwatch.ElapsedMilliseconds);
        }
    }
}

Error Handling and Alerting

For comprehensive alerting guidelines, see Alerting Documentation

Global Exception Handling

Exception Middleware

public class GlobalExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<GlobalExceptionMiddleware> _logger;

    public GlobalExceptionMiddleware(RequestDelegate next, ILogger<GlobalExceptionMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Unhandled exception occurred. Request: {Method} {Path}", 
                context.Request.Method, context.Request.Path);

            await HandleExceptionAsync(context, ex);
        }
    }

    private async Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        context.Response.ContentType = "application/json";

        var response = new ErrorResponse();

        switch (exception)
        {
            case ValidationException validationEx:
                response.Message = "Validation failed";
                response.Details = validationEx.Errors.Select(e => e.ErrorMessage).ToList();
                context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
                break;

            case NotFoundException notFoundEx:
                response.Message = notFoundEx.Message;
                context.Response.StatusCode = (int)HttpStatusCode.NotFound;
                break;

            case UnauthorizedAccessException unauthorizedEx:
                response.Message = "Access denied";
                context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
                break;

            default:
                response.Message = "An error occurred while processing your request";
                context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;

                // Alert on unhandled exceptions - see [Alerting Documentation](./ALERTING.md)
                await NotifyAlertingSystemAsync(exception, context);
                break;
        }

        response.TraceId = Activity.Current?.Id ?? context.TraceIdentifier;

        var jsonResponse = JsonSerializer.Serialize(response);
        await context.Response.WriteAsync(jsonResponse);
    }

    private async Task NotifyAlertingSystemAsync(Exception exception, HttpContext context)
    {
        // Implementation depends on alerting system - see [Alerting Documentation](./ALERTING.md)
        // Could use Application Insights, custom alerting service, etc.
    }
}

Custom Exception Types

public abstract class DomainException : Exception
{
    protected DomainException(string message) : base(message) { }
    protected DomainException(string message, Exception innerException) : base(message, innerException) { }
}

public class ServiceRequestNotFoundException : DomainException
{
    public ServiceRequestNotFoundException(int serviceRequestId) 
        : base($"Service request with ID {serviceRequestId} was not found")
    {
        ServiceRequestId = serviceRequestId;
    }

    public int ServiceRequestId { get; }
}

public class TechnicianNotAvailableException : DomainException
{
    public TechnicianNotAvailableException(string technicianId, DateTime requestedDate)
        : base($"Technician {technicianId} is not available on {requestedDate:yyyy-MM-dd}")
    {
        TechnicianId = technicianId;
        RequestedDate = requestedDate;
    }

    public string TechnicianId { get; }
    public DateTime RequestedDate { get; }
}

Circuit Breaker Pattern

public class CircuitBreakerService<T>
{
    private readonly CircuitBreakerPolicy _circuitBreaker;
    private readonly ILogger<CircuitBreakerService<T>> _logger;

    public CircuitBreakerService(ILogger<CircuitBreakerService<T>> logger)
    {
        _logger = logger;
        _circuitBreaker = Policy
            .Handle<HttpRequestException>()
            .Or<TaskCanceledException>()
            .CircuitBreakerAsync(
                handledEventsAllowedBeforeBreaking: 5,
                durationOfBreak: TimeSpan.FromMinutes(1),
                onBreak: OnBreak,
                onReset: OnReset);
    }

    public async Task<TResult> ExecuteAsync<TResult>(Func<Task<TResult>> operation)
    {
        return await _circuitBreaker.ExecuteAsync(operation);
    }

    private void OnBreak(Exception exception, TimeSpan duration)
    {
        _logger.LogWarning("Circuit breaker opened for {Duration} due to {ExceptionType}: {Message}", 
            duration, exception.GetType().Name, exception.Message);
    }

    private void OnReset()
    {
        _logger.LogInformation("Circuit breaker reset - service calls resumed");
    }
}

Performance Optimization

Caching Strategies

Memory Caching

public class CustomerService
{
    private readonly ICustomerRepository _repository;
    private readonly IMemoryCache _cache;
    private readonly ILogger<CustomerService> _logger;

    public async Task<Customer> GetCustomerAsync(string customerId)
    {
        var cacheKey = $"customer_{customerId}";

        if (_cache.TryGetValue(cacheKey, out Customer cachedCustomer))
        {
            _logger.LogDebug("Customer {CustomerId} retrieved from cache", customerId);
            return cachedCustomer;
        }

        var customer = await _repository.GetByIdAsync(customerId);
        if (customer != null)
        {
            var cacheOptions = new MemoryCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(15),
                SlidingExpiration = TimeSpan.FromMinutes(5),
                Priority = CacheItemPriority.Normal
            };

            _cache.Set(cacheKey, customer, cacheOptions);
            _logger.LogDebug("Customer {CustomerId} cached for 15 minutes", customerId);
        }

        return customer;
    }
}

Distributed Caching with Redis

public class DistributedCacheService
{
    private readonly IDistributedCache _distributedCache;
    private readonly ILogger<DistributedCacheService> _logger;

    public async Task<T> GetAsync<T>(string key) where T : class
    {
        var cachedValue = await _distributedCache.GetStringAsync(key);
        if (cachedValue != null)
        {
            _logger.LogDebug("Cache hit for key {CacheKey}", key);
            return JsonSerializer.Deserialize<T>(cachedValue);
        }

        _logger.LogDebug("Cache miss for key {CacheKey}", key);
        return null;
    }

    public async Task SetAsync<T>(string key, T value, TimeSpan expiration) where T : class
    {
        var options = new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = expiration
        };

        var serializedValue = JsonSerializer.Serialize(value);
        await _distributedCache.SetStringAsync(key, serializedValue, options);

        _logger.LogDebug("Cached value for key {CacheKey} with expiration {Expiration}", key, expiration);
    }
}

Database Optimization

Entity Framework Performance

public class ServiceRequestRepository
{
    private readonly DispatchCenterDbContext _context;

    public async Task<IEnumerable<ServiceRequest>> GetActiveServiceRequestsAsync()
    {
        return await _context.ServiceRequests
            .Where(sr => sr.Status == ServiceRequestStatus.Open || sr.Status == ServiceRequestStatus.Assigned)
            .Include(sr => sr.Customer)
            .Include(sr => sr.AssignedTechnician)
            .OrderBy(sr => sr.Priority)
            .ThenBy(sr => sr.RequestedDate)
            .AsNoTracking() // Read-only queries
            .ToListAsync();
    }

    public async Task<PagedResult<ServiceRequest>> GetServiceRequestsPagedAsync(int page, int pageSize)
    {
        var query = _context.ServiceRequests
            .Include(sr => sr.Customer)
            .OrderByDescending(sr => sr.RequestedDate);

        var totalCount = await query.CountAsync();
        var items = await query
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .AsNoTracking()
            .ToListAsync();

        return new PagedResult<ServiceRequest>(items, totalCount, page, pageSize);
    }
}

Testing Standards

Unit Testing Best Practices

Test Structure and Naming

[TestFixture]
public class ServiceRequestServiceTests
{
    private Mock<IServiceRequestRepository> _mockRepository;
    private Mock<ILogger<ServiceRequestService>> _mockLogger;
    private Mock<IEventPublisher> _mockEventPublisher;
    private ServiceRequestService _service;

    [SetUp]
    public void Setup()
    {
        _mockRepository = new Mock<IServiceRequestRepository>();
        _mockLogger = new Mock<ILogger<ServiceRequestService>>();
        _mockEventPublisher = new Mock<IEventPublisher>();
        _service = new ServiceRequestService(_mockRepository.Object, _mockLogger.Object, _mockEventPublisher.Object);
    }

    [Test]
    public async Task CreateServiceRequestAsync_WithValidCommand_ShouldReturnServiceRequest()
    {
        // Arrange
        var command = new CreateServiceRequestCommand
        {
            CustomerId = "CUST001",
            Description = "Equipment needs repair",
            Priority = Priority.High
        };

        var expectedServiceRequest = new ServiceRequest
        {
            Id = 1,
            CustomerId = command.CustomerId,
            Description = command.Description,
            Priority = command.Priority
        };

        _mockRepository.Setup(r => r.AddAsync(It.IsAny<ServiceRequest>()))
            .ReturnsAsync(expectedServiceRequest.Id);

        // Act
        var result = await _service.CreateServiceRequestAsync(command);

        // Assert
        Assert.That(result, Is.Not.Null);
        Assert.That(result.CustomerId, Is.EqualTo(command.CustomerId));
        Assert.That(result.Description, Is.EqualTo(command.Description));

        _mockRepository.Verify(r => r.AddAsync(It.IsAny<ServiceRequest>()), Times.Once);
        _mockEventPublisher.Verify(p => p.PublishAsync(It.IsAny<ServiceRequestCreatedEvent>()), Times.Once);
    }

    [Test]
    public void CreateServiceRequestAsync_WithNullCommand_ShouldThrowArgumentNullException()
    {
        // Act & Assert
        Assert.ThrowsAsync<ArgumentNullException>(() => _service.CreateServiceRequestAsync(null));
    }
}

Integration Testing

Web API Integration Tests

[TestFixture]
public class ServiceRequestsControllerIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;
    private readonly HttpClient _client;

    public ServiceRequestsControllerIntegrationTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
        _client = _factory.CreateClient();
    }

    [Test]
    public async Task GetServiceRequests_ShouldReturnOkWithServiceRequests()
    {
        // Arrange
        await SeedTestDataAsync();

        // Act
        var response = await _client.GetAsync("/api/servicerequests");

        // Assert
        response.EnsureSuccessStatusCode();
        var content = await response.Content.ReadAsStringAsync();
        var serviceRequests = JsonSerializer.Deserialize<List<ServiceRequestDto>>(content);

        Assert.That(serviceRequests, Is.Not.Null);
        Assert.That(serviceRequests.Count, Is.GreaterThan(0));
    }

    private async Task SeedTestDataAsync()
    {
        using var scope = _factory.Services.CreateScope();
        var context = scope.ServiceProvider.GetRequiredService<DispatchCenterDbContext>();

        if (!await context.ServiceRequests.AnyAsync())
        {
            context.ServiceRequests.AddRange(new[]
            {
                new ServiceRequest { CustomerId = "CUST001", Description = "Test request 1", Priority = Priority.High },
                new ServiceRequest { CustomerId = "CUST002", Description = "Test request 2", Priority = Priority.Medium }
            });

            await context.SaveChangesAsync();
        }
    }
}

Deployment Best Practices

For comprehensive deployment guidelines, see Integration Documentation and Security Documentation

Configuration Management

Environment-Specific Configuration

// appsettings.json
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=DispatchCenter;Trusted_Connection=true;"
  },
  "ExternalApis": {
    "Reach": {
      "BaseUrl": "https://api.reach.com",
      "TimeoutSeconds": 30
    },
    "GeoTab": {
      "BaseUrl": "https://my.geotab.com",
      "TimeoutSeconds": 15
    }
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}

// appsettings.Production.json
{
  "ConnectionStrings": {
    "DefaultConnection": "" // Set via environment variable or Key Vault
  },
  "ExternalApis": {
    "Reach": {
      "BaseUrl": "https://prod-api.reach.com",
      "ApiKey": "" // Retrieved from Key Vault
    }
  },
  "Logging": {
    "LogLevel": {
      "Default": "Warning",
      "DispatchCenter": "Information"
    }
  }
}

Health Checks

public static class HealthCheckExtensions
{
    public static IServiceCollection AddCustomHealthChecks(this IServiceCollection services, IConfiguration configuration)
    {
        services.AddHealthChecks()
            .AddSqlServer(
                connectionString: configuration.GetConnectionString("DefaultConnection"),
                name: "sql-server",
                tags: new[] { "database", "sql" })
            .AddUrlGroup(
                uri: new Uri(configuration["ExternalApis:Reach:BaseUrl"]),
                name: "reach-api",
                tags: new[] { "external", "api" })
            .AddUrlGroup(
                uri: new Uri(configuration["ExternalApis:GeoTab:BaseUrl"]),
                name: "geotab-api",
                tags: new[] { "external", "api" });

        return services;
    }
}

[ApiController]
[Route("api/[controller]")]
public class HealthController : ControllerBase
{
    private readonly HealthCheckService _healthCheckService;

    [HttpGet]
    public async Task<IActionResult> Get()
    {
        var healthReport = await _healthCheckService.CheckHealthAsync();

        var response = new
        {
            Status = healthReport.Status.ToString(),
            TotalDuration = healthReport.TotalDuration,
            Checks = healthReport.Entries.Select(e => new
            {
                Name = e.Key,
                Status = e.Value.Status.ToString(),
                Duration = e.Value.Duration,
                Description = e.Value.Description
            })
        };

        return Ok(response);
    }
}

Docker Configuration

Dockerfile

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

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["src/DispatchCenter.Api/DispatchCenter.Api.csproj", "src/DispatchCenter.Api/"]
COPY ["src/DispatchCenter.Application/DispatchCenter.Application.csproj", "src/DispatchCenter.Application/"]
COPY ["src/DispatchCenter.Domain/DispatchCenter.Domain.csproj", "src/DispatchCenter.Domain/"]
COPY ["src/DispatchCenter.Infrastructure/DispatchCenter.Infrastructure.csproj", "src/DispatchCenter.Infrastructure/"]

RUN dotnet restore "src/DispatchCenter.Api/DispatchCenter.Api.csproj"
COPY . .
WORKDIR "/src/src/DispatchCenter.Api"
RUN dotnet build "DispatchCenter.Api.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "DispatchCenter.Api.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .

# Create non-root user for security
RUN adduser --disabled-password --gecos '' appuser && chown -R appuser /app
USER appuser

ENTRYPOINT ["dotnet", "DispatchCenter.Api.dll"]

Integration Guidelines

For detailed integration patterns, see Integration Patterns Documentation

HTTP Client Configuration

Resilient HTTP Clients

public static class HttpClientExtensions
{
    public static IServiceCollection AddResilientHttpClients(this IServiceCollection services, IConfiguration configuration)
    {
        // Reach API client - see [Integration Patterns](./INTEGRATION_PATTERNS.md)
        services.AddHttpClient<IReachApiService, ReachApiService>(client =>
        {
            client.BaseAddress = new Uri(configuration["ExternalApis:Reach:BaseUrl"]);
            client.DefaultRequestHeaders.Add("Accept", "application/json");
            client.Timeout = TimeSpan.FromSeconds(30);
        })
        .AddPolicyHandler(GetRetryPolicy())
        .AddPolicyHandler(GetCircuitBreakerPolicy());

        // GeoTab API client
        services.AddHttpClient<IGeoTabService, GeoTabService>(client =>
        {
            client.BaseAddress = new Uri(configuration["ExternalApis:GeoTab:BaseUrl"]);
            client.Timeout = TimeSpan.FromSeconds(15);
        })
        .AddPolicyHandler(GetRetryPolicy())
        .AddPolicyHandler(GetCircuitBreakerPolicy());

        return services;
    }

    private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
    {
        return HttpPolicyExtensions
            .HandleTransientHttpError()
            .WaitAndRetryAsync(
                retryCount: 3,
                sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
                onRetry: (outcome, timespan, retryCount, context) =>
                {
                    var logger = context.GetLogger();
                    logger?.LogWarning("Retry {RetryCount} for {OperationKey} in {Delay}ms", 
                        retryCount, context.OperationKey, timespan.TotalMilliseconds);
                });
    }

    private static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
    {
        return HttpPolicyExtensions
            .HandleTransientHttpError()
            .CircuitBreakerAsync(
                handledEventsAllowedBeforeBreaking: 5,
                durationOfBreak: TimeSpan.FromSeconds(30));
    }
}

Service Bus Integration

Message Publishing

public class ServiceBusEventPublisher : IEventPublisher
{
    private readonly ServiceBusClient _serviceBusClient;
    private readonly ILogger<ServiceBusEventPublisher> _logger;

    public async Task PublishAsync<T>(T eventData) where T : class
    {
        var topicName = GetTopicName(typeof(T));
        var sender = _serviceBusClient.CreateSender(topicName);

        var message = new ServiceBusMessage(JsonSerializer.Serialize(eventData))
        {
            Subject = typeof(T).Name,
            MessageId = Guid.NewGuid().ToString(),
            TimeToLive = TimeSpan.FromHours(24),
            ContentType = "application/json"
        };

        // Add correlation properties
        message.ApplicationProperties["CorrelationId"] = Activity.Current?.Id ?? Guid.NewGuid().ToString();
        message.ApplicationProperties["PublishedAt"] = DateTimeOffset.UtcNow;

        try
        {
            await sender.SendMessageAsync(message);
            _logger.LogInformation("Published event {EventType} with ID {MessageId} to topic {TopicName}", 
                typeof(T).Name, message.MessageId, topicName);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to publish event {EventType} to topic {TopicName}", 
                typeof(T).Name, topicName);
            throw;
        }
    }

    private string GetTopicName(Type eventType)
    {
        return eventType.Name.ToLowerInvariant().Replace("event", "-events");
    }
}

Code Quality and Reviews

Code Analysis and Static Checking

EditorConfig

# .editorconfig

root = true

[*]
charset = utf-8
end_of_line = crlf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true

[*.{cs,csx,vb,vbx}]
dotnet_analyzer_diagnostic.category-style.severity = warning
dotnet_analyzer_diagnostic.category-maintainability.severity = warning

# C# formatting rules
csharp_new_line_before_open_brace = all
csharp_new_line_before_else = true
csharp_new_line_before_catch = true
csharp_new_line_before_finally = true
csharp_indent_case_contents = true
csharp_indent_switch_labels = true

# Naming conventions
dotnet_naming_rule.interfaces_should_be_prefixed_with_i.severity = warning
dotnet_naming_rule.interfaces_should_be_prefixed_with_i.symbols = interface
dotnet_naming_rule.interfaces_should_be_prefixed_with_i.style = prefix_interface_with_i

dotnet_naming_rule.types_should_be_pascal_case.severity = warning
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case

dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = warning
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case

Code Review Checklist

Security Review Points

  • [ ] Input validation implemented for all public methods
  • [ ] SQL injection prevention (parameterized queries)
  • [ ] XSS prevention in web controllers
  • [ ] Authentication and authorization properly applied
  • [ ] Sensitive data not logged or exposed
  • [ ] Secrets managed through Key Vault or secure configuration

Performance Review Points

  • [ ] Async/await used correctly (no .Result or .Wait())
  • [ ] Database queries optimized (no N+1 problems)
  • [ ] Appropriate caching strategies implemented
  • [ ] Memory leaks avoided (proper disposal of resources)
  • [ ] Large data sets handled with pagination

Code Quality Review Points

  • [ ] SOLID principles followed
  • [ ] Single responsibility maintained
  • [ ] Proper error handling and logging
  • [ ] Unit tests cover business logic
  • [ ] Integration tests cover API endpoints
  • [ ] Documentation updated

Document Version: 1.0
Last Updated: January 2026
Next Review: April 2026