Skip to main content

.NET Service Configuration

In modern .NET applications, proper service configuration is essential for building flexible, maintainable, and environment-agnostic applications. This guide will walk you through the fundamentals of configuring services in .NET, from basic settings to advanced configuration patterns.

Introduction to Service Configuration

Service configuration in .NET refers to the process of setting up and customizing your application's services, including:

  • Providing necessary parameters to services at startup
  • Managing application settings across different environments
  • Injecting configuration values into your services
  • Handling sensitive information securely

The .NET configuration system has evolved significantly from the older web.config and app.config approaches to a more flexible, modular system built around the Microsoft.Extensions.Configuration namespace and related packages.

Configuration Basics

The Configuration System

At the heart of .NET's configuration system is IConfiguration, which represents a set of key-value application configuration properties. This interface provides methods to access configuration values regardless of their source.

Let's start with a simple console application that uses configuration:

csharp
using Microsoft.Extensions.Configuration;

class Program
{
static void Main(string[] args)
{
// Build a configuration object
IConfiguration configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build();

// Access a configuration value
string applicationName = configuration["ApplicationName"];
Console.WriteLine($"Application name: {applicationName}");
}
}

The corresponding appsettings.json file might look like:

json
{
"ApplicationName": "MyAwesomeApp"
}

Output:

Application name: MyAwesomeApp

Configuration Sources

.NET supports various configuration sources:

  1. JSON files - Most common format (appsettings.json)
  2. Environment variables - Useful for containerized applications
  3. Command-line arguments - For runtime parameter overrides
  4. User secrets - For local development of sensitive settings
  5. Azure Key Vault - For secure cloud-based settings
  6. Custom providers - For specialized scenarios

Here's how to use multiple sources:

csharp
IConfiguration configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", optional: false)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables()
.AddCommandLine(args)
.Build();

Configuring Services with Dependency Injection

The real power of .NET service configuration comes when combined with the dependency injection (DI) system.

Basic Service Registration

Here's a simple example registering and configuring a service:

csharp
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;

// Setup our DI container
var services = new ServiceCollection();

// Build configuration
IConfiguration configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build();

// Register configuration with DI container
services.AddSingleton(configuration);

// Register our service
services.AddSingleton<IEmailService, SmtpEmailService>();

// Create the provider
var serviceProvider = services.BuildServiceProvider();

// Retrieve service
var emailService = serviceProvider.GetRequiredService<IEmailService>();

Using the Options Pattern

The options pattern is the recommended way to pass configuration settings to services. It provides:

  • Strong typing for configuration values
  • Validation
  • Reloadable configurations
  • Data annotation support

Step 1: Create a settings class

