Initial commit

pull/1/head
Matija Koželj 2 years ago
parent 3e43171e72
commit 100e9ed085

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>$(MSBuildProjectName)</AssemblyName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Connected\Connected\Connected.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>$(MSBuildProjectName)</AssemblyName>
<RootNamespace>Common</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Connected\Connected\Connected.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,8 @@
namespace Common;
public static class CommonRoutes
{
public const string Common = "/common";
public const string Management = "/management";
public const string Documents = "/documents";
}

@ -0,0 +1,41 @@
using Connected.Annotations;
using Connected.ServiceModel;
using System.ComponentModel.DataAnnotations;
namespace Common.Documents;
public class DocumentArgs : Dto
{
[MaxLength(32)]
public string? Code { get; set; } = default!;
}
public abstract class InsertDocumentArgs : DocumentArgs
{
public int? Author { get; set; } = default!;
public DateTimeOffset? Created { get; set; }
}
public abstract class UpdateDocumentArgs<TPrimaryKey> : DocumentArgs
where TPrimaryKey : notnull
{
[MinValue(1)]
public TPrimaryKey Id { get; set; } = default!;
public DateTimeOffset? Modified { get; set; }
public int? Owner { get; set; }
}
public sealed class SelectDocumentArgs : Dto
{
[Required, MaxLength(32)]
public string Domain { get; set; } = default!;
[Required, MaxLength(32)]
public string Type { get; set; } = default!;
[Required, MaxLength(32)]
public string Code { set; get; } = default!;
}

@ -0,0 +1,42 @@
using Connected.Data;
namespace Common.Documents
{
/// <summary>
/// Represents the base entity for all documents.
/// </summary>
/// <remarks>
/// Document is primary entity of the business processes. It provides
/// schema which is used in a business process lifecycle. Documents, apart from
/// some basic validation, do not provide any specifiec business logic. Business
/// processes are entirely responsible for the business logic.
/// </remarks>
public interface IDocument<TPrimaryKey> : IPrimaryKey<TPrimaryKey>
where TPrimaryKey : notnull
{
/// <summary>
/// The date when document was created.
/// </summary>
DateTimeOffset Created { get; init; }
/// <summary>
/// The date when document was last updated.
/// </summary>
DateTimeOffset? Modified { get; init; }
/// <summary>
/// The unique identifier of the document. This is
/// usually set by a customer specific numbering system.
/// </summary>
string? Code { get; init; }
/// <summary>
/// The user which created the document. Can be null if document was created by the system.
/// </summary>
int? Author { get; init; }
/// <summary>
/// The user last modified the document. Once user modifies the document it becomes the Owner.
/// </summary>
/// <remarks>
/// This behavior could be overriden in documents implementation.
/// </remarks>
int? Owner { get; init; }
}
}

@ -0,0 +1,9 @@
using Connected.Notifications;
namespace Common.Documents;
public interface IDocumentService<TDocumentPrimaryKey, TDetailPrimaryKey> : IServiceNotifications<TDocumentPrimaryKey>
{
event ServiceEventHandler<PrimaryKeyEventArgs<TDetailPrimaryKey>>? ItemInserted;
event ServiceEventHandler<PrimaryKeyEventArgs<TDetailPrimaryKey>>? ItemUpdated;
event ServiceEventHandler<PrimaryKeyEventArgs<TDetailPrimaryKey>>? ItemDeleted;
}

@ -0,0 +1,6 @@
namespace Common;
public static class Units
{
public const int MB = 1024 * 1024;
}

@ -0,0 +1,3 @@
using Connected.Annotations;
[assembly: MicroService(MicroServiceType.Contract)]

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

@ -0,0 +1,9 @@
using Connected.Data;
namespace Common.Notes;
public interface INote : IEntityContainer<long>
{
int Author { get; init; }
DateTimeOffset Created { get; init; }
}

@ -0,0 +1,9 @@
using Connected.Data;
namespace Common.Notes;
public interface INoteSearch : IEntityContainer<long>
{
int Author { get; init; }
string Text { get; init; }
DateTimeOffset Created { get; init; }
}

@ -0,0 +1,24 @@
using System.Collections.Immutable;
using Connected.Annotations;
using Connected.ServiceModel;
using Connected.ServiceModel.Search;
namespace Common.Notes;
[Service]
[ServiceUrl(NoteUrls.Notes)]
public interface INoteService
{
Task<ImmutableList<INote>> Query(NoteArgs args);
Task<INote?> Select(PrimaryKeyArgs<long> args);
Task<long> Insert(InsertNoteArgs args);
Task Update(UpdateNoteArgs args);
Task Delete(PrimaryKeyArgs<long> args);
Task<ImmutableList<INoteText>> QueryText(QueryNoteTextArgs args);
[ServiceMethod(ServiceMethodVerbs.Get)]
Task<INoteText?> SelectText(SelectNoteTextArgs args);
Task<ImmutableList<INoteSearch>> Search(SearchArgs args);
}

@ -0,0 +1,10 @@
using Connected.Data;
namespace Common.Notes;
public interface INoteText : IPrimaryKey<long>
{
string Entity { get; init; }
string EntityId { get; init; }
string? Text { get; init; }
}

@ -0,0 +1,44 @@
using System.ComponentModel.DataAnnotations;
using Connected.ServiceModel;
namespace Common.Notes;
public class NoteArgs : Dto
{
[Required, MaxLength(128)]
public string Entity { get; set; } = default!;
[Required, MaxLength(128)]
public string PrimaryKey { get; set; } = default!;
}
public class InsertNoteArgs : NoteArgs
{
[Range(1, int.MaxValue)]
public int Author { get; set; }
public DateTimeOffset? Created { get; set; }
[Required, MaxLength(1024 * 1024)]
public string Text { get; set; } = default!;
}
public sealed class UpdateNoteArgs : PrimaryKeyArgs<long>
{
[Required, MaxLength(Units.MB)]
public string Text { get; set; } = default!;
}
public sealed class QueryNoteTextArgs : PrimaryKeyListArgs<long>
{
[Required, MaxLength(128)]
public string Entity { get; set; } = default!;
}
public sealed class SelectNoteTextArgs : PrimaryKeyArgs<long>
{
[Required, MaxLength(128)]
public string Entity { get; set; } = default!;
[Required, MaxLength(128)]
public string EntityId { get; set; } = default!;
}

@ -0,0 +1,5 @@
namespace Common.Notes;
public static class NoteUrls
{
public const string Notes = $"{CommonRoutes.Common}/notes";
}

@ -0,0 +1,3 @@
using Connected.Annotations;
[assembly: MicroService(MicroServiceType.Sys)]

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Framework.ServiceModel\Connected.ServiceModel\Connected.ServiceModel.csproj" />
<ProjectReference Include="..\..\Framework\Connected.Entities\Connected.Entities.csproj" />
<ProjectReference Include="..\..\Framework\Connected.Services\Connected.Services.csproj" />
<ProjectReference Include="..\Common.Notes.Model\Common.Notes.Model.csproj" />
<ProjectReference Include="..\Common\Common.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,17 @@
using Connected.Annotations;
using Connected.Entities.Annotations;
using Connected.Entities.Containers;
namespace Common.Notes;
[Table(Schema = Constants.CommonSchema)]
public sealed record Note : ContainerEntity<long>, INote
{
public const string EntityKey = $"{Constants.CommonSchema}.{nameof(Note)}";
[Ordinal(0)]
public int Author { get; init; }
[Ordinal(1)]
public DateTimeOffset Created { get; init; }
}

@ -0,0 +1,240 @@
using System.Collections.Immutable;
using Connected.Caching;
using Connected.Entities;
using Connected.Entities.Storage;
using Connected.Notifications;
using Connected.Notifications.Events;
using Connected.ServiceModel;
using Connected.ServiceModel.Search;
using Connected.Services;
namespace Common.Notes;
internal static class NoteOps
{
/// <summary>
/// Deletes the note entity, its text and search entity from the storage.
/// </summary>
public class Delete : ServiceAction<PrimaryKeyArgs<long>>
{
/// <summary>
/// Create a new <see cref="Delete"/> instance.
/// </summary>
public Delete(IStorageProvider storage, IEventService events)
{
Storage = storage;
Events = events;
}
private IStorageProvider Storage { get; }
private IEventService Events { get; }
protected override async Task OnInvoke()
{
/*
* First delete the entity.
*/
await Storage.Open<Note>().Update(Arguments.AsEntity<Note>(State.Deleted));
/*
* Delete the note's text.
*/
await Storage.Open<NoteText>().Update(Arguments.AsEntity<NoteText>(State.Deleted));
/*
* Delete search entry.
*/
await Storage.Open<NoteSearch>().Update(Arguments.AsEntity<NoteSearch>(State.Deleted));
}
protected override async Task OnCommitted()
{
/*
* We are triggering notify only for the entity, not for text and search transactions.
* Note that we don't cache text and search entries. They are read from the storage every time
* it is requested.
*/
await Events.Enqueue(this, typeof(NoteService), nameof(IServiceNotifications<long>.Deleted), Arguments);
}
}
/// <summary>
/// Inserts a new note entity into storage.
/// </summary>
/// <remarks>
/// This class inserts a new <see cref="INote"/> entity into storage, then inserts text
/// into table storage and the creates a search index entry.
/// </remarks>
public sealed class Insert : ServiceFunction<InsertNoteArgs, long>
{
public Insert(IStorageProvider storage, IEventService events)
{
Storage = storage;
Events = events;
}
private IStorageProvider Storage { get; }
private IEventService Events { get; }
protected override async Task<long> OnInvoke()
{
/*
* This call inserts a new record into storage (database) and returns its id.
*/
var result = await Storage.Open<Note>().Update(Arguments.AsEntity<Note>(State.New));
/*
* We'll be using a newly inserted id to create a new record in the table
* storage for text.
*/
await Storage.Open<NoteText>().Update(Arguments.AsEntity<NoteText>(State.New, new { result.Id }));
/*
* And create entry in the search index.
*/
//await Storage.Open<NoteSearch>().Update(Arguments.AsEntity<NoteSearch>(State.New, new { result.Id }));
return result.Id;
}
protected override async Task OnCommitted()
{
await Events.Enqueue(this, typeof(NoteService), nameof(IServiceNotifications<long>.Inserted), new PrimaryKeyArgs<long> { Id = Result });
}
}
public sealed class Update : ServiceAction<UpdateNoteArgs>
{
public Update(IStorageProvider storage, ICacheContext cache, IEventService events, INoteService notes)
{
Storage = storage;
Cache = cache;
Events = events;
Notes = notes;
}
private IStorageProvider Storage { get; }
private ICacheContext Cache { get; }
private IEventService Events { get; }
private INoteService Notes { get; }
protected override async Task OnInvoke()
{
if (await Notes.Select(Arguments.Id) is not INote entity)
return;
await Storage.Open<Note>().Update(Arguments.AsEntity<Note>(State.Default), Arguments, async () =>
{
await Cache.Remove(Note.EntityKey, Arguments.Id);
return (await Notes.Select(Arguments.Id)) as Note;
});
await Storage.Open<NoteText>().Update(Arguments.AsEntity<NoteText>(State.Default), Arguments, async () =>
{
await Cache.Remove(NoteText.EntityKey, Arguments.Id);
return (await Notes.SelectText(new SelectNoteTextArgs
{
Entity = entity.Entity,
EntityId = entity.EntityId,
Id = Arguments.Id
})) as NoteText;
});
await Storage.Open<NoteSearch>().Update(Arguments.AsEntity<NoteSearch>(State.Default));
}
protected override async Task OnCommitted()
{
await Cache.Remove(Note.EntityKey, Arguments.Id);
await Cache.Remove(NoteText.EntityKey, Arguments.Id);
await Events.Enqueue(this, typeof(NoteService), nameof(IServiceNotifications<long>.Updated), new PrimaryKeyArgs<long> { Id = Arguments.Id });
}
}
public sealed class Query : ServiceFunction<NoteArgs, ImmutableList<INote>>
{
public Query(IStorageProvider provider)
{
Provider = provider;
}
private IStorageProvider Provider { get; }
protected override async Task<ImmutableList<INote>> OnInvoke()
{
return await (from dc in Provider.Open<Note>() select dc).AsEntities<INote>();
}
}
public sealed class Select : ServiceFunction<PrimaryKeyArgs<long>, INote>
{
public Select(IStorageProvider provider, ICacheContext cache)
{
Provider = provider;
Cache = cache;
}
private IStorageProvider Provider { get; }
private ICacheContext Cache { get; }
protected override Task<INote?> OnInvoke()
{
return Cache.Get<INote>(Note.EntityKey, Arguments.Id,
async (f) =>
{
return await (from dc in Provider.Open<Note>() where dc.Id == Arguments.Id select dc).AsEntity();
});
}
}
public sealed class Search : ServiceFunction<SearchArgs, ImmutableList<INoteSearch>>
{
public Search(IStorageProvider storage)
{
Storage = storage;
}
private IStorageProvider Storage { get; }
protected override async Task<ImmutableList<INoteSearch>?> OnInvoke()
{
return await (from dc in Storage.Open<NoteSearch>()
where dc.Text.Contains(Arguments.Text)
select dc).AsEntities<INoteSearch>();
}
}
public sealed class QueryText : ServiceFunction<QueryNoteTextArgs, ImmutableList<INoteText>>
{
public QueryText(IStorageProvider storage)
{
Storage = storage;
}
private IStorageProvider Storage { get; }
protected override async Task<ImmutableList<INoteText>?> OnInvoke()
{
return await (from dc in Storage.Open<NoteText>()
where string.Equals(dc.Entity, Arguments.Entity, StringComparison.Ordinal)
&& Arguments.IdList.Any(f => f == dc.Id)
select dc).AsEntities<INoteText>();
}
}
public sealed class SelectText : ServiceFunction<SelectNoteTextArgs, INoteText>
{
public SelectText(IStorageProvider provider)
{
Provider = provider;
}
private IStorageProvider Provider { get; }
protected override async Task<INoteText?> OnInvoke()
{
return await (from dc in Provider.Open<NoteText>()
where string.Equals(dc.Entity, Arguments.Entity)
&& string.Equals(dc.EntityId, Arguments.EntityId)
&& dc.Id == Arguments.Id
select dc).AsEntity();
}
}
}

