This commit is contained in:
2021-11-19 14:59:24 +01:00
parent 942161d6b9
commit c4b439dbb7
456 changed files with 4073 additions and 580 deletions
@@ -0,0 +1,252 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using zero.Core.Api;
using zero.Core.Entities;
using zero.Core.Identity;
using zero.Core.Options;
namespace zero.Backoffice;
[ZeroAuthorize]
//[ServiceFilter(typeof(ModelStateValidationFilterAttribute))]
[ApiController]
[Route("getsreplaced/[controller]/[action]")]
//[ServiceFilter(typeof(BackofficeFilterAttribute))]
public abstract class ZeroBackofficeController : ControllerBase
{
IZeroOptions _options;
public bool IsCoreDatabase { get; protected set; }
protected IZeroOptions Options => _options ?? (_options = HttpContext?.RequestServices?.GetService<IZeroOptions>());
/// <summary>
/// Is execuated when the scope changes.
/// The scope is evaluated by the BackofficeFilterAttribute.
/// </summary>
public virtual void OnScopeChanged(string scope) { }
/// <summary>
/// Creates an edit model with appropriate options and permissions
/// </summary>
public EditModel<T> Edit<T>(T data, bool typed = true, Action<EditModel<T>> transform = null) where T : ZeroIdEntity
{
Type type = typeof(T);
//ControllerContext.ActionDescriptor.FilterDescriptors[0].
if (data == null)
{
return null;
}
EditModel<T> model = new EditModel<T>();
model.Entity = data;
model.Meta.Token = null; // Token.Get(data);
//model.Meta.IsAppAware = AppAwareType.IsAssignableFrom(type); // TODO appx
//model.Meta.CanBeShared = canBeShared;
model.Meta.CanCreate = true;
//model.Meta.CanCreateShared = canBeShared;
model.Meta.CanEdit = true;
model.Meta.CanDelete = true;
model.Meta.IsShared = IsCoreDatabase;
transform?.Invoke(model);
return model;
}
/// <summary>
/// Creates an edit model with appropriate options and permissions
/// </summary>
public TWrapper Edit<T, TWrapper>(TWrapper data, bool typed = true, Action<EditModel<T>> transform = null)
where T : ZeroIdEntity
where TWrapper : EditModel<T>, new()
{
Type type = typeof(T);
//ControllerContext.ActionDescriptor.FilterDescriptors[0].
data.Meta.Token = null; // Token.Get(data.Entity);
//data.Meta.IsAppAware = AppAwareType.IsAssignableFrom(type); // TODO appx
//data.Meta.CanBeShared = canBeShared;
data.Meta.CanCreate = true;
//data.Meta.CanCreateShared = canBeShared;
data.Meta.CanEdit = true;
data.Meta.CanDelete = true;
data.Meta.IsShared = IsCoreDatabase;
transform?.Invoke(data);
return data;
}
public IList<PreviewModel> Previews<T>(Dictionary<string, T> items, Func<T, PreviewModel> transform)
{
IList<PreviewModel> previews = new List<PreviewModel>();
foreach (var item in items)
{
bool exists = item.Value != null;
if (!exists)
{
previews.Add(new PreviewModel()
{
HasError = true,
Icon = "fth-alert-circle color-red",
Id = item.Key,
Name = "@errors.preview.notfound",
Text = "@errors.preview.notfound_text"
});
}
else
{
previews.Add(transform(item.Value));
}
}
return previews;
}
public IList<PreviewModel> Previews<T>(Dictionary<string, T> items, Action<T, PreviewModel> transform = null) where T : ZeroIdEntity
{
IList<PreviewModel> previews = new List<PreviewModel>();
foreach (var item in items)
{
if (item.Value == null)
{
previews.Add(new PreviewModel()
{
HasError = true,
Icon = "fth-alert-circle color-red",
Id = item.Key,
Name = "@errors.preview.notfound",
Text = "@errors.preview.notfound_text"
});
}
else
{
PreviewModel model = new()
{
Id = item.Value.Id
};
if (item.Value is ZeroEntity)
{
model.Name = (item.Value as ZeroEntity).Name;
}
transform?.Invoke(item.Value, model);
previews.Add(model);
}
}
return previews;
}
public async Task<IList<SelectModel>> SelectList<T>(IAsyncEnumerable<T> enumerable, Action<T, SelectModel> transform = null) where T : ZeroIdEntity
{
List<SelectModel> items = new List<SelectModel>();
await foreach (T item in enumerable)
{
SelectModel model = new()
{
Id = item.Id
};
if (item is ZeroEntity)
{
model.Name = (item as ZeroEntity).Name;
model.IsActive = (item as ZeroEntity).IsActive;
}
transform?.Invoke(item, model);
items.Add(model);
}
return items;
}
public async Task<IList<PreviewModel>> Previews<T>(Dictionary<string, T> items, Func<T, Task<PreviewModel>> transform)
{
IList<PreviewModel> previews = new List<PreviewModel>();
foreach (var item in items)
{
bool exists = item.Value != null;
if (!exists)
{
previews.Add(new PreviewModel()
{
HasError = true,
Icon = "fth-alert-circle color-red",
Id = item.Key,
Name = "@errors.preview.notfound",
Text = "@errors.preview.notfound_text"
});
}
else
{
previews.Add(await transform(item.Value));
}
}
return previews;
}
protected IActionResult Download(Stream stream, string filename)
{
if (stream == null)
{
// TODO add success property + return error response
}
if (filename.Contains("{date}"))
{
filename = filename.Replace("{date}", DateTimeOffset.Now.ToString("yyyy-MM-dd"));
}
var provider = new FileExtensionContentTypeProvider();
if (filename == null || !provider.TryGetContentType(Path.GetFileName(filename), out string contentType))
{
contentType = "application/octet-stream";
}
Response.Headers.Add("zero-filename", filename);
return base.File(stream, contentType, filename, true);
}
protected IActionResult File(Core.Entities.FileResult file)
{
if (file == null)
{
return NotFound();
}
FileStream stream = System.IO.File.OpenRead(file.Path);
return File(stream, file.ContentType, file.Filename);
}
}
@@ -0,0 +1,71 @@
using Microsoft.AspNetCore.Mvc;
using Raven.Client.Documents.Linq;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using zero.Core;
using zero.Core.Entities;
namespace zero.Backoffice;
public abstract class ZeroBackofficeCollectionController<TEntity, TCollection> : ZeroBackofficeController
where TEntity : ZeroIdEntity, new()
where TCollection : IEntityCollection<TEntity>
{
protected TCollection Collection { get; private set; }
[Obsolete]
protected Func<IRavenQueryable<TEntity>, IRavenQueryable<TEntity>> DefaultQuery { get; set; }
protected Action<TEntity, PreviewModel> PreviewTransform { get; set; }
protected Action<TEntity, SelectModel> PickerTransform { get; set; }
public ZeroBackofficeCollectionController(TCollection collection)
{
Collection = collection;
}
public override void OnScopeChanged(string scope)
{
//Collection.ApplyScope(scope);
}
public virtual async Task<EditModel<TEntity>> GetById([FromQuery] string id, [FromQuery] string changeVector = null) => Edit(await Collection.Load(id, changeVector));
public virtual async Task<Dictionary<string, TEntity>> GetByIds([FromQuery] string[] ids) => await Collection.Load(ids);
public virtual async Task<EditModel<TEntity>> GetEmpty() => Edit(await Collection.Empty());
public virtual async Task<ListResult<TEntity>> GetByQuery([FromQuery] ListBackofficeQuery<TEntity> query)
{
return await Collection.Load(query);
}
public virtual async Task<ListResult<Revision>> GetRevisions([FromQuery] string id, [FromQuery] ListBackofficeQuery<TEntity> query)
{
return null; // TODO
//return await Collection.GetRevisions(id, query.Page, query.PageSize);
}
public virtual async Task<IEnumerable<SelectModel>> GetForPicker() => await SelectList(Collection.Stream(), PickerTransform);
public virtual async Task<IList<PreviewModel>> GetPreviews([FromQuery] List<string> ids) => Previews(await Collection.Load(ids), PreviewTransform);
[HttpPost]
public virtual async Task<EntityResult<TEntity>> Save([FromBody] TEntity model) => await Collection.Save(model);
[HttpDelete]
public virtual async Task<EntityResult<TEntity>> Delete([FromQuery] string id) => await Collection.Delete(id);
}
+23
View File
@@ -0,0 +1,23 @@
//using Microsoft.AspNetCore.Mvc;
//using Raven.Client.Documents;
//using Raven.Client.Documents.Linq;
//using System.Threading.Tasks;
//using zero;
//namespace zero.Backoffice
//{
// [ZeroAuthorize(Permissions.Settings.Countries, PermissionsValue.Read)]
// public class CountriesController : ZeroBackofficeCollectionController<Country, ICountriesCollection>
// {
// public CountriesController(ICountriesCollection collection) : base(collection)
// {
// PreviewTransform = (item, model) => model.Icon = "flag-" + item.Code.ToLowerInvariant();
// }
// public override Task<ListResult<Country>> GetByQuery([FromQuery] ListBackofficeQuery<Country> query)
// {
// query.OrderQuery = q => q.OrderByDescending(x => x.IsPreferred).ThenBy(x => x.Name);
// return Collection.Load<zero_Countries>(query);
// }
// }
//}
+61
View File
@@ -0,0 +1,61 @@
using System;
namespace zero.Backoffice;
public class EditModel : EditModel<object> { }
public class EditModel<T>
{
/// <summary>
/// Model
/// </summary>
public T Entity { get; set; }
/// <summary>
/// Meta data
/// </summary>
public EditMetaModel Meta { get; set; } = new();
}
public class EditMetaModel
{
/// <summary>
/// Whether an entity of this type can be created
/// </summary>
public bool CanCreate { get; set; }
/// <summary>
/// Whether an entity of this type can be created in the shared app space
/// </summary>
public bool CanCreateShared { get; set; }
/// <summary>
/// Whether this entity can be edited or only viewed
/// </summary>
public bool CanEdit { get; set; }
/// <summary>
/// Whether this entity can be deleted
/// </summary>
public bool CanDelete { get; set; }
/// <summary>
/// Wehther this entity is application aware
/// </summary>
public bool IsAppAware { get; set; }
/// <summary>
/// Whether this entity can be shared across applications (only for IsAppAware=true)
/// </summary>
public bool CanBeShared { get; set; }
public bool IsShared { get; set; }
/// <summary>
/// The change token maps to a database entity which holds ID and collection of the model to edit
/// If these values do not match the entity on save it is rejected
/// // TODO expiration expiry session.Advanced.GetMetadataFor(user)[Raven.Client.Constants.Documents.Metadata.Expires] = DateTime.UtcNow.AddMinutes(60);
/// </summary>
public string Token { get; set; }
}
+15
View File
@@ -0,0 +1,15 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
namespace zero.Backoffice;
internal class BackofficeModule : ZeroModule
{
/// <inheritdoc />
public override void Register(IZeroModuleConfiguration config)
{
config.Services.TryAddEnumerable(ServiceDescriptor.Transient<IConfigureOptions<MvcOptions>, ZeroBackofficeMvcOptions>());
}
}
@@ -0,0 +1,32 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using System;
namespace zero.Backoffice;
public class ZeroBackofficeControllerModelConvention : IControllerModelConvention
{
readonly AttributeRouteModel RouteModel;
readonly Type BaseClass = typeof(ZeroBackofficeController);
public ZeroBackofficeControllerModelConvention(string backofficePath)
{
RouteModel = new AttributeRouteModel(new RouteAttribute(backofficePath + "/api/[controller]/[action]"));
}
public void Apply(ControllerModel controller)
{
if (!controller.ControllerType.IsSubclassOf(BaseClass))
{
return;
}
foreach (var selector in controller.Selectors)
{
selector.AttributeRouteModel = RouteModel;
}
}
}
@@ -0,0 +1,19 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
namespace zero.Backoffice;
class ZeroBackofficeMvcOptions : IConfigureOptions<MvcOptions>
{
IZeroOptions Options { get; set; }
public ZeroBackofficeMvcOptions(IZeroOptions options)
{
Options = options;
}
public void Configure(MvcOptions options)
{
//options.Conventions.Add(new ZeroBackofficeControllerModelConvention(Options.BackofficePath));
}
}
+14
View File
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<PackageId>zero.Backoffice</PackageId>
<Version>0.1.0</Version>
<LangVersion>preview</LangVersion>
<TargetFramework>net6.0</TargetFramework>
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
</Project>
@@ -0,0 +1,14 @@
namespace zero;
public interface IZeroRouteEntity
{
/// <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; }
}
+76
View File
@@ -0,0 +1,76 @@
using Newtonsoft.Json;
using System;
using System.Diagnostics;
namespace zero;
[DebuggerDisplay("Id = {Id,nq}, Name = {Name}, Alias = {Alias}")]
public class ZeroEntity : ZeroIdEntity, IZeroDbConventions, IZeroRouteEntity
{
/// <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>
/// Configuration of the base entity (which this one inherits from)
/// </summary>
public BlueprintConfiguration Blueprint { get; set; }
/// <summary>
/// Language of the entity
/// </summary>
public string LanguageId { get; set; }
/// <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; }
}
+9
View File
@@ -0,0 +1,9 @@
namespace zero;
public class ZeroIdEntity
{
/// <summary>
/// Id of the entity
/// </summary>
public string Id { get; set; }
}
+47
View File
@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
namespace zero;
/// <summary>
/// An application is a website. zero can host multiple websites at once which share common assets
/// </summary>
[RavenCollection("Applications")]
public class Application : ZeroEntity
{
/// <summary>
/// Raven database name for application data
/// </summary>
public string Database { get; set; }
/// <summary>
/// Full company or product name
/// </summary>
public string FullName { get; set; }
/// <summary>
/// Generic contact email. Can be used in various locations
/// </summary>
public string Email { get; set; }
/// <summary>
/// Image of the application
/// </summary>
public string ImageId { get; set; }
/// <summary>
/// Simple image of the application (can be used as favicon)
/// </summary>
public string IconId { get; set; }
/// <summary>
/// All assigned domains for this application
/// </summary>
public Uri[] Domains { get; set; } = Array.Empty<Uri>();
/// <summary>
/// Features which are enabled for this application.
/// Can be user-defined and affect both backoffice and frontend
/// </summary>
public List<string> Features { get; set; } = new();
}
@@ -0,0 +1,12 @@
namespace zero;
public class ApplicationOptions
{
public ApplicationOptions()
{
EnableMultiple = false;
}
public bool EnableMultiple { get; set; }
}
@@ -0,0 +1,164 @@
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace zero;
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;
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;
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,30 @@
using System;
using System.Collections.Generic;
namespace zero;
/// <summary>
/// Defines a base entity which is synced and properties which are overridden
/// </summary>
public class BlueprintConfiguration
{
/// <summary>
/// Id of the entity the synchronisation is based upon
/// </summary>
public string Id { get; set; }
/// <summary>
/// A shallow copy of a blueprint can not be changed and is always fully synchronised with the parent entity
/// </summary>
public bool IsShallow { get; set; }
/// <summary>
/// Properties which are not synced and have their own values
/// </summary>
public string[] Desync { get; set; } = Array.Empty<string>();
/// <summary>
/// Additional custom sync options
/// </summary>
public Dictionary<string, string> Options = new();
}
@@ -0,0 +1,33 @@
using System;
using zero.Core.Blueprints;
namespace zero;
public class BlueprintOptions : ZeroBackofficeCollection<Blueprint>, IZeroCollectionOptions
{
public BlueprintOptions()
{
Enabled = false;
}
public bool Enabled { get; set; }
public void Add<T>() where T : Blueprint, new()
{
Items.Add(new T());
}
public void Add<T>(Blueprint<T> implementation) where T : ZeroEntity, new()
{
Items.Add(implementation);
}
public void Add<T>(Action<Blueprint<T>> createExpression = null) where T : ZeroEntity, new()
{
Items.Add(new DefaultBlueprint<T>(createExpression));
}
}
+8
View File
@@ -0,0 +1,8 @@
namespace zero;
public class ZeroModule
{
public virtual void Register(IZeroModuleConfiguration config) { }
public virtual void Configure(IZeroOptions options) { }
}
@@ -0,0 +1,25 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace zero;
public class ZeroModuleConfiguration : IZeroModuleConfiguration
{
public IServiceCollection Services { get; }
public IConfiguration Configuration { get; }
internal ZeroModuleConfiguration(IServiceCollection servicse, IConfiguration configuration)
{
Services = servicse;
Configuration = configuration;
}
}
public interface IZeroModuleConfiguration
{
IServiceCollection Services { get; }
IConfiguration Configuration { get; }
}
@@ -0,0 +1,21 @@
using zero.Core.Entities;
namespace zero;
public class FeatureOptions : OptionsEnumerable<Feature>, IOptionsEnumerable
{
public void Add<T>() where T : Feature, new()
{
Items.Add(new T());
}
public void Add(string alias, string name, string description)
{
Items.Add(new Feature()
{
Alias = alias,
Name = name,
Description = description
});
}
}
@@ -0,0 +1,24 @@
using zero.Core.Entities;
namespace zero;
public class IconOptions : OptionsEnumerable<IconSet>, IOptionsEnumerable
{
/// <summary>
/// Add a new backoffice icon set
/// </summary>
/// <param name="alias">Alias for reference</param>
/// <param name="name">Name of the icon set</param>
/// <param name="spritePath">Path to the SVG sprite containing addressable symbols</param>
/// <param name="prefix">Optional symbol identifier prefix (by default the alias is used)</param>
public void AddSet(string alias, string name, string spritePath, string prefix = null)
{
Items.Add(new IconSet()
{
Alias = alias,
Name = name,
SpritePath = spritePath,
Prefix = prefix ?? alias
});
}
}
@@ -0,0 +1,42 @@
using FluentValidation;
using System;
using System.Collections.Generic;
using zero.Core.Integrations;
namespace zero;
public class IntegrationOptions : OptionsEnumerable<IntegrationType>, IOptionsEnumerable
{
public void Add<T>(IntegrationType<T> integration) where T : Integration, new()
{
Items.Add(IntegrationType.Convert(integration));
}
public void Add<T>(string alias, string name, string description, List<string> tags = default, string imagePath = null, IValidator validator = null) where T : Integration, new()
{
Items.Add(new IntegrationType(typeof(T))
{
Alias = alias,
Name = name,
Description = description,
ImagePath = imagePath,
Tags = tags,
Validator = validator
});
}
public void Add(Type type, string alias, string name, string description, List<string> tags = default, string imagePath = null, IValidator validator = null)
{
Items.Add(new IntegrationType(type)
{
Alias = alias,
Name = name,
Description = description,
ImagePath = imagePath,
Tags = tags,
Validator = validator
});
}
}
@@ -0,0 +1,65 @@
using System;
using zero.Core.Collections;
namespace zero;
public class InterceptorOptions : OptionsEnumerable<InterceptorRegistration>, IOptionsEnumerable
{
public void Add<T>(int gravity = 0, Func<Type, bool> canHandle = null) where T : ICollectionInterceptor
{
Type type = typeof(T);
if (canHandle == null)
{
canHandle = _ => true;
}
Items.Add(new InterceptorRegistration()
{
Hash = IdGenerator.Create(),
Name = type.Name,
Gravity = gravity,
CanHandle = canHandle,
InterceptorType = type
});
}
public void Add<T, TBoxed>(int gravity = 0, Func<Type, bool> canHandle = null)
where T : ICollectionInterceptor<TBoxed>
where TBoxed : ZeroEntity
{
Type type = typeof(T);
Type boxedType = typeof(TBoxed);
Func<Type, bool> finalCanHandle = requestedType =>
{
return boxedType.IsAssignableFrom(requestedType) && (canHandle == null || canHandle.Invoke(requestedType));
};
Items.Add(new InterceptorRegistration()
{
Hash = IdGenerator.Create(),
Name = type.Name,
Gravity = gravity,
CanHandle = finalCanHandle,
InterceptorType = type,
IsInterceptorBoxed = true
});
}
}
public class InterceptorRegistration
{
public int Gravity { get; set; }
public Type InterceptorType { get; set; }
public string Hash { get; set; }
public string Name { get; set; }
public Func<Type, bool> CanHandle { get; set; }
public bool IsInterceptorBoxed { get; set; }
}
@@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using zero.Core.Entities;
namespace zero;
public class ModuleOptions : OptionsEnumerable<ModuleType>, IOptionsEnumerable
{
public void Add<T>(ModuleType<T> moduleType) where T : Module, new()
{
Items.Add(ModuleType.Convert(moduleType));
}
public void Add<T>(string alias, string name, string description, string icon, string group = null, List<string> tags = null, List<string> disallowedPageTypes = null) where T : Module, new()
{
Items.Add(new ModuleType(typeof(T))
{
Alias = alias,
Name = name,
Description = description,
Icon = icon,
Group = group,
Tags = tags ?? new List<string>(),
DisallowedPageTypes = disallowedPageTypes ?? new List<string>()
});
}
public void Add(Type type, string alias, string name, string description, string icon, string group = null, List<string> tags = null, List<string> disallowedPageTypes = null)
{
Items.Add(new ModuleType(type)
{
Alias = alias,
Name = name,
Description = description,
Icon = icon,
Group = group,
Tags = tags ?? new List<string>(),
DisallowedPageTypes = disallowedPageTypes ?? new List<string>()
});
}
}
@@ -0,0 +1,50 @@
using System;
using System.Linq;
using zero.Core.Entities;
namespace zero;
public class PageOptions : OptionsEnumerable<PageType>, IOptionsEnumerable
{
public PageOptions()
{
}
public string Root { get; set; } = Constants.Pages.DefaultRootPageTypeAlias;
public void Add<T>(PageType<T> pageType) where T : Page, new()
{
Items.Add(PageType.Convert(pageType));
}
public void Add<T>(string alias, string name, string description, string icon) where T : Page, new()
{
Items.Add(new PageType(typeof(T))
{
Alias = alias,
Name = name,
Description = description,
Icon = icon
});
}
public void Add(Type type, string alias, string name, string description, string icon)
{
Items.Add(new PageType(type)
{
Alias = alias,
Name = name,
Description = description,
Icon = icon
});
}
public PageType GetByAlias(string alias)
{
return Items.FirstOrDefault(x => x.Alias == alias);
}
}
@@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.Linq;
using zero.Core.Identity;
namespace zero;
public class PermissionOptions : OptionsEnumerable<PermissionCollection>, IOptionsEnumerable
{
public void AddCollection(PermissionCollection collection, int index = -1)
{
if (index > -1 && index < Items.Count)
{
Items.Insert(index, collection);
}
else
{
Items.Add(collection);
}
}
public void AddCollection<T>(int index = -1) where T : PermissionCollection, new()
{
if (index > -1 && index < Items.Count)
{
Items.Insert(index, new T());
}
else
{
Items.Add(new T());
}
}
public void Add(string collectionAlias, Permission permission, int index = -1)
{
PermissionCollection collection = Items.FirstOrDefault(x => x.Alias.Equals(collectionAlias, StringComparison.InvariantCultureIgnoreCase));
if (collection == null)
{
// TODO handle error
return;
}
IList<Permission> items = collection.Items;
if (index > -1 && index < items.Count)
{
items.Insert(index, permission);
}
else
{
items.Add(permission);
}
}
}
@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Linq;
using zero.Core.Routing;
namespace zero;
public class RoutingEndpointOptions
{
HashSet<ResolverMap> Resolvers { get; set; } = new();
class ResolverMap
{
public Type Type { get; set; }
public Func<IRouteModel, RouteEndpoint> Impl { get; set; }
}
public void Add<T>(Func<T, RouteEndpoint> resolver) where T : class, IRouteModel
{
Resolvers.Add(new()
{
Type = typeof(T),
Impl = obj => resolver(obj as T)
});
}
public void Replace<T>(Func<T, RouteEndpoint> resolver) where T : class, IRouteModel
{
Remove<T>();
Add(resolver);
}
public void Remove<T>() where T : IRouteModel
{
Type type = typeof(T);
Resolvers.RemoveWhere(x => x.Type == type);
}
public Func<IRouteModel, RouteEndpoint> Get<T>() where T : IRouteModel => Get(typeof(T));
public IEnumerable<Func<IRouteModel, RouteEndpoint>> GetAll<T>() where T : IRouteModel => GetAll(typeof(T));
public Func<IRouteModel, RouteEndpoint> Get(Type type)
{
ResolverMap map = Resolvers.LastOrDefault(x => x.Type == type);
return map?.Impl;
}
public IEnumerable<Func<IRouteModel, RouteEndpoint>> GetAll(Type type)
{
IEnumerable<ResolverMap> maps = Resolvers.Where(x => x.Type == type);
return maps.Select(map => map.Impl).Where(x => x != null);
}
}
@@ -0,0 +1,40 @@
using zero.Core.Routing;
namespace zero;
public class RoutingOptions
{
public RoutingOptions()
{
PageRouteIdBuilder = new PageRouteIdBuilder();
DefaultEndpoint = new("ZeroFrontend", "Index");
EndpointResolvers = new();
PageResolvers = new();
//ErrorReexecutionPath = "/error";
NotFoundEndpoint = null;
}
public IPageRouteIdBuilder PageRouteIdBuilder { get; set; }
public RouteEndpoint NotFoundEndpoint { get; set; }
public RouteEndpoint DefaultEndpoint { get; set; }
/// <summary>
///
/// </summary>
public RoutingEndpointOptions EndpointResolvers { get; set; }
/// <summary>
///
/// </summary>
public RoutingPageResolverOptions PageResolvers { get; set; }
/// <summary>
///
/// </summary>
public string ErrorReexecutionPath { get; set; }
}
@@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using zero.Core.Entities;
namespace zero;
public class RoutingPageResolverOptions
{
HashSet<ResolverMap> Resolvers { get; set; } = new();
class ResolverMap
{
public Type Type { get; set; }
public Expression<Func<Page, bool>> Impl { get; set; }
}
public void Add<T>(Expression<Func<Page, bool>> resolver)
{
Resolvers.Add(new()
{
Type = typeof(T),
Impl = resolver
});
}
public void Replace<T>(Expression<Func<Page, bool>> resolver)
{
Remove<T>();
Add<T>(resolver);
}
public void Remove<T>()
{
Type type = typeof(T);
Resolvers.RemoveWhere(x => x.Type == type);
}
public Expression<Func<Page, bool>> Get<T>() => Get(typeof(T));
public IEnumerable<Expression<Func<Page, bool>>> GetAll<T>() => GetAll(typeof(T));
public Expression<Func<Page, bool>> Get(Type type)
{
ResolverMap map = Resolvers.LastOrDefault(x => x.Type == type);
return map?.Impl;
}
public IEnumerable<Expression<Func<Page, bool>>> GetAll(Type type)
{
IEnumerable<ResolverMap> maps = Resolvers.Where(x => x.Type == type);
return maps.Select(map => map.Impl).Where(x => x != null);
}
}
@@ -0,0 +1,147 @@
using Raven.Client.Documents;
using System;
using System.Linq;
using System.Threading.Tasks;
using zero.Core.Models;
using zero.Core.Renderer;
namespace zero;
public class SearchOptions : OptionsEnumerable<SearchIndexMap>, IOptionsEnumerable
{
public bool IsEnabled { get; set; }
public SearchOptions()
{
IsEnabled = true;
//Map<Page>().Display((x, res, opts) =>
//{
// PageType pageType = opts.Pages.GetByAlias(x.PageTypeAlias);
// if (pageType != null)
// {
// res.Icon = pageType.Icon;
// }
// res.Url = "/pages/edit/" + x.Id;
//});
//Map<MediaFolder>("fth-image");
}
public SearchIndexMap<T> Map<T>(string icon = null) where T : ZeroEntity, new()
{
SearchIndexMap<T> map = new(icon);
Items.Add(map);
return map;
}
}
public class SearchIndexMap
{
internal Type Type;
internal string _Icon;
protected string _group;
protected string[] _fields;
protected Func<ZeroEntity, SearchResult, IZeroOptions, Task> _modify;
const string mapTemplate = @"map('{collection}', function (x) {
return {
Id: x.Id,
Group: '{group}',
Name: x.Name,
IsActive: x.IsActive,
Fields: [{fields}]
};
});";
internal SearchIndexMap(Type type, string icon = null)
{
Type = type;
_group = "__TODO";
_Icon = icon;
}
internal string BuildInstruction(IDocumentStore store)
{
return TokenReplacement.Apply(mapTemplate, new()
{
{ "collection", store.Conventions.GetCollectionName(Type) },
{ "group", _group },
{ "fields", BuildFieldArray(_fields) }
});
}
internal string BuildFieldArray(string[] fields)
{
if (fields == null || !fields.Any())
{
return String.Empty;
}
return "x." + String.Join(", x.", fields);
}
internal bool CanModify(Type type)
{
return Type.IsAssignableFrom(type);
}
internal async Task Modify(ZeroEntity entity, SearchResult result, IZeroOptions options)
{
if (_modify != null)
{
await _modify(entity, result, options);
}
}
}
public class SearchIndexMap<T> : SearchIndexMap where T : ZeroEntity
{
internal SearchIndexMap(string icon = null) : base(typeof(T), icon) {}
public SearchIndexMap<T> Icon(string icon)
{
_Icon = icon;
return this;
}
public SearchIndexMap<T> Display(Action<T, SearchResult> modify = null)
{
_modify = (x, res, opts) =>
{
modify?.Invoke(x as T, res);
return Task.CompletedTask;
};
return this;
}
public SearchIndexMap<T> Display(Func<T, SearchResult, Task> modify = null)
{
_modify = (x, res, opts) => modify?.Invoke(x as T, res);
return this;
}
public SearchIndexMap<T> Display(Action<T, SearchResult, IZeroOptions> modify = null)
{
_modify = (x, res, opts) =>
{
modify?.Invoke(x as T, res, opts);
return Task.CompletedTask;
};
return this;
}
public SearchIndexMap<T> Display(Func<T, SearchResult, IZeroOptions, Task> modify = null)
{
_modify = (x, res, opts) => modify?.Invoke(x as T, res, opts);
return this;
}
public SearchIndexMap<T> Fields(params string[] fieldNames)
{
_fields = fieldNames;
return this;
}
}
@@ -0,0 +1,29 @@
using zero.Core.Entities;
namespace zero;
public class SectionOptions : OptionsEnumerable<ISection>, IOptionsEnumerable
{
public SectionOptions()
{
}
public void Add<T>(int index = -1) where T : ISection, new()
{
if (index > -1 && index < Items.Count)
{
Items.Insert(index, new T());
}
else
{
Items.Add(new T());
}
}
public void Add(string alias, string name, string icon)
{
Items.Add(new Section(alias, name, icon));
}
}
@@ -0,0 +1,9 @@
namespace zero;
public class ServiceOptions
{
/// <summary>
/// Define a YouTube API Key so the zero videopicker can display previews
/// </summary>
public string YouTubeApiKey { get; set; }
}
@@ -0,0 +1,52 @@
using System.Linq;
using zero.Core.Entities;
namespace zero;
public class SettingsOptions : OptionsEnumerable<SettingsGroup>, IOptionsEnumerable
{
public void AddGroup<T>(int index = -1) where T : SettingsGroup, new()
{
if (index > -1 && index < Items.Count)
{
Items.Insert(index, new T());
}
else
{
Items.Add(new T());
}
}
public void AddGroup(SettingsGroup group, int index = -1)
{
if (index > -1 && index < Items.Count)
{
Items.Insert(index, group);
}
else
{
Items.Add(group);
}
}
public void AddToDefaultGroup(SettingsArea item, int index = -1)
{
SettingsGroup group = Items.FirstOrDefault(x => x.Name == "@settings.groups.system");
if (group == null)
{
return;
}
if (index > -1 && index < group.Items.Count)
{
group.Items.Insert(index, item);
}
else
{
group.Items.Add(item);
}
}
}
@@ -0,0 +1,86 @@
using System.Linq;
using zero.Core.Entities;
namespace zero;
public class SpaceOptions : OptionsEnumerable<Space>, IOptionsEnumerable
{
public SpaceOptions()
{
}
public void Add<T>() where T : Space, new()
{
Items.Add(new T());
}
public void AddList<T>(string alias, string name, string description, string icon) where T : SpaceContent, new()
{
Items.Add(new Space()
{
Alias = alias,
View = SpaceView.List,
Name = name,
Description = description,
Icon = icon,
Type = typeof(T)
});
}
public void AddEditor<T>(string alias, string name, string description, string icon) where T : SpaceContent, new()
{
Items.Add(new Space()
{
Alias = alias,
View = SpaceView.Editor,
Name = name,
Description = description,
Icon = icon,
Type = typeof(T)
});
}
public void AddSeparator()
{
Space lastSpace = Items.LastOrDefault();
if (lastSpace != null)
{
lastSpace.LineBelow = true;
}
}
public void AddCustom<T>(string componentPath, string alias, string name, string description, string icon) where T : SpaceContent, new()
{
Items.Add(new Space()
{
Alias = alias,
View = SpaceView.Custom,
ComponentPath = componentPath,
Name = name,
Description = description,
Icon = icon,
Type = typeof(T)
});
}
public void AddCustom(string componentPath, string alias, string name, string description, string icon)
{
Items.Add(new Space()
{
Alias = alias,
View = SpaceView.Custom,
ComponentPath = componentPath,
Name = name,
Description = description,
Icon = icon
});
}
}
+89
View File
@@ -0,0 +1,89 @@
namespace zero;
public static class Constants
{
public const string ErrorFieldNone = "__zero_no_field";
public static class Tabs
{
public const string General = "general";
}
public static class Auth
{
public const string SystemUser = "system";
public const string DefaultScheme = "zeroScheme";
public const string BackofficeDisplayName = "Zero Bacckoffice";
public const string BackofficeScheme = "zeroBackoffice";
public const string BackofficeCookieName = "zero.be.session";
public const string DefaultCookieName = "zero.session";
public static 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 class Database
{
public const string ReservationPrefix = "zero.";
public const string CoreIdPrefix = "core.";
public const string Expires = Raven.Client.Constants.Documents.Metadata.Expires;
}
public static 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 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 class PermissionCollections
{
public const string Sections = "permissionCollectionSections";
public const string Spaces = "permissionCollectionSpaces";
public const string Settings = "permisssionCollectionSettings";
public const string Modules = "permissionCollectionModules";
}
public static class Pages
{
public const string FolderAlias = "zero.folder";
public const string DefaultRootPageTypeAlias = "root";
public const string PageRouteProviderAlias = "zero.pages";
}
public static class Routing
{
public const string InternalRoutePrefix = "/__zero/";
public const string ErrorRoute = InternalRoutePrefix + "error";
}
}
+17
View File
@@ -0,0 +1,17 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace zero;
internal class ConfigurationModule : ZeroModule
{
/// <inheritdoc />
public override void Register(IZeroModuleConfiguration config)
{
config.Services.AddOptions<ZeroOptions>()
.Bind(config.Configuration.GetSection("Zero"))
.Configure(opts => opts.ZeroVersion = "0.0.1.0" /*// TODO*/);
config.Services.AddTransient<IZeroOptions>(factory => factory.GetService<IOptionsMonitor<ZeroOptions>>().CurrentValue);
}
}
@@ -0,0 +1,29 @@
using System.Collections.Generic;
namespace zero;
public abstract class OptionsEnumerable<T>
{
protected List<T> Items { get; set; } = new List<T>();
public IReadOnlyCollection<T> GetAllItems()
{
return Items.AsReadOnly();
}
public void RemoveAt(int index)
{
Items.RemoveAt(index);
}
public bool Remove(T item)
{
return Items.Remove(item);
}
}
public interface IOptionsEnumerable { }
+27
View File
@@ -0,0 +1,27 @@
using FluentValidation;
using System;
namespace zero;
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; }
}
+249
View File
@@ -0,0 +1,249 @@
using System;
using System.Collections.Generic;
namespace zero;
public class ZeroOptions : IZeroOptions
{
public ZeroOptions()
{
SupportedLanguages = new string[2] { "en-US", "de-DE" };
DefaultLanguage = SupportedLanguages[0];
TokenExpiration = 60 * 3;
BackofficePath = "/zero";
ExcludedPaths = new() { };
Raven = new()
{
CollectionPrefix = String.Empty
};
Sections = new();
Features = new();
Pages = new();
Modules = new();
Permissions = new();
Settings = new();
Spaces = new();
Integrations = new();
Icons = new();
Services = new();
Blueprints = new();
Routing = new();
Interceptors = new();
Applications = new();
//Raven.Indexes.Add<Backoffice_Search>();
//Raven.Indexes.Add<Media_ByChildren>();
//Raven.Indexes.Add<Media_ByParent>();
//Raven.Indexes.Add<MediaFolder_ByHierarchy>();
//Raven.Indexes.Add<MediaFolders_WithChildren>();
//Raven.Indexes.Add<Pages_AsHistory>();
//Raven.Indexes.Add<Pages_ByHierarchy>();
//Raven.Indexes.Add<Pages_WithChildren>();
//Raven.Indexes.Add<Pages_ByType>();
//Raven.Indexes.Add<Routes_ForResolver>();
//Raven.Indexes.Add<Routes_ByDependencies>();
//Raven.Indexes.Add<zero_Countries>();
//Raven.Indexes.Add<zero_Languages>();
//Raven.Indexes.Add<zero_Translations>();
//Raven.Indexes.Add<zero_MailTemplates>();
//Raven.Indexes.Add<zero_Spaces>();
//Raven.Indexes.Add<zero_RecycledEntities>();
Search = new();
}
/// <inheritdoc />
public bool SetupCompleted => !String.IsNullOrEmpty(Raven?.Database);
/// <inheritdoc />
public string ZeroVersion { get; set; }
/// <inheritdoc />
public string DefaultLanguage { get; set; }
/// <inheritdoc />
public string[] SupportedLanguages { get; private set; }
/// <inheritdoc />
public int TokenExpiration { get; set; }
/// <inheritdoc />
public RavenOptions Raven { get; set; }
/// <inheritdoc />
public ApplicationOptions Applications { get; set; }
/// <inheritdoc />
public string BackofficePath { get; set; }
/// <summary>
/// Paths in the backoffice which are not handled by zero
/// </summary>
public List<string> ExcludedPaths { get; private set; }
/// <inheritdoc />
//public IZeroPluginConfiguration Backoffice { get; set; }
/// <inheritdoc />
public SectionOptions Sections { get; private set; }
/// <inheritdoc />
public FeatureOptions Features { get; private set; }
/// <inheritdoc />
public PageOptions Pages { get; private set; }
/// <inheritdoc />
public ModuleOptions Modules { get; private set; }
/// <inheritdoc />
public PermissionOptions Permissions { get; private set; }
/// <inheritdoc />
public SettingsOptions Settings { get; private set; }
/// <inheritdoc />
public SpaceOptions Spaces { get; private set; }
/// <inheritdoc />
public IntegrationOptions Integrations { get; private set; }
/// <inheritdoc />
public IconOptions Icons { get; private set; }
/// <inheritdoc />
public ServiceOptions Services { get; private set; }
/// <inheritdoc />
public BlueprintOptions Blueprints { get; private set; }
/// <inheritdoc />
public RoutingOptions Routing { get; private set; }
/// <inheritdoc />
public InterceptorOptions Interceptors { get; private set; }
/// <inheritdoc />
public SearchOptions Search { get; private set; }
}
public interface IZeroOptions
{
/// <summary>
/// Whether the zero setup has already been completed and the instance is ready for use
/// </summary>
bool SetupCompleted { get; }
/// <summary>
/// The currently active version
/// This should not be set manually, as it is used for setup and migrations and incremented automatically
/// </summary>
string ZeroVersion { get; set; }
/// <summary>
/// Default language ISO code
/// </summary>
string DefaultLanguage { get; set; }
/// <summary>
/// Language ISO codes which are supported by the zero backoffice
/// </summary>
string[] SupportedLanguages { get; }
/// <summary>
/// Expiration in minutes of a generated change token for an entity
/// </summary>
int TokenExpiration { get; set; }
/// <summary>
/// RavenDB configuration data
/// </summary>
RavenOptions Raven { get; set; }
/// <summary>
/// Application options
/// </summary>
ApplicationOptions Applications { get; set; }
/// <summary>
/// URL path to the backoffice (defaults to /zero)
/// </summary>
string BackofficePath { get; set; }
/// <summary>
/// Paths in the backoffice which are not handled by zero
/// </summary>
List<string> ExcludedPaths { get; }
/// <summary>
///
/// </summary>
SectionOptions Sections { get; }
/// <summary>
///
/// </summary>
FeatureOptions Features { get; }
/// <summary>
///
/// </summary>
PageOptions Pages { get; }
/// <summary>
///
/// </summary>
ModuleOptions Modules { get; }
/// <summary>
///
/// </summary>
PermissionOptions Permissions { get; }
/// <summary>
///
/// </summary>
SettingsOptions Settings { get; }
/// <summary>
///
/// </summary>
SpaceOptions Spaces { get; }
/// <summary>
///
/// </summary>
IntegrationOptions Integrations { get; }
/// <summary>
///
/// </summary>
IconOptions Icons { get; }
/// <summary>
///
/// </summary>
ServiceOptions Services { get; }
/// <summary>
///
/// </summary>
BlueprintOptions Blueprints { get; }
/// <summary>
///
/// </summary>
RoutingOptions Routing { get; }
/// <summary>
///
/// </summary>
InterceptorOptions Interceptors { get; }
/// <summary>
///
/// </summary>
SearchOptions Search { get; }
}
@@ -0,0 +1,24 @@
using Microsoft.Extensions.DependencyInjection;
using System.Collections.Generic;
namespace zero;
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; }
}
+138 -139
View File
@@ -3,186 +3,185 @@ using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
namespace zero.Core.Extensions
namespace zero;
public static class StringExtensions
{
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)
{
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))
{
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;
}
return replaceMultipleSpacesRegex.Replace(value, SPACE).Trim();
}
public static string TrimStart(this string value, string forRemoving)
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))
{
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;
value = value.Remove(value.LastIndexOf(forRemoving, StringComparison.InvariantCultureIgnoreCase));
}
return value;
}
public static bool HasValue(this string input)
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))
{
return !String.IsNullOrWhiteSpace(input);
value = value.Substring(forRemoving.Length);
}
return value;
}
public static bool IsNullOrEmpty(this string input)
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 String.IsNullOrEmpty(input);
return Char.ToLowerInvariant(input[0]) + input.Substring(1);
}
return input;
}
public static bool IsNullOrWhiteSpace(this string input)
public static string ToPascalCase(this string input)
{
if (!String.IsNullOrEmpty(input) && input.Length > 1)
{
return String.IsNullOrWhiteSpace(input);
return Char.ToUpperInvariant(input[0]) + input.Substring(1);
}
return input;
}
public static string ToCamelCase(this string input)
public static string ToCamelCaseId(this string input)
{
if (String.IsNullOrEmpty(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 (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))
{
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 (input.Length < 2)
{
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()));
return input.ToUpperInvariant();
}
string[] parts = input.Split('.');
public static string ToPascalCaseId(this string input)
return String.Join(".", parts.Select(x => x.ToPascalCase()));
}
public static string RemoveNewLines(this string input)
{
if (String.IsNullOrEmpty(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()));
return input;
}
return newLineCharsRegex.Replace(input, String.Empty).Trim();
}
public static string RemoveNewLines(this string input)
public static string Shorten(this string input, int maxLength)
{
if (maxLength < 4)
{
if (String.IsNullOrEmpty(input))
{
return input;
}
return newLineCharsRegex.Replace(input, String.Empty).Trim();
throw new ArgumentOutOfRangeException("maxLength", "Has to be at least 4 characters long");
}
public static string Shorten(this string input, int maxLength)
if (String.IsNullOrEmpty(input) || input.Length <= 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) + "...";
return input;
}
return input.Substring(0, maxLength - 3) + "...";
}
}
@@ -0,0 +1,6 @@
namespace zero;
/// <summary>
/// Triggers custom Raven conventions for database operations
/// </summary>
public interface IZeroDbConventions { }
+36
View File
@@ -0,0 +1,36 @@
using System;
using System.Linq;
namespace zero;
public class IdGenerator
{
const string CHARS = "abcdefghijklmnopqrstuvwxyz0123456789";
private static Random random = new();
/// <summary>
/// Create a new unique Id
/// </summary>
public static string Create(int length = -1)
{
if (length < 1)
{
length = 12;
}
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();
}
}
+61
View File
@@ -0,0 +1,61 @@
using Microsoft.Extensions.DependencyInjection;
using Raven.Client.Documents;
using Raven.Client.Http;
using System;
namespace zero;
internal class PersistenceModule : ZeroModule
{
/// <inheritdoc />
public override void Register(IZeroModuleConfiguration config)
{
config.Services.AddSingleton<IZeroDocumentConventionsBuilder, ZeroDocumentConventionsBuilder>();
config.Services.AddSingleton<IZeroDocumentStore, ZeroDocumentStore>(CreateRavenStore);
config.Services.AddScoped<IZeroStore, ZeroStore>();
config.Services.AddScoped<IZeroTokenProvider, ZeroTokenProvider>();
}
/// <inheritdoc />
public override void Configure(IZeroOptions options)
{
base.Configure(options);
}
/// <summary>
/// Creates and configures the raven store
/// </summary>
protected ZeroDocumentStore CreateRavenStore(IServiceProvider services)
{
IZeroOptions options = services.GetService<IZeroOptions>();
IZeroDocumentConventionsBuilder conventionsBuilder = services.GetService<IZeroDocumentConventionsBuilder>();
IZeroDocumentStore store = new ZeroDocumentStore(options)
{
Urls = new string[1] { options.Raven.Url },
Conventions = // TODO activate and test this
{
AggressiveCache =
{
Duration = TimeSpan.FromHours(1),
Mode = AggressiveCacheMode.TrackChanges
}
}
};
conventionsBuilder.Run(store.Conventions);
IDocumentStore raven = store.Initialize();
// create all indexes
if (options.SetupCompleted)
{
//var indexes = options.Raven.Indexes.BuildAll(options, store);
//IndexCreation.CreateIndexes(indexes, store, database: options.Raven.Database);
}
return (ZeroDocumentStore)raven;
}
}
@@ -0,0 +1,17 @@
using System;
namespace zero;
/// <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;
}
}
+121
View File
@@ -0,0 +1,121 @@
using Raven.Client.Documents;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
namespace zero;
public class RavenOptions
{
public string Url { get; set; }
public string Database { get; set; }
public string CollectionPrefix { get; set; }
public RavenIndexesOptions Indexes { get; set; } = new();
}
public class RavenIndexesOptions : OptionsEnumerable<RavenIndexesOptions.Map>, IOptionsEnumerable
{
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()
{
Items.Add(new(typeof(T), () => new T()));
}
public void Add(Type indexType)
{
Items.Add(new(indexType, () => (IZeroIndexDefinition)Activator.CreateInstance(indexType)));
}
public void Add<T>(T index) where T : IZeroIndexDefinition
{
Items.Add(new(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 = Items.FirstOrDefault(x => x.Type == origin);
if (item != null)
{
Items.Remove(item);
}
Add(replaceWith);
}
public IEnumerable<IZeroIndexDefinition> BuildAll(IZeroOptions options, IDocumentStore store)
{
foreach (Map map in Items)
{
IZeroIndexDefinition index = map.CreateIndex.Compile().Invoke();
index.Setup(options, store);
index.RunModifiers(options);
yield return index;
}
}
}
public class RavenIndexModifiersOptions : OptionsEnumerable<RavenIndexModifiersOptions.Modifier>, IOptionsEnumerable
{
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()
{
Items.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 Items.Where(x => x.Type.IsAssignableFrom(type)))
{
yield return modifier;
}
}
}
@@ -0,0 +1,194 @@
//using Raven.Client.Documents;
//using Raven.Client.Documents.Linq;
//using Raven.Client.Documents.Session;
//using System.Collections.Generic;
//using System.Linq;
//using System.Threading.Tasks;
//namespace zero;
//public static class RavenQueryableExtensions
//{
// // TODO we need to simplify these extensions methods.
// // ToQueriedListAsyncX is used in MediaCollection for the Media_ByParent index, which produces MediaListItem (which is no ZeroEntity)
// /// <summary>
// ///
// /// </summary>
// public static async Task<ListResult<T>> ToQueriedListAsyncX<T>(this IRavenQueryable<T> queryable, ListQuery<T> query)
// {
// queryable = queryable.Statistics(out QueryStatistics stats);
// IQueryable<T> rawQuery = queryable;
// if (query != null)
// {
// if (!query.Search.IsNullOrEmpty() && query.SearchSelector != null)
// {
// rawQuery = rawQuery.SearchIf(query.SearchSelector, query.Search, "*", "*", Raven.Client.Documents.Queries.SearchOperator.And);
// }
// if (!query.Search.IsNullOrEmpty() && query.SearchSelectors.Length > 0)
// {
// foreach (var selector in query.SearchSelectors)
// {
// rawQuery = rawQuery.SearchIf(selector, query.Search, "*", "*", Raven.Client.Documents.Queries.SearchOperator.And);
// }
// }
// if (query.OrderQuery != null)
// {
// rawQuery = query.OrderQuery(rawQuery);
// }
// else if (!query.OrderBy.IsNullOrEmpty())
// {
// rawQuery = rawQuery.OrderBy(query.OrderBy, query.OrderIsDescending, query.OrderType == ListQueryOrderType.String ? OrderingType.String : OrderingType.Double);
// }
// if (query.PageSize > 0)
// {
// rawQuery = rawQuery.Paging(query.Page, query.PageSize);
// }
// }
// List<T> items = await rawQuery.ToListAsync();
// return new ListResult<T>(items, stats.TotalResults, query.Page, query.PageSize);
// }
// /// <summary>
// ///
// /// </summary>
// public static async Task<ListResult<T>> ToQueriedListAsync<T>(this IRavenQueryable<T> queryable, ListQuery<T> query) where T : ZeroEntity
// {
// queryable = queryable.Statistics(out QueryStatistics stats);
// IQueryable<T> rawQuery = queryable;
// if (query != null)
// {
// if (!query.IncludeInactive)
// {
// rawQuery = rawQuery.Where(x => x.IsActive);
// }
// if (!query.Search.IsNullOrEmpty() && query.SearchSelectors.Length > 0)
// {
// foreach (var selector in query.SearchSelectors)
// {
// rawQuery = rawQuery.SearchIf(selector, query.Search, "*", "*", Raven.Client.Documents.Queries.SearchOperator.And);
// }
// }
// else if (!query.Search.IsNullOrEmpty() && query.SearchSelector != null)
// {
// rawQuery = rawQuery.SearchIf(query.SearchSelector, query.Search, "*", "*", Raven.Client.Documents.Queries.SearchOperator.And);
// }
// if (query.OrderQuery != null)
// {
// rawQuery = query.OrderQuery(rawQuery);
// }
// else if (!query.OrderBy.IsNullOrEmpty())
// {
// rawQuery = rawQuery.OrderBy(query.OrderBy, query.OrderIsDescending, query.OrderType == ListQueryOrderType.String ? OrderingType.String : OrderingType.Double);
// }
// else
// {
// rawQuery = rawQuery.OrderByDescending(x => x.CreatedDate);
// }
// if (query.PageSize > 0)
// {
// rawQuery = rawQuery.Paging(query.Page, query.PageSize);
// }
// }
// List<T> items = await rawQuery.ToListAsync();
// return new ListResult<T>(items, stats.TotalResults, query.Page, query.PageSize);
// }
// /// <summary>
// ///
// /// </summary>
// public static async Task<ListResult<T>> ToQueriedListAsyncX<T, TFilter>(this IRavenQueryable<T> queryable, ListQuery<T, TFilter> query) where TFilter : IListSpecificQuery
// {
// queryable = queryable.Statistics(out QueryStatistics stats);
// IQueryable<T> rawQuery = queryable;
// if (query != null)
// {
// if (!query.Search.IsNullOrEmpty() && query.SearchSelector != null)
// {
// rawQuery = rawQuery.SearchIf(query.SearchSelector, query.Search, "*", "*");
// }
// if (!query.Search.IsNullOrEmpty() && query.SearchSelectors.Length > 0)
// {
// foreach (var selector in query.SearchSelectors)
// {
// rawQuery = rawQuery.SearchIf(selector, query.Search, "*", "*", Raven.Client.Documents.Queries.SearchOperator.Or);
// }
// }
// if (!query.OrderBy.IsNullOrEmpty())
// {
// rawQuery = rawQuery.OrderBy(query.OrderBy, query.OrderIsDescending, query.OrderType == ListQueryOrderType.String ? OrderingType.String : OrderingType.Double);
// }
// if (query.PageSize > 0)
// {
// rawQuery = rawQuery.Paging(query.Page, query.PageSize);
// }
// }
// List<T> items = await rawQuery.ToListAsync();
// return new ListResult<T>(items, stats.TotalResults, query.Page, query.PageSize);
// }
// /// <summary>
// ///
// /// </summary>
// public static async Task<ListResult<T>> ToQueriedListAsync<T, TFilter>(this IRavenQueryable<T> queryable, ListQuery<T, TFilter> query) where T : ZeroEntity where TFilter : IListSpecificQuery
// {
// queryable = queryable.Statistics(out QueryStatistics stats);
// IQueryable<T> rawQuery = queryable;
// if (query != null)
// {
// if (!query.IncludeInactive)
// {
// rawQuery = rawQuery.Where(x => x.IsActive);
// }
// if (!query.Search.IsNullOrEmpty() && query.SearchSelector != null)
// {
// rawQuery = rawQuery.SearchIf(query.SearchSelector, query.Search, "*", "*");
// }
// if (!query.Search.IsNullOrEmpty() && query.SearchSelectors.Length > 0)
// {
// foreach (var selector in query.SearchSelectors)
// {
// rawQuery = rawQuery.SearchIf(selector, query.Search, "*", "*", Raven.Client.Documents.Queries.SearchOperator.Or);
// }
// }
// if (!query.OrderBy.IsNullOrEmpty())
// {
// rawQuery = rawQuery.OrderBy(query.OrderBy, query.OrderIsDescending, query.OrderType == ListQueryOrderType.String ? OrderingType.String : OrderingType.Double);
// }
// if (query.PageSize > 0)
// {
// rawQuery = rawQuery.Paging(query.Page, query.PageSize);
// }
// }
// List<T> items = await rawQuery.ToListAsync();
// return new ListResult<T>(items, stats.TotalResults, query.Page, query.PageSize);
// }
//}
@@ -0,0 +1,107 @@
using System;
using System.Diagnostics;
using System.Net;
using System.Security.Cryptography;
using System.Text;
namespace zero;
/// <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[hash.Length - 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;
}
}
+15
View File
@@ -0,0 +1,15 @@
using System.Collections.Generic;
namespace zero;
[RavenCollection("Tokens")]
public class SecurityToken : IZeroDbConventions
{
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,278 @@
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
using Raven.Client.Documents.Session;
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace zero;
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 SecurityToken()
{
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
IMetadataDictionary tokenMetadata = session.Advanced.GetMetadataFor(securityToken);
tokenMetadata[Constants.Database.Expires] = DateTime.UtcNow.AddSeconds(expires.TotalSeconds);
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();
}
/// <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);
}
@@ -0,0 +1,145 @@
using Raven.Client.Documents.Conventions;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace zero;
public class ZeroDocumentConventionsBuilder : IZeroDocumentConventionsBuilder
{
protected HashSet<Type> PolymorphTypes { get; private set; } = new();
protected Type AcceptsZeroConventionsType { get; set; } = typeof(IZeroDbConventions);
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)
{
if (!Options.Raven.CollectionPrefix.IsNullOrWhiteSpace())
{
name = Options.Raven.CollectionPrefix.EnsureEndsWith(IdentityPartsSeparator) + name.TrimStart(Options.Raven.CollectionPrefix);
}
return name.ToCamelCaseId();
}
}
public interface IZeroDocumentConventionsBuilder
{
/// <summary>
/// Applies internal rules to the RavenDB document conventions
/// </summary>
void Run(DocumentConventions conventions);
}
@@ -0,0 +1,40 @@
using Raven.Client.Documents;
using Raven.Client.Documents.Session;
using System;
namespace zero;
public class ZeroDocumentSession : AsyncDocumentSession, IZeroDocumentSession
{
public IZeroDocumentSession Core { get; private set; }
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 interface IZeroDocumentSession : IAsyncDocumentSession
{
IZeroDocumentSession Core { get; }
bool IsDisposed { get; }
}
+150
View File
@@ -0,0 +1,150 @@
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;
using System;
using System.Threading;
using System.Threading.Tasks;
using zero.Core.Options;
namespace zero;
public class ZeroDocumentStore : DocumentStore, IZeroDocumentStore
{
public ZeroDocumentStore(IZeroOptions options) : base()
{
Options = options;
Database = null;
}
protected IZeroOptions 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.Raven.Database);
RegisterEvents(session);
AfterSessionCreated(session);
session.OnSessionDisposing += (sender, args) =>
{
session.IsDisposed = true;
};
session.Advanced.WaitForIndexesAfterSaveChanges();
session.Advanced.DocumentStore.AggressivelyCacheFor(TimeSpan.FromHours(1), options.Database);
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);
}
+291
View File
@@ -0,0 +1,291 @@
using Raven.Client.Documents;
using Raven.Client.Documents.Indexes;
using Raven.Client.Documents.Indexes.Spatial;
using Raven.Client.Documents.Operations.Attachments;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq.Expressions;
using zero.Core.Options;
namespace zero;
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,18 @@
using System;
using System.Collections.Generic;
namespace zero;
public static class ZeroIndexExtensions
{
internal static void RunModifiers<T>(this T index, IZeroOptions options) where T : IZeroIndexDefinition
{
IEnumerable<RavenIndexModifiersOptions.Modifier> modifiers = options.Raven.Indexes.Modifiers.GetAllForType(index.GetType());
foreach (RavenIndexModifiersOptions.Modifier modifier in modifiers)
{
Action<IZeroIndexDefinition> action = modifier.Modify.Compile();
action.Invoke(index);
}
}
}
+91
View File
@@ -0,0 +1,91 @@
using Raven.Client.Documents.Session;
using System.Collections.Generic;
using zero.Core.Options;
namespace zero;
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 />
public string ResolvedDatabase { get; set; }
/// <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 (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.Raven.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);
}
@@ -1,108 +0,0 @@
using System;
using System.Diagnostics;
using System.Net;
using System.Security.Cryptography;
using System.Text;
namespace zero.Core.Tokens
{
/// <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[hash.Length - 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;
}
}
}
-18
View File
@@ -1,18 +0,0 @@
using System.Collections.Generic;
using zero.Core.Attributes;
using zero.Core.Entities;
namespace zero.Core.Tokens
{
[Collection("Tokens")]
public class SecurityToken : IZeroDbConventions
{
public string Id { get; set; }
public string Key { get; set; }
public string Token { get; set; }
public Dictionary<string, string> Metadata { get; set; } = new Dictionary<string, string>();
}
}
-281
View File
@@ -1,281 +0,0 @@
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
using Raven.Client.Documents.Session;
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using zero.Core.Database;
using zero.Core.Extensions;
namespace zero.Core.Tokens
{
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 SecurityToken()
{
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
IMetadataDictionary tokenMetadata = session.Advanced.GetMetadataFor(securityToken);
tokenMetadata[Constants.Database.Expires] = DateTime.UtcNow.AddSeconds(expires.TotalSeconds);
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();
}
/// <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);
}
}
-16
View File
@@ -1,16 +0,0 @@
global using zero;
global using zero.Core;
global using zero.Core.Api;
global using zero.Core.Assemblies;
global using zero.Core.Collections;
global using zero.Core.Cultures;
global using zero.Core.Database;
global using zero.Core.Entities;
global using zero.Core.Extensions;
global using zero.Core.Handlers;
global using zero.Core.Identity;
global using zero.Core.Options;
global using zero.Core.Plugins;
global using zero.Core.Security;
global using zero.Core.Validation;

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