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(); // 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.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(); } /// /// Configures Raven database instance /// void ConfigureDatabase() { Services.AddSingleton(); Services.AddSingleton(context => { IZeroOptions options = context.GetService(); IZeroDocumentConventionsBuilder conventionsBuilder = context.GetService(); IDocumentStore store = new ZeroStore(options) { Urls = new string[1] { options.Raven.Url }, Conventions = // TODO activate and test this { AggressiveCache = { Duration = TimeSpan.FromHours(1), Mode = AggressiveCacheMode.TrackChanges } } }; conventionsBuilder.Run(store.Conventions); IDocumentStore raven = store.Initialize(); // create all indexes if (options.SetupCompleted) { var indexes = options.Raven.Indexes.BuildAll(options, store); IndexCreation.CreateIndexes(indexes, store, database: options.Raven.Database); } return (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)); } } } }