Service Defaults in Momentum
Service defaults provide a comprehensive set of pre-configured services and middleware that form the foundation of every Momentum application. They ensure consistency, observability, and best practices across all services.
Overview
The ServiceDefaultsExtensions
class provides a single entry point to configure all essential services:
using Momentum.ServiceDefaults;
var builder = WebApplication.CreateBuilder(args);
// Add all Momentum service defaults
builder.AddServiceDefaults();
var app = builder.Build();
await app.RunAsync(args);
What's Included
When you call AddServiceDefaults()
, Momentum automatically configures:
- HTTPS Configuration - Kestrel HTTPS setup
- Structured Logging - Serilog with structured logging
- OpenTelemetry - Distributed tracing, metrics, and logging
- Wolverine Messaging - CQRS command/query bus and event publishing
- FluentValidation - Automatic validator registration
- Health Checks - Basic health check endpoints
- Service Discovery - Service discovery configuration
- HTTP Client Resilience - Retry policies and circuit breakers
Core Configuration
Basic Setup
// Program.cs
using Momentum.ServiceDefaults;
using YourApp.Domain;
[assembly: DomainAssembly(typeof(IYourDomainAssembly))]
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
var app = builder.Build();
await app.RunAsync(args);
Domain Assembly Marking
Mark your domain assemblies so Momentum can discover commands, queries, and validators:
// In your domain assembly - IYourDomainAssembly.cs
namespace YourApp.Domain;
public interface IYourDomainAssembly;
// In your API assembly - Program.cs
using YourApp.Domain;
[assembly: DomainAssembly(typeof(IYourDomainAssembly))]
Why Domain Assemblies Matter:
- Automatic discovery of command and query handlers
- FluentValidation validator registration
- Integration event discovery
- Wolverine message handler registration
API Service Defaults
For API projects, add additional API-specific defaults:
using Momentum.ServiceDefaults;
using Momentum.ServiceDefaults.Api;
var builder = WebApplication.CreateSlimBuilder(args);
// Core service defaults
builder.AddServiceDefaults();
// API-specific defaults
builder.AddApiServiceDefaults();
var app = builder.Build();
// Configure API middleware and endpoints
app.ConfigureApiUsingDefaults(requireAuth: false);
await app.RunAsync(args);
API Configuration Options
// With authentication required
app.ConfigureApiUsingDefaults(requireAuth: true);
// Custom configuration
app.ConfigureApiUsingDefaults(options =>
{
options.RequireAuth = true;
options.EnableSwagger = true;
options.EnableCors = true;
options.CorsPolicy = "MyPolicy";
});
Service Discovery
Services are automatically registered for discovery:
// Automatic service registration
builder.AddServiceDefaults();
// Services are discovered via:
// - Assembly scanning for DomainAssembly attributes
// - Service discovery configuration
// - Health check registrations
Custom Service Registration
Add your own services alongside the defaults:
builder.AddServiceDefaults();
// Add custom services
builder.Services.AddScoped<IEmailService, EmailService>();
builder.Services.AddSingleton<IConfiguration>(builder.Configuration);
// Add database contexts
builder.Services.AddDbContext<AppDb>(options =>
options.UseNpgsql(connectionString));
HTTP Client Configuration
All HTTP clients get automatic resilience policies:
// Standard resilience is added automatically
builder.Services.ConfigureHttpClientDefaults(http =>
{
// These are configured automatically by service defaults:
http.AddStandardResilienceHandler();
});
// Custom HTTP client with additional configuration
builder.Services.AddHttpClient<IExternalService, ExternalService>(client =>
{
client.BaseAddress = new Uri("https://api.external.com/");
client.Timeout = TimeSpan.FromSeconds(30);
})
.AddStandardResilienceHandler(); // Already added by defaults
Validation System
FluentValidation is automatically configured to discover validators:
// Validators are automatically registered from:
// 1. Entry assembly
// 2. All assemblies marked with [DomainAssembly]
public class CreateUserValidator : AbstractValidator<CreateUserCommand>
{
public CreateUserValidator()
{
RuleFor(x => x.Name).NotEmpty();
RuleFor(x => x.Email).EmailAddress();
}
}
// No manual registration needed - automatically discovered
Manual Validator Registration
If needed, you can manually register validators:
builder.AddServiceDefaults();
// Manual registration (usually not needed)
builder.Services.AddValidatorsFromAssemblyContaining<CreateUserValidator>();
Health Checks
Basic health checks are included by default:
// Default health checks are registered automatically
builder.AddServiceDefaults();
// Map health check endpoints
app.MapDefaultHealthCheckEndpoints();
// Add custom health checks
builder.Services.AddHealthChecks()
.AddNpgSql(connectionString)
.AddKafka(kafkaOptions =>
{
kafkaOptions.BootstrapServers = "localhost:9092";
});
Health Check Endpoints
The default endpoints are:
/health
- Overall health status/health/ready
- Readiness probe/health/live
- Liveness probe
Application Lifecycle
The RunAsync()
method provides enhanced application lifecycle management:
await app.RunAsync(args);
Features:
- Initialization logging
- Wolverine command-line support
- Proper exception handling
- Log flushing on shutdown
Wolverine Commands
Supported command-line operations:
check-env
- Check environment configurationcodegen
- Generate codedb-apply
- Apply database migrationsdb-assert
- Assert database statedb-dump
- Dump database schemadb-patch
- Create database patchesdescribe
- Describe configurationhelp
- Show helpresources
- List resourcesstorage
- Storage operations
# Run application normally
dotnet run
# Run Wolverine command
dotnet run -- db-apply
dotnet run -- describe
Configuration Examples
Minimal API Service
// Program.cs
using Momentum.ServiceDefaults;
using MyApp.Domain;
[assembly: DomainAssembly(typeof(IMyAppDomainAssembly))]
var builder = WebApplication.CreateSlimBuilder(args);
builder.AddServiceDefaults();
var app = builder.Build();
app.MapGet("/health", () => "OK");
await app.RunAsync(args);
Full API Service
// Program.cs
using Momentum.ServiceDefaults;
using Momentum.ServiceDefaults.Api;
using MyApp.Domain;
using MyApp.Infrastructure;
[assembly: DomainAssembly(typeof(IMyAppDomainAssembly))]
var builder = WebApplication.CreateSlimBuilder(args);
// Core defaults
builder.AddServiceDefaults();
builder.AddApiServiceDefaults();
// Application-specific services
builder.AddMyAppServices();
builder.AddInfrastructure();
var app = builder.Build();
// Configure API
app.ConfigureApiUsingDefaults(requireAuth: true);
app.MapDefaultHealthCheckEndpoints();
// Add custom endpoints
app.MapControllers();
app.MapGet("/", () => "MyApp API");
await app.RunAsync(args);
Background Service
// Program.cs
using Momentum.ServiceDefaults;
using MyApp.Domain;
[assembly: DomainAssembly(typeof(IMyAppDomainAssembly))]
var builder = Host.CreateApplicationBuilder(args);
// Service defaults work with generic hosts too
builder.Services.AddServiceDefaults(builder);
// Add background services
builder.Services.AddHostedService<MyBackgroundService>();
var app = builder.Build();
await app.RunAsync();
Configuration Hierarchy
Momentum follows .NET's standard configuration hierarchy with specific recommendations for cloud-native applications.
Configuration File Strategy
appsettings.json - Development Baseline Configuration
The appsettings.json
file serves as the baseline configuration containing common settings and local development defaults:
{
"AllowedHosts": "*",
"ConnectionStrings": {
"AppDomainDb": "Host=localhost;Port=54320;Database=app_domain;",
"ServiceBus": "Host=localhost;Port=54320;Database=service_bus;",
"Messaging": "localhost:9092"
},
"Aspire": {
"Npgsql:DisableHealthChecks": true,
"Npgsql:DisableTracing": true
},
"Wolverine": {
"CodegenEnabled": false
}
}
appsettings.Development.json - Local Development Overrides
The appsettings.Development.json
file contains local development-specific overrides and will be excluded from cloud environments via .dockerignore
:
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft.AspNetCore": "Warning"
}
},
"Wolverine": {
"CodegenEnabled": true
}
}
[!WARNING] Do not rely on appsettings.Development.json
for cloud deployments. This file is excluded via .dockerignore
and will not be available in containerized environments.
Cloud Environment Configuration
Environment-Specific Configuration Files
Cloud environments (QA, Staging, Production) should use environment-specific appsettings files as the primary configuration method:
// appsettings.Production.json
{
"Logging": {
"LogLevel": {
"Default": "Warning",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"AppDomainDb": "Host=prod-db;Port=5432;Database=app_domain;",
"ServiceBus": "Host=prod-db;Port=5432;Database=service_bus;",
"Messaging": "prod-kafka:9092"
},
"Aspire": {
"Npgsql:DisableHealthChecks": false,
"Npgsql:DisableTracing": false
}
}
// appsettings.QA.json
{
"Logging": {
"LogLevel": {
"Default": "Information"
}
},
"ConnectionStrings": {
"AppDomainDb": "Host=qa-db;Port=5432;Database=app_domain;",
"Messaging": "qa-kafka:9092"
}
}
Environment Variables for Specific Overrides
Environment variables should be used for deployment-specific values and overrides, not as the primary configuration method:
# Example: Override specific database timeout for this deployment
export Database__CommandTimeout=45
# Example: Override log level for debugging
export Logging__LogLevel__Default=Debug
# Example: Override a connection string component
export ConnectionStrings__AppDomainDb="Host=prod-db;Port=5432;Database=app_domain;Timeout=30;"
Cloud-Native Secret Management
Secrets should use cloud-native secret management solutions:
- Azure: Azure Key Vault
- AWS: AWS Secrets Manager or Parameter Store
- GCP: Google Secret Manager
- Kubernetes: Kubernetes Secrets
// Example: Azure Key Vault integration
builder.Configuration.AddAzureKeyVault(
new Uri("https://vault.vault.azure.net/"),
new DefaultAzureCredential());
// Example: AWS Systems Manager Parameter Store
builder.Configuration.AddSystemsManager("/myapp",
options => options.Optional = true);
Configuration Hierarchy Order
.NET applies configuration in this order (later sources override earlier ones):
- appsettings.json (baseline configuration)
- appsettings.{Environment}.json (environment-specific configuration)
- User secrets (development only, via
dotnet user-secrets
) - Environment variables (deployment-specific overrides)
- Command-line arguments (container/pod startup)
- Cloud secret providers (Key Vault, Secrets Manager, etc.)
Environment Configuration
Service defaults adapt to different environments:
Development Environment
// appsettings.json (baseline) + appsettings.Development.json (local overrides)
{
"ConnectionStrings": {
"AppDomainDb": "Host=localhost;Port=54320;Database=app_domain;",
"Messaging": "localhost:9092"
},
"Aspire": {
"Npgsql:DisableHealthChecks": true,
"Npgsql:DisableTracing": true
},
"Logging": {
"LogLevel": {
"Default": "Debug"
}
}
}
Production Environment
// appsettings.Production.json (primary configuration)
{
"ConnectionStrings": {
"AppDomainDb": "Host=prod-db;Port=5432;Database=app_domain;",
"Messaging": "prod-kafka-cluster:9092"
},
"Aspire": {
"Npgsql:DisableHealthChecks": false,
"Npgsql:DisableTracing": false
},
"Logging": {
"LogLevel": {
"Default": "Warning"
}
}
}
# Environment variables for deployment-specific overrides only
DATABASE_TIMEOUT=45 # Override specific timeout for this deployment
LOGGING_LEVEL=Information # Temporary logging override
Advanced Configuration
Custom Entry Assembly
In some scenarios, you might need to specify a custom entry assembly:
using Momentum.ServiceDefaults;
// Set custom entry assembly before calling AddServiceDefaults()
ServiceDefaultsExtensions.EntryAssembly = typeof(MyCustomMarker).Assembly;
builder.AddServiceDefaults();
Selective Configuration
If you need more control, you can configure services individually:
// Instead of AddServiceDefaults(), configure selectively:
builder.WebHost.UseKestrelHttpsConfiguration();
builder.AddLogging();
builder.AddOpenTelemetry();
builder.AddWolverine();
builder.AddValidators();
builder.Services.AddHealthChecks();
builder.Services.AddServiceDiscovery();
Integration with .NET Aspire
Service defaults work seamlessly with .NET Aspire:
// AppHost project
var builder = DistributedApplication.CreateBuilder(args);
var apiService = builder.AddProject<Projects.MyApp_Api>("myapp-api");
builder.Build().Run();
// API project with Aspire integration
var builder = WebApplication.CreateSlimBuilder(args);
builder.AddServiceDefaults(); // Includes Aspire service discovery
builder.AddApiServiceDefaults();
// Aspire automatically configures service discovery
var app = builder.Build();
await app.RunAsync(args);
Troubleshooting
Common Issues
"Assembly not found" errors:
// Ensure you've marked domain assemblies
[assembly: DomainAssembly(typeof(IDomainMarker))]
// Or set manually
ServiceDefaultsExtensions.EntryAssembly = typeof(Program).Assembly;
Validators not found:
// Check that validators are in domain assemblies
// Check that domain assemblies are marked with [DomainAssembly]
// Manual registration as fallback
builder.Services.AddValidatorsFromAssemblyContaining<MyValidator>();
Service discovery not working:
// Ensure service defaults are added
builder.AddServiceDefaults();
// Check configuration
builder.Services.Configure<ServiceDiscoveryOptions>(options =>
{
// Custom configuration
});
Best Practices
Assembly Organization
- Create domain marker interfaces: Use interfaces to mark domain assemblies
- Mark all domain assemblies: Every assembly with commands/queries needs
[DomainAssembly]
- Use consistent naming: Follow naming conventions for discoverable components
Configuration Management
- Environment-specific configuration: Use appsettings.{Environment}.json files for each target environment (Production, QA, Staging)
- Local development: Use appsettings.Development.json only for local development overrides, excluded from containers
- Deployment overrides: Use environment variables for deployment-specific values and temporary overrides
- Secret management: Use cloud-native secret management for sensitive data
Service Registration
- Leverage automatic registration: Let service defaults handle common services
- Register custom services after defaults: Add your services after calling
AddServiceDefaults()
- Use appropriate lifetimes: Choose correct service lifetimes (Singleton, Scoped, Transient)
Error Handling
- Use the RunAsync method: Always use
app.RunAsync(args)
for proper lifecycle management - Configure logging early: Service defaults configure logging before other services
- Handle startup errors: Use try/catch around configuration code if needed
Next Steps
- Learn about API Setup for REST and gRPC configuration
- Understand Observability with OpenTelemetry
- Explore CQRS patterns for commands and queries
- See Messaging for event-driven architecture