From 0c091b3a27a74918e9715b1f316452ec98cc8ee7 Mon Sep 17 00:00:00 2001 From: TheXamlGuy Date: Thu, 3 Oct 2024 22:39:56 +0100 Subject: [PATCH] Add Config locking and caching --- Toolkit.Foundation/ConfigurationCache.cs | 26 ++ Toolkit.Foundation/ConfigurationLock.cs | 36 +++ Toolkit.Foundation/ConfigurationMonitor.cs | 15 +- Toolkit.Foundation/ConfigurationSource.cs | 222 +++++++++--------- .../ConfigurationValueViewModel.cs | 57 +++-- Toolkit.Foundation/IConfigurationSource.cs | 4 +- Toolkit.Foundation/IHostBuilderExtension.cs | 12 +- Toolkit.Foundation/IRemovable.cs | 4 + 8 files changed, 235 insertions(+), 141 deletions(-) create mode 100644 Toolkit.Foundation/ConfigurationCache.cs create mode 100644 Toolkit.Foundation/ConfigurationLock.cs create mode 100644 Toolkit.Foundation/IRemovable.cs diff --git a/Toolkit.Foundation/ConfigurationCache.cs b/Toolkit.Foundation/ConfigurationCache.cs new file mode 100644 index 0000000..e96cbd2 --- /dev/null +++ b/Toolkit.Foundation/ConfigurationCache.cs @@ -0,0 +1,26 @@ +using System.Collections.Concurrent; + +namespace Toolkit.Foundation; + +public static class ConfigurationCache +{ + private static readonly ConcurrentDictionary cache = new(); + + public static void Set(string section, + TConfiguration configuration) => cache[section] = configuration; + + public static bool Remove(string section) => cache.TryRemove(section, out _); + + public static bool TryGet(string section, + out TConfiguration? configuration) + { + if (cache.TryGetValue(section, out object? cachedValue)) + { + configuration = (TConfiguration?)cachedValue; + return true; + } + + configuration = default; + return false; + } +} diff --git a/Toolkit.Foundation/ConfigurationLock.cs b/Toolkit.Foundation/ConfigurationLock.cs new file mode 100644 index 0000000..20929e5 --- /dev/null +++ b/Toolkit.Foundation/ConfigurationLock.cs @@ -0,0 +1,36 @@ +namespace Toolkit.Foundation; + +public static class ConfigurationLock +{ + private static readonly ReaderWriterLockSlim readerWriterLock = new(); + + public static IDisposable EnterRead() + { + readerWriterLock.EnterReadLock(); + return new ConfigurationReaderLockDisposer(readerWriterLock); + } + + public static IDisposable EnterWrite() + { + readerWriterLock.EnterWriteLock(); + return new ConfigurationWriterLockDisposer(readerWriterLock); + } + + private class ConfigurationWriterLockDisposer(ReaderWriterLockSlim lockSlim) : + IDisposable + { + public void Dispose() + { + lockSlim.ExitWriteLock(); + } + } + + private class ConfigurationReaderLockDisposer(ReaderWriterLockSlim lockSlim) : + IDisposable + { + public void Dispose() + { + lockSlim.ExitReadLock(); + } + } +} diff --git a/Toolkit.Foundation/ConfigurationMonitor.cs b/Toolkit.Foundation/ConfigurationMonitor.cs index ca86ea6..20add1a 100644 --- a/Toolkit.Foundation/ConfigurationMonitor.cs +++ b/Toolkit.Foundation/ConfigurationMonitor.cs @@ -1,7 +1,10 @@ -namespace Toolkit.Foundation; +using Microsoft.Extensions.DependencyInjection; -public class ConfigurationMonitor(IConfigurationFile file, - IConfigurationReader reader, +namespace Toolkit.Foundation; + +public class ConfigurationMonitor(string section, + IConfigurationFile file, + IServiceProvider serviceProvider, IPublisher publisher) : IConfigurationMonitor where TConfiguration : @@ -14,9 +17,11 @@ public class ConfigurationMonitor(IConfigurationFile>(section) is + IConfigurationDescriptor configuration) { - publisher.PublishUI(new ChangedEventArgs(configuration)); + ConfigurationCache.Remove(section); + publisher.PublishUI(new ChangedEventArgs(configuration.Value)); } } diff --git a/Toolkit.Foundation/ConfigurationSource.cs b/Toolkit.Foundation/ConfigurationSource.cs index 9932602..142f10b 100644 --- a/Toolkit.Foundation/ConfigurationSource.cs +++ b/Toolkit.Foundation/ConfigurationSource.cs @@ -13,7 +13,7 @@ public class ConfigurationSource(IConfigurationFile defaultSerializerOptions = new(() => + private static readonly Func defaultSerializerOptions = () => { return new JsonSerializerOptions { @@ -26,133 +26,82 @@ public class ConfigurationSource(IConfigurationFile Set((object)value); public void Set(object value) { - lock (lockingObject) + using (ConfigurationLock.EnterWrite()) { IFileInfo fileInfo = configurationFile.FileInfo; - if (!File.Exists(fileInfo.PhysicalPath)) - { - string? fileDirectoryPath = Path.GetDirectoryName(fileInfo.PhysicalPath); - if (!string.IsNullOrEmpty(fileDirectoryPath)) - { - Directory.CreateDirectory(fileDirectoryPath); - } + EnsureFileExists(fileInfo.PhysicalPath); - File.WriteAllText(fileInfo.PhysicalPath!, "{}"); + string? content; + JsonNode? rootNode; + + using (Stream stream = new FileStream(fileInfo.PhysicalPath!, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite)) + using (StreamReader reader = new(stream)) + { + content = reader.ReadToEnd(); + stream.Seek(0, SeekOrigin.Begin); + + rootNode = JsonNode.Parse(content); } - using Stream stream = fileInfo.PhysicalPath is not null - ? new FileStream(fileInfo.PhysicalPath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite) - : fileInfo.CreateReadStream(); + JsonNode? valueNode = JsonNode.Parse(JsonSerializer.SerializeToUtf8Bytes(value, serializerOptions ?? defaultSerializerOptions())); - using StreamReader? reader = new(stream); + ApplyConfigurationUpdates(ref rootNode, valueNode, section); - string? content = reader.ReadToEnd(); - stream.Seek(0, SeekOrigin.Begin); + using Stream stream2 = new FileStream(fileInfo.PhysicalPath!, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite); + JsonSerializer.Serialize(stream2, rootNode, serializerOptions ?? defaultSerializerOptions()); - JsonNode? rootNode = JsonNode.Parse(content); - JsonNode? valueNode = JsonNode.Parse(JsonSerializer.SerializeToUtf8Bytes(value, - serializerOptions ?? defaultSerializerOptions())); - - string[] segments = section.Split(':'); - JsonNode? currentNode = rootNode; - int lastIndex = segments.Length - 1; - - for (int i = 0; i < lastIndex; i++) - { - if (currentNode is null) - { - return; - } - - string currentKey = segments[i]; - if (currentNode[currentKey] is null) - { - currentNode[currentKey] = new JsonObject(); - } - - currentNode = currentNode[currentKey]; - } - - if (currentNode is not null) - { - string lastKey = segments[lastIndex]; - if (currentNode is JsonArray array && int.TryParse(lastKey, out int index)) - { - if (array.Count <= index) - { - array.Add(value); - } - else - { - array[index] = MergeNodes(array[index], valueNode); - } - } - else - { - currentNode[lastKey] = MergeNodes(currentNode[lastKey], valueNode); - } - } - - using Stream stream2 = fileInfo.PhysicalPath is not null - ? new FileStream(fileInfo.PhysicalPath, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite) - : fileInfo.CreateReadStream(); - - JsonSerializer.Serialize(stream, rootNode, serializerOptions ?? defaultSerializerOptions()); + ConfigurationCache.Set(section, value); } } public bool TryGet(out TConfiguration? value) { - lock (lockingObject) + if (ConfigurationCache.TryGet(section, out value)) + { + return true; + } + + using (ConfigurationLock.EnterRead()) { IFileInfo fileInfo = configurationFile.FileInfo; - if (File.Exists(fileInfo.PhysicalPath)) + + if (!File.Exists(fileInfo.PhysicalPath)) { - static Stream OpenRead(IFileInfo fileInfo) + value = default; + return false; + } + + using Stream stream = new FileStream(fileInfo.PhysicalPath!, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite); + using StreamReader reader = new(stream); + string content = reader.ReadToEnd(); + JsonNode? rootNode = JsonNode.Parse(content); + + string[] segments = section.Split(':'); + JsonNode? currentNode = rootNode; + + int lastIndex = segments.Length - 1; + for (int i = 0; i < lastIndex; i++) + { + if (currentNode is null) { - return fileInfo.PhysicalPath is not null - ? new FileStream(fileInfo.PhysicalPath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite) - : fileInfo.CreateReadStream(); + value = default; + return false; } - using Stream stream = OpenRead(fileInfo); - using StreamReader? reader = new(stream); + currentNode = currentNode[segments[i]]; + } - string? content = reader.ReadToEnd(); - JsonNode? rootNode = JsonNode.Parse(content); - - string[] segments = section.Split(':'); - JsonNode? currentNode = rootNode; - int lastIndex = segments.Length - 1; - - for (int i = 0; i < lastIndex; i++) - { - if (currentNode is null) - { - value = default; - return false; - } - - currentNode = currentNode[segments[i]]; - } - - if (currentNode is not null) - { - if (currentNode[segments[lastIndex]] is JsonNode sectionNode) - { - value = JsonSerializer.Deserialize(sectionNode, - serializerOptions ?? defaultSerializerOptions()); - return true; - } - } + if (currentNode != null && currentNode[segments[lastIndex]] is JsonNode sectionNode) + { + value = JsonSerializer.Deserialize(sectionNode, serializerOptions ?? defaultSerializerOptions()); + ConfigurationCache.Set(section, value); + return true; } value = default; @@ -160,6 +109,49 @@ public class ConfigurationSource(IConfigurationFile(IConfigurationFile property in newObject) { - existingObject[property.Key] = MergeNodes(existingObject[property.Key], property.Value); + existingObject[property.Key] = MergeNodes(existingObject[property.Key], CloneNode(property.Value)); } return existingObject; @@ -188,7 +194,7 @@ public class ConfigurationSource(IConfigurationFile(IConfigurationFile(IServic INotificationHandler> where TConfiguration : class { - public Task Handle(ChangedEventArgs args) + public async Task Handle(ChangedEventArgs args) { - throw new NotImplementedException(); + if (args.Sender is TConfiguration configuration) + { + // await Task.Run(() => Value = read(configuration)); + } } - protected override void OnChanged(TValue? value) + public override async Task OnActivated() { - writer.Write(args => write(value, args)); + await Task.Run(() => Value = read(configuration)); + await base.OnActivated(); + } + + protected override async void OnChanged(TValue? value) + { + if (IsActivated) + { + await Task.Run(() => writer.Write(args => write(value, args))); + } + base.OnChanged(value); } - - public override Task OnActivated() - { - Value = read(configuration); - return base.OnActivated(); - } } public partial class ConfigurationValueViewModel : @@ -41,10 +48,9 @@ public partial class ConfigurationValueViewModel IDisposable { private readonly TConfiguration configuration; - private readonly IWritableConfiguration writer; private readonly Func read; private readonly Action write; - + private readonly IWritableConfiguration writer; public ConfigurationValueViewModel(IServiceProvider provider, IServiceFactory factory, IMediator mediator, @@ -86,20 +92,27 @@ public partial class ConfigurationValueViewModel Value = value; } - public Task Handle(ChangedEventArgs args) + public async Task Handle(ChangedEventArgs args) { - throw new NotImplementedException(); + if (args.Sender is TConfiguration configuration) + { + // await Task.Run(() => Value = read(configuration)); + } } - protected override void OnChanged(TValue? value) + public override async Task OnActivated() { - writer.Write(args => write(value, args)); + await Task.Run(() => Value = read(configuration)); + await base.OnActivated(); + } + + protected override async void OnChanged(TValue? value) + { + if (IsActivated) + { + await Task.Run(() => writer.Write(args => write(value, args))); + } + base.OnChanged(value); } - - public override Task OnActivated() - { - Value = read(configuration); - return base.OnActivated(); - } } \ No newline at end of file diff --git a/Toolkit.Foundation/IConfigurationSource.cs b/Toolkit.Foundation/IConfigurationSource.cs index 08da464..13d46f1 100644 --- a/Toolkit.Foundation/IConfigurationSource.cs +++ b/Toolkit.Foundation/IConfigurationSource.cs @@ -9,6 +9,4 @@ public interface IConfigurationSource void Set(TConfiguration value); void Set(object value); -} - -public interface IRemovable : IDisposable; \ No newline at end of file +} \ No newline at end of file diff --git a/Toolkit.Foundation/IHostBuilderExtension.cs b/Toolkit.Foundation/IHostBuilderExtension.cs index 85afc5c..631f278 100644 --- a/Toolkit.Foundation/IHostBuilderExtension.cs +++ b/Toolkit.Foundation/IHostBuilderExtension.cs @@ -141,10 +141,10 @@ public static class IHostBuilderExtension } return new ConfigurationSource(provider.GetRequiredService>(), - section, defaultSerializer); + section, + defaultSerializer); }); - //services.AddHostedService>(); services.TryAddKeyedTransient>(section, (provider, key) => new ConfigurationReader(provider.GetRequiredKeyedService>(key), provider.GetRequiredKeyedService>(key))); @@ -179,6 +179,12 @@ public static class IHostBuilderExtension services.AddTransient(provider => provider.GetRequiredKeyedService>(section).Value); + + services.AddHostedService(provider => + new ConfigurationMonitor(section, + provider.GetRequiredService>(), + provider.GetRequiredService(), + provider.GetRequiredService())); } }); @@ -186,7 +192,7 @@ public static class IHostBuilderExtension } public static IHostBuilder UseContentRoot(this IHostBuilder hostBuilder, - string contentRoot, + string contentRoot, bool createDirectory) { if (createDirectory) diff --git a/Toolkit.Foundation/IRemovable.cs b/Toolkit.Foundation/IRemovable.cs new file mode 100644 index 0000000..0c90d31 --- /dev/null +++ b/Toolkit.Foundation/IRemovable.cs @@ -0,0 +1,4 @@ +namespace Toolkit.Foundation; + +public interface IRemovable : + IDisposable; \ No newline at end of file