Initial commit

pull/2/head
Matija Koželj 2 years ago
parent f80b6bfac9
commit e38eb7aa1c

@ -0,0 +1,6 @@
namespace Connected.Caching.Annotations;
[AttributeUsage(AttributeTargets.Property)]
public sealed class CacheKeyAttribute : Attribute
{
}

@ -0,0 +1,432 @@
using System.Collections.Concurrent;
using System.Collections.Immutable;
using Connected.Interop;
namespace Connected.Caching;
internal abstract class Cache : ICache
{
private bool _disposedValue;
private readonly ConcurrentDictionary<string, Entries> _items;
private readonly Task _scavenger;
private readonly CancellationTokenSource _cancel = new();
public event CacheInvalidateHandler? Invalidating;
public event CacheInvalidateHandler? Invalidated;
public Cache()
{
_scavenger = new Task(OnScaveging, _cancel.Token, TaskCreationOptions.LongRunning);
_items = new ConcurrentDictionary<string, Entries>();
_scavenger.Start();
}
private ConcurrentDictionary<string, Entries> Items => _items;
private CancellationTokenSource Cancel => _cancel;
private void OnScaveging()
{
var token = Cancel.Token;
while (!token.IsCancellationRequested)
{
try
{
foreach (var i in Items)
i.Value.Scave();
var empties = Items.Where(f => f.Value.Count == 0).Select(f => f.Key);
foreach (var i in empties)
Items.TryRemove(i, out _);
token.WaitHandle.WaitOne(TimeSpan.FromMinutes(1));
}
catch { }
}
}
public virtual bool IsEmpty(string key)
{
if (Items.TryGetValue(key, out Entries? value))
return value.Any();
return true;
}
public virtual bool Exists(string key)
{
return Items.ContainsKey(key);
}
public void CreateKey(string key)
{
if (Exists(key))
return;
Items.TryAdd(key, new Entries());
}
public IEnumerator<T>? GetEnumerator<T>(string key)
{
if (Items.TryGetValue(key, out Entries? value))
return value.GetEnumerator<T>();
return new List<T>().GetEnumerator();
}
public virtual ImmutableList<T>? All<T>(string key)
{
if (Items.TryGetValue(key, out Entries? value))
return value.All<T>();
return default;
}
public int Count(string key)
{
if (Items.TryGetValue(key, out Entries? value))
return value.Count;
return 0;
}
public virtual T? Get<T>(string key, Func<T, bool> predicate)
{
if (Items.TryGetValue(key, out Entries? value) && value.Get(predicate) is IEntry entry)
return GetValue<T>(entry);
return default;
}
public virtual async Task<T?> Get<T>(string key, Func<T, bool> predicate, Func<EntryOptions, Task<T>> retrieve)
{
if (Items.TryGetValue(key, out Entries? value) && value.Get(predicate) is IEntry entry)
return GetValue<T>(entry);
if (retrieve is null)
return default;
var options = new EntryOptions();
T instance = await retrieve(options);
if (EqualityComparer<T>.Default.Equals(instance, default))
{
if (!options.AllowNull)
return default;
}
if (string.IsNullOrWhiteSpace(options.Key))
throw new SysException(this, SR.ErrCacheKeyNull);
Set(key, options.Key, instance, options.Duration, options.SlidingExpiration);
return instance;
}
public virtual async Task<T?> Get<T>(string key, object id, Func<EntryOptions, Task<T>>? retrieve)
{
if (Items.TryGetValue(key, out Entries? value) && value.Get(id is null ? null : id.ToString()) is IEntry entry)
return GetValue<T>(entry);
if (retrieve is null)
return default;
var options = new EntryOptions
{
Key = id is null ? null : id.ToString()
};
T instance = await retrieve(options);
if (EqualityComparer<T>.Default.Equals(instance, default))
{
if (!options.AllowNull)
return default;
}
Set(key, options.Key, instance, options.Duration, options.SlidingExpiration);
return instance;
}
internal void ClearCore(string key)
{
if (Items.TryGetValue(key, out Entries? value))
value.Clear();
}
public virtual async Task Clear(string key)
{
if (Items.TryGetValue(key, out Entries? value))
value.Clear();
await Task.CompletedTask;
}
public virtual T? Get<T>(string key, object id)
{
if (Items.TryGetValue(key, out Entries? value) && value.Get(id is null ? null : id.ToString()) is IEntry entry)
return GetValue<T>(entry);
return default;
}
public IEntry? Get(string key, object id)
{
if (Items.TryGetValue(key, out Entries? value))
return value.Get(id is null ? null : id.ToString());
return default;
}
public virtual T? Get<T>(string key, Func<dynamic, bool> predicate)
{
if (Items.TryGetValue(key, out Entries? value) && value.Get(predicate) is IEntry entry)
return GetValue<T>(entry);
return default;
}
public virtual T? First<T>(string key)
{
if (Items.TryGetValue(key, out Entries? value) && value.First() is IEntry entry)
return GetValue<T>(entry);
return default;
}
public virtual ImmutableList<T>? Where<T>(string key, Func<T, bool> predicate)
{
if (Items.TryGetValue(key, out Entries? value))
return value.Where(predicate);
return default;
}
public void CopyTo(string key, object id, IEntry instance)
{
if (!Items.TryGetValue(key, out Entries? value))
{
value = new Entries();
if (!Items.TryAdd(key, value))
return;
}
value.Set(id is null ? null : id.ToString(), instance.Instance, instance.Duration, instance.SlidingExpiration);
}
public virtual T? Set<T>(string key, object id, T? instance)
{
return Set(key, id, instance, TimeSpan.Zero);
}
public virtual T? Set<T>(string key, object id, T? instance, TimeSpan duration)
{
return Set(key, id, instance, duration, false);
}
public virtual T? Set<T>(string key, object id, T? instance, TimeSpan duration, bool slidingExpiration)
{
if (!Items.TryGetValue(key, out Entries? value))
{
value = new Entries();
if (!Items.TryAdd(key, value))
return default;
}
value.Set(id is null ? null : id.ToString(), instance, duration, slidingExpiration);
return instance;
}
internal void RemoveCore(string key, object id)
{
if (id is null)
return;
if (Items.TryGetValue(key, out Entries? value))
value.Remove(id.ToString());
}
public virtual async Task Remove(string key, object id)
{
await Remove(key, id, true);
}
private async Task Remove(string key, object id, bool removing)
{
if (Items.TryGetValue(key, out Entries? value))
value.Remove(id is null ? null : id.ToString());
if (removing)
await OnRemove(key, id);
}
protected virtual async Task OnRemove(string key, object id)
{
await Task.CompletedTask;
}
public async Task Invalidate(string key, object id)
{
await InvalidateCore(key, id, false);
}
internal async Task InvalidateCore(string key, object id, bool fromNotification)
{
/*
* we store existing instance but it is not
* removed from the cache yet. This is because other
* threads can access this instance while we are
* retrieving a new version from the source
*/
var existing = Get<object>(key, id);
var args = new CacheEventArgs(id is null ? null : id.ToString(), key);
/*
* this two events invalidate that cache reference.
* note that if no new version exists the existing one
* is still available to other threads.
*/
try
{
Invalidating?.Invoke(args);
}
catch { }
try
{
if (!fromNotification)
await OnInvalidating(args);
await OnInvalidate(args);
}
catch { }
/*
* now find out if a new version has been set for the
* specified key
*/
var newInstance = Get<object>(key, id);
/*
* if no existing reference exists there is no need for
* removing anything
*/
if (existing is not null)
{
/*
* we have an existing instance. we are dealing with two possible scenarios:
* - newInstance is null because entity has been deleted
* - newInstance is actually the same instance as the existing which means a new
* version does not exist. In both cases we must remove existing reference because
* at this point it is not valid anymore.
* note that the third case exists: reference has been replaced. in that case there
* is nothing to do because Invalidating events has already replaced reference with a
* new version.
*/
if (newInstance is null)
await Remove(key, id, false);
else if (existing.Equals(newInstance) && args.Behavior == InvalidateBehavior.RemoveSameInstance)
await Remove(key, id, false);
}
try
{
Invalidated?.Invoke(args);
}
catch { }
}
protected internal virtual async Task OnInvalidating(CacheEventArgs e)
{
await Task.CompletedTask;
}
protected virtual async Task OnInvalidate(CacheEventArgs e)
{
await Task.CompletedTask;
}
private void Clear()
{
foreach (var i in Items)
i.Value.Clear();
Items.Clear();
}
public virtual async Task<ImmutableList<string>?> Remove<T>(string key, Func<T, bool> predicate)
{
if (Items.TryGetValue(key, out Entries? value))
{
var result = value.Remove(predicate);
if (result is not null && result.Any())
await OnRemove(key, result);
}
return default;
}
protected virtual async Task OnRemove(string key, ImmutableList<string> ids)
{
await Task.CompletedTask;
}
public ImmutableList<string>? Keys(string key)
{
if (Items.TryGetValue(key, out Entries? value))
return value.Keys;
return default;
}
public ImmutableList<string> Keys()
{
return Items.Keys.ToImmutableList();
}
public bool Any(string key)
{
if (Items.TryGetValue(key, out Entries? value))
return value.Any();
return false;
}
private static T? GetValue<T>(IEntry entry)
{
if (entry is null || entry.Instance is null)
return default;
if (TypeConversion.TryConvert(entry.Instance, out T? result))
return result;
return default;
}
protected virtual void OnDisposing(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
Cancel.Cancel();
Clear();
if (_scavenger is not null)
{
_cancel.Cancel();
if (_scavenger.IsCompleted)
_scavenger.Dispose();
}
}
_disposedValue = true;
}
}
public void Dispose()
{
OnDisposing(true);
GC.SuppressFinalize(this);
}
}

