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;
}
}