From 6d17948babb531f41e154205dfe4c86261cd7f37 Mon Sep 17 00:00:00 2001 From: Tobias Klika Date: Wed, 27 May 2020 16:03:38 +0200 Subject: [PATCH] add assembly discovery rules + service registration extensions --- zero.Core/Api/ApplicationsApi.cs | 2 +- zero.Core/Api/BackofficeApi.cs | 4 +- zero.Core/Api/CountriesApi.cs | 2 +- zero.Core/Api/LanguagesApi.cs | 2 +- zero.Core/Api/MediaFolderApi.cs | 2 +- zero.Core/Api/PagesApi.cs | 2 +- zero.Core/Api/TranslationsApi.cs | 2 +- zero.Core/Assemblies/AssemblyDiscovery.cs | 129 ++++++++++++++ .../Assemblies/AssemblyDiscoveryContext.cs | 27 +++ .../Assemblies/IAssemblyDiscoveryRule.cs | 13 ++ .../Extensions/ServiceCollectionExtensions.cs | 111 ++++++++++++ zero.Core/Options/ZeroOptions.cs | 1 + zero.Core/Options/ZeroStartupOptions.cs | 15 ++ zero.Core/Renderer/AbstractRenderer.cs | 2 + zero.Core/ServiceCollectionExtensions.cs | 70 -------- zero.Debug/Startup.cs | 5 +- zero.Web.UI/App/editor/fields/rte.vue | 13 +- zero.Web/ApiControllerFeatureProvider.cs | 9 +- zero.Web/Defaults/DefaultBackofficePlugin.cs | 45 ----- .../Defaults/ZeroAssemblyDiscoveryRule.cs | 20 +++ zero.Web/Defaults/ZeroBackofficePlugin.cs | 79 +++++++++ zero.Web/ZeroBuilder.cs | 161 ++++++++---------- zero.Web/ZeroServiceCollectionExtensions.cs | 8 +- 23 files changed, 499 insertions(+), 225 deletions(-) create mode 100644 zero.Core/Assemblies/AssemblyDiscovery.cs create mode 100644 zero.Core/Assemblies/AssemblyDiscoveryContext.cs create mode 100644 zero.Core/Assemblies/IAssemblyDiscoveryRule.cs create mode 100644 zero.Core/Extensions/ServiceCollectionExtensions.cs create mode 100644 zero.Core/Options/ZeroStartupOptions.cs delete mode 100644 zero.Core/ServiceCollectionExtensions.cs delete mode 100644 zero.Web/Defaults/DefaultBackofficePlugin.cs create mode 100644 zero.Web/Defaults/ZeroAssemblyDiscoveryRule.cs create mode 100644 zero.Web/Defaults/ZeroBackofficePlugin.cs diff --git a/zero.Core/Api/ApplicationsApi.cs b/zero.Core/Api/ApplicationsApi.cs index b7096ed7..e552217f 100644 --- a/zero.Core/Api/ApplicationsApi.cs +++ b/zero.Core/Api/ApplicationsApi.cs @@ -62,7 +62,7 @@ namespace zero.Core.Api /// public async Task> Save(T model) { - return await Save(model, Validator); + return await SaveModel(model, Validator); } diff --git a/zero.Core/Api/BackofficeApi.cs b/zero.Core/Api/BackofficeApi.cs index 0abdffc1..d644e727 100644 --- a/zero.Core/Api/BackofficeApi.cs +++ b/zero.Core/Api/BackofficeApi.cs @@ -71,7 +71,7 @@ namespace zero.Core.Api /// - public async Task> Save(T model, IValidator validator = null) where T : IZeroIdEntity + public async Task> SaveModel(T model, IValidator validator = null) where T : IZeroIdEntity { // check for alias //if (model is IUrlAliasEntity) @@ -221,7 +221,7 @@ namespace zero.Core.Api /// /// Updates or creates an entity with an optional validator /// - Task> Save(T model, IValidator validator = null) where T : IZeroIdEntity; + Task> SaveModel(T model, IValidator validator = null) where T : IZeroIdEntity; /// /// Deletes an entity by Id diff --git a/zero.Core/Api/CountriesApi.cs b/zero.Core/Api/CountriesApi.cs index eb3b8de0..d42d7405 100644 --- a/zero.Core/Api/CountriesApi.cs +++ b/zero.Core/Api/CountriesApi.cs @@ -62,7 +62,7 @@ namespace zero.Core.Api /// public async Task> Save(T model) { - return await Save(model, Validator); + return await SaveModel(model, Validator); } diff --git a/zero.Core/Api/LanguagesApi.cs b/zero.Core/Api/LanguagesApi.cs index e8c04bc6..c1becc7c 100644 --- a/zero.Core/Api/LanguagesApi.cs +++ b/zero.Core/Api/LanguagesApi.cs @@ -75,7 +75,7 @@ namespace zero.Core.Api /// public async Task> Save(T model) { - return await Save(model, Validator); + return await SaveModel(model, Validator); } diff --git a/zero.Core/Api/MediaFolderApi.cs b/zero.Core/Api/MediaFolderApi.cs index 434289c9..dc5c4845 100644 --- a/zero.Core/Api/MediaFolderApi.cs +++ b/zero.Core/Api/MediaFolderApi.cs @@ -104,7 +104,7 @@ namespace zero.Core.Api /// public async Task> Save(MediaFolder model) { - return await Save(model, new MediaFolderValidator()); + return await SaveModel(model, new MediaFolderValidator()); } diff --git a/zero.Core/Api/PagesApi.cs b/zero.Core/Api/PagesApi.cs index a650f91f..43c21a66 100644 --- a/zero.Core/Api/PagesApi.cs +++ b/zero.Core/Api/PagesApi.cs @@ -63,7 +63,7 @@ namespace zero.Core.Api /// public async Task> Save(T model) { - return await Save(model, null); + return await SaveModel(model, null); } diff --git a/zero.Core/Api/TranslationsApi.cs b/zero.Core/Api/TranslationsApi.cs index 43f575d0..32ca9ad3 100644 --- a/zero.Core/Api/TranslationsApi.cs +++ b/zero.Core/Api/TranslationsApi.cs @@ -91,7 +91,7 @@ namespace zero.Core.Api /// public async Task> Save(T model) { - return await Save(model, Validator); + return await SaveModel(model, Validator); } diff --git a/zero.Core/Assemblies/AssemblyDiscovery.cs b/zero.Core/Assemblies/AssemblyDiscovery.cs new file mode 100644 index 00000000..a1c67614 --- /dev/null +++ b/zero.Core/Assemblies/AssemblyDiscovery.cs @@ -0,0 +1,129 @@ +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyModel; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace zero.Core.Assemblies +{ + public class AssemblyDiscovery : IAssemblyDiscovery + { + AssemblyDiscoveryContext Context; + + IMvcBuilder Mvc; + + + public AssemblyDiscovery(IMvcBuilder mvc) + { + Mvc = mvc; + Context = new AssemblyDiscoveryContext(); + } + + + /// + public void Execute(IEnumerable rules) + { + List assemblies = new List(); + DependencyContext dependencyContext = DependencyContext.Load(Context.EntryAssembly); + + if (dependencyContext == null) + { + return; + } + + string[] existingLibs = Mvc.PartManager.ApplicationParts.OfType().Select(p => p.Name).ToArray(); + + IEnumerable libraries = dependencyContext.RuntimeLibraries.Where(lib => !existingLibs.Contains(lib.Name, StringComparer.OrdinalIgnoreCase)); + + foreach (RuntimeLibrary library in libraries) + { + if (rules.Any(rule => rule.IsValid(library, Context))) + { + IEnumerable libraryAssemblies = library.GetDefaultAssemblyNames(dependencyContext).Select(name => Assembly.Load(name)); + + foreach (Assembly assembly in libraryAssemblies) + { + AddAssembly(assembly); + } + } + } + } + + + /// + public void AddAssembly(Assembly assembly) + { + Mvc.AddApplicationPart(assembly); + } + + + /// + public IEnumerable GetTypes() => GetTypes(typeof(TService)); + + + /// + public IEnumerable GetTypes(Type serviceType) + { + return GetConcreteTypes().Where(t => serviceType.GetTypeInfo().IsAssignableFrom(t) && t.AsType() != serviceType); + } + + + /// + /// Get all registered types + /// + public IEnumerable GetConcreteTypes() => GetTypes().Where(t => t.IsClass && !t.IsAbstract && !t.ContainsGenericParameters); + + + /// + /// Get all registered assemblies + /// + public IEnumerable GetAssemblies() => Mvc.PartManager.ApplicationParts.OfType().Select(p => p.Assembly); + + + /// + /// Get all registered types + /// + public IEnumerable GetTypes() => Mvc.PartManager.ApplicationParts.OfType().SelectMany(p => p.Types); + } + + + public interface IAssemblyDiscovery + { + /// + /// Discovers runtime assemblies based on the given rules + /// + void Execute(IEnumerable rules); + + /// + /// Manually add an assembly + /// + void AddAssembly(Assembly assembly); + + /// + /// Get all discovered types which implement a certain service + /// + IEnumerable GetTypes(); + + /// + /// Get all discovered types which implement a certain service + /// + IEnumerable GetTypes(Type serviceType); + + /// + /// Get all registered types + /// + IEnumerable GetTypes(); + + /// + /// Get all registered types + /// + IEnumerable GetConcreteTypes(); + + /// + /// Get all registered assemblies + /// + IEnumerable GetAssemblies(); + } +} diff --git a/zero.Core/Assemblies/AssemblyDiscoveryContext.cs b/zero.Core/Assemblies/AssemblyDiscoveryContext.cs new file mode 100644 index 00000000..8ae64a2d --- /dev/null +++ b/zero.Core/Assemblies/AssemblyDiscoveryContext.cs @@ -0,0 +1,27 @@ +using System.Reflection; + +namespace zero.Core.Assemblies +{ + public class AssemblyDiscoveryContext + { + public Assembly EntryAssembly { get; private set; } + + public string EntryAssemblyName { get; private set; } + + public bool HasEntryAssembly => EntryAssembly != null; + + + public AssemblyDiscoveryContext() + { + EntryAssembly = Assembly.GetEntryAssembly(); + EntryAssemblyName = EntryAssembly?.GetName().Name; + } + + + public AssemblyDiscoveryContext(Assembly entryAssembly) + { + EntryAssembly = entryAssembly; + EntryAssemblyName = entryAssembly.GetName().Name; + } + } +} diff --git a/zero.Core/Assemblies/IAssemblyDiscoveryRule.cs b/zero.Core/Assemblies/IAssemblyDiscoveryRule.cs new file mode 100644 index 00000000..7913679b --- /dev/null +++ b/zero.Core/Assemblies/IAssemblyDiscoveryRule.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.DependencyModel; + +namespace zero.Core.Assemblies +{ + public interface IAssemblyDiscoveryRule + { + /// + /// Returns true if the specified runtime library should be added to + /// the ApplicationPartManager; otherwise false. + /// + bool IsValid(RuntimeLibrary library, AssemblyDiscoveryContext context); + } +} diff --git a/zero.Core/Extensions/ServiceCollectionExtensions.cs b/zero.Core/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..d2fe5291 --- /dev/null +++ b/zero.Core/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,111 @@ +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using zero.Core.Assemblies; + +namespace zero.Core.Extensions +{ + public static class ServiceCollectionExtensions + { + public static IAssemblyDiscovery ZeroAssemblyDiscovery { get; set; } + + + /// + /// Adds all found implementations based on the service type and assembly discovery rules + /// + public static void AddAll(this IServiceCollection services, ServiceLifetime lifetime = ServiceLifetime.Transient) => services.AddAll(typeof(TService), lifetime); + + + /// + /// Adds all found implementations based on the service type and assembly discovery rules + /// + public static void AddAll(this IServiceCollection services, Type serviceType, ServiceLifetime lifetime = ServiceLifetime.Transient) + { + if (ZeroAssemblyDiscovery == null) + { + throw new Exception("services.AddAll() can only be run after mvcBuilder.AddZero()"); + } + + // add implementations with generic service types + if (serviceType.GetTypeInfo().IsGenericTypeDefinition) + { + IEnumerable<(Type, TypeInfo)> matches = ZeroAssemblyDiscovery.GetConcreteTypes().SelectMany(type => + { + IEnumerable genericTypes = type.GetInterfaces().Select(x => x.GetTypeInfo()).Where(x => x.IsGenericType && x.GetGenericTypeDefinition() == serviceType); + return genericTypes.Select(x => (x, type)); + }); + + foreach ((Type, Type) match in matches) + { + services.Add(new ServiceDescriptor(match.Item1, match.Item2, lifetime)); + } + } + // add implementations with specific service types + else + { + foreach (Type type in ZeroAssemblyDiscovery.GetTypes(serviceType)) + { + services.Add(new ServiceDescriptor(serviceType, type, lifetime)); + } + } + } + + + /// + /// Adds or overrides an implementation + /// + public static void Replace(this IServiceCollection services) + where TService : class + where TImplementation : class, TService + { + services.Remove(out ServiceLifetime lifetime); + services.Add(new ServiceDescriptor(typeof(TService), typeof(TImplementation), lifetime)); + } + + + /// + /// Adds or overrides an implementation + /// + public static void Replace(this IServiceCollection services, Func implementationFactory) + where TService : class + where TImplementation : class, TService + { + services.Remove(out ServiceLifetime lifetime); + services.Add(new ServiceDescriptor(typeof(TService), implementationFactory, lifetime)); + } + + + /// + /// Removes all instances of a registered service + /// + public static bool Remove(this IServiceCollection services) => services.Remove(out ServiceLifetime lifetime); + + + /// + /// Removes all instances of a registered service + /// + public static bool Remove(this IServiceCollection services, out ServiceLifetime lifetime) + { + Type serviceType = typeof(TService); + IEnumerable descriptors = services.Where(x => x.ServiceType == serviceType); + + if (descriptors.Any()) + { + lifetime = descriptors.First().Lifetime; + + foreach (ServiceDescriptor descriptor in descriptors) + { + services.Remove(descriptor); + } + } + else + { + lifetime = ServiceLifetime.Transient; + } + + return descriptors.Any(); + } + } +} diff --git a/zero.Core/Options/ZeroOptions.cs b/zero.Core/Options/ZeroOptions.cs index fd9dc401..0a363cc5 100644 --- a/zero.Core/Options/ZeroOptions.cs +++ b/zero.Core/Options/ZeroOptions.cs @@ -1,6 +1,7 @@ using FluentValidation; using System; using System.Collections.Generic; +using zero.Core.Assemblies; using zero.Core.Entities; using zero.Core.Mapper; using zero.Core.Plugins; diff --git a/zero.Core/Options/ZeroStartupOptions.cs b/zero.Core/Options/ZeroStartupOptions.cs new file mode 100644 index 00000000..194648ef --- /dev/null +++ b/zero.Core/Options/ZeroStartupOptions.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using zero.Core.Assemblies; + +namespace zero.Core.Options +{ + public class ZeroStartupOptions : IZeroStartupOptions + { + public IList AssemblyDiscoveryRules { get; private set; } = new List(); + } + + public interface IZeroStartupOptions + { + IList AssemblyDiscoveryRules { get; } + } +} diff --git a/zero.Core/Renderer/AbstractRenderer.cs b/zero.Core/Renderer/AbstractRenderer.cs index 2dfcb940..9f3a20cb 100644 --- a/zero.Core/Renderer/AbstractRenderer.cs +++ b/zero.Core/Renderer/AbstractRenderer.cs @@ -12,6 +12,8 @@ namespace zero.Core.Renderer { public Type TargetType { get; set; } + public AbstractGenericRenderer() { } + internal AbstractGenericRenderer(Type type, string alias, List properties, IValidator validator = null) { Alias = alias; diff --git a/zero.Core/ServiceCollectionExtensions.cs b/zero.Core/ServiceCollectionExtensions.cs deleted file mode 100644 index 331384e8..00000000 --- a/zero.Core/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,70 +0,0 @@ -using FluentValidation; -using Microsoft.Extensions.DependencyInjection; -using System; -using System.Linq; -using System.Reflection; -using zero.Core.Api; -using zero.Core.Renderer; - -namespace zero.Core.Entities -{ - public static class ServiceCollectionExtensions - { - public static void AddZeroCoreServices(this IServiceCollection services) - { - Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); - - services.AddAllByInterfaceTransient(typeof(IValidator), typeof(IValidator<>), assemblies, true); - services.AddAllByInterfaceTransient(typeof(IRenderer), typeof(IRenderer<>), assemblies); - - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - - - services.AddTransient(typeof(IApplicationsApi<>), typeof(ApplicationsApi<>)); - services.AddTransient(typeof(ICountriesApi<>), typeof(CountriesApi<>)); - services.AddTransient(typeof(ILanguagesApi<>), typeof(LanguagesApi<>)); - services.AddTransient(typeof(ITranslationsApi), typeof(TranslationsApi)); - services.AddTransient(typeof(ITranslationsApi<>), typeof(TranslationsApi<>)); - services.AddTransient(typeof(ITranslationsApiFacade), typeof(TranslationsApiFacade)); - services.AddTransient(typeof(IPagesApi<>), typeof(PagesApi<>)); - services.AddTransient(typeof(IPageTreeApi<>), typeof(PageTreeApi<>)); - } - - - public static void AddAllByInterfaceTransient(this IServiceCollection services, Assembly[] assemblies) - { - services.AddAllByInterfaceTransient(typeof(TService), typeof(TImplementation), assemblies); - } - - - public static void AddAllByInterfaceTransient(this IServiceCollection services, Type serviceType, Type implementingType, Assembly[] assemblies, bool transient = false) - { - foreach (Assembly assembly in assemblies) - { - foreach (Type type in assembly.GetExportedTypes()) - { - if (serviceType.IsAssignableFrom(type) && type.IsClass && !type.IsAbstract && !type.FullName.StartsWith("Fluent") && type.Name != "AbstractGenericRenderer") - { - Type service = type.GetInterfaces().FirstOrDefault(x => x.IsInterface && x.Name == implementingType.Name); - - if (service != null) - { - if (transient) - { - services.AddTransient(service, type); - } - else - { - services.AddScoped(service, type); - } - } - } - } - } - } - } -} diff --git a/zero.Debug/Startup.cs b/zero.Debug/Startup.cs index b224b26e..a9920db2 100644 --- a/zero.Debug/Startup.cs +++ b/zero.Debug/Startup.cs @@ -39,7 +39,10 @@ namespace zero.Debug options.LoginPath = "/Account/Index"; }); - ZeroBuilder zero = services.AddZero(Configuration); + IMvcBuilder mvc = services.AddMvc(); + + ZeroBuilder zero = mvc.AddZero(Configuration); + zero.AddPlugin(); zero.AddPlugin(); diff --git a/zero.Web.UI/App/editor/fields/rte.vue b/zero.Web.UI/App/editor/fields/rte.vue index 8f0d9731..c4c40934 100644 --- a/zero.Web.UI/App/editor/fields/rte.vue +++ b/zero.Web.UI/App/editor/fields/rte.vue @@ -1,12 +1,23 @@  \ No newline at end of file diff --git a/zero.Web/ApiControllerFeatureProvider.cs b/zero.Web/ApiControllerFeatureProvider.cs index 6ea24fea..3dfe2648 100644 --- a/zero.Web/ApiControllerFeatureProvider.cs +++ b/zero.Web/ApiControllerFeatureProvider.cs @@ -37,9 +37,12 @@ namespace zero.Web } Type implementation = EntityMap.Get(desiredInterface); - TypeInfo type = candidate.MakeGenericType(implementation).GetTypeInfo(); - - feature.Controllers.Add(type); + + if (implementation != null) + { + TypeInfo type = candidate.MakeGenericType(implementation).GetTypeInfo(); + feature.Controllers.Add(type); + } } } } diff --git a/zero.Web/Defaults/DefaultBackofficePlugin.cs b/zero.Web/Defaults/DefaultBackofficePlugin.cs deleted file mode 100644 index 4a4029a4..00000000 --- a/zero.Web/Defaults/DefaultBackofficePlugin.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using zero.Core; -using zero.Core.Entities; -using zero.Core.Options; -using zero.Core.Plugins; -using zero.Web.Mapper; -using zero.Web.Sections; - -namespace zero.Web.Defaults -{ - internal class DefaultBackofficePlugin : IZeroPlugin - { - public void ConfigureServices(IServiceCollection services) - { - EntityMap.Use(); - EntityMap.Use(); - EntityMap.Use(); - EntityMap.Use(); - EntityMap.Use(); - } - - public void Configure(IZeroPluginOptions plugin, IZeroOptions zero) - { - plugin.Name = "zero.Defaults"; - plugin.LocalizationPaths.Add("~/Resources/Localization/zero.{lang}.json"); - - zero.Sections.Add(); - zero.Sections.Add(); - zero.Sections.Add(); - zero.Sections.Add(); - zero.Sections.Add(); - - zero.Settings.AddGroup(); - zero.Settings.AddGroup(); - - zero.Mapper.Add(); - zero.Mapper.Add(); - zero.Mapper.Add(); - zero.Mapper.Add(); - zero.Mapper.Add(); - zero.Mapper.Add(); - zero.Mapper.Add(); - } - } -} \ No newline at end of file diff --git a/zero.Web/Defaults/ZeroAssemblyDiscoveryRule.cs b/zero.Web/Defaults/ZeroAssemblyDiscoveryRule.cs new file mode 100644 index 00000000..0dd3a5e6 --- /dev/null +++ b/zero.Web/Defaults/ZeroAssemblyDiscoveryRule.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.DependencyModel; +using System; +using zero.Core.Assemblies; + +namespace zero.Web.Defaults +{ + public class ZeroAssemblyDiscoveryRule : IAssemblyDiscoveryRule + { + const string ZERO_PREFIX = "zero."; + + + /// + public bool IsValid(RuntimeLibrary library, AssemblyDiscoveryContext context) + { + StringComparison casing = StringComparison.OrdinalIgnoreCase; + // TODO we need to auto-add assemblies and discover their types which have implementations of IZeroPlugin + return library.Name.StartsWith(ZERO_PREFIX, casing) || (context.HasEntryAssembly && library.Name.Contains(context.EntryAssemblyName, casing)); + } + } +} diff --git a/zero.Web/Defaults/ZeroBackofficePlugin.cs b/zero.Web/Defaults/ZeroBackofficePlugin.cs new file mode 100644 index 00000000..0e6b2f76 --- /dev/null +++ b/zero.Web/Defaults/ZeroBackofficePlugin.cs @@ -0,0 +1,79 @@ +using FluentValidation; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Reflection; +using zero.Core; +using zero.Core.Api; +using zero.Core.Entities; +using zero.Core.Extensions; +using zero.Core.Options; +using zero.Core.Plugins; +using zero.Core.Renderer; +using zero.Web.Mapper; +using zero.Web.Sections; + +namespace zero.Web.Defaults +{ + internal class ZeroBackofficePlugin : IZeroPlugin + { + public void ConfigureServices(IServiceCollection services) + { + Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); + + services.AddAll(typeof(IValidator<>)); + services.AddAll(typeof(IRenderer<>)); + + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + services.AddTransient(typeof(IApplicationsApi<>), typeof(ApplicationsApi<>)); + services.AddTransient(typeof(ICountriesApi<>), typeof(CountriesApi<>)); + services.AddTransient(typeof(ILanguagesApi<>), typeof(LanguagesApi<>)); + services.AddTransient(typeof(ITranslationsApi), typeof(TranslationsApi)); + services.AddTransient(typeof(ITranslationsApi<>), typeof(TranslationsApi<>)); + services.AddTransient(typeof(ITranslationsApiFacade), typeof(TranslationsApiFacade)); + services.AddTransient(typeof(IPagesApi<>), typeof(PagesApi<>)); + services.AddTransient(typeof(IPageTreeApi<>), typeof(PageTreeApi<>)); + + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + } + + public void Configure(IZeroPluginOptions plugin, IZeroOptions zero) + { + plugin.Name = "zero.Defaults"; + plugin.LocalizationPaths.Add("~/Resources/Localization/zero.{lang}.json"); + + zero.Sections.Add(); + zero.Sections.Add(); + zero.Sections.Add(); + zero.Sections.Add(); + zero.Sections.Add(); + + zero.Settings.AddGroup(); + zero.Settings.AddGroup(); + + zero.Mapper.Add(); + zero.Mapper.Add(); + zero.Mapper.Add(); + zero.Mapper.Add(); + zero.Mapper.Add(); + zero.Mapper.Add(); + zero.Mapper.Add(); + } + } +} \ No newline at end of file diff --git a/zero.Web/ZeroBuilder.cs b/zero.Web/ZeroBuilder.cs index 6223b2e7..b6477012 100644 --- a/zero.Web/ZeroBuilder.cs +++ b/zero.Web/ZeroBuilder.cs @@ -1,6 +1,7 @@ using FluentValidation; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -10,10 +11,13 @@ using Newtonsoft.Json.Serialization; using Raven.Client.Documents; using Raven.Client.Documents.Indexes; using System; +using System.Collections.Generic; +using System.Linq; using System.Reflection; using System.Threading.Tasks; using zero.Core; using zero.Core.Api; +using zero.Core.Assemblies; using zero.Core.Database.Indexes; using zero.Core.Entities; using zero.Core.Extensions; @@ -32,91 +36,55 @@ namespace zero.Web { public virtual IServiceCollection Services { get; } - IConfiguration Configuration { get; set; } + public virtual IMvcBuilder Mvc { get; } + + IConfiguration Configuration; + + IZeroStartupOptions StartupOptions; + + IAssemblyDiscovery AssemblyDiscovery; - public ZeroBuilder(IServiceCollection services, IConfiguration configuration) + public ZeroBuilder(IMvcBuilder builder, IConfiguration configuration, Action setupAction) { - Services = services; + Mvc = builder; + Services = Mvc.Services; Configuration = configuration; - //CultureInfo cultureInfo = new CultureInfo("en-US"); - //cultureInfo.NumberFormat.CurrencySymbol = "€"; - //CultureInfo.DefaultThreadCurrentCulture = cultureInfo; - AddPlugin(); - - AddConfiguration(); - ConfgureMvc(); - ConfigureDatabase(); - ConfigureValidation(); - ConfigureIdentity(); - AddServices(); - //AddPlugins(); - } + // create startup options + StartupOptions = new ZeroStartupOptions(); + StartupOptions.AssemblyDiscoveryRules.Add(new ZeroAssemblyDiscoveryRule()); + setupAction?.Invoke(StartupOptions); - /// - /// Adds zero specific configuration - /// - void AddConfiguration() - { + // adds and discovers additional and built-in assemblies + AssemblyDiscovery = new AssemblyDiscovery(Mvc); + AssemblyDiscovery.Execute(StartupOptions.AssemblyDiscoveryRules); + + ServiceCollectionExtensions.ZeroAssemblyDiscovery = AssemblyDiscovery; + + + // add default plugin + AddPlugin(); + + + // create and bind zero options Services.AddOptions() .Bind(Configuration.GetSection("Zero")) .Configure(opts => { + //opts.AssemblyDiscoveryRules.Add(new ZeroAssemblyDiscoveryRule()); opts.ZeroVersion = "0.0.1.0"; // TODO }); - Services.AddTransient(factory => factory.GetService>().CurrentValue); - } + + // add transient options to DI + Services.AddTransient(factory => factory.GetService>().CurrentValue); - /// - /// Adds all services which are required by zero - /// - void AddServices() - { - Services.AddHttpContextAccessor(); - - Services.AddTransient(); - Services.AddTransient(factory => - { - IWebHostEnvironment env = factory.GetService(); - return new Paths(env.WebRootPath, true); - }); - - Services.AddMapper(); - Services.AddZeroCoreServices(); - - Services.AddScoped(); - - Services.AddTransient(); - Services.AddTransient(typeof(IAppScope<>), typeof(AppScope<>)); - - Services.AddTransient(); - Services.AddTransient(); - Services.AddTransient(); - Services.AddTransient(); - Services.AddTransient(); - Services.AddTransient(); - Services.AddTransient(); - Services.AddTransient(); - Services.AddTransient(); - Services.AddTransient(); - Services.AddTransient(); - Services.AddTransient(); - } - - - /// - /// Configures ASP.NET Core MVC - /// - IMvcBuilder ConfgureMvc() - { - IMvcBuilder mvc = Services.AddMvc(); - - mvc.AddNewtonsoftJson(opts => + // configure MVC + Mvc.AddNewtonsoftJson(opts => { opts.SerializerSettings.Converters.Add(new IsoDateTimeConverter() { DateTimeFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" }); opts.SerializerSettings.Converters.Add(new StringEnumConverter(new CamelCaseNamingStrategy())); @@ -127,15 +95,42 @@ namespace zero.Web if (Environment.GetEnvironmentVariable("DOTNET_WATCH") == "1") { - mvc.AddRazorRuntimeCompilation(); + Mvc.AddRazorRuntimeCompilation(); } - mvc.ConfigureApplicationPartManager(setup => + Mvc.ConfigureApplicationPartManager(setup => { setup.FeatureProviders.Add(new ApiControllerFeatureProvider()); }); - return mvc; + + // configure Raven + Identity + ConfigureDatabase(); + ConfigureIdentity(); + + + // configure FluentValidation + ValidatorOptions.PropertyNameResolver = ValidatorCamelCasePropertyResolver.ResolvePropertyName; + + + // add default mapper + Services.AddMapper(); + + + // add default services + Services.AddScoped(); + + Services.AddTransient(); + Services.AddTransient(typeof(IAppScope<>), typeof(AppScope<>)); + + Services.AddHttpContextAccessor(); + + Services.AddTransient(); + Services.AddTransient(factory => + { + IWebHostEnvironment env = factory.GetService(); + return new Paths(env.WebRootPath, true); + }); } @@ -165,15 +160,6 @@ namespace zero.Web } - /// - /// Configures FluentValidation - /// - void ConfigureValidation() - { - ValidatorOptions.PropertyNameResolver = ValidatorCamelCasePropertyResolver.ResolvePropertyName; - } - - /// /// Configures user + roles /// @@ -234,24 +220,12 @@ namespace zero.Web } - private static EntityDefinition[] Entities = new EntityDefinition[] { }; - - //public ZeroPlugin Use(Action> configure) where TImplementation : TService - //{ - // //configure() - //} - - //public ZeroPlugin Use() where TImplementation : TService - //{ - // //configure() - //} - - /// /// Adds a zero plugin /// public void AddPlugin() where T : class, IZeroPlugin, new() { + Mvc.AddApplicationPart(typeof(T).Assembly); Services.AddScoped(); AddPluginServices(); } @@ -262,6 +236,7 @@ namespace zero.Web /// public void AddPlugin(Func implementationFactory) where T : class, IZeroPlugin, new() { + Mvc.AddApplicationPart(typeof(T).Assembly); Services.AddScoped(implementationFactory); AddPluginServices(); } diff --git a/zero.Web/ZeroServiceCollectionExtensions.cs b/zero.Web/ZeroServiceCollectionExtensions.cs index a40a4132..56417b32 100644 --- a/zero.Web/ZeroServiceCollectionExtensions.cs +++ b/zero.Web/ZeroServiceCollectionExtensions.cs @@ -7,14 +7,14 @@ namespace zero.Web { public static class ZeroServiceCollectionExtensions { - public static ZeroBuilder AddZero(this IServiceCollection services, IConfiguration configuration) + public static ZeroBuilder AddZero(this IMvcBuilder builder, IConfiguration configuration) { - return new ZeroBuilder(services, configuration); + return new ZeroBuilder(builder, configuration, null); } - public static ZeroBuilder AddZero(this IServiceCollection services, IConfiguration configuration, Action setupAction) + public static ZeroBuilder AddZero(this IMvcBuilder builder, IConfiguration configuration, Action setupAction) { - return services.AddZero(configuration).WithOptions(setupAction); + return new ZeroBuilder(builder, configuration, setupAction); } } } \ No newline at end of file