@ -0,0 +1,17 @@
using Connected.Annotations;
using Connected.Entities.Annotations;
using Connected.Entities.Containers;
namespace Common.Notes;
[Persistence(Persistence = ColumnPersistence.InMemory)]
public record NoteSearch : ContainerEntity<long>, INoteSearch
{
[Ordinal(0)]
public int Author { get; init; }
[Ordinal(1), Length(Units.MB)]
public string Text { get; init; } = default!;
[Ordinal(2)]
public DateTimeOffset Created { get; init; }
}

@ -0,0 +1,61 @@
using System.Collections.Immutable;
using Connected.ServiceModel;
using Connected.ServiceModel.Search;
using Connected.Services;
namespace Common.Notes;
internal sealed class NoteService : Service, INoteService
{
public NoteService(IContext context) : base(context)
{
}
public async Task Delete(PrimaryKeyArgs<long> args)
{
await Invoke(GetOperation<NoteOps.Delete>(), args);
}
public async Task<long> Insert(InsertNoteArgs args)
{
return await Invoke(GetOperation<NoteOps.Insert>(), args);
}
public async Task<ImmutableList<INote>> Query(NoteArgs args)
{
return await Invoke(GetOperation<NoteOps.Query>(), args);
}
public async Task<ImmutableList<INoteText>> QueryText(QueryNoteTextArgs args)
{
return await Invoke(GetOperation<NoteOps.QueryText>(), args);
}
public async Task<ImmutableList<INoteSearch>> Search(SearchArgs args)
{
return await Invoke(GetOperation<NoteOps.Search>(), args);
}
public async Task<INote?> Select(PrimaryKeyArgs<long> args)
{
return await Invoke(GetOperation<NoteOps.Select>(), args);
}
public async Task<INoteText?> SelectText(SelectNoteTextArgs args)
{
await Invoke(GetOperation<NoteOps.Insert>(), new InsertNoteArgs
{
Entity = "Entity",
PrimaryKey = "10",
Author = 10,
Created = DateTime.UtcNow,
Text = "Note text"
}, "Insert");
return await Invoke(GetOperation<NoteOps.SelectText>(), args);
}
public async Task Update(UpdateNoteArgs args)
{
await Invoke(GetOperation<NoteOps.Update>(), args);
}
}

@ -0,0 +1,20 @@
using System.ComponentModel.DataAnnotations;
using Connected.Annotations;
using Connected.Entities.Annotations;
using Connected.ServiceModel.Annotations;
using Connected.ServiceModel.Data;
namespace Common.Notes;
public sealed record NoteText : TableEntity<long, string>, INoteText
{
public const string EntityKey = $"{Constants.CommonSchema}.{nameof(NoteText)}";
[Ordinal(-50000), PartitionKey, MaxLength(128)]
public string Entity { get; init; } = default!;
[Ordinal(-49000), PrimaryKey, MaxLength(128)]
public string EntityId { get; init; } = default!;
[Ordinal(0), Length(Units.MB), Nullable]
public string? Text { get; init; }
}

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Common.Numbering</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Framework\Connected.Middleware\Connected.Middleware.csproj" />
<ProjectReference Include="..\Common.Numbering.Model\Common.Numbering.Model.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,16 @@
using Connected;
namespace Common.Numbering;
/// <summary>
/// Provides middleware for providing a numbering algorithm.
/// </summary>
public interface INumberingProvider : IMiddleware
{
/// <summary>
/// Creates a new value based on the specified arguments.
/// </summary>
/// <param name="args">The arguments providing information about the entity for which
/// value need to be provided.</param>
/// <returns>A new value if the numbering is supported by the middleware, <c>null</c> otherwise.</returns>
Task<string> Invoke(NumberingCalculateArgs args);
}

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

@ -0,0 +1,8 @@
using Connected.Data;
namespace Common.Numbering;
public interface INumbering : IPrimaryKey<int>
{
string Entity { get; init; }
string Value { get; init; }
}

@ -0,0 +1,17 @@
using Connected.Annotations;
using Connected.ServiceModel;
namespace Common.Numbering;
[Service]
[ServiceUrl(NumberingUrls.Numbering)]
public interface INumberingService
{
[ServiceMethod(ServiceMethodVerbs.Get | ServiceMethodVerbs.Post)]
Task<string> Calculate(NumberingCalculateArgs args);
[ServiceMethod(ServiceMethodVerbs.Get | ServiceMethodVerbs.Post)]
Task<INumbering?> Select(NumberingSelectArgs args);
[ServiceMethod(ServiceMethodVerbs.Get | ServiceMethodVerbs.Post)]
Task<INumbering?> Select(PrimaryKeyArgs<int> args);
}

@ -0,0 +1,15 @@
using Connected.ServiceModel;
using System.ComponentModel.DataAnnotations;
namespace Common.Numbering;
public sealed class NumberingCalculateArgs : Dto
{
[Required, MaxLength(128)]
public string Entity { get; set; } = default!;
}
public sealed class NumberingSelectArgs : Dto
{
[Required, MaxLength(128)]
public string Entity { get; set; } = default!;
}

@ -0,0 +1,5 @@
namespace Common.Numbering;
public static class NumberingUrls
{
public const string Numbering = "common/numbering";
}

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Framework\Connected.Services\Connected.Services.csproj" />
<ProjectReference Include="..\Common.Numbering.Middleware\Common.Numbering.Middleware.csproj" />
<ProjectReference Include="..\Common.Numbering.Model\Common.Numbering.Model.csproj" />
<ProjectReference Include="..\Common\Common.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,23 @@
using Connected.Annotations;
using Connected.Middleware;
namespace Common.Numbering;
[Priority(0)]
internal sealed class DefaultProvider : MiddlewareComponent, INumberingProvider
{
public DefaultProvider(INumberingService numbering)
{
Numbering = numbering;
}
private INumberingService Numbering { get; }
public async Task<string> Invoke(NumberingCalculateArgs args)
{
if (Numbering is not NumberingService service)
throw new InvalidCastException(nameof(NumberingService));
return await service.NextValue(args);
}
}

@ -0,0 +1,15 @@
using Connected.Annotations;
using Connected.Entities.Annotations;
using Connected.Entities.Consistency;
using System.ComponentModel.DataAnnotations;
namespace Common.Numbering;
[Table(Schema = CommonSchemas.CommonSchema)]
internal sealed record Numbering : ConsistentEntity<int>, INumbering
{
[Ordinal(0), MaxLength(128), Index(Name = $"ui_{CommonSchemas.CommonSchema}_numbering_entity")]
public string Entity { get; init; } = default!;
[Ordinal(1), MaxLength(128)]
public string Value { get; init; } = default!;
}

@ -0,0 +1,137 @@
using Connected.Entities;
using Connected.Entities.Storage;
using Connected.Interop;
using Connected.Middleware;
using Connected.ServiceModel;
using Connected.Services;
namespace Common.Numbering;
internal sealed class NumberingOps
{
public sealed class Calculate : ServiceFunction<NumberingCalculateArgs, string>
{
public Calculate(IMiddlewareService middleware)
{
Middleware = middleware;
}
private IMiddlewareService Middleware { get; }
protected override async Task<string?> OnInvoke()
{
foreach (var middleware in await Middleware.Query<INumberingProvider>())
{
if (await middleware.Invoke(Arguments) is string result && !string.IsNullOrEmpty(result))
return result;
}
return null;
}
}
public sealed class NextValue : ServiceFunction<NumberingCalculateArgs, string>
{
public NextValue(IStorageProvider storage, INumberingService numbering)
{
Storage = storage;
Numbering = numbering;
}
private IStorageProvider Storage { get; }
private INumberingService Numbering { get; }
protected override async Task<string?> OnInvoke()
{
var result = await Prepare();
await Storage.Open<Numbering>().Update(result, Arguments,
async () =>
{
result = await Prepare();
return result;
},
async (f) =>
{
await Task.CompletedTask;
return f;
});
return result.Value;
}
private async Task<Numbering> Prepare()
{
var current = await Numbering.Select(Arguments.AsArguments<NumberingSelectArgs>());
if (current is null)
{
var id = await TryInsert();
if (id == 0)
current = await Numbering.Select(Arguments.AsArguments<NumberingSelectArgs>());
else
current = await Numbering.Select(id);
}
if (current is null)
throw new NullReferenceException(nameof(INumbering));
var newValue = string.Empty;
if (TypeConversion.TryConvert(current.Value, out long existingValue))
newValue = existingValue++.ToString();
else
newValue = current.Value;
return (Numbering)current with { Value = newValue };
}
private async Task<int> TryInsert()
{
try
{
return (await Storage.Open<Numbering>().Update(Arguments.AsEntity<Numbering>(State.New))).Id;
}
catch
{
return 0;
}
}
}
public sealed class SelectByEntity : ServiceFunction<NumberingSelectArgs, INumbering>
{
public SelectByEntity(IStorageProvider storage)
{
Storage = storage;
}
private IStorageProvider Storage { get; }
protected override async Task<INumbering?> OnInvoke()
{
return await (from e in Storage.Open<Numbering>()
where string.Equals(e.Entity, Arguments.Entity, StringComparison.OrdinalIgnoreCase)
select e).AsEntity();
}
}
public sealed class Select : ServiceFunction<PrimaryKeyArgs<int>, INumbering>
{
public Select(IStorageProvider storage)
{
Storage = storage;
}
private IStorageProvider Storage { get; }
protected override async Task<INumbering?> OnInvoke()
{
return await (from e in Storage.Open<Numbering>()
where e.Id == Arguments.Id
select e).AsEntity();
}
}
}

@ -0,0 +1,31 @@
using Connected.ServiceModel;
using Connected.Services;
using Ops = Common.Numbering.NumberingOps;
namespace Common.Numbering;
internal sealed class NumberingService : Service, INumberingService
{
public NumberingService(IContext context) : base(context)
{
}
public async Task<string> Calculate(NumberingCalculateArgs args)
{
return await Invoke(GetOperation<Ops.Calculate>(), args);
}
public async Task<INumbering?> Select(NumberingSelectArgs args)
{
return await Invoke(GetOperation<Ops.SelectByEntity>(), args);
}
public async Task<INumbering?> Select(PrimaryKeyArgs<int> args)
{
return await Invoke(GetOperation<Ops.Select>(), args);
}
internal async Task<string> NextValue(NumberingCalculateArgs args)
{
return await Invoke(GetOperation<Ops.NextValue>(), args);
}
}