@ -0,0 +1,128 @@
using System.Collections;
using System.Collections.Immutable;
namespace Connected.Caching;
public abstract class CacheClient<TEntry, TKey> : ICacheClient<TEntry, TKey> where TEntry : class
{
protected CacheClient(ICachingService cachingService, string key)
{
if (cachingService is null)
throw new ArgumentException(nameof(cachingService));
CachingService = cachingService;
Key = key;
}
public string Key { get; }
protected bool IsDisposed { get; set; }
protected async Task Remove(TKey id)
{
if (id is null)
throw new ArgumentNullException(nameof(id));
await CachingService.Remove(Key, id);
}
protected async Task Remove(Func<TEntry, bool> predicate)
{
await CachingService.Remove(Key, predicate);
}
protected async Task Refresh(TKey id)
{
if (id is null)
throw new ArgumentNullException(nameof(id));
await CachingService.Invalidate(Key, id);
}
protected ICachingService CachingService { get; }
public int Count => CachingService.Count(Key);
protected virtual ICollection<string>? Keys => CachingService.Keys(Key);
protected virtual Task<ImmutableList<TEntry>?> All()
{
return Task.FromResult(CachingService.All<TEntry>(Key));
}
protected virtual async Task<TEntry?> Get(TKey id, Func<EntryOptions, Task<TEntry>> retrieve)
{
if (id is null)
throw new ArgumentNullException(nameof(id));
return await CachingService.Get(Key, id, retrieve);
}
protected virtual Task<TEntry?> Get(TKey id)
{
if (id is null)
throw new ArgumentNullException(nameof(id));
return Task.FromResult(CachingService.Get<TEntry>(Key, id));
}
protected virtual Task<TEntry?> First()
{
return Task.FromResult(CachingService.First<TEntry>(Key));
}
protected virtual async Task<TEntry?> Get(Func<TEntry, bool> predicate)
{
return await CachingService.Get(Key, predicate, null);
}
protected virtual Task<ImmutableList<TEntry>?> Where(Func<TEntry, bool> predicate)
{
return Task.FromResult(CachingService.Where(Key, predicate));
}
protected virtual void Set(TKey id, TEntry instance)
{
if (id is null)
throw new ArgumentNullException(nameof(id));
CachingService.Set(Key, id, instance);
}
protected virtual void Set(TKey id, TEntry instance, TimeSpan duration)
{
if (id is null)
throw new ArgumentNullException(nameof(id));
CachingService.Set(Key, id, instance, duration);
}
private void Dispose(bool disposing)
{
if (!IsDisposed)
{
if (disposing)
OnDisposing();
IsDisposed = true;
}
}
protected virtual void OnDisposing()
{
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
public virtual IEnumerator<TEntry> GetEnumerator()
{
return CachingService?.GetEnumerator<TEntry>(Key);
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}

@ -0,0 +1,220 @@
using System.Collections.Immutable;
using System.Reflection;
using Connected.Interop;
using Connected.ServiceModel.Transactions;
namespace Connected.Caching;
internal class CacheContext : Cache, ICacheContext
{
public CacheContext(ICachingService cachingService, ITransactionContext transactionContext)
{
CachingService = cachingService;
TransactionContext = transactionContext;
TransactionContext.StateChanged += OnTransactionContextStateChanged;
}
private void OnTransactionContextStateChanged(object? sender, EventArgs e)
{
if (TransactionContext.State == MiddlewareTransactionState.Committing)
Flush();
}
private ICachingService CachingService { get; }
private ITransactionContext TransactionContext { get; }
public override bool Exists(string key)
{
return base.Exists(key) || (CachingService is not null && CachingService.Exists(key));
}
public override bool IsEmpty(string key)
{
return base.IsEmpty(key) || (CachingService is not null && CachingService.IsEmpty(key));
}
public override ImmutableList<T>? All<T>(string key)
{
return Merge(base.All<T>(key), CachingService?.All<T>(key));
}
public override async Task<T> Get<T>(string key, object id, Func<EntryOptions, Task<T>>? retrieve)
{
if (!TransactionContext.IsDirty)
{
if (retrieve is null)
return default;
return await CachingService.Get(key, id, retrieve);
}
return await base.Get(key, id, (f) =>
{
var shared = CachingService.Get<T>(key, id);
if (shared is not null)
return Task.FromResult(shared);
if (retrieve is null)
return default;
return retrieve(f);
});
}
public override async Task<T> Get<T>(string key, Func<T, bool> predicate, Func<EntryOptions, Task<T>>? retrieve)
{
if (!TransactionContext.IsDirty)
{
if (retrieve is null)
return default;
return await CachingService.Get(key, predicate, retrieve);
}
return await base.Get(key, predicate, (f) =>
{
var shared = CachingService.Get(key, predicate);
if (shared is not null)
return Task.FromResult(shared);
return retrieve(f);
});
}
public override T Get<T>(string key, object id)
{
var contextItem = base.Get<T>(key, id);
if (contextItem is not null)
return contextItem;
return CachingService.Get<T>(key, id);
}
public override T Get<T>(string key, Func<T, bool> predicate)
{
var contextItem = base.Get<T>(key, predicate);
if (contextItem is not null)
return contextItem;
return CachingService.Get(key, predicate);
}
public override async Task Clear(string key)
{
await base.Clear(key);
await CachingService.Clear(key);
}
public override T First<T>(string key)
{
if (base.First<T>(key) is T result)
return result;
return CachingService.First<T>(key);
}
public override ImmutableList<T> Where<T>(string key, Func<T, bool> predicate)
{
return Merge(base.Where(key, predicate), CachingService.Where(key, predicate));
}
public override T Set<T>(string key, object id, T instance)
{
if (!TransactionContext.IsDirty)
return CachingService.Set(key, id, instance);
return base.Set(key, id, instance);
}
public override T Set<T>(string key, object id, T instance, TimeSpan duration)
{
if (!TransactionContext.IsDirty)
return CachingService.Set(key, id, instance, duration);
return base.Set(key, id, instance, duration);
}
public override T Set<T>(string key, object id, T instance, TimeSpan duration, bool slidingExpiration)
{
if (!TransactionContext.IsDirty)
return CachingService.Set(key, id, instance, duration, slidingExpiration);
return base.Set(key, id, instance, duration, slidingExpiration);
}
public override async Task Remove(string key, object id)
{
await base.Remove(key, id);
await CachingService.Remove(key, id);
}
public override async Task<ImmutableList<string>?> Remove<T>(string key, Func<T, bool> predicate)
{
var local = await base.Remove(key, predicate);
var shared = await CachingService.Remove(key, predicate);
if (local is not null && shared is not null)
return local.AddRange(shared);
return local is not null ? local : shared;
}
public void Flush()
{
CachingService.Merge(this);
}
private static ImmutableList<T>? Merge<T>(ImmutableList<T>? contextItems, ImmutableList<T>? sharedItems)
{
if (contextItems is null)
return sharedItems;
var result = new List<T>(contextItems);
foreach (var sharedItem in sharedItems)
{
if (sharedItem is null)
continue;
if (CachingExtensions.GetCacheKeyProperty(sharedItem) is not PropertyInfo cacheProperty)
{
//Q: should we compare every property and add only if not matched?
contextItems.Add(sharedItem);
continue;
}
if (FindExisting(cacheProperty.GetValue(sharedItems), contextItems) is null)
result.Add(sharedItem);
}
return result.ToImmutableList();
}
private static T? FindExisting<T>(object value, ImmutableList<T> items)
{
if (items is null || items.IsEmpty)
return default;
if (CachingExtensions.GetCacheKeyProperty(items[0]) is not PropertyInfo cacheProperty)
return default;
foreach (var item in items)
{
var id = cacheProperty.GetValue(item);
if (TypeComparer.Compare(id, value))
return item;
}
return default;
}
protected override async Task OnInvalidate(CacheEventArgs e)
{
await CachingService.Invalidate(e.Key, e.Id);
}
}

@ -0,0 +1,27 @@
namespace Connected.Caching;
public enum InvalidateBehavior : byte
{
RemoveSameInstance = 1,
KeepSameInstance = 2
}
public class CacheEventArgs : EventArgs
{
public CacheEventArgs(string id, string key)
{
Key = key;
Id = id;
}
public CacheEventArgs(string id, string key, InvalidateBehavior behavior)
{
Key = key;
Id = id;
Behavior = behavior;
}
public string Id { get; init; }
public string Key { get; init; }
public InvalidateBehavior Behavior { get; set; } = InvalidateBehavior.RemoveSameInstance;
}

@ -0,0 +1,16 @@
namespace Connected.Caching;
public class CacheNotificationArgs
{
public CacheNotificationArgs(string method)
{
if (string.IsNullOrWhiteSpace(method))
throw new ArgumentException(null, nameof(method));
Method = method;
}
public string? Key { get; init; }
public List<string>? Ids { get; init; }
public string Method { get; }
}

@ -0,0 +1,13 @@
using System.Reflection;
using Connected.Caching.Annotations;
using Connected.Interop;
namespace Connected.Caching;
public static class CachingExtensions
{
public static PropertyInfo? GetCacheKeyProperty(object instance)
{
return Properties.GetPropertyAttribute<CacheKeyAttribute>(instance);
}
}

@ -0,0 +1,151 @@
using System.Collections.Immutable;
using Connected.Caching.Net;
using Connected.Net.Server;
namespace Connected.Caching;
internal sealed class CachingService : MemoryCache, ICachingService, IDisposable, IAsyncDisposable
{
public CachingService(IEndpointServer server, CacheServer state, CacheServerConnection backplaneClient)
{
if (server is null)
throw new ArgumentException(null, nameof(server));
if (state is null)
throw new ArgumentException(null, nameof(state));
BackplaneClient = backplaneClient;
Server = server;
BackplaneServer = state;
server.Changed += OnServerChanged;
server.Initialized += OnServerInitialized;
BackplaneServer.Received += OnReceived;
BackplaneClient.Received += OnReceived;
}
private CacheServerConnection BackplaneClient { get; set; }
private CacheServer BackplaneServer { get; }
private IEndpointServer Server { get; }
private async void OnServerInitialized(object? sender, EventArgs e)
{
await Initialize();
}
public async Task Initialize()
{
await BackplaneClient.Disconnect();
try
{
if (!await Server.IsServer())
{
await BackplaneClient.Initialize(Server.ServerUrl);
await BackplaneClient.Connect();
}
}
catch
{
// Server probably not initalized yet
}
}
private async void OnReceived(object? sender, CacheNotificationArgs e)
{
if (string.Equals(e.Method, nameof(Clear), StringComparison.Ordinal))
ClearCore(e.Key);
else if (string.Equals(e.Method, nameof(Remove), StringComparison.Ordinal))
{
if (e.Ids is not null && e.Ids.Any())
{
foreach (var id in e.Ids)
RemoveCore(e.Key, id);
}
}
else if (string.Equals(e.Method, nameof(Invalidate), StringComparison.Ordinal))
{
if (e.Ids is not null && e.Ids.Any())
{
foreach (var id in e.Ids)
await InvalidateCore(e.Key, id, true);
}
}
}
private async void OnServerChanged(object? sender, ServerChangedArgs e)
{
await Initialize();
}
public override async Task Clear(string key)
{
await base.Clear(key);
var args = new CacheNotificationArgs(nameof(Clear)) { Key = key };
if (await Server.IsServer())
await BackplaneServer.Send(args);
else
await BackplaneClient.Notify(nameof(Clear), args);
}
protected internal override async Task OnInvalidating(CacheEventArgs e)
{
var args = new CacheNotificationArgs(nameof(Invalidate))
{
Ids = new List<string> { e.Id },
Key = e.Key
};
if (await Server.IsServer())
await BackplaneServer.Send(args);
else
await BackplaneClient.Notify(nameof(Invalidate), args);
}
protected override async Task OnRemove(string key, ImmutableList<string> ids)
{
await base.OnRemove(key, ids);
var args = new CacheNotificationArgs(nameof(Remove))
{
Ids = ids.ToList(),
Key = key
};
if (await Server.IsServer())
await BackplaneServer.Send(args);
else
await BackplaneClient.Notify(nameof(Remove), args);
}
protected override async Task OnRemove(string key, object? id)
{
await base.OnRemove(key, id);
var ids = new List<string>();
if (id is not null)
ids.Add(id.ToString());
await OnRemove(key, ids.ToImmutableList());
}
public async ValueTask DisposeAsync()
{
Server.Changed -= OnServerChanged;
Server.Initialized -= OnServerInitialized;
}
protected override void OnDisposing(bool disposing)
{
Server.Changed -= OnServerChanged;
Server.Initialized -= OnServerInitialized;
base.OnDisposing(disposing);
}
}

@ -0,0 +1,32 @@
using Connected.Annotations;
using Connected.Caching.Net;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
[assembly: MicroService(MicroServiceType.Sys)]
namespace Connected.Caching;
internal class CachingStartup : Startup
{
public const string CachingHub = "/caching";
protected override void OnConfigure(WebApplication app)
{
app.MapHub<CacheHub>(CachingHub);
}
protected override void OnConfigureServices(IServiceCollection services)
{
services.AddSingleton(typeof(CacheServer));
services.AddSingleton(typeof(CacheServerConnection));
services.AddSingleton(typeof(ICachingService), typeof(CachingService));
services.AddScoped(typeof(ICacheContext), typeof(CacheContext));
}
protected override async Task OnInitialize(Dictionary<string, string> args)
{
if (Services is not null && Services.GetService<ICachingService>() is ICachingService service)
await service.Initialize();
}
}

@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0-rc.2.22472.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Connected\Connected\Connected.csproj" />
<ProjectReference Include="..\Connected.Interop\Connected.Interop.csproj" />
<ProjectReference Include="..\Connected.Net\Connected.Net.csproj" />
<ProjectReference Include="..\Connected.Runtime\Connected.Runtime.csproj" />
<ProjectReference Include="..\Connected.Threading\Connected.Threading.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="SR.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>SR.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="SR.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>SR.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project>

@ -0,0 +1,260 @@
using System.Collections.Concurrent;
using System.Collections.Immutable;
using Connected.Interop;
namespace Connected.Caching;
internal class Entries
{
private readonly Lazy<ConcurrentDictionary<string, IEntry>> _items = new();
private ConcurrentDictionary<string, IEntry> Items => _items.Value;
public ImmutableList<string> Keys => Items.Keys.ToImmutableList();
public int Count => Items.Count;
public bool Any()
{
return Items.Any();
}
public void Scave()
{
var expired = new HashSet<string>();
foreach (var i in Items)
{
var r = i.Value;
if (r is null || r.Expired)
expired.Add(i.Key);
}
foreach (var i in expired)
Remove(i);
}
public ImmutableList<T> All<T>()
{
var r = new List<T>();
var expired = Items.Where(f => f.Value.Expired);
foreach (var e in expired)
Remove(e.Value.Id);
var instances = Items.Select(f => f.Value.Instance);
foreach (var i in instances)
{
if (TypeConversion.TryConvert(i, out T? result) && result is not null)
r.Add(result);
}
return r.ToImmutableList();
}
public void Remove(string key)
{
if (Items.IsEmpty)
return;
if (Items.TryRemove(key, out IEntry? v))
v.Dispose();
}
public void Set(string key, object? instance, TimeSpan duration, bool slidingExpiration)
{
Items[key] = new Entry(key, instance, duration, slidingExpiration);
}
public IEnumerator<T> GetEnumerator<T>()
{
return new EntryEnumerator<T>(Items);
}
public IEntry? Get(string key)
{
return Find(key);
}
public IEntry? First()
{
if (!Any())
return default;
return Items.First().Value;
}
public IEntry? Get<T>(Func<T, bool> predicate)
{
return Find(predicate);
}
public IEntry? Get<T>(Func<dynamic, bool> predicate)
{
return Find<T>(predicate);
}
public ImmutableList<string>? Remove<T>(Func<T, bool> predicate)
{
if (Where(predicate) is not ImmutableList<string> ds || ds.IsEmpty)
return default;
var result = new HashSet<string>();
foreach (var i in ds)
{
var key = Items.FirstOrDefault(f => InstanceEquals(f.Value?.Instance, i)).Key;
RemoveInternal(key);
result.Add(key);
}
return result.ToImmutableList();
}
public ImmutableList<T>? Where<T>(Func<T, bool> predicate)
{
var values = Items.Select(f => f.Value.Instance).Cast<T>();
if (values is null || !values.Any())
return default;
var filtered = values.Where(predicate);
if (filtered is null || !filtered.Any())
return default;
var r = new List<T>();
foreach (var i in filtered)
{
var ce = Items.FirstOrDefault(f => InstanceEquals(f.Value?.Instance, i));
if (ce.Value is null)
continue;
if (ce.Value.Expired)
{
RemoveInternal(ce.Value.Id);
continue;
}
ce.Value.Hit();
r.Add(i);
}
return r.ToImmutableList();
}
private void RemoveInternal(string key)
{
if (Items.TryRemove(key, out IEntry? d))
d.Dispose();
}
private IEntry? Find<T>(Func<T, bool> predicate)
{
var instances = Items.Select(f => f.Value?.Instance).Cast<T>();
if (instances is null || !instances.Any())
return default;
if (instances.FirstOrDefault(predicate) is not T instance)
return default;
var ce = Items.Values.FirstOrDefault(f => InstanceEquals(f.Instance, instance));
if (ce is null)
return default;
if (ce.Expired)
{
RemoveInternal(ce.Id);
return default;
}
ce.Hit();
return ce;
}
private IEntry? Find<T>(Func<dynamic, bool> predicate)
{
var instances = Items.Select(f => f.Value?.Instance).Cast<dynamic>();
if (instances is null || !instances.Any())
return default;
if (instances.FirstOrDefault(predicate) is not T instance)
return default;
if (Items.Values.FirstOrDefault(f => InstanceEquals(f.Instance, instance)) is not IEntry ce)
return default;
if (ce.Expired)
{
RemoveInternal(ce.Id);
return default;
}
ce.Hit();
return ce;
}
private IEntry? Find(string key)
{
if (!Items.ContainsKey(key))
return default;
if (Items.TryGetValue(key, out IEntry? d))
{
if (d.Expired)
{
RemoveInternal(key);
return default;
}
d.Hit();
return d;
}
else
{
RemoveInternal(key);
return default;
}
}
public bool Exists(string key)
{
return Find(key) is not null;
}
public void Clear()
{
foreach (var i in Items)
Remove(i.Key);
}
private static bool InstanceEquals(object? left, object? right)
{
/*
* TODO: implement IEquality check
*/
if (left is null || right is null)
return false;
if (left.GetType().IsPrimitive)
return left == right;
if (left is string && right is string)
return string.Compare(left.ToString(), right.ToString(), false) == 0;
if (left.GetType().IsValueType && right.GetType().IsValueType)
return left.Equals(right);
return ReferenceEqualityComparer.Instance.Equals(left, right);
}
}

@ -0,0 +1,35 @@
namespace Connected.Caching;
internal class Entry : IEntry
{
public Entry(string id, object? instance, TimeSpan duration, bool slidingExpiration)
{
Id = id;
Instance = instance;
SlidingExpiration = slidingExpiration;
Duration = duration;
if (Duration > TimeSpan.Zero)
ExpirationDate = DateTime.UtcNow.AddTicks(duration.Ticks);
}
public bool SlidingExpiration { get; }
private DateTime ExpirationDate { get; set; }
public TimeSpan Duration { get; set; }
public object? Instance { get; }
public string Id { get; }
public bool Expired => ExpirationDate != DateTime.MinValue && ExpirationDate < DateTime.UtcNow;
public void Hit()
{
if (SlidingExpiration && Duration > TimeSpan.Zero)
ExpirationDate = DateTime.UtcNow.AddTicks(Duration.Ticks);
}
public void Dispose()
{
if (Instance is IDisposable disposable)
disposable.Dispose();
}
}

@ -0,0 +1,43 @@
using System.Collections;
using System.Collections.Concurrent;
using Connected.Interop;
namespace Connected.Caching;
internal class EntryEnumerator<T> : IEnumerator<T>
{
public EntryEnumerator(ConcurrentDictionary<string, IEntry> items)
{
Items = items;
Index = -1;
}
private int Count => Items.Count;
private int Index { get; set; }
private ConcurrentDictionary<string, IEntry> Items { get; }
public T Current => TypeConversion.TryConvert(Items.ElementAt(Index).Value.Instance, out T result) ? result : default;
object IEnumerator.Current => Current;
public void Dispose()
{
}
public bool MoveNext()
{
if (Index < Count - 1)
{
Index++;
return true;
}
return false;
}
public void Reset()
{
Index = -1;
}
}

@ -0,0 +1,16 @@
namespace Connected.Caching;
public class EntryOptions
{
public string Key { get; set; }
public string? KeyProperty { get; set; }
public TimeSpan Duration { get; set; }
public bool SlidingExpiration { get; set; }
public bool AllowNull { get; set; }
public EntryOptions()
{
Duration = TimeSpan.FromMinutes(5);
SlidingExpiration = true;
}
}

@ -0,0 +1,39 @@
using System.Collections.Immutable;
namespace Connected.Caching;
public delegate void CacheInvalidateHandler(CacheEventArgs e);
public interface ICache : IDisposable
{
event CacheInvalidateHandler? Invalidating;
event CacheInvalidateHandler? Invalidated;
ImmutableList<T>? All<T>(string key);
Task<T?> Get<T>(string key, object id, Func<EntryOptions, Task<T?>>? retrieve);
T? Get<T>(string key, object id);
IEntry? Get(string key, object id);
Task<T?> Get<T>(string key, Func<T, bool> predicate, Func<EntryOptions, Task<T?>>? retrieve);
T? Get<T>(string key, Func<T, bool> predicate);
T? First<T>(string key);
IEnumerator<T>? GetEnumerator<T>(string key);
ImmutableList<T>? Where<T>(string key, Func<T, bool> predicate);
bool Exists(string key);
bool IsEmpty(string key);
void CreateKey(string key);
Task Clear(string key);
T? Set<T>(string key, object id, T? instance);
T? Set<T>(string key, object id, T? instance, TimeSpan duration);
T? Set<T>(string key, object id, T? instance, TimeSpan duration, bool slidingExpiration);
void CopyTo(string key, object id, IEntry entry);
Task<ImmutableList<string>?> Remove<T>(string key, Func<T, bool> predicate);
Task Remove(string key, object id);
Task Invalidate(string key, object id);
int Count(string key);
bool Any(string key);
ImmutableList<string>? Keys(string key);
ImmutableList<string>? Keys();
}

@ -0,0 +1,7 @@
namespace Connected.Caching;
public interface ICacheClient<TEntry, TKey> : IEnumerable<TEntry>, IDisposable
{
string Key { get; }
int Count { get; }
}

@ -0,0 +1,6 @@
namespace Connected.Caching;
public interface ICacheContext : ICache
{
void Flush();
}

@ -0,0 +1,7 @@
namespace Connected.Caching;
public interface ICachingService : ICache
{
void Merge(ICache cache);
Task Initialize();
}

@ -0,0 +1,11 @@
namespace Connected.Caching;
public interface IEntry : IDisposable
{
object? Instance { get; }
string Id { get; }
bool Expired { get; }
TimeSpan Duration { get; }
bool SlidingExpiration { get; }
void Hit();
}

@ -0,0 +1,6 @@
namespace Connected.Caching;
public interface IMemoryCache : ICache
{
void Merge(ICache cache);
}

@ -0,0 +1,6 @@
namespace Connected.Caching;
public interface IStatefulCacheClient<TEntry, TKey> : ICacheClient<TEntry, TKey>
{
event CacheInvalidateHandler Invalidate;
}

@ -0,0 +1,24 @@
using System.Collections.Immutable;
namespace Connected.Caching;
internal class MemoryCache : Cache, IMemoryCache
{
public void Merge(ICache cache)
{
if (cache.Keys() is not ImmutableList<string> keys)
return;
foreach (var key in keys)
{
if (cache.Keys(key) is not ImmutableList<string> entryKeys)
continue;
foreach (var entryKey in entryKeys)
{
if (cache.Get(key, entryKey) is IEntry entry)
CopyTo(key, entryKey, entry);
}
}
}
}

@ -0,0 +1,11 @@
using Connected.Net.Hubs;
namespace Connected.Caching.Net;
//TODO: implement authorization and add logic to reject connections if not an endpoint server
internal class CacheHub : StatefulHub<CacheNotificationArgs>
{
public CacheHub(CacheServer server) : base(server)
{
}
}

@ -0,0 +1,11 @@
using Connected.Net.Hubs;
using Microsoft.AspNetCore.SignalR;
namespace Connected.Caching.Net;
internal class CacheServer : Server<CacheHub, CacheNotificationArgs>
{
public CacheServer(IHubContext<CacheHub> hub) : base(hub)
{
}
}

@ -0,0 +1,36 @@
using Connected.Net.Hubs;
using Connected.Net.Messaging;
using Connected.Net.Server;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.Logging;
namespace Connected.Caching.Net;
internal class CacheServerConnection : ServerConnection
{
public event EventHandler<CacheNotificationArgs>? Received;
public CacheServerConnection(IEndpointServer server, ILogger<CacheServerConnection> logger) : base(server)
{
Logger = logger;
}
private ILogger<CacheServerConnection> Logger { get; }
public override async Task Initialize(string hubUrl)
{
await base.Initialize(hubUrl);
Connection.On<MessageAcknowledgeArgs, CacheNotificationArgs>("Notify", (a, e) =>
{
Connection.InvokeAsync(nameof(CacheHub.Acknowledge), a);
Received?.Invoke(this, e);
});
Connection.On<ServerExceptionArgs>("Exception", (e) =>
{
Logger.LogError("Caching hub exception: {message}", e.Message);
});
}
}

@ -0,0 +1,11 @@
using Connected.Net.Hubs;
using Microsoft.AspNetCore.SignalR;
namespace Connected.Caching.Net;
internal sealed class CacheWorker : ServerWorker<CacheNotificationArgs, CacheHub>
{
public CacheWorker(CacheServer server, IHubContext<CacheHub> hub) : base(server, hub)
{
}
}

@ -0,0 +1,72 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Connected.Caching {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class SR {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal SR() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Server.Caching.SR", typeof(SR).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Cache Key property not set. Please set &apos;Key&apos; property before returning value from cache retrieve handler or set CacheKeyAttribute on at least one property..
/// </summary>
internal static string ErrCacheKeyNull {
get {
return ResourceManager.GetString("ErrCacheKeyNull", resourceCulture);
}
}
}
}

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ErrCacheKeyNull" xml:space="preserve">
<value>Cache Key property not set. Please set 'Key' property before returning value from cache retrieve handler or set CacheKeyAttribute on at least one property.</value>
</data>
</root>

@ -0,0 +1,113 @@
using System.Collections.Immutable;
using System.Globalization;
using Connected.Interop;
using Connected.Threading;
namespace Connected.Caching;
public abstract class StatefulCacheClient<TEntry, TKey> : CacheClient<TEntry, TKey>, IStatefulCacheClient<TEntry, TKey> where TEntry : class
{
public event CacheInvalidateHandler Invalidate;
protected StatefulCacheClient(ICachingService cachingService, string key) : base(cachingService, key)
{
Locker = new AsyncLockerSlim();
CachingService.Invalidating += OnInvalidate;
}
private AsyncLockerSlim? Locker { get; set; }
protected virtual InvalidateBehavior InvalidateBehavior { get; } = InvalidateBehavior.KeepSameInstance;
private bool Initialized { get; set; }
private async void OnInvalidate(CacheEventArgs e)
{
if (string.Equals(e.Key, Key, StringComparison.OrdinalIgnoreCase))
{
if (Initialized)
await OnInvalidate(TypeConversion.Convert<TKey>(e.Id, CultureInfo.InvariantCulture));
Invalidate?.Invoke(e);
e.Behavior = InvalidateBehavior;
}
}
protected virtual async Task OnInvalidate(TKey id)
{
await Task.CompletedTask;
}
protected virtual async Task OnInitializing()
{
await Task.CompletedTask;
}
protected async Task Initialize()
{
if (Initialized || IsDisposed || Locker is null)
return;
await Locker.LockAsync(async () =>
{
if (Initialized || IsDisposed)
return;
await OnInitializing();
Initialized = true;
});
if (Initialized)
await OnInitialized();
}
protected virtual async Task OnInitialized()
{
await Task.CompletedTask;
}
protected override async Task<ImmutableList<TEntry>?> All()
{
await Initialize();
return base.All().Result;
}
protected override async Task<TEntry?> First()
{
await Initialize();
return await base.First();
}
protected override async Task<TEntry?> Get(Func<TEntry, bool> predicate)
{
await Initialize();
return await base.Get(predicate);
}
protected override async Task<TEntry?> Get(TKey id)
{
await Initialize();
return await base.Get(id);
}
protected override async Task<TEntry?> Get(TKey id, Func<EntryOptions, Task<TEntry>> retrieve)
{
await Initialize();
return await base.Get(id, retrieve);
}
protected override async Task<ImmutableList<TEntry>?> Where(Func<TEntry, bool> predicate)
{
await Initialize();
return await base.Where(predicate);
}
protected override void OnDisposing()
{
if (Locker is not null)
{
Locker?.Dispose();
Locker = null;
}
}
public override IEnumerator<TEntry> GetEnumerator()
{
AsyncUtils.RunSync(Initialize);
return base.GetEnumerator();
}
}

@ -0,0 +1,76 @@
using System.Collections.Immutable;
using System.Reflection;
using Connected.Annotations;
namespace Connected.Collections;
public static class CollectionExtensions
{
public static ImmutableArray<TSource> ToImmutableArray<TSource>(this IEnumerable<TSource> items, bool performLock)
{
if (!performLock)
return items.ToImmutableArray();
lock (items)
return items.ToImmutableArray();
}
public static ImmutableList<TSource> ToImmutableList<TSource>(this IEnumerable<TSource> items, bool performLock)
{
if (!performLock)
return items.ToImmutableList();
lock (items)
return items.ToImmutableList();
}
public static void SortByOrdinal<TElement>(this List<TElement> items)
{
items.Sort((left, right) =>
{
var leftOrdinal = left is Type lt ? lt.GetCustomAttribute<OrdinalAttribute>() : left?.GetType().GetCustomAttribute<OrdinalAttribute>();
var rightOrdinal = right is Type rt ? rt.GetCustomAttribute<OrdinalAttribute>() : right?.GetType().GetCustomAttribute<OrdinalAttribute>();
if (leftOrdinal is null && rightOrdinal is null)
return 0;
if (leftOrdinal is not null && rightOrdinal is null)
return -1;
if (leftOrdinal is null && rightOrdinal is not null)
return 1;
if (leftOrdinal?.Value == rightOrdinal?.Value)
return 0;
else if (leftOrdinal?.Value < rightOrdinal?.Value)
return 1;
else
return -1;
});
}
public static void SortByPriority<TElement>(this List<TElement> items)
{
items.Sort((left, right) =>
{
var leftPriority = left is Type lt ? lt.GetCustomAttribute<PriorityAttribute>() : left?.GetType().GetCustomAttribute<PriorityAttribute>();
var rightPriority = right is Type rt ? rt.GetCustomAttribute<PriorityAttribute>() : right?.GetType().GetCustomAttribute<PriorityAttribute>();
if (leftPriority is null && rightPriority is null)
return 0;
if (leftPriority is not null && rightPriority is null)
return -1;
if (leftPriority is null && rightPriority is not null)
return 1;
if (leftPriority?.Value == rightPriority?.Value)
return 0;
else if (leftPriority?.Value > rightPriority?.Value)
return -1;
else
return 1;
});
}
}

@ -0,0 +1,6 @@
namespace Connected.Collections;
public static class CollectionRoutes
{
public const string Queue = "/sys/queue";
}

@ -0,0 +1,16 @@
using Connected.Annotations;
using Microsoft.AspNetCore.Builder;
[assembly: MicroService(MicroServiceType.Sys)]
namespace Connected.Collections;
internal class CollectionsStartup : Startup
{
public static WebApplication? Application { get; private set; }
protected override void OnConfigure(WebApplication app)
{
Application = app;
}
}

@ -0,0 +1,177 @@
using System.Collections.Concurrent;
using Connected;
using Microsoft.Extensions.DependencyInjection;
namespace Connected.Collections.Concurrent;
public abstract class Dispatcher<TArgs, TJob> : IDispatcher<TArgs, TJob>
where TJob : IDispatcherJob<TArgs>
{
private CancellationTokenSource _tokenSource;
protected Dispatcher(int size)
{
WorkerSize = size;
_tokenSource = new();
Queue = new();
Jobs = new();
QueuedDispatchers = new();
}
public CancellationToken CancellationToken => _tokenSource.Token;
private ConcurrentQueue<TArgs> Queue { get; set; }
private List<DispatcherJob<TArgs>> Jobs { get; set; }
protected bool IsDisposed { get; private set; }
private int WorkerSize { get; }
public int Available => Math.Max(0, WorkerSize * 4 - Queue.Count - QueuedDispatchers.Sum(f => f.Value.Count));
private ConcurrentDictionary<string, QueuedDispatcher<TArgs, TJob>> QueuedDispatchers { get; set; }
public DispatcherProcessBehavior Behavior => DispatcherProcessBehavior.Parallel;
public void Cancel()
{
_tokenSource?.Cancel();
}
public bool Dequeue(out TArgs? item)
{
return Queue.TryDequeue(out item);
}
public bool Enqueue(string queue, TArgs item)
{
if (EnsureDispatcher(queue) is not QueuedDispatcher<TArgs, TJob> dispatcher)
throw new SysException(this, $"{SR.ErrCreateQueuedDispatcher} ({queue})");
return dispatcher.Enqueue(item);
}
public bool Enqueue(TArgs item)
{
Queue.Enqueue(item);
if (Jobs.Count < WorkerSize)
{
/*
* Dispatcher jobs should be transient so it's safe to request a service from the root collection.
*/
if (CollectionsStartup.Application.Services.GetService<TJob>() is not DispatcherJob<TArgs> job)
throw new NullReferenceException($"{SR.ErrCreateService} ({typeof(DispatcherJob<TArgs>).Name})");
job.Completed += OnCompleted;
lock (Jobs)
{
Jobs.Add(job);
}
job.Run(Queue, CancellationToken);
}
return true;
}
private void OnCompleted(object? sender, EventArgs e)
{
if (sender is not DispatcherJob<TArgs> job)
return;
if (Queue.IsEmpty)
{
lock (Jobs)
{
Jobs.Remove(job);
}
job.Dispose();
}
else
job.Run(Queue, CancellationToken);
}
private QueuedDispatcher<TArgs, TJob>? EnsureDispatcher(string queueName)
{
if (QueuedDispatchers.TryGetValue(queueName, out QueuedDispatcher<TArgs, TJob>? result))
return result;
result = new QueuedDispatcher<TArgs, TJob>(this, queueName);
result.Completed += OnQueuedCompleted;
if (!QueuedDispatchers.TryAdd(queueName, result))
{
result.Completed -= OnQueuedCompleted;
if (QueuedDispatchers.TryGetValue(queueName, out QueuedDispatcher<TArgs, TJob>? retryResult))
return retryResult;
else
return default;
}
return result;
}
private void OnQueuedCompleted(object? sender, EventArgs e)
{
if (sender is not QueuedDispatcher<TArgs, TJob> dispatcher)
return;
if (dispatcher.Count > 0)
return;
QueuedDispatchers.Remove(dispatcher.QueueName, out _);
dispatcher.Dispose();
}
private void Dispose(bool disposing)
{
if (!IsDisposed)
{
if (disposing)
{
if (_tokenSource is not null)
{
if (!_tokenSource.IsCancellationRequested)
_tokenSource.Cancel();
_tokenSource.Dispose();
_tokenSource = null;
}
if (Queue is not null)
{
Queue.Clear();
Queue = null;
}
if (Jobs is not null)
{
foreach (var job in Jobs)
job.Dispose();
Jobs.Clear();
Jobs = null;
}
if (QueuedDispatchers is not null)
{
foreach (var dispatcher in QueuedDispatchers)
dispatcher.Value.Dispose();
QueuedDispatchers.Clear();
Queue = null;
}
}
IsDisposed = true;
}
}
protected virtual void OnDisposing()
{
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}

@ -0,0 +1,103 @@
using System.Collections.Concurrent;
using System.ComponentModel;
using Connected.Data;
namespace Connected.Collections.Concurrent;
/// <summary>
/// This class acts as a job unit of the <see cref="IDispatcher{T}"/>.
/// </summary>
/// <typeparam name="TArgs"></typeparam>
public abstract class DispatcherJob<TArgs> : IDispatcherJob<TArgs>, IDisposable
{
public event EventHandler? Completed;
public bool IsRunning { get; private set; }
protected bool IsDisposed { get; private set; }
private CancellationToken CancellationToken { get; set; }
internal void Run(ConcurrentQueue<TArgs> queue, CancellationToken cancellationToken)
{
CancellationToken = cancellationToken;
if (IsRunning)
return;
Task.Run(async () =>
{
IsRunning = true;
TArgs? item = default;
try
{
while (queue.TryDequeue(out item))
{
if (item is null)
continue;
if (item is IPopReceipt pr && pr.NextVisible <= DateTime.UtcNow)
continue;
await Invoke(item);
if (cancellationToken.IsCancellationRequested || IsDisposed)
break;
}
}
catch (Exception ex)
{
await HandleException(item, ex);
}
IsRunning = false;
Completed?.Invoke(this, EventArgs.Empty);
}, CancellationToken);
}
private void OnCompleted(object? sender, RunWorkerCompletedEventArgs e)
{
Completed?.Invoke(this, EventArgs.Empty);
}
public async Task Invoke(TArgs args)
{
await OnInvoke(args, CancellationToken);
}
protected virtual async Task OnInvoke(TArgs args, CancellationToken cancellationToken)
{
await Task.CompletedTask;
}
private async Task HandleException(TArgs? args, Exception ex)
{
await OnHandleEception(args, ex);
}
protected virtual async Task OnHandleEception(TArgs? args, Exception ex)
{
await Task.CompletedTask;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (IsDisposed)
return;
if (disposing)
OnDisposing();
IsDisposed = true;
}
protected virtual void OnDisposing()
{
}
}

@ -0,0 +1,19 @@
namespace Connected.Collections.Concurrent;
public enum DispatcherProcessBehavior
{
Parallel = 1,
Queued = 2
}
public interface IDispatcher<TArgs, TJob> : IDisposable
where TJob : IDispatcherJob<TArgs>
{
bool Dequeue(out TArgs? item);
bool Enqueue(TArgs item);
bool Enqueue(string queue, TArgs item);
DispatcherProcessBehavior Behavior { get; }
CancellationToken CancellationToken { get; }
void Cancel();
}

@ -0,0 +1,7 @@
namespace Connected.Collections.Concurrent;
public interface IDispatcherJob<TArgs> : IDisposable
{
Task Invoke(TArgs args);
bool IsRunning { get; }
}

@ -0,0 +1,99 @@
using System.Collections.Concurrent;
using Microsoft.Extensions.DependencyInjection;
namespace Connected.Collections.Concurrent;
internal sealed class QueuedDispatcher<TArgs, TJob> : IDispatcher<TArgs, TJob>
where TJob : IDispatcherJob<TArgs>
{
public event EventHandler? Completed;
public QueuedDispatcher(IDispatcher<TArgs, TJob> dispatcher, string queueName)
{
Dispatcher = dispatcher;
Queue = new();
QueueName = queueName;
/*
* Dispatcher jobs should be transient so it's safe to request a service from the root collection.
*/
if (CollectionsStartup.Application?.Services.GetService<DispatcherJob<TArgs>>() is not DispatcherJob<TArgs> job)
throw new SysException(this, $"{SR.ErrCreateService} ({typeof(DispatcherJob<TArgs>).Name})");
job.Completed += OnCompleted;
Job = job;
}
public CancellationToken CancellationToken => Dispatcher.CancellationToken;
public bool IsDisposed { get; private set; }
public DispatcherProcessBehavior Behavior => DispatcherProcessBehavior.Queued;
public string QueueName { get; }
private DispatcherJob<TArgs> Job { get; set; }
private IDispatcher<TArgs, TJob> Dispatcher { get; set; }
public int Count => Queue.Count;
private ConcurrentQueue<TArgs> Queue { get; set; }
public void Cancel()
{
}
public bool Dequeue(out TArgs? item)
{
return Queue.TryDequeue(out item);
}
public bool Enqueue(TArgs item)
{
if (IsDisposed)
return false;
Queue.Enqueue(item);
if (!Job.IsRunning)
Job.Run(Queue, CancellationToken);
return true;
}
private void OnCompleted(object? sender, EventArgs e)
{
if (sender is not DispatcherJob<TArgs> job)
return;
if (!Queue.IsEmpty)
{
job.Run(Queue, CancellationToken);
return;
}
Completed?.Invoke(this, EventArgs.Empty);
}
public bool Enqueue(string queue, TArgs args)
{
return Dispatcher.Enqueue(queue, args);
}
private void Dispose(bool disposing)
{
if (!IsDisposed)
{
if (disposing)
{
if (Job is not null)
{
Job.Dispose();
Job = null;
}
}
IsDisposed = true;
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}

@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Connected\Connected\Connected.csproj" />
<ProjectReference Include="..\Connected.Interop\Connected.Interop.csproj" />
<ProjectReference Include="..\Connected.Runtime\Connected.Runtime.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="SR.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>SR.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="SR.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>SR.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project>

@ -0,0 +1,33 @@
using Connected.Collections.Iterators;
namespace Connected.Collections;
public class Iterator
{
private IIterator _iterator;
public Iterator(object value)
{
if (value is null)
throw new ArgumentException(nameof(value));
if (DictionaryIterator.CanHandle(value))
_iterator = new DictionaryIterator(value);
else if (ListIterator.CanHandle(value))
_iterator = new ListIterator(value);
else if (ArrayIterator.CanHandle(value))
_iterator = new ArrayIterator(value);
}
public async Task<bool> MoveNext(Func<object, Task<object>> processor)
{
if (!_iterator.MoveNext())
return false;
if (await processor(_iterator.Current) is object value)
_iterator.Add(value);
return true;
}
public object? Result => _iterator.Result;
}

@ -0,0 +1,32 @@
namespace Connected.Collections.Iterators;
internal class ArrayIterator : IIterator
{
public ArrayIterator(object value)
{
}
public object? Result => throw new NotImplementedException();
public object Current => throw new NotImplementedException();
public static bool CanHandle(object value)
{
return value.GetType().IsArray;
}
public void Add(object value)
{
throw new NotImplementedException();
}
public bool MoveNext()
{
throw new NotImplementedException();
}
public void Reset()
{
throw new NotImplementedException();
}
}

@ -0,0 +1,75 @@
using System.Collections;
using System.Collections.Immutable;
using Connected.Interop;
namespace Connected.Collections.Iterators;
internal class DictionaryIterator : IIterator
{
private readonly object _value;
private IDictionary _result;
public DictionaryIterator(object value)
{
_value = value;
if (value is not IDictionary dictionary)
return;
Enumerator = dictionary.GetEnumerator();
CreateResult();
}
public static bool CanHandle(object value)
{
var arguments = value.GetType().GetGenericArguments();
var kvp = typeof(KeyValuePair<,>).MakeGenericType(arguments);
var en = typeof(IEnumerable<>).MakeGenericType(kvp);
return value.GetType() == en;
}
private void CreateResult()
{
if (_value.GetType() == typeof(IImmutableDictionary<,>))
_result = _value.GetType().MakeGenericType(_value.GetType().GenericTypeArguments).CreateInstance<IDictionary>();
}
private IDictionaryEnumerator? Enumerator { get; }
public object? Current => Enumerator?.Value;
public object? Result
{
get
{
if (_value is null)
return null;
throw new NotImplementedException();
}
}
public bool MoveNext()
{
if (Enumerator is null)
return false;
return Enumerator.MoveNext();
}
public void Reset()
{
if (Enumerator is null)
return;
Enumerator.Reset();
}
public void Add(object value)
{
if (Current is null || Enumerator is null)
throw new InvalidOperationException(SR.ErrIteratorCurrentNull);
_result.Add(Enumerator.Key, value);
}
}

@ -0,0 +1,10 @@
using System.Collections;
namespace Connected.Collections.Iterators;
internal interface IIterator : IEnumerator
{
void Add(object value);
object? Result { get; }
}

@ -0,0 +1,36 @@
namespace Connected.Collections.Iterators;
internal class ListIterator : IIterator
{
public ListIterator(object value)
{
}
public object? Result => throw new NotImplementedException();
public object Current => throw new NotImplementedException();
public static bool CanHandle(object value)
{
var arguments = value.GetType().GetGenericArguments();
var list = typeof(IList<>).MakeGenericType(arguments);
return value.GetType() == list;
}
public void Add(object value)
{
throw new NotImplementedException();
}
public bool MoveNext()
{
throw new NotImplementedException();
}
public void Reset()
{
throw new NotImplementedException();
}
}

@ -0,0 +1,6 @@
namespace Connected.Collections.Queues;
public interface IQueueClient<TArgs> : IMiddleware
where TArgs : QueueArgs
{
Task Invoke(IQueueMessage message, TArgs args);
}

@ -0,0 +1,48 @@
using Connected.Data;
namespace Connected.Collections.Queues;
/// <summary>
/// Represents a single queue message.
/// </summary>
/// <remarks>
/// A queue message represents a unit of queued or deferred work which
/// can be processed distributed anywhere or in any client which
/// has access to the Queue REST service.
/// </remarks>
public interface IQueueMessage : IPrimaryKey<long>, IPopReceipt
{
/// <summary>
/// Date date and time the queue message was created.
/// </summary>
DateTime Created { get; init; }
/// <summary>
/// The number of times the clients dequeued the message.
/// </summary>
/// <remarks>
/// There are numerous reasons why queue message gets dequeued multiple
/// times. It could be that not all conditions were met at the time
/// of processing or that queue message was not processed quich enough and
/// its pop receipt expired. In such cases message returns to the queue and
/// waits for the next client to dequeue it.
/// </remarks>
int DequeueCount { get; init; }
/// <summary>
/// The timestamp message was last dequeued.
/// </summary>
DateTime? DequeueTimestamp { get; init; }
/// <summary>
/// The queue to which the message belongs.
/// </summary>
/// <remarks>
/// Every queue client must specify which queue processes.
/// </remarks>
string Queue { get; init; }
/// <summary>
/// The arguments object which contains information about the message.
/// </summary>
/// <remarks>
/// Most queue messages do have an argument object, mostly providing na id of the
/// entity or record for which processing should be performed.
/// </remarks>
QueueArgs Arguments { get; init; }
}

@ -0,0 +1,34 @@
using System.Collections.Immutable;
using Connected.Annotations;
namespace Connected.Collections.Queues;
/// <summary>
/// Represents a distributed service for processing queue messages.
/// </summary>
/// <remarks>
/// Queue mechanism is mostly used as an internal logic of processes
/// and resources to offload work from the main thread to achieve better
/// responsiveness of the system. Aggregations and calculations are good
/// examples of queue usage. You should use queue whenever you must
/// perform any kind of work that is not necessary to perform it in a single
/// transaction scope.
/// </remarks>
[Service]
[ServiceUrl(CollectionRoutes.Queue)]
public interface IQueueService
{
/// <summary>
/// Enqueues the queue message.
/// </summary>
/// <typeparam name="TArgs">The type of the arguments used in queue message</typeparam>
/// <param name="args">The arguments containing information about a queue message.</param>
Task Enqueue<TClient, TArgs>(TArgs args)
where TClient : IQueueClient<TArgs>
where TArgs : QueueArgs;
/// <summary>
/// Dequeues the queue messages based on the provided arguments.
/// </summary>
/// <param name="args">The arguments containing information about dequeue criteria.</param>
/// <returns>A list of valid queue messages that can be immediatelly processed.S</returns>
Task<ImmutableList<IQueueMessage>> Dequeue(DequeueArgs args);
}

@ -0,0 +1,44 @@
using System.ComponentModel.DataAnnotations;
using Connected.Annotations;
using Connected.ServiceModel;
namespace Connected.Collections.Queues;
public class QueueArgs : Dto
{
public QueueArgs()
{
Options = new();
}
public EnqueueOptions Options { get; set; }
}
public class PrimaryKeyQueueArgs<TPrimaryKey> : QueueArgs
where TPrimaryKey : notnull
{
public TPrimaryKey Id { get; set; } = default!;
}
public sealed class EnqueueOptions
{
/// <summary>
/// The date and time the queue message expires.
/// </summary>
/// <remarks>
/// Queue messages that are not processed until they expire
/// gets automatically deleted by the system.
/// </remarks>
public DateTime Expire { get; set; } = DateTime.UtcNow.AddHours(48);
}
public sealed class DequeueArgs : Dto
{
[NonDefault]
public List<string> Queues { get; set; } = default!;
[Range(1, int.MaxValue)]
public int MaxCount { get; set; }
public TimeSpan NextVisible { get; set; }
}

@ -0,0 +1,90 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Connected.Collections {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class SR {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal SR() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Server.Collections.SR", typeof(SR).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Cannot create queued dispatcher.
/// </summary>
internal static string ErrCreateQueuedDispatcher {
get {
return ResourceManager.GetString("ErrCreateQueuedDispatcher", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Cannot create service instance.
/// </summary>
internal static string ErrCreateService {
get {
return ResourceManager.GetString("ErrCreateService", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Iterator does not have current value.
/// </summary>
internal static string ErrIteratorCurrentNull {
get {
return ResourceManager.GetString("ErrIteratorCurrentNull", resourceCulture);
}
}
}
}

@ -0,0 +1,129 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ErrCreateQueuedDispatcher" xml:space="preserve">
<value>Cannot create queued dispatcher</value>
</data>
<data name="ErrCreateService" xml:space="preserve">
<value>Cannot create service instance</value>
</data>
<data name="ErrIteratorCurrentNull" xml:space="preserve">
<value>Iterator does not have current value</value>
</data>
</root>

@ -0,0 +1,11 @@
namespace Connected.Configuration.Authentication;
internal class AuthenticationConfiguration : IAuthenticationConfiguration
{
public AuthenticationConfiguration()
{
JwToken = new JwTokenConfiguration();
}
public IJwTokenConfiguration JwToken { get; }
}

@ -0,0 +1,6 @@
namespace Connected.Configuration.Authentication;
public interface IAuthenticationConfiguration
{
IJwTokenConfiguration JwToken { get; }
}

@ -0,0 +1,10 @@
namespace Connected.Configuration.Authentication
{
public interface IJwTokenConfiguration
{
string Issuer { get; }
string Audience { get; }
string Key { get; }
int Duration { get; }
}
}

@ -0,0 +1,13 @@
namespace Connected.Configuration.Authentication
{
internal class JwTokenConfiguration : IJwTokenConfiguration
{
public string? Issuer { get; set; }
public string? Audience { get; set; }
public string? Key { get; set; } = "D78RF30487F4G0F8Z34F834F";
public int Duration { get; set; } = 30;
}
}

@ -0,0 +1,23 @@
using Connected.Configuration.Authentication;
using Connected.Configuration.Endpoints;
namespace Connected.Configuration;
internal class ConfigurationService : IConfigurationService
{
public ConfigurationService()
{
Endpoint = new EndpointConfiguration();
Storage = new StorageConfiguration();
}
public IEndpointConfiguration Endpoint { get; }
public IAuthenticationConfiguration Authentication => throw new NotImplementedException();
public IStorageConfiguration Storage { get; }
/*
* TODO: hardcoded
*/
public ProcessType Type => ProcessType.BackEnd;
}

@ -0,0 +1,16 @@
using Connected.Annotations;
using Connected.Configuration.Environment;
using Microsoft.Extensions.DependencyInjection;
[assembly: MicroService(MicroServiceType.Sys)]
namespace Connected.Configuration;
internal class ConfigurationStart : Startup
{
protected override void OnConfigureServices(IServiceCollection services)
{
services.AddSingleton(typeof(IConfigurationService), typeof(ConfigurationService));
services.AddSingleton(typeof(IEnvironmentService), typeof(EnvironmentService));
}
}

@ -0,0 +1,7 @@
namespace Connected.Configuration
{
public static class ConfigurationUrls
{
public const string Settings = "/configuration/settings";
}
}

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Connected.Runtime\Connected.Runtime.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,23 @@
using System.Collections.Immutable;
namespace Connected.Configuration
{
internal class DatabaseConfiguration : IDatabaseConfiguration
{
private List<string> _shards;
public DatabaseConfiguration()
{
/*
* TODO: read from config
*/
DefaultConnectionString = "server=PIT-ZBOOK\\sqlexpress; database=connected; trusted_connection=true;TrustServerCertificate=True;multiple active result sets=true";
_shards = new();
}
public string? DefaultConnectionString { get; init; }
public ImmutableList<string> Shards => _shards.ToImmutableList();
}
}

@ -0,0 +1,7 @@
namespace Connected.Configuration.Endpoints
{
internal sealed class EndpointConfiguration : IEndpointConfiguration
{
public string? Address { get; set; }
}
}

@ -0,0 +1,7 @@
namespace Connected.Configuration.Endpoints
{
public interface IEndpointConfiguration
{
string Address { get; }
}
}

@ -0,0 +1,33 @@
using System.Collections.Immutable;
using System.Reflection;
using Connected.Annotations;
namespace Connected.Configuration.Environment;
internal class EnvironmentService : IEnvironmentService
{
private List<Assembly>? _assemblies;
public List<Assembly> All => _assemblies ??= QueryAssemblies();
public EnvironmentService()
{
Services = new EnvironmentServices();
}
public ImmutableList<Assembly> MicroServices => All.ToImmutableList();
public IEnvironmentServices Services { get; }
private static List<Assembly> QueryAssemblies()
{
var result = new List<Assembly>();
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
if (assembly.GetCustomAttribute<MicroServiceAttribute>() is not null)
result.Add(assembly);
}
return result;
}
}

@ -0,0 +1,17 @@
using System.Collections.Immutable;
namespace Connected.Configuration.Environment
{
internal class EnvironmentServices : IEnvironmentServices
{
public ImmutableList<Type> Services => RegisteredServices.Services;
public ImmutableList<Type> ServiceMethods => RegisteredServices.ServiceOperations;
public ImmutableList<Type> Arguments => RegisteredServices.Arguments;
public ImmutableList<Type> IoCEndpoints => RegisteredServices.Middleware;
public ImmutableList<Type> EntityCache => RegisteredServices.EntityCache;
}
}

@ -0,0 +1,12 @@
using System.Collections.Immutable;
using System.Reflection;
namespace Connected.Configuration.Environment
{
public interface IEnvironmentService
{
ImmutableList<Assembly> MicroServices { get; }
IEnvironmentServices Services { get; }
}
}

@ -0,0 +1,13 @@
using System.Collections.Immutable;
namespace Connected.Configuration.Environment
{
public interface IEnvironmentServices
{
ImmutableList<Type> Services { get; }
ImmutableList<Type> ServiceMethods { get; }
ImmutableList<Type> Arguments { get; }
ImmutableList<Type> IoCEndpoints { get; }
ImmutableList<Type> EntityCache { get; }
}
}

@ -0,0 +1,21 @@
using Connected.Configuration.Authentication;
using Connected.Configuration.Endpoints;
namespace Connected.Configuration;
public enum ProcessType
{
FrontEnd = 1,
BackEnd = 2,
Service = 3
}
public interface IConfigurationService
{
IEndpointConfiguration Endpoint { get; }
IAuthenticationConfiguration Authentication { get; }
IStorageConfiguration Storage { get; }
ProcessType Type { get; }
}

@ -0,0 +1,11 @@
using System.Collections.Immutable;
namespace Connected.Configuration
{
public interface IDatabaseConfiguration
{
string DefaultConnectionString { get; }
ImmutableList<string> Shards { get; }
}
}

@ -0,0 +1,7 @@
namespace Connected.Configuration
{
public interface IStorageConfiguration
{
IDatabaseConfiguration Databases { get; }
}
}

@ -0,0 +1,56 @@
using System.Collections.Immutable;
namespace Connected.Configuration
{
public static class RegisteredServices
{
private static readonly List<Type> _services;
private static readonly List<Type> _serviceOperations;
private static readonly List<Type> _arguments;
private static readonly List<Type> _middleware;
private static readonly List<Type> _entityCache;
static RegisteredServices()
{
_services = new List<Type>();
_serviceOperations = new List<Type>();
_arguments = new List<Type>();
_middleware = new List<Type>();
_entityCache = new List<Type>();
}
public static ImmutableList<Type> Services => _services.ToImmutableList();
public static ImmutableList<Type> ServiceOperations => _serviceOperations.ToImmutableList();
public static ImmutableList<Type> Arguments => _arguments.ToImmutableList();
public static ImmutableList<Type> Middleware => _middleware.ToImmutableList();
public static ImmutableList<Type> EntityCache => _entityCache.ToImmutableList();
public static void AddApiService(Type type)
{
_services.Add(type);
}
public static void AddApi(Type type)
{
_serviceOperations.Add(type);
}
public static void AddArgument(Type type)
{
_arguments.Add(type);
}
public static void AddMiddleware(Type type)
{
_middleware.Add(type);
}
public static void AddEntityCache(Type type)
{
_entityCache.Add(type);
}
}
}

@ -0,0 +1,9 @@
using Connected.Data;
namespace Connected.Configuration.Settings;
public interface ISetting : IPrimaryKey<int>
{
string Name { get; init; }
string Value { get; init; }
}

@ -0,0 +1,25 @@
using System.Collections.Immutable;
using Connected.Annotations;
using Connected.ServiceModel;
namespace Connected.Configuration.Settings;
[Service]
[ServiceUrl(ConfigurationUrls.Settings)]
public interface ISettingsService
{
[ServiceMethod(ServiceMethodVerbs.Get | ServiceMethodVerbs.Post)]
Task<ISetting?> Select(PrimaryKeyArgs<int> args);
[ServiceMethod(ServiceMethodVerbs.Get | ServiceMethodVerbs.Post)]
Task<ISetting?> Select(NameArgs args);
[ServiceMethod(ServiceMethodVerbs.Get)]
Task<ImmutableList<ISetting>> Query();
[ServiceMethod(ServiceMethodVerbs.Post | ServiceMethodVerbs.Patch)]
Task Update(SettingsArgs args);
[ServiceMethod(ServiceMethodVerbs.Post | ServiceMethodVerbs.Delete)]
Task Delete(PrimaryKeyArgs<int> args);
}

@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
using Connected.ServiceModel;
namespace Connected.Configuration.Settings;
public class SettingsArgs : Dto
{
[Required]
[MaxLength(128)]
public string? Name { get; set; }
[MaxLength(1024)]
public string? Value { get; set; }
}

@ -0,0 +1,12 @@
namespace Connected.Configuration
{
internal class StorageConfiguration : IStorageConfiguration
{
public StorageConfiguration()
{
Databases = new DatabaseConfiguration();
}
public IDatabaseConfiguration Databases { get; }
}
}

@ -0,0 +1,15 @@
using Connected.Entities.Annotations;
namespace Connected.Data.Annotations;
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = true)]
internal sealed class ColumnAttribute : MemberAttribute
{
public string? Name { get; set; }
public string? TableId { get; set; }
public string? DbType { get; set; }
public bool IsComputed { get; set; }
public bool IsPrimaryKey { get; set; }
public bool IsGenerated { get; set; }
public bool IsReadOnly { get; set; }
}

@ -0,0 +1,8 @@
namespace Connected.Data.Annotations
{
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
internal class MemberExtensionAttribute : Attribute
{
public string TableId { get; set; }
}
}

@ -0,0 +1,9 @@
using Connected.Entities.Annotations;
namespace Connected.Data.Annotations;
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = true)]
internal class NestedEntityAttribute : MemberAttribute
{
public Type? RuntimeType { get; set; }
}

@ -0,0 +1,40 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>False</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.1.0-preview1.22279.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Connected\Connected\Connected.csproj" />
<ProjectReference Include="..\Connected.Caching\Connected.Caching.csproj" />
<ProjectReference Include="..\Connected.Entities\Connected.Entities.csproj" />
<ProjectReference Include="..\Connected.Expressions\Connected.Expressions.csproj" />
<ProjectReference Include="..\Connected.Globalization\Connected.Globalization.csproj" />
<ProjectReference Include="..\Connected.Interop\Connected.Interop.csproj" />
<ProjectReference Include="..\Connected.Threading\Connected.Threading.csproj" />
<ProjectReference Include="..\Connected.Validation\Connected.Validation.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="SR.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>SR.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="SR.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>SR.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project>

@ -0,0 +1,100 @@
using Connected.Data.Storage;
using Connected.Entities.Annotations;
using Connected.Entities.Storage;
using Connected.Interop;
using Connected.ServiceModel;
using System.Data;
using System.Reflection;
namespace Connected.Data;
public static class DataExtensions
{
/// <summary>
/// Sets <see cref="StorageConnectionMode.Isolated"/> value to the <see cref="IConnectionProvider"/> on the
/// provided <see cref="IContext"/>.
/// </summary>
/// <param name="context">The <see cref="IContext"/> to set the <see cref="StorageConnectionMode.Isolated"/> value.</param>
public static void UseIsolatedConnections(this IContext context)
{
if (context.GetService<IConnectionProvider>() is IConnectionProvider provider)
provider.Mode = StorageConnectionMode.Isolated;
}
public static DbType ToDbType(PropertyInfo property)
{
var type = property.PropertyType;
if (type.IsEnum)
type = Enum.GetUnderlyingType(type);
if (type == typeof(char) || type == typeof(string))
{
if (property.FindAttribute<ETagAttribute>() != null)
return DbType.Binary;
var str = property.FindAttribute<StringAttribute>();
if (str is null)
return DbType.String;
return str.Kind switch
{
StringKind.NVarChar => DbType.String,
StringKind.VarChar => DbType.AnsiString,
StringKind.Char => DbType.AnsiStringFixedLength,
StringKind.NChar => DbType.StringFixedLength,
_ => DbType.String,
};
}
else if (type == typeof(byte))
return DbType.Byte;
else if (type == typeof(bool))
return DbType.Boolean;
else if (type == typeof(DateTime) || type == typeof(DateTimeOffset))
{
var att = property.FindAttribute<DateAttribute>();
if (att is null)
return DbType.DateTime2;
return att.Kind switch
{
DateKind.Date => DbType.Date,
DateKind.DateTime => DbType.DateTime,
DateKind.DateTime2 => DbType.DateTime2,
DateKind.SmallDateTime => DbType.DateTime,
DateKind.Time => DbType.Time,
_ => DbType.DateTime2,
};
}
else if (type == typeof(decimal))
return DbType.Decimal;
else if (type == typeof(double))
return DbType.Double;
else if (type == typeof(Guid))
return DbType.Guid;
else if (type == typeof(short))
return DbType.Int16;
else if (type == typeof(int))
return DbType.Int32;
else if (type == typeof(long))
return DbType.Int64;
else if (type == typeof(sbyte))
return DbType.SByte;
else if (type == typeof(float))
return DbType.Single;
else if (type == typeof(TimeSpan))
return DbType.Time;
else if (type == typeof(ushort))
return DbType.UInt16;
else if (type == typeof(uint))
return DbType.UInt32;
else if (type == typeof(ulong))
return DbType.UInt64;
else if (type == typeof(byte[]))
return DbType.Binary;
else
return DbType.Binary;
}
}

@ -0,0 +1,23 @@
using Connected.Annotations;
using Connected.Data.DataProtection;
using Connected.Data.Schema;
using Connected.Data.Sharding;
using Connected.Data.Storage;
using Connected.Entities.Storage;
using Microsoft.Extensions.DependencyInjection;
[assembly: MicroService(MicroServiceType.Sys)]
namespace Connected.Data;
internal sealed class DataStartup : Startup
{
protected override void OnConfigureServices(IServiceCollection services)
{
services.AddScoped(typeof(ISchemaService), typeof(SchemaService));
services.AddScoped(typeof(IShardingService), typeof(ShardingService));
services.AddScoped(typeof(IConnectionProvider), typeof(ConnectionProvider));
services.AddScoped(typeof(IStorageProvider), typeof(StorageProvider));
services.AddScoped(typeof(IEntityProtectionService), typeof(EntityProtectionService));
}
}

@ -0,0 +1,14 @@
using Connected.Entities;
namespace Connected.Data.EntityProtection;
public sealed class EntityProtectionArgs<TEntity> : EventArgs
{
public EntityProtectionArgs(TEntity entity, State state)
{
Entity = entity;
State = state;
}
public TEntity Entity { get; }
public State State { get; }
}

@ -0,0 +1,25 @@
using Connected.Data.EntityProtection;
using Connected.Middleware;
namespace Connected.Data.DataProtection;
internal class EntityProtectionService : IEntityProtectionService
{
public EntityProtectionService(IMiddlewareService middleware)
{
Middleware = middleware;
}
public IMiddlewareService Middleware { get; }
public async Task Invoke<TEntity>(EntityProtectionArgs<TEntity> args)
{
var middleware = await Middleware.Query<IEntityProtector<TEntity>>();
if (!middleware.Any())
return;
foreach (var m in middleware)
await m.Invoke(args);
}
}

@ -0,0 +1,9 @@
using Connected.Data.EntityProtection;
namespace Connected.Data.DataProtection
{
public interface IEntityProtectionService
{
Task Invoke<TEntity>(EntityProtectionArgs<TEntity> args);
}
}

@ -0,0 +1,8 @@
using Connected.Data.EntityProtection;
namespace Connected.Data.DataProtection;
public interface IEntityProtector<TEntity> : IMiddleware
{
Task Invoke(EntityProtectionArgs<TEntity> args);
}

@ -0,0 +1,125 @@
using Connected.Interop;
namespace Connected.Data;
internal class EntityVersion : IComparable, IEquatable<EntityVersion>, IComparable<EntityVersion>
{
public static readonly EntityVersion? Zero = default;
private readonly ulong Value;
private EntityVersion(ulong value)
{
Value = value;
}
public static EntityVersion? Parse(object value)
{
if (!TypeConversion.TryConvert(value, out string? v))
return Zero;
if (string.IsNullOrWhiteSpace(v))
return Zero;
return new EntityVersion(Convert.ToUInt64(v, 16));
}
public static implicit operator EntityVersion(ulong value)
{
return new EntityVersion(value);
}
public static implicit operator EntityVersion(long value)
{
return new EntityVersion(unchecked((ulong)value));
}
public static explicit operator EntityVersion?(byte[] value)
{
if (value is null)
return null;
return new EntityVersion((ulong)value[0] << 56 | (ulong)value[1] << 48 | (ulong)value[2] << 40 | (ulong)value[3] << 32 | (ulong)value[4] << 24 | (ulong)value[5] << 16 | (ulong)value[6] << 8 | value[7]);
}
public static implicit operator byte[](EntityVersion timestamp)
{
var r = new byte[8];
r[0] = (byte)(timestamp.Value >> 56);
r[1] = (byte)(timestamp.Value >> 48);
r[2] = (byte)(timestamp.Value >> 40);
r[3] = (byte)(timestamp.Value >> 32);
r[4] = (byte)(timestamp.Value >> 24);
r[5] = (byte)(timestamp.Value >> 16);
r[6] = (byte)(timestamp.Value >> 8);
r[7] = (byte)timestamp.Value;
return r;
}
public override bool Equals(object? obj)
{
return obj is Version version && Equals(version);
}
public override int GetHashCode()
{
return Value.GetHashCode();
}
public bool Equals(EntityVersion? other)
{
return other?.Value == Value;
}
int IComparable.CompareTo(object? obj)
{
return obj is null ? 1 : CompareTo((EntityVersion)obj);
}
public int CompareTo(EntityVersion? other)
{
return Value == other?.Value ? 0 : Value < other?.Value ? -1 : 1;
}
public static bool operator ==(EntityVersion comparand1, EntityVersion comparand2)
{
return comparand1.Equals(comparand2);
}
public static bool operator !=(EntityVersion comparand1, EntityVersion comparand2)
{
return !comparand1.Equals(comparand2);
}
public static bool operator >(EntityVersion comparand1, EntityVersion comparand2)
{
return comparand1.CompareTo(comparand2) > 0;
}
public static bool operator >=(EntityVersion comparand1, EntityVersion comparand2)
{
return comparand1.CompareTo(comparand2) >= 0;
}
public static bool operator <(EntityVersion comparand1, EntityVersion comparand2)
{
return comparand1.CompareTo(comparand2) < 0;
}
public static bool operator <=(EntityVersion comparand1, EntityVersion comparand2)
{
return comparand1.CompareTo(comparand2) <= 0;
}
public override string ToString()
{
return Value.ToString("x16");
}
public static EntityVersion Max(EntityVersion comparand1, EntityVersion comparand2)
{
return comparand1.Value < comparand2.Value ? comparand2 : comparand1;
}
}

@ -0,0 +1,192 @@
using Connected.Entities.Annotations;
using Connected.Interop;
using System.Data;
using System.Reflection;
namespace Connected.Data;
/// <summary>
/// Performs mapping between <see cref="IDataReader"/> fields and entity properties.
/// </summary>
/// <typeparam name="TEntity">The entity type to be used.</typeparam>
internal class FieldMappings<TEntity>
{
private Dictionary<int, PropertyInfo> _properties;
/// <summary>
/// Creates a new <see cref="FieldMappings{TEntity}"/> object.
/// </summary>
/// <param name="reader">The <see cref="IDataReader"/> providing entity records.</param>
public FieldMappings(IDataReader reader)
{
Initialize(reader);
}
/// <summary>
/// Cached properties use when looping through the records.
/// </summary>
private Dictionary<int, PropertyInfo> Properties => _properties;
/// <summary>
/// Initializes the mappings base on the provided <see cref="IDataReader"/>
/// and <typeparamref name="TEntity"/>
/// </summary>
/// <param name="reader">The active reader containing records.</param>
private void Initialize(IDataReader reader)
{
/*
* For primitive types there are no mappings since it's an scalar call.
*/
if (typeof(TEntity).IsTypePrimitive())
return;
_properties = new Dictionary<int, PropertyInfo>();
/*
* We are binding only properties, not fields.
*/
var properties = typeof(TEntity).GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
for (var i = 0; i < reader.FieldCount; i++)
{
if (FieldMappings<TEntity>.ResolveProperty(properties, reader.GetName(i)) is PropertyInfo property)
_properties.Add(i, property);
}
}
/// <summary>
/// Creates a new instance of the <see cref="TEntity"/> and binds
/// properties from the provided <see cref="IDataReader"/>.
/// </summary>
/// <param name="reader">The <see cref="IDataReader"/> containing the record.</param>
/// <returns>A new instance of the <see cref="TEntity"/> with bound values from the <see cref="IDataReader"/>.</returns>
public TEntity? CreateInstance(IDataReader reader)
{
/*
* For primitive return values we'll use only the first field and return itž
* to the caller.
*/
if (typeof(TEntity).IsTypePrimitive())
{
if (reader.FieldCount == 0)
return default;
if (TypeConversion.TryConvert(reader[0], out TEntity? result))
return result;
return default;
}
/*
* It's an actual entity. First, create a new instance. Entities should have
* public parameterless constructor.
*/
if (typeof(TEntity?).CreateInstance<TEntity>() is not TEntity instance)
throw new NullReferenceException(typeof(TEntity).FullName);
foreach (var property in Properties)
Bind(instance, property, reader);
return instance;
}
/// <summary>
/// Resolves a correct property from the entity's properties based on a <see cref="IDataReader"/> field name.
/// </summary>
/// <param name="properties">The entity's properties.</param>
/// <param name="name">The <see cref="IDataReader"/> field name.</param>
/// <returns>A <see cref="PropertyInfo"/> if found, <c>null</c> otherwise.</returns>
private static PropertyInfo? ResolveProperty(PropertyInfo[] properties, string name)
{
/*
* There are two ways to map a property (evaluated in the following order):
* 1. from property name
* 2. from MemberAttribute
*
* We'll first perform case insensitive comparison because fields in the database are usually stored in a camelCase format.
*/
if (properties.FirstOrDefault(f => string.Equals(f.Name, name, StringComparison.OrdinalIgnoreCase)) is PropertyInfo property && property.CanWrite)
{
/*
* Property is found, examine if the persistence from the storage is supported.
*/
var att = property.FindAttribute<PersistenceAttribute>();
if (att is null || att.Persistence.HasFlag(ColumnPersistence.Read))
return property;
}
/*
* Property wasn't found, let's try to find it via MemberAttribute.
*/
foreach (var prop in properties)
{
/*
* It's case insensitive comparison again because we don't want bother user with exact matching. Since a database is probably case insensitive anyway
* there is no option to store columns with case sensitive names.
*/
if (prop.FindAttribute<MemberAttribute>() is MemberAttribute nameAttribute && string.Compare(nameAttribute.Member, name, true) == 0 && prop.CanWrite)
return prop;
}
/*
* Property could't be found. The field will be ognored when reading data.
*/
return default;
}
/// <summary>
/// Binds the <see cref="IDataReader"/> value to the entity's property.
/// </summary>
/// <param name="instance">The instance of the entity.</param>
/// <param name="property">The property on which value to be set.</param>
/// <param name="reader">The <see cref="IDataReader"/>providing the value.</param>
private static void Bind(object instance, KeyValuePair<int, PropertyInfo> property, IDataReader reader)
{
var value = reader.GetValue(property.Key);
/*
* We won't bind null values. We'll leave the property as is.
*/
if (value is null || Convert.IsDBNull(value))
return;
/*
* We have a few exceptions when binding values.
*/
if (property.Value.PropertyType == typeof(string) && value is byte[] bv)
{
/*
* If the property is string and the reader's value is byte array we are probably dealing
* with Consistency field. We'll first check if the property contains the attribute. If so,
* we'll convert byte array to eTag kind of value. If not we'll simply convert value to base64
* string.
*/
if (property.Value.FindAttribute<ETagAttribute>() is not null)
{
var versionValue = (EntityVersion?)bv;
if (versionValue is null)
value = Convert.ToBase64String(bv);
else
value = versionValue.ToString();
}
else
value = Convert.ToBase64String(bv);
}
else if (property.Value.PropertyType == typeof(DateTimeOffset))
{
/*
* We don't perform any conversions on dates. All dates should be stored in a UTC
* format so we simply set the correct kind of date so it can be later correctly
* converted
*/
value = new DateTimeOffset(DateTime.SpecifyKind((DateTime)value, DateTimeKind.Utc));
}
else if (property.Value.PropertyType == typeof(DateTime))
{
/*
* Like DateTimeOffset, the same is true for DateTime values
*/
value = DateTime.SpecifyKind((DateTime)value, DateTimeKind.Utc);
}
else
{
/*
* For other values we just perform a conversion.
*/
value = TypeConversion.Convert(value, property.Value.PropertyType);
}
/*
* Now bind the property from the converted value.
*/
property.Value.SetValue(instance, value);
}
}

@ -0,0 +1,72 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Connected.Data {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class SR {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal SR() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Connected.Data.SR", typeof(SR).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Data concurrency issue occured.
/// </summary>
internal static string ErrDataConcurrency {
get {
return ResourceManager.GetString("ErrDataConcurrency", resourceCulture);
}
}
}
}

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ErrDataConcurrency" xml:space="preserve">
<value>Data concurrency issue occured</value>
</data>
</root>

@ -0,0 +1,38 @@
namespace Connected.Data.Schema
{
internal class EntitySchema : ISchema
{
public EntitySchema()
{
Columns = new();
}
public List<ISchemaColumn> Columns { get; }
public string? Schema { get; set; }
public string? Name { get; set; }
public string? Type { get; set; }
public bool Ignore { get; set; }
public bool Equals(ISchema? other)
{
if (other is null)
return false;
if (!string.Equals(Name, other.Name, StringComparison.Ordinal))
return false;
if (!string.Equals(Schema, other.Schema, StringComparison.Ordinal))
return false;
if (Columns.Count != other.Columns.Count)
return false;
for (var i = 0; i < Columns.Count; i++)
{
if (Columns[i] is not IEquatable<ISchemaColumn> left || !left.Equals(other.Columns[i]))
return false;
}
return true;
}
}
}

@ -0,0 +1,62 @@
using System.Collections.Immutable;
using System.Data;
using Connected.Data.Schema.Sql;
using Connected.Entities.Annotations;
namespace Connected.Data.Schema;
internal class ExistingColumn : ISchemaColumn, IExistingSchemaColumn
{
public ExistingColumn(ISchema schema)
{
Schema = schema;
}
private ISchema Schema { get; }
public string Name { get; set; }
public DbType DataType { get; set; }
public bool IsIdentity { get; set; }
public bool IsVersion { get; set; }
public bool IsUnique { get; set; }
public bool IsIndex { get; set; }
public bool IsPrimaryKey { get; set; }
public string DefaultValue { get; set; }
public int MaxLength { get; set; }
public bool IsNullable { get; set; }
public string DependencyType { get; set; }
public string DependencyProperty { get; set; }
public string Index { get; set; }
public int Precision { get; set; }
public int Scale { get; set; }
public DateKind DateKind { get; set; } = DateKind.DateTime;
public BinaryKind BinaryKind { get; set; } = BinaryKind.VarBinary;
public int DatePrecision { get; set; }
public ImmutableArray<string> QueryIndexColumns(string column)
{
if (Schema is not ExistingSchema existing)
return ImmutableArray<string>.Empty;
foreach (var index in existing.Indexes)
{
if (index.Columns.Contains(column, StringComparer.OrdinalIgnoreCase))
return index.Columns.ToImmutableArray();
}
return ImmutableArray<string>.Empty;
}
}

@ -0,0 +1,7 @@
namespace Connected.Data.Schema
{
internal interface IDatabase
{
List<ITable> Tables { get; }
}
}

@ -0,0 +1,9 @@
using System.Collections.Immutable;
namespace Connected.Data.Schema
{
internal interface IExistingSchemaColumn
{
ImmutableArray<string> QueryIndexColumns(string column);
}
}

@ -0,0 +1,12 @@
namespace Connected.Data.Schema
{
internal interface IReferentialConstraint
{
string Name { get; }
string ReferenceSchema { get; }
string ReferenceName { get; }
string MatchOption { get; }
string UpdateRule { get; }
string DeleteRule { get; }
}
}

@ -0,0 +1,12 @@
namespace Connected.Data.Schema
{
public interface ISchema : IEquatable<ISchema>
{
List<ISchemaColumn> Columns { get; }
string? Schema { get; }
string? Name { get; }
string? Type { get; }
bool Ignore { get; }
}
}

@ -0,0 +1,24 @@
using System.Data;
using Connected.Entities.Annotations;
namespace Connected.Data.Schema;
public interface ISchemaColumn
{
string? Name { get; }
DbType DataType { get; }
bool IsIdentity { get; }
bool IsUnique { get; }
bool IsIndex { get; }
bool IsPrimaryKey { get; }
bool IsVersion { get; }
string? DefaultValue { get; }
int MaxLength { get; }
bool IsNullable { get; }
string? Index { get; }
int Scale { get; }
int Precision { get; }
DateKind DateKind { get; }
BinaryKind BinaryKind { get; }
int DatePrecision { get; }
}

@ -0,0 +1,9 @@
namespace Connected.Data.Schema;
public interface ISchemaMiddleware : IMiddleware
{
Task<bool> IsEntitySupported(Type entity);
Task Synchronize(Type entity, ISchema schema);
Type ConnectionType { get; }
string DefaultConnectionString { get; }
}

@ -0,0 +1,7 @@
namespace Connected.Data.Schema
{
public interface ISchemaService
{
Task Synchronize(List<Type>? entities);
}
}

@ -0,0 +1,7 @@
namespace Connected.Data.Schema;
public interface ISchemaSynchronizationContext
{
Type ConnectionType { get; }
string ConnectionString { get; }
}

@ -0,0 +1,8 @@
namespace Connected.Data.Schema
{
internal interface ITable : ISchema
{
List<ITableColumn> Columns { get; }
List<ITableIndex> Indexes { get; }
}
}

@ -0,0 +1,22 @@
namespace Connected.Data.Schema
{
internal interface ITableColumn
{
string Name { get; }
string DataType { get; }
bool Identity { get; }
bool IsNullable { get; }
string DefaultValue { get; }
int Ordinal { get; }
int CharacterMaximumLength { get; }
int CharacterOctetLength { get; }
int NumericPrecision { get; }
int NumericPrecisionRadix { get; }
int NumericScale { get; }
int DateTimePrecision { get; }
string CharacterSetName { get; }
IReferentialConstraint Reference { get; }
List<ITableConstraint> Constraints { get; }
}
}

@ -0,0 +1,6 @@
namespace Connected.Data.Schema
{
internal interface ITableConstraint : ISchema
{
}
}

@ -0,0 +1,8 @@
namespace Connected.Data.Schema
{
internal interface ITableIndex
{
string Name { get; }
List<string> Columns { get; }
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save