diff --git a/old/zero.Core/Entities/ChangeToken.cs b/old/zero.Core/Entities/ChangeToken.cs deleted file mode 100644 index 8558b927..00000000 --- a/old/zero.Core/Entities/ChangeToken.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace zero.Core.Entities -{ - /// - /// A change token holds a reference to a database entity - /// This is used to verify change requests for entities in the zero backoffice - /// - public class ChangeToken : IZeroDbConventions - { - public string Id { get; set; } - - public string ReferenceId { get; set; } - } -} diff --git a/old/zero.Core/Entities/EntityResult.cs b/old/zero.Core/Entities/EntityResult.cs deleted file mode 100644 index 8ee91ae1..00000000 --- a/old/zero.Core/Entities/EntityResult.cs +++ /dev/null @@ -1,130 +0,0 @@ -using FluentValidation.Results; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.Serialization; - -namespace zero.Core.Entities -{ - [DataContract(Name = "result", Namespace = "")] - public class EntityResult - { - [DataMember(Name = "model")] - public T Model { get; set; } - - [DataMember(Name = "success")] - public bool IsSuccess { get; set; } - - [DataMember(Name = "errors")] - public IList Errors { get; set; } = new List(); - - public EntityResult() { } - - public EntityResult(ValidationResult validation) - { - IsSuccess = validation.IsValid; - Errors = validation.Errors.Select(x => new EntityResultError() - { - Property = x.PropertyName, - Message = x.ErrorMessage - }).ToList(); - } - - public static EntityResult From(EntityResult with, T model = default) - { - EntityResult result = new EntityResult(); - - result.IsSuccess = with.IsSuccess; - result.Errors = with.Errors; - result.Model = model; - - return result; - } - - public void AddError(string property, string message) - { - IsSuccess = false; - Errors.Add(new EntityResultError() - { - Property = property, - Message = message - }); - } - - - public void AddError(string message) - { - IsSuccess = false; - Errors.Add(new EntityResultError() - { - Property = Constants.ErrorFieldNone, - Message = message - }); - } - - - public EntityResult Combine(EntityResult with) - { - if (IsSuccess && !with.IsSuccess) - { - IsSuccess = false; - } - foreach (EntityResultError error in with.Errors) - { - Errors.Add(error); - } - return this; - } - - - public static EntityResult Success() => new EntityResult() { IsSuccess = true }; - - public static EntityResult Success(T model) => new EntityResult() { IsSuccess = true, Model = model }; - - public static EntityResult Fail() => new EntityResult() { }; - - public static EntityResult Fail(string property, string message) - { - EntityResult result = new EntityResult(); - result.AddError(property, message); - return result; - } - - public static EntityResult Fail(string message) - { - EntityResult result = new EntityResult(); - result.AddError(message); - return result; - } - - public static EntityResult Fail(ValidationResult validation) => new EntityResult(validation); - - public static EntityResult Fail(EntityResultError error) => Fail(error.Property, error.Message); - - public static EntityResult Fail(IEnumerable errors) => new EntityResult() { IsSuccess = !errors.Any(), Errors = errors.ToList() }; - } - - - public class EntityResult : EntityResult - { - public EntityResult() : base() { } - - public EntityResult(ValidationResult validation) : base(validation) { } - - public static EntityResult Maybe(bool isSuccess) => isSuccess ? Success() : Fail(); - - public new static EntityResult Success() => new EntityResult() { IsSuccess = true }; - - public new static EntityResult Fail() => new EntityResult() { }; - } - - - [DataContract(Name = "resultError", Namespace = "")] - public class EntityResultError - { - [DataMember(Name = "property")] - public string Property { get; set; } - - [DataMember(Name = "message")] - public string Message { get; set; } - } -} diff --git a/old/zero.Core/Entities/ListQuery.cs b/old/zero.Core/Entities/ListQuery.cs deleted file mode 100644 index 50362490..00000000 --- a/old/zero.Core/Entities/ListQuery.cs +++ /dev/null @@ -1,98 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Newtonsoft.Json; -using System; -using System.Linq; -using System.Linq.Expressions; - -namespace zero.Core.Entities -{ - public class ListBackofficeQuery : ListQuery - { - public ListBackofficeQuery() - { - IncludeInactive = true; - } - } - - public class ListBackofficeQuery : ListQuery where TFilter : IListSpecificQuery - { - public ListBackofficeQuery() - { - IncludeInactive = true; - } - } - - public class ListQuery - { - public string Search { get; set; } = null; - - public Expression> SearchSelector { get; set; } = null; - - public Expression>[] SearchSelectors { get; private set; } = new Expression>[0] { }; - - public string OrderBy { get; set; } = "createdDate"; - - public ListQueryOrderType OrderType { get; set; } = ListQueryOrderType.String; - - public bool OrderIsDescending { get; set; } = true; - - public Func, IQueryable> OrderQuery = null; - - public int Page { get; set; } = 1; - - public int PageSize { get; set; } = 30; - - public bool IncludeInactive { get; set; } = false; - - public void SearchFor(params Expression>[] selectors) - { - SearchSelectors = selectors; - } - } - - - public class ListQuery : ListQuery where TFilter : IListSpecificQuery - { - public TFilter Filter { get; set; } - } - - - public static class ListQueryExtensions - { - public static ListQuery AsList(this string options) where TFilter : IListSpecificQuery - { - return JsonConvert.DeserializeObject>(options); - } - - public static ListQuery AsList(this string options) where T : IListSpecificQuery - { - return JsonConvert.DeserializeObject>(options); - } - } - - - public interface IListSpecificQuery { } - - public class EmptyListSpecificQuery : IListSpecificQuery { } - - - public class ListQueryDateRange - { - public DateTimeOffset? From { get; set; } - - public DateTimeOffset? To { get; set; } - } - - public class ListQueryRange - { - public decimal? From { get; set; } - - public decimal? To { get; set; } - } - - public enum ListQueryOrderType - { - String, - Number - } -} diff --git a/old/zero.Core/Entities/ListResult.cs b/old/zero.Core/Entities/ListResult.cs deleted file mode 100644 index d61d9a32..00000000 --- a/old/zero.Core/Entities/ListResult.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace zero.Core.Entities -{ - /// - /// Represents a paged result for a model collection - /// - public class ListResult : ListResult - { - public ListResult(long totalItems, long page, long pageSize) - { - TotalItems = totalItems; - Page = page; - PageSize = pageSize; - - if (pageSize > 0) - { - TotalPages = (long)Math.Ceiling(totalItems / (decimal)pageSize); - } - else - { - TotalPages = 1; - } - - HasMore = TotalPages > Page; - } - - public ListResult(IList items, long totalItems, long pageNumber, long pageSize) : this(totalItems, pageNumber, pageSize) - { - Items = items; - } - - public ListResult MapTo(Func convertItem) - { - return new ListResult(Items.Select(x => convertItem(x)).Where(x => x != null).ToList(), TotalItems, Page, PageSize); - } - - public IList Items { get; set; } = new List(); - - - /// - /// Calculates the skip size based on the paged parameters specified - /// - /// - /// Returns 0 if the page number or page size is zero - /// - public int GetSkipSize() - { - if (Page > 0 && PageSize > 0) - { - return Convert.ToInt32((Page - 1) * PageSize); - } - return 0; - } - } - - - public class ListResult - { - public long Page { get; protected set; } - - public long PageSize { get; protected set; } - - public long TotalPages { get; protected set; } - - public long TotalItems { get; protected set; } - - public bool HasMore { get; protected set; } - } -} diff --git a/old/zero.Core/Entities/Revision.cs b/old/zero.Core/Entities/Revision.cs deleted file mode 100644 index ac0d88fd..00000000 --- a/old/zero.Core/Entities/Revision.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; - -namespace zero.Core.Entities -{ - public class Revision : Revision where T : ZeroEntity - { - public T Model { get; set; } - } - - public class Revision - { - public string ModelId { get; set; } - - public RevisionUser User { get; set; } - - public DateTimeOffset Date { get; set; } - - public string ChangeVector { get; set; } - } - - public class RevisionUser - { - public string Id { get; set; } - - public string Name { get; set; } - - public string AvatarId { get; set; } - } -} diff --git a/old/zero.Core/Entities/SearchResult.cs b/old/zero.Core/Entities/SearchResult.cs deleted file mode 100644 index 2c72af00..00000000 --- a/old/zero.Core/Entities/SearchResult.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections.Generic; - -namespace zero.Core.Models -{ - public class SearchResult - { - public string Id { get; set; } - - public string Name { get; set; } - - public string Description { get; set; } - - public string Icon { get; set; } - - public string Image { get; set; } - - public string Url { get; set; } - - public string Group { get; set; } - - public bool IsActive { get; set; } - } - - - public class SearchIndexResult - { - public string Id { get; set; } - - public string Group { get; set; } - - public string Name { get; set; } - - public bool IsActive { get; set; } - - public List Fields { get; set; } = new(); - } -} diff --git a/old/zero.Core/Entities/Sections/IChildSection.cs b/old/zero.Core/Entities/Sections/IChildSection.cs deleted file mode 100644 index 65a59877..00000000 --- a/old/zero.Core/Entities/Sections/IChildSection.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace zero.Core.Entities -{ - /// - /// A child section is a sub-navigation item of a section - /// - public interface IChildSection - { - /// - /// The section alias which acts as the url slug for navigation - /// - public string Alias { get; } - - /// - /// The name of the section (either a string or a translation key with @ prefix) - /// - public string Name { get; } - } -} diff --git a/old/zero.Core/Entities/Sections/ISection.cs b/old/zero.Core/Entities/Sections/ISection.cs deleted file mode 100644 index 66a3e665..00000000 --- a/old/zero.Core/Entities/Sections/ISection.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Collections.Generic; - -namespace zero.Core.Entities -{ - /// - /// Internal section - /// - public interface IInternalSection : ISection { } - - /// - /// A section is a main part of the backoffice application - /// - public interface ISection - { - /// - /// The section alias which acts as the url slug for navigation - /// - string Alias { get; } - - /// - /// The name of the section (either a string or a translation key with @ prefix) - /// - string Name { get; } - - /// - /// Icon of the section - /// - string Icon { get; } - - /// - /// HEX color (#aabbcc or #abc) - /// - string Color { get; } - - /// - /// Children are displayed as a sub-navigation in the main nav area - /// - IList Children { get; } - } -} diff --git a/old/zero.Core/Entities/Sections/Section.cs b/old/zero.Core/Entities/Sections/Section.cs deleted file mode 100644 index b7bfef26..00000000 --- a/old/zero.Core/Entities/Sections/Section.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Collections.Generic; - -namespace zero.Core.Entities -{ - /// - /// A section is a main part of the backoffice application - /// - public class Section : ISection, IChildSection - { - /// - public string Alias { get; set; } - - /// - public string Name { get; set; } - - /// - public string Icon { get; set; } - - /// - /// HEX color (#aabbcc or #abc). Defaults to a neutral color - /// - public string Color { get; set; } - - /// - public IList Children { get; } = new List(); - - - public Section() { } - - public Section(string alias, string name, string icon = null, string color = null) - { - Alias = alias; - Name = name; - Icon = icon; - Color = color; - } - } -} diff --git a/old/zero.Core/Entities/SelectModel.cs b/old/zero.Core/Entities/SelectModel.cs deleted file mode 100644 index 89610e7c..00000000 --- a/old/zero.Core/Entities/SelectModel.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace zero.Core.Entities -{ - public class SelectModel - { - public string Id { get; set; } - - public string Name { get; set; } - - public bool IsActive { get; set; } - } -} diff --git a/old/zero.Core/Entities/Tree/TreeItem.cs b/old/zero.Core/Entities/Tree/TreeItem.cs deleted file mode 100644 index fc652869..00000000 --- a/old/zero.Core/Entities/Tree/TreeItem.cs +++ /dev/null @@ -1,103 +0,0 @@ -namespace zero.Core.Entities -{ - /// - /// Represents an item in a tree - /// - public class TreeItem - { - /// - /// Id of the item - /// - public string Id { get; set; } - - /// - /// Parent id of the item - /// - public string ParentId { get; set; } - - /// - /// Sort order - /// - public uint Sort { get; set; } - - /// - /// Name of the item - /// - public string Name { get; set; } - - /// - /// Displays a description on hover - /// - public string Description { get; set; } - - /// - /// Icon to display alongside the name - /// - public string Icon { get; set; } - - /// - /// Whether this item is open in case it contains children - /// - public bool IsOpen { get; set; } - - /// - /// Displays a small icon (with hover text) next to the main item icon - /// - public TreeItemModifier Modifier { get; set; } - - /// - /// Whether this item has children - /// - public bool HasChildren { get; set; } - - /// - /// Count of children - /// - public int ChildCount { get; set; } - - /// - /// Whether this item is published or not - /// - public bool IsInactive { get; set; } - - /// - /// Whether to display the item icon with a dashed line - /// - public bool IsDashed { get; set; } - - /// - /// Whether to show actions menu. This will only work when the onActionsRequested cb is implemented in the component - /// - public bool HasActions { get; set; } - - /// - /// Output an additional count value. - /// - public int? CountOutput { get; set; } - } - - - /// - /// The modifier displays a small icon (with hover text) next to the main item icon - /// - public class TreeItemModifier - { - /// - /// Name of the modifier - /// - public string Name { get; set; } - - /// - /// Icon to display - /// - public string Icon { get; set; } - - public TreeItemModifier() { } - - public TreeItemModifier(string name, string icon) - { - Name = name; - Icon = icon; - } - } -} diff --git a/old/zero.Web/Controllers/SearchController.cs b/old/zero.Web/Controllers/SearchController.cs deleted file mode 100644 index 739dcf61..00000000 --- a/old/zero.Web/Controllers/SearchController.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using System.Threading.Tasks; -using zero.Core.Entities; -using zero.Core.Identity; -using zero.Core.Models; -using zero.Core.Services; - -namespace zero.Web.Controllers -{ - [ZeroAuthorize] - public class SearchController : BackofficeController - { - IBackofficeSearchService SearchService; - - public SearchController(IBackofficeSearchService searchService) - { - SearchService = searchService; - } - - public async Task> Query([FromQuery] string query) => await SearchService.Query(query); - } -} diff --git a/old/zero.Web/Filters/AddTokenAttribute.cs b/old/zero.Web/Filters/AddTokenAttribute.cs deleted file mode 100644 index d2c1694b..00000000 --- a/old/zero.Web/Filters/AddTokenAttribute.cs +++ /dev/null @@ -1,51 +0,0 @@ -//using Microsoft.AspNetCore.Mvc; -//using Microsoft.AspNetCore.Mvc.Filters; -//using System.Threading.Tasks; -//using zero.Core.Api; -//using zero.Web.Models; - -//namespace zero.Web.Filters -//{ -// public class AddTokenAttribute : TypeFilterAttribute -// { -// public AddTokenAttribute() : base(typeof(AddTokenAttributeImpl)) { } - - -// private class AddTokenAttributeImpl : IAsyncResultFilter -// { -// IToken token; - - -// public AddTokenAttributeImpl(IToken token) -// { -// this.token = token; -// } - -// public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) -// { -// if (context.Result is JsonResult) -// { -// JsonResult result = context.Result as JsonResult; - -// if (result.Value is ObsoleteEditModel) -// { -// ObsoleteEditModel model = result.Value as ObsoleteEditModel; - -// model.Meta = model.Meta ?? new EditModelMeta(); -// model.Meta.Token = await token.Get(model.Id); -// } - -// if (result.Value is ObsoleteEditModel) -// { -// ObsoleteEditModel model = result.Value as ObsoleteEditModel; - -// model.Meta = model.Meta ?? new EditModelMeta(); -// model.Meta.Token = await token.Get(model.Id); -// } -// } - -// await next(); -// } -// } -// } -//} diff --git a/old/zero.Web/Filters/BackofficeFilterAttribute.cs b/old/zero.Web/Filters/BackofficeFilterAttribute.cs deleted file mode 100644 index 65c252d7..00000000 --- a/old/zero.Web/Filters/BackofficeFilterAttribute.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using System; -using zero.Web.Controllers; - -namespace zero.Web.Filters -{ - public class BackofficeFilterAttribute : IActionFilter - { - const string SCOPE_KEY = "scope"; - - - public void OnActionExecuting(ActionExecutingContext context) - { - Type type = context.Controller.GetType(); - - if (typeof(BackofficeController).IsAssignableFrom(type)) - { - if (context.HttpContext.Request.Query.TryGetValue(SCOPE_KEY, out var scope)) - { - (context.Controller as BackofficeController).OnScopeChanged(scope.ToString()); - } - } - } - - public void OnActionExecuted(ActionExecutedContext context) - { - } - } -} diff --git a/old/zero.Web/Filters/CanEditAttribute.cs b/old/zero.Web/Filters/CanEditAttribute.cs deleted file mode 100644 index 6b5a9b94..00000000 --- a/old/zero.Web/Filters/CanEditAttribute.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using System.Threading.Tasks; -using zero.Core.Api; -using zero.Web.Models; - -namespace zero.Web.Filters -{ - public class CanEditAttribute : TypeFilterAttribute - { - public CanEditAttribute() : base(typeof(CanEditAttributeImpl)) { } - - - private class CanEditAttributeImpl : IAsyncResultFilter - { - //IToken token; - - - public CanEditAttributeImpl()//IToken token) - { - //this.token = token; - } - - public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) - { - if (context.Result is JsonResult) - { - JsonResult result = context.Result as JsonResult; - - if (result.Value is ObsoleteEditModel) - { - ObsoleteEditModel model = result.Value as ObsoleteEditModel; - - model.CanEdit = true; // TODO query authorize attrs to get permissions - } - } - - await next(); - } - } - } -} diff --git a/old/zero.Web/Filters/ModelStateValidationFilterAttribute.cs b/old/zero.Web/Filters/ModelStateValidationFilterAttribute.cs deleted file mode 100644 index 183f5597..00000000 --- a/old/zero.Web/Filters/ModelStateValidationFilterAttribute.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; - -namespace zero.Web.Filters -{ - public class ModelStateValidationFilterAttribute : IActionFilter - { - public void OnActionExecuting(ActionExecutingContext context) - { - if (!context.ModelState.IsValid) - { - context.Result = new BadRequestObjectResult(context.ModelState); - } - } - - public void OnActionExecuted(ActionExecutedContext context) - { - } - } -} diff --git a/old/zero.Web/Sections/DashboardSection.cs b/old/zero.Web/Sections/DashboardSection.cs deleted file mode 100644 index a3406762..00000000 --- a/old/zero.Web/Sections/DashboardSection.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections.Generic; -using zero.Core; -using zero.Core.Entities; - -namespace zero.Web.Sections -{ - /// - /// The dashboard aggregates data from all sections - /// - public class DashboardSection : IInternalSection - { - /// - public string Alias => Constants.Sections.Dashboard; - - /// - public string Name => "@sections.item.dashboard"; - - /// - public string Icon => "fth-pie-chart"; - - /// - public string Color => null; - - /// - public IList Children => new List(); - } -} diff --git a/old/zero.Web/Sections/MediaSection.cs b/old/zero.Web/Sections/MediaSection.cs deleted file mode 100644 index 9a5595d2..00000000 --- a/old/zero.Web/Sections/MediaSection.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections.Generic; -using zero.Core; -using zero.Core.Entities; - -namespace zero.Web.Sections -{ - /// - /// Media items (images, videos, documents) grouped in folders - /// - public class MediaSection : IInternalSection - { - /// - public string Alias => Constants.Sections.Media; - - /// - public string Name => "@sections.item.media"; - - /// - public string Icon => "fth-image"; - - /// - public string Color => "#d82853"; - - /// - public IList Children => new List(); - } -} diff --git a/old/zero.Web/Sections/PagesSection.cs b/old/zero.Web/Sections/PagesSection.cs deleted file mode 100644 index bd7d7be5..00000000 --- a/old/zero.Web/Sections/PagesSection.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections.Generic; -using zero.Core; -using zero.Core.Entities; - -namespace zero.Web.Sections -{ - /// - /// Manage the page tree in this section - /// - public class PagesSection : IInternalSection - { - /// - public string Alias => Constants.Sections.Pages; - - /// - public string Name => "@sections.item.pages"; - - /// - public string Icon => "fth-folder"; - - /// - public string Color => "#0cb0f5"; - - /// - public IList Children => new List(); - } -} diff --git a/old/zero.Web/Sections/SettingsSection.cs b/old/zero.Web/Sections/SettingsSection.cs deleted file mode 100644 index 2d5204ee..00000000 --- a/old/zero.Web/Sections/SettingsSection.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections.Generic; -using zero.Core; -using zero.Core.Entities; - -namespace zero.Web.Sections -{ - /// - /// Website and backoffice settings - /// - public class SettingsSection : IInternalSection - { - /// - public string Alias => Constants.Sections.Settings; - - /// - public string Name => "@sections.item.settings"; - - /// - public string Icon => "fth-settings"; - - /// - public string Color => null; - - /// - public IList Children => new List(); - } -} diff --git a/old/zero.Web/Sections/SpacesSection.cs b/old/zero.Web/Sections/SpacesSection.cs deleted file mode 100644 index 9ef08ea8..00000000 --- a/old/zero.Web/Sections/SpacesSection.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections.Generic; -using zero.Core; -using zero.Core.Entities; - -namespace zero.Web.Sections -{ - /// - /// Global list entities - /// - public class SpacesSection : IInternalSection - { - /// - public string Alias => Constants.Sections.Spaces; - - /// - public string Name => "@sections.item.spaces"; - - /// - public string Icon => "fth-layers"; - - /// - public string Color => "#f9c202"; - - /// - public IList Children => new List(); - } -} diff --git a/old/zero.Web/ZeroApplicationBuilder.cs b/old/zero.Web/ZeroApplicationBuilder.cs deleted file mode 100644 index 5a227e84..00000000 --- a/old/zero.Web/ZeroApplicationBuilder.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using System; -using zero.Core.Options; -using zero.Web.Middlewares; - -namespace zero.Web -{ - public class ZeroApplicationBuilder : IZeroApplicationBuilder - { - protected IApplicationBuilder App { get; private set; } - - protected IZeroOptions Options { get; private set; } - - - internal ZeroApplicationBuilder(IApplicationBuilder app) - { - App = app; - App.UseStaticFiles(); - App.UseMiddleware(); - App.UseRouting(); - App.UseAuthentication(); - App.UseAuthorization(); - } - - - public void WithEndpoints(Action configure) - { - App.UseEndpoints(e => - { - ZeroEndpointRouteBuilder builder = new(e); - configure(builder); - }); - } - - - public IZeroApplicationBuilder WithMiddleware(Action configure) - { - configure(App); - return this; - } - } - - - public interface IZeroApplicationBuilder - { - void WithEndpoints(Action endpoints); - - IZeroApplicationBuilder WithMiddleware(Action configure); - } -} diff --git a/old/zero.Web/ZeroApplicationBuilderExtensions.cs b/old/zero.Web/ZeroApplicationBuilderExtensions.cs deleted file mode 100644 index dd09c977..00000000 --- a/old/zero.Web/ZeroApplicationBuilderExtensions.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using System; -using zero.Web.Middlewares; - -namespace zero.Web -{ - public static class ZeroApplicationBuilderExtensions - { - public static IZeroApplicationBuilder UseZero(this IApplicationBuilder app) => new ZeroApplicationBuilder(app); - } -} diff --git a/old/zero.Web/ZeroBuilder.cs b/old/zero.Web/ZeroBuilder.cs deleted file mode 100644 index 8dee4296..00000000 --- a/old/zero.Web/ZeroBuilder.cs +++ /dev/null @@ -1,189 +0,0 @@ -using FluentValidation; -using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Options; -using Raven.Client.Documents; -using Raven.Client.Documents.Indexes; -using Raven.Client.Http; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using zero.Core; -using zero.Core.Api; -using zero.Core.Assemblies; -using zero.Core.Collections; -using zero.Core.Cultures; -using zero.Core.Database; -using zero.Core.Entities; -using zero.Core.Extensions; -using zero.Core.Handlers; -using zero.Core.Identity; -using zero.Core.Options; -using zero.Core.Plugins; -using zero.Core.Security; -using zero.Core.Validation; -using zero.Web.Controllers; -using zero.Web.Defaults; -using zero.Web.Filters; -using zero.Web.Security; -using Zero.Web.DevServer; - -namespace zero.Web -{ - // TODO maybe use a middleware like Hangfire does: https://github.com/HangfireIO/Hangfire/blob/master/src/Hangfire.AspNetCore/HangfireEndpointRouteBuilderExtensions.cs - - public class ZeroBuilder - { - public virtual IServiceCollection Services { get; } - - public virtual IMvcBuilder Mvc { get; } - - IConfiguration Configuration; - - IZeroStartupOptions StartupOptions; - - - public ZeroBuilder(IServiceCollection services, IConfiguration configuration, Action setupAction) - { - Services = services; - Mvc = services.AddMvc(); - Configuration = configuration; - - // create startup options - StartupOptions = new ZeroStartupOptions(Mvc); - StartupOptions.AssemblyDiscoveryRules.Add(new ZeroAssemblyDiscoveryRule()); - setupAction?.Invoke(StartupOptions); - - - // adds and discovers additional and built-in assemblies - new AssemblyDiscovery(Mvc).Execute(StartupOptions.AssemblyDiscoveryRules); - - - // add default plugin - AddPlugin(); - - - Mvc.AddNewtonsoftJson(x => - { - // TODO this shall only be configurated for backoffice controllers - BackofficeJsonSerlializerSettings.Setup(x.SerializerSettings); - }); - - if (Environment.GetEnvironmentVariable("DOTNET_WATCH") == "1") - { - Mvc.AddRazorRuntimeCompilation(); - } - - // configure FluentValidation - ValidatorOptions.Global.PropertyNameResolver = ValidatorCamelCasePropertyResolver.ResolvePropertyName; - - - // add default services - Services.AddHttpContextAccessor(); - Services.AddScoped(); - Services.AddScoped(); - Services.AddScoped(); - - Services.AddScoped(); - Services.AddScoped(); - Services.AddScoped(); - - //Services.AddScoped(); - - Services.AddHttpContextAccessor(); - - Services.AddTransient(); - Services.AddScoped(factory => new Paths(factory.GetService(), true)); - - Services.AddTransient(); - - // add dev server - Services.AddOptions() - .Bind(Configuration.GetSection("Zero:DevServer")) - .Configure((opts, env) => - { - opts.WorkingDirectory = Path.Combine(env.ContentRootPath, "..", "Zero.Web.UI", "App"); - }); - - Services.AddHostedService(); - } - - - /// - /// Use specified options - /// - public ZeroBuilder WithOptions(Action configureOptions) - { - Services.Configure(configureOptions); - return this; - } - - - /// - /// Adds a zero plugin - /// - public ZeroBuilder AddPlugin() where T : class, IZeroPlugin, new() - { - AssemblyDiscovery.Current.AddAssembly(typeof(T).Assembly); - Services.AddSingleton(); - AddPluginServices(); - return this; - } - - - /// - /// Adds a zero plugin - /// - public ZeroBuilder AddPlugin(Func implementationFactory) where T : class, IZeroPlugin, new() - { - AssemblyDiscovery.Current.AddAssembly(typeof(T).Assembly); - Services.AddSingleton(implementationFactory); - AddPluginServices(); - return this; - } - - - /// - /// Creates a temporary instance of the plugin to add additional services - /// - /// - void AddPluginServices() where T : class, IZeroPlugin, new() - { - try - { - T plugin = new T(); - - plugin.ConfigureServices(Services, Configuration); - - Services.Configure(opts => plugin.Configure(opts)); - } - catch - { - throw new Exception($"Plugin \"{nameof(T)}\" needs an additional parameterless constructor as ConfigureServices() is called before the DI container is built"); - } - } - - - class ZeroBuilderMvcOptions : IConfigureOptions - { - IZeroOptions Options { get; set; } - - public ZeroBuilderMvcOptions(IZeroOptions options) - { - Options = options; - } - - public void Configure(MvcOptions options) - { - options.Conventions.Add(new ZeroBackofficeControllerModelConvention(Options.BackofficePath)); - } - } - } -} diff --git a/old/zero.Web/ZeroServiceCollectionExtensions.cs b/old/zero.Web/ZeroServiceCollectionExtensions.cs deleted file mode 100644 index 5437e764..00000000 --- a/old/zero.Web/ZeroServiceCollectionExtensions.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using System; -using zero.Core.Options; - -namespace zero.Web -{ - public static class ZeroServiceCollectionExtensions - { - public static ZeroBuilder AddZero(this IServiceCollection services, IConfiguration configuration) - { - return new ZeroBuilder(services, configuration, null); - } - - public static ZeroBuilder AddZero(this IServiceCollection services, IConfiguration configuration, Action setupAction) - { - return new ZeroBuilder(services, configuration, setupAction); - } - } -} \ No newline at end of file diff --git a/zero.Backoffice/Filters/BackofficeFilterAttribute.cs b/zero.Backoffice/Filters/BackofficeFilterAttribute.cs new file mode 100644 index 00000000..ca7efd9a --- /dev/null +++ b/zero.Backoffice/Filters/BackofficeFilterAttribute.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Mvc.Filters; + +namespace zero.Backoffice.Filters; + +public class BackofficeFilterAttribute : IActionFilter +{ + const string SCOPE_KEY = "scope"; + + + public void OnActionExecuting(ActionExecutingContext context) + { + Type type = context.Controller.GetType(); + + if (typeof(BackofficeController).IsAssignableFrom(type)) + { + if (context.HttpContext.Request.Query.TryGetValue(SCOPE_KEY, out var scope)) + { + (context.Controller as BackofficeController).OnScopeChanged(scope.ToString()); + } + } + } + + public void OnActionExecuted(ActionExecutedContext context) + { + } +} \ No newline at end of file diff --git a/zero.Backoffice/Filters/CanEditAttribute.cs b/zero.Backoffice/Filters/CanEditAttribute.cs new file mode 100644 index 00000000..cb5d6ab8 --- /dev/null +++ b/zero.Backoffice/Filters/CanEditAttribute.cs @@ -0,0 +1,38 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace zero.Backoffice.Filters; + +public class CanEditAttribute : TypeFilterAttribute +{ + public CanEditAttribute() : base(typeof(CanEditAttributeImpl)) { } + + + private class CanEditAttributeImpl : IAsyncResultFilter + { + //IToken token; + + + public CanEditAttributeImpl()//IToken token) + { + //this.token = token; + } + + public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) + { + if (context.Result is JsonResult) + { + JsonResult result = context.Result as JsonResult; + + //if (result.Value is ObsoleteEditModel) + //{ + // ObsoleteEditModel model = result.Value as ObsoleteEditModel; + + // model.CanEdit = true; // TODO query authorize attrs to get permissions + //} + } + + await next(); + } + } +} \ No newline at end of file diff --git a/zero.Backoffice/Filters/ModelStateValidationFilterAttribute.cs b/zero.Backoffice/Filters/ModelStateValidationFilterAttribute.cs new file mode 100644 index 00000000..c50589bc --- /dev/null +++ b/zero.Backoffice/Filters/ModelStateValidationFilterAttribute.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace zero.Backoffice.Filters; + +public class ModelStateValidationFilterAttribute : IActionFilter +{ + public void OnActionExecuting(ActionExecutingContext context) + { + if (!context.ModelState.IsValid) + { + context.Result = new BadRequestObjectResult(context.ModelState); + } + } + + public void OnActionExecuted(ActionExecutedContext context) + { + } +} \ No newline at end of file diff --git a/old/zero.Web/Filters/VerifyTokenAttribute.cs b/zero.Backoffice/Filters/VerifyTokenAttribute.cs similarity index 100% rename from old/zero.Web/Filters/VerifyTokenAttribute.cs rename to zero.Backoffice/Filters/VerifyTokenAttribute.cs diff --git a/old/zero.Web/Filters/ZeroBackofficeAttribute.cs b/zero.Backoffice/Filters/ZeroBackofficeAttribute.cs similarity index 100% rename from old/zero.Web/Filters/ZeroBackofficeAttribute.cs rename to zero.Backoffice/Filters/ZeroBackofficeAttribute.cs diff --git a/zero.Backoffice/Indexes/zero_Translations.cs b/zero.Backoffice/Indexes/zero_Translations.cs deleted file mode 100644 index 69fa96a7..00000000 --- a/zero.Backoffice/Indexes/zero_Translations.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Raven.Client.Documents.Indexes; -using System.Linq; -using zero.Core.Entities; - -namespace zero.Core.Database.Indexes -{ - public class zero_Translations : ZeroIndex - { - protected override void Create() - { - Map = items => items.Select(item => new - { - Key = item.Key, - Value = item.Value, - CreatedDate = item.CreatedDate - }); - - Index(x => x.Key, FieldIndexing.Search); - Index(x => x.Value, FieldIndexing.Search); - } - } -} diff --git a/zero.Backoffice/Models/EditModel.cs b/zero.Backoffice/Models/EditModel.cs index 7d910902..e19a46a3 100644 --- a/zero.Backoffice/Models/EditModel.cs +++ b/zero.Backoffice/Models/EditModel.cs @@ -1,6 +1,4 @@ -using System; - -namespace zero.Backoffice; +namespace zero.Backoffice.Models; public class EditModel : EditModel { } diff --git a/zero.Backoffice/Models/ListQuery.cs b/zero.Backoffice/Models/ListQuery.cs new file mode 100644 index 00000000..ecf5e216 --- /dev/null +++ b/zero.Backoffice/Models/ListQuery.cs @@ -0,0 +1,94 @@ +using Newtonsoft.Json; +using System.Linq.Expressions; + +namespace zero.Backoffice.Models; + +public class ListBackofficeQuery : ListQuery +{ + public ListBackofficeQuery() + { + IncludeInactive = true; + } +} + +public class ListBackofficeQuery : ListQuery where TFilter : IListSpecificQuery +{ + public ListBackofficeQuery() + { + IncludeInactive = true; + } +} + +public class ListQuery +{ + public string Search { get; set; } = null; + + public Expression> SearchSelector { get; set; } = null; + + public Expression>[] SearchSelectors { get; private set; } = new Expression>[0] { }; + + public string OrderBy { get; set; } = "createdDate"; + + public ListQueryOrderType OrderType { get; set; } = ListQueryOrderType.String; + + public bool OrderIsDescending { get; set; } = true; + + public Func, IQueryable> OrderQuery = null; + + public int Page { get; set; } = 1; + + public int PageSize { get; set; } = 30; + + public bool IncludeInactive { get; set; } = false; + + public void SearchFor(params Expression>[] selectors) + { + SearchSelectors = selectors; + } +} + + +public class ListQuery : ListQuery where TFilter : IListSpecificQuery +{ + public TFilter Filter { get; set; } +} + + +public static class ListQueryExtensions +{ + public static ListQuery AsList(this string options) where TFilter : IListSpecificQuery + { + return JsonConvert.DeserializeObject>(options); + } + + public static ListQuery AsList(this string options) where T : IListSpecificQuery + { + return JsonConvert.DeserializeObject>(options); + } +} + + +public interface IListSpecificQuery { } + +public class EmptyListSpecificQuery : IListSpecificQuery { } + + +public class ListQueryDateRange +{ + public DateTimeOffset? From { get; set; } + + public DateTimeOffset? To { get; set; } +} + +public class ListQueryRange +{ + public decimal? From { get; set; } + + public decimal? To { get; set; } +} + +public enum ListQueryOrderType +{ + String, + Number +} \ No newline at end of file diff --git a/zero.Backoffice/Models/ListResult.cs b/zero.Backoffice/Models/ListResult.cs new file mode 100644 index 00000000..ec52b655 --- /dev/null +++ b/zero.Backoffice/Models/ListResult.cs @@ -0,0 +1,67 @@ +namespace zero.Backoffice.Models; + +/// +/// Represents a paged result for a model collection +/// +public class ListResult : ListResult +{ + public ListResult(long totalItems, long page, long pageSize) + { + TotalItems = totalItems; + Page = page; + PageSize = pageSize; + + if (pageSize > 0) + { + TotalPages = (long)Math.Ceiling(totalItems / (decimal)pageSize); + } + else + { + TotalPages = 1; + } + + HasMore = TotalPages > Page; + } + + public ListResult(IList items, long totalItems, long pageNumber, long pageSize) : this(totalItems, pageNumber, pageSize) + { + Items = items; + } + + public ListResult MapTo(Func convertItem) + { + return new ListResult(Items.Select(x => convertItem(x)).Where(x => x != null).ToList(), TotalItems, Page, PageSize); + } + + public IList Items { get; set; } = new List(); + + + /// + /// Calculates the skip size based on the paged parameters specified + /// + /// + /// Returns 0 if the page number or page size is zero + /// + public int GetSkipSize() + { + if (Page > 0 && PageSize > 0) + { + return Convert.ToInt32((Page - 1) * PageSize); + } + return 0; + } +} + + +public class ListResult +{ + public long Page { get; protected set; } + + public long PageSize { get; protected set; } + + public long TotalPages { get; protected set; } + + public long TotalItems { get; protected set; } + + public bool HasMore { get; protected set; } +} \ No newline at end of file diff --git a/zero.Backoffice/Models/SelectModel.cs b/zero.Backoffice/Models/SelectModel.cs new file mode 100644 index 00000000..b7eb8562 --- /dev/null +++ b/zero.Backoffice/Models/SelectModel.cs @@ -0,0 +1,10 @@ +namespace zero.Backoffice.Models; + +public class SelectModel +{ + public string Id { get; set; } + + public string Name { get; set; } + + public bool IsActive { get; set; } +} \ No newline at end of file diff --git a/zero.Backoffice/Models/TreeItem.cs b/zero.Backoffice/Models/TreeItem.cs new file mode 100644 index 00000000..da0c624b --- /dev/null +++ b/zero.Backoffice/Models/TreeItem.cs @@ -0,0 +1,102 @@ +namespace zero.Backoffice.Models; + +/// +/// Represents an item in a tree +/// +public class TreeItem +{ + /// + /// Id of the item + /// + public string Id { get; set; } + + /// + /// Parent id of the item + /// + public string ParentId { get; set; } + + /// + /// Sort order + /// + public uint Sort { get; set; } + + /// + /// Name of the item + /// + public string Name { get; set; } + + /// + /// Displays a description on hover + /// + public string Description { get; set; } + + /// + /// Icon to display alongside the name + /// + public string Icon { get; set; } + + /// + /// Whether this item is open in case it contains children + /// + public bool IsOpen { get; set; } + + /// + /// Displays a small icon (with hover text) next to the main item icon + /// + public TreeItemModifier Modifier { get; set; } + + /// + /// Whether this item has children + /// + public bool HasChildren { get; set; } + + /// + /// Count of children + /// + public int ChildCount { get; set; } + + /// + /// Whether this item is published or not + /// + public bool IsInactive { get; set; } + + /// + /// Whether to display the item icon with a dashed line + /// + public bool IsDashed { get; set; } + + /// + /// Whether to show actions menu. This will only work when the onActionsRequested cb is implemented in the component + /// + public bool HasActions { get; set; } + + /// + /// Output an additional count value. + /// + public int? CountOutput { get; set; } +} + + +/// +/// The modifier displays a small icon (with hover text) next to the main item icon +/// +public class TreeItemModifier +{ + /// + /// Name of the modifier + /// + public string Name { get; set; } + + /// + /// Icon to display + /// + public string Icon { get; set; } + + public TreeItemModifier() { } + + public TreeItemModifier(string name, string icon) + { + Name = name; + Icon = icon; + } +} diff --git a/zero.Backoffice/Modules/Countries/CountriesEndpoint.cs b/zero.Backoffice/Modules/Countries/CountriesEndpoint.cs index 74c1ec42..0d863475 100644 --- a/zero.Backoffice/Modules/Countries/CountriesEndpoint.cs +++ b/zero.Backoffice/Modules/Countries/CountriesEndpoint.cs @@ -1,8 +1,6 @@ using Microsoft.AspNetCore.Mvc; using Raven.Client.Documents; using Raven.Client.Documents.Linq; -using System.Threading.Tasks; -using zero; namespace zero.Backoffice.Modules; diff --git a/zero.Backoffice/Modules/Countries/CountryEditModel.cs b/zero.Backoffice/Modules/Countries/CountryEditModel.cs index c08f4960..d8f8bc3b 100644 --- a/zero.Backoffice/Modules/Countries/CountryEditModel.cs +++ b/zero.Backoffice/Modules/Countries/CountryEditModel.cs @@ -1,15 +1,14 @@ using System; -namespace zero.Web.Models +namespace zero.Backoffice.Modules; + +public class CountryEditModel : ObsoleteEditModel { - public class CountryEditModel : ObsoleteEditModel - { - public string Name { get; set; } + public string Name { get; set; } - public bool IsPreferred { get; set; } + public bool IsPreferred { get; set; } - public string Code { get; set; } + public string Code { get; set; } - public string LanguageId { get; set; } - } -} + public string LanguageId { get; set; } +} \ No newline at end of file diff --git a/zero.Backoffice/Modules/Countries/CountryListModel.cs b/zero.Backoffice/Modules/Countries/CountryListModel.cs index d4b011b0..16fc74f5 100644 --- a/zero.Backoffice/Modules/Countries/CountryListModel.cs +++ b/zero.Backoffice/Modules/Countries/CountryListModel.cs @@ -1,15 +1,12 @@ -using System; +namespace zero.Backoffice.Modules; -namespace zero.Web.Models +public class CountryListModel : ListModel { - public class CountryListModel : ListModel - { - public string Name { get; set; } + public string Name { get; set; } - public bool IsActive { get; set; } + public bool IsActive { get; set; } - public bool IsPreferred { get; set; } + public bool IsPreferred { get; set; } - public string Code { get; set; } - } + public string Code { get; set; } } diff --git a/zero.Backoffice/Modules/Countries/Module.cs b/zero.Backoffice/Modules/Countries/Module.cs index c1c18852..76e35379 100644 --- a/zero.Backoffice/Modules/Countries/Module.cs +++ b/zero.Backoffice/Modules/Countries/Module.cs @@ -1,6 +1,4 @@ -using Microsoft.Extensions.DependencyInjection; - -namespace zero.Backoffice.Modules; +namespace zero.Backoffice.Modules; internal class CountriesModule : ZeroModule { @@ -14,6 +12,6 @@ internal class CountriesModule : ZeroModule /// public override void Configure(IZeroOptions options) { - options.Raven.Indexes.Add(); + options.For().Indexes.Add(); } } \ No newline at end of file diff --git a/zero.Backoffice/Modules/Countries/Indexes/Countries_Listing.cs b/zero.Backoffice/Modules/Countries/zero_Backoffice_Countries_Listing.cs similarity index 81% rename from zero.Backoffice/Modules/Countries/Indexes/Countries_Listing.cs rename to zero.Backoffice/Modules/Countries/zero_Backoffice_Countries_Listing.cs index cba6d0f9..45b3713d 100644 --- a/zero.Backoffice/Modules/Countries/Indexes/Countries_Listing.cs +++ b/zero.Backoffice/Modules/Countries/zero_Backoffice_Countries_Listing.cs @@ -2,7 +2,7 @@ namespace zero.Backoffice.Modules; -public class Countries_Listing : ZeroIndex +public class zero_Backoffice_Countries_Listing : ZeroIndex { protected override void Create() { diff --git a/old/zero.Web/Models/LanguageEditModel.cs b/zero.Backoffice/Modules/Languages/LanguageEditModel.cs similarity index 100% rename from old/zero.Web/Models/LanguageEditModel.cs rename to zero.Backoffice/Modules/Languages/LanguageEditModel.cs diff --git a/old/zero.Web/Models/LanguageListModel.cs b/zero.Backoffice/Modules/Languages/LanguageListModel.cs similarity index 100% rename from old/zero.Web/Models/LanguageListModel.cs rename to zero.Backoffice/Modules/Languages/LanguageListModel.cs diff --git a/old/zero.Web/Controllers/LanguagesController.cs b/zero.Backoffice/Modules/Languages/LanguagesController.cs similarity index 100% rename from old/zero.Web/Controllers/LanguagesController.cs rename to zero.Backoffice/Modules/Languages/LanguagesController.cs diff --git a/zero.Backoffice/Indexes/zero_Languages.cs b/zero.Backoffice/Modules/Languages/zero_Languages.cs similarity index 100% rename from zero.Backoffice/Indexes/zero_Languages.cs rename to zero.Backoffice/Modules/Languages/zero_Languages.cs diff --git a/old/zero.Web/Controllers/MailTemplatesController.cs b/zero.Backoffice/Modules/Mails/MailTemplatesController.cs similarity index 100% rename from old/zero.Web/Controllers/MailTemplatesController.cs rename to zero.Backoffice/Modules/Mails/MailTemplatesController.cs diff --git a/zero.Backoffice/Indexes/zero_MailTemplates.cs b/zero.Backoffice/Modules/Mails/zero_MailTemplates.cs similarity index 100% rename from zero.Backoffice/Indexes/zero_MailTemplates.cs rename to zero.Backoffice/Modules/Mails/zero_MailTemplates.cs diff --git a/zero.Backoffice/Modules/Search/BackofficeSearchService.cs b/zero.Backoffice/Modules/Search/BackofficeSearchService.cs index 1d55cdb4..3f62f376 100644 --- a/zero.Backoffice/Modules/Search/BackofficeSearchService.cs +++ b/zero.Backoffice/Modules/Search/BackofficeSearchService.cs @@ -1,84 +1,73 @@ using Raven.Client.Documents; using Raven.Client.Documents.Queries; using Raven.Client.Documents.Session; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using zero.Core.Database; -using zero.Core.Database.Indexes; -using zero.Core.Entities; -using zero.Core.Extensions; -using zero.Core.Models; -using zero.Core.Options; -namespace zero.Core.Services +namespace zero.Backoffice.Modules; + +public class BackofficeSearchService : IBackofficeSearchService { - public class BackofficeSearchService : IBackofficeSearchService - { - protected IZeroStore Store { get; private set; } + protected IZeroStore Store { get; private set; } - protected IZeroOptions Options { get; private set; } + protected IZeroOptions Options { get; private set; } - public BackofficeSearchService(IZeroStore store, IZeroOptions options) - { - Store = store; - Options = options; - } - - - public async Task> Query(string searchTerm) - { - string[] searchParts = searchTerm.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries).Select(x => - { - return "*" + x + "*"; - }).ToArray(); - - List results = await Store.Session().Query() - .Statistics(out QueryStatistics stats) - .Search(x => x.Name, searchParts, 2, @operator: SearchOperator.And) - .Search(x => x.Fields, searchParts, 1, Raven.Client.Documents.SearchOptions.Or, @operator: SearchOperator.And) - .Paging(1, 20) - .As() - .ToListAsync(); - - List items = new(); - - IReadOnlyCollection maps = Options.Search.GetAllItems(); - - foreach (ZeroEntity result in results) - { - Type type = result.GetType(); - SearchIndexMap map = maps.FirstOrDefault(x => x.CanModify(type)); - - SearchResult searchResult = new() - { - Id = result.Id, - Icon = map._Icon.Or("fth-folder"), - Name = result.Name, - IsActive = result.IsActive, - Group = GetGroupName(map.Type), - Url = "/" - }; - - await map.Modify(result, searchResult, Options); - - items.Add(searchResult); - } - - return new ListResult(items, stats.TotalResults, 1, 20); - } - - - protected string GetGroupName(Type type) - { - return "@search.collection." + type.Name.ToCamelCase(); - } + public BackofficeSearchService(IZeroStore store, IZeroOptions options) + { + Store = store; + Options = options; } - public interface IBackofficeSearchService + + public async Task> Query(string searchTerm) { - Task> Query(string searchTerm); + string[] searchParts = searchTerm.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries).Select(x => + { + return "*" + x + "*"; + }).ToArray(); + + List results = await Store.Session().Query() + .Statistics(out QueryStatistics stats) + .Search(x => x.Name, searchParts, 2, @operator: SearchOperator.And) + .Search(x => x.Fields, searchParts, 1, Raven.Client.Documents.SearchOptions.Or, @operator: SearchOperator.And) + .Paging(1, 20) + .As() + .ToListAsync(); + + List items = new(); + + IReadOnlyCollection maps = Options.For().GetAllItems(); + + foreach (ZeroEntity result in results) + { + Type type = result.GetType(); + SearchIndexMap map = maps.FirstOrDefault(x => x.CanModify(type)); + + SearchResult searchResult = new() + { + Id = result.Id, + Icon = map._Icon.Or("fth-folder"), + Name = result.Name, + IsActive = result.IsActive, + Group = GetGroupName(map.Type), + Url = "/" + }; + + await map.Modify(result, searchResult, Options); + + items.Add(searchResult); + } + + return new ListResult(items, stats.TotalResults, 1, 20); + } + + + protected string GetGroupName(Type type) + { + return "@search.collection." + type.Name.ToCamelCase(); } } + +public interface IBackofficeSearchService +{ + Task> Query(string searchTerm); +} \ No newline at end of file diff --git a/zero.Backoffice/Modules/Search/Backoffice_Search.cs b/zero.Backoffice/Modules/Search/Backoffice_Search.cs deleted file mode 100644 index f3660c40..00000000 --- a/zero.Backoffice/Modules/Search/Backoffice_Search.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Raven.Client.Documents; -using zero.Core.Options; - -namespace zero.Core.Database.Indexes -{ - public class Backoffice_Search : ZeroJavascriptIndex - { - public override void Setup(IZeroOptions options, IDocumentStore store) - { - // TODO index.Conventions is null, but needed for collection name retrieval - - foreach (var map in options.Search.GetAllItems()) - { - Maps.Add(map.BuildInstruction(store)); - } - - - //Index(nameof(SearchIndexResult.Name), FieldIndexing.Search); - //Index(nameof(SearchIndexResult.Fields), FieldIndexing.Search); - } - } -} diff --git a/zero.Backoffice/Modules/Search/Module.cs b/zero.Backoffice/Modules/Search/Module.cs new file mode 100644 index 00000000..df802828 --- /dev/null +++ b/zero.Backoffice/Modules/Search/Module.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace zero.Backoffice.Modules; + +internal class SearchModule : ZeroModule +{ + /// + public override void Register(IZeroModuleConfiguration config) + { + config.Services.AddOptions().Bind(config.Configuration.GetSection(SearchOptions.KEY)); + config.Services.AddScoped(); + } + + + /// + public override void Configure(IZeroOptions options) + { + options.For().Indexes.Add(); + } +} \ No newline at end of file diff --git a/zero.Backoffice/Modules/Search/SearchEndpoint.cs b/zero.Backoffice/Modules/Search/SearchEndpoint.cs new file mode 100644 index 00000000..ad3800e3 --- /dev/null +++ b/zero.Backoffice/Modules/Search/SearchEndpoint.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc; + +namespace zero.Backoffice.Modules; + +[ZeroAuthorize] +public class SearchController : BackofficeController +{ + IBackofficeSearchService SearchService; + + public SearchController(IBackofficeSearchService searchService) + { + SearchService = searchService; + } + + public async Task> Query([FromQuery] string query) => await SearchService.Query(query); +} \ No newline at end of file diff --git a/zero.Core/Configuration/ConfigurationParts/SearchOptions.cs b/zero.Backoffice/Modules/Search/SearchIndexMap.cs similarity index 76% rename from zero.Core/Configuration/ConfigurationParts/SearchOptions.cs rename to zero.Backoffice/Modules/Search/SearchIndexMap.cs index fde2706a..65dfae9d 100644 --- a/zero.Core/Configuration/ConfigurationParts/SearchOptions.cs +++ b/zero.Backoffice/Modules/Search/SearchIndexMap.cs @@ -1,41 +1,6 @@ using Raven.Client.Documents; -using System; -using System.Linq; -using System.Threading.Tasks; -using zero.Core.Models; -using zero.Core.Renderer; - -namespace zero.Configuration; - -public class SearchOptions : OptionsEnumerable, IOptionsEnumerable -{ - public bool IsEnabled { get; set; } - - - public SearchOptions() - { - IsEnabled = true; - //Map().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("fth-image"); - } - - - public SearchIndexMap Map(string icon = null) where T : ZeroEntity, new() - { - SearchIndexMap map = new(icon); - Items.Add(map); - return map; - } -} +namespace zero.Backoffice.Modules; public class SearchIndexMap { @@ -99,7 +64,7 @@ public class SearchIndexMap public class SearchIndexMap : SearchIndexMap where T : ZeroEntity { - internal SearchIndexMap(string icon = null) : base(typeof(T), icon) {} + internal SearchIndexMap(string icon = null) : base(typeof(T), icon) { } public SearchIndexMap Icon(string icon) { diff --git a/zero.Backoffice/Modules/Search/SearchOptions.cs b/zero.Backoffice/Modules/Search/SearchOptions.cs new file mode 100644 index 00000000..a4c2ddc0 --- /dev/null +++ b/zero.Backoffice/Modules/Search/SearchOptions.cs @@ -0,0 +1,32 @@ +namespace zero.Backoffice.Modules; + +public class SearchOptions : OptionsEnumerable, IOptionsEnumerable +{ + public static string KEY { get; set; } = "Zero:Backoffice:Search"; + + public bool Enabled { get; set; } + + + public SearchOptions() + { + Enabled = true; + //Map().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("fth-image"); + } + + + public SearchIndexMap Map(string icon = null) where T : ZeroEntity, new() + { + SearchIndexMap map = new(icon); + Items.Add(map); + return map; + } +} \ No newline at end of file diff --git a/zero.Backoffice/Modules/Search/SearchResult.cs b/zero.Backoffice/Modules/Search/SearchResult.cs new file mode 100644 index 00000000..c0d5b958 --- /dev/null +++ b/zero.Backoffice/Modules/Search/SearchResult.cs @@ -0,0 +1,34 @@ +namespace zero.Backoffice.Modules; + +public class SearchResult +{ + public string Id { get; set; } + + public string Name { get; set; } + + public string Description { get; set; } + + public string Icon { get; set; } + + public string Image { get; set; } + + public string Url { get; set; } + + public string Group { get; set; } + + public bool IsActive { get; set; } +} + + +public class SearchIndexResult +{ + public string Id { get; set; } + + public string Group { get; set; } + + public string Name { get; set; } + + public bool IsActive { get; set; } + + public List Fields { get; set; } = new(); +} \ No newline at end of file diff --git a/zero.Backoffice/Modules/Search/zero_Backoffice_Search.cs b/zero.Backoffice/Modules/Search/zero_Backoffice_Search.cs new file mode 100644 index 00000000..e3be9821 --- /dev/null +++ b/zero.Backoffice/Modules/Search/zero_Backoffice_Search.cs @@ -0,0 +1,20 @@ +using Raven.Client.Documents; + +namespace zero.Backoffice.Modules; + +public class zero_Backoffice_Search : ZeroJavascriptIndex +{ + public override void Setup(IZeroOptions options, IDocumentStore store) + { + // TODO index.Conventions is null, but needed for collection name retrieval + + foreach (var map in options.For().GetAllItems()) + { + Maps.Add(map.BuildInstruction(store)); + } + + + //Index(nameof(SearchIndexResult.Name), FieldIndexing.Search); + //Index(nameof(SearchIndexResult.Fields), FieldIndexing.Search); + } +} \ No newline at end of file diff --git a/old/zero.Web/Models/SpaceContentEditModel.cs b/zero.Backoffice/Modules/Spaces/SpaceContentEditModel.cs similarity index 100% rename from old/zero.Web/Models/SpaceContentEditModel.cs rename to zero.Backoffice/Modules/Spaces/SpaceContentEditModel.cs diff --git a/old/zero.Web/Defaults/SpacePermissions.cs b/zero.Backoffice/Modules/Spaces/SpacePermissions.cs similarity index 100% rename from old/zero.Web/Defaults/SpacePermissions.cs rename to zero.Backoffice/Modules/Spaces/SpacePermissions.cs diff --git a/old/zero.Web/Controllers/SpacesController.cs b/zero.Backoffice/Modules/Spaces/SpacesController.cs similarity index 100% rename from old/zero.Web/Controllers/SpacesController.cs rename to zero.Backoffice/Modules/Spaces/SpacesController.cs diff --git a/zero.Backoffice/Indexes/zero_Spaces.cs b/zero.Backoffice/Modules/Spaces/zero_Spaces.cs similarity index 100% rename from zero.Backoffice/Indexes/zero_Spaces.cs rename to zero.Backoffice/Modules/Spaces/zero_Spaces.cs diff --git a/zero.Backoffice/Modules/Translations/Module.cs b/zero.Backoffice/Modules/Translations/Module.cs new file mode 100644 index 00000000..17ca36cd --- /dev/null +++ b/zero.Backoffice/Modules/Translations/Module.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace zero.Backoffice.Modules; + +internal class TranslationsModule : ZeroModule +{ + /// + public override void Register(IZeroModuleConfiguration config) + { + config.Services.AddOptions().Bind(config.Configuration.GetSection(SearchOptions.KEY)); + config.Services.AddScoped(); + } + + + /// + public override void Configure(IZeroOptions options) + { + options.For().Indexes.Add(); + } +} \ No newline at end of file diff --git a/old/zero.Web/Models/TranslationEditModel.cs b/zero.Backoffice/Modules/Translations/TranslationEditModel.cs similarity index 100% rename from old/zero.Web/Models/TranslationEditModel.cs rename to zero.Backoffice/Modules/Translations/TranslationEditModel.cs diff --git a/old/zero.Web/Models/TranslationListModel.cs b/zero.Backoffice/Modules/Translations/TranslationListModel.cs similarity index 100% rename from old/zero.Web/Models/TranslationListModel.cs rename to zero.Backoffice/Modules/Translations/TranslationListModel.cs diff --git a/old/zero.Web/Controllers/TranslationsController.cs b/zero.Backoffice/Modules/Translations/TranslationsController.cs similarity index 100% rename from old/zero.Web/Controllers/TranslationsController.cs rename to zero.Backoffice/Modules/Translations/TranslationsController.cs diff --git a/zero.Backoffice/Modules/Translations/zero_Backoffice_Translations.cs b/zero.Backoffice/Modules/Translations/zero_Backoffice_Translations.cs new file mode 100644 index 00000000..192d045b --- /dev/null +++ b/zero.Backoffice/Modules/Translations/zero_Backoffice_Translations.cs @@ -0,0 +1,19 @@ +using Raven.Client.Documents.Indexes; + +namespace zero.Backoffice.Modules; + +public class zero_Backoffice_Translations : ZeroIndex +{ + protected override void Create() + { + Map = items => items.Select(item => new + { + Key = item.Key, + Value = item.Value, + CreatedDate = item.CreatedDate + }); + + Index(x => x.Key, FieldIndexing.Search); + Index(x => x.Value, FieldIndexing.Search); + } +}; \ No newline at end of file diff --git a/zero.Backoffice/Sections/DashboardSection.cs b/zero.Backoffice/Sections/DashboardSection.cs new file mode 100644 index 00000000..3974da5e --- /dev/null +++ b/zero.Backoffice/Sections/DashboardSection.cs @@ -0,0 +1,22 @@ +namespace zero.Backoffice.Sections; + +/// +/// The dashboard aggregates data from all sections +/// +public class DashboardSection : IInternalSection +{ + /// + public string Alias => Constants.Sections.Dashboard; + + /// + public string Name => "@sections.item.dashboard"; + + /// + public string Icon => "fth-pie-chart"; + + /// + public string Color => null; + + /// + public IList Children => new List(); +} \ No newline at end of file diff --git a/zero.Backoffice/Sections/IChildSection.cs b/zero.Backoffice/Sections/IChildSection.cs new file mode 100644 index 00000000..643957e1 --- /dev/null +++ b/zero.Backoffice/Sections/IChildSection.cs @@ -0,0 +1,17 @@ +namespace zero.Backoffice.Sections; + +/// +/// A child section is a sub-navigation item of a section +/// +public interface IChildSection +{ + /// + /// The section alias which acts as the url slug for navigation + /// + public string Alias { get; } + + /// + /// The name of the section (either a string or a translation key with @ prefix) + /// + public string Name { get; } +} \ No newline at end of file diff --git a/zero.Backoffice/Sections/ISection.cs b/zero.Backoffice/Sections/ISection.cs new file mode 100644 index 00000000..db87e4e3 --- /dev/null +++ b/zero.Backoffice/Sections/ISection.cs @@ -0,0 +1,37 @@ +namespace zero.Backoffice.Sections; + +/// +/// Internal section +/// +public interface IInternalSection : ISection { } + +/// +/// A section is a main part of the backoffice application +/// +public interface ISection +{ + /// + /// The section alias which acts as the url slug for navigation + /// + string Alias { get; } + + /// + /// The name of the section (either a string or a translation key with @ prefix) + /// + string Name { get; } + + /// + /// Icon of the section + /// + string Icon { get; } + + /// + /// HEX color (#aabbcc or #abc) + /// + string Color { get; } + + /// + /// Children are displayed as a sub-navigation in the main nav area + /// + IList Children { get; } +} \ No newline at end of file diff --git a/zero.Backoffice/Sections/MediaSection.cs b/zero.Backoffice/Sections/MediaSection.cs new file mode 100644 index 00000000..c220f34f --- /dev/null +++ b/zero.Backoffice/Sections/MediaSection.cs @@ -0,0 +1,22 @@ +namespace zero.Backoffice.Sections; + +/// +/// Media items (images, videos, documents) grouped in folders +/// +public class MediaSection : IInternalSection +{ + /// + public string Alias => Constants.Sections.Media; + + /// + public string Name => "@sections.item.media"; + + /// + public string Icon => "fth-image"; + + /// + public string Color => "#d82853"; + + /// + public IList Children => new List(); +} \ No newline at end of file diff --git a/zero.Backoffice/Sections/PagesSection.cs b/zero.Backoffice/Sections/PagesSection.cs new file mode 100644 index 00000000..c7cab504 --- /dev/null +++ b/zero.Backoffice/Sections/PagesSection.cs @@ -0,0 +1,22 @@ +namespace zero.Backoffice.Sections; + +/// +/// Manage the page tree in this section +/// +public class PagesSection : IInternalSection +{ + /// + public string Alias => Constants.Sections.Pages; + + /// + public string Name => "@sections.item.pages"; + + /// + public string Icon => "fth-folder"; + + /// + public string Color => "#0cb0f5"; + + /// + public IList Children => new List(); +} \ No newline at end of file diff --git a/zero.Backoffice/Sections/Section.cs b/zero.Backoffice/Sections/Section.cs new file mode 100644 index 00000000..d5c356bc --- /dev/null +++ b/zero.Backoffice/Sections/Section.cs @@ -0,0 +1,35 @@ +namespace zero.Backoffice.Sections; + +/// +/// A section is a main part of the backoffice application +/// +public class Section : ISection, IChildSection +{ + /// + public string Alias { get; set; } + + /// + public string Name { get; set; } + + /// + public string Icon { get; set; } + + /// + /// HEX color (#aabbcc or #abc). Defaults to a neutral color + /// + public string Color { get; set; } + + /// + public IList Children { get; } = new List(); + + + public Section() { } + + public Section(string alias, string name, string icon = null, string color = null) + { + Alias = alias; + Name = name; + Icon = icon; + Color = color; + } +} diff --git a/zero.Backoffice/Sections/SettingsSection.cs b/zero.Backoffice/Sections/SettingsSection.cs new file mode 100644 index 00000000..ccc506f1 --- /dev/null +++ b/zero.Backoffice/Sections/SettingsSection.cs @@ -0,0 +1,22 @@ +namespace zero.Backoffice.Sections; + +/// +/// Website and backoffice settings +/// +public class SettingsSection : IInternalSection +{ + /// + public string Alias => Constants.Sections.Settings; + + /// + public string Name => "@sections.item.settings"; + + /// + public string Icon => "fth-settings"; + + /// + public string Color => null; + + /// + public IList Children => new List(); +} diff --git a/zero.Backoffice/Sections/SpacesSection.cs b/zero.Backoffice/Sections/SpacesSection.cs new file mode 100644 index 00000000..e038c7bb --- /dev/null +++ b/zero.Backoffice/Sections/SpacesSection.cs @@ -0,0 +1,22 @@ +namespace zero.Backoffice.Sections; + +/// +/// Global list entities +/// +public class SpacesSection : IInternalSection +{ + /// + public string Alias => Constants.Sections.Spaces; + + /// + public string Name => "@sections.item.spaces"; + + /// + public string Icon => "fth-layers"; + + /// + public string Color => "#f9c202"; + + /// + public IList Children => new List(); +} \ No newline at end of file diff --git a/zero.Backoffice/Usings.cs b/zero.Backoffice/Usings.cs index 8edf5a97..5d184060 100644 --- a/zero.Backoffice/Usings.cs +++ b/zero.Backoffice/Usings.cs @@ -22,4 +22,6 @@ global using zero.Rendering; global using zero.Media; global using zero.Applications; -global using zero.Backoffice; \ No newline at end of file +global using zero.Backoffice; +global using zero.Backoffice.Models; +global using zero.Backoffice.Modules; \ No newline at end of file diff --git a/old/zero.Web/Views/Zero/Index.cshtml b/zero.Backoffice/Views/Zero/Index.cshtml similarity index 100% rename from old/zero.Web/Views/Zero/Index.cshtml rename to zero.Backoffice/Views/Zero/Index.cshtml diff --git a/old/zero.Web/Views/Zero/Setup.cshtml b/zero.Backoffice/Views/Zero/Setup.cshtml similarity index 100% rename from old/zero.Web/Views/Zero/Setup.cshtml rename to zero.Backoffice/Views/Zero/Setup.cshtml diff --git a/old/zero.Web/Views/index.tpl.html b/zero.Backoffice/Views/index.tpl.html similarity index 100% rename from old/zero.Web/Views/index.tpl.html rename to zero.Backoffice/Views/index.tpl.html diff --git a/old/zero.Web/ZeroVue.cs b/zero.Backoffice/ZeroVue.cs similarity index 99% rename from old/zero.Web/ZeroVue.cs rename to zero.Backoffice/ZeroVue.cs index 94c66e5f..d232703b 100644 --- a/old/zero.Web/ZeroVue.cs +++ b/zero.Backoffice/ZeroVue.cs @@ -64,7 +64,7 @@ namespace zero.Web config.Path = Options.BackofficePath.EnsureEndsWith('/'); config.ApiPath = config.Path + "api/"; config.PluginPath = "@/Plugins"; - config.Version = Options.ZeroVersion; + config.Version = Options.Version; config.PluginCount = Plugins.Count(); config.ErrorFieldNone = Constants.ErrorFieldNone; config.Alias = CreateAliases(); diff --git a/zero.Backoffice/zero.Backoffice.csproj b/zero.Backoffice/zero.Backoffice.csproj index 51337038..5c89cc8e 100644 --- a/zero.Backoffice/zero.Backoffice.csproj +++ b/zero.Backoffice/zero.Backoffice.csproj @@ -12,6 +12,8 @@ + + @@ -30,6 +32,14 @@ true PreserveNewest + + true + PreserveNewest + + + true + PreserveNewest + diff --git a/old/zero.Web/zero.Web.targets b/zero.Backoffice/zero.Backoffice.targets similarity index 52% rename from old/zero.Web/zero.Web.targets rename to zero.Backoffice/zero.Backoffice.targets index ad581c96..0bcc4d76 100644 --- a/old/zero.Web/zero.Web.targets +++ b/zero.Backoffice/zero.Backoffice.targets @@ -1,18 +1,18 @@ - + $(PrepareForRunDependsOn); - CopyZeroWebUI + CopyZeroBackofficeUi - - + + diff --git a/zero.Core/Architecture/Blueprints/BlueprintChildInterceptor.cs b/zero.Core/Architecture/Blueprints/BlueprintChildInterceptor.cs index e5288635..5263050d 100644 --- a/zero.Core/Architecture/Blueprints/BlueprintChildInterceptor.cs +++ b/zero.Core/Architecture/Blueprints/BlueprintChildInterceptor.cs @@ -1,49 +1,31 @@ -//using Microsoft.Extensions.Logging; -//using System.Threading.Tasks; -//using zero.Core.Collections; -//using zero.Core.Database; -//using zero.Core.Entities; +namespace zero.Architecture; -//namespace zero.Core.Blueprints -//{ -// public class BlueprintChildInterceptor : CollectionInterceptor -// { -// protected IZeroContext Context { get; set; } - -// protected IZeroStore Store { get; set; } - -// protected ILogger Logger { get; set; } - -// protected IBlueprintService BlueprintService { get; set; } +public class BlueprintChildInterceptor : Interceptor, IBlueprintInterceptor +{ + string configuredZeroDatabase; -// public BlueprintChildInterceptor(IZeroContext context, IZeroStore store, ILogger logger, IBlueprintService blueprintService) -// { -// Context = context; -// Store = store; -// Logger = logger; -// BlueprintService = blueprintService; -// } + public BlueprintChildInterceptor(IZeroContext context) + { + configuredZeroDatabase = context.Options.For().Database; + } -// /// -// public override bool CanRun(InterceptorParameters args, ZeroIdEntity model) -// { -// // this interceptor does only work for child entities -// return args.Context.Store.ResolvedDatabase != Context.Options.Raven.Database; -// } + /// + /// Only run this interceptor when the database is an app database + /// + public override bool CanHandle(InterceptorParameters args, Type modelType) => args.Context.Store.ResolvedDatabase != configuredZeroDatabase; -// /// -// public override Task> Deleting(InterceptorParameters args, ZeroIdEntity model) -// { -// if (model is not ZeroEntity || (model as ZeroEntity).Blueprint != null) -// { -// InterceptorResult result = new(); -// result.Result = EntityResult.Fail("@blueprint.errors.cannotDeleteChild"); -// return Task.FromResult(result); -// } + /// + public override Task> Deleting(InterceptorParameters args, ZeroEntity model) + { + if (model.Blueprint == null) + { + return default; + } -// return base.Deleting(args, model); -// } -// } -//} + InterceptorResult result = new(); + result.Result = EntityResult.Fail("@blueprint.errors.cannotDeleteChild"); + return Task.FromResult(result); + } +} \ No newline at end of file diff --git a/zero.Core/Architecture/Blueprints/BlueprintInterceptor.cs b/zero.Core/Architecture/Blueprints/BlueprintInterceptor.cs index c8fae0bd..a7cce1f6 100644 --- a/zero.Core/Architecture/Blueprints/BlueprintInterceptor.cs +++ b/zero.Core/Architecture/Blueprints/BlueprintInterceptor.cs @@ -1,134 +1,134 @@ -//using Microsoft.Extensions.Logging; -//using Raven.Client.Documents; -//using Raven.Client.Documents.Session; -//using System.Collections.Generic; -//using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Raven.Client.Documents; +using Raven.Client.Documents.Session; -//namespace zero.Core.Blueprints -//{ -// public class BlueprintInterceptor : CollectionInterceptor -// { -// protected IZeroContext Context { get; set; } +namespace zero.Architecture; -// protected IZeroStore Store { get; set; } +public class BlueprintInterceptor : Interceptor, IBlueprintInterceptor +{ + protected IZeroContext Context { get; set; } -// protected ILogger Logger { get; set; } + protected IZeroStore Store { get; set; } -// protected IBlueprintService BlueprintService { get; set; } + protected ILogger Logger { get; set; } -// IList Apps { get; set; } + protected IBlueprintService BlueprintService { get; set; } + + protected IInterceptors Interceptors { get; set; } + + IList apps; + + string configuredZeroDatabase; -// public BlueprintInterceptor(IZeroContext context, IZeroStore store, ILogger logger, IBlueprintService blueprintService) -// { -// Context = context; -// Store = store; -// Logger = logger; -// BlueprintService = blueprintService; -// } + public BlueprintInterceptor(IZeroContext context, IZeroStore store, ILogger logger, IBlueprintService blueprintService, IInterceptors interceptors) + { + Context = context; + Store = store; + Logger = logger; + BlueprintService = blueprintService; + configuredZeroDatabase = context.Options.For().Database; + Interceptors = interceptors; + } -// /// -// public override bool CanRun(InterceptorParameters args, ZeroIdEntity model) -// { -// // we do only update children if we operate on the shared database -// return args.Context.Store.ResolvedDatabase == Context.Options.Raven.Database; -// } + /// + /// Only run when operations are on the zero database + /// + public override bool CanHandle(InterceptorParameters args, Type modelType) => args.Context.Store.ResolvedDatabase == configuredZeroDatabase; -// /// -// public override async Task Saved(InterceptorParameters args, ZeroIdEntity model) -// { -// if (model is not ZeroEntity || !BlueprintService.TryGetBlueprint(model, out Blueprint blueprint)) -// { -// return; -// } - -// ZeroEntity entity = model as ZeroEntity; -// int count = 0; - -// foreach (Application app in await GetApplications()) -// { -// using ZeroContextScope scope = Context.CreateScope(app); -// IZeroDocumentSession session = scope.Store.Session(scope.Database); - -// ZeroEntity child = await session.LoadAsync(model.Id); - -// if (child == null) -// { -// child = entity.Clone(); -// child.Blueprint = new() { Id = model.Id }; -// } -// else -// { -// blueprint.Apply(entity, child); -// } - -// count += 1; - -// // now we have to store the child -// // but this will not work with the session as we need to run the scoped interceptors, -// // therefore we need access to the collection - -// await session.StoreAsync(child); -// await session.SaveChangesAsync(); -// } - -// Logger.LogDebug("Blueprint: Synced {count} children for {name} ({id})", count, entity.Name, model.Id); -// } + /// + public override Task Created(InterceptorParameters args, ZeroEntity model) => Saved(args, model, false); -// /// -// public override async Task Deleted(InterceptorParameters args, ZeroIdEntity model) -// { -// if (model is not ZeroEntity || !BlueprintService.TryGetBlueprint(model, out Blueprint blueprint)) -// { -// return; -// } - -// ZeroEntity entity = model as ZeroEntity; -// int count = 0; - -// foreach (Application app in await GetApplications()) -// { -// using ZeroContextScope scope = Context.CreateScope(app); -// IZeroDocumentSession session = scope.Store.Session(scope.Database); - -// count += 1; - -// // now we have to delete the child -// // but this will not work with the session as we need to run the scoped interceptors, -// // therefore we need access to the collection -// session.Delete(entity.Id); -// await session.SaveChangesAsync(); -// } - -// Logger.LogDebug("Blueprint: Deleted {count} children for {name} ({id})", count, entity.Name, entity.Id); -// } + /// + public override Task Updated(InterceptorParameters args, ZeroEntity model) => Saved(args, model, true); -// /// -// /// Get all applications to choose from -// /// -// async Task> GetApplications() -// { -// if (Apps != null) -// { -// return Apps; -// } + /// + public async Task Saved(InterceptorParameters args, ZeroEntity model, bool update = false) + { + if (!BlueprintService.TryGetBlueprint(model, out Blueprint blueprint)) + { + return; + } -// IAsyncDocumentSession session = Store.Session(global: true); -// Apps = await session.Query().ToListAsync(); -// return Apps; -// } + int count = 0; + + foreach (Application app in await GetApplications()) + { + using ZeroContextScope scope = Context.CreateScope(app); + IZeroDocumentSession session = scope.Store.Session(scope.Database); + + ZeroEntity child = await session.LoadAsync(model.Id); + + if (child == null) + { + child = model.Clone(); + child.Blueprint = new() { Id = model.Id }; + } + else + { + blueprint.Apply(model, child); + } + + count += 1; + + InterceptorInstruction interceptor = update ? Interceptors.ForUpdate(model) : Interceptors.ForCreate(model); + interceptor.Filter(x => x is not IBlueprintInterceptor); + + await interceptor.Start(); + await session.StoreAsync(child); + await interceptor.Complete(); + await session.SaveChangesAsync(); + } + + Logger.LogDebug("Blueprint: Synced {count} children for {name} ({id})", count, model.Name, model.Id); + } + /// + public override async Task Deleted(InterceptorParameters args, ZeroEntity model) + { + if (!BlueprintService.TryGetBlueprint(model, out Blueprint blueprint)) + { + return; + } + + int count = 0; + + foreach (Application app in await GetApplications()) + { + using ZeroContextScope scope = Context.CreateScope(app); + IZeroDocumentSession session = scope.Store.Session(scope.Database); + + count += 1; + + InterceptorInstruction interceptor = Interceptors.ForDelete(model); + interceptor.Filter(x => x is not IBlueprintInterceptor); + + await interceptor.Start(); + session.Delete(model.Id); + await interceptor.Complete(); + await session.SaveChangesAsync(); + } + + Logger.LogDebug("Blueprint: Deleted {count} children for {name} ({id})", count, model.Name, model.Id); + } -// /// -// //public override async Task Saved(SaveParameters args) -// //{ + /// + /// Get all applications to choose from + /// + async Task> GetApplications() + { + if (apps != null) + { + return apps; + } -// // //Logger.LogInformation("Route updates completed (+{added}/~{updated}/-{removed}) for {model} (id: {id})", countRoutes - countUpdatedRoutes, countUpdatedRoutes, obsoleteRoutes.Count, args.Model.Name, args.Model.Id); -// //} -// } -//} + IAsyncDocumentSession session = Store.Session(global: true); + apps = await session.Query().ToListAsync(); + return apps; + } +} \ No newline at end of file diff --git a/zero.Core/Architecture/Blueprints/IBlueprintInterceptor.cs b/zero.Core/Architecture/Blueprints/IBlueprintInterceptor.cs new file mode 100644 index 00000000..564f2ad4 --- /dev/null +++ b/zero.Core/Architecture/Blueprints/IBlueprintInterceptor.cs @@ -0,0 +1,6 @@ +namespace zero.Architecture; + +public interface IBlueprintInterceptor +{ + +} \ No newline at end of file diff --git a/zero.Core/Architecture/Module.cs b/zero.Core/Architecture/Module.cs index 246a588b..6275afcb 100644 --- a/zero.Core/Architecture/Module.cs +++ b/zero.Core/Architecture/Module.cs @@ -7,8 +7,8 @@ internal class ArchitectureModule : ZeroModule /// public override void Register(IZeroModuleConfiguration config) { - config.Services.AddScoped(); - //config.Services.AddScoped(); - //config.Services.AddScoped(); + config.Services.AddScoped(); + config.Services.AddScoped(); + config.Services.AddScoped(); } } \ No newline at end of file diff --git a/zero.Core/Architecture/Modules/ZeroModule.cs b/zero.Core/Architecture/Modules/ZeroModule.cs index 4d701b07..fe602b34 100644 --- a/zero.Core/Architecture/Modules/ZeroModule.cs +++ b/zero.Core/Architecture/Modules/ZeroModule.cs @@ -5,4 +5,4 @@ public class ZeroModule public virtual void Register(IZeroModuleConfiguration config) { } public virtual void Configure(IZeroOptions options) { } -} +} \ No newline at end of file diff --git a/zero.Core/Architecture/Modules/ZeroModuleInitializer.cs b/zero.Core/Architecture/Modules/ZeroModuleInitializer.cs new file mode 100644 index 00000000..6e95139d --- /dev/null +++ b/zero.Core/Architecture/Modules/ZeroModuleInitializer.cs @@ -0,0 +1,20 @@ +namespace zero.Architecture; + +internal class ZeroModuleInitializer +{ + public static void RegisterAll(IZeroModuleConfiguration config) + { + foreach (ZeroModule module in Registrations.Modules) + { + module.Register(config); + } + } + + public static void ConfigureAll(IZeroOptions options) + { + foreach (ZeroModule module in Registrations.Modules) + { + module.Configure(options); + } + } +} \ No newline at end of file diff --git a/zero.Core/Architecture/Plugins/ZeroPluginInitializer.cs b/zero.Core/Architecture/Plugins/ZeroPluginInitializer.cs new file mode 100644 index 00000000..11bb9a1c --- /dev/null +++ b/zero.Core/Architecture/Plugins/ZeroPluginInitializer.cs @@ -0,0 +1,48 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace zero.Architecture; + +internal class ZeroPluginInitializer +{ + /// + /// Adds a zero plugin + /// + public static void AddPlugin(IServiceCollection services, IConfiguration configuration) where T : class, IZeroPlugin, new() + { + AssemblyDiscovery.Current.AddAssembly(typeof(T).Assembly); + services.AddSingleton(); + AddPluginServices(services, configuration); + } + + + /// + /// Adds a zero plugin + /// + public static void AddPlugin(IServiceCollection services, IConfiguration configuration, Func implementationFactory) where T : class, IZeroPlugin, new() + { + AssemblyDiscovery.Current.AddAssembly(typeof(T).Assembly); + services.AddSingleton(implementationFactory); + AddPluginServices(services, configuration); + } + + + /// + /// Creates a temporary instance of the plugin to add additional services + /// + /// + static void AddPluginServices(IServiceCollection services, IConfiguration configuration) where T : class, IZeroPlugin, new() + { + try + { + T plugin = new T(); + + plugin.ConfigureServices(services, configuration); + services.Configure(opts => plugin.Configure(opts)); + } + catch + { + throw new Exception($"Plugin \"{nameof(T)}\" needs an additional parameterless constructor as ConfigureServices() is called before the DI container is built"); + } + } +} \ No newline at end of file diff --git a/zero.Core/Communication/Interceptors/CollectionInterceptor.cs b/zero.Core/Communication/Interceptors/CollectionInterceptor.cs deleted file mode 100644 index e545ea9e..00000000 --- a/zero.Core/Communication/Interceptors/CollectionInterceptor.cs +++ /dev/null @@ -1,88 +0,0 @@ -namespace zero.Communication; - -public abstract partial class CollectionInterceptor : ICollectionInterceptor where T : ZeroIdEntity -{ - /// - public virtual bool CanRun(InterceptorParameters args, T model) => true; - - /// - public virtual Task> Creating(InterceptorParameters args, T model) => Task.FromResult>(default); - - /// - public virtual Task> Updating(InterceptorParameters args, T model) => Task.FromResult>(default); - - /// - public virtual Task> Saving(InterceptorParameters args, T model) => Task.FromResult>(default); - - /// - public virtual Task> Deleting(InterceptorParameters args, T model) => Task.FromResult>(default); - - /// - public virtual Task Created(InterceptorParameters args, T model) => Task.CompletedTask; - - /// - public virtual Task Updated(InterceptorParameters args, T model) => Task.CompletedTask; - - /// - public virtual Task Saved(InterceptorParameters args, T model) => Task.CompletedTask; - - /// - public virtual Task Deleted(InterceptorParameters args, T model) => Task.CompletedTask; -} - - -public abstract partial class CollectionInterceptor : CollectionInterceptor, ICollectionInterceptor -{ - /// - public override bool CanRun(InterceptorParameters args, ZeroIdEntity model) => base.CanRun(args, model); -} - -public interface ICollectionInterceptor : ICollectionInterceptor { } - -public interface ICollectionInterceptor where T : ZeroIdEntity -{ - /// - /// Whether any of the interceptor methods is allowed to run based on the parameters - /// - bool CanRun(InterceptorParameters args, T model); - - /// - /// Called after an entity has been stored but before the session has saved its changes - /// - Task Created(InterceptorParameters args, T model); - - /// - /// Called before an entity is stored and validated - /// - Task> Creating(InterceptorParameters args, T model); - - /// - /// Called after an entity has been deleted but before the session has saved its changes - /// - Task Deleted(InterceptorParameters args, T model); - - /// - /// Called before an entity is deleted - /// - Task> Deleting(InterceptorParameters args, T model); - - /// - /// Called after an entity has been updated but before the session has saved its changes - /// - Task Updated(InterceptorParameters args, T model); - - /// - /// Called before an entity is stored and validated - /// - Task> Updating(InterceptorParameters args, T model); - - /// - /// Called after an entity has been saved (created or updated) but before the session has saved its changes - /// - Task Saved(InterceptorParameters args, T model); - - /// - /// Called before an entity is stored and validated - /// - Task> Saving(InterceptorParameters args, T model); -} \ No newline at end of file diff --git a/zero.Core/Communication/Interceptors/CollectionInterceptorHandler.cs b/zero.Core/Communication/Interceptors/CollectionInterceptorHandler.cs deleted file mode 100644 index aac896a1..00000000 --- a/zero.Core/Communication/Interceptors/CollectionInterceptorHandler.cs +++ /dev/null @@ -1,190 +0,0 @@ -//using FluentValidation; -//using Microsoft.Extensions.Logging; -//using System; -//using System.Collections.Concurrent; -//using System.Linq; -//using System.Linq.Expressions; -//using System.Threading.Tasks; -//using zero.Core.Entities; -//using zero.Core.Options; - -//namespace zero.Core.Collections -//{ -// public class CollectionInterceptorHandler : ICollectionInterceptorHandler -// { -// protected ConcurrentDictionary Interceptors { get; private set; } = new(); - -// protected IZeroOptions Options { get; set; } - -// protected IServiceProvider Services { get; set; } - -// protected ILogger Logger { get; set; } - - -// public CollectionInterceptorHandler(IZeroOptions options, IServiceProvider serviceProvider, ILogger logger) -// { -// Logger = logger; -// Options = options; -// Services = serviceProvider; -// } - - -// /// -// public InterceptorInstruction Create(string operationName, TParameters parameters) -// where T : ZeroEntity -// where TParameters : CollectionInterceptor.Parameters -// { -// InterceptorInstruction instruction = new(parameters); -// instruction.Operation = operationName; -// instruction.BeforeOperationHandler = async expression => await HandleBefore(expression, instruction); -// instruction.AfterOperationHandler = async expression => await HandleAfter(expression, instruction); - -// return instruction; -// } - - - -// /// -// /// Calls all matching interceptors with the specified expression -// /// -// internal async Task HandleBefore(Expression, Task>>> expression, InterceptorInstruction instruction) -// where T : ZeroEntity -// where TParameters : CollectionInterceptor.Parameters -// { -// string typeName = (typeof(T)).Name; -// Func, Task>> func = default; - -// foreach (InterceptorRegistration registration in ForType(typeof(T))) -// { -// if (!TryResolve(registration, out ICollectionInterceptor interceptor)) -// { -// continue; -// } - -// if (!interceptor.CanRun(instruction.Parameters)) -// { -// continue; -// } - -// if (func == default) -// { -// func = expression.Compile(); -// } - -// // we do not log save operations as they are always called for update/create which are already logged beforehand -// if (instruction.Operation != "save") -// { -// Logger.LogDebug("Run interceptor {interceptor} for {type}:{operation}", registration.Name, typeName, instruction.Operation); -// } - -// InterceptorResult result = await func(interceptor); - -// if (result == default) -// { -// result = new(); -// } - -// result.InterceptorHash = registration.Hash; -// instruction.Results.Add(result); - -// if (result.Result != null) -// { -// instruction.EntityResult = result.Result; -// break; -// } -// if (result.Prevent) -// { -// break; -// } -// } -// } - - - -// /// -// /// Calls all matching interceptors with the specified expression -// /// -// internal async Task HandleAfter(Expression, Task>> expression, InterceptorInstruction instruction) -// where T : ZeroEntity -// where TParameters : CollectionInterceptor.Parameters -// { -// Func, Task> func = default; -// instruction ??= new(); - -// foreach (InterceptorRegistration registration in ForType(typeof(T))) -// { -// if (!TryResolve(registration, out ICollectionInterceptor interceptor)) -// { -// continue; -// } - -// if (!interceptor.CanRun(instruction.Parameters)) -// { -// continue; -// } - -// InterceptorResult beforeResult = instruction?.Results.FirstOrDefault(res => res.InterceptorHash == registration.Hash); - -// if (func == default) -// { -// func = expression.Compile(); -// } - -// await func(interceptor); -// } -// } - - -// /// -// /// Get all interceptors for a certain type -// /// -// IOrderedEnumerable ForType(Type targetType) -// { -// return Options.Interceptors.GetAllItems().Where(x => x.CanHandle(targetType)).OrderByDescending(x => x.Gravity); -// // return Interceptors.Where(item => item.Types.Count == 0 || item.Types.Any(type => targetType.IsAssignableFrom(type))); -// } - - -// /// -// /// Resolves an interceptor from the service provider -// /// -// bool TryResolve(InterceptorRegistration registration, out ICollectionInterceptor interceptor) where T : ZeroEntity -// { -// Type type = registration.InterceptorType; - -// if (Interceptors.TryGetValue(type, out object interceptorObj)) -// { -// interceptor = interceptorObj as ICollectionInterceptor; -// return interceptor != null; -// } - -// object service = Services.GetService(type); -// interceptor = service as ICollectionInterceptor; - -// if (interceptor == null && service != null && service is ICollectionInterceptor) -// { -// interceptor = new CollectionInterceptorShim(service as ICollectionInterceptor); -// } - -// if (interceptor == null) -// { -// Logger.LogWarning("Could not resolve interceptor {interceptor}", registration.Name); -// } - -// Interceptors.TryAdd(type, interceptor); - -// return interceptor != null; -// } -// } - - -// public interface ICollectionInterceptorHandler -// { -// /// -// /// Creates a new interceptor instruction -// /// -// InterceptorInstruction Create(string operationName, TParameters parameters) -// where T : ZeroEntity -// where TParameters : CollectionInterceptor.Parameters; -// } -//} diff --git a/zero.Core/Communication/Interceptors/CollectionInterceptorShim.cs b/zero.Core/Communication/Interceptors/CollectionInterceptorShim.cs deleted file mode 100644 index 4f504d67..00000000 --- a/zero.Core/Communication/Interceptors/CollectionInterceptorShim.cs +++ /dev/null @@ -1,49 +0,0 @@ -namespace zero.Communication; - -public sealed class CollectionInterceptorShim : CollectionInterceptor where T : ZeroIdEntity -{ - ICollectionInterceptor _base; - - public CollectionInterceptorShim(ICollectionInterceptor baseInterceptor) - { - _base = baseInterceptor; - } - - InterceptorResult Result(InterceptorResult result) - { - return result == null ? null : new InterceptorResult() - { - InterceptorHash = result.InterceptorHash, - Parameters = result.Parameters, - Prevent = result.Prevent, - Result = result.Result != null ? EntityResult.From(result.Result, result.Result.Model as T) : null - }; - } - - /// - public override bool CanRun(InterceptorParameters args, T model) => _base.CanRun(args, model); - - /// - public override async Task> Creating(InterceptorParameters args, T model) => Result(await _base.Creating(args, model)); - - /// - public override async Task> Updating(InterceptorParameters args, T model) => Result(await _base.Updating(args, model)); - - /// - public override async Task> Saving(InterceptorParameters args, T model) => Result(await _base.Saving(args, model)); - - /// - public override async Task> Deleting(InterceptorParameters args, T model) => Result(await _base.Deleting(args, model)); - - /// - public override Task Created(InterceptorParameters args, T model) => _base.Created(args, model); - - /// - public override Task Updated(InterceptorParameters args, T model) => _base.Updated(args, model); - - /// - public override Task Saved(InterceptorParameters args, T model) => _base.Saved(args, model); - - /// - public override Task Deleted(InterceptorParameters args, T model) => _base.Deleted(args, model); -} diff --git a/zero.Core/Communication/Interceptors/Interceptor.cs b/zero.Core/Communication/Interceptors/Interceptor.cs new file mode 100644 index 00000000..0a841e97 --- /dev/null +++ b/zero.Core/Communication/Interceptors/Interceptor.cs @@ -0,0 +1,208 @@ +namespace zero.Communication; + + +public abstract partial class Interceptor : Interceptor, IInterceptor where T : ZeroIdEntity +{ + /// + public Type ModelType { get; protected set; } + + + public Interceptor() + { + Gravity = 0; + ModelType = typeof(T); + Name = ModelType.Name; + } + + + /// + public override bool CanHandle(InterceptorParameters args, Type modelType) => ModelType.IsAssignableFrom(modelType); + + /// + public virtual Task> Creating(InterceptorParameters args, T model) => Task.FromResult>(default); + + /// + public sealed override async Task> Creating(InterceptorParameters args, ZeroIdEntity model) => Convert(await Creating(args, model as T)); + + /// + public virtual Task> Updating(InterceptorParameters args, T model) => Task.FromResult>(default); + + /// + public sealed override async Task> Updating(InterceptorParameters args, ZeroIdEntity model) => Convert(await Updating(args, model as T)); + + /// + public virtual Task> Deleting(InterceptorParameters args, T model) => Task.FromResult>(default); + + /// + public sealed override async Task> Deleting(InterceptorParameters args, ZeroIdEntity model) => Convert(await Deleting(args, model as T)); + + /// + public virtual Task Created(InterceptorParameters args, T model) => Task.CompletedTask; + + /// + public sealed override Task Created(InterceptorParameters args, ZeroIdEntity model) => Created(args, model as T); + + /// + public virtual Task Updated(InterceptorParameters args, T model) => Task.CompletedTask; + + /// + public sealed override Task Updated(InterceptorParameters args, ZeroIdEntity model) => Updated(args, model as T); + + /// + public virtual Task Deleted(InterceptorParameters args, T model) => Task.CompletedTask; + + /// + public sealed override Task Deleted(InterceptorParameters args, ZeroIdEntity model) => Deleted(args, model as T); + + + InterceptorResult Convert(InterceptorResult result) + { + if (result == default) + { + return default; + } + + return new InterceptorResult() + { + Parameters = result.Parameters, + Continue = result.Continue, + InterceptorHash = result.InterceptorHash, + Result = result.Result != null ? EntityResult.From(result.Result, result.Result.Model) : null + }; + } +} + + +public abstract partial class Interceptor : IInterceptor +{ + /// + public int Gravity { get; protected set; } + + /// + public string Name { get; protected set; } + + /// + public string Hash { get; protected set; } + + + public Interceptor() + { + Gravity = 0; + Hash = IdGenerator.Create(); + } + + + /// + public virtual bool CanHandle(InterceptorParameters args, Type modelType) => true; + + /// + public virtual Task> Creating(InterceptorParameters args, ZeroIdEntity model) => Task.FromResult>(default); + + /// + public virtual Task> Updating(InterceptorParameters args, ZeroIdEntity model) => Task.FromResult>(default); + + /// + public virtual Task> Deleting(InterceptorParameters args, ZeroIdEntity model) => Task.FromResult>(default); + + /// + public virtual Task Created(InterceptorParameters args, ZeroIdEntity model) => Task.CompletedTask; + + /// + public virtual Task Updated(InterceptorParameters args, ZeroIdEntity model) => Task.CompletedTask; + + /// + public virtual Task Deleted(InterceptorParameters args, ZeroIdEntity model) => Task.CompletedTask; +} + + +public interface IInterceptor : IInterceptor where T : ZeroIdEntity +{ + /// + /// Type of the associated model + /// + Type ModelType { get; } + + /// + /// Called after an entity has been stored but before the session has saved its changes + /// + Task Created(InterceptorParameters args, T model); + + /// + /// Called before an entity is stored and validated + /// + Task> Creating(InterceptorParameters args, T model); + + /// + /// Called after an entity has been deleted but before the session has saved its changes + /// + Task Deleted(InterceptorParameters args, T model); + + /// + /// Called before an entity is deleted + /// + Task> Deleting(InterceptorParameters args, T model); + + /// + /// Called after an entity has been updated but before the session has saved its changes + /// + Task Updated(InterceptorParameters args, T model); + + /// + /// Called before an entity is stored and validated + /// + Task> Updating(InterceptorParameters args, T model); +} + + +public interface IInterceptor +{ + /// + /// Interceptors with a higher gravity will run before any with lower gravity + /// + int Gravity { get; } + + /// + /// Readable name for this interceptor + /// + string Name { get; } + + /// + /// Hash which is used for this interceptor to be able to pass results from Start to Complete methods + /// + string Hash { get; } + + /// + /// Whether any of the interceptor methods is allowed to run based on the parameters + /// + bool CanHandle(InterceptorParameters args, Type modelType); + + /// + /// Called after an entity has been stored but before the session has saved its changes + /// + Task Created(InterceptorParameters args, ZeroIdEntity model); + + /// + /// Called before an entity is stored and validated + /// + Task> Creating(InterceptorParameters args, ZeroIdEntity model); + + /// + /// Called after an entity has been deleted but before the session has saved its changes + /// + Task Deleted(InterceptorParameters args, ZeroIdEntity model); + + /// + /// Called before an entity is deleted + /// + Task> Deleting(InterceptorParameters args, ZeroIdEntity model); + + /// + /// Called after an entity has been updated but before the session has saved its changes + /// + Task Updated(InterceptorParameters args, ZeroIdEntity model); + + /// + /// Called before an entity is stored and validated + /// + Task> Updating(InterceptorParameters args, ZeroIdEntity model); +} \ No newline at end of file diff --git a/zero.Core/Communication/Interceptors/InterceptorInstruction.cs b/zero.Core/Communication/Interceptors/InterceptorInstruction.cs index 96b94b85..ed3feac2 100644 --- a/zero.Core/Communication/Interceptors/InterceptorInstruction.cs +++ b/zero.Core/Communication/Interceptors/InterceptorInstruction.cs @@ -1,74 +1,138 @@ -namespace zero.Communication; +using Microsoft.Extensions.Logging; + +namespace zero.Communication; public class InterceptorInstruction where T : ZeroIdEntity, new() { - public Guid Guid { get; private set; } = Guid.NewGuid(); + public Guid Guid { get; private set; } - public InterceptorType InterceptorType { get; private set; } + public InterceptorRunType Runtype { get; private set; } public T Model { get; private set; } - public IEntityCollection Collection { get;private set; } - - public InterceptorParameters Parameters { get; private set; } + public Type ModelType { get; private set; } public EntityResult Result { get; private set; } + protected IZeroContext Context { get; private set; } - internal InterceptorInstruction(IZeroContext context, IEntityCollection collection, InterceptorType type, T model) + protected IEnumerable Interceptors { get; private set; } + + protected ILogger Logger { get; private set; } + + protected Dictionary CachedParameters { get; private set; } = new(); + + protected Func InterceptorFilter { get; private set; } = x => true; + + + internal InterceptorInstruction(IZeroContext context, IEnumerable registrations, ILogger logger, InterceptorRunType runtype, T model) { - Collection = collection; - InterceptorType = type; + Context = context; + Guid = Guid.NewGuid(); + Logger = logger; + Runtype = runtype; Model = model; - Parameters = new InterceptorParameters() - { - Context = context, - Store = context.Store, - Collection = collection, - Properties = new() - }; + ModelType = model.GetType(); + Interceptors = registrations.OrderByDescending(x => x.Gravity); } - public async Task Run() + /// + /// Custom interceptor filter for this instruction (the CanHandle() method for each interceptor is still activated) + /// + public void Filter(Func predicate) { - ICollectionInterceptor interceptor = default; + InterceptorFilter = predicate; + } - //await HandleBefore(interceptor); - //await interceptor.Created(Parameters, Model); - //interceptor.Created(new InterceptorParameters() - //{ - - //}) - await Task.Delay(0); + /// + /// Run all interceptors (in order) which can handle the given type. + /// Depending on the action any of the following methods on the interceptor is called: Creating(), Updating(), Deleting(). + /// If any of the interceptors returns a result the operation is cancelled and this result is returned. + /// + public async Task Start() + { + foreach (IInterceptor interceptor in GetInterceptors()) + { + InterceptorParameters parameters = new() + { + Context = Context, + Store = Context.Store, + Properties = new() + }; + + if (!interceptor.CanHandle(parameters, ModelType)) + { + continue; + } + + Logger.LogDebug("Run interceptor {interceptor} for {type}:{operation}", interceptor.Name, ModelType, Runtype); + + InterceptorResult result = (await HandleBefore(interceptor, parameters)) ?? new(); + result.InterceptorHash = IdGenerator.Create(32); + + CachedParameters.Add(interceptor.Hash, parameters); + + // we cancel all further interceptors if a result is available and return this instead + if (result.Result != null) + { + Result = EntityResult.From(result.Result, result.Result.Model as T); + return false; + } + + // the Continue task will cancel all further interceptors + if (!result.Continue) + { + break; + } + } + return true; } + /// + /// Run all interceptors (in order) which can handle the given type. + /// Depending on the action any of the following methods on the interceptor is called: Created(), Updated(), Deleted(). + /// The parameters which are returned from the Start() operation are passed to the methods. + /// public async Task Complete() { - await Task.Delay(0); + foreach (IInterceptor interceptor in GetInterceptors()) + { + await HandleAfter(interceptor, CachedParameters.GetValueOrDefault(interceptor.Hash)); + } } - protected Task HandleBefore(ICollectionInterceptor interceptor) => InterceptorType switch + protected IEnumerable GetInterceptors() { - InterceptorType.Save => interceptor.Saving(Parameters, Model), - InterceptorType.Create => interceptor.Creating(Parameters, Model), - InterceptorType.Update => interceptor.Updating(Parameters, Model), - InterceptorType.Delete => interceptor.Deleting(Parameters, Model), + return Interceptors.Where(InterceptorFilter).OrderByDescending(x => x.Gravity); + } + + + /// + /// Proxy for handling methods on an interceptor + /// + protected Task> HandleBefore(IInterceptor interceptor, InterceptorParameters parameters) => Runtype switch + { + InterceptorRunType.Create => interceptor.Creating(parameters, Model), + InterceptorRunType.Update => interceptor.Updating(parameters, Model), + InterceptorRunType.Delete => interceptor.Deleting(parameters, Model), _ => throw new NotImplementedException() }; - protected Task HandleAfter(ICollectionInterceptor interceptor) => InterceptorType switch + /// + /// Proxy for handling methods on an interceptor + /// + protected Task HandleAfter(IInterceptor interceptor, InterceptorParameters parameters) => Runtype switch { - InterceptorType.Save => interceptor.Saved(Parameters, Model), - InterceptorType.Create => interceptor.Created(Parameters, Model), - InterceptorType.Update => interceptor.Updated(Parameters, Model), - InterceptorType.Delete => interceptor.Deleted(Parameters, Model), + InterceptorRunType.Create => interceptor.Created(parameters, Model), + InterceptorRunType.Update => interceptor.Updated(parameters, Model), + InterceptorRunType.Delete => interceptor.Deleted(parameters, Model), _ => throw new NotImplementedException() }; } \ No newline at end of file diff --git a/zero.Core/Communication/Interceptors/InterceptorInstructionProxy.cs b/zero.Core/Communication/Interceptors/InterceptorInstructionProxy.cs deleted file mode 100644 index dac5357e..00000000 --- a/zero.Core/Communication/Interceptors/InterceptorInstructionProxy.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace zero.Communication; - -public class InterceptorRunnerProxy -{ - public InterceptorInstructionProxy CreateInstruction(InterceptorType type, T model) where T : ZeroIdEntity - { - return null; - //return new InterceptorInstructionProxy(Context, collection, type, model); - } -} - - -public class InterceptorInstructionProxy -{ - public async Task Run() - { - await Task.Delay(0); - return new EntityResult(); - } - - - public async Task Complete() - { - await Task.Delay(0); - } -} \ No newline at end of file diff --git a/zero.Core/Communication/Interceptors/InterceptorParameters.cs b/zero.Core/Communication/Interceptors/InterceptorParameters.cs index 5cd1e1fe..0497a7a1 100644 --- a/zero.Core/Communication/Interceptors/InterceptorParameters.cs +++ b/zero.Core/Communication/Interceptors/InterceptorParameters.cs @@ -1,13 +1,5 @@ namespace zero.Communication; -public class InterceptorParameters : InterceptorParameters where T : ZeroIdEntity, new() -{ - /// - /// Access to the collection which has initiated the interceptor instruction - /// - public IEntityCollection Collection { get; set; } -} - public class InterceptorParameters { /// @@ -20,11 +12,6 @@ public class InterceptorParameters /// public IZeroStore Store { get; set; } - /// - /// TODO - /// - public InterceptorRunnerProxy Interceptors { get; set; } - /// /// Parameters from the interceptor which ran on before the operation (only available for completed operations) /// diff --git a/zero.Core/Communication/Interceptors/InterceptorResult.cs b/zero.Core/Communication/Interceptors/InterceptorResult.cs index af6a1ff3..99a2b7b2 100644 --- a/zero.Core/Communication/Interceptors/InterceptorResult.cs +++ b/zero.Core/Communication/Interceptors/InterceptorResult.cs @@ -10,15 +10,10 @@ public class InterceptorResult /// /// Prevent further interceptors from running for this operation (only valid for the current interception type, e.g. Update/Created/Purge/...) /// - public bool Prevent { get; set; } + public bool Continue { get; set; } = true; /// /// Set a result. This will prevent further execution of the operation and cancels all subsequent interceptors /// public EntityResult Result { get; set; } - - /// - /// Additional parameters which can be passed to the interceptor after the operation was completed - /// - public Dictionary Parameters { get; set; } = new(); } \ No newline at end of file diff --git a/zero.Core/Communication/Interceptors/InterceptorRunType.cs b/zero.Core/Communication/Interceptors/InterceptorRunType.cs new file mode 100644 index 00000000..82be301e --- /dev/null +++ b/zero.Core/Communication/Interceptors/InterceptorRunType.cs @@ -0,0 +1,8 @@ +namespace zero.Communication; + +public enum InterceptorRunType +{ + Create = 1, + Update = 2, + Delete = 3 +} \ No newline at end of file diff --git a/zero.Core/Communication/Interceptors/InterceptorRunner.cs b/zero.Core/Communication/Interceptors/InterceptorRunner.cs deleted file mode 100644 index 0a0f5c77..00000000 --- a/zero.Core/Communication/Interceptors/InterceptorRunner.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System.Collections.Concurrent; - -namespace zero.Communication; - -public class InterceptorRunner : IInterceptorRunner where T : ZeroIdEntity, new() -{ - protected IZeroContext Context { get; set; } - - protected ConcurrentDictionary Interceptors { get; private set; } = new(); - - - public InterceptorRunner(IZeroContext context) - { - Context = context; - } - - - public InterceptorInstruction CreateInstruction(EntityCollection collection, InterceptorType type, T model) - { - return new InterceptorInstruction(Context, collection, type, model); - } - - - /// - /// Get all interceptors for a certain type - /// - IOrderedEnumerable ForType(Type targetType) - { - return Context.Options.Interceptors.GetAllItems().Where(x => x.CanHandle(targetType)).OrderByDescending(x => x.Gravity); - // return Interceptors.Where(item => item.Types.Count == 0 || item.Types.Any(type => targetType.IsAssignableFrom(type))); - } - - - /// - /// Resolves an interceptor from the service provider - /// - bool TryResolve(InterceptorRegistration registration, out ICollectionInterceptor interceptor) - { - Type type = registration.InterceptorType; - - if (Interceptors.TryGetValue(type, out object interceptorObj)) - { - interceptor = interceptorObj as ICollectionInterceptor; - return interceptor != null; - } - - object service = Context.Services.GetService(type); - interceptor = service as ICollectionInterceptor; - - if (interceptor == null && service != null && service is ICollectionInterceptor) - { - interceptor = new CollectionInterceptorShim(service as ICollectionInterceptor); - } - - Interceptors.TryAdd(type, interceptor); - - return interceptor != null; - } -} - - -public interface IInterceptorRunner where T : ZeroIdEntity, new() -{ - InterceptorInstruction CreateInstruction(EntityCollection collection, InterceptorType type, T model); -} \ No newline at end of file diff --git a/zero.Core/Communication/Interceptors/InterceptorType.cs b/zero.Core/Communication/Interceptors/InterceptorType.cs deleted file mode 100644 index f1260abb..00000000 --- a/zero.Core/Communication/Interceptors/InterceptorType.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace zero.Communication; - -public enum InterceptorType -{ - Save = 0, - Update = 1, - Create = 2, - Delete = 3 -} \ No newline at end of file diff --git a/zero.Core/Communication/Interceptors/Interceptors.cs b/zero.Core/Communication/Interceptors/Interceptors.cs new file mode 100644 index 00000000..2b0ee49e --- /dev/null +++ b/zero.Core/Communication/Interceptors/Interceptors.cs @@ -0,0 +1,49 @@ +using Microsoft.Extensions.Logging; + +namespace zero.Communication; + +public class Interceptors : IInterceptors +{ + protected IZeroContext Context { get; set; } + + protected IEnumerable Registrations { get; set; } + + protected ILogger Logger { get; set; } + + + public Interceptors(IZeroContext context, IEnumerable registrations, ILogger logger) + { + Context = context; + Registrations = registrations; + Logger = logger; + } + + + /// + public InterceptorInstruction ForCreate(T model) where T : ZeroIdEntity, new() => new(Context, Registrations, Logger, InterceptorRunType.Create, model); + + /// + public InterceptorInstruction ForUpdate(T model) where T : ZeroIdEntity, new() => new(Context, Registrations, Logger, InterceptorRunType.Update, model); + + /// + public InterceptorInstruction ForDelete(T model) where T : ZeroIdEntity, new() => new(Context, Registrations, Logger, InterceptorRunType.Delete, model); +} + + +public interface IInterceptors +{ + /// + /// Instruction which can run interceptors before and after a creating an entity + /// + InterceptorInstruction ForCreate(T model) where T : ZeroIdEntity, new(); + + /// + /// Instruction which can run interceptors before and after updating an entity + /// + InterceptorInstruction ForUpdate(T model) where T : ZeroIdEntity, new(); + + /// + /// Instruction which can run interceptors before and after deleting an entity + /// + InterceptorInstruction ForDelete(T model) where T : ZeroIdEntity, new(); +} \ No newline at end of file diff --git a/zero.Core/Communication/Module.cs b/zero.Core/Communication/Module.cs index 1f767c84..fde71425 100644 --- a/zero.Core/Communication/Module.cs +++ b/zero.Core/Communication/Module.cs @@ -8,5 +8,6 @@ internal class CommunicationModule : ZeroModule public override void Register(IZeroModuleConfiguration config) { config.Services.AddSingleton(); + config.Services.AddTransient(); } } \ No newline at end of file diff --git a/zero.Core/Configuration/ConfigurationParts/FeatureOptions.cs b/zero.Core/Configuration/ConfigurationParts/FeatureOptions.cs index ac17585d..bb01a67a 100644 --- a/zero.Core/Configuration/ConfigurationParts/FeatureOptions.cs +++ b/zero.Core/Configuration/ConfigurationParts/FeatureOptions.cs @@ -1,6 +1,4 @@ -using zero.Core.Entities; - -namespace zero.Configuration; +namespace zero.Configuration; public class FeatureOptions : OptionsEnumerable, IOptionsEnumerable { diff --git a/zero.Core/Configuration/ConfigurationParts/IconOptions.cs b/zero.Core/Configuration/ConfigurationParts/IconOptions.cs index 1b8d5eaf..632b3daf 100644 --- a/zero.Core/Configuration/ConfigurationParts/IconOptions.cs +++ b/zero.Core/Configuration/ConfigurationParts/IconOptions.cs @@ -1,6 +1,4 @@ -using zero.Core.Entities; - -namespace zero.Configuration; +namespace zero.Configuration; public class IconOptions : OptionsEnumerable, IOptionsEnumerable { diff --git a/zero.Core/Configuration/ConfigurationParts/IntegrationOptions.cs b/zero.Core/Configuration/ConfigurationParts/IntegrationOptions.cs index 9ee6199e..c244b621 100644 --- a/zero.Core/Configuration/ConfigurationParts/IntegrationOptions.cs +++ b/zero.Core/Configuration/ConfigurationParts/IntegrationOptions.cs @@ -1,7 +1,4 @@ using FluentValidation; -using System; -using System.Collections.Generic; -using zero.Core.Integrations; namespace zero.Configuration; diff --git a/zero.Core/Configuration/ConfigurationParts/InterceptorOptions.cs b/zero.Core/Configuration/ConfigurationParts/InterceptorOptions.cs deleted file mode 100644 index 43fbcae0..00000000 --- a/zero.Core/Configuration/ConfigurationParts/InterceptorOptions.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using zero.Core.Collections; - -namespace zero.Configuration; - -public class InterceptorOptions : OptionsEnumerable, IOptionsEnumerable -{ - public void Add(int gravity = 0, Func 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(int gravity = 0, Func canHandle = null) - where T : ICollectionInterceptor - where TBoxed : ZeroEntity - { - Type type = typeof(T); - Type boxedType = typeof(TBoxed); - Func 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 CanHandle { get; set; } - - public bool IsInterceptorBoxed { get; set; } -} \ No newline at end of file diff --git a/zero.Core/Configuration/ConfigurationParts/PermissionOptions.cs b/zero.Core/Configuration/ConfigurationParts/PermissionOptions.cs index 1ac2caeb..d90fe9f1 100644 --- a/zero.Core/Configuration/ConfigurationParts/PermissionOptions.cs +++ b/zero.Core/Configuration/ConfigurationParts/PermissionOptions.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using zero.Core.Identity; - -namespace zero.Configuration; +namespace zero.Configuration; public class PermissionOptions : OptionsEnumerable, IOptionsEnumerable { diff --git a/zero.Core/Configuration/Module.cs b/zero.Core/Configuration/Module.cs index fa77ab49..423fd347 100644 --- a/zero.Core/Configuration/Module.cs +++ b/zero.Core/Configuration/Module.cs @@ -8,10 +8,7 @@ internal class ConfigurationModule : ZeroModule /// public override void Register(IZeroModuleConfiguration config) { - config.Services.AddOptions() - .Bind(config.Configuration.GetSection("Zero")) - .Configure(opts => opts.ZeroVersion = "0.0.1.0" /*// TODO*/); - + config.Services.AddOptions().Bind(config.Configuration.GetSection("Zero")).Configure(opts => { }); config.Services.AddTransient(factory => factory.GetService>().CurrentValue); } } \ No newline at end of file diff --git a/zero.Core/Configuration/ZeroOptions.cs b/zero.Core/Configuration/ZeroOptions.cs index 8574c5a7..58f15ed4 100644 --- a/zero.Core/Configuration/ZeroOptions.cs +++ b/zero.Core/Configuration/ZeroOptions.cs @@ -1,237 +1,164 @@ -namespace zero.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using System.Collections.Concurrent; + +namespace zero.Configuration; public class ZeroOptions : IZeroOptions { - public ZeroOptions() + protected IServiceProvider ServiceProvider { get; set; } + + protected ConcurrentDictionary OptionsCache { get; private set; } = new(); + + + public ZeroOptions(IServiceProvider serviceProvider) { - 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(); - Interceptors = new(); - Applications = new(); + Version = "0.0.1-alpha.1"; + ServiceProvider = serviceProvider; - //Raven.Indexes.Add(); - //Raven.Indexes.Add(); - //Raven.Indexes.Add(); - //Raven.Indexes.Add(); - //Raven.Indexes.Add(); - //Raven.Indexes.Add(); - //Raven.Indexes.Add(); - //Raven.Indexes.Add(); - //Raven.Indexes.Add(); - //Raven.Indexes.Add(); - //Raven.Indexes.Add(); - - //Raven.Indexes.Add(); - //Raven.Indexes.Add(); - //Raven.Indexes.Add(); - //Raven.Indexes.Add(); - //Raven.Indexes.Add(); - //Raven.Indexes.Add(); - - Search = new(); + //SupportedLanguages = new string[2] { "en-US", "de-DE" }; + //DefaultLanguage = SupportedLanguages[0]; + //TokenExpiration = 60 * 3; + //BackofficePath = "/zero"; + //ExcludedPaths = new() { }; + //Raven = new() + //{ + // CollectionPrefix = String.Empty + //}; } - /// - public bool SetupCompleted => !String.IsNullOrEmpty(Raven?.Database); /// - public string ZeroVersion { get; set; } + public TOptions For() where TOptions : class + { + Type type = typeof(TOptions); + + if (!OptionsCache.TryGetValue(type, out object value)) + { + IOptions options = ServiceProvider.GetService>(); + value = options.Value; + OptionsCache.TryAdd(type, value); + } + + return value as TOptions; + } + + /// - public string DefaultLanguage { get; set; } + public bool Initialized => !String.IsNullOrEmpty(For()?.Database); /// - public string[] SupportedLanguages { get; private set; } + public string Version { get; private set; } - /// - public int TokenExpiration { get; set; } + ///// + //public string DefaultLanguage { get; set; } - /// - public RavenOptions Raven { get; set; } + ///// + //public string[] SupportedLanguages { get; private set; } - /// - public ApplicationOptions Applications { get; set; } + ///// + //public int TokenExpiration { get; set; } - /// - public string BackofficePath { get; set; } + ///// + //public RavenOptions Raven { get; set; } - /// - /// Paths in the backoffice which are not handled by zero - /// - public List ExcludedPaths { get; private set; } + ///// + //public ApplicationOptions Applications { get; set; } - /// - //public IZeroPluginConfiguration Backoffice { get; set; } + ///// + //public string BackofficePath { get; set; } - /// - public SectionOptions Sections { get; private set; } + ///// + ///// Paths in the backoffice which are not handled by zero + ///// + //public List ExcludedPaths { get; private set; } - /// - public FeatureOptions Features { get; private set; } + ///// + //public SectionOptions Sections { get; private set; } - /// - public PageOptions Pages { get; private set; } + ///// + //public FeatureOptions Features { get; private set; } - /// - public ModuleOptions Modules { get; private set; } + ///// + //public PageOptions Pages { get; private set; } - /// - public PermissionOptions Permissions { get; private set; } + ///// + //public ModuleOptions Modules { get; private set; } - /// - public SettingsOptions Settings { get; private set; } + ///// + //public PermissionOptions Permissions { get; private set; } - /// - public SpaceOptions Spaces { get; private set; } + ///// + //public SettingsOptions Settings { get; private set; } - /// - public IntegrationOptions Integrations { get; private set; } + ///// + //public SpaceOptions Spaces { get; private set; } - /// - public IconOptions Icons { get; private set; } + ///// + //public IntegrationOptions Integrations { get; private set; } - /// - public ServiceOptions Services { get; private set; } + ///// + //public IconOptions Icons { get; private set; } - /// - public BlueprintOptions Blueprints { get; private set; } + ///// + //public ServiceOptions Services { get; private set; } - /// - public InterceptorOptions Interceptors { get; private set; } - - /// - public SearchOptions Search { get; private set; } + ///// + //public BlueprintOptions Blueprints { get; private set; } } public interface IZeroOptions { - /// - /// Whether the zero setup has already been completed and the instance is ready for use - /// - bool SetupCompleted { get; } - /// /// The currently active version /// This should not be set manually, as it is used for setup and migrations and incremented automatically /// - string ZeroVersion { get; set; } + string Version { get; } /// - /// Default language ISO code + /// Whether this zero instance is initialized (setup is completed) /// - string DefaultLanguage { get; set; } + bool Initialized { get; } /// - /// Language ISO codes which are supported by the zero backoffice + /// Get typed options (proxy to IOptions) /// - string[] SupportedLanguages { get; } + TOptions For() where TOptions : class; - /// - /// Expiration in minutes of a generated change token for an entity - /// - int TokenExpiration { get; set; } + ///// + ///// Default language ISO code + ///// + //string DefaultLanguage { get; set; } - /// - /// RavenDB configuration data - /// - RavenOptions Raven { get; set; } + ///// + ///// Language ISO codes which are supported by the zero backoffice + ///// + //string[] SupportedLanguages { get; } - /// - /// Application options - /// - ApplicationOptions Applications { get; set; } + ///// + ///// Expiration in minutes of a generated change token for an entity + ///// + //int TokenExpiration { get; set; } - /// - /// URL path to the backoffice (defaults to /zero) - /// - string BackofficePath { get; set; } + ///// + ///// RavenDB configuration data + ///// + //RavenOptions Raven { get; set; } - /// - /// Paths in the backoffice which are not handled by zero - /// - List ExcludedPaths { get; } + ///// + ///// Application options + ///// + //ApplicationOptions Applications { get; set; } - /// - /// - /// - SectionOptions Sections { get; } + ///// + ///// URL path to the backoffice (defaults to /zero) + ///// + //string BackofficePath { get; set; } - /// - /// - /// - FeatureOptions Features { get; } - - /// - /// - /// - PageOptions Pages { get; } - - /// - /// - /// - ModuleOptions Modules { get; } - - /// - /// - /// - PermissionOptions Permissions { get; } - - /// - /// - /// - SettingsOptions Settings { get; } - - /// - /// - /// - SpaceOptions Spaces { get; } - - /// - /// - /// - IntegrationOptions Integrations { get; } - - /// - /// - /// - IconOptions Icons { get; } - - /// - /// - /// - ServiceOptions Services { get; } - - /// - /// - /// - BlueprintOptions Blueprints { get; } - - /// - /// - /// - InterceptorOptions Interceptors { get; } - - /// - /// - /// - SearchOptions Search { get; } + ///// + ///// Paths in the backoffice which are not handled by zero + ///// + //List ExcludedPaths { get; } } diff --git a/zero.Core/Context/Module.cs b/zero.Core/Context/Module.cs new file mode 100644 index 00000000..0d0bf81c --- /dev/null +++ b/zero.Core/Context/Module.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace zero.Applications; + +internal class ContextModule : ZeroModule +{ + /// + public override void Register(IZeroModuleConfiguration config) + { + config.Services.AddScoped(); + config.Services.AddHttpContextAccessor(); + } +} \ No newline at end of file diff --git a/old/zero.Web/Middlewares/ZeroContextMiddleware.cs b/zero.Core/Context/ZeroContextMiddleware.cs similarity index 82% rename from old/zero.Web/Middlewares/ZeroContextMiddleware.cs rename to zero.Core/Context/ZeroContextMiddleware.cs index 3d2f15a9..3fd1938b 100644 --- a/old/zero.Web/Middlewares/ZeroContextMiddleware.cs +++ b/zero.Core/Context/ZeroContextMiddleware.cs @@ -1,8 +1,6 @@ using Microsoft.AspNetCore.Http; -using System.Threading.Tasks; -using zero.Core; -namespace zero.Web.Middlewares +namespace zero.Context { public class ZeroContextMiddleware { diff --git a/zero.Core/Localization/Module.cs b/zero.Core/Localization/Module.cs index 67f4a7d8..ecf61e1c 100644 --- a/zero.Core/Localization/Module.cs +++ b/zero.Core/Localization/Module.cs @@ -7,6 +7,7 @@ internal class LocalizationModule : ZeroModule /// public override void Register(IZeroModuleConfiguration config) { + config.Services.AddScoped(); config.Services.AddScoped(); } } \ No newline at end of file diff --git a/zero.Core/Persistence/EntityResult.cs b/zero.Core/Persistence/EntityResult.cs new file mode 100644 index 00000000..a70f2e45 --- /dev/null +++ b/zero.Core/Persistence/EntityResult.cs @@ -0,0 +1,127 @@ +using FluentValidation.Results; +using System.Runtime.Serialization; + +namespace zero.Persistence; + +[DataContract(Name = "result", Namespace = "")] +public class EntityResult +{ + [DataMember(Name = "model")] + public T Model { get; set; } + + [DataMember(Name = "success")] + public bool IsSuccess { get; set; } + + [DataMember(Name = "errors")] + public IList Errors { get; set; } = new List(); + + public EntityResult() { } + + public EntityResult(ValidationResult validation) + { + IsSuccess = validation.IsValid; + Errors = validation.Errors.Select(x => new EntityResultError() + { + Property = x.PropertyName, + Message = x.ErrorMessage + }).ToList(); + } + + public static EntityResult From(EntityResult with, T model = default) + { + EntityResult result = new EntityResult(); + + result.IsSuccess = with.IsSuccess; + result.Errors = with.Errors; + result.Model = model; + + return result; + } + + public void AddError(string property, string message) + { + IsSuccess = false; + Errors.Add(new EntityResultError() + { + Property = property, + Message = message + }); + } + + + public void AddError(string message) + { + IsSuccess = false; + Errors.Add(new EntityResultError() + { + Property = Constants.ErrorFieldNone, + Message = message + }); + } + + + public EntityResult Combine(EntityResult with) + { + if (IsSuccess && !with.IsSuccess) + { + IsSuccess = false; + } + foreach (EntityResultError error in with.Errors) + { + Errors.Add(error); + } + return this; + } + + + public static EntityResult Success() => new EntityResult() { IsSuccess = true }; + + public static EntityResult Success(T model) => new EntityResult() { IsSuccess = true, Model = model }; + + public static EntityResult Fail() => new EntityResult() { }; + + public static EntityResult Fail(string property, string message) + { + EntityResult result = new EntityResult(); + result.AddError(property, message); + return result; + } + + public static EntityResult Fail(string message) + { + EntityResult result = new EntityResult(); + result.AddError(message); + return result; + } + + public static EntityResult Fail(ValidationResult validation) => new EntityResult(validation); + + public static EntityResult Fail(EntityResultError error) => Fail(error.Property, error.Message); + + public static EntityResult Fail(IEnumerable errors) => new EntityResult() { IsSuccess = !errors.Any(), Errors = errors.ToList() }; +} + + +public class EntityResult : EntityResult +{ + public EntityResult() : base() { } + + public EntityResult(ValidationResult validation) : base(validation) { } + + public static EntityResult Maybe(bool isSuccess) => isSuccess ? Success() : Fail(); + + public new static EntityResult Success() => new EntityResult() { IsSuccess = true }; + + public new static EntityResult Fail() => new EntityResult() { }; +} + + +[DataContract(Name = "resultError", Namespace = "")] +public class EntityResultError +{ + [DataMember(Name = "property")] + public string Property { get; set; } + + [DataMember(Name = "message")] + public string Message { get; set; } +} diff --git a/zero.Core/Persistence/Module.cs b/zero.Core/Persistence/Module.cs index 584a4820..493d02ae 100644 --- a/zero.Core/Persistence/Module.cs +++ b/zero.Core/Persistence/Module.cs @@ -33,7 +33,7 @@ internal class PersistenceModule : ZeroModule IZeroDocumentStore store = new ZeroDocumentStore(options) { - Urls = new string[1] { options.Raven.Url }, + Urls = new string[1] { options.For().Url }, Conventions = // TODO activate and test this { AggressiveCache = @@ -49,7 +49,7 @@ internal class PersistenceModule : ZeroModule IDocumentStore raven = store.Initialize(); // create all indexes - if (options.SetupCompleted) + if (options.Initialized) { //var indexes = options.Raven.Indexes.BuildAll(options, store); //IndexCreation.CreateIndexes(indexes, store, database: options.Raven.Database); diff --git a/zero.Core/Persistence/Revision.cs b/zero.Core/Persistence/Revision.cs new file mode 100644 index 00000000..d8aa45ce --- /dev/null +++ b/zero.Core/Persistence/Revision.cs @@ -0,0 +1,26 @@ +namespace zero.Persistence; + +public class Revision : Revision where T : ZeroEntity +{ + public T Model { get; set; } +} + +public class Revision +{ + public string ModelId { get; set; } + + public RevisionUser User { get; set; } + + public DateTimeOffset Date { get; set; } + + public string ChangeVector { get; set; } +} + +public class RevisionUser +{ + public string Id { get; set; } + + public string Name { get; set; } + + public string AvatarId { get; set; } +} \ No newline at end of file diff --git a/zero.Core/Persistence/Tokens/ChangeToken.cs b/zero.Core/Persistence/Tokens/ChangeToken.cs new file mode 100644 index 00000000..392504a3 --- /dev/null +++ b/zero.Core/Persistence/Tokens/ChangeToken.cs @@ -0,0 +1,12 @@ +namespace zero.Persistence; + +/// +/// A change token holds a reference to a database entity +/// This is used to verify change requests for entities in the zero backoffice +/// +public class ChangeToken : IZeroDbConventions +{ + public string Id { get; set; } + + public string ReferenceId { get; set; } +} \ No newline at end of file diff --git a/zero.Core/Persistence/ZeroDocumentStore.cs b/zero.Core/Persistence/ZeroDocumentStore.cs index 57ad2edd..9304c2f4 100644 --- a/zero.Core/Persistence/ZeroDocumentStore.cs +++ b/zero.Core/Persistence/ZeroDocumentStore.cs @@ -11,11 +11,11 @@ public class ZeroDocumentStore : DocumentStore, IZeroDocumentStore { public ZeroDocumentStore(IZeroOptions options) : base() { - Options = options; + Options = options.For(); Database = null; } - protected IZeroOptions Options { get; set; } + protected RavenOptions Options { get; set; } /// public string ResolvedDatabase { get; set; } @@ -45,7 +45,7 @@ public class ZeroDocumentStore : DocumentStore, IZeroDocumentStore EnsureNotClosed(); var sessionId = Guid.NewGuid(); - var session = new ZeroDocumentSession(this, sessionId, options, Options.Raven.Database); + var session = new ZeroDocumentSession(this, sessionId, options, Options.Database); RegisterEvents(session); AfterSessionCreated(session); session.OnSessionDisposing += (sender, args) => diff --git a/zero.Core/Registrations.cs b/zero.Core/Registrations.cs new file mode 100644 index 00000000..3d64c28b --- /dev/null +++ b/zero.Core/Registrations.cs @@ -0,0 +1,26 @@ +namespace zero; + +internal class Registrations +{ + public static List Modules { get; } = new() + { + new ArchitectureModule(), + new ApplicationsModule(), + new ContextModule(), + new CommunicationModule(), + new ConfigurationModule(), + new IdentityModule(), + new LocalizationModule(), + new MailsModule(), + new PagesModule(), + new PersistenceModule(), + new RenderingModule(), + new RoutingModule() + }; + + + public static List Plugins { get; } = new() + { + + }; +} \ No newline at end of file diff --git a/zero.Core/Routing/Module.cs b/zero.Core/Routing/Module.cs index 83e62103..c6edb493 100644 --- a/zero.Core/Routing/Module.cs +++ b/zero.Core/Routing/Module.cs @@ -20,16 +20,18 @@ internal class RoutingModule : ZeroModule config.Services.AddScoped(); config.Services.AddScoped(); config.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); - config.Services.AddScoped(); + config.Services.AddScoped(); } /// public override void Configure(IZeroOptions options) { - options.Raven.Indexes.Add(); - options.Raven.Indexes.Add(); - options.Raven.Indexes.Add(); - options.Interceptors.Add(gravity: 100); + RavenOptions raven = options.For(); + InterceptorOptions interceptors = options.For(); + + raven.Indexes.Add(); + raven.Indexes.Add(); + raven.Indexes.Add(); } } \ No newline at end of file diff --git a/zero.Core/Routing/ZeroEntityRouteInterceptor.cs b/zero.Core/Routing/ZeroEntityRouteInterceptor.cs index 33d9b851..56630f91 100644 --- a/zero.Core/Routing/ZeroEntityRouteInterceptor.cs +++ b/zero.Core/Routing/ZeroEntityRouteInterceptor.cs @@ -4,7 +4,7 @@ using Raven.Client.Documents.Linq; namespace zero.Routing; -public class ZeroEntityRouteInterceptor : CollectionInterceptor +public class ZeroEntityRouteInterceptor : Interceptor { protected IZeroContext Context { get; set; } @@ -21,6 +21,7 @@ public class ZeroEntityRouteInterceptor : CollectionInterceptor public ZeroEntityRouteInterceptor(IZeroContext context, IZeroStore store, IRoutes routes, ILogger logger, IEnumerable providers, IRedirectAutomation redirectAutomation) { + Gravity = 100; Context = context; Store = store; Routes = routes; @@ -31,7 +32,39 @@ public class ZeroEntityRouteInterceptor : CollectionInterceptor /// - public override async Task Saved(InterceptorParameters args, ZeroEntity model) + public override Task Created(InterceptorParameters args, ZeroEntity model) => Saved(args, model, false); + + + /// + public override Task Updated(InterceptorParameters args, ZeroEntity model) => Saved(args, model, true); + + + /// + /// Remove all dependent routes and redirects on route deletion + /// + public override async Task Deleted(InterceptorParameters args, ZeroEntity model) + { + RoutingContext context = GetContext(); + + string id = model.Id; + List dependencies = await GetDependencies(context, model); + + await context.Store.Raven.PurgeAsync(context.Context.Application.Database, $"where c.Dependencies IN ($id)", new Raven.Client.Parameters() + { + { "id", id } + }); + + // delete associated redirects for obsolete routes + await RedirectAutomation.DeleteForRoutes(dependencies.ToArray()); + + Logger.LogInformation("Route deletes completed (-{removed}) for {model} (id: {id})", dependencies.Count, model.Name, model.Id); + } + + + /// + /// Create or update a route when an entity changes and rebuild all dependent routes + /// + protected async Task Saved(InterceptorParameters args, ZeroEntity model, bool update = false) { // DONE [1] assume we have an update for a Product: // this will not trigger a new seeding as the ProductRouteProvider handles entities, but not products itself @@ -137,26 +170,6 @@ public class ZeroEntityRouteInterceptor : CollectionInterceptor } - /// - public override async Task Deleted(InterceptorParameters args, ZeroEntity model) - { - RoutingContext context = GetContext(); - - string id = model.Id; - List dependencies = await GetDependencies(context, model); - - await context.Store.Raven.PurgeAsync(context.Context.Application.Database, $"where c.Dependencies IN ($id)", new Raven.Client.Parameters() - { - { "id", id } - }); - - // delete associated redirects for obsolete routes - await RedirectAutomation.DeleteForRoutes(dependencies.ToArray()); - - Logger.LogInformation("Route deletes completed (-{removed}) for {model} (id: {id})", dependencies.Count, model.Name, model.Id); - } - - /// /// Get route dependencies for an entity /// diff --git a/zero.Core/Usings.cs b/zero.Core/Usings.cs index f2061baa..512a1975 100644 --- a/zero.Core/Usings.cs +++ b/zero.Core/Usings.cs @@ -6,18 +6,19 @@ global using System.Linq; global using System.Threading; global using System.Threading.Tasks; +global using zero.Applications; global using zero.Architecture; -global using zero.Configuration; -global using zero.Extensions; -global using zero.Identity; -global using zero.Pages; -global using zero.Validation; -global using zero.Utils; -global using zero.Persistence; -global using zero.Localization; -global using zero.Routing; -global using zero.Context; global using zero.Communication; +global using zero.Configuration; +global using zero.Context; +global using zero.Extensions; +global using zero.FileStorage; +global using zero.Identity; +global using zero.Localization; +global using zero.Mails; +global using zero.Pages; +global using zero.Persistence; global using zero.Rendering; -global using zero.Media; -global using zero.Applications; \ No newline at end of file +global using zero.Routing; +global using zero.Utils; +global using zero.Validation; diff --git a/zero.Core/Validation/ValidatorExtensions.cs b/zero.Core/Validation/ValidatorExtensions.cs index b548cf8a..2be1dcc4 100644 --- a/zero.Core/Validation/ValidatorExtensions.cs +++ b/zero.Core/Validation/ValidatorExtensions.cs @@ -1,8 +1,6 @@ using FluentValidation; using Raven.Client.Documents; -using System; using System.Globalization; -using System.Linq; namespace zero.Validation; diff --git a/zero.Core/Validation/ZeroValidator.cs b/zero.Core/Validation/ZeroValidator.cs index 2aba007a..837f54f0 100644 --- a/zero.Core/Validation/ZeroValidator.cs +++ b/zero.Core/Validation/ZeroValidator.cs @@ -1,8 +1,5 @@ using FluentValidation; using FluentValidation.Results; -using System; -using System.Threading; -using System.Threading.Tasks; namespace zero.Validation; diff --git a/zero.Core/ZeroApplicationBuilder.cs b/zero.Core/ZeroApplicationBuilder.cs new file mode 100644 index 00000000..7069bcf6 --- /dev/null +++ b/zero.Core/ZeroApplicationBuilder.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Builder; + +namespace zero; + +public class ZeroApplicationBuilder : IZeroApplicationBuilder +{ + protected IApplicationBuilder App { get; private set; } + + protected IZeroOptions Options { get; private set; } + + + internal ZeroApplicationBuilder(IApplicationBuilder app) + { + App = app; + App.UseStaticFiles(); + App.UseMiddleware(); + App.UseRouting(); + App.UseAuthentication(); + App.UseAuthorization(); + } + + + public void WithEndpoints(Action configure) + { + App.UseEndpoints(e => + { + ZeroEndpointRouteBuilder builder = new(e); + configure(builder); + }); + } + + + public IZeroApplicationBuilder WithMiddleware(Action configure) + { + configure(App); + return this; + } +} + + +public interface IZeroApplicationBuilder +{ + void WithEndpoints(Action endpoints); + + IZeroApplicationBuilder WithMiddleware(Action configure); +} diff --git a/zero.Core/ZeroApplicationBuilderExtensions.cs b/zero.Core/ZeroApplicationBuilderExtensions.cs new file mode 100644 index 00000000..06cafaf0 --- /dev/null +++ b/zero.Core/ZeroApplicationBuilderExtensions.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Builder; + +namespace zero; + +public static class ZeroApplicationBuilderExtensions +{ + public static IZeroApplicationBuilder UseZero(this IApplicationBuilder app) => new ZeroApplicationBuilder(app); +} diff --git a/zero.Core/ZeroBuilder.cs b/zero.Core/ZeroBuilder.cs new file mode 100644 index 00000000..b800ff5b --- /dev/null +++ b/zero.Core/ZeroBuilder.cs @@ -0,0 +1,109 @@ +using FluentValidation; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using System.IO; + +namespace zero; + +// TODO maybe use a middleware like Hangfire does: https://github.com/HangfireIO/Hangfire/blob/master/src/Hangfire.AspNetCore/HangfireEndpointRouteBuilderExtensions.cs + +public class ZeroBuilder +{ + public virtual IServiceCollection Services { get; } + + public virtual IMvcBuilder Mvc { get; } + + IConfiguration Configuration; + + IZeroStartupOptions StartupOptions; + + + public ZeroBuilder(IServiceCollection services, IConfiguration configuration, Action setupAction) + { + ZeroModuleInitializer.RegisterAll(new ZeroModuleConfiguration(services, configuration)); + + Services = services; + Mvc = services.AddMvc(); + Configuration = configuration; + + // create startup options + StartupOptions = new ZeroStartupOptions(Mvc); + StartupOptions.AssemblyDiscoveryRules.Add(new ZeroAssemblyDiscoveryRule()); + setupAction?.Invoke(StartupOptions); + + + // adds and discovers additional and built-in assemblies + new AssemblyDiscovery(Mvc).Execute(StartupOptions.AssemblyDiscoveryRules); + + + // add default plugin + AddPlugin(); + + + Mvc.AddNewtonsoftJson(x => + { + // TODO this shall only be configurated for backoffice controllers + BackofficeJsonSerlializerSettings.Setup(x.SerializerSettings); + }); + + if (Environment.GetEnvironmentVariable("DOTNET_WATCH") == "1") + { + Mvc.AddRazorRuntimeCompilation(); + } + + // configure FluentValidation + ValidatorOptions.Global.PropertyNameResolver = ValidatorCamelCasePropertyResolver.ResolvePropertyName; + + + // add default services + Services.AddScoped(); + Services.AddScoped(); + Services.AddScoped(); + + //Services.AddScoped(); + + Services.AddTransient(); + Services.AddScoped(factory => new Paths(factory.GetService(), true)); + + // add dev server + Services.AddOptions() + .Bind(Configuration.GetSection("Zero:DevServer")) + .Configure((opts, env) => + { + opts.WorkingDirectory = Path.Combine(env.ContentRootPath, "..", "Zero.Web.UI", "App"); + }); + + Services.AddHostedService(); + } + + + /// + /// Use specified options + /// + public ZeroBuilder WithOptions(Action configureOptions) + { + Services.Configure(configureOptions); + return this; + } + + + /// + /// Adds a zero plugin + /// + public ZeroBuilder AddPlugin() where T : class, IZeroPlugin, new() + { + ZeroPluginInitializer.AddPlugin(Services, Configuration); + return this; + } + + + /// + /// Adds a zero plugin + /// + public ZeroBuilder AddPlugin(Func implementationFactory) where T : class, IZeroPlugin, new() + { + ZeroPluginInitializer.AddPlugin(Services, Configuration, implementationFactory); + return this; + } +} diff --git a/zero.Core/ZeroServiceCollectionExtensions.cs b/zero.Core/ZeroServiceCollectionExtensions.cs new file mode 100644 index 00000000..e7347ec7 --- /dev/null +++ b/zero.Core/ZeroServiceCollectionExtensions.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace zero; + +public static class ZeroServiceCollectionExtensions +{ + public static ZeroBuilder AddZero(this IServiceCollection services, IConfiguration configuration) + { + return new ZeroBuilder(services, configuration, null); + } + + public static ZeroBuilder AddZero(this IServiceCollection services, IConfiguration configuration, Action setupAction) + { + return new ZeroBuilder(services, configuration, setupAction); + } +} \ No newline at end of file diff --git a/zero.Core/zero.Core.csproj b/zero.Core/zero.Core.csproj index a520d623..dac059fc 100644 --- a/zero.Core/zero.Core.csproj +++ b/zero.Core/zero.Core.csproj @@ -6,6 +6,7 @@ preview net6.0 true + zero diff --git a/zero.sln b/zero.sln index 530d6d5f..e16cafe4 100644 --- a/zero.sln +++ b/zero.sln @@ -9,7 +9,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zero.Commerce", "plugins\ze EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zero.Web", "old\zero.Web\zero.Web.csproj", "{06156006-7684-41EC-8BD4-3B2A762147CD}" EndProject -Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "zero.Web.UI", "zero.Backoffice.UI\", "{8CE1A69A-35DB-4748-AE1C-86F5BC2E11D4}" +Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "zero.Backoffice.UI", "zero.Backoffice.UI\", "{8CE1A69A-35DB-4748-AE1C-86F5BC2E11D4}" ProjectSection(WebsiteProperties) = preProject TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.5" Debug.AspNetCompiler.VirtualPath = "/localhost_56763"