@ -0,0 +1,121 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.4.32916.344
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Dependencies", "Dependencies", "{75ED46E6-38CD-4948-9F3C-5167A9FFD7FA}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common", "Common\Common.csproj", "{CB582FC6-7A9A-46D1-BA75-4A103E096674}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.Globalization", "..\Framework\Connected.Globalization\Connected.Globalization.csproj", "{B9438432-CD2B-4570-B96C-C1B47A1CF5BB}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.Net", "..\Framework\Connected.Net\Connected.Net.csproj", "{664BD509-4D4A-45D4-8B82-5E54094A95E6}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.Services", "..\Framework\Connected.Services\Connected.Services.csproj", "{A4616625-88A0-434B-8433-EC8693E1E53B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.Validation", "..\Framework\Connected.Validation\Connected.Validation.csproj", "{71403C62-2D04-4E0B-9FAB-1ED23B0FB6B7}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected", "..\Connected\Connected\Connected.csproj", "{B2AE8588-0786-4556-9563-0D941A145C7C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common.Model", "Common.Model\Common.Model.csproj", "{ABF6BF35-ED9F-43A9-8581-23CBD9701E94}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common.Notes", "Common.Notes\Common.Notes.csproj", "{05B6148F-467C-4090-8FEA-8EA16A4D9956}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common.Notes.Model", "Common.Notes.Model\Common.Notes.Model.csproj", "{DF77BAAB-8223-4BEA-B7BA-0C087D9C6750}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.Entities", "..\Framework\Connected.Entities\Connected.Entities.csproj", "{0D2BE8CB-1C1B-4C74-9940-8F1BA5B1ED42}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.ServiceModel", "..\Framework.ServiceModel\Connected.ServiceModel\Connected.ServiceModel.csproj", "{3F224810-2034-45BB-BB53-DE5F6E83A07B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common.Numbering.Model", "Common.Numbering.Model\Common.Numbering.Model.csproj", "{83F83DE6-28BF-4AB7-902E-42D38A54A578}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common.Numbering", "Common.Numbering\Common.Numbering.csproj", "{4E810676-E37E-4C25-95FC-747BB32E5A69}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common.Numbering.Middleware", "Common.Numbering.Middleware\Common.Numbering.Middleware.csproj", "{424C1CBE-8490-431C-A0A8-1ACB22A65864}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.Middleware", "..\Framework\Connected.Middleware\Connected.Middleware.csproj", "{085068A9-6739-423D-957F-DFA22564B574}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{CB582FC6-7A9A-46D1-BA75-4A103E096674}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CB582FC6-7A9A-46D1-BA75-4A103E096674}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CB582FC6-7A9A-46D1-BA75-4A103E096674}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CB582FC6-7A9A-46D1-BA75-4A103E096674}.Release|Any CPU.Build.0 = Release|Any CPU
{B9438432-CD2B-4570-B96C-C1B47A1CF5BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B9438432-CD2B-4570-B96C-C1B47A1CF5BB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B9438432-CD2B-4570-B96C-C1B47A1CF5BB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B9438432-CD2B-4570-B96C-C1B47A1CF5BB}.Release|Any CPU.Build.0 = Release|Any CPU
{664BD509-4D4A-45D4-8B82-5E54094A95E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{664BD509-4D4A-45D4-8B82-5E54094A95E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{664BD509-4D4A-45D4-8B82-5E54094A95E6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{664BD509-4D4A-45D4-8B82-5E54094A95E6}.Release|Any CPU.Build.0 = Release|Any CPU
{A4616625-88A0-434B-8433-EC8693E1E53B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A4616625-88A0-434B-8433-EC8693E1E53B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A4616625-88A0-434B-8433-EC8693E1E53B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A4616625-88A0-434B-8433-EC8693E1E53B}.Release|Any CPU.Build.0 = Release|Any CPU
{71403C62-2D04-4E0B-9FAB-1ED23B0FB6B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{71403C62-2D04-4E0B-9FAB-1ED23B0FB6B7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{71403C62-2D04-4E0B-9FAB-1ED23B0FB6B7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{71403C62-2D04-4E0B-9FAB-1ED23B0FB6B7}.Release|Any CPU.Build.0 = Release|Any CPU
{B2AE8588-0786-4556-9563-0D941A145C7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B2AE8588-0786-4556-9563-0D941A145C7C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B2AE8588-0786-4556-9563-0D941A145C7C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B2AE8588-0786-4556-9563-0D941A145C7C}.Release|Any CPU.Build.0 = Release|Any CPU
{ABF6BF35-ED9F-43A9-8581-23CBD9701E94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ABF6BF35-ED9F-43A9-8581-23CBD9701E94}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ABF6BF35-ED9F-43A9-8581-23CBD9701E94}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ABF6BF35-ED9F-43A9-8581-23CBD9701E94}.Release|Any CPU.Build.0 = Release|Any CPU
{05B6148F-467C-4090-8FEA-8EA16A4D9956}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{05B6148F-467C-4090-8FEA-8EA16A4D9956}.Debug|Any CPU.Build.0 = Debug|Any CPU
{05B6148F-467C-4090-8FEA-8EA16A4D9956}.Release|Any CPU.ActiveCfg = Release|Any CPU
{05B6148F-467C-4090-8FEA-8EA16A4D9956}.Release|Any CPU.Build.0 = Release|Any CPU
{DF77BAAB-8223-4BEA-B7BA-0C087D9C6750}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DF77BAAB-8223-4BEA-B7BA-0C087D9C6750}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DF77BAAB-8223-4BEA-B7BA-0C087D9C6750}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DF77BAAB-8223-4BEA-B7BA-0C087D9C6750}.Release|Any CPU.Build.0 = Release|Any CPU
{0D2BE8CB-1C1B-4C74-9940-8F1BA5B1ED42}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0D2BE8CB-1C1B-4C74-9940-8F1BA5B1ED42}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0D2BE8CB-1C1B-4C74-9940-8F1BA5B1ED42}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0D2BE8CB-1C1B-4C74-9940-8F1BA5B1ED42}.Release|Any CPU.Build.0 = Release|Any CPU
{3F224810-2034-45BB-BB53-DE5F6E83A07B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3F224810-2034-45BB-BB53-DE5F6E83A07B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3F224810-2034-45BB-BB53-DE5F6E83A07B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3F224810-2034-45BB-BB53-DE5F6E83A07B}.Release|Any CPU.Build.0 = Release|Any CPU
{83F83DE6-28BF-4AB7-902E-42D38A54A578}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{83F83DE6-28BF-4AB7-902E-42D38A54A578}.Debug|Any CPU.Build.0 = Debug|Any CPU
{83F83DE6-28BF-4AB7-902E-42D38A54A578}.Release|Any CPU.ActiveCfg = Release|Any CPU
{83F83DE6-28BF-4AB7-902E-42D38A54A578}.Release|Any CPU.Build.0 = Release|Any CPU
{4E810676-E37E-4C25-95FC-747BB32E5A69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4E810676-E37E-4C25-95FC-747BB32E5A69}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4E810676-E37E-4C25-95FC-747BB32E5A69}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4E810676-E37E-4C25-95FC-747BB32E5A69}.Release|Any CPU.Build.0 = Release|Any CPU
{424C1CBE-8490-431C-A0A8-1ACB22A65864}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{424C1CBE-8490-431C-A0A8-1ACB22A65864}.Debug|Any CPU.Build.0 = Debug|Any CPU
{424C1CBE-8490-431C-A0A8-1ACB22A65864}.Release|Any CPU.ActiveCfg = Release|Any CPU
{424C1CBE-8490-431C-A0A8-1ACB22A65864}.Release|Any CPU.Build.0 = Release|Any CPU
{085068A9-6739-423D-957F-DFA22564B574}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{085068A9-6739-423D-957F-DFA22564B574}.Debug|Any CPU.Build.0 = Debug|Any CPU
{085068A9-6739-423D-957F-DFA22564B574}.Release|Any CPU.ActiveCfg = Release|Any CPU
{085068A9-6739-423D-957F-DFA22564B574}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{B9438432-CD2B-4570-B96C-C1B47A1CF5BB} = {75ED46E6-38CD-4948-9F3C-5167A9FFD7FA}
{664BD509-4D4A-45D4-8B82-5E54094A95E6} = {75ED46E6-38CD-4948-9F3C-5167A9FFD7FA}
{A4616625-88A0-434B-8433-EC8693E1E53B} = {75ED46E6-38CD-4948-9F3C-5167A9FFD7FA}
{71403C62-2D04-4E0B-9FAB-1ED23B0FB6B7} = {75ED46E6-38CD-4948-9F3C-5167A9FFD7FA}
{B2AE8588-0786-4556-9563-0D941A145C7C} = {75ED46E6-38CD-4948-9F3C-5167A9FFD7FA}
{0D2BE8CB-1C1B-4C74-9940-8F1BA5B1ED42} = {75ED46E6-38CD-4948-9F3C-5167A9FFD7FA}
{3F224810-2034-45BB-BB53-DE5F6E83A07B} = {75ED46E6-38CD-4948-9F3C-5167A9FFD7FA}
{085068A9-6739-423D-957F-DFA22564B574} = {75ED46E6-38CD-4948-9F3C-5167A9FFD7FA}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {23EF0531-5260-4441-876C-7569138BD7FA}
EndGlobalSection
EndGlobal

@ -0,0 +1,25 @@
using Connected.Collections.Concurrent;
using Connected.Collections.Queues;
using Connected.ServiceModel;
namespace Common.Collections;
internal sealed class MessageJob : DispatcherJob<IQueueMessage>
{
public MessageJob(IContextProvider provider)
{
Provider = provider;
}
public IContextProvider Provider { get; }
protected override async Task OnInvoke(IQueueMessage args, CancellationToken cancellationToken)
{
//TODO: need to do proper conversions
using var ctx = Provider.Create();
var type = Type.GetType(args.Queue);
var client = ctx.GetService(type) as IQueueClient<QueueArgs>;
await client.Initialize();
await client.Invoke(args, args.Arguments);
}
}

@ -0,0 +1,19 @@
using Connected.Entities.Caching;
namespace Common.Collections;
internal interface IQueueCache : IEntityCacheClient<QueueMessage, long>
{
void Update(QueueMessage message);
}
internal class QueueCache : EntityCacheClient<QueueMessage, long>, IQueueCache
{
public QueueCache(IEntityCacheContext context) : base(context, QueueMessage.CacheKey)
{
}
public void Update(QueueMessage message)
{
Set(message.Id, message, TimeSpan.Zero);
}
}

@ -0,0 +1,53 @@
using Connected.Collections.Queues;
using Connected.Hosting.Workers;
using Connected.Middleware;
using Connected.ServiceModel;
namespace Common.Collections;
internal sealed class QueueClientService : ScheduledWorker
{
public QueueClientService(IContextProvider provider)
{
Dispatcher = new();
Timer = TimeSpan.FromMilliseconds(500);
Queues = new();
Provider = provider;
}
private IContextProvider Provider { get; }
private QueueMessageDispatcher Dispatcher { get; }
private List<string> Queues { get; }
public override async Task StartAsync(CancellationToken cancellationToken)
{
using var ctx = Provider.Create();
if (ctx.GetService<IMiddlewareService>() is not IMiddlewareService middleware)
return;
foreach (var m in await middleware.Query<IQueueClient<QueueArgs>>())
{
if (m.GetType().FullName is string fullName)
Queues.Add(fullName);
}
}
protected override async Task OnInvoke(CancellationToken cancellationToken)
{
using var ctx = Provider.Create();
if (ctx.GetService<IQueueService>() is not IQueueService queue)
return;
var messages = await queue.Dequeue(new DequeueArgs
{
MaxCount = Dispatcher.Available,
NextVisible = TimeSpan.FromSeconds(30),
Queues = Queues
});
foreach (var message in messages)
Dispatcher.Enqueue(message);
}
}

@ -0,0 +1,49 @@
using Connected.Annotations;
using Connected.Collections.Queues;
using Connected.Data;
using Connected.Entities.Annotations;
using Connected.Entities.Concurrency;
namespace Common.Collections;
/// <inheritdoc cref="IQueueMessage"/>
[Table(Schema = SchemaAttribute.SysSchema)]
internal sealed record QueueMessage : ConcurrentEntity<long>, IQueueMessage
{
public const string CacheKey = $"{SchemaAttribute.SysSchema}.{nameof(QueueMessage)}";
/// <inheritdoc cref="IQueueMessage.Created"/>
[Ordinal(0), Date(Kind = DateKind.DateTime)]
public DateTime Created { get; init; }
/// <inheritdoc cref="IQueueMessage.DequeueCount"/>
[Ordinal(2)]
public int DequeueCount { get; init; }
/// <inheritdoc cref="IQueueMessage.DequeueTimestamp"/>
[Ordinal(3), Date(Kind = DateKind.DateTime)]
public DateTime? DequeueTimestamp { get; init; }
/// <inheritdoc cref="IQueueMessage.Queue"/>
[Ordinal(4), Length(32)]
public string Queue { get; init; } = default!;
/// <summary>
/// The serialized value of the arguments if specified.
/// </summary>
/// <remarks>
/// If the queue message must be persisted the arguments get serialized
/// and stored in this property. When selected from the storage, the
/// Arguments object is recreated from this property.
/// Note that queue serialized Arguments must be less that 1024 characters long.
/// </
[Ordinal(7), Length(1024)]
public string? SerializedArguments { get; init; }
/// <inheritdoc cref="IPopReceipt.NextVisible"/>
[Ordinal(8), Date(Kind = DateKind.DateTime)]
public DateTime NextVisible { get; init; }
/// <inheritdoc cref="IPopReceipt.PopReceipt"/>
[Ordinal(9)]
public Guid? PopReceipt { get; init; }
/// <inheritdoc cref="IPopReceipt.PopReceipt"/>
/// <remarks>
/// This property is persisted through the <see cref="SerializedArguments"/>
/// property.
/// </remarks>
[Persistence(Persistence = ColumnPersistence.InMemory)]
public QueueArgs Arguments { get; init; }
}

@ -0,0 +1,10 @@
using Connected.Collections.Concurrent;
using Connected.Collections.Queues;
namespace Common.Collections;
internal sealed class QueueMessageDispatcher : Dispatcher<IQueueMessage, MessageJob>
{
public QueueMessageDispatcher() : base(128)
{
}
}

@ -0,0 +1,98 @@
using System.Collections.Immutable;
using Connected.Collections.Queues;
using Connected.Entities;
using Connected.Interop;
using Connected.Services;
namespace Common.Collections;
internal sealed class QueueOps
{
public sealed class Dequeue : ServiceFunction<DequeueArgs, ImmutableList<IQueueMessage>>
{
public Dequeue(IQueueCache cache)
{
Cache = cache;
}
private IQueueCache Cache { get; }
protected override async Task<ImmutableList<IQueueMessage>?> OnInvoke()
{
var targets = await SelectTargets();
var result = new List<IQueueMessage>();
if (!targets.Any())
return ImmutableList<IQueueMessage>.Empty;
foreach (var message in targets)
{
var modified = new QueueMessage
{
DequeueTimestamp = DateTime.UtcNow,
Arguments = message.Arguments,
Created = message.Created,
DequeueCount = message.DequeueCount + 1,
Id = message.Id,
NextVisible = DateTime.UtcNow.Add(Arguments.NextVisible),
PopReceipt = Guid.NewGuid(),
Queue = message.Queue,
State = State.Default,
Sync = message.Sync,
ETag = message.ETag
};
try
{
Cache.Update(modified);
result.Add(modified);
}
catch
{
//concurrent exception, someone was faster
}
}
return result.ToImmutableList();
}
private async Task<ImmutableList<QueueMessage>> SelectTargets()
{
var targets = new List<QueueMessage>();
var items = await (from dc in Cache
where dc.NextVisible <= DateTime.UtcNow
&& dc.Arguments.Options.Expire > DateTime.UtcNow
&& Arguments.Queues.Any(f => string.Equals(f, dc.Queue, StringComparison.OrdinalIgnoreCase))
select dc).AsEntities();
if (!items.Any())
return ImmutableList<QueueMessage>.Empty;
var ordered = targets.OrderBy(f => f.NextVisible).ThenBy(f => f.Id);
if (ordered.Count() <= Arguments.MaxCount)
return ordered.ToImmutableList();
return ordered.Take(Arguments.MaxCount).ToImmutableList();
}
}
public sealed class Enqueue<TClient> : ServiceAction<QueueArgs>
{
public Enqueue(IQueueCache cache)
{
Cache = cache;
}
private IQueueCache Cache { get; }
protected override Task OnInvoke()
{
var message = Serializer.Serialize(Arguments);
Cache.Update(Arguments.AsEntity<QueueMessage>(State.New, new { Arguments, SerializedMessage = message, Queue = typeof(TClient).FullName }));
return Task.CompletedTask;
}
}
}

@ -0,0 +1,40 @@
using System.Collections.Immutable;
using Connected.Collections;
using Connected.Collections.Queues;
using Connected.Net;
using Connected.ServiceModel;
using Connected.Services;
using Ops = Common.Collections.QueueOps;
namespace Common.Collections;
internal sealed class QueueService : DistributedService, IQueueService
{
public QueueService(IContext context) : base(context)
{
}
public async Task<ImmutableList<IQueueMessage>> Dequeue(DequeueArgs args)
{
if (await IsServer())
return await Invoke(GetOperation<Ops.Dequeue>(), args) ?? ImmutableList<IQueueMessage>.Empty;
if (await Http.Get<List<QueueMessage>>(await ParseUrl(CollectionRoutes.Queue), args) is List<QueueMessage> result)
return result.ToImmutableList<IQueueMessage>();
return ImmutableList<IQueueMessage>.Empty;
}
public async Task Enqueue<TClient, TArgs>(TArgs args)
where TClient : IQueueClient<TArgs>
where TArgs : QueueArgs
{
if (await IsServer())
{
await Invoke(GetOperation<Ops.Enqueue<TClient>>(), args);
return;
}
await Http.Post<ImmutableList<IQueueMessage>>(await ParseUrl(CollectionRoutes.Queue), new object[] { typeof(TClient), args });
}
}

