Refactor service registration for modularity

Introduced `EnvelopeGeneratorOptions` and `SqlCacheOptions` to enable fine-grained control over optional service registrations in `AddEnvelopeGenerator`. Updated `AddEnvelopeGenerator` to conditionally register services like `HttpContextAccessor`, `DistributedSqlServerCache`, `Dispatcher`, `MemoryCache`, and `UserManager` based on these options.

Updated `DependencyInjection.csproj` to include necessary framework references and package dependencies. Simplified `Program.cs` by consolidating service registrations into `AddEnvelopeGenerator`, reducing boilerplate and improving maintainability.

Improved extensibility by centralizing service registration logic, allowing consuming projects to customize configurations without modifying the core library. Updated documentation and removed unused directives.
This commit is contained in:
2026-05-28 21:57:03 +02:00
parent bc4905d2f4
commit 0a4daccc0f
3 changed files with 119 additions and 71 deletions

View File

@@ -4,12 +4,59 @@ using EnvelopeGenerator.Application;
using EnvelopeGenerator.Application.Common.Interfaces.Services;
using EnvelopeGenerator.Application.Services;
using EnvelopeGenerator.Infrastructure;
using DigitalData.EmailProfilerDispatcher;
using DigitalData.UserManager.DependencyInjection;
namespace EnvelopeGenerator.DependencyInjection;
/// <summary>
/// Controls which optional services are registered by <see cref="DependencyInjection.AddEnvelopeGenerator"/>.
/// All flags default to <c>true</c>. Set a flag to <c>false</c> if the consuming project
/// already registers that service itself or simply does not need it.
/// </summary>
public sealed class EnvelopeGeneratorOptions
{
/// <summary>Calls <c>AddHttpContextAccessor()</c>. Default: <c>true</c>.</summary>
public bool AddHttpContextAccessor { get; set; } = true;
/// <summary>
/// Calls <c>AddDistributedSqlServerCache()</c> with the supplied <see cref="SqlCacheOptions"/>.
/// Requires <see cref="SqlCacheOptions"/> to be configured. Default: <c>true</c>.
/// </summary>
public bool AddDistributedSqlServerCache { get; set; } = true;
/// <summary>
/// Options for the distributed SQL Server cache.
/// Required when <see cref="AddDistributedSqlServerCache"/> is <c>true</c>.
/// </summary>
public SqlCacheOptions? SqlCacheOptions { get; set; }
/// <summary>Calls <c>AddDispatcher&lt;TDbContext&gt;()</c>. Default: <c>true</c>.</summary>
public bool AddDispatcher { get; set; } = true;
/// <summary>Calls <c>AddMemoryCache()</c>. Default: <c>true</c>.</summary>
public bool AddMemoryCache { get; set; } = true;
/// <summary>Calls <c>AddUserManager&lt;TDbContext&gt;()</c>. Default: <c>true</c>.</summary>
public bool AddUserManager { get; set; } = true;
}
/// <summary>Options for <c>AddDistributedSqlServerCache</c>.</summary>
public sealed class SqlCacheOptions
{
/// <summary>SQL Server connection string.</summary>
public string ConnectionString { get; set; } = string.Empty;
/// <summary>Schema name. Default: <c>dbo</c>.</summary>
public string SchemaName { get; set; } = "dbo";
/// <summary>Table name. Default: <c>TBDD_CACHE</c>.</summary>
public string TableName { get; set; } = "TBDD_CACHE";
}
/// <summary>
/// Extension methods for registering EnvelopeGenerator services into an <see cref="IServiceCollection"/>.
/// Use <see cref="AddEnvelopeGenerator"/> as the single entry-point for projects that need both the
/// Use <see cref="AddEnvelopeGenerator{TDbContext}"/> as the single entry-point for projects that need both the
/// application layer (MediatR, AutoMapper, CRUD services, configuration sections) and the infrastructure
/// layer (repositories, DbContext, SQL executors).
/// For projects that do not need a database (e.g. lightweight API gateways or unit-test hosts), use
@@ -18,61 +65,56 @@ namespace EnvelopeGenerator.DependencyInjection;
public static class DependencyInjection
{
/// <summary>
/// Registers the full EnvelopeGenerator stack application <em>and</em> infrastructure services into
/// the provided <see cref="IServiceCollection"/>.
/// <para>
/// Internally this calls <c>AddEnvelopeGeneratorServices</c> (application layer) and
/// <c>AddEnvelopeGeneratorInfrastructureServices</c> (infrastructure layer).
/// A <see cref="Microsoft.EntityFrameworkCore.DbContext"/> and / or <c>DbTriggerParams</c> must be
/// configured through <paramref name="infrastructureOptions"/>; without it no database connection will
/// be established at runtime.
/// </para>
/// Registers the full EnvelopeGenerator stack using <see cref="EGDbContext"/> as the DbContext type.
/// </summary>
/// <param name="services">Service collection to register services into.</param>
/// <param name="configuration">
/// Application configuration. Used to bind <c>DispatcherParams</c>, <c>MailParams</c>,
/// <c>AuthenticatorParams</c>, <c>TotpSmsParams</c>, <c>GtxMessagingParams</c> and other
/// application-level option sections.
/// </param>
/// <param name="infrastructureOptions">
/// Optional callback to configure the infrastructure layer registration.
/// Typical usage:
/// <code>
/// services.AddEnvelopeGenerator(config, opt =>
/// {
/// opt.AddDbContext(o => o.UseSqlServer(connectionString));
/// opt.AddDbTriggerParams(config);
/// });
/// </code>
/// </param>
/// <returns>The updated <see cref="IServiceCollection"/>.</returns>
#pragma warning disable CS0618 // AddEnvelopeGeneratorServices / AddEnvelopeGeneratorInfrastructureServices are intentionally wrapped here
public static IServiceCollection AddEnvelopeGenerator(
this IServiceCollection services,
IConfiguration configuration,
Action<EnvelopeGenerator.Infrastructure.DependencyInjection.Config>? infrastructureOptions = null)
Action<EnvelopeGenerator.Infrastructure.DependencyInjection.Config>? infrastructureOptions = null,
Action<EnvelopeGeneratorOptions>? options = null)
{
var opt = new EnvelopeGeneratorOptions();
options?.Invoke(opt);
#pragma warning disable CS0618
// Application layer: CRUD services, MediatR, AutoMapper, configuration sections.
services.AddEnvelopeGeneratorServices(configuration);
// Infrastructure layer: repositories, DbContext, Dapper type maps, SQL executors.
services.AddEnvelopeGeneratorInfrastructureServices(opt =>
services.AddEnvelopeGeneratorInfrastructureServices(cfg =>
{
infrastructureOptions?.Invoke(opt);
infrastructureOptions?.Invoke(cfg);
});
#pragma warning restore CS0618
if (opt.AddHttpContextAccessor)
services.AddHttpContextAccessor();
if (opt.AddDistributedSqlServerCache && opt.SqlCacheOptions is { } cacheOpts)
services.AddDistributedSqlServerCache(o =>
{
o.ConnectionString = cacheOpts.ConnectionString;
o.SchemaName = cacheOpts.SchemaName;
o.TableName = cacheOpts.TableName;
});
if (opt.AddDispatcher)
services.AddDispatcher<EGDbContext>();
if (opt.AddMemoryCache)
services.AddMemoryCache();
#pragma warning disable CS0618
if (opt.AddUserManager)
services.AddUserManager<EGDbContext>();
#pragma warning restore CS0618
return services;
}
#pragma warning restore CS0618
/// <summary>
/// Registers only the <em>application</em> layer services (MediatR handlers, AutoMapper profiles,
/// CRUD services, configuration sections) without any infrastructure / database dependencies.
/// <para>
/// Useful for projects that already manage their own DbContext or do not require direct database
/// access, such as lightweight API gateways, console tools or unit/integration test hosts that
/// use an in-memory database configured elsewhere.
/// </para>
/// </summary>
/// <param name="services">Service collection to register services into.</param>
/// <param name="configuration">Application configuration used to bind application-level option sections.</param>
@@ -90,11 +132,6 @@ public static class DependencyInjection
/// <summary>
/// Registers <see cref="EnvelopeMailService"/> as the <see cref="IEnvelopeMailService"/> scoped
/// implementation.
/// <para>
/// Call this in addition to <see cref="AddEnvelopeGenerator"/> when the consuming project needs to
/// send envelope e-mails directly (e.g. a Worker Service or the Web project). Projects that rely
/// purely on MediatR commands to trigger mail delivery do not need to call this.
/// </para>
/// </summary>
/// <param name="services">Service collection to register services into.</param>
/// <returns>The updated <see cref="IServiceCollection"/>.</returns>

View File

@@ -25,9 +25,28 @@
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
</PropertyGroup>
<!-- ASP.NET Core shared framework (AddHttpContextAccessor, AddMemoryCache, etc.) -->
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.6" />
<PackageReference Include="UserManager" Version="1.1.3" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="7.0.20" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="8.0.17" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="9.0.6" />
</ItemGroup>
<ItemGroup>

View File

@@ -12,7 +12,6 @@ using DigitalData.EmailProfilerDispatcher;
using EnvelopeGenerator.Infrastructure;
using EnvelopeGenerator.Web.Sanitizers;
using EnvelopeGenerator.Web.Models.Annotation;
using DigitalData.UserManager.DependencyInjection;
using EnvelopeGenerator.Web.Middleware;
using EnvelopeGenerator.DependencyInjection;
using EnvelopeGenerator.Web;
@@ -55,8 +54,6 @@ try
});
});
builder.Services.AddHttpContextAccessor();
builder.ConfigureBySection<TFARegParams>();
// Add controllers and razor views
@@ -93,26 +90,29 @@ try
var connStr = config.GetConnectionString(cnnStrName)
?? throw new InvalidOperationException($"Connection string '{cnnStrName}' is missing in the application configuration.");
builder.Services.AddDistributedSqlServerCache(options =>
{
options.ConnectionString = connStr;
options.SchemaName = "dbo";
options.TableName = "TBDD_CACHE";
});
// Add envelope generator services
builder.Services.AddEnvelopeGenerator(config, opt =>
{
opt.AddDbTriggerParams(config);
opt.AddDbContext((provider, options) =>
builder.Services.AddEnvelopeGenerator(config,
infrastructureOptions: opt =>
{
var logger = provider.GetRequiredService<ILogger<EGDbContext>>();
options.UseSqlServer(connStr)
.LogTo(log => logger.LogInformation("{log}", log), Microsoft.Extensions.Logging.LogLevel.Trace)
.EnableSensitiveDataLogging()
.EnableDetailedErrors();
opt.AddDbTriggerParams(config);
opt.AddDbContext((provider, options) =>
{
var logger = provider.GetRequiredService<ILogger<EGDbContext>>();
options.UseSqlServer(connStr)
.LogTo(log => logger.LogInformation("{log}", log), Microsoft.Extensions.Logging.LogLevel.Trace)
.EnableSensitiveDataLogging()
.EnableDetailedErrors();
});
},
options: opt =>
{
opt.SqlCacheOptions = new()
{
ConnectionString = connStr,
SchemaName = "dbo",
TableName = "TBDD_CACHE"
};
});
});
builder.Services.Configure<CookiePolicyOptions>(options =>
{
@@ -162,18 +162,10 @@ try
// Register mail services
builder.Services.AddEnvelopeMailService();
builder.Services.AddDispatcher<EGDbContext>();
builder.Services.AddMemoryCache();
builder.ConfigureBySection<CustomImages>();
builder.ConfigureBySection<AnnotationParams>();
#pragma warning disable CS0618 // Type or member is obsolete
builder.Services.AddUserManager<EGDbContext>();
#pragma warning restore CS0618 // Type or member is obsolete
var app = builder.Build();
app.UseMiddleware<ExceptionHandlingMiddleware>();