CSharp Configuration Handling

From bibbleWiki
Jump to navigation Jump to search

Introduction

Struggled with the right way to do configuration as there were two approaches so thought I would right down the one that works for me in 2025

Example Redis Service

Create a Config to hold the Setting

namespace Infrastructure.Caching.Configuration;

using Domain.Interfaces.Configuration;

public class CacheOptions : ICacheConfig
{
    public required string Host { get; set; }
    public required int Port { get; set; }
    public required string Password { get; set; }
    public string InstanceName { get; set; } = "DvdrentalApi_";
    public TimeSpan DefaultTtl { get; set; } = TimeSpan.FromMinutes(5);
    public string ConnectionString => $"{Host}:{Port},password={Password},ssl=False,abortConnect=False";
}

Create a Service to do it

namespace Infrastructure.Caching.Services;

using System.Text.Json;
using Domain.Interfaces.Caching;
using Domain.Interfaces.Configuration;
using Microsoft.Extensions.Caching.Distributed;

public class RedisCacheService(IDistributedCache cache, ICacheConfig config) : ICacheService
{
    private readonly IDistributedCache _cache = cache;
    private readonly ICacheConfig _config = config;

    public async Task<T?> GetAsync<T>(string key, CancellationToken ct = default)
    {
        var namespacedKey = $"{_config.InstanceName}{key}";
        var cached = await _cache.GetStringAsync(namespacedKey, ct);
        return cached is null ? default : JsonSerializer.Deserialize<T>(cached);
    }

    public async Task SetAsync<T>(string key, T value, TimeSpan? ttl = null, CancellationToken ct = default)
    {
        var namespacedKey = $"{_config.InstanceName}{key}";
        var serialized = JsonSerializer.Serialize(value);

        await _cache.SetStringAsync(
            namespacedKey,
            serialized,
            new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = ttl ?? _config.DefaultTtl
            },
            ct);
    }

    public Task RemoveAsync(string key, CancellationToken ct = default)
    {
        var namespacedKey = $"{_config.InstanceName}{key}";
        return _cache.RemoveAsync(namespacedKey, ct);
    }
}

Add IServiceCollection Extension for Startup

The main thing with Redis was when it failed, it caused the connection string to be printed in the error. If you use ConfigurationOptions (part of AddStackExchangeRedisCache) this is not the case.

    private static IServiceCollection AddCachingService(this IServiceCollection services, IConfiguration config)
    {
        // Bind CacheOptions from configuration
        var cacheConfig = config.GetSection("Cache").Get<CacheOptions>() ??
            throw new InvalidOperationException("Cache configuration section 'Cache' is missing or invalid.");

        // Register ICacheConfig for DI
        services.AddSingleton<ICacheConfig>(cacheConfig);

        // Register Redis as IDistributedCache using ConfigurationOptions
        services.AddStackExchangeRedisCache(options =>
        {
            Console.WriteLine("Configuring Redis Cache");

            var redisOptions = new ConfigurationOptions
            {
                EndPoints = { { cacheConfig.Host, cacheConfig.Port } },
                Password = cacheConfig.Password,
                AbortOnConnectFail = false,
                Ssl = false
            };

            options.ConfigurationOptions = redisOptions;
            options.InstanceName = cacheConfig.InstanceName;
        });

        // Register RedisCacheService
        services.AddSingleton<ICacheService, RedisCacheService>();

        return services;
    }

Make an appsetting.json

Here is my example appsetting.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "System.Net.Http": "Warning",
      "Microsoft": "Warning",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "Cache": {
    "host": "192.168.1.220",
    "port": 6379,
    "password": "MOVED TO USER SECRETS",
    "InstanceName": "DvdrentalApi_",
    "DefaultTtl": "00:05:00"
  }
}

Put password in User-Secrets

In the project directory for the binary. (Note if you are using ! or other characters you will need to escape with \!

dotnet user-secrets init
dotnet user-secrets set Cache:Password NotSaying!!!

.NET creates an entry in your project. This is how it knows to use user secrets

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <UserSecretsId>blah-blah-blah</UserSecretsId>
  </PropertyGroup>

Putting into k8s

Here is an example of the k8s config map for environment variables.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      containers:
        - name: api
          image: yourregistry/api:latest
          env:
            - name: Cache__Password
              value: "blahblah"

Order of Play

This is the order parameters are read

appsettings.json
   ↓
appsettings.{Environment}.json
   ↓
User Secrets
   ↓
Environment Variables
   ↓
Command-line arguments