@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>$(MSBuildProjectName)</AssemblyName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Connected\Connected\Connected.csproj" />
<ProjectReference Include="..\..\Framework.ServiceModel\Connected.ServiceModel\Connected.ServiceModel.csproj" />
<ProjectReference Include="..\..\Framework\Connected.Globalization\Connected.Globalization.csproj" />
<ProjectReference Include="..\..\Framework\Connected.Net\Connected.Net.csproj" />
<ProjectReference Include="..\..\Framework\Connected.Services\Connected.Services.csproj" />
<ProjectReference Include="..\..\Framework\Connected.Validation\Connected.Validation.csproj" />
<ProjectReference Include="..\Common.Model\Common.Model.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>
<CustomToolNamespace></CustomToolNamespace>
</EmbeddedResource>
</ItemGroup>
</Project>

@ -0,0 +1,6 @@
namespace Common;
public static class CommonSchemas
{
public const string DocumentSchema = "dcm";
public const string CommonSchema = "cmn";
}

@ -0,0 +1,39 @@
using Common.Documents;
using Common.Globalization;
using Common.Security.Identity;
using Connected;
using Connected.Annotations;
using Connected.Globalization;
using Connected.Net.Endpoints;
using Connected.Net.Server;
using Connected.Security.Identity;
using Connected.ServiceModel;
using Microsoft.Extensions.DependencyInjection;
[assembly: MicroService(MicroServiceType.Service)]
namespace Common
{
internal class CommonStartup : Startup
{
protected override void OnConfigureServices(IServiceCollection services)
{
services.AddScoped(typeof(IGlobalizationService), typeof(GlobalizationService));
services.AddScoped(typeof(IIdentityService), typeof(IdentityService));
services.AddTransient(typeof(IDocumentLocker<,>), typeof(DocumentLocker<,>));
}
protected override async Task OnInitialize(Dictionary<string, string> args)
{
if (Services is null || Services.GetService<IContextProvider>() is not IContextProvider provider)
return;
using var ctx = provider.Create();
if (ctx.GetService<IEndpointService>() is not IEndpointService endpoints || ctx.GetService<IEndpointServer>() is not IEndpointServer server)
return;
await server.Initialize(await endpoints.Query(), ctx.CancellationToken);
}
}
}

@ -0,0 +1,19 @@
using Connected.Data;
namespace Common;
public static class RecordStatusLocalizer
{
public static string Localize(Status status) =>
status switch
{
Status.Enabled => SR.RecordStatusEnabled,
Status.Disabled => SR.RecordStatusDisabled,
_ => status.ToString()
};
}
public static class Constants
{
public const string CommonSchema = "cmn";
}

@ -0,0 +1,22 @@
using System.ComponentModel.DataAnnotations;
using Connected.Annotations;
using Connected.Entities.Annotations;
using Connected.ServiceModel.Annotations;
using Connected.ServiceModel.Data;
namespace Common.Distributed;
[Table(Schema = CommonSchemas.CommonSchema)]
internal sealed record DistributedLock : TableEntity<Guid, string>, IDistributedLock
{
public const string EntityKey = $"{SchemaAttribute.SysSchema}.{nameof(DistributedLock)}";
[Ordinal(-50000), PartitionKey, MaxLength(128)]
public string Entity { get; init; } = default!;
[Ordinal(-49000), PrimaryKey, MaxLength(128)]
public string EntityId { get; init; } = default!;
[Ordinal(1)]
public DateTimeOffset Expiration { get; init; }
}

@ -0,0 +1,19 @@
using Connected.ServiceModel;
using System.ComponentModel.DataAnnotations;
namespace Common.Distributed;
public sealed class DistributedLockArgs : Dto
{
[Required, MaxLength(128)]
public string Entity { get; set; } = default!;
[Required, MaxLength(128)]
public string EntityId { get; set; } = default!;
public TimeSpan? Duration { get; set; }
}
public sealed class DistributedLockPingArgs : PrimaryKeyArgs<Guid>
{
public TimeSpan? Duration { get; set; }
}

@ -0,0 +1,126 @@
using Connected.Caching;
using Connected.Entities;
using Connected.Entities.Storage;
using Connected.ServiceModel;
using Connected.Services;
namespace Common.Distributed;
internal sealed class DistributedLockOps
{
public class Lock : ServiceFunction<DistributedLockArgs, Guid>
{
static Lock()
{
SynchronizationState = new();
}
private static HashSet<string> SynchronizationState { get; }
public Lock(IStorageProvider storage, ICacheContext cache)
{
Cache = cache;
Storage = storage;
}
private ICacheContext Cache { get; }
private IStorageProvider Storage { get; }
private IDistributedLock Entity { get; set; }
protected override async Task<Guid> OnInvoke()
{
/*
* We must ensure that only one lock request is performed at a time for each entity. This is
* because we must guarantee that one and only one entry exist for each entity and its primary key.
*/
var key = $"{Arguments.Entity}.{Arguments.EntityId}".ToLowerInvariant();
/*
* If the hashset already holds the key it means someone else was faster than we and we are not able to
* perform a lock.
*/
if (!SynchronizationState.Add(key))
throw new SynchronizationLockException($"{SR.ValLock} ({key})");
try
{
Entity = await Storage.Open<DistributedLock>().Update(Arguments.AsEntity<DistributedLock>(State.New, new
{
Id = Guid.NewGuid(),
Expiration = DateTime.UtcNow.Add(Arguments.Duration ?? TimeSpan.FromSeconds(5))
}));
return Entity.Id;
}
finally
{
/*
* Free to remove the hash lock. Any subsequent lock requests will fail at data protection level.
*/
SynchronizationState.Remove(key);
}
}
protected override async Task OnCommitted()
{
/*
* Put the lock it the local cache.
*/
Cache.Set(DistributedLock.EntityKey, Entity.Id, Entity, TimeSpan.Zero);
await Task.CompletedTask;
}
}
public sealed class Unlock : ServiceAction<PrimaryKeyArgs<Guid>>
{
public Unlock(IStorageProvider storage, ICacheContext cache)
{
Storage = storage;
Cache = cache;
}
private IStorageProvider Storage { get; }
private ICacheContext Cache { get; }
protected override async Task OnInvoke()
{
await Storage.Open<DistributedLock>().Update(Arguments.AsEntity<DistributedLock>(State.Deleted));
}
protected override async Task OnCommitted()
{
await Cache.Remove(DistributedLock.EntityKey, Arguments.Id);
}
}
public sealed class Ping : ServiceAction<DistributedLockPingArgs>
{
public Ping(IStorageProvider storage, ICacheContext cache)
{
Storage = storage;
Cache = cache;
}
private IStorageProvider Storage { get; }
private ICacheContext Cache { get; }
protected override async Task OnInvoke()
{
var entity = await Cache.Get<IDistributedLock>(DistributedLock.EntityKey, Arguments.Id,
async (f) =>
{
return await (from dc in Storage.Open<DistributedLock>() where dc.Id == Arguments.Id select dc).AsEntity();
});
if (entity is not null)
{
await Storage.Open<DistributedLock>().Update(Arguments.AsEntity<DistributedLock>(State.Default, new
{
Expiration = DateTime.UtcNow.Add(Arguments.Duration ?? TimeSpan.FromSeconds(5))
}));
}
}
protected override async Task OnCommitted()
{
await Cache.Remove(DistributedLock.EntityKey, Arguments.Id);
}
}
}

@ -0,0 +1,73 @@
using Connected.Caching;
using Connected.Data.DataProtection;
using Connected.Data.EntityProtection;
using Connected.Entities;
using Connected.Entities.Storage;
using Connected.Middleware;
namespace Common.Distributed;
internal sealed class DistributedLockProtector : MiddlewareComponent, IEntityProtector<DistributedLock>
{
public DistributedLockProtector(IStorageProvider storage, ICacheContext cache)
{
Storage = storage;
Cache = cache;
}
private IStorageProvider Storage { get; }
private ICacheContext Cache { get; }
public async Task Invoke(EntityProtectionArgs<DistributedLock> args)
{
/*
* We don't care for deleted entities.
*/
if (args.State == State.Deleted)
return;
/*
* The most important thing is to prevent duplicate inserts. If the lock
* already exists we will reject the transaction. Each entity record can have
* only one entry in the distributed lock table.
*/
if (args.State == State.New)
{
/*
* First we'll look into the memory cache.
*/
var existing = Cache.Get<DistributedLock>(DistributedLock.EntityKey,
f => string.Equals(f.Entity, args.Entity.Entity, StringComparison.OrdinalIgnoreCase)
&& string.Equals(f.EntityId, args.Entity.EntityId, StringComparison.OrdinalIgnoreCase));
/*
* If the cache entry exists and holds a valid lock, we simply reject the transaction.
*/
if (existing is not null && existing.Expiration > DateTime.UtcNow)
throw new InvalidOperationException($"{SR.ValLock} ({args.Entity.Entity}, {args.Entity.EntityId})");
/*
* Entry doesn't exist in the cache. Let's look in the storage. Note that this scenarios is unusual because
* locks tend to be short and there are only two possible schenarios that lock exists in the storage but not
* in the cache:
* - the process has rebooted
* - the master has changed. It this case all distributed services are redirected to the new master which must
* load record by record from the cache. In this case it's most probably that all locks already expired but we
* must check that anyway
*/
var entry = await (from dc in Storage.Open<DistributedLock>()
where string.Equals(dc.Entity, args.Entity.Entity, StringComparison.OrdinalIgnoreCase)
&& string.Equals(dc.EntityId, args.Entity.EntityId, StringComparison.OrdinalIgnoreCase)
select dc).AsEntity();
/*
* It exists, we must perform additional checks.
*/
if (entry is not null)
{
/*
* Set it in the cache so it gets deleted by the recycling service.
*/
Cache.Set(DistributedLock.EntityKey, entry.Id, entry, TimeSpan.Zero);
if (entry.Expiration > DateTime.UtcNow)
throw new InvalidOperationException($"{SR.ValLock} ({args.Entity.Entity}, {args.Entity.EntityId})");
}
}
}
}

@ -0,0 +1,32 @@
using Connected.ServiceModel;
using Connected.Services;
using Ops = Common.Distributed.DistributedLockOps;
namespace Common.Distributed;
internal sealed class DistributedLockService : DistributedService, IDistributedLockService
{
public DistributedLockService(IContext context) : base(context)
{
}
/// <summary>
/// This method performs a distribubuted lock on an entity. If the lock
/// cannot be obtained the exception is thrown.
/// </summary>
/// <param name="args">The arguments containing the entity on which
/// a distributed lock will be performed.</param>
/// <returns>The key of a newly acquired lock.</returns>
public async Task<Guid> Lock(DistributedLockArgs args)
{
return await Invoke(GetOperation<Ops.Lock>(), args);
}
public async Task Ping(DistributedLockPingArgs args)
{
await Invoke(GetOperation<Ops.Ping>(), args);
}
public async Task Unlock(PrimaryKeyArgs<Guid> args)
{
await Invoke(GetOperation<Ops.Unlock>(), args);
}
}

@ -0,0 +1,9 @@
using Connected.Data;
namespace Common.Distributed;
public interface IDistributedLock : IPrimaryKey<Guid>
{
string Entity { get; init; }
string EntityId { get; init; }
DateTimeOffset Expiration { get; init; }
}

@ -0,0 +1,12 @@
using Connected.Annotations;
using Connected.ServiceModel;
namespace Common.Distributed;
[Service]
public interface IDistributedLockService
{
Task<Guid> Lock(DistributedLockArgs args);
Task Unlock(PrimaryKeyArgs<Guid> args);
Task Ping(DistributedLockPingArgs args);
}

@ -0,0 +1,32 @@
using System.ComponentModel.DataAnnotations;
using Connected.Annotations;
using Connected.Entities.Annotations;
using Connected.Entities.Consistency;
namespace Common.Documents;
/// <summary>
/// Default implementation of the <see cref="IDocument{TPrimaryKey}"/> interface.
/// </summary>
/// <typeparam name="TPrimaryKey">The type of the primary key used by document.
/// This is usually <c>int</c> or <c>long</c>.
/// </typeparam>
[Table(Schema = CommonSchemas.DocumentSchema)]
public abstract record Document<TPrimaryKey> : ConsistentEntity<TPrimaryKey>, IDocument<TPrimaryKey>
where TPrimaryKey : notnull
{
/// <inheritdoc cref="IDocument{TPrimaryKey}.Created"/>
[Ordinal(-1007), Date(Kind = DateKind.SmallDateTime)]
public DateTimeOffset Created { get; init; }
/// <inheritdoc cref="IDocument{TPrimaryKey}.Modified"/>
[Ordinal(-1006), Nullable, Date(Kind = DateKind.SmallDateTime)]
public DateTimeOffset? Modified { get; init; }
/// <inheritdoc cref="IDocument{TPrimaryKey}.Code"/>
[Ordinal(-1005), MaxLength(32), Nullable]
public string? Code { get; init; } = default!;
/// <inheritdoc cref="IDocument{TPrimaryKey}.Author"/>
[Ordinal(-1004), Nullable]
public int? Author { get; init; }
/// <inheritdoc cref="IDocument{TPrimaryKey}.Owner"/>
[Ordinal(-1003), Nullable]
public int? Owner { get; init; }
}

@ -0,0 +1,60 @@
using Connected;
using Connected.Notifications.Events;
namespace Common.Documents;
public abstract class DocumentListener<TArgs, TDocument, TPrimaryKey> : EventListener<TArgs>
where TArgs : IDto
where TDocument : IDocument<TPrimaryKey>
where TPrimaryKey : notnull
{
public DocumentListener(IDocumentLocker<TDocument, TPrimaryKey> locker)
{
Locker = locker;
}
private IDocumentLocker<TDocument, TPrimaryKey> Locker { get; }
protected abstract TDocument Document { get; }
protected override async Task OnInvoke()
{
await OnPreparing();
if (Document is null)
return;
Locker.Expired += async (s, e) => { await OnException(new TimeoutException()); };
try
{
await Locker.Lock(Document);
await OnInvoking();
await Locker.Unlock();
}
catch (Exception ex)
{
await Locker.Unlock();
await OnException(ex);
}
}
protected virtual async Task<bool> OnPreparing()
{
await Task.CompletedTask;
return false;
}
protected virtual async Task OnInvoking()
{
await Task.CompletedTask;
}
protected virtual async Task OnException(Exception ex)
{
await Task.CompletedTask;
}
protected override void OnDisposing()
{
Locker.Dispose();
}
}

