feat(configmanager): add ConfigFileService with tests

Add config file loading and saving service following TDD approach:
- IConfigFileService interface for loading/saving config files
- ConfigLoadException for descriptive error handling
- ConfigFileService implementation with JSON serialization
- Unit tests with mocked IFileSystem dependency
This commit is contained in:
Joseph Doherty
2026-01-19 17:38:20 -05:00
parent 4335286560
commit 54620ccb2e
4 changed files with 158 additions and 0 deletions
@@ -0,0 +1,75 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using JdeScoping.ConfigManager.Models;
using Microsoft.Extensions.Logging;
namespace JdeScoping.ConfigManager.Services;
/// <summary>
/// Service for loading and saving configuration files.
/// </summary>
public class ConfigFileService : IConfigFileService
{
private readonly IFileSystem _fileSystem;
private readonly ILogger<ConfigFileService>? _logger;
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNameCaseInsensitive = true
};
public ConfigFileService(IFileSystem fileSystem, ILogger<ConfigFileService>? logger = null)
{
_fileSystem = fileSystem;
_logger = logger;
}
public async Task<ConfigModel> LoadAppSettingsAsync(string path, CancellationToken ct = default)
{
_logger?.LogInformation("Loading appsettings from {Path}", path);
try
{
var json = await _fileSystem.ReadAllTextAsync(path, ct);
var config = JsonSerializer.Deserialize<ConfigModel>(json, JsonOptions);
return config ?? new ConfigModel();
}
catch (JsonException ex)
{
throw new ConfigLoadException(path, $"Failed to parse appsettings.json: {ex.Message}", ex);
}
}
public async Task<PipelinesConfigModel> LoadPipelinesAsync(string path, CancellationToken ct = default)
{
_logger?.LogInformation("Loading pipelines from {Path}", path);
try
{
var json = await _fileSystem.ReadAllTextAsync(path, ct);
var config = JsonSerializer.Deserialize<PipelinesConfigModel>(json, JsonOptions);
return config ?? new PipelinesConfigModel();
}
catch (JsonException ex)
{
throw new ConfigLoadException(path, $"Failed to parse pipelines.json: {ex.Message}", ex);
}
}
public async Task SaveAppSettingsAsync(string path, ConfigModel config, CancellationToken ct = default)
{
_logger?.LogInformation("Saving appsettings to {Path}", path);
var json = JsonSerializer.Serialize(config, JsonOptions);
await _fileSystem.WriteAllTextAsync(path, json, ct);
}
public async Task SavePipelinesAsync(string path, PipelinesConfigModel config, CancellationToken ct = default)
{
_logger?.LogInformation("Saving pipelines to {Path}", path);
var json = JsonSerializer.Serialize(config, JsonOptions);
await _fileSystem.WriteAllTextAsync(path, json, ct);
}
}
@@ -0,0 +1,15 @@
namespace JdeScoping.ConfigManager.Services;
/// <summary>
/// Exception thrown when configuration file loading fails.
/// </summary>
public class ConfigLoadException : Exception
{
public string FilePath { get; }
public ConfigLoadException(string filePath, string message, Exception? inner = null)
: base(message, inner)
{
FilePath = filePath;
}
}
@@ -0,0 +1,14 @@
using JdeScoping.ConfigManager.Models;
namespace JdeScoping.ConfigManager.Services;
/// <summary>
/// Service for loading and saving configuration files.
/// </summary>
public interface IConfigFileService
{
Task<ConfigModel> LoadAppSettingsAsync(string path, CancellationToken ct = default);
Task<PipelinesConfigModel> LoadPipelinesAsync(string path, CancellationToken ct = default);
Task SaveAppSettingsAsync(string path, ConfigModel config, CancellationToken ct = default);
Task SavePipelinesAsync(string path, PipelinesConfigModel config, CancellationToken ct = default);
}