diff --git a/zero.Api/ApiParameterTransformer.cs b/zero.Api/ApiParameterTransformer.cs new file mode 100644 index 00000000..16fd80e9 --- /dev/null +++ b/zero.Api/ApiParameterTransformer.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Routing; + +namespace zero.Api; + +public class ApiParameterTransformer : IOutboundParameterTransformer +{ + public string TransformOutbound(object value) + { + return value == null ? null : Safenames.Alias(value); + } +} \ No newline at end of file diff --git a/zero.Api/Filters/ActionFilters/ApiMetadataFilter.cs b/zero.Api/Filters/ActionFilters/ApiMetadataFilter.cs new file mode 100644 index 00000000..ad3981ac --- /dev/null +++ b/zero.Api/Filters/ActionFilters/ApiMetadataFilter.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc.Filters; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace zero.Api.Filters; + +public class ApiMetadataFilterAttribute : ActionFilterAttribute +{ + public override void OnActionExecuting(ActionExecutingContext context) + { + context.HttpContext.Items["zero.action.started"] = DateTimeOffset.Now; + } +} \ No newline at end of file diff --git a/zero.Api/Filters/ResultFilters/ApiResponseFilter.cs b/zero.Api/Filters/ResultFilters/ApiResponseFilter.cs new file mode 100644 index 00000000..0133df40 --- /dev/null +++ b/zero.Api/Filters/ResultFilters/ApiResponseFilter.cs @@ -0,0 +1,56 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace zero.Api.Filters; + +public class ApiResponseFilterAttribute : ResultFilterAttribute +{ + public override void OnResultExecuting(ResultExecutingContext context) + { + DateTimeOffset started = (DateTimeOffset)context.HttpContext.Items["zero.action.started"]; + TimeSpan duration = DateTimeOffset.Now - started; + + if (context.Result is ObjectResult result && result.StatusCode.HasValue) + { + if (typeof(ZeroIdEntity).IsAssignableFrom(result.DeclaredType)) + { + result.Value = new ModelApiResponse() + { + Model = result.Value, + Success = true, + Status = result.StatusCode.Value, + Metadata = new ModelApiResponseMetadata() + { + RequestDate = started, + Duration = duration, + Token = IdGenerator.Create(32) + } + }; + } + else if (typeof(Paged).IsAssignableFrom(result.DeclaredType)) + { + Paged paged = result.Value as Paged; + result.Value = new PagedApiResponse() + { + Items = paged.GetItems(), + Paging = new() + { + Page = paged.Page, + PageSize = paged.PageSize, + TotalItems = paged.TotalItems, + TotalPages = paged.TotalPages, + HasMore = paged.HasMore + }, + Success = true, + Status = result.StatusCode.Value, + Metadata = new ModelApiResponseMetadata() + { + RequestDate = started, + Duration = duration, + Token = IdGenerator.Create(32) + } + }; + } + } + } +} \ No newline at end of file diff --git a/zero.Api/Models/ApiResponse.cs b/zero.Api/Models/ApiResponse.cs index e0017c39..fb8a23c3 100644 --- a/zero.Api/Models/ApiResponse.cs +++ b/zero.Api/Models/ApiResponse.cs @@ -2,7 +2,12 @@ { public class ApiResponse { - [JsonPropertyName("@metadata")] + [JsonPropertyOrder(-2)] + public bool Success { get; set; } + + [JsonPropertyOrder(-1)] + public int Status { get; set; } + public ApiResponseMetadata Metadata { get; set; } = new(); } } diff --git a/zero.Api/Models/ApiResponseMetadata.cs b/zero.Api/Models/ApiResponseMetadata.cs index 25c6cabf..492f79a8 100644 --- a/zero.Api/Models/ApiResponseMetadata.cs +++ b/zero.Api/Models/ApiResponseMetadata.cs @@ -3,4 +3,6 @@ public class ApiResponseMetadata { public DateTimeOffset RequestDate { get; set; } + + public TimeSpan Duration { get; set; } } \ No newline at end of file diff --git a/zero.Api/Models/ErrorApiResponse.cs b/zero.Api/Models/ErrorApiResponse.cs new file mode 100644 index 00000000..f6558850 --- /dev/null +++ b/zero.Api/Models/ErrorApiResponse.cs @@ -0,0 +1,15 @@ +namespace zero.Api.Models; + +public class ErrorApiResponse : ApiResponse +{ + public ErrorApiResponseError Error { get; set; } +} + +public class ErrorApiResponseError +{ + public string Code { get; set; } + + public string Message { get; set; } + + public string ApiPath { get; set; } +} \ No newline at end of file diff --git a/zero.Api/Models/ModelApiResponse.cs b/zero.Api/Models/ModelApiResponse.cs new file mode 100644 index 00000000..a24ff714 --- /dev/null +++ b/zero.Api/Models/ModelApiResponse.cs @@ -0,0 +1,6 @@ +namespace zero.Api.Models; + +public class ModelApiResponse : ApiResponse +{ + public object Model { get; set; } +} diff --git a/zero.Api/Models/ModelApiResponseMetadata.cs b/zero.Api/Models/ModelApiResponseMetadata.cs new file mode 100644 index 00000000..d575dc4e --- /dev/null +++ b/zero.Api/Models/ModelApiResponseMetadata.cs @@ -0,0 +1,44 @@ +namespace zero.Api.Models; + +public class ModelApiResponseMetadata : ApiResponseMetadata +{ + /// + /// Wehther this entity is application aware + /// + public bool IsAppAware { get; set; } + + /// + /// Whether this entity can be shared across applications (only for IsAppAware=true) + /// + public bool CanBeShared { get; set; } + + public bool IsShared { get; set; } + + /// + /// The change token maps to a database entity which holds ID and collection of the model to edit + /// If these values do not match the entity on save it is rejected + /// // TODO expiration expiry session.Advanced.GetMetadataFor(user)[Raven.Client.Constants.Documents.Metadata.Expires] = DateTime.UtcNow.AddMinutes(60); + /// + public string Token { get; set; } + + + /// + /// Whether an entity of this type can be created + /// + public bool CanCreate { get; set; } + + /// + /// Whether an entity of this type can be created in the shared app space + /// + public bool CanCreateShared { get; set; } + + /// + /// Whether this entity can be edited or only viewed + /// + public bool CanEdit { get; set; } + + /// + /// Whether this entity can be deleted + /// + public bool CanDelete { get; set; } +} diff --git a/zero.Api/Models/PagedApiResponse.cs b/zero.Api/Models/PagedApiResponse.cs new file mode 100644 index 00000000..41d1294b --- /dev/null +++ b/zero.Api/Models/PagedApiResponse.cs @@ -0,0 +1,21 @@ +namespace zero.Api.Models; + +public class PagedApiResponse : ApiResponse +{ + public object Items { get; set; } + public PagedApiResponsePaging Paging { get; set; } +} + + +public class PagedApiResponsePaging +{ + public long Page { get; set; } + + public long PageSize { get; set; } + + public long TotalPages { get; set; } + + public long TotalItems { get; set; } + + public bool HasMore { get; set; } +} \ No newline at end of file diff --git a/zero.Api/Models/_new/BasicModel.cs b/zero.Api/Models/_new/BasicModel.cs index cc9e6c6e..72d943f8 100644 --- a/zero.Api/Models/_new/BasicModel.cs +++ b/zero.Api/Models/_new/BasicModel.cs @@ -26,4 +26,10 @@ public abstract class BasicModel : ZeroIdEntity where T : ZeroEntity /// Date of creation /// public DateTimeOffset CreatedDate { get; set; } + + /// + /// API link for this entity + /// + [JsonPropertyName("@link")] + public string Link { get; set; } } \ No newline at end of file diff --git a/zero.Api/Models/_new/DisplayModel.cs b/zero.Api/Models/_new/DisplayModel.cs index 971b9e3f..f362cf16 100644 --- a/zero.Api/Models/_new/DisplayModel.cs +++ b/zero.Api/Models/_new/DisplayModel.cs @@ -1,20 +1,7 @@ namespace zero.Api.Models; -public abstract class DisplayModel : ZeroIdEntity -{ - /// - /// Meta data - /// - public DisplayModelConfiguration Configuration { get; set; } = new(); - - /// - /// Permissions for this entity - /// - public DisplayModelPermissions Permissions { get; set; } = new(); -} - -public abstract class DisplayModel : DisplayModel where T : ZeroEntity +public abstract class DisplayModel : ZeroIdEntity where T : ZeroEntity { /// /// Full name of the entity @@ -70,51 +57,4 @@ public abstract class DisplayModel : DisplayModel where T : ZeroEntity /// Language of the entity /// public string LanguageId { get; set; } -} - - -public class DisplayModelConfiguration -{ - /// - /// Wehther this entity is application aware - /// - public bool IsAppAware { get; set; } - - /// - /// Whether this entity can be shared across applications (only for IsAppAware=true) - /// - public bool CanBeShared { get; set; } - - public bool IsShared { get; set; } - - /// - /// The change token maps to a database entity which holds ID and collection of the model to edit - /// If these values do not match the entity on save it is rejected - /// // TODO expiration expiry session.Advanced.GetMetadataFor(user)[Raven.Client.Constants.Documents.Metadata.Expires] = DateTime.UtcNow.AddMinutes(60); - /// - public string Token { get; set; } -} - - -public class DisplayModelPermissions -{ - /// - /// Whether an entity of this type can be created - /// - public bool CanCreate { get; set; } - - /// - /// Whether an entity of this type can be created in the shared app space - /// - public bool CanCreateShared { get; set; } - - /// - /// Whether this entity can be edited or only viewed - /// - public bool CanEdit { get; set; } - - /// - /// Whether this entity can be deleted - /// - public bool CanDelete { get; set; } } \ No newline at end of file diff --git a/zero.Api/Modules/Countries/CountriesController.cs b/zero.Api/Modules/Countries/CountriesController.cs index cb4ee4c0..1ac378e3 100644 --- a/zero.Api/Modules/Countries/CountriesController.cs +++ b/zero.Api/Modules/Countries/CountriesController.cs @@ -7,19 +7,15 @@ public class CountriesController : ZeroApiController { protected ICountryStore Store { get; set; } - protected IPickerProvider Picker { get; set; } - - - public CountriesController(ICountryStore store, IPickerProvider picker) + public CountriesController(ICountryStore store) { Store = store; - Picker = picker; } [HttpGet("empty")] [ZeroAuthorize(CountryPermissions.Create)] - public virtual async Task> Empty() + public virtual async Task> Empty() { throw new NotImplementedException("TEST ZERO"); @@ -30,32 +26,13 @@ public class CountriesController : ZeroApiController return NotFound(); } - return Mapper.Map(model); - } - - - [HttpGet("pick")] - [ZeroAuthorize(CountryPermissions.Read)] - public virtual async Task> Pick([FromQuery] ListQuery query) - { - query.OrderQuery = q => q.OrderByDescending(x => x.IsPreferred).ThenBy(x => x.Name); - Paged result = await Store.Load(query.Page, query.PageSize, q => q.Filter(query)); - return Mapper.Map(result); - } - - - [HttpGet("pickpreview")] - [ZeroAuthorize(CountryPermissions.Read)] - public virtual async Task> Pick([FromQuery] string[] ids) - { - Dictionary model = await Store.Load(ids); - return Mapper.Map(model); + return Mapper.Map(model); } [HttpGet("{id}")] [ZeroAuthorize(CountryPermissions.Read)] - public virtual async Task> Get(string id, string changeVector = null) + public virtual async Task> Get(string id, string changeVector = null) { Country model = await Store.Load(id, changeVector); @@ -64,7 +41,7 @@ public class CountriesController : ZeroApiController return NotFound(); } - return Mapper.Map(model); + return Mapper.Map(model); } @@ -74,7 +51,11 @@ public class CountriesController : ZeroApiController { query.OrderQuery = q => q.OrderByDescending(x => x.IsPreferred).ThenBy(x => x.Name); Paged result = await Store.Load(query.Page, query.PageSize, q => q.Filter(query)); - return Mapper.Map(result); + + return Mapper.Map(result, (src, dest) => + { + dest.Link = Url.Action(nameof(CountriesController.Get), new { id = dest.Id }); + }); } @@ -87,7 +68,7 @@ public class CountriesController : ZeroApiController if (result.IsSuccess) { - Result mappedResult = Mapper.Map(result); + Result mappedResult = Mapper.Map(result); return CreatedAtAction(nameof(CountriesController.Get), new { id = model.Id }, mappedResult); } @@ -96,7 +77,7 @@ public class CountriesController : ZeroApiController return result.WithoutModel(); } - return Mapper.Map(result); + return Mapper.Map(result); } @@ -126,7 +107,7 @@ public class CountriesController : ZeroApiController } // TODO add Preference-Applied header, see https://github.com/microsoft/api-guidelines/blob/vNext/Guidelines.md#76-standard-response-headers - return Mapper.Map(result); + return Mapper.Map(result); } diff --git a/zero.Api/Modules/Countries/CountryPickerProvider.cs b/zero.Api/Modules/Countries/CountryPickerProvider.cs deleted file mode 100644 index 962ad412..00000000 --- a/zero.Api/Modules/Countries/CountryPickerProvider.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace zero.Api.Modules.Countries; - -public class CountryPickerProvider : PickerProvider -{ - public CountryPickerProvider(IStoreOperations ops) : base(ops) { } - - - /// - protected override PickerPreviewModel ConvertToPreview(Country source) - { - return new() - { - Id = source.Id, - Name = source.Name, - Icon = "flag-" + source.Code.ToLowerInvariant() - }; - } -} \ No newline at end of file diff --git a/zero.Api/Modules/Countries/Maps/CountryDisplay.cs b/zero.Api/Modules/Countries/Maps/CountryEdit.cs similarity index 69% rename from zero.Api/Modules/Countries/Maps/CountryDisplay.cs rename to zero.Api/Modules/Countries/Maps/CountryEdit.cs index 449419ea..d815de5d 100644 --- a/zero.Api/Modules/Countries/Maps/CountryDisplay.cs +++ b/zero.Api/Modules/Countries/Maps/CountryEdit.cs @@ -1,6 +1,6 @@ namespace zero.Api.Modules.Countries; -public class CountryDisplay : DisplayModel +public class CountryEdit : DisplayModel { public bool IsPreferred { get; set; } diff --git a/zero.Api/Modules/Countries/Maps/CountryMapperProfile.cs b/zero.Api/Modules/Countries/Maps/CountryMapperProfile.cs index c6c94902..b4db0acb 100644 --- a/zero.Api/Modules/Countries/Maps/CountryMapperProfile.cs +++ b/zero.Api/Modules/Countries/Maps/CountryMapperProfile.cs @@ -5,7 +5,7 @@ public class CountryMapperProfile : ZeroMapperProfile public override void Configure(IZeroMapper mapper) { mapper.Define((source, ctx) => new(), Map); - mapper.Define((source, ctx) => new(), Map); + mapper.Define((source, ctx) => new(), Map); mapper.Define((source, ctx) => new(), Map); mapper.Define((source, ctx) => new(), Map); mapper.Define((source, ctx) => new(), Map); @@ -19,7 +19,7 @@ public class CountryMapperProfile : ZeroMapperProfile target.IsPreferred = source.IsPreferred; } - void Map(Country source, CountryDisplay target, IZeroMapperContext ctx) + void Map(Country source, CountryEdit target, IZeroMapperContext ctx) { this.MapDisplayData(source, target); target.Code = source.Code; diff --git a/zero.Api/Modules/Countries/ServiceCollectionExtensions.cs b/zero.Api/Modules/Countries/ServiceCollectionExtensions.cs index 450ebe34..a5127e70 100644 --- a/zero.Api/Modules/Countries/ServiceCollectionExtensions.cs +++ b/zero.Api/Modules/Countries/ServiceCollectionExtensions.cs @@ -6,7 +6,6 @@ public static class ServiceCollectionExtensions { public static IServiceCollection AddZeroBackofficeCountries(this IServiceCollection services) { - services.AddScoped, CountryPickerProvider>(); services.AddSingleton(); services.AddSingleton(); diff --git a/zero.Api/Modules/Error/ErrorController.cs b/zero.Api/Modules/Error/ErrorController.cs new file mode 100644 index 00000000..1fa27d8f --- /dev/null +++ b/zero.Api/Modules/Error/ErrorController.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Mvc; + +namespace zero.Api.Modules.Error; + +public class ErrorController : ZeroApiController +{ + [HttpGet("")] + public virtual ActionResult Index() + { + IExceptionHandlerFeature exception = HttpContext.Features.Get(); + + return new ErrorApiResponse() + { + Success = false, + Status = HttpContext.Response.StatusCode, + Error = new() + { + ApiPath = exception.Path, + Message = exception.Error.Message + } + }; + } +} \ No newline at end of file diff --git a/zero.Api/Modules/Error/ServiceCollectionExtensions.cs b/zero.Api/Modules/Error/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..065d4cec --- /dev/null +++ b/zero.Api/Modules/Error/ServiceCollectionExtensions.cs @@ -0,0 +1,11 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace zero.Api.Modules.Error; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddZeroBackofficeErrorHandler(this IServiceCollection services) + { + return services; + } +} \ No newline at end of file diff --git a/zero.Api/Plugin.cs b/zero.Api/Plugin.cs index fea541a5..1704916b 100644 --- a/zero.Api/Plugin.cs +++ b/zero.Api/Plugin.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -33,6 +34,11 @@ public class ZeroApiPlugin : ZeroPlugin services.AddZeroBackofficeModules(configuration); + services.AddControllers(opts => + { + opts.Conventions.Add(new RouteTokenTransformerConvention(new ApiParameterTransformer())); + }); + //services.AddTransient(); //services.AddTransient(); diff --git a/zero.Api/ZeroApiController.cs b/zero.Api/ZeroApiController.cs index 2c73fa7c..c3a0e44f 100644 --- a/zero.Api/ZeroApiController.cs +++ b/zero.Api/ZeroApiController.cs @@ -1,11 +1,14 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; +using zero.Api.Filters; namespace zero.Api.Controllers; [ApiController] [ZeroAuthorize] [DisableBrowserCache] +[ApiMetadataFilter] +[ApiResponseFilter] //[ServiceFilter(typeof(ModelStateValidationFilterAttribute))] //[ServiceFilter(typeof(BackofficeFilterAttribute))] public abstract class ZeroApiController : ControllerBase diff --git a/zero.Api/ZeroBackofficeMvcOptions.cs b/zero.Api/ZeroBackofficeMvcOptions.cs index 6f76eaf5..e645f6ac 100644 --- a/zero.Api/ZeroBackofficeMvcOptions.cs +++ b/zero.Api/ZeroBackofficeMvcOptions.cs @@ -1,5 +1,7 @@ using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.Extensions.Options; +using zero.Api.Filters; namespace zero.Api; @@ -15,5 +17,6 @@ internal class ZeroBackofficeMvcOptions : IConfigureOptions public void Configure(MvcOptions options) { options.Conventions.Add(new ZeroBackofficeControllerModelConvention(Options.ZeroPath)); + //options.Conventions.Add(new RouteTokenTransformerConvention(new ApiParameterTransformer())); } } \ No newline at end of file diff --git a/zero.Core/Applications/ApplicationModule.cs b/zero.Core/Applications/ApplicationModule.cs new file mode 100644 index 00000000..744673d0 --- /dev/null +++ b/zero.Core/Applications/ApplicationModule.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace zero.Applications; + +public class ApplicationModule : ZeroModule +{ + public override void ConfigureServices(IServiceCollection services, IConfiguration configuration) + { + services.AddScoped(); + services.AddScoped(); + services.AddOptions().Bind(configuration.GetSection("Zero:Applications")); + } +} \ No newline at end of file diff --git a/zero.Core/Applications/ServiceCollectionExtensions.cs b/zero.Core/Applications/ServiceCollectionExtensions.cs deleted file mode 100644 index 98c26e8e..00000000 --- a/zero.Core/Applications/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace zero.Applications; - -internal static class ServiceCollectionExtensions -{ - public static IServiceCollection AddZeroApplications(this IServiceCollection services, IConfiguration config) - { - services.AddScoped(); - services.AddScoped(); - - services.AddOptions().Bind(config.GetSection("Zero:Applications")); - - return services; - } -} \ No newline at end of file diff --git a/zero.Core/Architecture/ServiceCollectionExtensions.cs b/zero.Core/Architecture/ArchitectureModule.cs similarity index 54% rename from zero.Core/Architecture/ServiceCollectionExtensions.cs rename to zero.Core/Architecture/ArchitectureModule.cs index 189d9a6e..a7aefb34 100644 --- a/zero.Core/Architecture/ServiceCollectionExtensions.cs +++ b/zero.Core/Architecture/ArchitectureModule.cs @@ -3,16 +3,13 @@ using Microsoft.Extensions.DependencyInjection; namespace zero.Architecture; -internal static class ServiceCollectionExtensions +public class ArchitectureModule : ZeroModule { - public static IServiceCollection AddZeroBlueprints(this IServiceCollection services, IConfiguration config) + public override void ConfigureServices(IServiceCollection services, IConfiguration configuration) { services.AddScoped(); services.AddScoped(); services.AddScoped(); - - services.AddOptions().Bind(config.GetSection("Zero:Blueprints")); - - return services; + services.AddOptions().Bind(configuration.GetSection("Zero:Blueprints")); } } \ No newline at end of file diff --git a/zero.Core/Architecture/ZeroModule.cs b/zero.Core/Architecture/ZeroModule.cs new file mode 100644 index 00000000..959cdea8 --- /dev/null +++ b/zero.Core/Architecture/ZeroModule.cs @@ -0,0 +1,50 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace zero.Architecture; + +public abstract class ZeroModule : IZeroModule +{ + /// + public virtual int Order { get; } = 0; + + /// + public virtual int ConfigureOrder => Order; + + /// + public virtual void ConfigureServices(IServiceCollection services, IConfiguration configuration) { } + + /// + public virtual void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) { } +} + + +public interface IZeroModule +{ + /// + /// Get the value to use to order startups to configure services. The default is 0. + /// + int Order { get; } + + /// + /// Get the value to use to order startups to build the pipeline. The default is the 'Order' property. + /// + int ConfigureOrder { get; } + + /// + /// This method gets called by the runtime. Use this method to add services to the container. + /// For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940 + /// + /// The collection of service descriptors. + void ConfigureServices(IServiceCollection services, IConfiguration configuration); + + /// + /// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + /// + /// + /// + /// + void Configure(IApplicationBuilder builder, IEndpointRouteBuilder routes, IServiceProvider serviceProvider); +} \ No newline at end of file diff --git a/zero.Core/Architecture/ZeroModuleCollection.cs b/zero.Core/Architecture/ZeroModuleCollection.cs new file mode 100644 index 00000000..50997efe --- /dev/null +++ b/zero.Core/Architecture/ZeroModuleCollection.cs @@ -0,0 +1,50 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using System.Collections.Concurrent; + +namespace zero.Architecture; + +internal class ZeroModuleCollection +{ + static ConcurrentDictionary _modules = new(); + + + /// + /// Get all registered modules + /// + public static IEnumerable GetAll() => _modules.Values; + + + /// + /// Adds a zero module + /// + public static void AddModule(IServiceCollection services, IConfiguration configuration) where T : class, IZeroModule, new() + { + AddModule(services, configuration, new T()); + } + + + /// + /// Adds a zero module + /// + public static void AddModule(IServiceCollection services, IConfiguration configuration, T moduleInstance) where T : IZeroModule + { + AddModule(typeof(T), services, configuration, moduleInstance); + } + + + /// + /// Adds a zero module + /// + public static void AddModule(Type moduleType, IServiceCollection services, IConfiguration configuration, IZeroModule moduleInstance) + { + if (_modules.ContainsKey(moduleType)) + { + return; + } + + _modules.TryAdd(moduleType, moduleInstance); + + moduleInstance.ConfigureServices(services, configuration); + } +} \ No newline at end of file diff --git a/zero.Core/Communication/ServiceCollectionExtensions.cs b/zero.Core/Communication/CommunicationModule.cs similarity index 66% rename from zero.Core/Communication/ServiceCollectionExtensions.cs rename to zero.Core/Communication/CommunicationModule.cs index 7f08b1a4..439f0059 100644 --- a/zero.Core/Communication/ServiceCollectionExtensions.cs +++ b/zero.Core/Communication/CommunicationModule.cs @@ -3,13 +3,12 @@ using Microsoft.Extensions.DependencyInjection; namespace zero.Communication; -internal static class ServiceCollectionExtensions +public class CommunicationModule : ZeroModule { - public static IServiceCollection AddZeroCommunication(this IServiceCollection services) + public override void ConfigureServices(IServiceCollection services, IConfiguration configuration) { services.AddScoped(); services.AddSingleton(); services.AddTransient(); - return services; } } \ No newline at end of file diff --git a/zero.Core/Configuration/ServiceCollectionExtensions.cs b/zero.Core/Configuration/ConfigurationModule.cs similarity index 57% rename from zero.Core/Configuration/ServiceCollectionExtensions.cs rename to zero.Core/Configuration/ConfigurationModule.cs index bc1f6341..fde5dd90 100644 --- a/zero.Core/Configuration/ServiceCollectionExtensions.cs +++ b/zero.Core/Configuration/ConfigurationModule.cs @@ -4,21 +4,20 @@ using Microsoft.Extensions.Options; namespace zero.Configuration; -internal static class ServiceCollectionExtensions +public class ConfigurationModule : ZeroModule { - public static IServiceCollection AddZeroConfiguration(this IServiceCollection services, IConfiguration config) + public override void ConfigureServices(IServiceCollection services, IConfiguration configuration) { services.AddOptions().Configure((opts, svc) => { opts.ServiceProvider = svc; opts.Version = "0.0.1-alpha.1"; opts.ZeroPath = "/zero"; - opts.TokenExpiration = TimeSpan.FromHours(3); - }).Bind(config.GetSection("Zero")); + opts.TokenExpiration = TimeSpan.FromHours(3); + }).Bind(configuration.GetSection("Zero")); + services.AddTransient(factory => factory.GetService>().Value); - services.AddOptions().Bind(config.GetSection("Zero:Features")); - - return services; + services.AddOptions().Bind(configuration.GetSection("Zero:Features")); } } \ No newline at end of file diff --git a/zero.Core/Context/ContextModule.cs b/zero.Core/Context/ContextModule.cs new file mode 100644 index 00000000..b9445b73 --- /dev/null +++ b/zero.Core/Context/ContextModule.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace zero.Context; + +public class ContextModule : ZeroModule +{ + public override void ConfigureServices(IServiceCollection services, IConfiguration configuration) + { + services.AddScoped(); + services.AddHttpContextAccessor(); + } +} \ No newline at end of file diff --git a/zero.Core/Context/ServiceCollectionExtensions.cs b/zero.Core/Context/ServiceCollectionExtensions.cs deleted file mode 100644 index ec97450b..00000000 --- a/zero.Core/Context/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; - -namespace zero.Context; - -internal static class ServiceCollectionExtensions -{ - public static IServiceCollection AddZeroContext(this IServiceCollection services) - { - services.AddScoped(); - services.AddHttpContextAccessor(); - return services; - } -} \ No newline at end of file diff --git a/zero.Core/FileStorage/ServiceCollectionExtensions.cs b/zero.Core/FileStorage/FileStorageModule.cs similarity index 52% rename from zero.Core/FileStorage/ServiceCollectionExtensions.cs rename to zero.Core/FileStorage/FileStorageModule.cs index 49bdfe0c..4ebca033 100644 --- a/zero.Core/FileStorage/ServiceCollectionExtensions.cs +++ b/zero.Core/FileStorage/FileStorageModule.cs @@ -4,17 +4,15 @@ using Microsoft.Extensions.DependencyInjection; namespace zero.FileStorage; -internal static class ServiceCollectionExtensions +public class FileStorageModule : ZeroModule { - public static IServiceCollection AddZeroFileStorage(this IServiceCollection services, IConfiguration config) + public override void ConfigureServices(IServiceCollection services, IConfiguration configuration) { services.AddScoped(factory => new Paths(factory.GetService())); - services.AddOptions().Bind(config.GetSection("Zero:FileSystem")).Configure(opts => + services.AddOptions().Bind(configuration.GetSection("Zero:FileSystem")).Configure(opts => { opts.ZeroAssetsPath = "zero"; }); - - return services; } } \ No newline at end of file diff --git a/zero.Core/Identity/ServiceCollectionExtensions.cs b/zero.Core/Identity/IdentityModule.cs similarity index 85% rename from zero.Core/Identity/ServiceCollectionExtensions.cs rename to zero.Core/Identity/IdentityModule.cs index f0019b4a..e9bb21cb 100644 --- a/zero.Core/Identity/ServiceCollectionExtensions.cs +++ b/zero.Core/Identity/IdentityModule.cs @@ -1,14 +1,15 @@ using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; -namespace zero.Identity; +namespace zero.Applications; -internal static class ServiceCollectionExtensions +public class IdentityModule : ZeroModule { - public static IServiceCollection AddZeroIdentity(this IServiceCollection services) + public override void ConfigureServices(IServiceCollection services, IConfiguration configuration) { services.TryAddEnumerable(ServiceDescriptor.Singleton, PostConfigureCookieAuthenticationOptions>()); @@ -26,6 +27,5 @@ internal static class ServiceCollectionExtensions .AddZeroBackofficeCookie(); services.AddAuthorization(); - return services; } } \ No newline at end of file diff --git a/zero.Core/Localization/ServiceCollectionExtensions.cs b/zero.Core/Localization/LocalizationModule.cs similarity index 61% rename from zero.Core/Localization/ServiceCollectionExtensions.cs rename to zero.Core/Localization/LocalizationModule.cs index 2eada28f..75a61557 100644 --- a/zero.Core/Localization/ServiceCollectionExtensions.cs +++ b/zero.Core/Localization/LocalizationModule.cs @@ -1,10 +1,11 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; namespace zero.Localization; -internal static class ServiceCollectionExtensions +public class LocalizationModule : ZeroModule { - public static IServiceCollection AddZeroLocalization(this IServiceCollection services) + public override void ConfigureServices(IServiceCollection services, IConfiguration configuration) { services.AddScoped(); services.AddScoped(); @@ -12,6 +13,5 @@ internal static class ServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); - return services; } } \ No newline at end of file diff --git a/zero.Core/Mails/MailsModule.cs b/zero.Core/Mails/MailsModule.cs new file mode 100644 index 00000000..eefad1ea --- /dev/null +++ b/zero.Core/Mails/MailsModule.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace zero.Mails; + +public class MailsModule : ZeroModule +{ + public override void ConfigureServices(IServiceCollection services, IConfiguration configuration) + { + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + } +} \ No newline at end of file diff --git a/zero.Core/Mails/ServiceCollectionExtensions.cs b/zero.Core/Mails/ServiceCollectionExtensions.cs deleted file mode 100644 index 3d89191a..00000000 --- a/zero.Core/Mails/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; - -namespace zero.Mails; - -internal static class ServiceCollectionExtensions -{ - public static IServiceCollection AddZeroMails(this IServiceCollection services) - { - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - return services; - } -} \ No newline at end of file diff --git a/zero.Core/Mapper/MapperExtensions.cs b/zero.Core/Mapper/MapperExtensions.cs index 66239b95..38df094e 100644 --- a/zero.Core/Mapper/MapperExtensions.cs +++ b/zero.Core/Mapper/MapperExtensions.cs @@ -24,9 +24,14 @@ public static class MapperExtensions return mapper.Map(source, typeof(TSource), default); } - public static Paged Map(this IZeroMapper mapper, Paged source) + public static Paged Map(this IZeroMapper mapper, Paged source, Action modify = null) { - return source.MapTo(srcItem => mapper.Map(srcItem)); + return source.MapTo(srcItem => + { + TDestination destination = mapper.Map(srcItem); + modify?.Invoke(srcItem, destination); + return destination; + }); } public static Result Map(this IZeroMapper mapper, Result source) diff --git a/zero.Core/Mapper/MapperModule.cs b/zero.Core/Mapper/MapperModule.cs new file mode 100644 index 00000000..14a25d74 --- /dev/null +++ b/zero.Core/Mapper/MapperModule.cs @@ -0,0 +1,12 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace zero.Mapper; + +public class MapperModule : ZeroModule +{ + public override void ConfigureServices(IServiceCollection services, IConfiguration configuration) + { + services.AddScoped(); + } +} \ No newline at end of file diff --git a/zero.Core/Mapper/ServiceCollectionExtensions.cs b/zero.Core/Mapper/ServiceCollectionExtensions.cs deleted file mode 100644 index 35ce62d9..00000000 --- a/zero.Core/Mapper/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; - -namespace zero.Mapper; - -internal static class ServiceCollectionExtensions -{ - public static IServiceCollection AddZeroMapper(this IServiceCollection services) - { - services.AddScoped(); - return services; - } -} \ No newline at end of file diff --git a/zero.Core/Media/ServiceCollectionExtensions.cs b/zero.Core/Media/MediaModule.cs similarity index 84% rename from zero.Core/Media/ServiceCollectionExtensions.cs rename to zero.Core/Media/MediaModule.cs index c340ae87..568b746d 100644 --- a/zero.Core/Media/ServiceCollectionExtensions.cs +++ b/zero.Core/Media/MediaModule.cs @@ -7,9 +7,9 @@ using System.IO; namespace zero.Media; -internal static class ServiceCollectionExtensions +public class MediaModule : ZeroModule { - public static IServiceCollection AddZeroMedia(this IServiceCollection services, IConfiguration config) + public override void ConfigureServices(IServiceCollection services, IConfiguration configuration) { services.AddSingleton(svc => { @@ -22,7 +22,7 @@ internal static class ServiceCollectionExtensions services.AddScoped(); services.AddScoped(); - services.AddOptions().Bind(config.GetSection("Zero:Media")).Configure(opts => + services.AddOptions().Bind(configuration.GetSection("Zero:Media")).Configure(opts => { opts.FolderPath = "uploads"; opts.AllowedOtherFileExtensions = new() { ".pdf" }; @@ -41,6 +41,5 @@ internal static class ServiceCollectionExtensions raven.Indexes.Add(); raven.Indexes.Add(); }); - return services; } } \ No newline at end of file diff --git a/zero.Core/Models/Results/Paged.cs b/zero.Core/Models/Results/Paged.cs index 4ead0606..0505fb78 100644 --- a/zero.Core/Models/Results/Paged.cs +++ b/zero.Core/Models/Results/Paged.cs @@ -13,10 +13,15 @@ public class Paged : Paged { return new Paged(Items.Select(x => convertItem(x)).Where(x => x != null).ToList(), TotalItems, Page, PageSize); } + + public override IEnumerable GetItems() + { + return Items; + } } -public class Paged +public abstract class Paged { public long Page { get; protected set; } @@ -55,4 +60,6 @@ public class Paged } return 0; } + + public abstract IEnumerable GetItems(); } \ No newline at end of file diff --git a/zero.Core/Pages/ServiceCollectionExtensions.cs b/zero.Core/Pages/PagesModule.cs similarity index 68% rename from zero.Core/Pages/ServiceCollectionExtensions.cs rename to zero.Core/Pages/PagesModule.cs index 1298eb5d..a16899ce 100644 --- a/zero.Core/Pages/ServiceCollectionExtensions.cs +++ b/zero.Core/Pages/PagesModule.cs @@ -3,16 +3,16 @@ using Microsoft.Extensions.DependencyInjection; namespace zero.Pages; -internal static class ServiceCollectionExtensions +public class PagesModule : ZeroModule { - public static IServiceCollection AddZeroPages(this IServiceCollection services, IConfiguration config) + public override void ConfigureServices(IServiceCollection services, IConfiguration configuration) { services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddOptions().Bind(config.GetSection("Zero:Pages")); - services.AddOptions().Bind(config.GetSection("Zero:PageModules")); + services.AddOptions().Bind(configuration.GetSection("Zero:Pages")); + services.AddOptions().Bind(configuration.GetSection("Zero:PageModules")); services.Configure(opts => { @@ -24,6 +24,5 @@ internal static class ServiceCollectionExtensions opts.For().Add(Constants.Pages.FolderAlias, "@page.folder.name", "@page.folder.description", "fth-folder"); }); - return services; } } \ No newline at end of file diff --git a/zero.Core/Persistence/ServiceCollectionExtensions.cs b/zero.Core/Persistence/PersistenceModule.cs similarity index 81% rename from zero.Core/Persistence/ServiceCollectionExtensions.cs rename to zero.Core/Persistence/PersistenceModule.cs index 6067fc60..bc2a5179 100644 --- a/zero.Core/Persistence/ServiceCollectionExtensions.cs +++ b/zero.Core/Persistence/PersistenceModule.cs @@ -5,9 +5,9 @@ using Raven.Client.Http; namespace zero.Persistence; -internal static class ServiceCollectionExtensions +public class PersistenceModule : ZeroModule { - public static IServiceCollection AddZeroPersistence(this IServiceCollection services, IConfiguration config) + public override void ConfigureServices(IServiceCollection services, IConfiguration configuration) { services.AddSingleton(); services.AddSingleton(CreateRavenStore); @@ -15,16 +15,13 @@ internal static class ServiceCollectionExtensions services.AddScoped(); services.AddScoped(); - services.AddOptions().Bind(config.GetSection("Zero:Raven")); - - return services; + services.AddOptions().Bind(configuration.GetSection("Zero:Raven")); } - /// /// Creates and configures the raven store /// - static ZeroDocumentStore CreateRavenStore(IServiceProvider services) + protected ZeroDocumentStore CreateRavenStore(IServiceProvider services) { IZeroOptions options = services.GetService(); IZeroDocumentConventionsBuilder conventionsBuilder = services.GetService(); diff --git a/zero.Core/Persistence/Safenames.cs b/zero.Core/Persistence/Safenames.cs index 6821692a..7d41297d 100644 --- a/zero.Core/Persistence/Safenames.cs +++ b/zero.Core/Persistence/Safenames.cs @@ -58,7 +58,7 @@ public class Safenames } char previous = default; - StringBuilder output = new StringBuilder(); + StringBuilder output = new(); for (int i = 0; i < value.Length; i++) { diff --git a/zero.Core/Rendering/RenderingModule.cs b/zero.Core/Rendering/RenderingModule.cs new file mode 100644 index 00000000..a4cc72dc --- /dev/null +++ b/zero.Core/Rendering/RenderingModule.cs @@ -0,0 +1,12 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace zero.Rendering; + +public class RenderingModule : ZeroModule +{ + public override void ConfigureServices(IServiceCollection services, IConfiguration configuration) + { + services.AddScoped(); + } +} \ No newline at end of file diff --git a/zero.Core/Rendering/ServiceCollectionExtensions.cs b/zero.Core/Rendering/ServiceCollectionExtensions.cs deleted file mode 100644 index 114b5b90..00000000 --- a/zero.Core/Rendering/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; - -namespace zero.Rendering; - -internal static class ServiceCollectionExtensions -{ - public static IServiceCollection AddZeroRendering(this IServiceCollection services) - { - services.AddScoped(); - return services; - } -} \ No newline at end of file diff --git a/zero.Core/Routing/ServiceCollectionExtensions.cs b/zero.Core/Routing/RoutingModule.cs similarity index 82% rename from zero.Core/Routing/ServiceCollectionExtensions.cs rename to zero.Core/Routing/RoutingModule.cs index 3fd44bf2..5894a993 100644 --- a/zero.Core/Routing/ServiceCollectionExtensions.cs +++ b/zero.Core/Routing/RoutingModule.cs @@ -5,9 +5,9 @@ using Microsoft.Extensions.DependencyInjection.Extensions; namespace zero.Routing; -internal static class ServiceCollectionExtensions +public class RoutingModule : ZeroModule { - public static IServiceCollection AddZeroRouting(this IServiceCollection services, IConfiguration config) + public override void ConfigureServices(IServiceCollection services, IConfiguration configuration) { services.AddScoped(); services.AddScoped(); @@ -22,7 +22,7 @@ internal static class ServiceCollectionExtensions services.TryAddEnumerable(ServiceDescriptor.Singleton()); services.AddScoped(); - services.AddOptions().Bind(config.GetSection("Zero:Routing")); + services.AddOptions().Bind(configuration.GetSection("Zero:Routing")); services.Configure(opts => { @@ -31,7 +31,5 @@ internal static class ServiceCollectionExtensions raven.Indexes.Add(); raven.Indexes.Add(); }); - - return services; } } \ No newline at end of file diff --git a/zero.Core/Spaces/ServiceCollectionExtensions.cs b/zero.Core/Spaces/SpacesModule.cs similarity index 53% rename from zero.Core/Spaces/ServiceCollectionExtensions.cs rename to zero.Core/Spaces/SpacesModule.cs index 49a4a527..3b6c7a6b 100644 --- a/zero.Core/Spaces/ServiceCollectionExtensions.cs +++ b/zero.Core/Spaces/SpacesModule.cs @@ -3,16 +3,14 @@ using Microsoft.Extensions.DependencyInjection; namespace zero.Spaces; -internal static class ServiceCollectionExtensions +public class SpacesModule : ZeroModule { - public static IServiceCollection AddZeroSpaces(this IServiceCollection services, IConfiguration config) + public override void ConfigureServices(IServiceCollection services, IConfiguration configuration) { services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddOptions().Bind(config.GetSection("Zero:Spaces")); - - return services; + services.AddOptions().Bind(configuration.GetSection("Zero:Spaces")); } } \ No newline at end of file diff --git a/zero.Core/Stores/ServiceCollectionExtensions.cs b/zero.Core/Stores/ServiceCollectionExtensions.cs deleted file mode 100644 index c0ddfe20..00000000 --- a/zero.Core/Stores/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; - -namespace zero.Stores; - -internal static class ServiceCollectionExtensions -{ - public static IServiceCollection AddZeroStores(this IServiceCollection services) - { - services.AddScoped(); - services.AddScoped(); - services.AddSingleton(); - return services; - } -} \ No newline at end of file diff --git a/zero.Core/Stores/StoresModule.cs b/zero.Core/Stores/StoresModule.cs new file mode 100644 index 00000000..3038640d --- /dev/null +++ b/zero.Core/Stores/StoresModule.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace zero.Stores; + +public class StoresModule : ZeroModule +{ + public override void ConfigureServices(IServiceCollection services, IConfiguration configuration) + { + services.AddScoped(); + services.AddScoped(); + services.AddSingleton(); + } +} \ No newline at end of file diff --git a/zero.Core/ZeroApplicationBuilder.cs b/zero.Core/ZeroApplicationBuilder.cs index 008de8f0..cbe5fdc9 100644 --- a/zero.Core/ZeroApplicationBuilder.cs +++ b/zero.Core/ZeroApplicationBuilder.cs @@ -13,6 +13,7 @@ public class ZeroApplicationBuilder : IZeroApplicationBuilder { App = app; App.UseStaticFiles(); + App.UseExceptionHandler("/zero/api/error"); App.UseMiddleware(); App.UseRouting(); App.UseAuthentication(); diff --git a/zero.Core/ZeroBuilder.cs b/zero.Core/ZeroBuilder.cs index 7ae1f161..944312ab 100644 --- a/zero.Core/ZeroBuilder.cs +++ b/zero.Core/ZeroBuilder.cs @@ -30,27 +30,27 @@ public class ZeroBuilder services.AddControllers(); - // adds and discovers additional and built-in assemblies new AssemblyDiscovery(Mvc).Execute(StartupOptions.AssemblyDiscoveryRules); + AddModule(); + AddModule(); + AddModule(); + AddModule(); + AddModule(); + AddModule(); + AddModule(); + AddModule(); + AddModule(); + AddModule(); + AddModule(); + AddModule(); - services.AddZeroApplications(Configuration); - services.AddZeroBlueprints(Configuration); - services.AddZeroCommunication(); - services.AddZeroConfiguration(Configuration); - services.AddZeroContext(); - services.AddZeroFileStorage(Configuration); - services.AddZeroIdentity(); - services.AddZeroLocalization(); - services.AddZeroMails(); - services.AddZeroPages(Configuration); - services.AddZeroMedia(Configuration); - services.AddZeroPersistence(Configuration); - services.AddZeroRendering(); - services.AddZeroRouting(Configuration); - services.AddZeroStores(); - services.AddZeroMapper(); + AddModule(); + AddModule(); + AddModule(); + AddModule(); + AddModule(); //if (Environment.GetEnvironmentVariable("DOTNET_WATCH") == "1") //{ @@ -72,6 +72,13 @@ public class ZeroBuilder } + public ZeroBuilder AddModule() where T : class, IZeroModule, new() + { + ZeroModuleCollection.AddModule(Services, Configuration); + return this; + } + + /// /// Adds a zero plugin ///