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
- Project Structure
- Security Best Practices
- Logging Implementation
- Error Handling and Alerting
- Performance Optimization
- Testing Standards
- Deployment Best Practices
- Integration Guidelines
- Code Quality and Reviews
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¶
Recommended Solution 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
Related Documentation¶
- 🔒 Security Documentation - Security best practices and compliance
- 📝 Logging Documentation - Comprehensive logging strategies
- 🚨 Alerting Documentation - Alerting and incident response
- 🔄 Integration Patterns - System integration guidelines
- 🔗 Integration Systems - External system specifications