add necessary modules to new zero
This commit is contained in:
@@ -1,75 +0,0 @@
|
||||
//using System;
|
||||
//using System.Threading.Tasks;
|
||||
//using zero.Core.Collections;
|
||||
//using zero.Core.Entities;
|
||||
//using zero.Core.Extensions;
|
||||
//using Rc = Raven.Client;
|
||||
|
||||
//namespace zero.Core.Api
|
||||
//{
|
||||
// public class PreviewApi : BackofficeApi, IPreviewApi
|
||||
// {
|
||||
// Preview Blueprint;
|
||||
|
||||
// public PreviewApi(IStoreContext store, Preview blueprint) : base(store)
|
||||
// {
|
||||
// Blueprint = blueprint;
|
||||
// }
|
||||
|
||||
|
||||
// /// <inheritdoc />
|
||||
// public async Task<EntityResult<Preview>> Add<TEntity>(TEntity model) where TEntity : ZeroEntity
|
||||
// {
|
||||
// return await Update(null, model);
|
||||
// }
|
||||
|
||||
|
||||
// /// <inheritdoc />
|
||||
// public async Task<EntityResult<Preview>> Update<TEntity>(string id, TEntity model) where TEntity : ZeroEntity
|
||||
// {
|
||||
// Preview entity = id == null ? await GetById<Preview>(id) : Blueprint.Clone();
|
||||
// entity.Content = model;
|
||||
// entity.OriginalId = model.Id;
|
||||
// entity.Name = model.Name;
|
||||
|
||||
// return await SaveModel(entity, meta: x =>
|
||||
// {
|
||||
// x[Rc.Constants.Documents.Metadata.Expires] = GetExpiry(model);
|
||||
// });
|
||||
// }
|
||||
|
||||
|
||||
// /// <inheritdoc />
|
||||
// public async Task<Preview> GetById(string id)
|
||||
// {
|
||||
// return await GetById<Preview>(id);
|
||||
// }
|
||||
|
||||
|
||||
// /// <summary>
|
||||
// /// Get preview expiration for a document
|
||||
// /// </summary>
|
||||
// DateTime GetExpiry(ZeroEntity model)
|
||||
// {
|
||||
// return DateTime.Now.AddHours(1);
|
||||
// }
|
||||
// }
|
||||
|
||||
// public interface IPreviewApi
|
||||
// {
|
||||
// /// <summary>
|
||||
// /// Adds an entity to the preview collection. This will generate a preview with an id which can be used for the preview view.
|
||||
// /// </summary>
|
||||
// Task<EntityResult<Preview>> Add<TEntity>(TEntity model) where TEntity : ZeroEntity;
|
||||
|
||||
// /// <summary>
|
||||
// /// Updates an entity in the preview collection. This will generate a preview with an id which can be used for the preview view.
|
||||
// /// </summary>
|
||||
// Task<EntityResult<Preview>> Update<TEntity>(string id, TEntity model) where TEntity : ZeroEntity;
|
||||
|
||||
// /// <summary>
|
||||
// /// Get preview entity by Id
|
||||
// /// </summary>
|
||||
// Task<Preview> GetById(string id);
|
||||
// }
|
||||
//}
|
||||
@@ -13,6 +13,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "plugins", "plugins", "{6FAA
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zero.Core", "zero.Core\zero.Core.csproj", "{CED2A7FA-8D35-4A2A-AF57-8B95307547CB}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "zero", "zero\zero.csproj", "{33CD6E46-CD81-42C3-9019-C0EAD557EE99}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "legacy", "legacy", "{93A1CCEB-A323-445E-8116-4575C5939B46}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -35,6 +39,10 @@ Global
|
||||
{CED2A7FA-8D35-4A2A-AF57-8B95307547CB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CED2A7FA-8D35-4A2A-AF57-8B95307547CB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CED2A7FA-8D35-4A2A-AF57-8B95307547CB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{33CD6E46-CD81-42C3-9019-C0EAD557EE99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{33CD6E46-CD81-42C3-9019-C0EAD557EE99}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{33CD6E46-CD81-42C3-9019-C0EAD557EE99}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{33CD6E46-CD81-42C3-9019-C0EAD557EE99}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -43,6 +51,7 @@ Global
|
||||
{63A8AD15-15F0-4555-BD62-C38B4465FFC1} = {6FAA92A8-CA1A-48EA-9857-C9092971B4D1}
|
||||
{C23CF90A-DB90-427F-816C-0E2FE20E29D0} = {6FAA92A8-CA1A-48EA-9857-C9092971B4D1}
|
||||
{A8E5F34F-5400-4F92-96C6-7F91645BC220} = {6FAA92A8-CA1A-48EA-9857-C9092971B4D1}
|
||||
{CED2A7FA-8D35-4A2A-AF57-8B95307547CB} = {93A1CCEB-A323-445E-8116-4575C5939B46}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {AD3F31B9-8BBB-4334-9571-F556B9E632C9}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
|
||||
namespace zero;
|
||||
|
||||
public static class ApplicationBuilderExtensions
|
||||
{
|
||||
public static IZeroApplicationBuilder UseZero(this IApplicationBuilder app) => new ZeroApplicationBuilder(app);
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyModel;
|
||||
using System.Reflection;
|
||||
|
||||
namespace zero.Architecture;
|
||||
|
||||
public class AssemblyDiscovery : IAssemblyDiscovery
|
||||
{
|
||||
public static IAssemblyDiscovery Current;
|
||||
|
||||
AssemblyDiscoveryContext Context;
|
||||
|
||||
IMvcBuilder Mvc;
|
||||
|
||||
|
||||
|
||||
public AssemblyDiscovery(IMvcBuilder mvc)
|
||||
{
|
||||
Mvc = mvc;
|
||||
Context = new AssemblyDiscoveryContext();
|
||||
Current = this;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Execute(IEnumerable<IAssemblyDiscoveryRule> rules)
|
||||
{
|
||||
List<Assembly> assemblies = new List<Assembly>();
|
||||
DependencyContext dependencyContext = DependencyContext.Load(Context.EntryAssembly);
|
||||
|
||||
if (dependencyContext == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string[] existingLibs = Mvc.PartManager.ApplicationParts.OfType<AssemblyPart>().Select(p => p.Name).ToArray();
|
||||
|
||||
IEnumerable<RuntimeLibrary> libraries = dependencyContext.RuntimeLibraries.Where(lib => !existingLibs.Contains(lib.Name, StringComparer.OrdinalIgnoreCase));
|
||||
|
||||
foreach (RuntimeLibrary library in libraries)
|
||||
{
|
||||
if (rules.Any(rule => rule.IsValid(library, Context)))
|
||||
{
|
||||
IEnumerable<Assembly> libraryAssemblies = library.GetDefaultAssemblyNames(dependencyContext).Select(name => Assembly.Load(name));
|
||||
|
||||
foreach (Assembly assembly in libraryAssemblies)
|
||||
{
|
||||
Mvc.AddApplicationPart(assembly);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public void AddAssembly(Assembly assembly)
|
||||
{
|
||||
string[] existingLibs = Mvc.PartManager.ApplicationParts.OfType<AssemblyPart>().Select(p => p.Name).ToArray();
|
||||
|
||||
if (!existingLibs.Contains(assembly.GetName().Name))
|
||||
{
|
||||
Mvc.AddApplicationPart(assembly);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<TypeInfo> GetTypes<TService>(bool allowGenerics = false) => GetTypes(typeof(TService), allowGenerics);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<TypeInfo> GetTypes<TService>(IEnumerable<ApplicationPart> parts, bool allowGenerics = false) => GetTypes(typeof(TService), parts, allowGenerics);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<TypeInfo> GetTypes(Type serviceType, bool allowGenerics = false) => GetAllClassTypes(allowGenerics).Where(t => serviceType.GetTypeInfo().IsAssignableFrom(t) && t.AsType() != serviceType);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<TypeInfo> GetTypes(Type serviceType, IEnumerable<ApplicationPart> parts, bool allowGenerics = false) => GetAllClassTypes(parts, allowGenerics).Where(t => serviceType.GetTypeInfo().IsAssignableFrom(t) && t.AsType() != serviceType);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<TypeInfo> GetAllClassTypes(bool allowGenerics = false) => GetAllTypes().Where(t => t.IsClass && !t.IsAbstract && (allowGenerics || !t.ContainsGenericParameters));
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<TypeInfo> GetAllClassTypes(IEnumerable<ApplicationPart> parts, bool allowGenerics = false) => GetAllTypes(parts).Where(t => t.IsClass && !t.IsAbstract && (allowGenerics || !t.ContainsGenericParameters));
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<Assembly> GetAssemblies() => Mvc.PartManager.ApplicationParts.OfType<AssemblyPart>().Select(p => p.Assembly);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<Assembly> GetAssemblies(IEnumerable<ApplicationPart> parts) => parts.OfType<AssemblyPart>().Select(p => p.Assembly);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<TypeInfo> GetAllTypes() => Mvc.PartManager.ApplicationParts.OfType<AssemblyPart>().SelectMany(p => p.Types);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<TypeInfo> GetAllTypes(IEnumerable<ApplicationPart> parts) => parts.OfType<AssemblyPart>().SelectMany(p => p.Types);
|
||||
}
|
||||
|
||||
|
||||
public interface IAssemblyDiscovery
|
||||
{
|
||||
/// <summary>
|
||||
/// Discovers runtime assemblies based on the given rules
|
||||
/// </summary>
|
||||
void Execute(IEnumerable<IAssemblyDiscoveryRule> rules);
|
||||
|
||||
/// <summary>
|
||||
/// Manually add an assembly
|
||||
/// </summary>
|
||||
void AddAssembly(Assembly assembly);
|
||||
|
||||
/// <summary>
|
||||
/// Get all discovered types which implement a certain service
|
||||
/// </summary>
|
||||
IEnumerable<TypeInfo> GetTypes<TService>(bool allowGenerics = false);
|
||||
|
||||
/// <summary>
|
||||
/// Get all discovered types which implement a certain service
|
||||
/// </summary>
|
||||
IEnumerable<TypeInfo> GetTypes<TService>(IEnumerable<ApplicationPart> parts, bool allowGenerics = false);
|
||||
|
||||
/// <summary>
|
||||
/// Get all discovered types which implement a certain service
|
||||
/// </summary>
|
||||
IEnumerable<TypeInfo> GetTypes(Type serviceType, bool allowGenerics = false);
|
||||
|
||||
/// <summary>
|
||||
/// Get all discovered types which implement a certain service
|
||||
/// </summary>
|
||||
IEnumerable<TypeInfo> GetTypes(Type serviceType, IEnumerable<ApplicationPart> parts, bool allowGenerics = false);
|
||||
|
||||
/// <summary>
|
||||
/// Get all registered types
|
||||
/// </summary>
|
||||
IEnumerable<TypeInfo> GetAllTypes();
|
||||
|
||||
/// <summary>
|
||||
/// Get all registered types
|
||||
/// </summary>
|
||||
IEnumerable<TypeInfo> GetAllTypes(IEnumerable<ApplicationPart> parts);
|
||||
|
||||
/// <summary>
|
||||
/// Get all registered types
|
||||
/// </summary>
|
||||
IEnumerable<TypeInfo> GetAllClassTypes(bool allowGenerics = false);
|
||||
|
||||
/// <summary>
|
||||
/// Get all registered types
|
||||
/// </summary>
|
||||
IEnumerable<TypeInfo> GetAllClassTypes(IEnumerable<ApplicationPart> parts, bool allowGenerics = false);
|
||||
|
||||
/// <summary>
|
||||
/// Get all registered assemblies
|
||||
/// </summary>
|
||||
IEnumerable<Assembly> GetAssemblies();
|
||||
|
||||
/// <summary>
|
||||
/// Get all registered assemblies
|
||||
/// </summary>
|
||||
IEnumerable<Assembly> GetAssemblies(IEnumerable<ApplicationPart> parts);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System.Reflection;
|
||||
|
||||
namespace zero.Architecture;
|
||||
|
||||
public class AssemblyDiscoveryContext
|
||||
{
|
||||
public Assembly EntryAssembly { get; private set; }
|
||||
|
||||
public string EntryAssemblyName { get; private set; }
|
||||
|
||||
public bool HasEntryAssembly => EntryAssembly != null;
|
||||
|
||||
|
||||
public AssemblyDiscoveryContext()
|
||||
{
|
||||
EntryAssembly = Assembly.GetEntryAssembly();
|
||||
EntryAssemblyName = EntryAssembly?.GetName().Name;
|
||||
}
|
||||
|
||||
|
||||
public AssemblyDiscoveryContext(Assembly entryAssembly)
|
||||
{
|
||||
EntryAssembly = entryAssembly;
|
||||
EntryAssemblyName = entryAssembly.GetName().Name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using Microsoft.Extensions.DependencyModel;
|
||||
|
||||
namespace zero.Architecture;
|
||||
|
||||
public interface IAssemblyDiscoveryRule
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns true if the specified runtime library should be added to
|
||||
/// the ApplicationPartManager; otherwise false.
|
||||
/// </summary>
|
||||
bool IsValid(RuntimeLibrary library, AssemblyDiscoveryContext context);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using Microsoft.Extensions.DependencyModel;
|
||||
|
||||
namespace zero.Architecture;
|
||||
|
||||
public class ZeroAssemblyDiscoveryRule : IAssemblyDiscoveryRule
|
||||
{
|
||||
const string ZERO_PREFIX = "zero.";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsValid(RuntimeLibrary library, AssemblyDiscoveryContext context)
|
||||
{
|
||||
StringComparison casing = StringComparison.OrdinalIgnoreCase;
|
||||
// TODO we need to auto-add assemblies and discover their types which have implementations of IZeroPlugin
|
||||
return library.Name.StartsWith(ZERO_PREFIX, casing) || (context.HasEntryAssembly && library.Name.Contains(context.EntryAssemblyName, casing));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace zero.Architecture;
|
||||
|
||||
public abstract class ZeroModule : IZeroModule
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public virtual int Order { get; } = 0;
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual int ConfigureOrder => Order;
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void ConfigureServices(IServiceCollection services, IConfiguration configuration) { }
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) { }
|
||||
}
|
||||
|
||||
|
||||
public interface IZeroModule
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the value to use to order startups to configure services. The default is 0.
|
||||
/// </summary>
|
||||
int Order { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get the value to use to order startups to build the pipeline. The default is the 'Order' property.
|
||||
/// </summary>
|
||||
int ConfigureOrder { get; }
|
||||
|
||||
/// <summary>
|
||||
/// This method gets called by the runtime. Use this method to add services to the container.
|
||||
/// For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940
|
||||
/// </summary>
|
||||
/// <param name="services">The collection of service descriptors.</param>
|
||||
void ConfigureServices(IServiceCollection services, IConfiguration configuration);
|
||||
|
||||
/// <summary>
|
||||
/// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||
/// </summary>
|
||||
/// <param name="builder"></param>
|
||||
/// <param name="routes"></param>
|
||||
/// <param name="serviceProvider"></param>
|
||||
void Configure(IApplicationBuilder builder, IEndpointRouteBuilder routes, IServiceProvider serviceProvider);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace zero.Architecture;
|
||||
|
||||
public class ZeroModuleCollection : ZeroModule
|
||||
{
|
||||
ConcurrentDictionary<Type, IZeroModule> _modules = new();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get all registered modules
|
||||
/// </summary>
|
||||
public IEnumerable<IZeroModule> GetAll() => _modules.Values;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Adds a zero module
|
||||
/// </summary>
|
||||
public void Add<T>() where T : class, IZeroModule, new()
|
||||
{
|
||||
Add(new T());
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Adds a zero module
|
||||
/// </summary>
|
||||
public void Add<T>(T moduleInstance) where T : IZeroModule
|
||||
{
|
||||
Add(typeof(T), moduleInstance);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Adds a zero module
|
||||
/// </summary>
|
||||
public void Add(Type moduleType, IZeroModule moduleInstance)
|
||||
{
|
||||
if (_modules.ContainsKey(moduleType))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_modules.TryAdd(moduleType, moduleInstance);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void ConfigureServices(IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
foreach (var module in _modules.OrderBy(x => x.Value.Order))
|
||||
{
|
||||
module.Value.ConfigureServices(services, configuration);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider)
|
||||
{
|
||||
foreach (var module in _modules.OrderBy(x => x.Value.ConfigureOrder))
|
||||
{
|
||||
module.Value.Configure(app, routes, serviceProvider);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace zero.Communication;
|
||||
|
||||
public interface IHandler
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace zero.Communication;
|
||||
|
||||
public class HandlerHolder : IHandlerHolder
|
||||
{
|
||||
IServiceProvider Services { get; }
|
||||
|
||||
public HandlerHolder(IServiceProvider services)
|
||||
{
|
||||
Services = services;
|
||||
}
|
||||
|
||||
|
||||
public T Get<T>() where T : IHandler
|
||||
{
|
||||
return Services.GetService<T>();
|
||||
}
|
||||
|
||||
|
||||
public IEnumerable<T> GetAll<T>() where T : IHandler
|
||||
{
|
||||
return Services.GetService<IEnumerable<T>>();
|
||||
}
|
||||
}
|
||||
|
||||
public interface IHandlerHolder
|
||||
{
|
||||
T Get<T>() where T : IHandler;
|
||||
|
||||
IEnumerable<T> GetAll<T>() where T : IHandler;
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
namespace zero.Communication;
|
||||
|
||||
|
||||
public abstract partial class Interceptor<T> : Interceptor, IInterceptor<T> where T : ZeroIdEntity
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Type ModelType { get; protected set; }
|
||||
|
||||
|
||||
public Interceptor()
|
||||
{
|
||||
Gravity = 0;
|
||||
ModelType = typeof(T);
|
||||
Name = ModelType.Name;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanHandle(InterceptorParameters args, Type modelType) => ModelType.IsAssignableFrom(modelType);
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Task<InterceptorResult<T>> Creating(InterceptorParameters args, T model) => Task.FromResult<InterceptorResult<T>>(default);
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed override async Task<InterceptorResult<ZeroIdEntity>> Creating(InterceptorParameters args, ZeroIdEntity model) => Convert(await Creating(args, model as T));
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Task<InterceptorResult<T>> Updating(InterceptorParameters args, T model) => Task.FromResult<InterceptorResult<T>>(default);
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed override async Task<InterceptorResult<ZeroIdEntity>> Updating(InterceptorParameters args, ZeroIdEntity model) => Convert(await Updating(args, model as T));
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Task<InterceptorResult<T>> Deleting(InterceptorParameters args, T model) => Task.FromResult<InterceptorResult<T>>(default);
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed override async Task<InterceptorResult<ZeroIdEntity>> Deleting(InterceptorParameters args, ZeroIdEntity model) => Convert(await Deleting(args, model as T));
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Task Created(InterceptorParameters args, T model) => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed override Task Created(InterceptorParameters args, ZeroIdEntity model) => Created(args, model as T);
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Task Updated(InterceptorParameters args, T model) => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed override Task Updated(InterceptorParameters args, ZeroIdEntity model) => Updated(args, model as T);
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Task Deleted(InterceptorParameters args, T model) => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed override Task Deleted(InterceptorParameters args, ZeroIdEntity model) => Deleted(args, model as T);
|
||||
|
||||
|
||||
InterceptorResult<ZeroIdEntity> Convert(InterceptorResult<T> result)
|
||||
{
|
||||
if (result == default)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return new InterceptorResult<ZeroIdEntity>()
|
||||
{
|
||||
Continue = result.Continue,
|
||||
InterceptorHash = result.InterceptorHash,
|
||||
Result = result.Result != null ? result.Result.ConvertTo<ZeroIdEntity>(result.Result.Model) : null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public abstract partial class Interceptor : IInterceptor
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public int Gravity { get; protected set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name { get; protected set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Hash { get; protected set; }
|
||||
|
||||
|
||||
public Interceptor()
|
||||
{
|
||||
Gravity = 0;
|
||||
Hash = IdGenerator.Create();
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual bool CanHandle(InterceptorParameters args, Type modelType) => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Task<InterceptorResult<ZeroIdEntity>> Creating(InterceptorParameters args, ZeroIdEntity model) => Task.FromResult<InterceptorResult<ZeroIdEntity>>(default);
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Task<InterceptorResult<ZeroIdEntity>> Updating(InterceptorParameters args, ZeroIdEntity model) => Task.FromResult<InterceptorResult<ZeroIdEntity>>(default);
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Task<InterceptorResult<ZeroIdEntity>> Deleting(InterceptorParameters args, ZeroIdEntity model) => Task.FromResult<InterceptorResult<ZeroIdEntity>>(default);
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Task Created(InterceptorParameters args, ZeroIdEntity model) => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Task Updated(InterceptorParameters args, ZeroIdEntity model) => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Task Deleted(InterceptorParameters args, ZeroIdEntity model) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
||||
public interface IInterceptor<T> : IInterceptor where T : ZeroIdEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of the associated model
|
||||
/// </summary>
|
||||
Type ModelType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Called after an entity has been stored but before the session has saved its changes
|
||||
/// </summary>
|
||||
Task Created(InterceptorParameters args, T model);
|
||||
|
||||
/// <summary>
|
||||
/// Called before an entity is stored and validated
|
||||
/// </summary>
|
||||
Task<InterceptorResult<T>> Creating(InterceptorParameters args, T model);
|
||||
|
||||
/// <summary>
|
||||
/// Called after an entity has been deleted but before the session has saved its changes
|
||||
/// </summary>
|
||||
Task Deleted(InterceptorParameters args, T model);
|
||||
|
||||
/// <summary>
|
||||
/// Called before an entity is deleted
|
||||
/// </summary>
|
||||
Task<InterceptorResult<T>> Deleting(InterceptorParameters args, T model);
|
||||
|
||||
/// <summary>
|
||||
/// Called after an entity has been updated but before the session has saved its changes
|
||||
/// </summary>
|
||||
Task Updated(InterceptorParameters args, T model);
|
||||
|
||||
/// <summary>
|
||||
/// Called before an entity is stored and validated
|
||||
/// </summary>
|
||||
Task<InterceptorResult<T>> Updating(InterceptorParameters args, T model);
|
||||
}
|
||||
|
||||
|
||||
public interface IInterceptor
|
||||
{
|
||||
/// <summary>
|
||||
/// Interceptors with a higher gravity will run before any with lower gravity
|
||||
/// </summary>
|
||||
int Gravity { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Readable name for this interceptor
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Hash which is used for this interceptor to be able to pass results from Start to Complete methods
|
||||
/// </summary>
|
||||
string Hash { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether any of the interceptor methods is allowed to run based on the parameters
|
||||
/// </summary>
|
||||
bool CanHandle(InterceptorParameters args, Type modelType);
|
||||
|
||||
/// <summary>
|
||||
/// Called after an entity has been stored but before the session has saved its changes
|
||||
/// </summary>
|
||||
Task Created(InterceptorParameters args, ZeroIdEntity model);
|
||||
|
||||
/// <summary>
|
||||
/// Called before an entity is stored and validated
|
||||
/// </summary>
|
||||
Task<InterceptorResult<ZeroIdEntity>> Creating(InterceptorParameters args, ZeroIdEntity model);
|
||||
|
||||
/// <summary>
|
||||
/// Called after an entity has been deleted but before the session has saved its changes
|
||||
/// </summary>
|
||||
Task Deleted(InterceptorParameters args, ZeroIdEntity model);
|
||||
|
||||
/// <summary>
|
||||
/// Called before an entity is deleted
|
||||
/// </summary>
|
||||
Task<InterceptorResult<ZeroIdEntity>> Deleting(InterceptorParameters args, ZeroIdEntity model);
|
||||
|
||||
/// <summary>
|
||||
/// Called after an entity has been updated but before the session has saved its changes
|
||||
/// </summary>
|
||||
Task Updated(InterceptorParameters args, ZeroIdEntity model);
|
||||
|
||||
/// <summary>
|
||||
/// Called before an entity is stored and validated
|
||||
/// </summary>
|
||||
Task<InterceptorResult<ZeroIdEntity>> Updating(InterceptorParameters args, ZeroIdEntity model);
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace zero.Communication;
|
||||
|
||||
public class InterceptorInstruction<T> where T : ZeroIdEntity, new()
|
||||
{
|
||||
public Guid Guid { get; private set; }
|
||||
|
||||
public InterceptorRunType Runtype { get; private set; }
|
||||
|
||||
public T Model { get; private set; }
|
||||
|
||||
public T PreviousModel { get; private set; }
|
||||
|
||||
public Type ModelType { get; private set; }
|
||||
|
||||
public Result<T> Result { get; private set; }
|
||||
|
||||
protected IZeroContext Context { get; private set; }
|
||||
|
||||
protected Lazy<IEnumerable<IInterceptor>> Interceptors { get; private set; }
|
||||
|
||||
protected Dictionary<IInterceptor, InterceptorParameters> InterceptorCache { get; private set; } = new();
|
||||
|
||||
protected IInterceptors InterceptorHandler { get; private set; }
|
||||
|
||||
protected ILogger<IInterceptor> Logger { get; private set; }
|
||||
|
||||
protected Func<IInterceptor, bool> InterceptorFilter { get; private set; } = x => true;
|
||||
|
||||
|
||||
internal InterceptorInstruction(IInterceptors interceptors, IZeroContext context, Lazy<IEnumerable<IInterceptor>> registrations, ILogger<IInterceptor> logger, InterceptorRunType runtype, T model, T previousModel = null)
|
||||
{
|
||||
InterceptorHandler = interceptors;
|
||||
Context = context;
|
||||
Guid = Guid.NewGuid();
|
||||
Logger = logger;
|
||||
Runtype = runtype;
|
||||
Model = model;
|
||||
PreviousModel = previousModel;
|
||||
Interceptors = registrations;
|
||||
|
||||
ModelType = model.GetType();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Custom interceptor filter for this instruction (the CanHandle() method for each interceptor is still activated)
|
||||
/// </summary>
|
||||
public void Filter(Func<IInterceptor, bool> predicate)
|
||||
{
|
||||
InterceptorFilter = predicate;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Run all interceptors (in order) which can handle the given type.
|
||||
/// Depending on the action any of the following methods on the interceptor is called: Creating(), Updating(), Deleting().
|
||||
/// If any of the interceptors returns a result the operation is cancelled and this result is returned.
|
||||
/// </summary>
|
||||
public async Task<bool> Start(IStoreOperations operations)
|
||||
{
|
||||
foreach (IInterceptor interceptor in GetInterceptors())
|
||||
{
|
||||
InterceptorParameters parameters = new()
|
||||
{
|
||||
Context = Context,
|
||||
Store = Context.Store,
|
||||
Properties = new(),
|
||||
Interceptors = InterceptorHandler,
|
||||
Operations = operations,
|
||||
PreviousModel = PreviousModel
|
||||
};
|
||||
|
||||
if (!interceptor.CanHandle(parameters, ModelType))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
Logger.LogDebug("Run interceptor {interceptor} for {type}:{operation}", interceptor.Name, ModelType, Runtype);
|
||||
|
||||
InterceptorResult<ZeroIdEntity> result = (await HandleBefore(interceptor, parameters)) ?? new();
|
||||
result.InterceptorHash = IdGenerator.Create(32);
|
||||
|
||||
InterceptorCache.Add(interceptor, parameters);
|
||||
|
||||
// we cancel all further interceptors if a result is available and return this instead
|
||||
if (result.Result != null)
|
||||
{
|
||||
Result = result.Result.ConvertTo(result.Result.Model as T);
|
||||
return false;
|
||||
}
|
||||
|
||||
// the Continue task will cancel all further interceptors
|
||||
if (!result.Continue)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Run all interceptors (in order) which can handle the given type.
|
||||
/// Depending on the action any of the following methods on the interceptor is called: Created(), Updated(), Deleted().
|
||||
/// The parameters which are returned from the Start() operation are passed to the methods.
|
||||
/// </summary>
|
||||
public async Task Complete()
|
||||
{
|
||||
foreach ((IInterceptor interceptor, InterceptorParameters parameters) in InterceptorCache)
|
||||
{
|
||||
await HandleAfter(interceptor, parameters);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected IEnumerable<IInterceptor> GetInterceptors()
|
||||
{
|
||||
return Interceptors.Value.Where(InterceptorFilter).OrderByDescending(x => x.Gravity);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Proxy for handling methods on an interceptor
|
||||
/// </summary>
|
||||
protected Task<InterceptorResult<ZeroIdEntity>> HandleBefore(IInterceptor interceptor, InterceptorParameters parameters) => Runtype switch
|
||||
{
|
||||
InterceptorRunType.Create => interceptor.Creating(parameters, Model),
|
||||
InterceptorRunType.Update => interceptor.Updating(parameters, Model),
|
||||
InterceptorRunType.Delete => interceptor.Deleting(parameters, Model),
|
||||
_ => throw new NotImplementedException()
|
||||
};
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Proxy for handling methods on an interceptor
|
||||
/// </summary>
|
||||
protected Task HandleAfter(IInterceptor interceptor, InterceptorParameters parameters) => Runtype switch
|
||||
{
|
||||
InterceptorRunType.Create => interceptor.Created(parameters, Model),
|
||||
InterceptorRunType.Update => interceptor.Updated(parameters, Model),
|
||||
InterceptorRunType.Delete => interceptor.Deleted(parameters, Model),
|
||||
_ => throw new NotImplementedException()
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
namespace zero.Communication;
|
||||
|
||||
public class InterceptorParameters
|
||||
{
|
||||
/// <summary>
|
||||
/// The current zero context
|
||||
/// </summary>
|
||||
public IZeroContext Context { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Raven document store
|
||||
/// </summary>
|
||||
public IZeroStore Store { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Access to other interceptor methods
|
||||
/// </summary>
|
||||
public IInterceptors Interceptors { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Access to operations
|
||||
/// </summary>
|
||||
public IStoreOperations Operations { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Parameters from the interceptor which ran on before the operation (only available for completed operations)
|
||||
/// </summary>
|
||||
public Dictionary<string, object> Properties { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Get a typed property
|
||||
/// </summary>
|
||||
public TProp Property<TProp>(string key) => Properties.GetValueOrDefault<TProp>(key);
|
||||
|
||||
/// <summary>
|
||||
/// Get a typed property
|
||||
/// </summary>
|
||||
public bool TryGetProperty<TProp>(string key, out TProp property) => Properties.TryGetValue(key, out property);
|
||||
|
||||
/// <summary>
|
||||
/// Holds a reference to the previously existing model when an update happens (can be null)
|
||||
/// </summary>
|
||||
public object PreviousModel { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace zero.Communication;
|
||||
|
||||
public class InterceptorResult<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Autoset. Hash used to match interceptors in order to correctly pass parameters
|
||||
/// </summary>
|
||||
internal string InterceptorHash { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Prevent further interceptors from running for this operation (only valid for the current interception type, e.g. Update/Created/Purge/...)
|
||||
/// </summary>
|
||||
public bool Continue { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Set a result. This will prevent further execution of the operation and cancels all subsequent interceptors
|
||||
/// </summary>
|
||||
public Result<T> Result { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace zero.Communication;
|
||||
|
||||
public enum InterceptorRunType
|
||||
{
|
||||
Create = 1,
|
||||
Update = 2,
|
||||
Delete = 3
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace zero.Communication;
|
||||
|
||||
public class Interceptors : IInterceptors
|
||||
{
|
||||
protected IZeroContext Context { get; set; }
|
||||
|
||||
protected Lazy<IEnumerable<IInterceptor>> Registrations { get; set; }
|
||||
|
||||
protected ILogger<IInterceptor> Logger { get; set; }
|
||||
|
||||
|
||||
public Interceptors(IZeroContext context, Lazy<IEnumerable<IInterceptor>> registrations, ILogger<IInterceptor> logger)
|
||||
{
|
||||
Context = context;
|
||||
Registrations = registrations;
|
||||
Logger = logger;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public InterceptorInstruction<T> ForCreate<T>(T model) where T : ZeroIdEntity, new() => new(this, Context, Registrations, Logger, InterceptorRunType.Create, model);
|
||||
|
||||
/// <inheritdoc />
|
||||
public InterceptorInstruction<T> ForUpdate<T>(T model, T previousModel = null) where T : ZeroIdEntity, new() => new(this, Context, Registrations, Logger, InterceptorRunType.Update, model, previousModel);
|
||||
|
||||
/// <inheritdoc />
|
||||
public InterceptorInstruction<T> ForDelete<T>(T model) where T : ZeroIdEntity, new() => new(this, Context, Registrations, Logger, InterceptorRunType.Delete, model);
|
||||
}
|
||||
|
||||
|
||||
public interface IInterceptors
|
||||
{
|
||||
/// <summary>
|
||||
/// Instruction which can run interceptors before and after a creating an entity
|
||||
/// </summary>
|
||||
InterceptorInstruction<T> ForCreate<T>(T model) where T : ZeroIdEntity, new();
|
||||
|
||||
/// <summary>
|
||||
/// Instruction which can run interceptors before and after updating an entity
|
||||
/// </summary>
|
||||
InterceptorInstruction<T> ForUpdate<T>(T model, T previousModel = null) where T : ZeroIdEntity, new();
|
||||
|
||||
/// <summary>
|
||||
/// Instruction which can run interceptors before and after deleting an entity
|
||||
/// </summary>
|
||||
InterceptorInstruction<T> ForDelete<T>(T model) where T : ZeroIdEntity, new();
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace zero.Communication;
|
||||
|
||||
public class LazilyResolved<T> : Lazy<T>
|
||||
{
|
||||
public LazilyResolved(IServiceProvider serviceProvider): base(serviceProvider.GetRequiredService<T>)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
|
||||
namespace zero.Communication;
|
||||
|
||||
public interface IMessage
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
namespace zero.Communication;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates a handler that can perform an action when a message of
|
||||
/// a certain type is received. (could be an interface rather than concrete type)
|
||||
/// </summary>
|
||||
public interface IMessageHandler<TMessage> where TMessage : IMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Method to invoke when a message of type TMessage is published
|
||||
/// </summary>
|
||||
Task Handle(TMessage message);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Indicates a handler that can perform an action when a message of
|
||||
/// a certain type is received. (could be an interface rather than concrete type)
|
||||
/// </summary>
|
||||
public interface IBatchMessageHandler<TMessage> : IMessageHandler<TMessage> where TMessage : IMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Method to invoke when a batch of messages of type TMessage are published
|
||||
/// </summary>
|
||||
Task HandleBatchAsync(IReadOnlyCollection<TMessage> message);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace zero.Communication;
|
||||
|
||||
public class MessageAggregator : IMessageAggregator
|
||||
{
|
||||
readonly ConcurrentBag<IMessageSubscription> Subscription = new ConcurrentBag<IMessageSubscription>();
|
||||
readonly IServiceProvider ServiceProvider;
|
||||
|
||||
|
||||
public MessageAggregator(IServiceProvider serviceProvider)
|
||||
{
|
||||
ServiceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Publish<TMessage>(TMessage message) where TMessage : class, IMessage
|
||||
{
|
||||
IEnumerable<IMessageSubscription> subscriptions = Subscription.Where(s => s.CanDeliver<TMessage>());
|
||||
|
||||
foreach (IMessageSubscription subscription in subscriptions)
|
||||
{
|
||||
await subscription.Deliver(ServiceProvider, message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Activate<TMessage, TMessageHandler>()
|
||||
where TMessage : class, IMessage
|
||||
where TMessageHandler : IMessageHandler<TMessage>
|
||||
{
|
||||
Subscription.Add(new MessageSubscription<TMessage, TMessageHandler>());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface IMessageAggregator
|
||||
{
|
||||
/// <summary>
|
||||
/// Publishes the specified message, invoking any handlers subscribed to the message.
|
||||
/// </summary>
|
||||
Task Publish<TMessage>(TMessage message) where TMessage : class, IMessage;
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes the specified handler to the spified message type
|
||||
/// </summary>
|
||||
void Activate<TMessage, TMessageHandler>()
|
||||
where TMessage : class, IMessage
|
||||
where TMessageHandler : IMessageHandler<TMessage>;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.Reflection;
|
||||
|
||||
namespace zero.Communication;
|
||||
|
||||
internal class MessageSubscription<TMessage, TMessageHandler> : IMessageSubscription
|
||||
where TMessage : class, IMessage
|
||||
where TMessageHandler : IMessageHandler<TMessage>
|
||||
{
|
||||
public bool CanDeliver<T>()
|
||||
{
|
||||
return typeof(TMessage).GetTypeInfo().IsAssignableFrom(typeof(T));
|
||||
}
|
||||
|
||||
public async Task Deliver(IServiceProvider serviceProvider, object message)
|
||||
{
|
||||
if (message == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(message));
|
||||
}
|
||||
if (!(message is TMessage))
|
||||
{
|
||||
throw new ArgumentException($"{ nameof(message) } must be of type '{typeof(TMessage).FullName}'");
|
||||
}
|
||||
|
||||
var handlers = serviceProvider.GetServices<TMessageHandler>();
|
||||
|
||||
foreach (var handler in handlers)
|
||||
{
|
||||
await handler.Handle((TMessage)message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface IMessageSubscription
|
||||
{
|
||||
bool CanDeliver<TMessage>();
|
||||
|
||||
Task Deliver(IServiceProvider serviceProvider, object message);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace zero.Communication;
|
||||
|
||||
internal class ZeroCommunicationModule : ZeroModule
|
||||
{
|
||||
public override void ConfigureServices(IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddScoped<IInterceptors, Interceptors>();
|
||||
services.AddScoped<IMessageAggregator, MessageAggregator>();
|
||||
services.AddTransient<IHandlerHolder, HandlerHolder>();
|
||||
services.AddTransient(typeof(Lazy<>), typeof(LazilyResolved<>));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
namespace zero.Configuration;
|
||||
|
||||
public static partial class Constants
|
||||
{
|
||||
public const string ErrorFieldNone = "__zero_no_field";
|
||||
|
||||
public static partial class Tabs
|
||||
{
|
||||
public const string General = "general";
|
||||
}
|
||||
|
||||
public static partial class Auth
|
||||
{
|
||||
public const string SystemUser = "system";
|
||||
public const string DefaultScheme = "zeroScheme";
|
||||
public const string BackofficeDisplayName = "Zero Backoffice";
|
||||
public const string BackofficeScheme = "zeroBackoffice";
|
||||
public const string BackofficeCookieName = "zero.be.session";
|
||||
public const string DefaultCookieName = "zero.session";
|
||||
|
||||
public static partial class Claims
|
||||
{
|
||||
public const string IsZero = "zero.claim.iszero";
|
||||
public const string IsSuper = "zero.claim.issuper";
|
||||
public const string UserId = "zero.claim.userid";
|
||||
public const string UserName = "zero.claim.username";
|
||||
public const string Role = "zero.claim.rolealias";
|
||||
public const string SecurityStamp = "zero.claim.securitystamp";
|
||||
public const string Email = "zero.claim.email";
|
||||
public const string Permission = "zero.claim.permission";
|
||||
public const string DefaultAppId = "zero.claim.defaultAppId";
|
||||
public const string AppId = "zero.claim.appId";
|
||||
public const string TicketExpires = "zero.claim.ticketExpires";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static partial class Permissions
|
||||
{
|
||||
public static partial class Groups
|
||||
{
|
||||
public const string Sections = "zero.permissions.groups.sections";
|
||||
public const string Spaces = "zero.permissions.groups.spaces";
|
||||
public const string Settings = "zero.permissions.groups.settings";
|
||||
public const string Pages = "zero.permissions.groups.pages";
|
||||
public const string Modules = "zero.permissions.groups.modules";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static partial class Database
|
||||
{
|
||||
public const string ReservationPrefix = "zero.";
|
||||
public const string Expires = Raven.Client.Constants.Documents.Metadata.Expires;
|
||||
}
|
||||
|
||||
public static partial class Sections
|
||||
{
|
||||
public const string Dashboard = "dashboard";
|
||||
public const string Pages = "pages";
|
||||
public const string Spaces = "spaces";
|
||||
public const string Media = "media";
|
||||
public const string Settings = "settings";
|
||||
}
|
||||
|
||||
public static partial class Settings
|
||||
{
|
||||
public const string Updates = "updates";
|
||||
public const string Applications = "applications";
|
||||
public const string Users = "users";
|
||||
public const string Languages = "languages";
|
||||
public const string Translations = "translations";
|
||||
public const string Countries = "countries";
|
||||
public const string Mails = "mailTemplates";
|
||||
public const string Integrations = "integrations";
|
||||
public const string Logging = "logs";
|
||||
public const string Plugins = "plugins";
|
||||
public const string CreatePlugin = "createplugin";
|
||||
}
|
||||
|
||||
public static partial class PermissionCollections
|
||||
{
|
||||
public const string Sections = "permissionCollectionSections";
|
||||
public const string Spaces = "permissionCollectionSpaces";
|
||||
public const string Settings = "permisssionCollectionSettings";
|
||||
public const string Modules = "permissionCollectionModules";
|
||||
}
|
||||
|
||||
public static partial class Pages
|
||||
{
|
||||
public const string FolderAlias = "zero.folder";
|
||||
public const string DefaultRootPageTypeAlias = "root";
|
||||
public const string PageRouteProviderAlias = "zero.pages";
|
||||
}
|
||||
|
||||
public static partial class Routing
|
||||
{
|
||||
public const string InternalRoutePrefix = "/__zero/";
|
||||
|
||||
public const string ErrorRoute = InternalRoutePrefix + "error";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace zero.Configuration;
|
||||
|
||||
public interface IZeroCollectionOptions
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace zero.Configuration;
|
||||
|
||||
public abstract class OptionsType
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of the associated model class
|
||||
/// </summary>
|
||||
public Type ContentType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The alias
|
||||
/// </summary>
|
||||
public string Alias { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the options type
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Set a validator for the editor
|
||||
/// </summary>
|
||||
public IValidator Validator { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace zero.Configuration;
|
||||
|
||||
internal class ZeroConfigurationModule : ZeroModule
|
||||
{
|
||||
public override int Order => -1000;
|
||||
|
||||
public override void ConfigureServices(IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddOptions<ZeroOptions>().Configure<IServiceProvider>((opts, svc) =>
|
||||
{
|
||||
opts.ServiceProvider = svc;
|
||||
opts.Version = "1.0.0-alpha.1";
|
||||
opts.TokenExpiration = TimeSpan.FromHours(3);
|
||||
}).Bind(configuration.GetSection("Zero"));
|
||||
|
||||
services.AddTransient<IZeroOptions, ZeroOptions>(factory => factory.GetService<IOptions<ZeroOptions>>().Value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace zero.Configuration;
|
||||
|
||||
public class ZeroOptions : IZeroOptions
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public bool Initialized => !string.IsNullOrEmpty(For<RavenOptions>()?.Database);
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Version { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan TokenExpiration { get; set; }
|
||||
|
||||
|
||||
internal IServiceProvider ServiceProvider { get; set; }
|
||||
|
||||
protected ConcurrentDictionary<Type, object> OptionsCache { get; private set; } = new();
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public TOptions For<TOptions>() where TOptions : class
|
||||
{
|
||||
Type type = typeof(TOptions);
|
||||
|
||||
if (!OptionsCache.TryGetValue(type, out object value) && ServiceProvider != null)
|
||||
{
|
||||
IOptions<TOptions> options = ServiceProvider.GetService<IOptions<TOptions>>();
|
||||
value = options.Value;
|
||||
OptionsCache.TryAdd(type, value);
|
||||
}
|
||||
|
||||
return value as TOptions;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface IZeroOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// The currently active version
|
||||
/// This should not be set manually, as it is used for setup and migrations and incremented automatically
|
||||
/// </summary>
|
||||
string Version { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this zero instance is initialized (setup is completed)
|
||||
/// </summary>
|
||||
bool Initialized { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Expiration of a generated change token for an entity
|
||||
/// </summary>
|
||||
TimeSpan TokenExpiration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Get typed options (proxy to IOptions<TOptions>)
|
||||
/// </summary>
|
||||
TOptions For<TOptions>() where TOptions : class;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace zero.Configuration;
|
||||
|
||||
public class ZeroStartupOptions : IZeroStartupOptions
|
||||
{
|
||||
public IList<IAssemblyDiscoveryRule> AssemblyDiscoveryRules { get; private set; } = new List<IAssemblyDiscoveryRule>();
|
||||
|
||||
public IMvcBuilder Mvc { get; private set; }
|
||||
|
||||
|
||||
public ZeroStartupOptions(IMvcBuilder mvc)
|
||||
{
|
||||
Mvc = mvc;
|
||||
}
|
||||
}
|
||||
|
||||
public interface IZeroStartupOptions
|
||||
{
|
||||
IList<IAssemblyDiscoveryRule> AssemblyDiscoveryRules { get; }
|
||||
|
||||
IMvcBuilder Mvc { get; }
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace zero.Context;
|
||||
|
||||
public class ZeroContext : IZeroContext
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IZeroOptions Options { get; protected set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IZeroStore Store { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IServiceProvider Services { get; private set; }
|
||||
|
||||
protected ICultureResolver CultureResolver { get; private set; }
|
||||
|
||||
protected ILogger<ZeroContext> Logger { get; private set; }
|
||||
|
||||
protected IHandlerHolder Handler { get; private set; }
|
||||
|
||||
protected IHttpContextAccessor HttpContextAccessor { get; private set; }
|
||||
|
||||
protected IPrimitiveTypeCollection ValueCollection { get; private set; }
|
||||
|
||||
|
||||
bool _resolved = false;
|
||||
|
||||
|
||||
public ZeroContext(IZeroOptions options, IHttpContextAccessor httpContextAccessor, ICultureResolver cultureResolver,
|
||||
ILogger<ZeroContext> logger, IZeroStore store, IHandlerHolder handler, IServiceProvider services)
|
||||
{
|
||||
Options = options;
|
||||
CultureResolver = cultureResolver;
|
||||
Logger = logger;
|
||||
Store = store;
|
||||
Handler = handler;
|
||||
ValueCollection = new PrimitiveTypeCollection();
|
||||
HttpContextAccessor = httpContextAccessor;
|
||||
Services = services;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual async Task Resolve(HttpContext context)
|
||||
{
|
||||
if (_resolved)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_resolved = true;
|
||||
|
||||
// set current culture
|
||||
await CultureResolver.Resolve(this);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public T Get<T>() => ValueCollection.Get<T>();
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Set<T>(T value) => ValueCollection.Set(value);
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Remove<T>() => ValueCollection.Remove<T>();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public interface IZeroContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Global zero options
|
||||
/// </summary>
|
||||
IZeroOptions Options { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Document store
|
||||
/// </summary>
|
||||
IZeroStore Store { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Service container
|
||||
/// </summary>
|
||||
IServiceProvider Services { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the current application (for backoffice + frontend requests) and
|
||||
/// the currently active backoffice user, as users are not signed in with the default scheme and do therefore not populate HttpContext.User
|
||||
/// </summary>
|
||||
Task Resolve(HttpContext context);
|
||||
|
||||
/// <summary>
|
||||
/// Get a custom property from this scoped context
|
||||
/// </summary>
|
||||
T Get<T>();
|
||||
|
||||
/// <summary>
|
||||
/// Add a custom property to this scoped context
|
||||
/// </summary>
|
||||
void Set<T>(T value);
|
||||
|
||||
/// <summary>
|
||||
/// Remove a custom property from this scoped context
|
||||
/// </summary>
|
||||
void Remove<T>();
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace zero.Context
|
||||
{
|
||||
public class ZeroContextMiddleware
|
||||
{
|
||||
RequestDelegate _next;
|
||||
|
||||
public ZeroContextMiddleware(RequestDelegate next)
|
||||
{
|
||||
_next = next;
|
||||
}
|
||||
|
||||
public async Task Invoke(HttpContext httpContext, IZeroContext zeroContext)
|
||||
{
|
||||
await zeroContext.Resolve(httpContext);
|
||||
await _next(httpContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace zero.Context;
|
||||
|
||||
internal class ZeroContextModule : ZeroModule
|
||||
{
|
||||
public override void ConfigureServices(IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddScoped<IZeroContext, ZeroContext>();
|
||||
services.AddHttpContextAccessor();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
namespace zero.Extensions;
|
||||
|
||||
public static class CharExtensions
|
||||
{
|
||||
private static Dictionary<char, char[]> accents { get; } = new Dictionary<char, char[]>()
|
||||
{
|
||||
{ 'ä', new char[2] { 'a', 'e' } },
|
||||
{ 'á', new char[1] { 'a' } },
|
||||
{ 'à', new char[1] { 'a' } },
|
||||
{ 'ó', new char[1] { 'o' } },
|
||||
{ 'ò', new char[1] { 'o' } },
|
||||
{ 'é', new char[1] { 'e' } },
|
||||
{ 'è', new char[1] { 'e' } },
|
||||
{ 'ú', new char[1] { 'u' } },
|
||||
{ 'ù', new char[1] { 'u' } },
|
||||
{ 'í', new char[1] { 'i' } },
|
||||
{ 'ì', new char[1] { 'i' } },
|
||||
{ 'ö', new char[2] { 'o', 'e' } },
|
||||
{ 'ü', new char[2] { 'u', 'e' } },
|
||||
{ 'ß', new char[2] { 's', 's' } },
|
||||
{ '&', new char[1] { '+' } }
|
||||
};
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Check if a character is from a-z, A-Z or 0-9
|
||||
/// </summary>
|
||||
public static bool IsAZor09(this char value)
|
||||
{
|
||||
return (value >= 0x41 && value <= 0x5A) || (value >= 0x61 && value <= 0x7a) || (value >= 0x30 && value <= 0x39);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Check if a character is in ASCII range
|
||||
/// </summary>
|
||||
public static bool IsASCII(this char value)
|
||||
{
|
||||
return value < 128;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Replaces an accent or umlaut with the appropriate URL + file ready variant
|
||||
/// </summary>
|
||||
public static bool TryReplaceAccent(this char value, out char[] result)
|
||||
{
|
||||
if (!accents.ContainsKey(value))
|
||||
{
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
result = accents[value];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using System.Drawing;
|
||||
|
||||
namespace zero.Extensions;
|
||||
|
||||
public static class ColorExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Get color object from a hex string
|
||||
/// </summary>
|
||||
public static Color FromHex(this string color)
|
||||
{
|
||||
if (color.StartsWith("#"))
|
||||
{
|
||||
color = color[1..];
|
||||
}
|
||||
|
||||
if (color.Length != 6)
|
||||
{
|
||||
throw new Exception("Hex color has to be in format #RRGGBB");
|
||||
}
|
||||
|
||||
return Color.FromArgb(
|
||||
int.Parse(color.Substring(0, 2), System.Globalization.NumberStyles.HexNumber),
|
||||
int.Parse(color.Substring(2, 2), System.Globalization.NumberStyles.HexNumber),
|
||||
int.Parse(color.Substring(4, 2), System.Globalization.NumberStyles.HexNumber)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get contrast ratio between two color values
|
||||
/// <see cref="https://github.com/LeaVerou/contrast-ratio/blob/gh-pages/color.js#L95" />
|
||||
/// </summary>
|
||||
public static double GetContrastRatio(this Color color1, Color color2)
|
||||
{
|
||||
double lum1 = color1.GetLuminance();
|
||||
double lum2 = color2.GetLuminance();
|
||||
double brightest = Math.Max(lum1, lum2);
|
||||
double darkest = Math.Min(lum1, lum2);
|
||||
return (brightest + 0.05) / (darkest + 0.05);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get luminance value of a color based on WCAG-conform JS implementation
|
||||
/// <see cref="https://github.com/LeaVerou/contrast-ratio/blob/gh-pages/color.js#L42" />
|
||||
/// </summary>
|
||||
public static double GetLuminance(this Color color)
|
||||
{
|
||||
double[] a = new double[3] { color.R, color.G, color.B }.Select(v =>
|
||||
{
|
||||
v /= 255;
|
||||
return v <= 0.03928 ? v / 12.92 : Math.Pow((v + 0.055) / 1.055, 2.4);
|
||||
}).ToArray();
|
||||
|
||||
return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
namespace zero.Extensions;
|
||||
|
||||
public static class DictionaryExtensions
|
||||
{
|
||||
public static bool TryGetValue<T>(this Dictionary<string, object> model, string key, out T value)
|
||||
{
|
||||
if (!model.TryGetValue(key, out object valueObj) || !(valueObj is T))
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = (T)valueObj;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public static T GetValueOrDefault<T>(this Dictionary<string, object> model, string key)
|
||||
{
|
||||
object value = model.GetValueOrDefault(key);
|
||||
return value == default || !(value is T) ? default : (T)value;
|
||||
}
|
||||
|
||||
|
||||
public static Dictionary<TKey, TElement> ToDistinctDictionary<TSource, TKey, TElement>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector)
|
||||
{
|
||||
Dictionary<TKey, TElement> result = new();
|
||||
|
||||
foreach (TSource sourceElement in source)
|
||||
{
|
||||
result.TryAdd(keySelector(sourceElement), elementSelector(sourceElement));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
namespace zero.Extensions;
|
||||
|
||||
public static class EnumerableExtensions
|
||||
{
|
||||
public static IEnumerable<T> Paging<T>(this IEnumerable<T> source, int pageNumber, int pageSize)
|
||||
{
|
||||
pageNumber = pageNumber.Limit(1, 10_000_000);
|
||||
pageSize = pageSize.Limit(1, 1_000);
|
||||
|
||||
if (pageNumber <= 0 || pageSize <= 0)
|
||||
{
|
||||
throw new NotSupportedException("Both pageNumber and pageSize must be greater than zero");
|
||||
}
|
||||
|
||||
return source.Skip((pageNumber - 1) * pageSize).Take(pageSize);
|
||||
}
|
||||
|
||||
|
||||
public static bool TryGet<T>(this IEnumerable<T> source, Func<T, bool> predicate, out T model)
|
||||
{
|
||||
model = source.FirstOrDefault(predicate);
|
||||
return model != null;
|
||||
}
|
||||
|
||||
|
||||
public static bool TryAdd<T>(this IList<T> source, T model) where T : class
|
||||
{
|
||||
if (model == default)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
source.Add(model);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using System.Linq.Expressions;
|
||||
using System.Text;
|
||||
|
||||
namespace zero.Extensions;
|
||||
|
||||
public static class ExpressionExtensions
|
||||
{
|
||||
internal static string GetPropertyPath(this Expression expr, out MemberExpression member)
|
||||
{
|
||||
StringBuilder path = new();
|
||||
MemberExpression memberExpression = GetMemberExpression(expr);
|
||||
do
|
||||
{
|
||||
member = memberExpression;
|
||||
|
||||
if (path.Length > 0)
|
||||
{
|
||||
path.Insert(0, ".");
|
||||
}
|
||||
path.Insert(0, memberExpression.Member.Name);
|
||||
memberExpression = GetMemberExpression(memberExpression.Expression);
|
||||
}
|
||||
while (memberExpression != null);
|
||||
|
||||
return path.ToString();
|
||||
}
|
||||
|
||||
internal static MemberExpression GetMemberExpression(this Expression expression)
|
||||
{
|
||||
if (expression is MemberExpression)
|
||||
{
|
||||
return (MemberExpression)expression;
|
||||
}
|
||||
else if (expression is LambdaExpression)
|
||||
{
|
||||
var lambdaExpression = expression as LambdaExpression;
|
||||
if (lambdaExpression.Body is MemberExpression)
|
||||
{
|
||||
return (MemberExpression)lambdaExpression.Body;
|
||||
}
|
||||
else if (lambdaExpression.Body is UnaryExpression)
|
||||
{
|
||||
return ((MemberExpression)((UnaryExpression)lambdaExpression.Body).Operand);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace zero.Extensions;
|
||||
|
||||
public static class HttpContextExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the current request is an AJAX request
|
||||
/// </summary>
|
||||
public static bool IsAjaxRequest(this HttpContext context)
|
||||
{
|
||||
if (context?.Request?.Headers == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return context.Request.Headers["X-Requested-With"] == "XMLHttpRequest";
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Whether the current request only accepts application/json
|
||||
/// </summary>
|
||||
public static bool IsJsonRequest(this HttpContext context)
|
||||
{
|
||||
if (context?.Request?.Headers == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return context.Request.Headers["Accept"] == "application/json";
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get IP Address of the client
|
||||
/// </summary>
|
||||
public static string GetClientIpAddress(this HttpContext context)
|
||||
{
|
||||
return context.Connection.RemoteIpAddress?.ToString();
|
||||
//string ipAddress = context.GetServerVariable("HTTP_X_FORWARDED_FOR");
|
||||
//return !ipAddress.IsNullOrEmpty() ? ipAddress.Split(',')[0] : context.GetServerVariable("REMOTE_ADDR");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
namespace zero.Extensions;
|
||||
|
||||
public static class NumberExtensions
|
||||
{
|
||||
static Dictionary<FileSizeNotation, string[]> FileSizeUnits = new()
|
||||
{
|
||||
{ FileSizeNotation.SI, new string[] { "B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" } },
|
||||
{ FileSizeNotation.IEC, new string[] { "B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB" } },
|
||||
{ FileSizeNotation.JEDEC, new string[] { "B", "KB", "MB", "GB" } }
|
||||
};
|
||||
|
||||
public static int Limit(this int input, int min, int max)
|
||||
{
|
||||
if (input < min)
|
||||
{
|
||||
return min;
|
||||
}
|
||||
if (input > max)
|
||||
{
|
||||
return max;
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
|
||||
public static string GetFileSize(this long sizeInBytes, FileSizeNotation notation = FileSizeNotation.JEDEC)
|
||||
{
|
||||
if (!FileSizeUnits.ContainsKey(notation))
|
||||
{
|
||||
throw new NotImplementedException($"The notation {notation} has no implementation for generating human-readable file sizes");
|
||||
}
|
||||
|
||||
int power = notation == FileSizeNotation.SI ? 1000 : 1024;
|
||||
|
||||
string[] units = FileSizeUnits[notation];
|
||||
|
||||
int order = 0;
|
||||
|
||||
while (sizeInBytes >= power && order + 1 < units.Length)
|
||||
{
|
||||
order++;
|
||||
sizeInBytes = sizeInBytes / power;
|
||||
}
|
||||
|
||||
return String.Format("{0:0.##} {1}", sizeInBytes, units[order]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
//using Newtonsoft.Json;
|
||||
|
||||
//namespace zero.Extensions;
|
||||
|
||||
//[Obsolete("we don't want this for every object (use a Utils class instead)")]
|
||||
//public static class ObjectExtensions
|
||||
//{
|
||||
// public static bool Is<T>(this Type type)
|
||||
// {
|
||||
// return type.IsAssignableFrom(typeof(T));
|
||||
// }
|
||||
|
||||
|
||||
// public static bool Is<T>(this object obj)
|
||||
// {
|
||||
// return obj.GetType().IsAssignableFrom(typeof(T));
|
||||
// }
|
||||
|
||||
|
||||
// public static T Clone<T>(this T obj)
|
||||
// {
|
||||
// Type type = obj.GetType();
|
||||
// return (T)JsonConvert.DeserializeObject(JsonConvert.SerializeObject(obj), type);
|
||||
// }
|
||||
|
||||
// public static T CloneLax<T>(this object obj)
|
||||
// {
|
||||
// return JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(obj));
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,48 @@
|
||||
namespace zero.Extensions;
|
||||
|
||||
public static class ResultExtensions
|
||||
{
|
||||
public static Result WithoutModel<TSource>(this Result<TSource> origin)
|
||||
{
|
||||
Result result = new();
|
||||
result.IsSuccess = origin.IsSuccess;
|
||||
result.Errors = origin.Errors;
|
||||
return result;
|
||||
}
|
||||
|
||||
public static Result<TTarget> ConvertTo<TTarget>(this Result origin, TTarget model)
|
||||
{
|
||||
Result<TTarget> result = new();
|
||||
result.IsSuccess = origin.IsSuccess;
|
||||
result.Errors = origin.Errors;
|
||||
result.Model = model;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
public static Result AddError(this Result origin, string property, string message)
|
||||
{
|
||||
origin.IsSuccess = false;
|
||||
origin.Errors.Add(new(property, message));
|
||||
return origin;
|
||||
}
|
||||
|
||||
|
||||
public static Result AddError(this Result origin, string message)
|
||||
{
|
||||
origin.IsSuccess = false;
|
||||
origin.Errors.Add(new(Constants.ErrorFieldNone, message));
|
||||
return origin;
|
||||
}
|
||||
|
||||
|
||||
public static Result AppendErrors(this Result origin, Result with)
|
||||
{
|
||||
foreach (ResultError error in with.Errors)
|
||||
{
|
||||
origin.Errors.Add(error);
|
||||
}
|
||||
origin.IsSuccess = origin.Errors.Any();
|
||||
return origin;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using System.Reflection;
|
||||
|
||||
namespace zero.Extensions;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds all found implementations based on the service type and assembly discovery rules
|
||||
/// </summary>
|
||||
public static void AddAll<TService>(this IServiceCollection services, ServiceLifetime lifetime = ServiceLifetime.Transient, bool asSelf = false) => services.AddAll(typeof(TService), lifetime, asSelf);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Adds all found implementations based on the service type and assembly discovery rules
|
||||
/// </summary>
|
||||
public static void AddAll(this IServiceCollection services, Type serviceType, ServiceLifetime lifetime = ServiceLifetime.Transient, bool asSelf = false)
|
||||
{
|
||||
if (AssemblyDiscovery.Current == null)
|
||||
{
|
||||
throw new Exception("services.AddAll() can only be run after mvcBuilder.AddZero()");
|
||||
}
|
||||
|
||||
// add implementations with generic service types
|
||||
if (serviceType.GetTypeInfo().IsGenericTypeDefinition)
|
||||
{
|
||||
IEnumerable<(Type, TypeInfo)> matches = AssemblyDiscovery.Current.GetAllClassTypes().SelectMany(type =>
|
||||
{
|
||||
var interfaces = type.GetInterfaces().Select(x => x.GetTypeInfo());
|
||||
IEnumerable<Type> genericTypes = interfaces.Where(x => x.IsGenericType && x.GetGenericTypeDefinition() == serviceType);
|
||||
if (genericTypes.Any())
|
||||
{
|
||||
var genericTypeDefinitions = genericTypes.First();
|
||||
}
|
||||
return genericTypes.Select(x => (x, type));
|
||||
});
|
||||
|
||||
foreach ((Type, Type) match in matches)
|
||||
{
|
||||
services.Add(new ServiceDescriptor(asSelf ? match.Item2 : match.Item1, match.Item2, lifetime));
|
||||
}
|
||||
}
|
||||
// add implementations with specific service types
|
||||
else
|
||||
{
|
||||
foreach (Type type in AssemblyDiscovery.Current.GetTypes(serviceType))
|
||||
{
|
||||
services.Add(new ServiceDescriptor(asSelf ? type : serviceType, type, lifetime));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Adds or overrides an implementation
|
||||
/// </summary>
|
||||
public static void Replace<TService, TImplementation>(this IServiceCollection services, ServiceLifetime lifetime = ServiceLifetime.Transient)
|
||||
where TService : class
|
||||
where TImplementation : class, TService
|
||||
{
|
||||
services.RemoveAll<TService>();
|
||||
services.Add(new ServiceDescriptor(typeof(TService), typeof(TImplementation), lifetime));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Adds or overrides an implementation
|
||||
/// </summary>
|
||||
public static void Replace<TService, TImplementation>(this IServiceCollection services, Func<IServiceProvider, TImplementation> implementationFactory, ServiceLifetime lifetime = ServiceLifetime.Transient)
|
||||
where TService : class
|
||||
where TImplementation : class, TService
|
||||
{
|
||||
services.RemoveAll<TService>();
|
||||
services.Add(new ServiceDescriptor(typeof(TService), implementationFactory, lifetime));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Adds or overrides an implementation
|
||||
/// </summary>
|
||||
public static void Replace<TService, TOldImplementation, TNewImplementation>(this IServiceCollection services)
|
||||
where TService : class
|
||||
where TOldImplementation : class, TService
|
||||
where TNewImplementation : class, TService
|
||||
{
|
||||
ServiceDescriptor oldDescriptor = services.FirstOrDefault(x => x.ImplementationType == typeof(TOldImplementation));
|
||||
if (oldDescriptor != null)
|
||||
{
|
||||
services.Remove(oldDescriptor);
|
||||
services.Add(new ServiceDescriptor(typeof(TService), typeof(TNewImplementation), oldDescriptor.Lifetime));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Removes an implementation
|
||||
/// </summary>
|
||||
public static void Remove<TService, TImplementation>(this IServiceCollection services)
|
||||
where TService : class
|
||||
where TImplementation : class, TService
|
||||
{
|
||||
ServiceDescriptor oldDescriptor = services.FirstOrDefault(x => x.ImplementationType == typeof(TImplementation));
|
||||
if (oldDescriptor != null)
|
||||
{
|
||||
services.Remove(oldDescriptor);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace zero.Extensions;
|
||||
|
||||
public static class StringExtensions
|
||||
{
|
||||
const string SPACE = " ";
|
||||
|
||||
static Regex replaceMultipleSpacesRegex { get; } = new Regex("[ ]{2,}", RegexOptions.None);
|
||||
|
||||
static Regex newLineCharsRegex { get; } = new Regex("\t|\n|\r", RegexOptions.None);
|
||||
|
||||
|
||||
public static string FullTrim(this string value)
|
||||
{
|
||||
if (String.IsNullOrEmpty(value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
return replaceMultipleSpacesRegex.Replace(value, SPACE).Trim();
|
||||
}
|
||||
|
||||
|
||||
public static string EnsureStartsWith(this string input, string toStartWith)
|
||||
{
|
||||
if (input.StartsWith(toStartWith)) return input;
|
||||
return toStartWith + input.TrimStart(toStartWith);
|
||||
}
|
||||
|
||||
|
||||
public static string EnsureStartsWith(this string input, char value)
|
||||
{
|
||||
return input.StartsWith(value.ToString(CultureInfo.InvariantCulture)) ? input : value + input;
|
||||
}
|
||||
|
||||
|
||||
public static string EnsureEndsWith(this string input, char value)
|
||||
{
|
||||
return input.EndsWith(value.ToString(CultureInfo.InvariantCulture)) ? input : input + value;
|
||||
}
|
||||
|
||||
|
||||
public static string EnsureEndsWith(this string input, string toEndWith)
|
||||
{
|
||||
return input.EndsWith(toEndWith.ToString(CultureInfo.InvariantCulture)) ? input : input + toEndWith;
|
||||
}
|
||||
|
||||
|
||||
public static string EnsureSurroundedWith(this string input, char toSurroundWith)
|
||||
{
|
||||
return input.EnsureStartsWith(toSurroundWith).EnsureEndsWith(toSurroundWith);
|
||||
}
|
||||
|
||||
|
||||
public static string Or(this string value, string fallback)
|
||||
{
|
||||
return String.IsNullOrWhiteSpace(value) ? fallback : value;
|
||||
}
|
||||
|
||||
|
||||
public static string TrimEnd(this string value, string forRemoving)
|
||||
{
|
||||
if (String.IsNullOrEmpty(value)) return value;
|
||||
if (String.IsNullOrEmpty(forRemoving)) return value;
|
||||
|
||||
while (value.EndsWith(forRemoving, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
value = value.Remove(value.LastIndexOf(forRemoving, StringComparison.InvariantCultureIgnoreCase));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
public static string TrimStart(this string value, string forRemoving)
|
||||
{
|
||||
if (String.IsNullOrEmpty(value)) return value;
|
||||
if (String.IsNullOrEmpty(forRemoving)) return value;
|
||||
|
||||
while (value.StartsWith(forRemoving, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
value = value.Substring(forRemoving.Length);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
public static bool HasValue(this string input)
|
||||
{
|
||||
return !String.IsNullOrWhiteSpace(input);
|
||||
}
|
||||
|
||||
|
||||
public static bool IsNullOrEmpty(this string input)
|
||||
{
|
||||
return String.IsNullOrEmpty(input);
|
||||
}
|
||||
|
||||
|
||||
public static bool IsNullOrWhiteSpace(this string input)
|
||||
{
|
||||
return String.IsNullOrWhiteSpace(input);
|
||||
}
|
||||
|
||||
public static string ToCamelCase(this string input)
|
||||
{
|
||||
if (!String.IsNullOrEmpty(input) && input.Length > 1)
|
||||
{
|
||||
return Char.ToLowerInvariant(input[0]) + input.Substring(1);
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
public static string ToPascalCase(this string input)
|
||||
{
|
||||
if (!String.IsNullOrEmpty(input) && input.Length > 1)
|
||||
{
|
||||
return Char.ToUpperInvariant(input[0]) + input.Substring(1);
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
public static string ToCamelCaseId(this string input)
|
||||
{
|
||||
if (String.IsNullOrEmpty(input))
|
||||
{
|
||||
return input;
|
||||
}
|
||||
|
||||
if (input.Length < 2)
|
||||
{
|
||||
return input.ToLowerInvariant();
|
||||
}
|
||||
|
||||
string[] parts = input.Split('.');
|
||||
|
||||
return String.Join(".", parts.Select(x => x.ToCamelCase()));
|
||||
}
|
||||
|
||||
|
||||
public static string ToPascalCaseId(this string input)
|
||||
{
|
||||
if (String.IsNullOrEmpty(input))
|
||||
{
|
||||
return input;
|
||||
}
|
||||
|
||||
if (input.Length < 2)
|
||||
{
|
||||
return input.ToUpperInvariant();
|
||||
}
|
||||
|
||||
string[] parts = input.Split('.');
|
||||
|
||||
return String.Join(".", parts.Select(x => x.ToPascalCase()));
|
||||
}
|
||||
|
||||
|
||||
public static string RemoveNewLines(this string input)
|
||||
{
|
||||
if (String.IsNullOrEmpty(input))
|
||||
{
|
||||
return input;
|
||||
}
|
||||
|
||||
return newLineCharsRegex.Replace(input, String.Empty).Trim();
|
||||
}
|
||||
|
||||
|
||||
public static string Shorten(this string input, int maxLength)
|
||||
{
|
||||
if (maxLength < 4)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("maxLength", "Has to be at least 4 characters long");
|
||||
}
|
||||
|
||||
if (String.IsNullOrEmpty(input) || input.Length <= maxLength)
|
||||
{
|
||||
return input;
|
||||
}
|
||||
|
||||
return input.Substring(0, maxLength - 3) + "...";
|
||||
}
|
||||
|
||||
|
||||
public static string ReplaceFirst(this string text, string search, string replace)
|
||||
{
|
||||
int pos = text.IndexOf(search);
|
||||
if (pos < 0)
|
||||
{
|
||||
return text;
|
||||
}
|
||||
return text.Substring(0, pos) + replace + text.Substring(pos + search.Length);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace zero.FileStorage;
|
||||
|
||||
public class FileResult
|
||||
{
|
||||
public string Filename { get; set; }
|
||||
|
||||
public string Path { get; set; }
|
||||
|
||||
public string ContentType { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace zero.FileStorage;
|
||||
|
||||
public enum FileSizeNotation
|
||||
{
|
||||
/// <summary>
|
||||
/// Multiply by 1.000, as specified by the International System of Units (e.g. kB, MB, GB)
|
||||
/// </summary>
|
||||
SI = 0,
|
||||
/// <summary>
|
||||
/// Binary notation with a multiplier of 1.024 (e.g. KiB, MiB, GiB)
|
||||
/// </summary>
|
||||
IEC = 1,
|
||||
/// <summary>
|
||||
/// Binary notation with a multiplier of 1.024 (e.g. KB, MB, GB).
|
||||
/// Ends with GB, there is no TB, ...
|
||||
/// </summary>
|
||||
JEDEC = 2
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace zero.FileStorage;
|
||||
|
||||
public class FileSystemException : Exception
|
||||
{
|
||||
public FileSystemException() { }
|
||||
|
||||
public FileSystemException(string message) : base(message) { }
|
||||
|
||||
public FileSystemException(string message, Exception innerException) : base(message, innerException) { }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace zero.FileStorage;
|
||||
|
||||
public class FileSystemOptions
|
||||
{
|
||||
public string ZeroAssetsPath { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
namespace zero.FileStorage;
|
||||
|
||||
public interface IFileMeta
|
||||
{
|
||||
/// <summary>
|
||||
/// Full path of the item within the file system
|
||||
/// </summary>
|
||||
string Path { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Absolute path of the item
|
||||
/// </summary>
|
||||
string AbsolutePath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Name of the item
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Full path to the directory containing the item
|
||||
/// </summary>
|
||||
string DirectoryPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Length of the file in bytes
|
||||
/// </summary>
|
||||
long Length { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Date when the item was last modified
|
||||
/// </summary>
|
||||
DateTimeOffset LastModifiedDate { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this item is directory or a file
|
||||
/// </summary>
|
||||
bool IsDirectory { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional properties
|
||||
/// </summary>
|
||||
Dictionary<string, object> Properties { get; }
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using System.IO;
|
||||
|
||||
namespace zero.FileStorage;
|
||||
|
||||
public interface IFileSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// Map a relative path to an absolute internal path
|
||||
/// </summary>
|
||||
string Map(string path);
|
||||
|
||||
/// <summary>
|
||||
/// Map an internal path to a public accessible path
|
||||
/// </summary>
|
||||
string MapToPublicPath(string path);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a file or directory at the given path already exists
|
||||
/// </summary>
|
||||
Task<bool> Exists(string path, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get information of a file
|
||||
/// </summary>
|
||||
Task<IFileMeta> GetFileInfo(string path, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a readable stream for a file
|
||||
/// </summary>
|
||||
Task<Stream> StreamFile(string path, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a file at the given path
|
||||
/// </summary>
|
||||
Task CreateFile(string path, Stream fileStream, bool overwrite = false, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a file at the given path. This method returns if the file does not exist.
|
||||
/// </summary>
|
||||
Task DeleteFile(string path, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Moves a file to another path (can also be used to rename resources)
|
||||
/// </summary>
|
||||
Task MoveFile(string oldPath, string newPath, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Copies a file to another path
|
||||
/// </summary>
|
||||
Task CopyFile(string oldPath, string newPath, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get information of a directory
|
||||
/// </summary>
|
||||
Task<IFileMeta> GetDirectoryInfo(string path, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get all items within a directory
|
||||
/// </summary>
|
||||
IAsyncEnumerable<IFileMeta> GetDirectoryContent(string path = null, bool recursive = false, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Tries to create a directory at the given path. This method returns if the directory already exists.
|
||||
/// </summary>
|
||||
Task CreateDirectory(string path, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a directory at the given path. This method returns if the directory does not exist.
|
||||
/// </summary>
|
||||
Task DeleteDirectory(string path, bool recursive = false, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace zero.FileStorage;
|
||||
|
||||
public class Paths : IPaths
|
||||
{
|
||||
public string WebRoot { get; set; }
|
||||
|
||||
public string ContentRoot { get; set; }
|
||||
|
||||
public string SecureRoot { get; set; }
|
||||
|
||||
public string Media { get; set; }
|
||||
|
||||
protected IWebHostEnvironment Env { get; set; }
|
||||
|
||||
public const string MEDIA_FOLDER = "Uploads";
|
||||
|
||||
public const char PATH_SEPARATOR = '/';
|
||||
|
||||
static char[] InvalidFilenameChars = null;
|
||||
|
||||
const char REPLACEMENT_CHAR = '-';
|
||||
|
||||
FileExtensionContentTypeProvider FileExtensionContentTypeProvider { get; set; }
|
||||
|
||||
static readonly Dictionary<char, string> Replacements = new()
|
||||
{
|
||||
{ 'ä', "ae" },
|
||||
{ 'ü', "ue" },
|
||||
{ 'ö', "oe" },
|
||||
{ 'ß', "ss" }
|
||||
};
|
||||
|
||||
|
||||
public Paths(IWebHostEnvironment env)
|
||||
{
|
||||
Env = env;
|
||||
WebRoot = env.WebRootPath;
|
||||
ContentRoot = env.ContentRootPath;
|
||||
SecureRoot = Path.Combine(ContentRoot, "wwwroot.secure");
|
||||
Media = Path.Combine(WebRoot, MEDIA_FOLDER);
|
||||
FileExtensionContentTypeProvider = new();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Combine a path
|
||||
/// </summary>
|
||||
public string Combine(params string[] paths)
|
||||
{
|
||||
return Path.Combine(paths);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Map a path
|
||||
/// </summary>
|
||||
public string Map(string path)
|
||||
{
|
||||
return Path.Combine(WebRoot, path);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Map a secure path
|
||||
/// </summary>
|
||||
public string MapSecure(string path)
|
||||
{
|
||||
return Path.Combine(SecureRoot, path);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Map a path
|
||||
/// </summary>
|
||||
public string Map(params string[] paths)
|
||||
{
|
||||
return Path.Combine(WebRoot, Path.Combine(paths));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Map a secure path
|
||||
/// </summary>
|
||||
public string MapSecure(params string[] paths)
|
||||
{
|
||||
return Path.Combine(SecureRoot, Path.Combine(paths));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Create a directory if it does not exist yet
|
||||
/// </summary>
|
||||
public void Create(string directory)
|
||||
{
|
||||
if (!Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get content type for a filename
|
||||
/// </summary>
|
||||
public string GetContentType(string filename, string fallback = "application/octet-stream")
|
||||
{
|
||||
if (filename == null || !FileExtensionContentTypeProvider.TryGetContentType(Path.GetFileName(filename), out string contentType))
|
||||
{
|
||||
contentType = fallback;
|
||||
}
|
||||
return contentType;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes a filename and removes invalid chars
|
||||
/// </summary>
|
||||
public string ToFilename(string value)
|
||||
{
|
||||
if (String.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
// lowercase for string
|
||||
value = value.ToLower();
|
||||
|
||||
StringBuilder sb = new(value.Length);
|
||||
|
||||
var invalids = InvalidFilenameChars ??= Path.GetInvalidFileNameChars();
|
||||
bool changed = false;
|
||||
|
||||
for (int i = 0; i < value.Length; i++)
|
||||
{
|
||||
char c = value[i];
|
||||
|
||||
if (Replacements.ContainsKey(c))
|
||||
{
|
||||
changed = true;
|
||||
sb.Append(Replacements[c]);
|
||||
}
|
||||
else if (invalids.Contains(c))
|
||||
{
|
||||
changed = true;
|
||||
sb.Append(REPLACEMENT_CHAR);
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(c);
|
||||
}
|
||||
}
|
||||
|
||||
if (sb.Length == 0)
|
||||
{
|
||||
return "_";
|
||||
}
|
||||
|
||||
return changed ? sb.ToString() : value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface IPaths
|
||||
{
|
||||
string ContentRoot { get; set; }
|
||||
|
||||
string WebRoot { get; set; }
|
||||
|
||||
string SecureRoot { get; set; }
|
||||
|
||||
string Media { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Combine a path
|
||||
/// </summary>
|
||||
string Combine(params string[] paths);
|
||||
|
||||
/// <summary>
|
||||
/// Map a path
|
||||
/// </summary>
|
||||
string Map(string path);
|
||||
|
||||
/// <summary>
|
||||
/// Map a secure path
|
||||
/// </summary>
|
||||
string MapSecure(string path);
|
||||
|
||||
/// <summary>
|
||||
/// Map a path
|
||||
/// </summary>
|
||||
string Map(params string[] paths);
|
||||
|
||||
/// <summary>
|
||||
/// Map a secure path
|
||||
/// </summary>
|
||||
string MapSecure(params string[] paths);
|
||||
|
||||
/// <summary>
|
||||
/// Create a directory if it does not exist yet
|
||||
/// </summary>
|
||||
void Create(string directory);
|
||||
|
||||
/// <summary>
|
||||
/// Get content type for a filename
|
||||
/// </summary>
|
||||
string GetContentType(string filename, string fallback = "application/octet-stream");
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes a filename and removes invalid chars
|
||||
/// </summary>
|
||||
string ToFilename(string value);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
|
||||
namespace zero.FileStorage;
|
||||
|
||||
public class PhysicalFileMeta : IFileMeta
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string Name { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Path { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string AbsolutePath { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DirectoryPath { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public long Length { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public DateTimeOffset LastModifiedDate { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsDirectory { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Dictionary<string, object> Properties { get; }
|
||||
|
||||
|
||||
public PhysicalFileMeta(IFileInfo fileInfo, string path)
|
||||
{
|
||||
Path = path;
|
||||
AbsolutePath = fileInfo.PhysicalPath;
|
||||
Name = fileInfo.Name;
|
||||
DirectoryPath = path.Substring(0, path.Length - fileInfo.Name.Length).TrimEnd('/');
|
||||
LastModifiedDate = fileInfo.LastModified;
|
||||
Length = fileInfo.Length;
|
||||
IsDirectory = fileInfo.IsDirectory;
|
||||
Properties = new();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
using Microsoft.Extensions.FileProviders.Physical;
|
||||
using System.IO;
|
||||
|
||||
namespace zero.FileStorage;
|
||||
|
||||
public class PhysicalFileSystem : IFileSystem
|
||||
{
|
||||
readonly string _root;
|
||||
|
||||
|
||||
public PhysicalFileSystem(string root)
|
||||
{
|
||||
_root = root;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual string Map(string path)
|
||||
{
|
||||
return ResolvePath(path);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual string MapToPublicPath(string path)
|
||||
{
|
||||
return path.EnsureStartsWith('/');
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Task<bool> Exists(string path, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
string resolvedPath = ResolvePath(path);
|
||||
return Task.FromResult(File.Exists(resolvedPath) || Directory.Exists(resolvedPath));
|
||||
}
|
||||
catch (Exception ex) when (ex is not FileSystemException)
|
||||
{
|
||||
throw new FileSystemException($"Could not get file exists info for path '{path}'", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Task<IFileMeta> GetFileInfo(string path, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
string resolvedPath = ResolvePath(path);
|
||||
|
||||
PhysicalFileInfo fileInfo = new(new FileInfo(resolvedPath));
|
||||
|
||||
if (!fileInfo.Exists)
|
||||
{
|
||||
return Task.FromResult<IFileMeta>(default);
|
||||
}
|
||||
|
||||
return Task.FromResult<IFileMeta>(new PhysicalFileMeta(fileInfo, path));
|
||||
}
|
||||
catch (Exception ex) when (ex is not FileSystemException)
|
||||
{
|
||||
throw new FileSystemException($"Could not get file info for path '{path}'", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Task<Stream> StreamFile(string path, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
string resolvedPath = ResolvePath(path);
|
||||
|
||||
if (resolvedPath.IsNullOrEmpty() || !File.Exists(resolvedPath))
|
||||
{
|
||||
throw new FileSystemException($"The path '{path}' does not exist in the file system");
|
||||
}
|
||||
|
||||
return Task.FromResult<Stream>(File.OpenRead(resolvedPath));
|
||||
}
|
||||
catch (Exception ex) when (ex is not FileSystemException)
|
||||
{
|
||||
throw new FileSystemException($"Could not get file stream for path '{path}'", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual async Task CreateFile(string path, Stream fileStream, bool overwrite = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
string resolvedPath = ResolvePath(path);
|
||||
|
||||
if ((!overwrite && File.Exists(resolvedPath)) || Directory.Exists(resolvedPath))
|
||||
{
|
||||
throw new FileSystemException($"A file/directory at the path '{path}' already existsin the file system");
|
||||
}
|
||||
|
||||
string directoryPath = Path.GetDirectoryName(resolvedPath);
|
||||
Directory.CreateDirectory(directoryPath);
|
||||
|
||||
FileInfo fileInfo = new(resolvedPath);
|
||||
using var outputStream = fileInfo.Create();
|
||||
await fileStream.CopyToAsync(outputStream, cancellationToken);
|
||||
}
|
||||
catch (Exception ex) when (ex is not FileSystemException)
|
||||
{
|
||||
throw new FileSystemException($"Could not get file stream for path '{path}'", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Task DeleteFile(string path, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
string resolvedPath = ResolvePath(path);
|
||||
File.Delete(resolvedPath);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
catch (Exception ex) when (ex is not FileSystemException)
|
||||
{
|
||||
throw new FileSystemException($"Could not delete file for path '{path}'", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Task MoveFile(string oldPath, string newPath, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
string resolvedOldPath = ResolvePath(oldPath);
|
||||
string resolvedNewPath = ResolvePath(newPath);
|
||||
|
||||
if (!File.Exists(resolvedOldPath))
|
||||
{
|
||||
throw new FileSystemException($"The path '{oldPath}' does not exist in the file system");
|
||||
}
|
||||
|
||||
if (File.Exists(resolvedNewPath) || Directory.Exists(resolvedNewPath))
|
||||
{
|
||||
throw new FileSystemException($"A file/directory at the path '{newPath}' already exists in the file system");
|
||||
}
|
||||
|
||||
File.Move(resolvedOldPath, resolvedNewPath);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
catch (Exception ex) when (ex is not FileSystemException)
|
||||
{
|
||||
throw new FileSystemException($"Could not move file from path '{oldPath}' to '{newPath}'", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Task CopyFile(string oldPath, string newPath, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
string resolvedOldPath = ResolvePath(oldPath);
|
||||
string resolvedNewPath = ResolvePath(newPath);
|
||||
|
||||
if (!File.Exists(resolvedOldPath))
|
||||
{
|
||||
throw new FileSystemException($"The path '{oldPath}' does not exist in the file system");
|
||||
}
|
||||
|
||||
if (File.Exists(resolvedNewPath) || Directory.Exists(resolvedNewPath))
|
||||
{
|
||||
throw new FileSystemException($"A file/directory at the path '{newPath}' already exists in the file system");
|
||||
}
|
||||
|
||||
File.Copy(resolvedOldPath, resolvedNewPath);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
catch (Exception ex) when (ex is not FileSystemException)
|
||||
{
|
||||
throw new FileSystemException($"Could not copy file from path '{oldPath}' to '{newPath}'", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Task<IFileMeta> GetDirectoryInfo(string path, CancellationToken cancellationToken = default) => GetFileInfo(path, cancellationToken);
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual IAsyncEnumerable<IFileMeta> GetDirectoryContent(string path = null, bool recursive = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
string resolvedPath = ResolvePath(path);
|
||||
List<IFileMeta> results = new();
|
||||
SearchOption searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
|
||||
|
||||
if (!Directory.Exists(resolvedPath))
|
||||
{
|
||||
return results.ToAsyncEnumerable();
|
||||
}
|
||||
|
||||
results.AddRange(Directory.GetDirectories(resolvedPath, "*", searchOption).Select(f =>
|
||||
{
|
||||
PhysicalDirectoryInfo fileSystemInfo = new(new DirectoryInfo(f));
|
||||
return new PhysicalFileMeta(fileSystemInfo, ResolvePath(f.Substring(_root.Length)));
|
||||
}));
|
||||
|
||||
results.AddRange(Directory.GetFiles(resolvedPath, "*", searchOption).Select(f =>
|
||||
{
|
||||
PhysicalFileInfo fileSystemInfo = new(new FileInfo(f));
|
||||
return new PhysicalFileMeta(fileSystemInfo, ResolvePath(f.Substring(_root.Length)));
|
||||
}));
|
||||
|
||||
return results.ToAsyncEnumerable();
|
||||
}
|
||||
catch (Exception ex) when (ex is not FileSystemException)
|
||||
{
|
||||
throw new FileSystemException($"Could not get directory content for path '{path}'", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Task CreateDirectory(string path, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
string resolvedPath = ResolvePath(path);
|
||||
|
||||
if (File.Exists(resolvedPath))
|
||||
{
|
||||
throw new FileSystemException($"A file at the path '{path}' already exists in the file system");
|
||||
}
|
||||
|
||||
if (!Directory.Exists(resolvedPath))
|
||||
{
|
||||
Directory.CreateDirectory(resolvedPath);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
catch (Exception ex) when (ex is not FileSystemException)
|
||||
{
|
||||
throw new FileSystemException($"Could not create directory at path '{path}'", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Task DeleteDirectory(string path, bool recursive = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
string resolvedPath = ResolvePath(path);
|
||||
Directory.Delete(resolvedPath, recursive);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
catch (Exception ex) when (ex is not FileSystemException)
|
||||
{
|
||||
throw new FileSystemException($"Could not delete directory at path '{path}'", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates a fully-usable absolte path from the given path
|
||||
/// </summary>
|
||||
protected virtual string ResolvePath(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (path.IsNullOrWhiteSpace())
|
||||
{
|
||||
return _root;
|
||||
}
|
||||
|
||||
// normalize path
|
||||
path = path.Replace('\\', '/').FullTrim().Trim('/');
|
||||
|
||||
string physicalPath = Path.Combine(_root, path);
|
||||
string rootPath = Path.GetFullPath(_root);
|
||||
|
||||
// do not allow paths which are outside this file system
|
||||
// the boundaries are set be the basePath which is assigned in the file system constructor
|
||||
if (!Path.GetFullPath(physicalPath).StartsWith(rootPath, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
throw new FileSystemException($"The path '{path}' is outside the file system");
|
||||
}
|
||||
|
||||
return physicalPath;
|
||||
}
|
||||
catch (FileSystemException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new FileSystemException($"Could not resolve path '{path}'", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace zero.FileStorage;
|
||||
|
||||
public class WebRootFileSystem : PhysicalFileSystem, IWebRootFileSystem
|
||||
{
|
||||
protected string PublicPathPrefix { get; }
|
||||
|
||||
|
||||
public WebRootFileSystem(string root, string publicPathPrefix) : base(root)
|
||||
{
|
||||
PublicPathPrefix = (publicPathPrefix ?? String.Empty).EnsureEndsWith('/');
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string MapToPublicPath(string path)
|
||||
{
|
||||
if (path.IsNullOrEmpty())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return PublicPathPrefix + path.TrimStart('/');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface IWebRootFileSystem : IFileSystem
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace zero.FileStorage;
|
||||
|
||||
internal class ZeroFileStorageModule : ZeroModule
|
||||
{
|
||||
public override void ConfigureServices(IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddScoped<IPaths>(factory => new Paths(factory.GetService<IWebHostEnvironment>()));
|
||||
|
||||
services.AddOptions<FileSystemOptions>().Bind(configuration.GetSection("Zero:FileSystem")).Configure(opts =>
|
||||
{
|
||||
opts.ZeroAssetsPath = "zero";
|
||||
});
|
||||
|
||||
services.AddSingleton<IWebRootFileSystem, WebRootFileSystem>(svc =>
|
||||
{
|
||||
IWebHostEnvironment env = svc.GetRequiredService<IWebHostEnvironment>();
|
||||
return new(env.WebRootPath, null);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using FluentValidation;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Raven.Client.Documents;
|
||||
using Raven.Client.Documents.Session;
|
||||
using System.Globalization;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace zero.Localization;
|
||||
|
||||
public class CultureResolver : ICultureResolver
|
||||
{
|
||||
protected ILogger<CultureResolver> Logger { get; private set; }
|
||||
|
||||
|
||||
public CultureResolver(ILogger<CultureResolver> logger)
|
||||
{
|
||||
Logger = logger;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<CultureInfo> Resolve(IZeroContext context)
|
||||
{
|
||||
//var session = context.Store.Session();
|
||||
//Language language = await session.Query<Language>().FirstOrDefaultAsync();
|
||||
string isoCode = "de-AT";
|
||||
|
||||
try
|
||||
{
|
||||
CultureInfo culture = CultureInfo.CreateSpecificCulture(isoCode);
|
||||
|
||||
if (culture.ThreeLetterISOLanguageName.IsNullOrEmpty())
|
||||
{
|
||||
throw new Exception("ThreeLetterISOLanguageName is empty");
|
||||
}
|
||||
|
||||
CultureInfo.CurrentCulture = culture;
|
||||
CultureInfo.CurrentUICulture = culture;
|
||||
ValidatorOptions.Global.LanguageManager.Culture = culture;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Could not create culture from Language code {code}", isoCode);
|
||||
return CultureInfo.CurrentCulture;
|
||||
}
|
||||
|
||||
return CultureInfo.CurrentCulture;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface ICultureResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves the current application from either the backoffice user (in case it is backoffice request)
|
||||
/// or the domain (in case it is frontend request).
|
||||
/// </summary>
|
||||
Task<CultureInfo> Resolve(IZeroContext context);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace zero.Localization;
|
||||
|
||||
public class CultureService : ICultureService
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public List<Culture> GetAllCultures(params string[] codes)
|
||||
{
|
||||
return CultureInfo.GetCultures(CultureTypes.AllCultures)
|
||||
.Where(x => !x.Name.IsNullOrWhiteSpace())
|
||||
.Select(x => new CultureInfo(x.Name))
|
||||
.Where(x => codes.Length == 0 || codes.Contains(x.Name, StringComparer.InvariantCultureIgnoreCase))
|
||||
.OrderBy(x => x.DisplayName)
|
||||
.Select(x => new Culture()
|
||||
{
|
||||
Code = x.Name,
|
||||
Name = x.DisplayName
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface ICultureService
|
||||
{
|
||||
/// <summary>
|
||||
/// Get all available cultures
|
||||
/// </summary>
|
||||
List<Culture> GetAllCultures(params string[] codes);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace zero.Localization;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Field, AllowMultiple = false)]
|
||||
public class LocalizeAttribute : Attribute
|
||||
{
|
||||
public string Key;
|
||||
|
||||
public LocalizeAttribute(string key)
|
||||
{
|
||||
Key = key;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reflection;
|
||||
|
||||
namespace zero.Localization;
|
||||
|
||||
public class Localizer : ILocalizer
|
||||
{
|
||||
protected ConcurrentDictionary<string, string> Cache { get; private set; } = new();
|
||||
|
||||
protected IZeroStore Store { get; private set; }
|
||||
|
||||
public Localizer(IZeroStore store)
|
||||
{
|
||||
Store = store;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Text(string key) => Text(key, null);
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Text(string key, Dictionary<string, string> tokens)
|
||||
{
|
||||
if (key.IsNullOrEmpty())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!Cache.TryGetValue(key, out string value))
|
||||
{
|
||||
Translation translation = LoadTranslation(key);
|
||||
|
||||
if (translation == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
value = translation.Value;
|
||||
Cache.TryAdd(key, value);
|
||||
}
|
||||
|
||||
if (tokens != null)
|
||||
{
|
||||
value = TokenReplacement.Apply(value, tokens);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Text<T>(T enumValue) where T : Enum => Text(enumValue, null);
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Text<T>(T enumValue, Dictionary<string, string> tokens) where T : Enum
|
||||
{
|
||||
Type type = enumValue.GetType();
|
||||
MemberInfo memInfo = type.GetMember(enumValue.ToString())[0];
|
||||
return Text(memInfo.GetCustomAttribute<LocalizeAttribute>()?.Key, tokens);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Maybe(string key) => Maybe(key, null);
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Maybe(string key, Dictionary<string, string> tokens)
|
||||
{
|
||||
return key.IsNullOrEmpty() || !key.StartsWith("@") ? key : Text(key.Substring(1), tokens);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get translation from database or any other source
|
||||
/// </summary>
|
||||
protected virtual Translation LoadTranslation(string key)
|
||||
{
|
||||
return Store.Session().Synchronous.Query<Translation>().FirstOrDefault(x => x.Key == key);
|
||||
}
|
||||
}
|
||||
|
||||
public interface ILocalizer
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
string Text(string key);
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
string Text(string key, Dictionary<string, string> tokens);
|
||||
|
||||
/// <summary>
|
||||
/// Get a text string from a [Localize] attribute
|
||||
/// </summary>
|
||||
string Text<T>(T enumValue) where T : Enum;
|
||||
|
||||
/// <summary>
|
||||
/// Get a text string from a [Localize] attribute
|
||||
/// </summary>
|
||||
string Text<T>(T enumValue, Dictionary<string, string> tokens) where T : Enum;
|
||||
|
||||
/// <summary>
|
||||
/// Only tries to resolve the key when it is prefixed with an @
|
||||
/// </summary>
|
||||
string Maybe(string key);
|
||||
|
||||
/// <summary>
|
||||
/// Only tries to resolve the key when it is prefixed with an @
|
||||
/// </summary>
|
||||
string Maybe(string key, Dictionary<string, string> tokens);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using Microsoft.AspNetCore.Html;
|
||||
|
||||
namespace zero.Localization;
|
||||
|
||||
public static class LocalizerExtensions
|
||||
{
|
||||
public static IHtmlContent Html(this ILocalizer localizer, string key)
|
||||
{
|
||||
string value = localizer.Text(key);
|
||||
|
||||
HtmlContentBuilder builder = new();
|
||||
builder.SetHtmlContent(value);
|
||||
return builder;
|
||||
}
|
||||
|
||||
|
||||
public static IHtmlContent Html(this ILocalizer localizer, string key, Dictionary<string, string> tokens)
|
||||
{
|
||||
string value = localizer.Text(key, tokens);
|
||||
|
||||
HtmlContentBuilder builder = new();
|
||||
builder.SetHtmlContent(value);
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace zero.Localization;
|
||||
|
||||
public class Culture
|
||||
{
|
||||
public string Code { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace zero.Localization;
|
||||
|
||||
[RavenCollection("Translations")]
|
||||
public class Translation : ZeroEntity, IAlwaysActive
|
||||
{
|
||||
public Translation()
|
||||
{
|
||||
IsActive = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Value of the translation
|
||||
/// </summary>
|
||||
public string Value { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Display + input type
|
||||
/// </summary>
|
||||
public TranslationDisplay Display { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public enum TranslationDisplay
|
||||
{
|
||||
Text = 0,
|
||||
HTML = 1
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using FluentValidation;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace zero.Localization;
|
||||
|
||||
internal class ZeroLocalizationModule : ZeroModule
|
||||
{
|
||||
public override void ConfigureServices(IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddScoped<ICultureResolver, CultureResolver>();
|
||||
services.AddScoped<ICultureService, CultureService>();
|
||||
services.AddScoped<ILocalizer, Localizer>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace zero.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Entities decorated with this interface are always set to IsActive=true
|
||||
/// </summary>
|
||||
public interface IAlwaysActive
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the entity is visible in the frontend
|
||||
/// </summary>
|
||||
bool IsActive { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace zero.Models;
|
||||
|
||||
public interface ISupportsFlavors
|
||||
{
|
||||
/// <summary>
|
||||
/// Id of the entity
|
||||
/// </summary>
|
||||
string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Alias of the used flavor
|
||||
/// </summary>
|
||||
string Flavor { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace zero.Models;
|
||||
|
||||
public interface ISupportsRouting
|
||||
{
|
||||
/// <summary>
|
||||
/// Id of the entity
|
||||
/// </summary>
|
||||
string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Unique hash for this entity (primarily used for routing)
|
||||
/// </summary>
|
||||
string Hash { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace zero.Models;
|
||||
|
||||
public interface ISupportsSorting
|
||||
{
|
||||
/// <summary>
|
||||
/// Sort order
|
||||
/// </summary>
|
||||
uint Sort { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace zero.Models;
|
||||
|
||||
public interface ISupportsTrees
|
||||
{
|
||||
/// <summary>
|
||||
/// Id of the entity
|
||||
/// </summary>
|
||||
string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Parent id
|
||||
/// </summary>
|
||||
string ParentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sort order
|
||||
/// </summary>
|
||||
uint Sort { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
namespace zero.Models;
|
||||
|
||||
public class Paged<T> : Paged
|
||||
{
|
||||
public IList<T> Items { get; set; } = new List<T>();
|
||||
|
||||
public Paged(IList<T> items, long totalItems, long pageNumber, long pageSize) : base(totalItems, pageNumber, pageSize)
|
||||
{
|
||||
Items = items;
|
||||
}
|
||||
|
||||
public Paged(long totalItems, long pageNumber, long pageSize) : base(totalItems, pageNumber, pageSize) { }
|
||||
|
||||
public Paged<TTarget> MapTo<TTarget>(Func<T, TTarget> convertItem)
|
||||
{
|
||||
return new Paged<TTarget>(Items.Select(x => convertItem(x)).Where(x => x != null).ToList(), TotalItems, Page, PageSize);
|
||||
}
|
||||
|
||||
public override IEnumerable GetItems() => Items;
|
||||
}
|
||||
|
||||
|
||||
public abstract class Paged
|
||||
{
|
||||
public long Page { get; protected set; }
|
||||
|
||||
public long PageSize { get; protected set; }
|
||||
|
||||
public long TotalPages { get; protected set; }
|
||||
|
||||
public long TotalItems { get; protected set; }
|
||||
|
||||
public bool HasMore { get; protected set; }
|
||||
|
||||
public Dictionary<string, string> Properties { get; set; } = new();
|
||||
|
||||
|
||||
public Paged(long totalItems, long page, long pageSize)
|
||||
{
|
||||
TotalItems = totalItems;
|
||||
Page = page;
|
||||
PageSize = pageSize;
|
||||
|
||||
if (pageSize > 0)
|
||||
{
|
||||
TotalPages = (long)Math.Ceiling(totalItems / (decimal)pageSize);
|
||||
}
|
||||
else
|
||||
{
|
||||
TotalPages = 1;
|
||||
}
|
||||
|
||||
HasMore = TotalPages > Page;
|
||||
}
|
||||
|
||||
public int GetSkipSize()
|
||||
{
|
||||
if (Page > 0 && PageSize > 0)
|
||||
{
|
||||
return Convert.ToInt32((Page - 1) * PageSize);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public virtual IEnumerable GetItems() => Array.Empty<object>();
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
using FluentValidation.Results;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace zero.Models;
|
||||
|
||||
|
||||
[DataContract(Name = "result", Namespace = "")]
|
||||
public class Result
|
||||
{
|
||||
[DataMember(Name = "success")]
|
||||
public bool IsSuccess { get; set; }
|
||||
|
||||
[DataMember(Name = "errors")]
|
||||
public List<ResultError> Errors { get; set; } = new();
|
||||
|
||||
public Result() { }
|
||||
|
||||
public Result(ValidationResult validation)
|
||||
{
|
||||
IsSuccess = validation.IsValid;
|
||||
Errors = validation.Errors.Select(x => new ResultError()
|
||||
{
|
||||
Property = x.PropertyName,
|
||||
Message = x.ErrorMessage
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public virtual object GetModel() => null;
|
||||
|
||||
public static Result Maybe(bool isSuccess) => isSuccess ? Success() : Fail();
|
||||
|
||||
public static Result Success() => new() { IsSuccess = true };
|
||||
|
||||
public static Result Fail() => new() { };
|
||||
|
||||
public static Result Fail(string property, string message)
|
||||
{
|
||||
Result result = new();
|
||||
result.Errors.Add(new(property, message));
|
||||
return result;
|
||||
}
|
||||
|
||||
public static Result Fail(string message)
|
||||
{
|
||||
Result result = new();
|
||||
result.AddError(message);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static Result Fail(ValidationResult validation) => new(validation);
|
||||
|
||||
public static Result Fail(ResultError error) => Fail(error.Property, error.Message);
|
||||
|
||||
public static Result Fail(IEnumerable<ResultError> errors) => new() { IsSuccess = !errors.Any(), Errors = errors.ToList() };
|
||||
}
|
||||
|
||||
|
||||
public class Result<T> : Result
|
||||
{
|
||||
[DataMember(Name = "model")]
|
||||
public T Model { get; set; }
|
||||
|
||||
public Result() : base() { }
|
||||
|
||||
public Result(ValidationResult validation) : base(validation) { }
|
||||
|
||||
|
||||
public new static Result<T> Success() => new() { IsSuccess = true };
|
||||
|
||||
public static Result<T> Success(T model) => new() { IsSuccess = true, Model = model };
|
||||
|
||||
public new static Result<T> Fail() => new() { };
|
||||
|
||||
public new static Result<T> Fail(string property, string message)
|
||||
{
|
||||
Result<T> result = new();
|
||||
result.Errors.Add(new(property, message));
|
||||
return result;
|
||||
}
|
||||
|
||||
public new static Result<T> Fail(string message)
|
||||
{
|
||||
Result<T> result = new();
|
||||
result.AddError(message);
|
||||
return result;
|
||||
}
|
||||
|
||||
public new static Result<T> Fail(ValidationResult validation) => new(validation);
|
||||
|
||||
public new static Result<T> Fail(ResultError error) => Fail(error.Property, error.Message);
|
||||
|
||||
public new static Result<T> Fail(IEnumerable<ResultError> errors) => new() { IsSuccess = !errors.Any(), Errors = errors.ToList() };
|
||||
|
||||
public override object GetModel() => Model;
|
||||
}
|
||||
|
||||
|
||||
[DataContract(Name = "resultError", Namespace = "")]
|
||||
public class ResultError
|
||||
{
|
||||
[DataMember(Name = "property")]
|
||||
public string Property { get; set; }
|
||||
|
||||
[DataMember(Name = "message")]
|
||||
public string Message { get; set; }
|
||||
|
||||
public ResultError() { }
|
||||
|
||||
public ResultError(string property, string message)
|
||||
{
|
||||
Property = property;
|
||||
Message = message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
namespace zero.Models;
|
||||
|
||||
public class UrlResult
|
||||
{
|
||||
public string Url { get; set; }
|
||||
|
||||
public string Domain { get; set; }
|
||||
|
||||
public UrlResult(string domain, string url)
|
||||
{
|
||||
Domain = domain;
|
||||
Url = url;
|
||||
}
|
||||
|
||||
public UrlResult(Uri domain, string url)
|
||||
{
|
||||
if (domain != null)
|
||||
{
|
||||
Domain = domain.Scheme + "://" + domain.Authority.TrimEnd("/");
|
||||
}
|
||||
Url = url;
|
||||
}
|
||||
}
|
||||
|
||||
public class UrlsResult
|
||||
{
|
||||
public string[] Urls { get; set; } = Array.Empty<string>();
|
||||
|
||||
public string Domain { get; set; }
|
||||
|
||||
public UrlsResult(string domain, params string[] urls)
|
||||
{
|
||||
Domain = domain;
|
||||
Urls = urls.Where(x => x.HasValue()).ToArray();
|
||||
}
|
||||
|
||||
public UrlsResult(Uri domain, params string[] urls)
|
||||
{
|
||||
if (domain != null)
|
||||
{
|
||||
Domain = domain.Scheme + "://" + domain.Authority.TrimEnd("/");
|
||||
}
|
||||
Urls = urls.Where(x => x.HasValue()).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class PreviewUrlResult
|
||||
{
|
||||
public string Url { get; set; }
|
||||
|
||||
public PreviewUrlResult(string url)
|
||||
{
|
||||
Url = url;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using Newtonsoft.Json;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace zero.Models;
|
||||
|
||||
[DebuggerDisplay("Id = {Id,nq}, Name = {Name}, Alias = {Alias}")]
|
||||
public class ZeroEntity : ZeroIdEntity, ISupportsDbConventions, ISupportsRouting, ISupportsFlavors, ISupportsSorting
|
||||
{
|
||||
/// <summary>
|
||||
/// Full name of the entity
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Alias (non-unique) which can be used in the frontend and URLs
|
||||
/// </summary>
|
||||
public string Alias { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A key which can be used to query this entity in code
|
||||
/// </summary>
|
||||
public string Key { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sort order
|
||||
/// </summary>
|
||||
public uint Sort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the entity is visible in the frontend
|
||||
/// </summary>
|
||||
public bool IsActive { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Unique hash for this entity (primarily used for routing)
|
||||
/// </summary>
|
||||
public string Hash { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Backoffice user who last modified this content
|
||||
/// </summary>
|
||||
public string LastModifiedById { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Date of last modification
|
||||
/// </summary>
|
||||
public DateTimeOffset LastModifiedDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Backoffice user who created this content
|
||||
/// </summary>
|
||||
public string CreatedById { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Date of creation
|
||||
/// </summary>
|
||||
public DateTimeOffset CreatedDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Alias of the used flavor (uses default if empty)
|
||||
/// </summary>
|
||||
public string Flavor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional properties for this entity
|
||||
/// </summary>
|
||||
public Dictionary<string, string> Properties { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// [Warning] This field is always empty when bound to the database.
|
||||
/// It is only filled in the app-code for routing.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public string Url { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace zero.Models;
|
||||
|
||||
public class ZeroIdEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Id of the entity
|
||||
/// </summary>
|
||||
public string Id { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
namespace zero.Models;
|
||||
|
||||
public class ZeroReference
|
||||
{
|
||||
public ZeroReference() { }
|
||||
|
||||
public ZeroReference(ZeroEntity entity)
|
||||
{
|
||||
Id = entity.Id;
|
||||
Name = entity.Name;
|
||||
}
|
||||
|
||||
public static ZeroReference From(ZeroEntity entity)
|
||||
{
|
||||
return entity == null ? null : new ZeroReference(entity);
|
||||
}
|
||||
|
||||
public static ZeroReference From<T>(T entity, Func<T, string> transform) where T : ZeroEntity
|
||||
{
|
||||
return entity == null ? null : new ZeroReference()
|
||||
{
|
||||
Id = entity.Id,
|
||||
Name = transform(entity)
|
||||
};
|
||||
}
|
||||
|
||||
public string Id { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace zero.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// Automatically generate ID with the specified length and insert it into this property on entity save
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
|
||||
public class GenerateIdAttribute : Attribute
|
||||
{
|
||||
public int? Length = null;
|
||||
|
||||
public GenerateIdAttribute(int length)
|
||||
{
|
||||
Length = length;
|
||||
}
|
||||
|
||||
public GenerateIdAttribute() { }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace zero.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// Triggers custom Raven conventions for database operations
|
||||
/// </summary>
|
||||
public interface ISupportsDbConventions { }
|
||||
@@ -0,0 +1,112 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace zero.Persistence;
|
||||
|
||||
public class IdGenerator
|
||||
{
|
||||
const string CHARS = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||
const string CHARS_COMPLEX = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-@#.:!?*";
|
||||
|
||||
private static Random random = new();
|
||||
|
||||
/// <summary>
|
||||
/// Create a new unique Id
|
||||
/// </summary>
|
||||
public static string Create(int length = -1, Charset charset = Charset.az09)
|
||||
{
|
||||
if (length < 1)
|
||||
{
|
||||
length = 12;
|
||||
}
|
||||
|
||||
string chars = charset == Charset.az09 ? CHARS : CHARS_COMPLEX;
|
||||
|
||||
return new string(Enumerable.Repeat(chars, length).Select(s => s[random.Next(s.Length)]).ToArray());
|
||||
|
||||
//if (length > 0)
|
||||
//{
|
||||
//return Convert.ToBase64String(Guid.NewGuid().ToByteArray())
|
||||
// .Replace("/", String.Empty)
|
||||
// .Replace("+", String.Empty)
|
||||
// .Replace("-", String.Empty)
|
||||
// .ToLowerInvariant()
|
||||
// .Substring(0, length);
|
||||
//}
|
||||
|
||||
//return Guid.NewGuid().ToString();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates a simple hash from a string
|
||||
/// </summary>
|
||||
public static string HashString(string value)
|
||||
{
|
||||
return GetStableHashCode(value).ToString().Replace("-", String.Empty);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates a simple hash from a string
|
||||
/// </summary>
|
||||
public static string HashObject(params object[] values)
|
||||
{
|
||||
return GetStableHashCode(JsonSerializer.Serialize(values)).ToString().Replace("-", String.Empty);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Autofill IDs on an object with [GenerateId] attributes
|
||||
/// </summary>
|
||||
public static T Autofill<T>(T model)
|
||||
{
|
||||
// find all Raven Ids
|
||||
List<ObjectTraverser.Result<GenerateIdAttribute>> ravenIds = ObjectTraverser.FindAttribute<GenerateIdAttribute>(model);
|
||||
|
||||
// set unset Raven Ids
|
||||
foreach (ObjectTraverser.Result<GenerateIdAttribute> item in ravenIds)
|
||||
{
|
||||
string id = item.Property.GetValue(item.Parent, null) as string;
|
||||
if (id.IsNullOrWhiteSpace())
|
||||
{
|
||||
id = item.Item.Length.HasValue ? Create(item.Item.Length.Value) : Create();
|
||||
item.Property.SetValue(item.Parent, id);
|
||||
}
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
|
||||
static int GetStableHashCode(string str)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
int hash1 = 5381;
|
||||
int hash2 = hash1;
|
||||
|
||||
for (int i = 0; i < str.Length && str[i] != '\0'; i += 2)
|
||||
{
|
||||
hash1 = ((hash1 << 5) + hash1) ^ str[i];
|
||||
if (i == str.Length - 1 || str[i + 1] == '\0')
|
||||
break;
|
||||
hash2 = ((hash2 << 5) + hash2) ^ str[i + 1];
|
||||
}
|
||||
|
||||
return hash1 + (hash2 * 1566083941);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public enum Charset
|
||||
{
|
||||
/// <summary>
|
||||
/// a-z, 0-9
|
||||
/// </summary>
|
||||
az09 = 0,
|
||||
/// <summary>
|
||||
/// a-z, A-Z, 0-9, _-@#.:!?*
|
||||
/// </summary>
|
||||
azAZ09x = 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
using Raven.Client.Documents;
|
||||
using Raven.Client.Documents.Indexes;
|
||||
using Raven.Client.Documents.Indexes.Spatial;
|
||||
using Raven.Client.Documents.Operations.Attachments;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace zero.Persistence;
|
||||
|
||||
|
||||
public abstract class ZeroJavascriptIndex : AbstractJavaScriptIndexCreationTask, IZeroIndexDefinition
|
||||
{
|
||||
public ZeroJavascriptIndex() { Create(); }
|
||||
protected virtual void Create() { }
|
||||
|
||||
public virtual void Setup(IZeroOptions options, IDocumentStore store) { }
|
||||
|
||||
public new string Reduce { get => base.Reduce; set => base.Reduce = value; }
|
||||
public new string OutputReduceToCollection { get => base.OutputReduceToCollection; set => base.OutputReduceToCollection = value; }
|
||||
public new string PatternReferencesCollectionName { get => base.PatternReferencesCollectionName; set => base.PatternReferencesCollectionName = value; }
|
||||
public new string PatternForOutputReduceToCollectionReferences { get => base.PatternForOutputReduceToCollectionReferences; set => base.PatternForOutputReduceToCollectionReferences = value; }
|
||||
|
||||
// AbstractIndexCreationTask
|
||||
public new IJsonObject AsJson(object doc) => base.AsJson(doc);
|
||||
public new IEnumerable<AttachmentName> AttachmentsFor(object doc) => base.AttachmentsFor(doc);
|
||||
public new IEnumerable<string> CounterNamesFor(object doc) => base.CounterNamesFor(doc);
|
||||
public new IJsonObject.IMetadata MetadataFor(object doc) => base.MetadataFor(doc);
|
||||
public new IEnumerable<string> TimeSeriesNamesFor(object doc) => base.TimeSeriesNamesFor(doc);
|
||||
|
||||
// AbstractIndexCreationTaskBase<TIndexDefinition>
|
||||
public new object CreateField(string name, object value, CreateFieldOptions options) => base.CreateField(name, value, options);
|
||||
public new object CreateField(string name, object value, bool stored, bool analyzed) => base.CreateField(name, value, stored, analyzed);
|
||||
public new object CreateField(string name, object value) => base.CreateField(name, value);
|
||||
|
||||
// AbstractCommonApiForIndexes
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, SortedSet<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, ISet<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, IList<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, TResult[]> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, List<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, ICollection<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, IEnumerable<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, TResult> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, HashSet<TResult>> func) => base.Recurse(source, func);
|
||||
public new T? TryConvert<T>(object value) where T : struct => base.TryConvert<T>(value);
|
||||
}
|
||||
|
||||
public abstract class ZeroMultiMapIndex : AbstractMultiMapIndexCreationTask<object>, IZeroIndexDefinition
|
||||
{
|
||||
public ZeroMultiMapIndex() { Create(); }
|
||||
protected virtual void Create() { }
|
||||
|
||||
public virtual void Setup(IZeroOptions options, IDocumentStore store) { }
|
||||
|
||||
// AbstractMultiMapIndexCreationTask<TReduceResult>
|
||||
public new void AddMap<TSource>(Expression<Func<IEnumerable<TSource>, IEnumerable>> map) => base.AddMap(map);
|
||||
public new void AddMapForAll<TBase>(Expression<Func<IEnumerable<TBase>, IEnumerable>> map) => base.AddMapForAll(map);
|
||||
|
||||
// AbstractGenericIndexCreationTask<TReduceResult>
|
||||
public new Expression<Func<IEnumerable<object>, IEnumerable>> Reduce { get => base.Reduce; set => base.Reduce = value; }
|
||||
public new string OutputReduceToCollection { get => base.OutputReduceToCollection; set => base.OutputReduceToCollection = value; }
|
||||
public new IDictionary<string, FieldIndexing> IndexesStrings { get => base.IndexesStrings; set => base.IndexesStrings = value; }
|
||||
public new IDictionary<Expression<Func<object, object>>, FieldIndexing> Indexes { get => base.Indexes; set => base.Indexes = value; }
|
||||
public new IDictionary<string, SpatialOptions> SpatialIndexesStrings { get => base.SpatialIndexesStrings; set => base.SpatialIndexesStrings = value; }
|
||||
public new IDictionary<Expression<Func<object, object>>, SpatialOptions> SpatialIndexes { get => base.SpatialIndexes; set => base.SpatialIndexes = value; }
|
||||
public new IDictionary<string, FieldTermVector> TermVectorsStrings { get => base.TermVectorsStrings; set => base.TermVectorsStrings = value; }
|
||||
public new IDictionary<Expression<Func<object, object>>, FieldTermVector> TermVectors { get => base.TermVectors; set => base.TermVectors = value; }
|
||||
public new IDictionary<string, string> AnalyzersStrings { get => base.AnalyzersStrings; set => base.AnalyzersStrings = value; }
|
||||
public new IDictionary<Expression<Func<object, object>>, string> Analyzers { get => base.Analyzers; set => base.Analyzers = value; }
|
||||
public new ISet<Expression<Func<object, object>>> IndexSuggestions { get => base.IndexSuggestions; set => base.IndexSuggestions = value; }
|
||||
public new IDictionary<string, FieldStorage> StoresStrings { get => base.StoresStrings; set => base.StoresStrings = value; }
|
||||
public new IDictionary<Expression<Func<object, object>>, FieldStorage> Stores { get => base.Stores; set => base.Stores = value; }
|
||||
public new string PatternReferencesCollectionName { get => base.PatternReferencesCollectionName; set => base.PatternReferencesCollectionName = value; }
|
||||
public new Expression<Func<object, string>> PatternForOutputReduceToCollectionReferences { get => base.PatternForOutputReduceToCollectionReferences; set => base.PatternForOutputReduceToCollectionReferences = value; }
|
||||
public new void AddAssembly(AdditionalAssembly assembly) => base.AddAssembly(assembly);
|
||||
public new void Analyze(string field, string analyzer) => base.Analyze(field, analyzer);
|
||||
public new void Analyze(Expression<Func<object, object>> field, string analyzer) => base.Analyze(field, analyzer);
|
||||
public new void Index(Expression<Func<object, object>> field, FieldIndexing indexing) => base.Index(field, indexing);
|
||||
public new void Index(string field, FieldIndexing indexing) => base.Index(field, indexing);
|
||||
public new void Spatial(string field, Func<SpatialOptionsFactory, SpatialOptions> indexing) => base.Spatial(field, indexing);
|
||||
public new void Spatial(Expression<Func<object, object>> field, Func<SpatialOptionsFactory, SpatialOptions> indexing) => base.Spatial(field, indexing);
|
||||
public new void Store(string field, FieldStorage storage) => base.Store(field, storage);
|
||||
public new void Store(Expression<Func<object, object>> field, FieldStorage storage) => base.Store(field, storage);
|
||||
public new void StoreAllFields(FieldStorage storage) => base.StoreAllFields(storage);
|
||||
public new void Suggestion(Expression<Func<object, object>> field) => base.Suggestion(field);
|
||||
public new void TermVector(string field, FieldTermVector termVector) => base.TermVector(field, termVector);
|
||||
public new void TermVector(Expression<Func<object, object>> field, FieldTermVector termVector) => base.TermVector(field, termVector);
|
||||
|
||||
// AbstractIndexCreationTask
|
||||
public new IJsonObject AsJson(object doc) => base.AsJson(doc);
|
||||
public new IEnumerable<AttachmentName> AttachmentsFor(object doc) => base.AttachmentsFor(doc);
|
||||
public new IEnumerable<string> CounterNamesFor(object doc) => base.CounterNamesFor(doc);
|
||||
public new IJsonObject.IMetadata MetadataFor(object doc) => base.MetadataFor(doc);
|
||||
public new IEnumerable<string> TimeSeriesNamesFor(object doc) => base.TimeSeriesNamesFor(doc);
|
||||
|
||||
// AbstractIndexCreationTaskBase<TIndexDefinition>
|
||||
public new object CreateField(string name, object value, CreateFieldOptions options) => base.CreateField(name, value, options);
|
||||
public new object CreateField(string name, object value, bool stored, bool analyzed) => base.CreateField(name, value, stored, analyzed);
|
||||
public new object CreateField(string name, object value) => base.CreateField(name, value);
|
||||
|
||||
// AbstractCommonApiForIndexes
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, SortedSet<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, ISet<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, IList<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, TResult[]> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, List<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, ICollection<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, IEnumerable<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, TResult> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, HashSet<TResult>> func) => base.Recurse(source, func);
|
||||
public new T? TryConvert<T>(object value) where T : struct => base.TryConvert<T>(value);
|
||||
}
|
||||
|
||||
|
||||
public abstract class ZeroMultiMapIndex<TReduceResult> : AbstractMultiMapIndexCreationTask<TReduceResult>, IZeroIndexDefinition
|
||||
{
|
||||
public ZeroMultiMapIndex() { Create(); }
|
||||
protected virtual void Create() { }
|
||||
|
||||
public virtual void Setup(IZeroOptions options, IDocumentStore store) { }
|
||||
|
||||
// AbstractMultiMapIndexCreationTask<TReduceResult>
|
||||
public new void AddMap<TSource>(Expression<Func<IEnumerable<TSource>, IEnumerable>> map) => base.AddMap(map);
|
||||
public new void AddMapForAll<TBase>(Expression<Func<IEnumerable<TBase>, IEnumerable>> map) => base.AddMapForAll(map);
|
||||
|
||||
// AbstractGenericIndexCreationTask<TReduceResult>
|
||||
public new Expression<Func<IEnumerable<TReduceResult>, IEnumerable>> Reduce { get => base.Reduce; set => base.Reduce = value; }
|
||||
public new string OutputReduceToCollection { get => base.OutputReduceToCollection; set => base.OutputReduceToCollection = value; }
|
||||
public new IDictionary<string, FieldIndexing> IndexesStrings { get => base.IndexesStrings; set => base.IndexesStrings = value; }
|
||||
public new IDictionary<Expression<Func<TReduceResult, object>>, FieldIndexing> Indexes { get => base.Indexes; set => base.Indexes = value; }
|
||||
public new IDictionary<string, SpatialOptions> SpatialIndexesStrings { get => base.SpatialIndexesStrings; set => base.SpatialIndexesStrings = value; }
|
||||
public new IDictionary<Expression<Func<TReduceResult, object>>, SpatialOptions> SpatialIndexes { get => base.SpatialIndexes; set => base.SpatialIndexes = value; }
|
||||
public new IDictionary<string, FieldTermVector> TermVectorsStrings { get => base.TermVectorsStrings; set => base.TermVectorsStrings = value; }
|
||||
public new IDictionary<Expression<Func<TReduceResult, object>>, FieldTermVector> TermVectors { get => base.TermVectors; set => base.TermVectors = value; }
|
||||
public new IDictionary<string, string> AnalyzersStrings { get => base.AnalyzersStrings; set => base.AnalyzersStrings = value; }
|
||||
public new IDictionary<Expression<Func<TReduceResult, object>>, string> Analyzers { get => base.Analyzers; set => base.Analyzers = value; }
|
||||
public new ISet<Expression<Func<TReduceResult, object>>> IndexSuggestions { get => base.IndexSuggestions; set => base.IndexSuggestions = value; }
|
||||
public new IDictionary<string, FieldStorage> StoresStrings { get => base.StoresStrings; set => base.StoresStrings = value; }
|
||||
public new IDictionary<Expression<Func<TReduceResult, object>>, FieldStorage> Stores { get => base.Stores; set => base.Stores = value; }
|
||||
public new string PatternReferencesCollectionName { get => base.PatternReferencesCollectionName; set => base.PatternReferencesCollectionName = value; }
|
||||
public new Expression<Func<TReduceResult, string>> PatternForOutputReduceToCollectionReferences { get => base.PatternForOutputReduceToCollectionReferences; set => base.PatternForOutputReduceToCollectionReferences = value; }
|
||||
public new void AddAssembly(AdditionalAssembly assembly) => base.AddAssembly(assembly);
|
||||
public new void Analyze(string field, string analyzer) => base.Analyze(field, analyzer);
|
||||
public new void Analyze(Expression<Func<TReduceResult, object>> field, string analyzer) => base.Analyze(field, analyzer);
|
||||
public new void Index(Expression<Func<TReduceResult, object>> field, FieldIndexing indexing) => base.Index(field, indexing);
|
||||
public new void Index(string field, FieldIndexing indexing) => base.Index(field, indexing);
|
||||
public new void Spatial(string field, Func<SpatialOptionsFactory, SpatialOptions> indexing) => base.Spatial(field, indexing);
|
||||
public new void Spatial(Expression<Func<TReduceResult, object>> field, Func<SpatialOptionsFactory, SpatialOptions> indexing) => base.Spatial(field, indexing);
|
||||
public new void Store(string field, FieldStorage storage) => base.Store(field, storage);
|
||||
public new void Store(Expression<Func<TReduceResult, object>> field, FieldStorage storage) => base.Store(field, storage);
|
||||
public new void StoreAllFields(FieldStorage storage) => base.StoreAllFields(storage);
|
||||
public new void Suggestion(Expression<Func<TReduceResult, object>> field) => base.Suggestion(field);
|
||||
public new void TermVector(string field, FieldTermVector termVector) => base.TermVector(field, termVector);
|
||||
public new void TermVector(Expression<Func<TReduceResult, object>> field, FieldTermVector termVector) => base.TermVector(field, termVector);
|
||||
|
||||
// AbstractIndexCreationTask
|
||||
public new IJsonObject AsJson(object doc) => base.AsJson(doc);
|
||||
public new IEnumerable<AttachmentName> AttachmentsFor(object doc) => base.AttachmentsFor(doc);
|
||||
public new IEnumerable<string> CounterNamesFor(object doc) => base.CounterNamesFor(doc);
|
||||
public new IJsonObject.IMetadata MetadataFor(object doc) => base.MetadataFor(doc);
|
||||
public new IEnumerable<string> TimeSeriesNamesFor(object doc) => base.TimeSeriesNamesFor(doc);
|
||||
|
||||
// AbstractIndexCreationTaskBase<TIndexDefinition>
|
||||
public new object CreateField(string name, object value, CreateFieldOptions options) => base.CreateField(name, value, options);
|
||||
public new object CreateField(string name, object value, bool stored, bool analyzed) => base.CreateField(name, value, stored, analyzed);
|
||||
public new object CreateField(string name, object value) => base.CreateField(name, value);
|
||||
|
||||
// AbstractCommonApiForIndexes
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, SortedSet<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, ISet<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, IList<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, TResult[]> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, List<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, ICollection<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, IEnumerable<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, TResult> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, HashSet<TResult>> func) => base.Recurse(source, func);
|
||||
public new T? TryConvert<T>(object value) where T : struct => base.TryConvert<T>(value);
|
||||
}
|
||||
|
||||
|
||||
public abstract class ZeroIndex<TDocument, TReduceResult> : AbstractIndexCreationTask<TDocument, TReduceResult>, IZeroIndexDefinition
|
||||
{
|
||||
public ZeroIndex() { Create(); }
|
||||
protected virtual void Create() { }
|
||||
|
||||
public virtual void Setup(IZeroOptions options, IDocumentStore store) { }
|
||||
|
||||
// AbstractIndexCreationTask<TDocument, TReduceResult>
|
||||
public new Expression<Func<IEnumerable<TDocument>, IEnumerable>> Map { get => base.Map; set => base.Map = value; }
|
||||
|
||||
// AbstractGenericIndexCreationTask<TReduceResult>
|
||||
public new Expression<Func<IEnumerable<TReduceResult>, IEnumerable>> Reduce { get => base.Reduce; set => base.Reduce = value; }
|
||||
public new string OutputReduceToCollection { get => base.OutputReduceToCollection; set => base.OutputReduceToCollection = value; }
|
||||
public new IDictionary<string, FieldIndexing> IndexesStrings { get => base.IndexesStrings; set => base.IndexesStrings = value; }
|
||||
public new IDictionary<Expression<Func<TReduceResult, object>>, FieldIndexing> Indexes { get => base.Indexes; set => base.Indexes = value; }
|
||||
public new IDictionary<string, SpatialOptions> SpatialIndexesStrings { get => base.SpatialIndexesStrings; set => base.SpatialIndexesStrings = value; }
|
||||
public new IDictionary<Expression<Func<TReduceResult, object>>, SpatialOptions> SpatialIndexes { get => base.SpatialIndexes; set => base.SpatialIndexes = value; }
|
||||
public new IDictionary<string, FieldTermVector> TermVectorsStrings { get => base.TermVectorsStrings; set => base.TermVectorsStrings = value; }
|
||||
public new IDictionary<Expression<Func<TReduceResult, object>>, FieldTermVector> TermVectors { get => base.TermVectors; set => base.TermVectors = value; }
|
||||
public new IDictionary<string, string> AnalyzersStrings { get => base.AnalyzersStrings; set => base.AnalyzersStrings = value; }
|
||||
public new IDictionary<Expression<Func<TReduceResult, object>>, string> Analyzers { get => base.Analyzers; set => base.Analyzers = value; }
|
||||
public new ISet<Expression<Func<TReduceResult, object>>> IndexSuggestions { get => base.IndexSuggestions; set => base.IndexSuggestions = value; }
|
||||
public new IDictionary<string, FieldStorage> StoresStrings { get => base.StoresStrings; set => base.StoresStrings = value; }
|
||||
public new IDictionary<Expression<Func<TReduceResult, object>>, FieldStorage> Stores { get => base.Stores; set => base.Stores = value; }
|
||||
public new string PatternReferencesCollectionName { get => base.PatternReferencesCollectionName; set => base.PatternReferencesCollectionName = value; }
|
||||
public new Expression<Func<TReduceResult, string>> PatternForOutputReduceToCollectionReferences { get => base.PatternForOutputReduceToCollectionReferences; set => base.PatternForOutputReduceToCollectionReferences = value; }
|
||||
public new void AddAssembly(AdditionalAssembly assembly) => base.AddAssembly(assembly);
|
||||
public new void Analyze(string field, string analyzer) => base.Analyze(field, analyzer);
|
||||
public new void Analyze(Expression<Func<TReduceResult, object>> field, string analyzer) => base.Analyze(field, analyzer);
|
||||
public new void Index(Expression<Func<TReduceResult, object>> field, FieldIndexing indexing) => base.Index(field, indexing);
|
||||
public new void Index(string field, FieldIndexing indexing) => base.Index(field, indexing);
|
||||
public new void Spatial(string field, Func<SpatialOptionsFactory, SpatialOptions> indexing) => base.Spatial(field, indexing);
|
||||
public new void Spatial(Expression<Func<TReduceResult, object>> field, Func<SpatialOptionsFactory, SpatialOptions> indexing) => base.Spatial(field, indexing);
|
||||
public new void Store(string field, FieldStorage storage) => base.Store(field, storage);
|
||||
public new void Store(Expression<Func<TReduceResult, object>> field, FieldStorage storage) => base.Store(field, storage);
|
||||
public new void StoreAllFields(FieldStorage storage) => base.StoreAllFields(storage);
|
||||
public new void Suggestion(Expression<Func<TReduceResult, object>> field) => base.Suggestion(field);
|
||||
public new void TermVector(string field, FieldTermVector termVector) => base.TermVector(field, termVector);
|
||||
public new void TermVector(Expression<Func<TReduceResult, object>> field, FieldTermVector termVector) => base.TermVector(field, termVector);
|
||||
|
||||
// AbstractIndexCreationTask
|
||||
public new IJsonObject AsJson(object doc) => base.AsJson(doc);
|
||||
public new IEnumerable<AttachmentName> AttachmentsFor(object doc) => base.AttachmentsFor(doc);
|
||||
public new IEnumerable<string> CounterNamesFor(object doc) => base.CounterNamesFor(doc);
|
||||
public new IJsonObject.IMetadata MetadataFor(object doc) => base.MetadataFor(doc);
|
||||
public new IEnumerable<string> TimeSeriesNamesFor(object doc) => base.TimeSeriesNamesFor(doc);
|
||||
|
||||
// AbstractIndexCreationTaskBase<TIndexDefinition>
|
||||
public new object CreateField(string name, object value, CreateFieldOptions options) => base.CreateField(name, value, options);
|
||||
public new object CreateField(string name, object value, bool stored, bool analyzed) => base.CreateField(name, value, stored, analyzed);
|
||||
public new object CreateField(string name, object value) => base.CreateField(name, value);
|
||||
|
||||
// AbstractCommonApiForIndexes
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, SortedSet<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, ISet<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, IList<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, TResult[]> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, List<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, ICollection<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, IEnumerable<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, TResult> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, HashSet<TResult>> func) => base.Recurse(source, func);
|
||||
public new T? TryConvert<T>(object value) where T : struct => base.TryConvert<T>(value);
|
||||
}
|
||||
|
||||
|
||||
public abstract class ZeroIndex<TDocument> : ZeroIndex<TDocument, TDocument>
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public abstract class ZeroIndex : AbstractIndexCreationTask, IZeroIndexDefinition
|
||||
{
|
||||
public ZeroIndex() { Create(); }
|
||||
protected virtual void Create() { }
|
||||
|
||||
public virtual void Setup(IZeroOptions options, IDocumentStore store) { }
|
||||
|
||||
// AbstractIndexCreationTask
|
||||
public new IJsonObject AsJson(object doc) => base.AsJson(doc);
|
||||
public new IEnumerable<AttachmentName> AttachmentsFor(object doc) => base.AttachmentsFor(doc);
|
||||
public new IEnumerable<string> CounterNamesFor(object doc) => base.CounterNamesFor(doc);
|
||||
public new IJsonObject.IMetadata MetadataFor(object doc) => base.MetadataFor(doc);
|
||||
public new IEnumerable<string> TimeSeriesNamesFor(object doc) => base.TimeSeriesNamesFor(doc);
|
||||
|
||||
// AbstractIndexCreationTaskBase<TIndexDefinition>
|
||||
public new object CreateField(string name, object value, CreateFieldOptions options) => base.CreateField(name, value, options);
|
||||
public new object CreateField(string name, object value, bool stored, bool analyzed) => base.CreateField(name, value, stored, analyzed);
|
||||
public new object CreateField(string name, object value) => base.CreateField(name, value);
|
||||
|
||||
// AbstractCommonApiForIndexes
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, SortedSet<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, ISet<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, IList<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, TResult[]> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, List<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, ICollection<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, IEnumerable<TResult>> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, TResult> func) => base.Recurse(source, func);
|
||||
public new IEnumerable<TResult> Recurse<TSource, TResult>(TSource source, Func<TSource, HashSet<TResult>> func) => base.Recurse(source, func);
|
||||
public new T? TryConvert<T>(object value) where T : struct => base.TryConvert<T>(value);
|
||||
}
|
||||
|
||||
|
||||
public interface IZeroIndexDefinition : IAbstractIndexCreationTask
|
||||
{
|
||||
void Setup(IZeroOptions options, IDocumentStore store);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace zero.Persistence;
|
||||
|
||||
public static class ZeroIndexExtensions
|
||||
{
|
||||
internal static void RunModifiers<T>(this T index, RavenOptions options) where T : IZeroIndexDefinition
|
||||
{
|
||||
IEnumerable<RavenIndexModifiersOptions.Modifier> modifiers = options.Indexes.Modifiers.GetAllForType(index.GetType());
|
||||
|
||||
foreach (RavenIndexModifiersOptions.Modifier modifier in modifiers)
|
||||
{
|
||||
Action<IZeroIndexDefinition> action = modifier.Modify.Compile();
|
||||
action.Invoke(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Raven.Client.Documents;
|
||||
using Raven.Client.Documents.Indexes;
|
||||
|
||||
namespace zero.Persistence;
|
||||
|
||||
public class ZeroTreeHierarchyIndexResult : ZeroIdEntity, ISupportsDbConventions
|
||||
{
|
||||
public List<string> Path { get; set; } = new List<string>();
|
||||
}
|
||||
|
||||
public abstract class ZeroTreeHierarchyIndex<T> : ZeroIndex<T, ZeroTreeHierarchyIndexResult> where T : ZeroIdEntity, ISupportsTrees
|
||||
{
|
||||
protected override void Create()
|
||||
{
|
||||
Map = items => items.Select(item => new ZeroTreeHierarchyIndexResult
|
||||
{
|
||||
Id = item.Id,
|
||||
Path = Recurse(item, x => LoadDocument<T>(x.ParentId))
|
||||
.Where(x => x != null && x.Id != null && x.Id != item.Id)
|
||||
.Reverse()
|
||||
.Select(current => current.Id)
|
||||
.ToList()
|
||||
});
|
||||
|
||||
StoreAllFields(FieldStorage.Yes);
|
||||
//Index(x => x.ChannelId, FieldIndexing.Exact);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace zero.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// This attribute will allow the usage of custom collection names for Raven collections
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class, AllowMultiple = false)]
|
||||
public class RavenCollectionAttribute : Attribute
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public RavenCollectionAttribute(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace zero.Persistence;
|
||||
|
||||
public static partial class RavenConstants
|
||||
{
|
||||
public static partial class Indexing
|
||||
{
|
||||
public const string ThrottlingTimeIntervalInMs = "Indexing.Throttling.TimeIntervalInMs";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using Raven.Client.Documents.Indexes;
|
||||
|
||||
namespace zero.Persistence;
|
||||
|
||||
public static class RavenIndexExtensions
|
||||
{
|
||||
public static void Throttle(this AbstractCommonApiForIndexes index, TimeSpan delay)
|
||||
{
|
||||
index.Configuration[RavenConstants.Indexing.ThrottlingTimeIntervalInMs] = delay.TotalMilliseconds.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
using Raven.Client.Documents;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace zero.Persistence;
|
||||
|
||||
public class RavenOptions
|
||||
{
|
||||
public string Url { get; set; }
|
||||
|
||||
public string Database { get; set; }
|
||||
|
||||
public bool DatabaseIsDefault { get; set; } = false;
|
||||
|
||||
public string CollectionPrefix { get; set; } = String.Empty;
|
||||
|
||||
public RavenIndexesOptions Indexes { get; set; } = new();
|
||||
}
|
||||
|
||||
|
||||
public class RavenIndexesOptions : List<RavenIndexesOptions.Map>
|
||||
{
|
||||
public class Map
|
||||
{
|
||||
internal Type Type { get; set; }
|
||||
|
||||
internal Expression<Func<IZeroIndexDefinition>> CreateIndex { get; set; }
|
||||
|
||||
internal Map(Type type, Expression<Func<IZeroIndexDefinition>> create)
|
||||
{
|
||||
Type = type;
|
||||
CreateIndex = create;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public RavenIndexModifiersOptions Modifiers { get; private set; } = new();
|
||||
|
||||
public void Add<T>() where T : IZeroIndexDefinition, new()
|
||||
{
|
||||
base.Add(new Map(typeof(T), () => new T()));
|
||||
}
|
||||
|
||||
public void Add(Type indexType)
|
||||
{
|
||||
base.Add(new Map(indexType, () => (IZeroIndexDefinition)Activator.CreateInstance(indexType)));
|
||||
}
|
||||
|
||||
public void Add<T>(T index) where T : IZeroIndexDefinition
|
||||
{
|
||||
base.Add(new Map(typeof(T), () => index));
|
||||
}
|
||||
|
||||
public void AddRange(params Type[] indexes)
|
||||
{
|
||||
foreach (Type type in indexes)
|
||||
{
|
||||
Add(type);
|
||||
}
|
||||
}
|
||||
|
||||
public void Replace<T, TReplaceWith>()
|
||||
where T : IZeroIndexDefinition, new()
|
||||
where TReplaceWith : IZeroIndexDefinition, new()
|
||||
{
|
||||
Replace(typeof(T), typeof(TReplaceWith));
|
||||
}
|
||||
|
||||
public void Replace(Type origin, Type replaceWith)
|
||||
{
|
||||
var item = this.FirstOrDefault(x => x.Type == origin);
|
||||
if (item != null)
|
||||
{
|
||||
Remove(item);
|
||||
}
|
||||
Add(replaceWith);
|
||||
}
|
||||
|
||||
public IEnumerable<IZeroIndexDefinition> BuildAll(IZeroOptions options, IDocumentStore store)
|
||||
{
|
||||
RavenOptions ravenOptions = options.For<RavenOptions>();
|
||||
|
||||
foreach (Map map in this)
|
||||
{
|
||||
IZeroIndexDefinition index = map.CreateIndex.Compile().Invoke();
|
||||
index.Setup(options, store);
|
||||
index.RunModifiers(ravenOptions);
|
||||
yield return index;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class RavenIndexModifiersOptions : List<RavenIndexModifiersOptions.Modifier>
|
||||
{
|
||||
public class Modifier
|
||||
{
|
||||
public Type Type { get; set; }
|
||||
|
||||
public Expression<Action<IZeroIndexDefinition>> Modify { get; set; }
|
||||
}
|
||||
|
||||
public void Add<T>(Action<T> modify) where T : IZeroIndexDefinition, new()
|
||||
{
|
||||
Add(new()
|
||||
{
|
||||
Type = typeof(T),
|
||||
Modify = x => modify((T)x)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public IEnumerable<Modifier> GetAllForType<T>() where T : IZeroIndexDefinition, new() => GetAllForType(typeof(T));
|
||||
|
||||
|
||||
public IEnumerable<Modifier> GetAllForType(Type type)
|
||||
{
|
||||
foreach (Modifier modifier in this.Where(x => x.Type.IsAssignableFrom(type)))
|
||||
{
|
||||
yield return modifier;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
using Raven.Client.Documents;
|
||||
using Raven.Client.Documents.Linq;
|
||||
using Raven.Client.Documents.Queries;
|
||||
using Raven.Client.Documents.Session;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace zero.Persistence;
|
||||
|
||||
public static class RavenQueryableExtensions
|
||||
{
|
||||
public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> source, string path, bool isDescending, OrderingType type = OrderingType.String)
|
||||
{
|
||||
if (!String.IsNullOrEmpty(path))
|
||||
{
|
||||
char[] a = path.ToCharArray();
|
||||
a[0] = char.ToUpper(a[0]);
|
||||
path = new string(a);
|
||||
}
|
||||
|
||||
if (isDescending)
|
||||
{
|
||||
return source.OrderByDescending(path, type);
|
||||
}
|
||||
return source.OrderBy(path, type);
|
||||
}
|
||||
|
||||
|
||||
public static IOrderedQueryable<T> ThenBy<T>(this IOrderedQueryable<T> source, string path, bool isDescending, OrderingType type = OrderingType.String)
|
||||
{
|
||||
if (!String.IsNullOrEmpty(path))
|
||||
{
|
||||
char[] a = path.ToCharArray();
|
||||
a[0] = char.ToUpper(a[0]);
|
||||
path = new string(a);
|
||||
}
|
||||
|
||||
if (isDescending)
|
||||
{
|
||||
return source.ThenByDescending(path, type);
|
||||
}
|
||||
return source.ThenBy(path, type);
|
||||
}
|
||||
|
||||
|
||||
public static IQueryable<T> Paging<T>(this IQueryable<T> source, int pageNumber, int pageSize)
|
||||
{
|
||||
pageNumber = pageNumber.Limit(1, 10_000_000);
|
||||
pageSize = pageSize.Limit(1, 1_000);
|
||||
|
||||
if (pageNumber <= 0 || pageSize <= 0)
|
||||
{
|
||||
throw new NotSupportedException("Both pageNumber and pageSize must be greater than zero");
|
||||
}
|
||||
|
||||
return source.Skip((pageNumber - 1) * pageSize).Take(pageSize);
|
||||
}
|
||||
|
||||
|
||||
public static IRavenQueryable<T> WhereIf<T>(this IRavenQueryable<T> source, Expression<Func<T, bool>> predicate, bool condition, Expression<Func<T, bool>> elsePredicate = null)
|
||||
{
|
||||
if (!condition)
|
||||
{
|
||||
if (elsePredicate != null)
|
||||
{
|
||||
return source.Where(elsePredicate);
|
||||
}
|
||||
return source;
|
||||
}
|
||||
|
||||
return source.Where(predicate);
|
||||
}
|
||||
|
||||
|
||||
public static IQueryable<T> WhereIf<T>(this IQueryable<T> source, Expression<Func<T, bool>> predicate, bool condition, Expression<Func<T, bool>> elsePredicate = null)
|
||||
{
|
||||
if (!condition)
|
||||
{
|
||||
if (elsePredicate != null)
|
||||
{
|
||||
return source.Where(elsePredicate);
|
||||
}
|
||||
return source;
|
||||
}
|
||||
|
||||
return source.Where(predicate);
|
||||
}
|
||||
|
||||
|
||||
public static IQueryable<T> SearchIf<T>(this IQueryable<T> source, Expression<Func<T, object>> fieldSelector, string searchTerms, string suffix = null, string prefix = null, SearchOperator @operator = SearchOperator.Or)
|
||||
{
|
||||
if (String.IsNullOrWhiteSpace(searchTerms))
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
searchTerms = searchTerms.Trim();
|
||||
|
||||
string[] searchParts = searchTerms.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries).Select(x =>
|
||||
{
|
||||
if (suffix != null)
|
||||
{
|
||||
x += suffix;
|
||||
}
|
||||
if (prefix != null)
|
||||
{
|
||||
x = prefix + x;
|
||||
}
|
||||
return x;
|
||||
}).ToArray();
|
||||
|
||||
if (searchTerms.StartsWith('"') && searchTerms.EndsWith('"'))
|
||||
{
|
||||
searchParts = new[] { searchTerms };
|
||||
}
|
||||
|
||||
return source.Search(fieldSelector, searchParts, @operator: @operator);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace zero.Persistence;
|
||||
|
||||
public class Safenames
|
||||
{
|
||||
public enum Scope
|
||||
{
|
||||
Url,
|
||||
File
|
||||
}
|
||||
|
||||
const char HYPHEN = '-';
|
||||
|
||||
const char DOT = '.';
|
||||
|
||||
const char PLUS = '+';
|
||||
|
||||
const char AMPERSAND = '&';
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Converts an untrusted to a safe filename
|
||||
/// </summary>
|
||||
public static string File(string value)
|
||||
{
|
||||
return Generate(Path.GetFileName(value), Scope.File);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Converts a term to a safe alias (suitable for URLs)
|
||||
/// </summary>
|
||||
public static string Alias(string value)
|
||||
{
|
||||
return Generate(value, Scope.Url);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Converts a term to a safe alias (suitable for URLs)
|
||||
/// </summary>
|
||||
public static string Alias(object value)
|
||||
{
|
||||
return Generate(value?.ToString(), Scope.Url);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
static string Generate(string value, Scope scope)
|
||||
{
|
||||
if (String.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return String.Empty;
|
||||
}
|
||||
|
||||
char previous = default;
|
||||
StringBuilder output = new();
|
||||
|
||||
for (int i = 0; i < value.Length; i++)
|
||||
{
|
||||
// get character in lower case
|
||||
char character = char.ToLower(value[i]);
|
||||
char target;
|
||||
|
||||
// do not handle surrogates
|
||||
if (char.IsSurrogate(character))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// special replacements accents + umlauts
|
||||
if (character.TryReplaceAccent(out char[] replacement))
|
||||
{
|
||||
if (replacement.Length > 1)
|
||||
{
|
||||
output.Append(replacement);
|
||||
output.Remove(output.Length - 1, 1);
|
||||
}
|
||||
target = replacement[replacement.Length - 1];
|
||||
}
|
||||
// append character a-z, 0-9
|
||||
else if (character.IsAZor09())
|
||||
{
|
||||
target = character;
|
||||
}
|
||||
// + sign for + and &
|
||||
else if (character == PLUS || character == AMPERSAND)
|
||||
{
|
||||
target = PLUS;
|
||||
}
|
||||
else if (scope == Scope.File && character == DOT)
|
||||
{
|
||||
target = DOT;
|
||||
}
|
||||
// add hyphen for all other characters
|
||||
else
|
||||
{
|
||||
target = HYPHEN;
|
||||
}
|
||||
|
||||
// add default characters
|
||||
if (target != HYPHEN && target != PLUS)
|
||||
{
|
||||
output.Append(target);
|
||||
}
|
||||
// add hyphen if it isn't first and previous char is not + or -
|
||||
else if (target == HYPHEN && previous != default && previous != PLUS && previous != HYPHEN)
|
||||
{
|
||||
output.Append(target);
|
||||
}
|
||||
// add plus. do remove hyphen it is the previous character
|
||||
else if (target == PLUS)
|
||||
{
|
||||
if (previous == HYPHEN)
|
||||
{
|
||||
output.Remove(output.Length - 1, 1);
|
||||
}
|
||||
output.Append(target);
|
||||
}
|
||||
|
||||
if (output.Length > 0)
|
||||
{
|
||||
previous = output[output.Length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
if (output.Length > 0 && !output[output.Length - 1].IsAZor09())
|
||||
{
|
||||
output.Remove(output.Length - 1, 1);
|
||||
}
|
||||
|
||||
if (output.Length == 0)
|
||||
{
|
||||
output.Append(HYPHEN);
|
||||
}
|
||||
|
||||
return output.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace zero.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// Replicates the Rfc6238AuthenticationService from ASP.NET Core internals as it isn't part of the public API
|
||||
/// (see: https://github.com/dotnet/aspnetcore/blob/release/5.0/src/Identity/Extensions.Core/src/Rfc6238AuthenticationService.cs)
|
||||
/// </summary>
|
||||
public static class Rfc6238AuthenticationService
|
||||
{
|
||||
private static readonly TimeSpan _timestep = TimeSpan.FromMinutes(3);
|
||||
private static readonly Encoding _encoding = new UTF8Encoding(false, true);
|
||||
|
||||
// Generates a new 80-bit security token
|
||||
public static byte[] GenerateRandomKey()
|
||||
{
|
||||
byte[] bytes = new byte[20];
|
||||
RandomNumberGenerator.Fill(bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
internal static int ComputeTotp(HashAlgorithm hashAlgorithm, ulong timestepNumber, string modifier)
|
||||
{
|
||||
// # of 0's = length of pin
|
||||
const int Mod = 1000000;
|
||||
|
||||
// See https://tools.ietf.org/html/rfc4226
|
||||
// We can add an optional modifier
|
||||
var timestepAsBytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((long)timestepNumber));
|
||||
var hash = hashAlgorithm.ComputeHash(ApplyModifier(timestepAsBytes, modifier));
|
||||
|
||||
// Generate DT string
|
||||
var offset = hash[^1] & 0xf;
|
||||
Debug.Assert(offset + 4 < hash.Length);
|
||||
var binaryCode = (hash[offset] & 0x7f) << 24
|
||||
| (hash[offset + 1] & 0xff) << 16
|
||||
| (hash[offset + 2] & 0xff) << 8
|
||||
| (hash[offset + 3] & 0xff);
|
||||
|
||||
return binaryCode % Mod;
|
||||
}
|
||||
|
||||
private static byte[] ApplyModifier(byte[] input, string modifier)
|
||||
{
|
||||
if (String.IsNullOrEmpty(modifier))
|
||||
{
|
||||
return input;
|
||||
}
|
||||
|
||||
var modifierBytes = _encoding.GetBytes(modifier);
|
||||
var combined = new byte[checked(input.Length + modifierBytes.Length)];
|
||||
Buffer.BlockCopy(input, 0, combined, 0, input.Length);
|
||||
Buffer.BlockCopy(modifierBytes, 0, combined, input.Length, modifierBytes.Length);
|
||||
return combined;
|
||||
}
|
||||
|
||||
// More info: https://tools.ietf.org/html/rfc6238#section-4
|
||||
private static ulong GetCurrentTimeStepNumber()
|
||||
{
|
||||
var delta = DateTimeOffset.UtcNow - DateTimeOffset.UnixEpoch;
|
||||
return (ulong)(delta.Ticks / _timestep.Ticks);
|
||||
}
|
||||
|
||||
public static int GenerateCode(byte[] securityToken, string modifier = null)
|
||||
{
|
||||
if (securityToken == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(securityToken));
|
||||
}
|
||||
|
||||
// Allow a variance of no greater than 9 minutes in either direction
|
||||
var currentTimeStep = GetCurrentTimeStepNumber();
|
||||
using var hashAlgorithm = new HMACSHA1(securityToken);
|
||||
return ComputeTotp(hashAlgorithm, currentTimeStep, modifier);
|
||||
}
|
||||
|
||||
public static bool ValidateCode(byte[] securityToken, int code, string modifier = null)
|
||||
{
|
||||
if (securityToken == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(securityToken));
|
||||
}
|
||||
|
||||
// Allow a variance of no greater than 9 minutes in either direction
|
||||
var currentTimeStep = GetCurrentTimeStepNumber();
|
||||
using (var hashAlgorithm = new HMACSHA1(securityToken))
|
||||
{
|
||||
for (var i = -2; i <= 2; i++)
|
||||
{
|
||||
var computedTotp = ComputeTotp(hashAlgorithm, (ulong)((long)currentTimeStep + i), modifier);
|
||||
if (computedTotp == code)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No match
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace zero.Persistence;
|
||||
|
||||
[RavenCollection("Tokens")]
|
||||
public class SecurityToken : ISupportsDbConventions
|
||||
{
|
||||
public string Id { get; set; }
|
||||
|
||||
public string Key { get; set; }
|
||||
|
||||
public string Token { get; set; }
|
||||
|
||||
public Dictionary<string, string> Metadata { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
|
||||
using Raven.Client.Documents.Session;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace zero.Persistence;
|
||||
|
||||
public class ZeroTokenProvider : IZeroTokenProvider
|
||||
{
|
||||
readonly char[] chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-".ToCharArray();
|
||||
|
||||
readonly RandomNumberGenerator randonNumberGenerator;
|
||||
|
||||
protected IZeroStore Store { get; private set; }
|
||||
|
||||
|
||||
public ZeroTokenProvider(IZeroStore store)
|
||||
{
|
||||
Store = store;
|
||||
randonNumberGenerator = RandomNumberGenerator.Create();
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual async Task<string> Create(string key, TimeSpan expires, int length = 82, Dictionary<string, string> metadata = default)
|
||||
{
|
||||
if (key.IsNullOrWhiteSpace())
|
||||
{
|
||||
throw new ArgumentException("Key cannot be empty");
|
||||
}
|
||||
if (length < 16)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("Use at least a length of 16 for the token");
|
||||
}
|
||||
|
||||
string tokenKey = Random(length);
|
||||
|
||||
SecurityToken securityToken = new()
|
||||
{
|
||||
Id = TokenToId(tokenKey),
|
||||
Token = tokenKey,
|
||||
Key = HashKey(key),
|
||||
Metadata = metadata ?? new()
|
||||
};
|
||||
|
||||
IZeroDocumentSession session = Store.Session();
|
||||
|
||||
// saves the token
|
||||
await session.StoreAsync(securityToken);
|
||||
|
||||
// set the expires flag for the token
|
||||
session.Expires(securityToken, expires);
|
||||
await session.SaveChangesAsync();
|
||||
|
||||
return tokenKey;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual async Task<bool> Verify(string key, string token)
|
||||
{
|
||||
if (token.IsNullOrWhiteSpace() || key.IsNullOrWhiteSpace())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
IZeroDocumentSession session = Store.Session();
|
||||
|
||||
// try to find a valid token
|
||||
SecurityToken securityToken = await session.LoadAsync<SecurityToken>(TokenToId(token));
|
||||
bool isValid = securityToken != null && VerifyKey(securityToken.Key, key);
|
||||
|
||||
// remove token from DB if it is valid
|
||||
if (isValid)
|
||||
{
|
||||
session.Delete(securityToken);
|
||||
await session.SaveChangesAsync();
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual async Task<SecurityToken> VerifyAndReturn(string key, string token)
|
||||
{
|
||||
if (token.IsNullOrWhiteSpace() || key.IsNullOrWhiteSpace())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
IZeroDocumentSession session = Store.Session();
|
||||
|
||||
// try to find a valid token
|
||||
SecurityToken securityToken = await session.LoadAsync<SecurityToken>(TokenToId(token));
|
||||
bool isValid = securityToken != null && VerifyKey(securityToken.Key, key);
|
||||
|
||||
// remove token from DB if it is valid
|
||||
if (isValid)
|
||||
{
|
||||
session.Delete(securityToken);
|
||||
await session.SaveChangesAsync();
|
||||
return securityToken;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Random(int length)
|
||||
{
|
||||
byte[] data = new byte[4 * length];
|
||||
randonNumberGenerator.GetBytes(data);
|
||||
|
||||
StringBuilder result = new(length);
|
||||
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
var rnd = BitConverter.ToUInt32(data, i * 4);
|
||||
var idx = rnd % chars.Length;
|
||||
|
||||
result.Append(chars[idx]);
|
||||
}
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual async Task<bool> Exists(string token)
|
||||
{
|
||||
if (token.IsNullOrWhiteSpace())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
IZeroDocumentSession session = Store.Session();
|
||||
|
||||
// try to find a valid token
|
||||
SecurityToken securityToken = await session.LoadAsync<SecurityToken>(TokenToId(token));
|
||||
return securityToken != null;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Converts the token to a database id
|
||||
/// </summary>
|
||||
string TokenToId(string token)
|
||||
{
|
||||
string collection = Store.Raven.Conventions.GetCollectionName(typeof(SecurityToken));
|
||||
string idPrefix = Store.Raven.Conventions.TransformTypeCollectionNameToDocumentIdPrefix(collection);
|
||||
return idPrefix + Store.Raven.Conventions.IdentityPartsSeparator + token;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates a hash for the security token key.
|
||||
/// Borrowed from the .NET Core PasswordHasher
|
||||
/// (see: https://github.com/dotnet/aspnetcore/blob/release/5.0/src/Identity/Extensions.Core/src/PasswordHasher.cs)
|
||||
/// </summary>
|
||||
string HashKey(string key)
|
||||
{
|
||||
key = key.ToLowerInvariant();
|
||||
|
||||
KeyDerivationPrf prf = KeyDerivationPrf.HMACSHA256;
|
||||
int iterationCount = 10000;
|
||||
int saltSize = 128 / 8;
|
||||
int numBytesRequested = 256 / 8;
|
||||
|
||||
byte[] salt = new byte[saltSize];
|
||||
randonNumberGenerator.GetBytes(salt);
|
||||
byte[] subkey = KeyDerivation.Pbkdf2(key, salt, prf, iterationCount, numBytesRequested);
|
||||
|
||||
var outputBytes = new byte[13 + salt.Length + subkey.Length];
|
||||
outputBytes[0] = 0x01;
|
||||
WriteNetworkByteOrder(outputBytes, 1, (uint)prf);
|
||||
WriteNetworkByteOrder(outputBytes, 5, (uint)iterationCount);
|
||||
WriteNetworkByteOrder(outputBytes, 9, (uint)saltSize);
|
||||
Buffer.BlockCopy(salt, 0, outputBytes, 13, salt.Length);
|
||||
Buffer.BlockCopy(subkey, 0, outputBytes, 13 + saltSize, subkey.Length);
|
||||
return Convert.ToBase64String(outputBytes);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the given key.
|
||||
/// Borrowed from the .NET Core PasswordHasher
|
||||
/// (see: https://github.com/dotnet/aspnetcore/blob/release/5.0/src/Identity/Extensions.Core/src/PasswordHasher.cs)
|
||||
/// </summary>
|
||||
bool VerifyKey(string storedKey, string key)
|
||||
{
|
||||
byte[] decodedStoredKey = Convert.FromBase64String(storedKey);
|
||||
|
||||
try
|
||||
{
|
||||
KeyDerivationPrf prf = (KeyDerivationPrf)ReadNetworkByteOrder(decodedStoredKey, 1);
|
||||
int embeddedIterCount = (int)ReadNetworkByteOrder(decodedStoredKey, 5);
|
||||
int saltLength = (int)ReadNetworkByteOrder(decodedStoredKey, 9);
|
||||
|
||||
// Read the salt: must be >= 128 bits
|
||||
if (saltLength < 128 / 8)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
byte[] salt = new byte[saltLength];
|
||||
Buffer.BlockCopy(decodedStoredKey, 13, salt, 0, salt.Length);
|
||||
|
||||
// Read the subkey (the rest of the payload): must be >= 128 bits
|
||||
int subkeyLength = decodedStoredKey.Length - 13 - salt.Length;
|
||||
if (subkeyLength < 128 / 8)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
byte[] expectedSubkey = new byte[subkeyLength];
|
||||
Buffer.BlockCopy(decodedStoredKey, 13 + salt.Length, expectedSubkey, 0, expectedSubkey.Length);
|
||||
|
||||
// Hash the incoming key and verify it
|
||||
byte[] actualSubkey = KeyDerivation.Pbkdf2(key, salt, prf, embeddedIterCount, subkeyLength);
|
||||
|
||||
return CryptographicOperations.FixedTimeEquals(actualSubkey, expectedSubkey);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void WriteNetworkByteOrder(byte[] buffer, int offset, uint value)
|
||||
{
|
||||
buffer[offset + 0] = (byte)(value >> 24);
|
||||
buffer[offset + 1] = (byte)(value >> 16);
|
||||
buffer[offset + 2] = (byte)(value >> 8);
|
||||
buffer[offset + 3] = (byte)(value >> 0);
|
||||
}
|
||||
|
||||
static uint ReadNetworkByteOrder(byte[] buffer, int offset)
|
||||
{
|
||||
return ((uint)(buffer[offset + 0]) << 24)
|
||||
| ((uint)(buffer[offset + 1]) << 16)
|
||||
| ((uint)(buffer[offset + 2]) << 8)
|
||||
| ((uint)(buffer[offset + 3]));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface IZeroTokenProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a token for a <paramref name="key"/> with a specified <paramref name="expires"/> lifespan.
|
||||
/// </summary>
|
||||
/// <param name="key">The purpose the token will be used for. The key must match when verifying the token and should always be hidden from the user.</param>
|
||||
/// <param name="expires">The token will automatically expire after this timespan.</param>
|
||||
/// <param name="length">Length of the geneated token.</param>
|
||||
/// <param name="metadata">Additional metadata to store with the token. This data can be retrieved on verification with <see cref="VerifyAndReturn(string, string)"/></param>
|
||||
/// <returns>
|
||||
/// The generated token with the specified <paramref name="length"/>.
|
||||
/// </returns>
|
||||
Task<string> Create(string key, TimeSpan expires, int length = 82, Dictionary<string, string> metadata = default);
|
||||
|
||||
/// <summary>
|
||||
/// Validates the passed <paramref name="token"/> for the specified <paramref name="key"/>.
|
||||
/// </summary>
|
||||
/// <param name="key">The purpose the token was used for.</param>
|
||||
/// <param name="token">The previously generated token.</param>
|
||||
/// <returns>
|
||||
/// False, if the token could not be found or it has already expired.
|
||||
/// </returns>
|
||||
Task<bool> Verify(string key, string token);
|
||||
|
||||
/// <summary>
|
||||
/// Validates the passed <paramref name="token"/> for the specified <paramref name="key"/>.
|
||||
/// </summary>
|
||||
/// <param name="key">The purpose the token was used for.</param>
|
||||
/// <param name="token">The previously generated token.</param>
|
||||
/// <returns>
|
||||
/// Null, if the token could not be found or it has already expired.
|
||||
/// </returns>
|
||||
Task<SecurityToken> VerifyAndReturn(string key, string token);
|
||||
|
||||
/// <summary>
|
||||
/// Generates a random token with the specified <paramref name="length"/> using the RNGCryptoServiceProvider.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method won't store the token in the database and can't be used for verification. Use <see cref="Create(string, TimeSpan, int)"/> instead.
|
||||
/// </remarks>
|
||||
string Random(int length);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a token exists in the database.
|
||||
/// </summary>
|
||||
Task<bool> Exists(string token);
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
using Raven.Client.Documents.Conventions;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace zero.Persistence;
|
||||
|
||||
public class ZeroDocumentConventionsBuilder : IZeroDocumentConventionsBuilder
|
||||
{
|
||||
protected HashSet<Type> PolymorphTypes { get; private set; } = new();
|
||||
|
||||
protected Type AcceptsZeroConventionsType { get; set; } = typeof(ISupportsDbConventions);
|
||||
|
||||
protected char IdentityPartsSeparator { get; set; } = '.';
|
||||
|
||||
protected IZeroOptions Options { get; private set; }
|
||||
|
||||
protected static ConcurrentDictionary<Type, string> CachedTypeCollectionNameMap = new();
|
||||
|
||||
|
||||
public ZeroDocumentConventionsBuilder(IZeroOptions options)
|
||||
{
|
||||
Options = options;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Run(DocumentConventions conventions)
|
||||
{
|
||||
conventions.MaxNumberOfRequestsPerSession = 1000;
|
||||
conventions.IdentityPartsSeparator = IdentityPartsSeparator;
|
||||
conventions.TransformTypeCollectionNameToDocumentIdPrefix = TransformTypeCollectionNameToDocumentIdPrefix;
|
||||
conventions.FindCollectionName = FindCollectionName;
|
||||
conventions.RegisterAsyncIdConvention<ZeroIdEntity>((_, entity) => GetDocumentId(conventions, entity));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get a document ID from an entity
|
||||
/// </summary>
|
||||
protected virtual Task<string> GetDocumentId<T>(DocumentConventions conventions, T entity)
|
||||
{
|
||||
string collection = conventions.GetCollectionName(entity);
|
||||
|
||||
StringBuilder documentId = new();
|
||||
documentId.Append(conventions.TransformTypeCollectionNameToDocumentIdPrefix(collection));
|
||||
documentId.Append(IdentityPartsSeparator);
|
||||
documentId.Append(IdGenerator.Create());
|
||||
|
||||
return Task.FromResult(documentId.ToString());
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Finds the collection name for a certain type based on internal rules
|
||||
/// </summary>
|
||||
protected virtual string FindCollectionName(Type originalType)
|
||||
{
|
||||
string collection = null;
|
||||
|
||||
Type type = originalType;
|
||||
|
||||
Func<string, string> cache = name =>
|
||||
{
|
||||
CachedTypeCollectionNameMap.TryAdd(type, name);
|
||||
return name;
|
||||
};
|
||||
|
||||
// get inner type for revisions
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Raven.Client.Documents.Subscriptions.Revision<>))
|
||||
{
|
||||
type = type.GetGenericArguments().FirstOrDefault();
|
||||
}
|
||||
|
||||
// try to resolve from cache
|
||||
if (CachedTypeCollectionNameMap.TryGetValue(type, out collection))
|
||||
{
|
||||
return collection;
|
||||
}
|
||||
|
||||
// do not alter non-internal entities
|
||||
if (!AcceptsZeroConventionsType.IsAssignableFrom(type))
|
||||
{
|
||||
return cache(DocumentConventions.DefaultGetCollectionName(type));
|
||||
}
|
||||
|
||||
// use name from attribute if available
|
||||
RavenCollectionAttribute collectionAttribute = type.GetCustomAttribute<RavenCollectionAttribute>(true);
|
||||
|
||||
if (collectionAttribute != null)
|
||||
{
|
||||
return cache(collectionAttribute.Name);
|
||||
}
|
||||
|
||||
// use base interface if available
|
||||
Type interfaceBaseType = type.GetInterfaces().FirstOrDefault(x => x.IsInterface && AcceptsZeroConventionsType.IsAssignableFrom(x) && x.Name != AcceptsZeroConventionsType.Name);
|
||||
|
||||
if (interfaceBaseType != null)
|
||||
{
|
||||
// use name from attribute if available
|
||||
collectionAttribute = interfaceBaseType.GetCustomAttribute<RavenCollectionAttribute>(true);
|
||||
if (collectionAttribute != null)
|
||||
{
|
||||
return cache(collectionAttribute.Name);
|
||||
}
|
||||
}
|
||||
|
||||
// use base type for polymorphism
|
||||
Type polymorphBaseType = PolymorphTypes.FirstOrDefault(x => type.IsSubclassOf(x));
|
||||
|
||||
if (polymorphBaseType != null)
|
||||
{
|
||||
return cache(DocumentConventions.DefaultGetCollectionName(polymorphBaseType));
|
||||
}
|
||||
|
||||
return cache(DocumentConventions.DefaultGetCollectionName(type));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Translates the types collection name to the document id prefix
|
||||
/// </summary>
|
||||
protected virtual string TransformTypeCollectionNameToDocumentIdPrefix(string name)
|
||||
{
|
||||
RavenOptions options = Options.For<RavenOptions>();
|
||||
if (options != null && !options.CollectionPrefix.IsNullOrWhiteSpace())
|
||||
{
|
||||
name = options.CollectionPrefix.EnsureEndsWith(IdentityPartsSeparator) + name.TrimStart(options.CollectionPrefix);
|
||||
}
|
||||
|
||||
return name.ToCamelCaseId();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface IZeroDocumentConventionsBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies internal rules to the RavenDB document conventions
|
||||
/// </summary>
|
||||
void Run(DocumentConventions conventions);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using Raven.Client.Documents;
|
||||
using Raven.Client.Documents.Session;
|
||||
|
||||
namespace zero.Persistence;
|
||||
|
||||
public class ZeroDocumentSession : AsyncDocumentSession, IZeroDocumentSession
|
||||
{
|
||||
public IZeroDocumentSession Core { get; private set; }
|
||||
|
||||
public IDocumentSession Synchronous => _synchronous ?? (_synchronous = DocumentStore.OpenSession(DatabaseName));
|
||||
|
||||
IDocumentSession _synchronous;
|
||||
|
||||
public ZeroDocumentSession(DocumentStore documentStore, Guid id, SessionOptions options, string coreDatabase = null) : base(documentStore, id, options)
|
||||
{
|
||||
if (coreDatabase.HasValue())
|
||||
{
|
||||
Core = new ZeroDocumentSession(documentStore, id, new SessionOptions()
|
||||
{
|
||||
Database = coreDatabase,
|
||||
DisableAtomicDocumentWritesInClusterWideTransaction = options.DisableAtomicDocumentWritesInClusterWideTransaction,
|
||||
NoCaching = options.NoCaching,
|
||||
NoTracking = options.NoTracking,
|
||||
RequestExecutor = options.RequestExecutor,
|
||||
TransactionMode = options.TransactionMode
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
Core = this;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsDisposed { get; set; }
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
_synchronous?.Dispose();
|
||||
base.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface IZeroDocumentSession : IAsyncDocumentSession
|
||||
{
|
||||
IZeroDocumentSession Core { get; }
|
||||
|
||||
IDocumentSession Synchronous { get; }
|
||||
|
||||
bool IsDisposed { get; }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace zero.Persistence;
|
||||
|
||||
public static class ZeroDocumentSessionExtensions
|
||||
{
|
||||
public static void SetCollection<T>(this IZeroDocumentSession session, T model, string collectionName)
|
||||
{
|
||||
session.Advanced.GetMetadataFor(model)[Raven.Client.Constants.Documents.Metadata.Collection] = collectionName;
|
||||
}
|
||||
|
||||
public static void Expires<T>(this IZeroDocumentSession session, T model, TimeSpan expires)
|
||||
{
|
||||
session.Advanced.GetMetadataFor(model)[Raven.Client.Constants.Documents.Metadata.Expires] = DateTime.UtcNow.AddSeconds(expires.TotalSeconds);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
using Raven.Client;
|
||||
using Raven.Client.Documents;
|
||||
using Raven.Client.Documents.BulkInsert;
|
||||
using Raven.Client.Documents.Operations;
|
||||
using Raven.Client.Documents.Queries;
|
||||
using Raven.Client.Documents.Session;
|
||||
|
||||
namespace zero.Persistence;
|
||||
|
||||
public class ZeroDocumentStore : DocumentStore, IZeroDocumentStore
|
||||
{
|
||||
public ZeroDocumentStore(IZeroOptions options) : base()
|
||||
{
|
||||
Options = options.For<RavenOptions>();
|
||||
Database = null;
|
||||
}
|
||||
|
||||
protected RavenOptions Options { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string ResolvedDatabase { get; set; }
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public OperationExecutor GetOperationExecutor(string database = null)
|
||||
{
|
||||
return Operations.ForDatabase(database ?? ResolvedDatabase);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IAsyncDocumentSession OpenAsyncSession(string database)
|
||||
{
|
||||
return OpenAsyncSession(new SessionOptions()
|
||||
{
|
||||
Database = database
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IAsyncDocumentSession OpenAsyncSession(SessionOptions options)
|
||||
{
|
||||
options.Database = options.Database ?? ResolvedDatabase;
|
||||
|
||||
AssertInitialized();
|
||||
EnsureNotClosed();
|
||||
|
||||
var sessionId = Guid.NewGuid();
|
||||
var session = new ZeroDocumentSession(this, sessionId, options, Options.Database);
|
||||
RegisterEvents(session);
|
||||
AfterSessionCreated(session);
|
||||
session.OnSessionDisposing += (sender, args) =>
|
||||
{
|
||||
session.IsDisposed = true;
|
||||
};
|
||||
|
||||
session.Advanced.WaitForIndexesAfterSaveChanges(TimeSpan.FromSeconds(0.5), throwOnTimeout: false);
|
||||
session.Advanced.DocumentStore.AggressivelyCacheFor(TimeSpan.FromHours(1), options.Database); // TODO I guess this will not work in backoffice when we want to see changes immediately
|
||||
// maybe use caching for frontend but not for backoffice?
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IAsyncDocumentSession OpenAsyncSession()
|
||||
{
|
||||
return OpenAsyncSession(new SessionOptions()
|
||||
{
|
||||
Database = ResolvedDatabase
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IDocumentSession OpenSession(SessionOptions options)
|
||||
{
|
||||
options.Database = options.Database ?? ResolvedDatabase;
|
||||
|
||||
AssertInitialized();
|
||||
EnsureNotClosed();
|
||||
|
||||
var sessionId = Guid.NewGuid();
|
||||
var session = new DocumentSession(this, sessionId, options);
|
||||
RegisterEvents(session);
|
||||
AfterSessionCreated(session);
|
||||
|
||||
session.Advanced.DocumentStore.AggressivelyCacheFor(TimeSpan.FromHours(1), options.Database);
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IDocumentSession OpenSession(string database)
|
||||
{
|
||||
return OpenSession(new SessionOptions()
|
||||
{
|
||||
Database = database
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IDocumentSession OpenSession()
|
||||
{
|
||||
return OpenSession(new SessionOptions()
|
||||
{
|
||||
Database = ResolvedDatabase
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override BulkInsertOperation BulkInsert(string database = null, CancellationToken token = default)
|
||||
{
|
||||
return base.BulkInsert(database ?? ResolvedDatabase, token);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task PurgeAsync<T>(string database = null, string querySuffix = null, Parameters parameters = null)
|
||||
{
|
||||
var collectionName = Conventions.FindCollectionName(typeof(T));
|
||||
var operationQuery = new DeleteByQueryOperation(new IndexQuery()
|
||||
{
|
||||
Query = $"from {collectionName} c {querySuffix ?? String.Empty}",
|
||||
QueryParameters = parameters
|
||||
}, new QueryOperationOptions { AllowStale = true });
|
||||
|
||||
Operation operation = await GetOperationExecutor(database ?? ResolvedDatabase).SendAsync(operationQuery);
|
||||
|
||||
await operation.WaitForCompletionAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface IZeroDocumentStore : IDocumentStore
|
||||
{
|
||||
/// <summary>
|
||||
/// The database which has been resolved from the current application
|
||||
/// </summary>
|
||||
string ResolvedDatabase { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Get operation executor
|
||||
/// </summary>
|
||||
OperationExecutor GetOperationExecutor(string database = null);
|
||||
|
||||
/// <summary>
|
||||
/// Purges a collection
|
||||
/// </summary>
|
||||
Task PurgeAsync<T>(string database = null, string querySuffix = null, Parameters parameters = null);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Raven.Client.Documents;
|
||||
using Raven.Client.Documents.Indexes;
|
||||
using Raven.Client.Http;
|
||||
|
||||
namespace zero.Persistence;
|
||||
|
||||
internal class ZeroPersistenceModule : ZeroModule
|
||||
{
|
||||
public override void ConfigureServices(IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddSingleton<IZeroDocumentConventionsBuilder, ZeroDocumentConventionsBuilder>();
|
||||
services.AddSingleton<IZeroDocumentStore, ZeroDocumentStore>(CreateRavenStore);
|
||||
services.AddScoped<IZeroStore, ZeroStore>();
|
||||
services.AddScoped<IZeroTokenProvider, ZeroTokenProvider>();
|
||||
|
||||
services.AddOptions<RavenOptions>().Bind(configuration.GetSection("Zero:Raven"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and configures the raven store
|
||||
/// </summary>
|
||||
protected ZeroDocumentStore CreateRavenStore(IServiceProvider services)
|
||||
{
|
||||
IZeroOptions options = services.GetService<IZeroOptions>();
|
||||
RavenOptions ravenOptions = options.For<RavenOptions>();
|
||||
IZeroDocumentConventionsBuilder conventionsBuilder = services.GetService<IZeroDocumentConventionsBuilder>();
|
||||
|
||||
IZeroDocumentStore store = new ZeroDocumentStore(options)
|
||||
{
|
||||
Database = ravenOptions.DatabaseIsDefault ? ravenOptions.Database : null,
|
||||
Urls = new string[1] { ravenOptions.Url },
|
||||
Conventions =
|
||||
{
|
||||
AggressiveCache =
|
||||
{
|
||||
Duration = TimeSpan.FromHours(1),
|
||||
Mode = AggressiveCacheMode.TrackChanges
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
conventionsBuilder.Run(store.Conventions);
|
||||
|
||||
IDocumentStore raven = store.Initialize();
|
||||
|
||||
// create all indexes
|
||||
if (options.Initialized)
|
||||
{
|
||||
var indexes = ravenOptions.Indexes.BuildAll(options, store);
|
||||
IndexCreation.CreateIndexes(indexes, store, database: ravenOptions.Database);
|
||||
}
|
||||
|
||||
return (ZeroDocumentStore)raven;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
using Raven.Client.Documents.Session;
|
||||
|
||||
namespace zero.Persistence;
|
||||
|
||||
public class ZeroStore : IZeroStore
|
||||
{
|
||||
public ZeroStore(IZeroDocumentStore raven, IZeroOptions options) : base()
|
||||
{
|
||||
Options = options;
|
||||
Raven = raven;
|
||||
//Database = null;
|
||||
}
|
||||
|
||||
protected IZeroOptions Options { get; set; }
|
||||
protected Dictionary<string, IZeroDocumentSession> ScopedSessions { get; set; } = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
string _resolvedDatabase = null;
|
||||
public string ResolvedDatabase
|
||||
{
|
||||
get => _resolvedDatabase;
|
||||
set
|
||||
{
|
||||
_resolvedDatabase = value;
|
||||
Raven.ResolvedDatabase = value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public IZeroDocumentStore Raven { get; private set; }
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public IZeroDocumentSession Session(string database = null, ZeroSessionResolution resolution = ZeroSessionResolution.Reuse, SessionOptions options = null)
|
||||
{
|
||||
database ??= ResolvedDatabase;
|
||||
options ??= new SessionOptions() { Database = database };
|
||||
|
||||
if (!database.HasValue())
|
||||
{
|
||||
return Session(true, database, resolution, options);
|
||||
}
|
||||
|
||||
if (resolution == ZeroSessionResolution.Create)
|
||||
{
|
||||
return Raven.OpenAsyncSession(options) as IZeroDocumentSession;
|
||||
}
|
||||
|
||||
if (!ScopedSessions.TryGetValue(database, out IZeroDocumentSession session))
|
||||
{
|
||||
session = Raven.OpenAsyncSession(options) as IZeroDocumentSession;
|
||||
ScopedSessions.TryAdd(database, session);
|
||||
}
|
||||
|
||||
if (session.IsDisposed)
|
||||
{
|
||||
session = Raven.OpenAsyncSession(options) as IZeroDocumentSession;
|
||||
ScopedSessions.Remove(database);
|
||||
ScopedSessions.TryAdd(database, session);
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public IZeroDocumentSession Session(bool global, string database = null, ZeroSessionResolution resolution = ZeroSessionResolution.Reuse, SessionOptions options = null)
|
||||
{
|
||||
return Session(global ? Options.For<RavenOptions>().Database : database, resolution, options);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public enum ZeroSessionResolution
|
||||
{
|
||||
Reuse = 0,
|
||||
Create = 1
|
||||
}
|
||||
|
||||
|
||||
public interface IZeroStore
|
||||
{
|
||||
/// <summary>
|
||||
/// The database which has been resolved from the current application
|
||||
/// </summary>
|
||||
string ResolvedDatabase { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Get underlying raven document store
|
||||
/// </summary>
|
||||
IZeroDocumentStore Raven { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Use a specific session
|
||||
/// </summary>
|
||||
IZeroDocumentSession Session(string database = null, ZeroSessionResolution resolution = ZeroSessionResolution.Reuse, SessionOptions options = null);
|
||||
|
||||
/// <summary>
|
||||
/// Use a session for the core database
|
||||
/// </summary>
|
||||
IZeroDocumentSession Session(bool global, string database = null, ZeroSessionResolution resolution = ZeroSessionResolution.Reuse, SessionOptions options = null);
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Mvc.Razor;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.AspNetCore.Mvc.ViewEngines;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text.Encodings.Web;
|
||||
|
||||
namespace zero.Rendering;
|
||||
|
||||
public class RazorRenderer : IRazorRenderer, IDisposable
|
||||
{
|
||||
protected IRazorViewEngine ViewEngine { get; set; }
|
||||
|
||||
protected IRazorPageActivator PageActivator { get; set; }
|
||||
|
||||
protected ITempDataDictionaryFactory TempDataDictionaryFactory { get; set; }
|
||||
|
||||
protected IModelMetadataProvider ModelMetadataProvider { get; set; }
|
||||
|
||||
protected IHttpContextAccessor HttpContextAccessor { get; set; }
|
||||
|
||||
protected IServiceScope ServiceScope { get; set; }
|
||||
|
||||
protected HtmlHelperOptions HtmlHelperOptions { get; set; }
|
||||
|
||||
|
||||
public RazorRenderer(IRazorViewEngine viewEngine, IRazorPageActivator pageActivator, IHttpContextAccessor httpContextAccessor, ITempDataDictionaryFactory tempDataDictionaryFactory,
|
||||
IModelMetadataProvider modelMetadataProvider, IServiceProvider serviceProvider, IOptions<MvcViewOptions> mvcHelperOptions)
|
||||
{
|
||||
ViewEngine = viewEngine;
|
||||
PageActivator = pageActivator;
|
||||
HttpContextAccessor = httpContextAccessor;
|
||||
TempDataDictionaryFactory = tempDataDictionaryFactory;
|
||||
ModelMetadataProvider = modelMetadataProvider;
|
||||
ServiceScope = serviceProvider.CreateScope();
|
||||
HtmlHelperOptions = mvcHelperOptions.Value.HtmlHelperOptions;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Renders a razor component to a string
|
||||
/// </summary>
|
||||
public async Task<string> ComponentAsync<T>(object args = null) where T : ViewComponent
|
||||
{
|
||||
return await ComponentAsync(typeof(T), args);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Renders a razor component to a string
|
||||
/// </summary>
|
||||
public async Task<string> ComponentAsync(Type componentType, object args = null)
|
||||
{
|
||||
return await ComponentAsync(componentType, BuildActionContext(), args);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Renders a razor component to a string
|
||||
/// </summary>
|
||||
public async Task<string> ComponentAsync(string componentName, object args = null)
|
||||
{
|
||||
return await ComponentAsync(componentName, BuildActionContext(), args);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Renders a razor component to a string
|
||||
/// </summary>
|
||||
public async Task<string> ComponentAsync<T>(ActionContext context, object args = null) where T : ViewComponent
|
||||
{
|
||||
return await ComponentAsync(typeof(T), context, args);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Renders a razor component to a string
|
||||
/// </summary>
|
||||
public async Task<string> ComponentAsync(Type componentType, ActionContext context, object args = null)
|
||||
{
|
||||
IViewComponentHelper viewComponentHelper = context.HttpContext.RequestServices.GetRequiredService<IViewComponentHelper>();
|
||||
|
||||
using StringWriter stringWriter = new();
|
||||
|
||||
ViewContext viewContext = BuildViewContext(context, stringWriter);
|
||||
|
||||
(viewComponentHelper as IViewContextAware)?.Contextualize(viewContext);
|
||||
IHtmlContent result = await viewComponentHelper.InvokeAsync(componentType, args);
|
||||
|
||||
result.WriteTo(stringWriter, HtmlEncoder.Default);
|
||||
await stringWriter.FlushAsync();
|
||||
return stringWriter.ToString();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Renders a razor component to a string
|
||||
/// </summary>
|
||||
public async Task<string> ComponentAsync(string componentName, ActionContext context, object args = null)
|
||||
{
|
||||
IViewComponentHelper viewComponentHelper = context.HttpContext.RequestServices.GetRequiredService<IViewComponentHelper>();
|
||||
|
||||
using StringWriter stringWriter = new();
|
||||
|
||||
ViewContext viewContext = BuildViewContext(context, stringWriter);
|
||||
|
||||
(viewComponentHelper as IViewContextAware)?.Contextualize(viewContext);
|
||||
IHtmlContent result = await viewComponentHelper.InvokeAsync(componentName, args);
|
||||
|
||||
result.WriteTo(stringWriter, HtmlEncoder.Default);
|
||||
await stringWriter.FlushAsync();
|
||||
return stringWriter.ToString();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Renders a razor page to a string
|
||||
/// </summary>
|
||||
public async Task<string> PageAsync<T>(T model) where T : PageModel
|
||||
{
|
||||
IRazorPage pageResult = FindPage(model.PageContext, model.PageContext.ActionDescriptor.RelativePath);
|
||||
|
||||
using StringWriter stringWriter = new();
|
||||
|
||||
RazorView view = new RazorView(ViewEngine, PageActivator, new List<IRazorPage>(), pageResult, HtmlEncoder.Default, new DiagnosticListener("ViewRenderService"));
|
||||
|
||||
ViewContext viewContext = BuildViewContext(model.PageContext, stringWriter, view);
|
||||
viewContext.RouteData = model.RouteData;
|
||||
viewContext.ViewData = model.ViewData;
|
||||
|
||||
Microsoft.AspNetCore.Mvc.RazorPages.Page razorPage = (Microsoft.AspNetCore.Mvc.RazorPages.Page)pageResult;
|
||||
razorPage.PageContext = model.PageContext;
|
||||
razorPage.ViewContext = viewContext;
|
||||
|
||||
PageActivator.Activate(razorPage, viewContext);
|
||||
|
||||
await razorPage.ExecuteAsync();
|
||||
await stringWriter.FlushAsync();
|
||||
|
||||
return stringWriter.ToString();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Renders a razor view to a string
|
||||
/// </summary>
|
||||
public async Task<string> ViewAsync(string view, object model = null)
|
||||
{
|
||||
return await ViewAsync(BuildActionContext(), view, model);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Renders a razor view to a string
|
||||
/// </summary>
|
||||
public async Task<string> ViewAsync(ActionContext context, string view, object model = null)
|
||||
{
|
||||
IView viewResult = FindView(context, view);
|
||||
|
||||
using StringWriter stringWriter = new();
|
||||
|
||||
ViewContext viewContext = BuildViewContext(context, stringWriter, viewResult);
|
||||
viewContext.RouteData = context.RouteData;
|
||||
viewContext.ViewData.Model = model;
|
||||
|
||||
await viewResult.RenderAsync(viewContext);
|
||||
await stringWriter.FlushAsync();
|
||||
|
||||
return stringWriter.ToString();
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
ServiceScope?.Dispose();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Build the view context
|
||||
/// </summary>
|
||||
protected virtual ViewContext BuildViewContext(ActionContext context, StringWriter writer, IView view = null)
|
||||
{
|
||||
ViewDataDictionary viewData = new(ModelMetadataProvider, context.ModelState); // result.ViewData ?? new ViewData...;
|
||||
ITempDataDictionary tempData = TempDataDictionaryFactory.GetTempData(context.HttpContext); // result.TempData ?? TempData...;
|
||||
|
||||
return new ViewContext(context, view ?? NullView.Instance, viewData, tempData, writer, HtmlHelperOptions);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Builds a new view context
|
||||
/// </summary>
|
||||
protected virtual ActionContext BuildActionContext()
|
||||
{
|
||||
HttpContext context = GetHttpContext();
|
||||
RouteData routeData = context.GetRouteData();
|
||||
return new ActionContext(context, routeData, new ActionDescriptor());
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get HTTP context or mock one
|
||||
/// </summary>
|
||||
protected virtual HttpContext GetHttpContext()
|
||||
{
|
||||
HttpContext context = HttpContextAccessor.HttpContext;
|
||||
context ??= new DefaultHttpContext()
|
||||
{
|
||||
RequestServices = ServiceScope.ServiceProvider
|
||||
};
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Tries to find a view
|
||||
/// </summary>
|
||||
protected virtual IView FindView(ActionContext actionContext, string viewName)
|
||||
{
|
||||
ViewEngineResult getViewResult = ViewEngine.GetView(executingFilePath: null, viewPath: viewName, isMainPage: false);
|
||||
if (getViewResult.Success)
|
||||
{
|
||||
return getViewResult.View;
|
||||
}
|
||||
|
||||
ViewEngineResult findViewResult = ViewEngine.FindView(actionContext, viewName, isMainPage: false);
|
||||
if (findViewResult.Success)
|
||||
{
|
||||
return findViewResult.View;
|
||||
}
|
||||
|
||||
IEnumerable<string> searchedLocations = getViewResult.SearchedLocations.Concat(findViewResult.SearchedLocations);
|
||||
string errorMessage = String.Join(Environment.NewLine, new[] { $"Unable to find view '{viewName}'. The following locations were searched:" }.Concat(searchedLocations));
|
||||
|
||||
throw new InvalidOperationException(errorMessage);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Tries to find a page
|
||||
/// </summary>
|
||||
protected virtual IRazorPage FindPage(ActionContext actionContext, string pageName)
|
||||
{
|
||||
RazorPageResult getPageResult = ViewEngine.GetPage(executingFilePath: null, pagePath: pageName);
|
||||
if (getPageResult.Page != null)
|
||||
{
|
||||
return getPageResult.Page;
|
||||
}
|
||||
|
||||
RazorPageResult findPageResult = ViewEngine.FindPage(actionContext, pageName: pageName);
|
||||
if (findPageResult.Page != null)
|
||||
{
|
||||
return findPageResult.Page;
|
||||
}
|
||||
|
||||
IEnumerable<string> searchedLocations = getPageResult.SearchedLocations.Concat(findPageResult.SearchedLocations);
|
||||
string errorMessage = String.Join(Environment.NewLine, new[] { $"Unable to find page '{pageName}'. The following locations were searched:" }.Concat(searchedLocations));
|
||||
|
||||
throw new InvalidOperationException(errorMessage);
|
||||
}
|
||||
|
||||
|
||||
class GenericController : ControllerBase { }
|
||||
|
||||
|
||||
class NullView : IView
|
||||
{
|
||||
public static readonly NullView Instance = new NullView();
|
||||
|
||||
public string Path => string.Empty;
|
||||
|
||||
public Task RenderAsync(ViewContext context)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
public interface IRazorRenderer
|
||||
{
|
||||
/// <summary>
|
||||
/// Renders a razor component to a string
|
||||
/// </summary>
|
||||
Task<string> ComponentAsync<T>(object args = null) where T : ViewComponent;
|
||||
|
||||
/// <summary>
|
||||
/// Renders a razor component to a string
|
||||
/// </summary>
|
||||
Task<string> ComponentAsync(Type componentType, object args = null);
|
||||
|
||||
/// <summary>
|
||||
/// Renders a razor component to a string
|
||||
/// </summary>
|
||||
Task<string> ComponentAsync(string componentName, object args = null);
|
||||
|
||||
/// <summary>
|
||||
/// Renders a razor component to a string
|
||||
/// </summary>
|
||||
Task<string> ComponentAsync<T>(ActionContext context, object args = null) where T : ViewComponent;
|
||||
|
||||
/// <summary>
|
||||
/// Renders a razor component to a string
|
||||
/// </summary>
|
||||
Task<string> ComponentAsync(Type componentType, ActionContext context, object args = null);
|
||||
|
||||
/// <summary>
|
||||
/// Renders a razor component to a string
|
||||
/// </summary>
|
||||
Task<string> ComponentAsync(string componentName, ActionContext context, object args = null);
|
||||
|
||||
/// <summary>
|
||||
/// Renders a razor page to a string
|
||||
/// </summary>
|
||||
Task<string> PageAsync<T>(T model) where T : PageModel;
|
||||
|
||||
/// <summary>
|
||||
/// Renders a razor view to a string
|
||||
/// </summary>
|
||||
Task<string> ViewAsync(string view, object model = null);
|
||||
|
||||
/// <summary>
|
||||
/// Renders a razor view to a string
|
||||
/// </summary>
|
||||
Task<string> ViewAsync(ActionContext context, string view, object model = null);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace zero.Rendering;
|
||||
|
||||
public class ZeroRenderingModule : ZeroModule
|
||||
{
|
||||
public override void ConfigureServices(IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddScoped<IRazorRenderer, RazorRenderer>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace zero;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static ZeroBuilder AddZero(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
return new ZeroBuilder(services, configuration, null);
|
||||
}
|
||||
|
||||
public static ZeroBuilder AddZero(this IServiceCollection services, IConfiguration configuration, Action<IZeroStartupOptions> setupAction)
|
||||
{
|
||||
return new ZeroBuilder(services, configuration, setupAction);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace zero.Stores;
|
||||
|
||||
|
||||
public class ConfigureFlavorJsonOptions : IConfigureOptions<JsonOptions>
|
||||
{
|
||||
private readonly IZeroOptions _zeroOptions;
|
||||
|
||||
public ConfigureFlavorJsonOptions(IZeroOptions options)
|
||||
{
|
||||
_zeroOptions = options;
|
||||
}
|
||||
|
||||
public void Configure(JsonOptions options)
|
||||
{
|
||||
options.JsonSerializerOptions.Converters.Add(new JsonFlavorVariantConverterFactory(_zeroOptions));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace zero.Stores;
|
||||
|
||||
/// <summary>
|
||||
/// A flavor config holds information about a flavor implementation
|
||||
/// </summary>
|
||||
public class FlavorConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of the associated entity
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public Type FlavorType { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Alias for querying
|
||||
/// </summary>
|
||||
public string Alias { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Name of the flavor
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional description
|
||||
/// </summary>
|
||||
public string Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Icon of the flavor
|
||||
/// </summary>
|
||||
public string Icon { get; set; }
|
||||
|
||||
|
||||
[JsonIgnore]
|
||||
public Func<FlavorConfig, object> Construct { get; set; }
|
||||
|
||||
|
||||
public FlavorConfig(Type type)
|
||||
{
|
||||
FlavorType = type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace zero.Stores;
|
||||
|
||||
public class FlavorOptions
|
||||
{
|
||||
public ConcurrentDictionary<Type, FlavorProvider> Providers { get; private set; } = new();
|
||||
|
||||
public void Configure<TEntity>(Action<FlavorProviderOptions<TEntity>> configure) where TEntity : class, ISupportsFlavors, new()
|
||||
{
|
||||
Type type = typeof(TEntity);
|
||||
FlavorProvider provider = Providers.GetOrAdd(type, _ => CreateProvider<TEntity>());
|
||||
configure(new FlavorProviderOptions<TEntity>(this, provider));
|
||||
}
|
||||
|
||||
|
||||
public bool CanUseWithoutFlavors<TEntity>() where TEntity : class, ISupportsFlavors, new()
|
||||
{
|
||||
return Providers.GetValueOrDefault(typeof(TEntity), new()).CanUseWithoutFlavors;
|
||||
}
|
||||
|
||||
public string DefaultFlavorFor<TEntity>() where TEntity : class, ISupportsFlavors, new()
|
||||
{
|
||||
FlavorProvider provider = Providers.GetValueOrDefault(typeof(TEntity), new());
|
||||
|
||||
if (!provider.Flavors.Any())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (provider.DefaultFlavor.HasValue() && provider.Flavors.Any(x => x.Alias == provider.DefaultFlavor))
|
||||
{
|
||||
return provider.DefaultFlavor;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public TFlavor Construct<TEntity, TFlavor>(string alias = null)
|
||||
where TEntity : class, ISupportsFlavors, new()
|
||||
where TFlavor : class, TEntity, new()
|
||||
{
|
||||
// if no flavor provider is registered we return the base entity
|
||||
if (!Providers.TryGetValue(typeof(TEntity), out FlavorProvider provider))
|
||||
{
|
||||
return new TFlavor();
|
||||
}
|
||||
|
||||
// throw if this entity is not allowed to be created without a flavor
|
||||
if (alias.IsNullOrEmpty() && !provider.CanUseWithoutFlavors)
|
||||
{
|
||||
string defaultFlavorAlias = provider.DefaultFlavor;
|
||||
|
||||
if (defaultFlavorAlias.IsNullOrEmpty() || !provider.Flavors.Any(x => x.Alias == defaultFlavorAlias))
|
||||
{
|
||||
throw new ArgumentException("Can not create instance of an entity which is configured to to be only used as a flavor", nameof(alias));
|
||||
}
|
||||
|
||||
alias = defaultFlavorAlias;
|
||||
}
|
||||
|
||||
// return default instance if no flavor is required
|
||||
if (alias.IsNullOrEmpty())
|
||||
{
|
||||
return provider.FlavorlessConstruct() as TFlavor;
|
||||
}
|
||||
|
||||
// try to load and construct a specific flavor
|
||||
FlavorConfig config = provider.Flavors.FirstOrDefault(x => x.Alias == alias);
|
||||
TFlavor result = config?.Construct(config) as TFlavor;
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
result.Flavor = alias;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
public IEnumerable<FlavorConfig> GetAll<TEntity>() => GetAll(typeof(TEntity));
|
||||
|
||||
|
||||
public IEnumerable<FlavorConfig> GetAll(Type type) => Providers.GetValueOrDefault(type, new()).Flavors;
|
||||
|
||||
|
||||
public FlavorConfig Get<TEntity, TFlavor>() => Get(typeof(TEntity), typeof(TFlavor));
|
||||
|
||||
|
||||
public FlavorConfig Get<TEntity, TFlavor>(string alias) => Get(typeof(TEntity), typeof(TFlavor), alias);
|
||||
|
||||
|
||||
public FlavorConfig Get<TEntity>(string alias) => Get(typeof(TEntity), alias);
|
||||
|
||||
|
||||
public FlavorConfig Get(Type baseType, Type flavorType)
|
||||
{
|
||||
FlavorProvider provider = Providers.GetValueOrDefault(baseType, new());
|
||||
return provider.Flavors.FirstOrDefault(x => x.FlavorType == flavorType);
|
||||
}
|
||||
|
||||
|
||||
public FlavorConfig Get(Type baseType, Type flavorType, string alias)
|
||||
{
|
||||
FlavorProvider provider = Providers.GetValueOrDefault(baseType, new());
|
||||
return provider.Flavors.FirstOrDefault(x => x.FlavorType == flavorType && x.Alias == alias);
|
||||
}
|
||||
|
||||
|
||||
public FlavorConfig Get(Type baseType, string alias)
|
||||
{
|
||||
FlavorProvider provider = Providers.GetValueOrDefault(baseType, new());
|
||||
return provider.Flavors.FirstOrDefault(x => x.Alias == alias);
|
||||
}
|
||||
|
||||
|
||||
public bool Exists<TEntity>(string alias)
|
||||
{
|
||||
FlavorProvider provider = Providers.GetValueOrDefault(typeof(TEntity), new());
|
||||
return provider.Flavors.Any(x => x.Alias == alias);
|
||||
}
|
||||
|
||||
|
||||
public void Add<TEntity, TFlavor>(string alias, string name, string description = null, string icon = null)
|
||||
where TEntity : class, ISupportsFlavors, new()
|
||||
where TFlavor : TEntity, new()
|
||||
{
|
||||
Add<TEntity, TFlavor>(typeof(TFlavor), _ => new TFlavor(), alias, name, description, icon);
|
||||
}
|
||||
|
||||
|
||||
public void Add<TEntity, TFlavor>(Func<FlavorConfig, TFlavor> construct, string alias, string name, string description = null, string icon = null)
|
||||
where TEntity : class, ISupportsFlavors, new()
|
||||
where TFlavor : TEntity, new()
|
||||
{
|
||||
Add<TEntity, TFlavor>(typeof(TFlavor), construct, alias, name, description, icon);
|
||||
}
|
||||
|
||||
|
||||
public void Add<TEntity, TFlavor>(Type flavorType, Func<FlavorConfig, TFlavor> construct, string alias, string name, string description = null, string icon = null)
|
||||
where TEntity : class, ISupportsFlavors, new()
|
||||
where TFlavor : TEntity, new()
|
||||
{
|
||||
Add<TEntity, TFlavor>(new(flavorType)
|
||||
{
|
||||
Alias = alias,
|
||||
Name = name,
|
||||
Description = description,
|
||||
Icon = icon,
|
||||
Construct = _ => construct(_)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public void Add<TEntity, TFlavor>(FlavorConfig config)
|
||||
where TEntity : class, ISupportsFlavors, new()
|
||||
where TFlavor : TEntity, new()
|
||||
{
|
||||
Type baseEntityType = typeof(TEntity);
|
||||
FlavorProvider provider = Providers.GetOrAdd(baseEntityType, _ => CreateProvider<TEntity>());
|
||||
provider.Flavors.Add(config);
|
||||
}
|
||||
|
||||
|
||||
public void Implement<TEntity, TDefaultImplementation>()
|
||||
where TEntity : class, ISupportsFlavors, new()
|
||||
where TDefaultImplementation : TEntity, new()
|
||||
{
|
||||
Type baseEntityType = typeof(TEntity);
|
||||
FlavorProvider provider = Providers.GetOrAdd(baseEntityType, _ => CreateProvider<TEntity>());
|
||||
|
||||
provider.FlavorlessConstruct = () => new TDefaultImplementation();
|
||||
provider.FlavorlessType = typeof(TDefaultImplementation);
|
||||
}
|
||||
|
||||
|
||||
FlavorProvider CreateProvider<TEntity>()
|
||||
where TEntity : class, ISupportsFlavors, new()
|
||||
{
|
||||
return new FlavorProvider()
|
||||
{
|
||||
BaseType = typeof(TEntity),
|
||||
FlavorlessConstruct = () => new TEntity(),
|
||||
FlavorlessType = typeof(TEntity),
|
||||
ConverterCreator = cfg => new JsonFlavorVariantConverter<TEntity>(cfg)
|
||||
};
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user