SlashPineTech.Forestry.ServiceModules 2.0.0

.NET 6.0
NuGet\Install-Package SlashPineTech.Forestry.ServiceModules -Version 2.0.0
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
dotnet add package SlashPineTech.Forestry.ServiceModules --version 2.0.0
<PackageReference Include="SlashPineTech.Forestry.ServiceModules" Version="2.0.0" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add SlashPineTech.Forestry.ServiceModules --version 2.0.0
#r "nuget: SlashPineTech.Forestry.ServiceModules, 2.0.0"
#r directive can be used in F# Interactive, C# scripting and .NET Interactive. Copy this into the interactive tool or source code of the script to reference the package.
// Install SlashPineTech.Forestry.ServiceModules as a Cake Addin
#addin nuget:?package=SlashPineTech.Forestry.ServiceModules&version=2.0.0

// Install SlashPineTech.Forestry.ServiceModules as a Cake Tool
#tool nuget:?package=SlashPineTech.Forestry.ServiceModules&version=2.0.0

MIT License

Forestry .NET -- Service Modules

Forestry .NET is a set of open-source libraries for building modern web applications using ASP.NET Core.

This service modules package adds support for configuring services using an object-oriented approach.

Usage

Simple Example

Getting started with Service Modules is easy. Create a class that implements IServiceModule, like so:

public class BugsnagModule : IServiceModule
{
    public void Configure(IServiceCollection services, IServiceConfigurationContext ctx)
    {
        services.AddBugsnag(configuration =>
        {
            configuration.ApiKey = "YourApiKeyGoesHere";
            configuration.ReleaseStage = ctx.Environment.EnvironmentName;
        });
    }
}

Then, in your Startup class, add the following:

services.AddModules(typeof(Startup).Assembly, Environment, Configuration)
    .AddModule<BugsnagModule>();

That's the simplest use of service modules. However, we included our API key above in the code, which is a no-no. Let's look at how to bind our configuration to a module to keep the API key safe.

Configuration Binding

Continuing with our Bugsnag example above, let's consider a configuration section in our appsettings.json.

{
  "Bugsnag": {
    "ApiKey": "YourApiKeyGoesHere"
  }
}

We can tell the service modules system to bind that configuration to our module by adding the name of the config section to the AddModule<T>() call.

services.AddModules(typeof(Startup).Assembly, Environment, Configuration)
    .AddModule<BugsnagModule>("Bugsnag");

Let's revisit the example module and add a new property to bind to.

public class BugsnagModule : IServiceModule
{
    public string? ApiKey { get; set; }

    public void Configure(IServiceCollection services, IServiceConfigurationContext ctx)
    {
        if (!string.IsNullOrEmpty(ApiKey))
        {
            services.AddBugsnag(configuration =>
            {
                configuration.ApiKey = ApiKey;
                configuration.ReleaseStage = ctx.Environment.EnvironmentName;
            });
        }
    }
}

The value of our ApiKey JSON property will be bound to the ApiKey property in our module. If we don't set an API key, we can check that the property is not null and disable Bugsnag integration.

Configuration Validation

In the Bugsnag example, we didn't configure our services if the API key was null. What if we need a configuration property to be present. Let's consider an example that configures EF core.

{
  "Database": {
    "ConnectionString": "YourSqlServerConnectionStringGoesHere;",
    "EnableSensitiveDataLogging": true
  }
}
public class DatabaseModule : IServiceModule
{
    [Required]
    public string? ConnectionString { get; set; }

    public bool EnableSensitiveDataLogging { get; set; } = false;

    public void Configure(IServiceCollection services, IServiceConfigurationContext ctx)
    {
        services.AddDbContext<ApplicationDbContext>(options =>
        {
            options.UseSqlServer(ConnectionString!)
                .EnableSensitiveDataLogging(EnableSensitiveDataLogging);
        });
    }
}

You can use any validation attributes on your properties in a module to ensure that your configuration values are provided. If validation fails, startup will halt, and error messages will be written to the log informing you of the validation errors.

services.AddModules(typeof(Startup).Assembly, Environment, Configuration)
    .AddModule<BugsnagModule>("Bugsnag")
    .AddModule<DatabaseModule>("Database");

Polymorphic Configuration Binding

So far, so good, right? But what if you want to bind to a different module at runtime based on our configuration? Let's consider an example for sending email via SMTP or an HTTP-based service like SendGrid.

Our configuration could like like either of these:

{
  "Email": {
    "Type": "Smtp",
    "FromName": "My Web App",
    "FromAddress": "no-reply@example.com",
    "Hostname": "localhost",
    "Port": 25
  }
}
{
  "Email": {
    "Type": "SendGrid",
    "FromName": "My Web App",
    "FromAddress": "no-reply@example.com",
    "ApiKey": "YourSendGridApiKey"
  }
}

Next, let's look at our modules.

[ServiceModuleInfo(DefaultImpl = typeof(SmtpEmailModule))]
public abstract class EmailModule : IServiceModule
{
    [Required]
    public string? FromName { get; set; }

    [Required]
    [EmailAddress]
    public string? FromAddress { get; set; }

    public abstract void Configure(IServiceCollection services, IServiceConfigurationContext ctx);
}
[ServiceModuleName("Smtp")]
public class SmtpEmailModule : EmailModule
{
    [Required]
    public string? Host { get; set; }

    public int Port { get; set; } = 25;

    public override void Configure(IServiceCollection services, IServiceConfigurationContext ctx)
    {
        services.AddSingleton(_ => new SmtpSettings(
            new EmailAddress(FromAddress!, FromName),
            Host!,
            Port
        ));
        services.AddTransient<IEmailSender, SmtpEmailSender>();
    }
}
[ServiceModuleName("SendGrid")]
public class SendGridEmailModule : EmailModule
{
    [Required]
    public string? ApiKey { get; set; }

    public override void Configure(IServiceCollection services, IServiceConfigurationContext ctx)
    {
        services.AddSingleton(_ => new SendGridSettings(
            new EmailAddress(FromAddress!, FromName),
            ApiKey!
        ));

        services.AddTransient<IEmailSender, SendGridEmailSender>();
    }
}

Then we register our module in Startup.

services.AddModules(typeof(Startup).Assembly, Environment, Configuration)
    .AddModule<BugsnagModule>("Bugsnag")
    .AddModule<DatabaseModule>("Database")
    .AddModule<EmailModule>("Email");

When registering a polymorphic module, we peek at the Type property to see which implementation to bind to.

Product Versions
.NET net6.0 net6.0-android net6.0-ios net6.0-maccatalyst net6.0-macos net6.0-tvos net6.0-windows
Compatible target framework(s)
Additional computed target framework(s)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
2.0.0 486 7/21/2022
1.0.0 1,324 3/31/2022