using Esiur.Protocol; using Esiur.Resource; using Esiur.Security.Authority; using Esiur.Security.Authority.Providers; using Esiur.Security.Cryptography; using Esiur.Stores; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; namespace Esiur.AspNetCore; /// /// Fluent registration surface for an Esiur runtime hosted by ASP.NET Core. /// public sealed class EsiurBuilder { internal EsiurBuilder(IServiceCollection services) => Services = services; /// The application service collection. public IServiceCollection Services { get; } /// Uses an existing Warehouse. The ASP.NET host owns its lifecycle by default. public EsiurBuilder UseWarehouse(Warehouse warehouse, bool manageLifecycle = true) { ArgumentNullException.ThrowIfNull(warehouse); Services.Configure(options => { options.Warehouse = warehouse; options.ManageWarehouseLifecycle = manageLifecycle; }); return this; } /// /// Uses an existing EP server. is used only when the /// server is not already attached to the Warehouse. /// public EsiurBuilder UseServer(EpServer server, string serverPath = "sys/server") { ArgumentNullException.ThrowIfNull(server); var path = EsiurConfigurationPath.Normalize(serverPath); Services.Configure(options => { options.Server = server; options.ServerPath = path; }); return this; } /// Configures the final EP server, including externally supplied servers. public EsiurBuilder ConfigureServer(Action configure) { ArgumentNullException.ThrowIfNull(configure); Services.Configure(options => options.ServerConfigurations.Add(configure)); return this; } /// /// Explicitly allows unauthenticated EP sessions. Use this only for resources whose /// authorization policy is safe for anonymous callers. /// public EsiurBuilder AllowAnonymous() { Services.Configure(options => options.ServerConfigurations.Add(server => server.AllowUnauthorizedAccess = true)); return this; } /// Rejects EP sessions that do not negotiate authenticated encryption. public EsiurBuilder RequireEncryption() { Services.Configure(options => options.ServerConfigurations.Add(server => server.RequireEncryption = true)); return this; } /// /// Includes remote exception messages in addition to stable error codes. Prefer the /// code-only default in production because messages can contain application details. /// public EsiurBuilder IncludeExceptionMessages() { Services.Configure(options => options.ServerConfigurations.Add(server => server.ExceptionLevel |= Esiur.Core.ExceptionLevel.Code | Esiur.Core.ExceptionLevel.Message)); return this; } /// Limits copied outbound WebSocket data retained per connection. public EsiurBuilder LimitPendingWebSocketSendBytes(long maximumBytes) { if (maximumBytes <= 0) throw new ArgumentOutOfRangeException(nameof(maximumBytes)); Services.Configure(options => options.MaximumPendingWebSocketSendBytes = maximumBytes); return this; } /// Limits copied outbound WebSocket data retained by the whole Esiur host. public EsiurBuilder LimitTotalPendingWebSocketSendBytes(long maximumBytes) { if (maximumBytes <= 0) throw new ArgumentOutOfRangeException(nameof(maximumBytes)); Services.Configure(options => options.MaximumTotalPendingWebSocketSendBytes = maximumBytes); return this; } /// Configures the final Warehouse runtime limits. public EsiurBuilder ConfigureWarehouse(Action configure) { ArgumentNullException.ThrowIfNull(configure); Services.Configure(options => options.WarehouseConfigurations.Add(configure)); return this; } /// Adds an in-memory root store. public EsiurBuilder AddMemoryStore(string path = "sys") => AddResourceRegistration( path, _ => new MemoryStore(), reuseExistingStore: true); /// Adds a resource constructed through ASP.NET Core dependency injection. public EsiurBuilder AddResource(string path) where TResource : class, IResource => AddResourceRegistration( path, services => ActivatorUtilities.CreateInstance(services), reuseExistingStore: false); /// Adds an existing resource instance. public EsiurBuilder AddResource(string path, TResource resource) where TResource : class, IResource { ArgumentNullException.ThrowIfNull(resource); return AddResourceRegistration(path, _ => resource, reuseExistingStore: false); } /// Adds a resource created by an application service factory. public EsiurBuilder AddResource( string path, Func factory) where TResource : class, IResource { ArgumentNullException.ThrowIfNull(factory); return AddResourceRegistration( path, services => factory(services) ?? throw new InvalidOperationException( $"The resource factory for '{path}' returned null."), reuseExistingStore: false); } /// /// Registers and allows an authentication provider under its default protocol name. /// public EsiurBuilder UseAuthentication(IAuthenticationProvider provider) { ArgumentNullException.ThrowIfNull(provider); return UseAuthentication(_ => provider); } /// /// Registers and allows a dependency-injected authentication provider. /// public EsiurBuilder UseAuthentication() where TProvider : class, IAuthenticationProvider { Services.TryAddSingleton(); return UseAuthentication( services => services.GetRequiredService()); } /// /// Registers and allows an authentication provider factory under the provider's /// default protocol name. /// public EsiurBuilder UseAuthentication( Func factory) { ArgumentNullException.ThrowIfNull(factory); Services.Configure(options => options.AuthenticationProviders.Add(factory)); return this; } /// /// Adds server-side password authentication without requiring a custom provider /// class. The synchronous lookup should read a cached, precomputed Esiur password /// credential and return for an unknown account. /// public EsiurBuilder UsePasswordAuthentication( Func findCredential) { ArgumentNullException.ThrowIfNull(findCredential); return UsePasswordAuthentication( (_, identity, domain) => findCredential(identity, domain)); } /// /// Adds server-side password authentication with access to the application service /// provider. Resolve only singleton/cached services because authentication providers /// are host-scoped and credential lookup is synchronous. /// public EsiurBuilder UsePasswordAuthentication( Func findCredential) { ArgumentNullException.ThrowIfNull(findCredential); return UseAuthentication(services => new CallbackPasswordAuthenticationProvider( (identity, domain) => findCredential(services, identity, domain))); } /// Registers and allows an encryption provider under its default name. public EsiurBuilder UseEncryption(IEncryptionProvider provider) { ArgumentNullException.ThrowIfNull(provider); return UseEncryption(_ => provider); } /// Registers and allows a dependency-injected encryption provider. public EsiurBuilder UseEncryption() where TProvider : class, IEncryptionProvider { Services.TryAddSingleton(); return UseEncryption( services => services.GetRequiredService()); } /// /// Registers and allows an encryption provider factory under the provider's default /// protocol name. /// public EsiurBuilder UseEncryption( Func factory) { ArgumentNullException.ThrowIfNull(factory); Services.Configure(options => options.EncryptionProviders.Add(factory)); return this; } /// Allows browser WebSocket requests from the specified origins. public EsiurBuilder AllowWebSocketOrigins(params string[] origins) { ArgumentNullException.ThrowIfNull(origins); if (origins.Length == 0) throw new ArgumentException("At least one origin is required.", nameof(origins)); var normalized = origins.Select(EsiurOrigin.Normalize).ToArray(); Services.Configure(options => { foreach (var origin in normalized) options.AllowedWebSocketOrigins.Add(origin); }); return this; } /// Allows browser WebSocket requests from every origin. public EsiurBuilder AllowAnyWebSocketOrigin() { Services.Configure(options => options.AllowAnyWebSocketOrigin = true); return this; } private EsiurBuilder AddResourceRegistration( string path, Func factory, bool reuseExistingStore) { var normalizedPath = EsiurConfigurationPath.Normalize(path); Services.Configure(options => options.Resources.Add( new EsiurResourceRegistration(normalizedPath, factory, reuseExistingStore))); return this; } }