csharp
public class EmailSettings
{
public string SmtpServer { get; set; }
public int Port { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public bool UseSsl { get; set; }
}

Step 2: Add the settings to your appsettings.json

json
{
"EmailSettings": {
"SmtpServer": "smtp.example.com",
"Port": 587,
"Username": "user@example.com",
"Password": "securePassword123",
"UseSsl": true
}
}

Step 3: Register and use options

csharp
// Register configuration
services.Configure<EmailSettings>(configuration.GetSection("EmailSettings"));

// Inject into a service
public class SmtpEmailService : IEmailService
{
private readonly EmailSettings _settings;

public SmtpEmailService(IOptions<EmailSettings> options)
{
_settings = options.Value;
}

public async Task SendEmailAsync(string to, string subject, string body)
{
// Use _settings.SmtpServer, _settings.Port, etc.
var client = new SmtpClient(_settings.SmtpServer, _settings.Port)
{
EnableSsl = _settings.UseSsl,
Credentials = new NetworkCredential(_settings.Username, _settings.Password)
};

await client.SendMailAsync(
new MailMessage(_settings.Username, to, subject, body));
}
}

Real-World Example: Configuring a Weather Service

Let's create a more complete example of configuring a weather service that retrieves data from an external API.

Step 1: Define the models

csharp
public class WeatherServiceSettings
{
public string ApiKey { get; set; }
public string BaseUrl { get; set; }
public int CacheDurationMinutes { get; set; }
}

public class WeatherForecast
{
public DateTime Date { get; set; }
public int Temperature { get; set; }
public string Summary { get; set; }
}

public interface IWeatherService
{
Task<WeatherForecast> GetForecastAsync(string city);
}

Step 2: Create the service implementation

csharp
public class WeatherService : IWeatherService
{
private readonly HttpClient _httpClient;
private readonly WeatherServiceSettings _settings;
private readonly ILogger<WeatherService> _logger;

public WeatherService(
HttpClient httpClient,
IOptions<WeatherServiceSettings> options,
ILogger<WeatherService> logger)
{
_httpClient = httpClient;
_settings = options.Value;
_logger = logger;

// Configure the HttpClient with the base address
_httpClient.BaseAddress = new Uri(_settings.BaseUrl);
}

public async Task<WeatherForecast> GetForecastAsync(string city)
{
try
{
_logger.LogInformation("Requesting forecast for {City}", city);

// Real implementation would call the API with the API key
var response = await _httpClient.GetAsync(
$"forecast?city={Uri.EscapeDataString(city)}&apiKey={_settings.ApiKey}");

response.EnsureSuccessStatusCode();

var content = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<WeatherForecast>(content);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error retrieving forecast for {City}", city);
throw;
}
}
}

Step 3: Configure in Program.cs or Startup.cs

csharp
// In Program.cs for .NET 6+ minimal API
var builder = WebApplication.CreateBuilder(args);

// Register and configure the weather service
builder.Services.Configure<WeatherServiceSettings>(
builder.Configuration.GetSection("WeatherService"));

builder.Services.AddHttpClient<IWeatherService, WeatherService>();

var app = builder.Build();

// Define an endpoint that uses the service
app.MapGet("/weather/{city}", async (string city, IWeatherService weatherService) =>
await weatherService.GetForecastAsync(city));

app.Run();

Step 4: Create the configuration in appsettings.json

json
{
"WeatherService": {
"ApiKey": "your-api-key-here",
"BaseUrl": "https://api.weatherprovider.com/",
"CacheDurationMinutes": 15
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

Advanced Configuration Techniques

Named Options

When you need multiple configurations of the same type:

csharp
// Register multiple email provider configurations
services.Configure<EmailSettings>("Gmail", configuration.GetSection("GmailSettings"));
services.Configure<EmailSettings>("Outlook", configuration.GetSection("OutlookSettings"));

// Usage in a service
public class MultiProviderEmailService
{
private readonly EmailSettings _gmailSettings;
private readonly EmailSettings _outlookSettings;

public MultiProviderEmailService(
IOptionsSnapshot<EmailSettings> optionsSnapshot)
{
_gmailSettings = optionsSnapshot.Get("Gmail");
_outlookSettings = optionsSnapshot.Get("Outlook");
}

// Use the appropriate settings based on requirements
}

Configuration Change Monitoring

To respond to configuration changes in real-time (useful for cloud environments):

csharp
public class ReloadableService
{
private EmailSettings _currentSettings;
private readonly IDisposable _changeToken;

public ReloadableService(IOptionsMonitor<EmailSettings> monitor)
{
_currentSettings = monitor.CurrentValue;

// This callback is invoked whenever the configuration changes
_changeToken = monitor.OnChange(settings =>
{
_currentSettings = settings;
Console.WriteLine("Settings were updated!");
});
}

public void Dispose()
{
_changeToken?.Dispose();
}
}

Validation

Add validation to your configuration:

csharp
services.AddOptions<EmailSettings>()
.Bind(configuration.GetSection("EmailSettings"))
.ValidateDataAnnotations() // Uses data annotations
.Validate(settings =>
{
// Custom validation logic
return !string.IsNullOrEmpty(settings.SmtpServer) &&
settings.Port > 0 &&
settings.Port < 65536;
}, "Email settings are invalid");

Environment-Specific Configuration

.NET makes it easy to have different configurations per environment:

  1. Create environment-specific appsettings files:

    • appsettings.json - Base settings
    • appsettings.Development.json - Development overrides
    • appsettings.Production.json - Production overrides
  2. Set the environment variable:

    • ASPNETCORE_ENVIRONMENT=Development (or Production, Staging, etc.)
  3. Add both files to your configuration builder:

csharp
IConfiguration configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", optional: false)
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}.json", optional: true)
.Build();

Security Considerations

Sensitive Data

Never store sensitive information like API keys, passwords, or connection strings directly in your configuration files. Instead:

  1. For development: Use the Secret Manager tool
  2. For production: Use environment variables or specialized services like Azure Key Vault

Example using Secret Manager (Development only):

  1. Initialize Secret Manager:
bash
dotnet user-secrets init
  1. Set a secret:
bash
dotnet user-secrets set "WeatherService:ApiKey" "super-secret-api-key"
  1. Add it to your configuration:
csharp
IConfiguration configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.AddUserSecrets<Program>() // Add user secrets
.Build();

Example using Environment Variables (Production):

csharp
IConfiguration configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.AddEnvironmentVariables() // Add environment variables
.Build();

Set the environment variable like:

WEATHERSERVICE__APIKEY=super-secret-api-key

Note: Double underscores (__) represent the colon (:) in nested configuration keys.

Summary

.NET service configuration provides a flexible and powerful system for configuring your application's services:

  1. The configuration system supports multiple sources (JSON, environment variables, etc.)
  2. The Options pattern provides strongly-typed access to configuration values
  3. Different environments can have different configuration values
  4. Sensitive data should be handled securely
  5. Configuration can be validated and even reloaded at runtime

By mastering these concepts, you can build applications that are more maintainable, secure, and adaptable to different environments.

Additional Resources

Exercises

  1. Create a simple console application that reads configuration from both appsettings.json and environment variables, with the environment variables taking precedence.
  2. Implement the Options pattern for a database connection service with validation.
  3. Create a Web API with different configuration settings for Development and Production environments.
  4. Build a service that monitors configuration changes and logs when the configuration has been updated.
  5. Implement a secure way to store and retrieve API keys for a third-party service.


If you spot any mistakes on this website, please let me know at feedback@compilenrun.com. I’d greatly appreciate your feedback! :)