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 System; 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(); // 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 }); // add transient options to DI Services.AddTransient(factory => factory.GetService>().CurrentValue); // configure MVC Services.TryAddEnumerable(ServiceDescriptor.Transient, ZeroBuilderMvcOptions>()); 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 Raven + identity ConfigureDatabase(); ConfigureIdentity(); // 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.AddSingleton(); Services.AddHttpContextAccessor(); Services.AddTransient(); Services.AddTransient(factory => { IWebHostEnvironment env = factory.GetService(); return new Paths(env.WebRootPath, 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(); } /// /// Configures Raven database instance /// void ConfigureDatabase() { // add raven Services.AddSingleton(context => { IZeroOptions options = context.GetService(); IDocumentStore store = new ZeroStore(options) { Urls = new string[1] { options.Raven.Url } }; IDocumentStore raven = store.Setup(options).Initialize(); // create all indexes var assemblies = AssemblyDiscovery.Current.GetAssemblies().ToList(); // TODO maybe we shouldn't use all auto-registered assemblies but specify them directly via options? if (options.SetupCompleted) { foreach (Assembly assembly in assemblies) { IndexCreation.CreateIndexes(assembly, store, database: options.Raven.Database); } } return (ZeroStore)raven; }); Services.AddScoped(services => { var session = services.GetRequiredService()!.OpenAsyncSession(); session.Advanced.WaitForIndexesAfterSaveChanges(); return session as ZeroDocumentSession; }); } /// /// Configures identity /// void ConfigureIdentity() { Services.TryAddEnumerable(ServiceDescriptor.Singleton, PostConfigureCookieAuthenticationOptions>()); Services.AddZeroIdentity(); Services.Replace, ZeroBackofficeClaimsPrincipalFactory>(); Services.Replace, RavenCoreUserStore>(ServiceLifetime.Scoped); Services.Replace, RavenCoreRoleStore>(ServiceLifetime.Scoped); Services.AddAuthentication(Constants.Auth.BackofficeScheme) .AddZeroBackofficeCookie(); Services.AddAuthorization(); } /// /// 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)); } } } }