@ -0,0 +1,96 @@
using Common.Distributed;
using Connected;
using Connected.Interop;
using Connected.ServiceModel;
using Connected.Threading;
namespace Common.Documents;
internal sealed class DocumentLocker<TDocument, TPrimaryKey> : IDocumentLocker<TDocument, TPrimaryKey>
where TDocument : IDocument<TPrimaryKey>
where TPrimaryKey : notnull
{
public event EventHandler? Expired;
public DocumentLocker(IDistributedLockService locking)
{
Locking = locking;
Cancel = new();
}
private bool IsDisposed { get; set; }
public Guid Key { get; private set; }
private IDistributedLockService Locking { get; }
public TimeSpan LockTimeout { get; set; } = TimeSpan.FromSeconds(2);
public TimeSpan Timeout { get; set; } = TimeSpan.FromMilliseconds(1500);
public TimeSpan Lifetime { get; set; } = TimeSpan.FromSeconds(30);
private CancellationTokenSource Cancel { get; }
public async Task Lock(TDocument document)
{
var id = TypeConversion.Convert<string>(document.Id);
if (string.IsNullOrEmpty(id))
throw new NullReferenceException(nameof(document.Id));
/*
* Obtain the lock on the document to ensure no other process
* will change it while we are updating the stock. If the lock
* could not be obtained the exception will be thrown.
*/
Key = await Locking.Lock(new DistributedLockArgs
{
Duration = LockTimeout,
Entity = typeof(TDocument).Name,
EntityId = id
});
/*
* Make sure we don't get outdated by using the scheduled
* task which will ping the lock before it expires.
*/
using var timeout = new ScheduledTask(async () =>
{
await Locking.Ping(new DistributedLockPingArgs
{
Id = Key,
Duration = LockTimeout
});
}, async () =>
{
await Unlock();
Expired?.Invoke(this, EventArgs.Empty);
}, Timeout, Lifetime, Cancel.Token);
timeout.Start();
}
public async Task Unlock()
{
/*
* Finally release the lock on the document so it becomes
* updatable again.
*/
await Locking.Unlock(new PrimaryKeyArgs<Guid> { Id = Key });
}
private void Dispose(bool disposing)
{
if (!IsDisposed)
{
if (disposing)
{
AsyncUtils.RunSync(Unlock);
Cancel.Cancel();
Cancel.Dispose();
}
IsDisposed = true;
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}

@ -0,0 +1,15 @@
using Connected.Notifications;
using Connected.ServiceModel;
using Connected.Services;
namespace Common.Documents;
public abstract class DocumentService<TDocumentPrimaryKey, TItemPrimaryKey> : EntityService<TDocumentPrimaryKey>, IDocumentService<TDocumentPrimaryKey, TItemPrimaryKey>
{
protected DocumentService(IContext context) : base(context)
{
}
public event ServiceEventHandler<PrimaryKeyEventArgs<TItemPrimaryKey>>? ItemInserted;
public event ServiceEventHandler<PrimaryKeyEventArgs<TItemPrimaryKey>>? ItemUpdated;
public event ServiceEventHandler<PrimaryKeyEventArgs<TItemPrimaryKey>>? ItemDeleted;
}

@ -0,0 +1,13 @@
namespace Common.Documents;
public interface IDocumentLocker<TDocument, TPrimaryKey> : IDisposable
where TDocument : IDocument<TPrimaryKey>
where TPrimaryKey : notnull
{
event EventHandler? Expired;
Guid Key { get; }
TimeSpan LockTimeout { get; set; }
TimeSpan Timeout { get; set; }
TimeSpan Lifetime { get; set; }
Task Lock(TDocument document);
Task Unlock();
}

@ -0,0 +1,61 @@
using Connected.Middleware;
using Connected.Security.Identity;
using Connected.Validation;
namespace Common.Documents;
public abstract class InsertDocumentValidator<TDocumentArgs> : MiddlewareComponent, IValidator<TDocumentArgs>
where TDocumentArgs : InsertDocumentArgs
{
public InsertDocumentValidator(IUserService users)
{
Users = users;
}
protected IUserService Users { get; }
protected TDocumentArgs Arguments { get; private set; }
public async Task Validate(TDocumentArgs args)
{
Arguments = args;
await ValidateAuthor();
}
protected virtual async Task OnValidating()
{
await Task.CompletedTask;
}
private async Task ValidateAuthor()
{
if (Arguments.Author is null)
return;
if (await Users.Select(Arguments.Author) is null)
throw new ArgumentException(nameof(Arguments.Author));
}
}
public abstract class UpdateDocumentValidator<TDocumentArgs, TPrimaryKey> : MiddlewareComponent, IValidator<TDocumentArgs>
where TDocumentArgs : UpdateDocumentArgs<TPrimaryKey>
where TPrimaryKey : notnull
{
public UpdateDocumentValidator(IUserService users)
{
Users = users;
}
protected IUserService Users { get; }
public async Task Validate(TDocumentArgs args)
{
await ValidateOwner(args);
}
private async Task ValidateOwner(TDocumentArgs args)
{
if (args.Owner is null)
return;
if (await Users.Select(args.Owner) is null)
throw new ArgumentException(nameof(args.Owner));
}
}

@ -0,0 +1,105 @@
using System.Globalization;
using Connected;
using Connected.Data;
using Connected.Globalization;
using Connected.Globalization.Languages;
using Connected.Security.Identity;
using Connected.ServiceModel;
namespace Common.Globalization;
internal class GlobalizationService : IGlobalizationService
{
private ILanguage _language;
private CultureInfo _culture;
public GlobalizationService(IIdentityService identityService, ILanguageService languageService)
{
IdentityService = identityService;
LanguageService = languageService;
}
private IIdentityService IdentityService { get; }
private ILanguageService LanguageService { get; }
public TimeZoneInfo TimeZone
{
get
{
if (!IdentityService.IsAuthenticated || IdentityService.CurrentUser is null)
return TimeZoneInfo.Utc;
else
{
try
{
if (TimeZoneInfo.FindSystemTimeZoneById(IdentityService.CurrentUser.TimeZone) is not TimeZoneInfo timezone)
return TimeZoneInfo.Utc;
else
return timezone;
}
catch
{
return TimeZoneInfo.Utc;
}
}
}
}
public DateTimeOffset Now => FromUtc(DateTimeOffset.UtcNow);
public async Task<ILanguage?> GetCurrentLanguage()
{
if (_language is null)
{
if (IdentityService.CurrentUser is not null && IdentityService.CurrentUser.Language > 0)
_language = await LanguageService.Select(new PrimaryKeyArgs<int>() { Id = IdentityService.CurrentUser.Language });
if (_language is not null && _language.Status == Status.Disabled)
_language = null;
}
return _language;
}
public async Task<CultureInfo> GetCurrentCulture()
{
if (_culture is null)
{
if (await GetCurrentLanguage() is not ILanguage language)
_culture = Thread.CurrentThread.CurrentUICulture;
else
{
try
{
if (CultureInfo.GetCultureInfo(language.Lcid) is not CultureInfo culture)
_culture = Thread.CurrentThread.CurrentUICulture;
}
catch
{
_culture = Thread.CurrentThread.CurrentUICulture;
}
}
}
return _culture;
}
public DateTime FromUtc(DateTime value)
{
return DateExtensions.FromUtc(value, TimeZone);
}
public DateTimeOffset FromUtc(DateTimeOffset value)
{
return DateExtensions.FromUtc(value, TimeZone);
}
public DateTime ToUtc(DateTime value)
{
return DateExtensions.ToUtc(value, TimeZone);
}
public DateTimeOffset ToUtc(DateTimeOffset value)
{
return DateExtensions.ToUtc(value, TimeZone);
}
}

@ -0,0 +1,31 @@
using Connected.Annotations;
using Connected.Data;
using Connected.Entities.Annotations;
using Connected.Entities.Consistency;
using Connected.Globalization.Languages;
namespace Common.Globalization;
/// <summary>
/// The implementation of the <see cref="ILanguage"/> entity.
/// </summary>
[Table(Schema = SchemaAttribute.SysSchema)]
internal record Language : ConsistentEntity<int>, ILanguage
{
public const string CacheKey = $"{SchemaAttribute.SysSchema}.{nameof(Language)}";
[Length(128)]
[Ordinal(0)]
public string? Name { get; init; }
[Ordinal(1)]
public int Lcid { get; init; }
[Default(Status.Enabled)]
[Ordinal(2)]
public Status Status { get; init; }
[Length(128)]
[Ordinal(3)]
public string? Mappings { get; init; }
}

@ -0,0 +1,77 @@
using System.Collections.Immutable;
using Connected.Entities.Caching;
using Connected.ServiceModel;
namespace Common.Globalization;
internal interface ILanguageCache : IEntityCacheClient<Language, int>
{
Language? Select(string mappings);
}
/// <summary>
/// Represents stateful cache of the <see cref="Language"/> entities.
/// </summary>
internal class LanguageCache : EntityCacheClient<Language, int>, ILanguageCache
{
private readonly Dictionary<string, Language> _mappings;
public LanguageCache(IEntityCacheContext context) : base(context, Language.CacheKey)
{
_mappings = new Dictionary<string, Language>(StringComparer.OrdinalIgnoreCase);
}
private Dictionary<string, Language> Mappings => _mappings;
protected override async Task<ImmutableList<Language>> OnInitializing(IContext context)
{
var result = await base.OnInitializing(context);
await ResetMappings();
return result;
}
protected override async Task<Language> OnInvalidating(IContext context, int id)
{
var result = await base.OnInvalidating(context, id);
await ResetMappings();
return result;
}
public Language? Select(string mappings)
{
if (string.IsNullOrWhiteSpace(mappings))
return null;
var tokens = mappings.Split(',', StringSplitOptions.RemoveEmptyEntries);
foreach (var token in tokens)
{
if (Mappings.TryGetValue(token.Trim(), out Language? language))
return language;
}
return null;
}
private async Task ResetMappings()
{
Mappings.Clear();
foreach (var language in await All())
{
if (string.IsNullOrWhiteSpace(language.Mappings))
continue;
var tokens = language.Mappings.Split(',', StringSplitOptions.RemoveEmptyEntries);
foreach (var token in tokens)
{
if (Mappings.TryGetValue(token.Trim(), out _))
continue;
Mappings.Add(token.Trim(), language);
}
}
}
}

@ -0,0 +1,234 @@
using System.Collections.Immutable;
using Connected;
using Connected.Entities;
using Connected.Entities.Storage;
using Connected.Globalization.Languages;
using Connected.Notifications.Events;
using Connected.ServiceModel;
using Connected.Services;
namespace Common.Globalization;
/// <summary>
/// Queries all <see cref="ILanguage"/> records except those marked as deleting.
/// </summary>
internal sealed class QueryLanguages : ServiceFunction<IDto, ImmutableList<ILanguage>?>
{
public QueryLanguages(ILanguageCache cache)
{
Cache = cache;
}
private ILanguageCache Cache { get; }
protected override async Task<ImmutableList<ILanguage>?> OnInvoke()
{
/*
* Filter records to return only currently valid record. Those with
* deleting state should never be returned to any client.
*/
return await (from dc in Cache
select dc).AsEntities<ILanguage>();
}
}
/// <summary>
/// Queries the <see cref="ILanguage"/> records for the specified set of ids.
/// </summary>
internal sealed class LookupLanguages : ServiceFunction<PrimaryKeyListArgs<int>, ImmutableList<ILanguage>>
{
public LookupLanguages(ILanguageCache cache)
{
Cache = cache;
}
private ILanguageCache Cache { get; }
protected override async Task<ImmutableList<ILanguage>?> OnInvoke()
{
if (Arguments?.IdList is null)
return default;
return await (from dc in Cache
where Arguments.IdList.Any(f => f == dc.Id)
select dc).AsEntities<ILanguage>();
}
}
/// <summary>
/// Returns first <see cref="ILanguage"/> with matches the provided mapping.
/// </summary>
internal sealed class ResolveLanguage : ServiceFunction<LanguageResolveArgs, ILanguage?>
{
public ResolveLanguage(ILanguageCache cache)
{
Cache = cache;
}
private ILanguageCache Cache { get; }
protected override Task<ILanguage?> OnInvoke()
{
return Task.FromResult<ILanguage?>(Cache.Select(Arguments.Mapping));
}
}
/// <summary>
/// Returns <see cref="ILanguage"/> with the specified id or null
/// if the record for the specified id does not exist.
/// </summary>
internal sealed class SelectLanguage : ServiceFunction<PrimaryKeyArgs<int>, ILanguage?>
{
public SelectLanguage(ILanguageCache cache)
{
Cache = cache;
}
private ILanguageCache Cache { get; }
protected override async Task<ILanguage?> OnInvoke()
{
return await (from dc in Cache
where dc.Id == Arguments.Id
select dc).AsEntity();
}
}
/// <summary>
/// Returns <see cref="ILanguage"/> with the specified name and rate or null if
/// the record with the specified arguments does not exist.
/// </summary>
internal sealed class SelectLanguageByName : ServiceFunction<NameArgs, ILanguage?>
{
public SelectLanguageByName(ILanguageCache cache)
{
Cache = cache;
}
private ILanguageCache Cache { get; }
protected override async Task<ILanguage?> OnInvoke()
{
return await (from dc in Cache
where string.Equals(dc.Name, Arguments.Name, StringComparison.OrdinalIgnoreCase)
select dc).AsEntity();
}
}
/// <summary>
/// Inserts a new <see cref="ILanguage"/> and returns its Id.
/// </summary>
internal sealed class InsertLanguage : ServiceFunction<LanguageInsertArgs, int>
{
public InsertLanguage(ILanguageService languageService, IStorageProvider storage, IEventService events, ILanguageCache cache)
{
LanguageService = languageService;
Storage = storage;
Events = events;
Cache = cache;
}
/// <summary>
/// We need this service to call a distribute event.
/// </summary>
private ILanguageService LanguageService { get; }
private IStorageProvider Storage { get; }
private IEventService Events { get; }
private ILanguageCache Cache { get; }
protected override async Task<int> OnInvoke()
{
/*
* First, create a new entity from the passed arguments and mark its state as new. This will
* signal the DatabaseContext to perform an insert operation when calling the Update.
*/
var entity = Arguments.AsEntity<Language>(State.New);
/*
* Call update on the DatabaseContext. This call will return a new ILanguage of the inserted
* entity.
*/
var result = await Storage.Open<Language>().Update(entity);
/*
* Return a newly inserted id to the caller.
*/
return result.Id;
}
protected override async Task OnCommitted()
{
await Cache.Refresh(Result);
/*
* If ILanguageServer is our implementation (and should be) it's a IServerNotificationsTriggers
* for sure.
*/
await Events.Enqueue(this, LanguageService, ServiceEvents.Inserted, Result);
}
}
internal sealed class DeleteLanguage : ServiceAction<PrimaryKeyArgs<int>>
{
public DeleteLanguage(ILanguageService languageService, IStorageProvider storage, ILanguageCache cache, IEventService events)
{
LanguageService = languageService;
Storage = storage;
Cache = cache;
Events = events;
}
private ILanguageService LanguageService { get; }
private IStorageProvider Storage { get; }
private ILanguageCache Cache { get; }
private IEventService Events { get; }
protected override async Task OnInvoke()
{
var entity = new Language { Id = Arguments.Id, State = State.Deleted };
await Storage.Open<Language>().Update(entity);
}
protected override async Task OnCommitted()
{
await Cache.Remove(Arguments.Id);
await Events.Enqueue(this, LanguageService, ServiceEvents.Deleted, Arguments.Id);
}
}
/// <summary>
/// Updates <see cref="ILanguage"/> entity.
/// </summary>
internal sealed class UpdateLanguage : ServiceAction<LanguageUpdateArgs>
{
public UpdateLanguage(ILanguageService languageService, IStorageProvider storage, ILanguageCache cache, IEventService events)
{
LanguageService = languageService;
Storage = storage;
Cache = cache;
Events = events;
}
private ILanguageService LanguageService { get; }
private IStorageProvider Storage { get; }
private ILanguageCache Cache { get; }
private IEventService Events { get; }
protected override async Task OnInvoke()
{
/*
* Updating Concurrency entity requires a bit more logic. Since Concurrency entity
* guarantees data consistency we must use a retry logic in case of
* Concurrency failure. We'll call Update method with reload lambda function.
*/
await Storage.Open<Language>().Update(await Load(), Arguments, async () =>
{
/*
* Remove entry from the cache to ensure it will be loaded from the database
* next time.
*/
await Cache.Refresh(Arguments.Id);
return await Load();
});
}
private async Task<Language> Load() => await (from dc in Cache where dc.Id == Arguments.Id select dc).AsEntity();
protected override async Task OnCommitted()
{
/*
* Once the update is complete remove the entity from the cache because its concurrency
* state is not valid enymore.
*/
await Cache.Remove(Arguments.Id);
/*
* Now trigger the distributed event notifying the update has completed.
*/
await Events.Enqueue(this, LanguageService, ServiceEvents.Updated, Arguments.Id);
}
}

@ -0,0 +1,66 @@
using System.Collections.Immutable;
using Common.Security;
using Connected.Globalization.Languages;
using Connected.ServiceModel;
using Connected.Services;
using Connected.Services.Annotations;
namespace Common.Globalization;
/// <summary>
/// The implementation of the <see cref="ILanguageService"/> service.
/// </summary>
internal class LanguageService : EntityService<int>, ILanguageService
{
public LanguageService(IContext context) : base(context)
{
}
[ServiceAuthorization(CommonClaims.CommonDelete)]
public async Task Delete(PrimaryKeyArgs<int> args)
{
await Invoke(GetOperation<DeleteLanguage>(), args);
}
[ServiceAuthorization(CommonClaims.CommonAdd)]
public async Task<int> Insert(LanguageInsertArgs args)
{
return await Invoke(GetOperation<InsertLanguage>(), args);
}
[ServiceAuthorization(CommonClaims.CommonRead)]
public async Task<ImmutableList<ILanguage>?> Query()
{
return await Invoke(GetOperation<QueryLanguages>(), Dto.Empty);
}
[ServiceAuthorization(CommonClaims.CommonRead)]
public async Task<ImmutableList<ILanguage>?> Query(PrimaryKeyListArgs<int> args)
{
return await Invoke(GetOperation<LookupLanguages>(), args);
}
[ServiceAuthorization(CommonClaims.CommonRead)]
public async Task<ILanguage?> Resolve(LanguageResolveArgs args)
{
return await Invoke(GetOperation<ResolveLanguage>(), args);
}
[ServiceAuthorization(CommonClaims.CommonRead)]
public async Task<ILanguage?> Select(PrimaryKeyArgs<int> args)
{
return await Invoke(GetOperation<SelectLanguage>(), args);
}
[ServiceAuthorization(CommonClaims.CommonRead)]
public async Task<ILanguage?> Select(NameArgs args)
{
return await Invoke(GetOperation<SelectLanguageByName>(), args);
}
[ServiceAuthorization(CommonClaims.CommonModify)]
public async Task Update(LanguageUpdateArgs args)
{
await Invoke(GetOperation<UpdateLanguage>(), args);
}
}

@ -0,0 +1,29 @@
using Connected.Annotations;
using Connected.Data;
using Connected.Entities.Annotations;
using Connected.Entities.Consistency;
using Connected.Net.Endpoints;
namespace Common.Net;
[Table(Schema = SchemaAttribute.DefaultSchema)]
internal record Endpoint : ConsistentEntity<int>, IEndpoint
{
public const string CacheKey = $"{SchemaAttribute.SysSchema}.{nameof(Endpoint)}";
[Ordinal(0)]
[Length(128)]
public string? Name { get; init; }
[Length(128)]
[Ordinal(1)]
public string? Address { get; init; }
[Default(Status.Enabled)]
[Ordinal(2)]
[Length(128)]
public string? AuthenticationToken { get; init; }
[Ordinal(3)]
public Status Status { get; init; }
}

@ -0,0 +1,11 @@
using Connected.Entities.Caching;
namespace Common.Net;
internal interface IEndpointCache : IEntityCacheClient<Endpoint, int> { }
internal class EndpointCache : EntityCacheClient<Endpoint, int>, IEndpointCache
{
public EndpointCache(IEntityCacheContext cachingService) : base(cachingService, Endpoint.CacheKey)
{
}
}

@ -0,0 +1,43 @@
using System.Collections.Immutable;
using Connected;
using Connected.Entities;
using Connected.Net.Endpoints;
using Connected.ServiceModel;
using Connected.Services;
namespace Common.Net;
/// <summary>
/// Endpoints are singleton but their service is scoped so we must use Isolated database connections for all methods.
/// </summary>
internal sealed class QueryEndpoints : ServiceFunction<IDto, ImmutableList<IEndpoint>?>
{
public QueryEndpoints(IEndpointCache cache)
{
Cache = cache;
}
private IEndpointCache Cache { get; }
protected override async Task<ImmutableList<IEndpoint>?> OnInvoke()
{
return await (from dc in Cache
select dc).AsEntities<IEndpoint>();
}
}
internal sealed class SelectEndpoint : ServiceFunction<PrimaryKeyArgs<int>, IEndpoint?>
{
public SelectEndpoint(IEndpointCache cache)
{
Cache = cache;
}
private IEndpointCache Cache { get; }
protected override async Task<IEndpoint?> OnInvoke()
{
return await (from dc in Cache
where dc.Id == Arguments.Id
select dc).AsEntity();
}
}

@ -0,0 +1,27 @@
using System.Collections.Immutable;
using Connected.Net;
using Connected.Net.Endpoints;
using Connected.ServiceModel;
using Connected.Services;
using Connected.Services.Annotations;
namespace Common.Net;
internal sealed class EndpointService : EntityService<int>, IEndpointService
{
public EndpointService(IContext context) : base(context)
{
}
[ServiceAuthorization(NetClaims.NetDiscovery)]
public async Task<ImmutableList<IEndpoint>?> Query()
{
return await Invoke(GetOperation<QueryEndpoints>(), Dto.Empty);
}
[ServiceAuthorization(NetClaims.NetDiscovery)]
public async Task<IEndpoint?> Select(PrimaryKeyArgs<int> args)
{
return await Invoke(GetOperation<SelectEndpoint>(), args);
}
}

@ -0,0 +1,99 @@
//------------------------------------------------------------------------------
// <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 Common {
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("Common.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 System roles are read only.
/// </summary>
internal static string ErrSysRole {
get {
return ResourceManager.GetString("ErrSysRole", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Disabled.
/// </summary>
internal static string RecordStatusDisabled {
get {
return ResourceManager.GetString("RecordStatusDisabled", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Enabled.
/// </summary>
internal static string RecordStatusEnabled {
get {
return ResourceManager.GetString("RecordStatusEnabled", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Cannot acquire distributed lock.
/// </summary>
internal static string ValLock {
get {
return ResourceManager.GetString("ValLock", resourceCulture);
}
}
}
}

@ -0,0 +1,132 @@
<?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="ErrSysRole" xml:space="preserve">
<value>System roles are read only</value>
</data>
<data name="RecordStatusDisabled" xml:space="preserve">
<value>Disabled</value>
</data>
<data name="RecordStatusEnabled" xml:space="preserve">
<value>Enabled</value>
</data>
<data name="ValLock" xml:space="preserve">
<value>Cannot acquire distributed lock</value>
</data>
</root>

@ -0,0 +1,9 @@
namespace Common.Security;
public static class CommonClaims
{
public const string CommonDelete = "Common Delete";
public const string CommonRead = "Common Read";
public const string CommonAdd = "Common Add";
public const string CommonModify = "Common Modify";
}

@ -0,0 +1,17 @@
using Connected.Security.Identity;
using Microsoft.AspNetCore.Http;
namespace Common.Security.Identity;
internal class IdentityService : IIdentityService
{
public IdentityService(IHttpContextAccessor contextAccessor)
{
HttpContext = contextAccessor.HttpContext;
}
private HttpContext? HttpContext { get; }
public IUser? CurrentUser => Identity?.User;
public bool IsAuthenticated => Identity is not null && Identity.IsAuthenticated;
private UserIdentity? Identity => HttpContext?.User?.Identity as UserIdentity;
}

@ -0,0 +1,16 @@
using Connected.Annotations;
using Connected.Entities.Annotations;
using Connected.Entities.Consistency;
using Connected.Security.Identity;
namespace Common.Security.Identity;
[Table(Schema = SchemaAttribute.SysSchema)]
internal record Role : ConsistentEntity<int>, IRole
{
public const string CacheKey = $"{SchemaAttribute.SysSchema}.{nameof(Role)}";
[Length(128)]
[Ordinal(0)]
public string? Name { get; init; }
}

@ -0,0 +1,40 @@
using Connected.Entities.Caching;
using Connected.Security.Identity;
namespace Common.Security.Identity;
internal interface IRoleCache : IEntityCacheClient<Role, int> { }
internal sealed class RoleCache : EntityCacheClient<Role, int>, IRoleCache
{
public RoleCache(IEntityCacheContext context) : base(context, Role.CacheKey)
{
}
protected override Task OnInitialized()
{
/*
* Register system or predefined roles. This roles cannot be changed. They differ
* from other roles in that they have a negative ids.
*/
/*
* Full control role. This role passed all authorization policies.
*/
Set(-1, new Role { Id = -1, Name = Roles.FullControl }, TimeSpan.Zero);
/*
* Implicit role assigned to every authenticated user.
*/
Set(-2, new Role { Id = -2, Name = Roles.Authenticated }, TimeSpan.Zero);
/*
* Implicit role assigned to non authenticated user.
*/
Set(-3, new Role { Id = -3, Name = Roles.Anonymous }, TimeSpan.Zero);
/*
* Implicit role assigned to every user
* regardless if it's authenticated or not.
*/
Set(-4, new Role { Id = -4, Name = Roles.Everyone }, TimeSpan.Zero);
return Task.CompletedTask;
}
}

@ -0,0 +1,183 @@
using System.Collections.Immutable;
using Connected;
using Connected.Entities;
using Connected.Entities.Storage;
using Connected.Notifications;
using Connected.Notifications.Events;
using Connected.Security.Identity;
using Connected.ServiceModel;
using Connected.Services;
using Connected.Validation;
namespace Common.Security.Identity;
internal sealed class QueryRoles : ServiceFunction<IDto, ImmutableList<IRole>>
{
public QueryRoles(IRoleCache cache)
{
Cache = cache;
}
private IRoleCache Cache { get; }
protected override async Task<ImmutableList<IRole>?> OnInvoke()
{
return await (from dc in Cache
select dc).AsEntities<IRole>();
}
}
internal sealed class SelectRole : ServiceFunction<PrimaryKeyArgs<int>, IRole?>
{
public SelectRole(IRoleCache cache)
{
Cache = cache;
}
private IRoleCache Cache { get; }
protected override async Task<IRole?> OnInvoke()
{
return await (from dc in Cache
where dc.Id == Arguments.Id
select dc).AsEntity();
}
}
internal sealed class SelectRoleByName : ServiceFunction<NameArgs, IRole?>
{
public SelectRoleByName(IRoleCache cache)
{
Cache = cache;
}
private IRoleCache Cache { get; }
protected override async Task<IRole?> OnInvoke()
{
return await (from dc in Cache
where string.Equals(dc.Name, Arguments.Name, StringComparison.OrdinalIgnoreCase)
select dc).AsEntity();
}
}
internal sealed class LookupRoles : ServiceFunction<PrimaryKeyListArgs<int>, ImmutableList<IRole>?>
{
public LookupRoles(IRoleCache cache)
{
Cache = cache;
}
private IRoleCache Cache { get; }
protected override async Task<ImmutableList<IRole>?> OnInvoke()
{
if (Arguments.IdList is null)
return default;
return await (from dc in Cache
where Arguments.IdList.Any(f => f == dc.Id)
select dc).AsEntities<IRole>();
}
}
internal sealed class InsertRole : ServiceFunction<RoleArgs, int>
{
public InsertRole(IRoleService roleService, IRoleCache cache, IStorageProvider storage, IEventService events)
{
RoleService = roleService;
Cache = cache;
Storage = storage;
Events = events;
}
private IStorageProvider Storage { get; }
private IEventService Events { get; }
private IRoleCache Cache { get; }
private IRoleService RoleService { get; }
private IRole? Entity { get; set; }
protected override async Task<int> OnInvoke()
{
if (await RoleService.Select(new NameArgs { Name = Arguments.Name }) is not null)
throw ValidationExceptions.ValueExists(nameof(Arguments.Name), Arguments.Name);
if (Arguments.AsEntity<Role>(State.New) is not Role entity)
throw EntityExceptions.EntityCastException(Arguments.GetType(), typeof(Role));
Entity = await Storage.Open<Role>().Update(entity);
return Entity.Id;
}
protected override async Task OnCommitted()
{
await Cache.Refresh(Entity.Id);
await Events.Enqueue(this, RoleService, nameof(IServiceNotifications<int>.Inserted), Result);
}
protected override async Task OnRolledBack()
{
await Cache.Refresh(Result);
}
}
internal sealed class DeleteRole : ServiceAction<PrimaryKeyArgs<int>>
{
public DeleteRole(IRoleService roleService, IStorageProvider storage, IRoleCache cache, IEventService events)
{
RoleService = roleService;
Storage = storage;
Cache = cache;
Events = events;
}
private IRoleService RoleService { get; }
private IStorageProvider Storage { get; }
private IRoleCache Cache { get; }
private IEventService Events { get; }
protected override async Task OnInvoke()
{
if (await RoleService.Select(Arguments.Id) is IRole existing && existing.Id < 1)
throw new AccessViolationException($"{SR.ErrSysRole} ({existing.Name})");
await Storage.Open<Role>().Update(new Role { Id = Arguments.Id, State = State.Deleted });
}
protected override async Task OnCommitted()
{
await Cache.Remove(Arguments.Id);
await Events.Enqueue(this, RoleService, ServiceEvents.Deleted, Arguments.Id);
}
}
internal sealed class UpdateRole : ServiceAction<RoleUpdateArgs>
{
public UpdateRole(IRoleService roleService, IStorageProvider storage, IRoleCache cache, IEventService events)
{
RoleService = roleService;
Storage = storage;
Cache = cache;
Events = events;
}
private IStorageProvider Storage { get; }
private IRoleCache Cache { get; }
private IEventService Events { get; }
private IRoleService RoleService { get; }
protected override async Task OnInvoke()
{
await Storage.Open<Role>().Update(await Load(), Arguments, async () =>
{
await Cache.Refresh(Arguments.Id);
return await Load();
});
await Cache.Refresh(Arguments.Id);
}
private async Task<Role> Load() => await (from dc in Cache where dc.Id == Arguments.Id select dc).AsEntity();
protected override async Task OnCommitted()
{
await Events.Enqueue(this, RoleService, ServiceEvents.Updated, Arguments.Id);
}
}

@ -0,0 +1,57 @@
using System.Collections.Immutable;
using Connected.Security;
using Connected.Security.Identity;
using Connected.ServiceModel;
using Connected.Services;
using Connected.Services.Annotations;
namespace Common.Security.Identity;
internal class RoleService : EntityService<int>, IRoleService
{
public RoleService(IContext context) : base(context)
{
}
[ServiceAuthorization(SecurityClaims.SecurityDelete)]
public async Task Delete(PrimaryKeyArgs<int> args)
{
await Invoke(GetOperation<DeleteRole>(), args);
}
[ServiceAuthorization(SecurityClaims.SecurityAdd)]
public async Task<int> Insert(RoleArgs args)
{
return await Invoke(GetOperation<InsertRole>(), args);
}
[ServiceAuthorization(SecurityClaims.SecurityRead)]
public async Task<ImmutableList<IRole>?> Query()
{
return await Invoke(GetOperation<QueryRoles>(), Dto.Empty);
}
[ServiceAuthorization(SecurityClaims.SecurityRead)]
public async Task<ImmutableList<IRole>?> Query(PrimaryKeyListArgs<int> args)
{
return await Invoke(GetOperation<LookupRoles>(), args);
}
[ServiceAuthorization(SecurityClaims.SecurityRead)]
public async Task<IRole?> Select(PrimaryKeyArgs<int> args)
{
return await Invoke(GetOperation<SelectRole>(), args);
}
[ServiceAuthorization(SecurityClaims.SecurityRead)]
public async Task<IRole?> Select(NameArgs args)
{
return await Invoke(GetOperation<SelectRoleByName>(), args);
}
[ServiceAuthorization(SecurityClaims.SecurityModify)]
public async Task Update(RoleUpdateArgs args)
{
await Invoke(GetOperation<UpdateRole>(), args);
}
}

@ -0,0 +1,62 @@
using Connected.Annotations;
using Connected.Entities.Annotations;
using Connected.Entities.Consistency;
using Connected.Security.Identity;
namespace Common.Security.Identity;
[Table(Schema = SchemaAttribute.SysSchema)]
internal record class User : ConsistentEntity<int>, IUser, IUserPassport
{
public const string CacheKey = $"{SchemaAttribute.SysSchema}.{nameof(User)}";
[Length(32)]
[Ordinal(0)]
[Nullable]
public string? FirstName { get; init; }
[Length(64)]
[Ordinal(1)]
[Nullable]
public string? LastName { get; init; }
[Length(128)]
[Ordinal(2)]
[Nullable]
public string? LoginName { get; init; }
[Length(256)]
[Ordinal(3)]
[Nullable]
public string? Email { get; init; }
[Length(256)]
[Ordinal(4)]
[Nullable]
public string? TimeZone { get; init; }
[Ordinal(5)]
[Nullable]
public int Language { get; init; }
[Ordinal(6)]
[Nullable]
[Length(256)]
public byte[] Password { get; init; }
[Ordinal(7)]
[Nullable]
[Length(32)]
public byte[] Pin { get; init; }
[Ordinal(8)]
public UserStatus Status { get; init; }
[Nullable]
[Ordinal(9)]
public DateTime PasswordExpiration { get; init; }
[Nullable]
[Ordinal(10)]
public Guid AuthenticationToken { get; init; }
}

@ -0,0 +1,11 @@
using Connected.Entities.Caching;
namespace Common.Security.Identity;
internal interface IUserCache : IEntityCacheClient<User, int> { }
internal sealed class UserCache : EntityCacheClient<User, int>, IUserCache
{
public UserCache(IEntityCacheContext context) : base(context, User.CacheKey)
{
}
}

@ -0,0 +1,237 @@
using System.Collections.Immutable;
using Connected;
using Connected.Entities;
using Connected.Entities.Storage;
using Connected.Notifications.Events;
using Connected.Security.Cryptography;
using Connected.Security.Identity;
using Connected.ServiceModel;
using Connected.Services;
namespace Common.Security.Identity;
internal sealed class QueryUsers : ServiceFunction<IDto, ImmutableList<IUser>?>
{
public QueryUsers(IUserCache cache)
{
Cache = cache;
}
private IUserCache Cache { get; }
protected override async Task<ImmutableList<IUser>?> OnInvoke()
{
return await (from dc in Cache select dc).AsEntities<IUser>();
}
}
internal sealed class SelectUser : ServiceFunction<PrimaryKeyArgs<int>, IUser?>
{
public SelectUser(IUserCache cache)
{
Cache = cache;
}
private IUserCache Cache { get; }
protected override async Task<IUser?> OnInvoke()
{
return await (from dc in Cache
where dc.Id == Arguments.Id
select dc).AsEntity();
}
}
/// <summary>
/// Resolves user by a specified criteria string
/// </summary>
internal sealed class ResolveUser : ServiceFunction<UserResolveArgs, IUser?>
{
public ResolveUser(IUserCache cache)
{
Cache = cache;
}
private IUserCache Cache { get; }
protected override async Task<IUser?> OnInvoke()
{
/*
* First, try to resolve by login name
*/
if (await (from dc in Cache where string.Equals(dc.LoginName, Arguments.Criteria, StringComparison.OrdinalIgnoreCase) select dc).AsEntity() is IUser user)
return user;
/*
* Next, try by authentication token
*/
if (Guid.TryParse(Arguments.Criteria, out Guid authenticationToken))
{
if (await (from dc in Cache where dc.AuthenticationToken == authenticationToken select dc).AsEntity() is IUser authUser)
return authUser;
}
/*
* Next, try by email
*/
if (Arguments.Criteria?.Contains('@') == true)
{
if (await (from dc in Cache where string.Equals(dc.Email, Arguments.Criteria, StringComparison.OrdinalIgnoreCase) select dc).AsEntity() is IUser emailUser)
return emailUser;
}
/*
* Now by id
*/
if (int.TryParse(Arguments.Criteria, out int id))
{
if (await (from dc in Cache where dc.Id == id select dc).AsEntity() is IUser idUser)
return idUser;
}
/*
* Doesn't exist.
*/
return null;
}
}
internal sealed class LookupUsers : ServiceFunction<PrimaryKeyListArgs<int>, ImmutableList<IUser>?>
{
public LookupUsers(IUserCache cache)
{
Cache = cache;
}
private IUserCache Cache { get; }
protected override async Task<ImmutableList<IUser>?> OnInvoke()
{
if (Arguments.IdList is null)
return default;
return await (from dc in Cache
where Arguments.IdList.Contains(dc.Id)
select dc).AsEntities<IUser>();
}
}
internal sealed class InsertUser : ServiceFunction<UserInsertArgs, int>
{
public InsertUser(IUserService userService, IUserCache cache, IStorageProvider storage, IEventService events)
{
UserService = userService;
Cache = cache;
Storage = storage;
Events = events;
}
private IUserCache Cache { get; }
private IStorageProvider Storage { get; }
private IEventService Events { get; }
private IUserService UserService { get; }
protected override async Task<int> OnInvoke()
{
if (Arguments.AsEntity<User>(State.New) is not User entity)
throw EntityExceptions.EntityCastException(Arguments.GetType(), typeof(User));
var result = await Storage.Open<User>().Update(entity);
return result.Id;
}
protected override async Task OnCommitted()
{
await Cache.Refresh(Result);
await Events.Enqueue(this, UserService, ServiceEvents.Inserted, Result);
}
}
internal sealed class DeleteUser : ServiceAction<PrimaryKeyArgs<int>>
{
public DeleteUser(IUserService userService, IStorageProvider storage, IUserCache cache, IEventService events)
{
UserService = userService;
Storage = storage;
Cache = cache;
Events = events;
}
private IUserService UserService { get; }
private IStorageProvider Storage { get; }
private IUserCache Cache { get; }
private IEventService Events { get; }
protected override async Task OnInvoke()
{
await Storage.Open<User>().Update(new User { Id = Arguments.Id, State = State.Deleted });
}
protected override async Task OnCommitted()
{
await Cache.Remove(Arguments.Id);
await Events.Enqueue(this, UserService, ServiceEvents.Deleted, Arguments.Id);
}
}
internal sealed class UpdateUser : ServiceAction<UserUpdateArgs>
{
public UpdateUser(IUserService userService, IUserCache cache, IStorageProvider storage, IEventService events)
{
UserService = userService;
Cache = cache;
Storage = storage;
Events = events;
}
private IUserService UserService { get; }
private IUserCache Cache { get; }
private IStorageProvider Storage { get; }
private IEventService Events { get; }
protected override async Task OnInvoke()
{
await Storage.Open<User>().Update(await Load(), Arguments, async () =>
{
await Cache.Refresh(Arguments.Id);
return await Load();
});
}
private async Task<User?> Load() => await (from dc in Cache where dc.Id == Arguments.Id select dc).AsEntity();
protected override async Task OnCommitted()
{
await Cache.Refresh(Arguments.Id);
await Events.Enqueue(this, UserService, ServiceEvents.Updated, Arguments.Id);
}
}
internal sealed class UserUpdatePassword : ServiceAction<UserPasswordArgs>
{
public UserUpdatePassword(IUserService userService, ICryptographyService cryptographyService, IStorageProvider storage, IUserCache cache, IEventService events)
{
UserService = userService;
CryptographyService = cryptographyService;
Storage = storage;
Cache = cache;
Events = events;
}
private IUserService UserService { get; }
private ICryptographyService CryptographyService { get; }
private IStorageProvider Storage { get; }
public IUserCache Cache { get; }
public IEventService Events { get; }
protected override async Task OnInvoke()
{
await Storage.Open<User>().Update(await Load(), Arguments, async () =>
{
await Cache.Refresh(Arguments.Id);
return await Load();
});
}
private async Task<User?> Load() => await (from dc in Cache where dc.Id == Arguments.Id select dc).AsEntity();
protected override async Task OnCommitted()
{
await Cache.Refresh(Arguments.Id);
await Events.Enqueue(this, UserService, ServiceEvents.Updated, Arguments.Id);
}
}

@ -0,0 +1,63 @@
using System.Collections.Immutable;
using Connected.Security;
using Connected.Security.Identity;
using Connected.ServiceModel;
using Connected.Services;
using Connected.Services.Annotations;
namespace Common.Security.Identity;
internal class UserService : EntityService<int>, IUserService
{
public UserService(IContext context) : base(context)
{
}
[ServiceAuthorization(SecurityClaims.SecurityDelete)]
public async Task Delete(PrimaryKeyArgs<int> args)
{
await Invoke(GetOperation<DeleteUser>(), args);
}
[ServiceAuthorization(SecurityClaims.SecurityAdd)]
public async Task<int> Insert(UserInsertArgs args)
{
return await Invoke(GetOperation<InsertUser>(), args);
}
[ServiceAuthorization(SecurityClaims.SecurityRead)]
public async Task<ImmutableList<IUser>?> Query()
{
return await Invoke(GetOperation<QueryUsers>(), Dto.Empty);
}
[ServiceAuthorization(SecurityClaims.SecurityRead)]
public async Task<ImmutableList<IUser>?> Query(PrimaryKeyListArgs<int> args)
{
return await Invoke(GetOperation<LookupUsers>(), args);
}
[ServiceAuthorization(SecurityClaims.SecurityRead)]
public async Task<IUser?> Resolve(UserResolveArgs args)
{
return await Invoke(GetOperation<ResolveUser>(), args);
}
[ServiceAuthorization(SecurityClaims.SecurityRead)]
public async Task<IUser?> Select(PrimaryKeyArgs<int> args)
{
return await Invoke(GetOperation<SelectUser>(), args);
}
[ServiceAuthorization(SecurityClaims.SecurityModify, SecurityClaims.SecurityModifySelf)]
public async Task Update(UserUpdateArgs args)
{
await Invoke(GetOperation<UpdateUser>(), args);
}
[ServiceAuthorization(SecurityClaims.SecurityModify, SecurityClaims.SecurityModifySelf)]
public async Task UpdatePassword(UserPasswordArgs args)
{
await Invoke(GetOperation<UserUpdatePassword>(), args);
}
}

@ -0,0 +1,20 @@
using Connected.Annotations;
using Connected.Entities;
using Connected.Entities.Annotations;
using Connected.Security.Membership;
namespace Common.Security.Membership;
[Table(Schema = SchemaAttribute.SysSchema)]
internal sealed record Membership : Entity<int>, IMembership
{
public const string CacheKey = $"{SchemaAttribute.SysSchema}.{nameof(Membership)}";
[Ordinal(0)]
[Index(Name = $"idx_{SchemaAttribute.SysSchema}_{nameof(User)}_{nameof(Role)}", Unique = true)]
public int User { get; init; }
[Ordinal(1)]
[Index(Name = $"idx_{SchemaAttribute.SysSchema}_{nameof(User)}_{nameof(Role)}", Unique = true)]
public int Role { get; init; }
}

@ -0,0 +1,11 @@
using Connected.Entities.Caching;
namespace Common.Security.Membership;
internal interface IMembershipCache : IEntityCacheClient<Membership, int> { }
internal class MembershipCache : EntityCacheClient<Membership, int>, IMembershipCache
{
public MembershipCache(IEntityCacheContext context) : base(context, Membership.CacheKey)
{
}
}

@ -0,0 +1,116 @@
using System.Collections.Immutable;
using Connected;
using Connected.Entities;
using Connected.Entities.Storage;
using Connected.Notifications.Events;
using Connected.Security.Membership;
using Connected.ServiceModel;
using Connected.Services;
namespace Common.Security.Membership;
internal sealed class DeleteMembership : ServiceAction<PrimaryKeyArgs<int>>
{
public DeleteMembership(IMembershipService membershipService, IStorageProvider storage, IMembershipCache cache, IEventService events)
{
MembershipService = membershipService;
Storage = storage;
Cache = cache;
Events = events;
}
private IMembershipService MembershipService { get; }
private IStorageProvider Storage { get; }
private IMembershipCache Cache { get; }
private IEventService Events { get; }
protected override async Task OnInvoke()
{
await Storage.Open<Membership>().Update(new Membership { Id = Arguments.Id, State = State.Deleted });
}
protected override async Task OnCommitted()
{
await Cache.Refresh(Arguments.Id);
await Events.Enqueue(this, MembershipService, ServiceEvents.Deleted, Arguments.Id);
}
}
internal sealed class QueryMembership : ServiceFunction<IDto, ImmutableList<IMembership>>
{
public QueryMembership(IMembershipCache membershipCache)
{
MembershipCache = membershipCache;
}
private IMembershipCache MembershipCache { get; }
protected override async Task<ImmutableList<IMembership>?> OnInvoke()
{
return await (from dc in MembershipCache
select dc).AsEntities<IMembership>();
}
}
internal sealed class SearchMembership : ServiceFunction<MembershipQueryArgs, ImmutableList<IMembership>?>
{
public SearchMembership(IMembershipCache membershipCache)
{
MembershipCache = membershipCache;
}
private IMembershipCache MembershipCache { get; }
protected override async Task<ImmutableList<IMembership>?> OnInvoke()
{
return await (from dc in MembershipCache
where (Arguments.User == 0 || (Arguments.User == dc.User))
&& (Arguments.Role == 0 || (Arguments.Role == dc.Role))
select dc).AsEntities<IMembership>();
}
}
internal sealed class SelectMembership : ServiceFunction<PrimaryKeyArgs<int>, IMembership?>
{
public SelectMembership(IMembershipCache membershipCache)
{
MembershipCache = membershipCache;
}
private IMembershipCache MembershipCache { get; }
protected override async Task<IMembership?> OnInvoke()
{
return await (from dc in MembershipCache
where dc.Id == Arguments.Id
select dc).AsEntity();
}
}
internal sealed class InsertMembership : ServiceFunction<MembershipArgs, int>
{
public InsertMembership(IMembershipService membershipService, IMembershipCache cache, IStorageProvider storage, IEventService events)
{
MembershipService = membershipService;
Cache = cache;
Storage = storage;
Events = events;
}
private IMembershipService MembershipService { get; }
private IMembershipCache Cache { get; }
private IStorageProvider Storage { get; }
private IEventService Events { get; }
protected override async Task<int> OnInvoke()
{
var entity = Arguments.AsEntity<Membership>(State.New);
var result = await Storage.Open<Membership>().Update(entity);
return result.Id;
}
protected override async Task OnCommitted()
{
await Cache.Refresh(Result);
await Events.Enqueue(this, MembershipService, ServiceEvents.Inserted, Result);
}
}

@ -0,0 +1,45 @@
using System.Collections.Immutable;
using Connected.Security;
using Connected.Security.Membership;
using Connected.ServiceModel;
using Connected.Services;
using Connected.Services.Annotations;
namespace Common.Security.Membership;
internal class MembershipService : EntityService<int>, IMembershipService
{
public MembershipService(IContext context) : base(context)
{
}
[ServiceAuthorization(SecurityClaims.SecurityDelete)]
public async Task Delete(PrimaryKeyArgs<int> args)
{
await Invoke(GetOperation<DeleteMembership>(), args);
}
[ServiceAuthorization(SecurityClaims.SecurityRead)]
public async Task<int> Insert(MembershipArgs args)
{
return await Invoke(GetOperation<InsertMembership>(), args);
}
[ServiceAuthorization(SecurityClaims.SecurityRead)]
public async Task<ImmutableList<IMembership>?> Query()
{
return await Invoke(GetOperation<QueryMembership>(), Dto.Empty);
}
[ServiceAuthorization(SecurityClaims.SecurityRead)]
public async Task<ImmutableList<IMembership>?> Query(MembershipQueryArgs args)
{
return await Invoke(GetOperation<SearchMembership>(), args);
}
[ServiceAuthorization(SecurityClaims.SecurityRead)]
public async Task<IMembership?> Select(PrimaryKeyArgs<int> args)
{
return await Invoke(GetOperation<SelectMembership>(), args);
}
}

@ -0,0 +1,36 @@
using Connected.Annotations;
using Connected.Entities.Annotations;
using Connected.Entities.Consistency;
using Connected.Security.Permissions;
namespace Common.Security.Permissions;
[Table(Schema = SchemaAttribute.SysSchema)]
internal sealed record Permission : ConsistentEntity<int>, IPermission
{
public const string CacheKey = $"{SchemaAttribute.SysSchema}.{nameof(Permission)}";
[Ordinal(0), Length(32)]
public string Evidence { get; init; } = default!;
[Ordinal(1), Length(32)]
public string Schema { get; init; } = default!;
[Ordinal(2), Length(32)]
public string Claim { get; init; } = default!;
[Ordinal(3), Length(256), Nullable]
public string? PrimaryKey { get; init; }
[Ordinal(4), Length(256), Nullable]
public string? Entity { get; init; }
[Ordinal(5)]
public PermissionValue Value { get; init; }
[Ordinal(6), Length(256), Nullable]
public string? Component { get; init; }
[Ordinal(7), Length(256), Nullable]
public string? Method { get; init; }
}

@ -0,0 +1,11 @@
using Connected.Entities.Caching;
namespace Common.Security.Permissions;
internal interface IPermissionCache : IEntityCacheClient<Permission, int> { }
internal class PermissionCache : EntityCacheClient<Permission, int>, IPermissionCache
{
public PermissionCache(IEntityCacheContext context) : base(context, Permission.CacheKey)
{
}
}

@ -0,0 +1,150 @@
using System.Collections.Immutable;
using Connected;
using Connected.Entities;
using Connected.Entities.Storage;
using Connected.Notifications.Events;
using Connected.Security.Permissions;
using Connected.ServiceModel;
using Connected.Services;
namespace Common.Security.Permissions;
internal sealed class DeletePermission : ServiceAction<PrimaryKeyArgs<int>>
{
public DeletePermission(IPermissionService permissionService, IStorageProvider storage, IPermissionCache cache, IEventService events)
{
PermissionService = permissionService;
Storage = storage;
Cache = cache;
Events = events;
}
private IPermissionService PermissionService { get; }
private IStorageProvider Storage { get; }
private IPermissionCache Cache { get; }
private IEventService Events { get; }
protected override async Task OnInvoke()
{
await Storage.Open<Permission>().Update(new Permission { Id = Arguments.Id, State = State.Deleted });
}
protected override async Task OnCommitted()
{
await Cache.Refresh(Arguments.Id);
await Events.Enqueue(this, PermissionService, ServiceEvents.Deleted, Arguments.Id);
}
}
internal sealed class QueryPermissions : ServiceFunction<IDto, ImmutableList<IPermission>?>
{
public QueryPermissions(IPermissionCache permissionCache)
{
PermissionCache = permissionCache;
}
private IPermissionCache PermissionCache { get; }
protected override async Task<ImmutableList<IPermission>?> OnInvoke()
{
return await (from dc in PermissionCache
select dc).AsEntities<IPermission>();
}
}
internal sealed class SearchPermissions : ServiceFunction<PermissionSearchArgs, ImmutableList<IPermission>?>
{
public SearchPermissions(IPermissionCache permissionCache)
{
PermissionCache = permissionCache;
}
private IPermissionCache PermissionCache { get; }
protected override async Task<ImmutableList<IPermission>?> OnInvoke()
{
return await (from dc in PermissionCache
where (string.IsNullOrEmpty(Arguments.Entity) || string.Equals(Arguments.Entity, dc.Entity, StringComparison.OrdinalIgnoreCase))
&& (string.IsNullOrEmpty(Arguments.Claim) || string.Equals(Arguments.Claim, dc.Claim, StringComparison.OrdinalIgnoreCase))
&& (string.IsNullOrEmpty(Arguments.PrimaryKey) || string.Equals(Arguments.PrimaryKey, dc.PrimaryKey, StringComparison.OrdinalIgnoreCase))
select dc).AsEntities<IPermission>();
}
}
internal sealed class SelectPermission : ServiceFunction<PrimaryKeyArgs<int>, IPermission?>
{
public SelectPermission(IPermissionCache permissionCache)
{
PermissionCache = permissionCache;
}
private IPermissionCache PermissionCache { get; }
protected override async Task<IPermission?> OnInvoke()
{
return await (from dc in PermissionCache
where dc.Id == Arguments.Id
select dc).AsEntity();
}
}
internal sealed class InsertPermission : ServiceFunction<PermissionArgs, int>
{
public InsertPermission(IPermissionService permissionService, IStorageProvider storage, IEventService events, IPermissionCache cache)
{
PermissionService = permissionService;
Storage = storage;
Events = events;
Cache = cache;
}
private IPermissionService PermissionService { get; }
private IStorageProvider Storage { get; }
private IEventService Events { get; }
private IPermissionCache Cache { get; }
protected override async Task<int> OnInvoke()
{
var entity = Arguments.AsEntity<Permission>(State.New);
var result = await Storage.Open<Permission>().Update(entity);
return result.Id;
}
protected override async Task OnCommitted()
{
await Cache.Refresh(Result);
await Events.Enqueue(this, PermissionService, ServiceEvents.Inserted, Result);
}
}
internal sealed class UpdatePermission : ServiceAction<PermissionUpdateArgs>
{
public UpdatePermission(IPermissionService permissionService, IStorageProvider storage, IPermissionCache cache, IEventService events)
{
PermissionService = permissionService;
Storage = storage;
Cache = cache;
Events = events;
}
private IPermissionService PermissionService { get; }
private IStorageProvider Storage { get; }
private IPermissionCache Cache { get; }
private IEventService Events { get; }
protected override async Task OnInvoke()
{
await Storage.Open<Permission>().Update(await Load(), Arguments, async () =>
{
await Cache.Refresh(Arguments.Id);
return await Load();
});
}
private async Task<Permission?> Load() => await (from dc in Cache where dc.Id == Arguments.Id select dc).AsEntity();
protected override async Task OnCommitted()
{
await Cache.Refresh(Arguments.Id);
await Events.Enqueue(this, PermissionService, ServiceEvents.Updated, Arguments.Id);
}
}

@ -0,0 +1,51 @@
using System.Collections.Immutable;
using Connected.Security;
using Connected.Security.Permissions;
using Connected.ServiceModel;
using Connected.Services;
using Connected.Services.Annotations;
namespace Common.Security.Permissions;
internal class PermissionService : EntityService<int>, IPermissionService
{
public PermissionService(IContext context) : base(context)
{
}
[ServiceAuthorization(SecurityClaims.SecurityDelete)]
public async Task Delete(PrimaryKeyArgs<int> args)
{
await Invoke(GetOperation<DeletePermission>(), args);
}
[ServiceAuthorization(SecurityClaims.SecurityAdd)]
public async Task<int> Insert(PermissionArgs args)
{
return await Invoke(GetOperation<InsertPermission>(), args);
}
[ServiceAuthorization(SecurityClaims.SecurityRead, Stage = AuthorizationStage.Result)]
public async Task<ImmutableList<IPermission>?> Query()
{
return await Invoke(GetOperation<QueryPermissions>(), Dto.Empty);
}
[ServiceAuthorization(SecurityClaims.SecurityRead, Stage = AuthorizationStage.Result)]
public async Task<ImmutableList<IPermission>?> Query(PermissionSearchArgs args)
{
return await Invoke(GetOperation<SearchPermissions>(), args);
}
[ServiceAuthorization(SecurityClaims.SecurityRead, Stage = AuthorizationStage.Result)]
public async Task<IPermission?> Select(PrimaryKeyArgs<int> args)
{
return await Invoke(GetOperation<SelectPermission>(), args);
}
[ServiceAuthorization(SecurityClaims.SecurityModify)]
public async Task Update(PermissionUpdateArgs args)
{
await Invoke(GetOperation<UpdatePermission>(), args);
}
}

@ -0,0 +1,37 @@
using System.Collections.Immutable;
using Connected.Configuration.Settings;
using Connected.ServiceModel;
using Connected.Services;
namespace Common.Settings;
internal class SettingsService : EntityService<int>, ISettingsService
{
public SettingsService(IContext context) : base(context)
{
}
public Task Delete(PrimaryKeyArgs<int> args)
{
throw new NotImplementedException();
}
public Task<ImmutableList<ISetting>> Query()
{
throw new NotImplementedException();
}
public Task<ISetting?> Select(PrimaryKeyArgs<int> args)
{
throw new NotImplementedException();
}
public Task<ISetting?> Select(NameArgs args)
{
throw new NotImplementedException();
}
public Task Update(SettingsArgs args)
{
throw new NotImplementedException();
}
}
Loading…
Cancel
Save