Add Config locking and caching

This commit is contained in:
TheXamlGuy
2024-10-03 22:39:56 +01:00
parent 8136739372
commit 0c091b3a27
8 changed files with 235 additions and 141 deletions
+26
View File
@@ -0,0 +1,26 @@
using System.Collections.Concurrent;
namespace Toolkit.Foundation;
public static class ConfigurationCache
{
private static readonly ConcurrentDictionary<string, object?> cache = new();
public static void Set<TConfiguration>(string section,
TConfiguration configuration) => cache[section] = configuration;
public static bool Remove(string section) => cache.TryRemove(section, out _);
public static bool TryGet<TConfiguration>(string section,
out TConfiguration? configuration)
{
if (cache.TryGetValue(section, out object? cachedValue))
{
configuration = (TConfiguration?)cachedValue;
return true;
}
configuration = default;
return false;
}
}
+36
View File
@@ -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();
}
}
}
+10 -5
View File
@@ -1,7 +1,10 @@
namespace Toolkit.Foundation; using Microsoft.Extensions.DependencyInjection;
public class ConfigurationMonitor<TConfiguration>(IConfigurationFile<TConfiguration> file, namespace Toolkit.Foundation;
IConfigurationReader<TConfiguration> reader,
public class ConfigurationMonitor<TConfiguration>(string section,
IConfigurationFile<TConfiguration> file,
IServiceProvider serviceProvider,
IPublisher publisher) : IPublisher publisher) :
IConfigurationMonitor<TConfiguration> IConfigurationMonitor<TConfiguration>
where TConfiguration : where TConfiguration :
@@ -14,9 +17,11 @@ public class ConfigurationMonitor<TConfiguration>(IConfigurationFile<TConfigurat
void ChangedHandler(object sender, void ChangedHandler(object sender,
FileSystemEventArgs args) FileSystemEventArgs args)
{ {
if (reader.Read() is { } configuration) if (serviceProvider.GetRequiredKeyedService<IConfigurationDescriptor<TConfiguration>>(section) is
IConfigurationDescriptor<TConfiguration> configuration)
{ {
publisher.PublishUI(new ChangedEventArgs<TConfiguration>(configuration)); ConfigurationCache.Remove(section);
publisher.PublishUI(new ChangedEventArgs<TConfiguration>(configuration.Value));
} }
} }
+114 -108
View File
@@ -13,7 +13,7 @@ public class ConfigurationSource<TConfiguration>(IConfigurationFile<TConfigurati
where TConfiguration : where TConfiguration :
class class
{ {
private static readonly Func<JsonSerializerOptions> defaultSerializerOptions = new(() => private static readonly Func<JsonSerializerOptions> defaultSerializerOptions = () =>
{ {
return new JsonSerializerOptions return new JsonSerializerOptions
{ {
@@ -26,133 +26,82 @@ public class ConfigurationSource<TConfiguration>(IConfigurationFile<TConfigurati
new DictionaryStringObjectJsonConverter() new DictionaryStringObjectJsonConverter()
} }
}; };
}); };
private readonly object lockingObject = new();
public void Set(TConfiguration value) => Set((object)value); public void Set(TConfiguration value) => Set((object)value);
public void Set(object value) public void Set(object value)
{ {
lock (lockingObject) using (ConfigurationLock.EnterWrite())
{ {
IFileInfo fileInfo = configurationFile.FileInfo; IFileInfo fileInfo = configurationFile.FileInfo;
if (!File.Exists(fileInfo.PhysicalPath)) EnsureFileExists(fileInfo.PhysicalPath);
{
string? fileDirectoryPath = Path.GetDirectoryName(fileInfo.PhysicalPath);
if (!string.IsNullOrEmpty(fileDirectoryPath))
{
Directory.CreateDirectory(fileDirectoryPath);
}
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 JsonNode? valueNode = JsonNode.Parse(JsonSerializer.SerializeToUtf8Bytes(value, serializerOptions ?? defaultSerializerOptions()));
? new FileStream(fileInfo.PhysicalPath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite)
: fileInfo.CreateReadStream();
using StreamReader? reader = new(stream); ApplyConfigurationUpdates(ref rootNode, valueNode, section);
string? content = reader.ReadToEnd(); using Stream stream2 = new FileStream(fileInfo.PhysicalPath!, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite);
stream.Seek(0, SeekOrigin.Begin); JsonSerializer.Serialize(stream2, rootNode, serializerOptions ?? defaultSerializerOptions());
JsonNode? rootNode = JsonNode.Parse(content); ConfigurationCache.Set(section, value);
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());
} }
} }
public bool TryGet(out TConfiguration? value) public bool TryGet(out TConfiguration? value)
{ {
lock (lockingObject) if (ConfigurationCache.TryGet(section, out value))
{
return true;
}
using (ConfigurationLock.EnterRead())
{ {
IFileInfo fileInfo = configurationFile.FileInfo; 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 value = default;
? new FileStream(fileInfo.PhysicalPath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite) return false;
: fileInfo.CreateReadStream();
} }
using Stream stream = OpenRead(fileInfo); currentNode = currentNode[segments[i]];
using StreamReader? reader = new(stream); }
string? content = reader.ReadToEnd(); if (currentNode != null && currentNode[segments[lastIndex]] is JsonNode sectionNode)
JsonNode? rootNode = JsonNode.Parse(content); {
value = JsonSerializer.Deserialize<TConfiguration>(sectionNode, serializerOptions ?? defaultSerializerOptions());
string[] segments = section.Split(':'); ConfigurationCache.Set(section, value);
JsonNode? currentNode = rootNode; return true;
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<TConfiguration>(sectionNode,
serializerOptions ?? defaultSerializerOptions());
return true;
}
}
} }
value = default; value = default;
@@ -160,6 +109,49 @@ public class ConfigurationSource<TConfiguration>(IConfigurationFile<TConfigurati
} }
} }
private void ApplyConfigurationUpdates(ref JsonNode? rootNode, JsonNode? valueNode, string section)
{
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(valueNode);
}
else
{
array[index] = MergeNodes(array[index], valueNode);
}
}
else
{
currentNode[lastKey] = MergeNodes(currentNode[lastKey], valueNode);
}
}
}
private JsonNode? CloneNode(JsonNode? node) private JsonNode? CloneNode(JsonNode? node)
{ {
if (node is null) if (node is null)
@@ -171,15 +163,29 @@ public class ConfigurationSource<TConfiguration>(IConfigurationFile<TConfigurati
return JsonNode.Parse(serialized); return JsonNode.Parse(serialized);
} }
private void EnsureFileExists(string? filePath)
{
if (filePath == null || File.Exists(filePath))
{
return;
}
string? directoryPath = Path.GetDirectoryName(filePath);
if (!string.IsNullOrEmpty(directoryPath))
{
Directory.CreateDirectory(directoryPath);
}
File.WriteAllText(filePath, "{}");
}
private JsonNode? MergeNodes(JsonNode? existingNode, JsonNode? newNode) private JsonNode? MergeNodes(JsonNode? existingNode, JsonNode? newNode)
{ {
newNode = CloneNode(newNode);
if (existingNode is JsonObject existingObject && newNode is JsonObject newObject) if (existingNode is JsonObject existingObject && newNode is JsonObject newObject)
{ {
foreach (KeyValuePair<string, JsonNode?> property in newObject) foreach (KeyValuePair<string, JsonNode?> property in newObject)
{ {
existingObject[property.Key] = MergeNodes(existingObject[property.Key], property.Value); existingObject[property.Key] = MergeNodes(existingObject[property.Key], CloneNode(property.Value));
} }
return existingObject; return existingObject;
@@ -188,7 +194,7 @@ public class ConfigurationSource<TConfiguration>(IConfigurationFile<TConfigurati
{ {
foreach (JsonNode? item in newArray) foreach (JsonNode? item in newArray)
{ {
existingArray.Add(item); existingArray.Add(CloneNode(item));
} }
return existingArray; return existingArray;
@@ -198,4 +204,4 @@ public class ConfigurationSource<TConfiguration>(IConfigurationFile<TConfigurati
return newNode; return newNode;
} }
} }
} }
@@ -15,22 +15,29 @@ public partial class ConfigurationValueViewModel<TConfiguration, TValue>(IServic
INotificationHandler<ChangedEventArgs<TConfiguration>> INotificationHandler<ChangedEventArgs<TConfiguration>>
where TConfiguration : class where TConfiguration : class
{ {
public Task Handle(ChangedEventArgs<TConfiguration> args) public async Task Handle(ChangedEventArgs<TConfiguration> 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); base.OnChanged(value);
} }
public override Task OnActivated()
{
Value = read(configuration);
return base.OnActivated();
}
} }
public partial class ConfigurationValueViewModel<TConfiguration, TValue, TItem> : public partial class ConfigurationValueViewModel<TConfiguration, TValue, TItem> :
@@ -41,10 +48,9 @@ public partial class ConfigurationValueViewModel<TConfiguration, TValue, TItem>
IDisposable IDisposable
{ {
private readonly TConfiguration configuration; private readonly TConfiguration configuration;
private readonly IWritableConfiguration<TConfiguration> writer;
private readonly Func<TConfiguration, TValue?> read; private readonly Func<TConfiguration, TValue?> read;
private readonly Action<TValue?, TConfiguration> write; private readonly Action<TValue?, TConfiguration> write;
private readonly IWritableConfiguration<TConfiguration> writer;
public ConfigurationValueViewModel(IServiceProvider provider, public ConfigurationValueViewModel(IServiceProvider provider,
IServiceFactory factory, IServiceFactory factory,
IMediator mediator, IMediator mediator,
@@ -86,20 +92,27 @@ public partial class ConfigurationValueViewModel<TConfiguration, TValue, TItem>
Value = value; Value = value;
} }
public Task Handle(ChangedEventArgs<TConfiguration> args) public async Task Handle(ChangedEventArgs<TConfiguration> 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); base.OnChanged(value);
} }
public override Task OnActivated()
{
Value = read(configuration);
return base.OnActivated();
}
} }
+1 -3
View File
@@ -9,6 +9,4 @@ public interface IConfigurationSource<TConfiguration>
void Set(TConfiguration value); void Set(TConfiguration value);
void Set(object value); void Set(object value);
} }
public interface IRemovable : IDisposable;
+9 -3
View File
@@ -141,10 +141,10 @@ public static class IHostBuilderExtension
} }
return new ConfigurationSource<TConfiguration>(provider.GetRequiredService<IConfigurationFile<TConfiguration>>(), return new ConfigurationSource<TConfiguration>(provider.GetRequiredService<IConfigurationFile<TConfiguration>>(),
section, defaultSerializer); section,
defaultSerializer);
}); });
//services.AddHostedService<ConfigurationMonitor<TConfiguration>>();
services.TryAddKeyedTransient<IConfigurationReader<TConfiguration>>(section, (provider, key) => services.TryAddKeyedTransient<IConfigurationReader<TConfiguration>>(section, (provider, key) =>
new ConfigurationReader<TConfiguration>(provider.GetRequiredKeyedService<IConfigurationSource<TConfiguration>>(key), new ConfigurationReader<TConfiguration>(provider.GetRequiredKeyedService<IConfigurationSource<TConfiguration>>(key),
provider.GetRequiredKeyedService<IConfigurationFactory<TConfiguration>>(key))); provider.GetRequiredKeyedService<IConfigurationFactory<TConfiguration>>(key)));
@@ -179,6 +179,12 @@ public static class IHostBuilderExtension
services.AddTransient(provider => services.AddTransient(provider =>
provider.GetRequiredKeyedService<IConfigurationDescriptor<TConfiguration>>(section).Value); provider.GetRequiredKeyedService<IConfigurationDescriptor<TConfiguration>>(section).Value);
services.AddHostedService(provider =>
new ConfigurationMonitor<TConfiguration>(section,
provider.GetRequiredService<IConfigurationFile<TConfiguration>>(),
provider.GetRequiredService<IServiceProvider>(),
provider.GetRequiredService<IPublisher>()));
} }
}); });
@@ -186,7 +192,7 @@ public static class IHostBuilderExtension
} }
public static IHostBuilder UseContentRoot(this IHostBuilder hostBuilder, public static IHostBuilder UseContentRoot(this IHostBuilder hostBuilder,
string contentRoot, string contentRoot,
bool createDirectory) bool createDirectory)
{ {
if (createDirectory) if (createDirectory)
+4
View File
@@ -0,0 +1,4 @@
namespace Toolkit.Foundation;
public interface IRemovable :
IDisposable;