diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/Services/ConfigFileService.cs b/NEW/src/Utils/JdeScoping.ConfigManager/Services/ConfigFileService.cs
new file mode 100644
index 0000000..a11c952
--- /dev/null
+++ b/NEW/src/Utils/JdeScoping.ConfigManager/Services/ConfigFileService.cs
@@ -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;
+
+///
+/// Service for loading and saving configuration files.
+///
+public class ConfigFileService : IConfigFileService
+{
+ private readonly IFileSystem _fileSystem;
+ private readonly ILogger? _logger;
+
+ private static readonly JsonSerializerOptions JsonOptions = new()
+ {
+ WriteIndented = true,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ PropertyNameCaseInsensitive = true
+ };
+
+ public ConfigFileService(IFileSystem fileSystem, ILogger? logger = null)
+ {
+ _fileSystem = fileSystem;
+ _logger = logger;
+ }
+
+ public async Task 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(json, JsonOptions);
+ return config ?? new ConfigModel();
+ }
+ catch (JsonException ex)
+ {
+ throw new ConfigLoadException(path, $"Failed to parse appsettings.json: {ex.Message}", ex);
+ }
+ }
+
+ public async Task 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(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);
+ }
+}
diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/Services/ConfigLoadException.cs b/NEW/src/Utils/JdeScoping.ConfigManager/Services/ConfigLoadException.cs
new file mode 100644
index 0000000..12a78de
--- /dev/null
+++ b/NEW/src/Utils/JdeScoping.ConfigManager/Services/ConfigLoadException.cs
@@ -0,0 +1,15 @@
+namespace JdeScoping.ConfigManager.Services;
+
+///
+/// Exception thrown when configuration file loading fails.
+///
+public class ConfigLoadException : Exception
+{
+ public string FilePath { get; }
+
+ public ConfigLoadException(string filePath, string message, Exception? inner = null)
+ : base(message, inner)
+ {
+ FilePath = filePath;
+ }
+}
diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/Services/IConfigFileService.cs b/NEW/src/Utils/JdeScoping.ConfigManager/Services/IConfigFileService.cs
new file mode 100644
index 0000000..c532253
--- /dev/null
+++ b/NEW/src/Utils/JdeScoping.ConfigManager/Services/IConfigFileService.cs
@@ -0,0 +1,14 @@
+using JdeScoping.ConfigManager.Models;
+
+namespace JdeScoping.ConfigManager.Services;
+
+///
+/// Service for loading and saving configuration files.
+///
+public interface IConfigFileService
+{
+ Task LoadAppSettingsAsync(string path, CancellationToken ct = default);
+ Task LoadPipelinesAsync(string path, CancellationToken ct = default);
+ Task SaveAppSettingsAsync(string path, ConfigModel config, CancellationToken ct = default);
+ Task SavePipelinesAsync(string path, PipelinesConfigModel config, CancellationToken ct = default);
+}
diff --git a/NEW/tests/JdeScoping.ConfigManager.Tests/Services/ConfigFileServiceTests.cs b/NEW/tests/JdeScoping.ConfigManager.Tests/Services/ConfigFileServiceTests.cs
new file mode 100644
index 0000000..351de8f
--- /dev/null
+++ b/NEW/tests/JdeScoping.ConfigManager.Tests/Services/ConfigFileServiceTests.cs
@@ -0,0 +1,54 @@
+using JdeScoping.ConfigManager.Models;
+using JdeScoping.ConfigManager.Services;
+
+namespace JdeScoping.ConfigManager.Tests.Services;
+
+public class ConfigFileServiceTests
+{
+ private readonly IFileSystem _fileSystem;
+ private readonly ConfigFileService _sut;
+
+ public ConfigFileServiceTests()
+ {
+ _fileSystem = Substitute.For();
+ _sut = new ConfigFileService(_fileSystem);
+ }
+
+ [Fact]
+ public async Task LoadAppSettingsAsync_WithValidJson_ReturnsConfigModel()
+ {
+ // Arrange
+ var json = """
+ {
+ "DataSync": {
+ "Enabled": true,
+ "MaxDegreeOfParallelism": 8
+ }
+ }
+ """;
+ _fileSystem.ReadAllTextAsync(Arg.Any(), Arg.Any())
+ .Returns(Task.FromResult(json));
+
+ // Act
+ var result = await _sut.LoadAppSettingsAsync("/config/appsettings.json");
+
+ // Assert
+ result.ShouldNotBeNull();
+ result.DataSync.Enabled.ShouldBeTrue();
+ result.DataSync.MaxDegreeOfParallelism.ShouldBe(8);
+ }
+
+ [Fact]
+ public async Task LoadAppSettingsAsync_WithInvalidJson_ThrowsWithHelpfulMessage()
+ {
+ // Arrange
+ var json = "{ invalid json }";
+ _fileSystem.ReadAllTextAsync(Arg.Any(), Arg.Any())
+ .Returns(Task.FromResult(json));
+
+ // Act & Assert
+ var ex = await Should.ThrowAsync(
+ () => _sut.LoadAppSettingsAsync("/config/appsettings.json"));
+ ex.Message.ShouldContain("parse");
+ }
+}