Modular Startup

INFO

For more information on the earlier Modular Startup in ServiceStack v5.x see our Legacy Modular Startup docs

Taking advantage of C# 9 top level statements and .NET 6 WebApplication Hosting Model, ServiceStack templates by utilize both these features to simplify configuring your AppHost in a modular way.

Program.cs becomes a script-like file since C# 9 top level statements are generating application entry point implicitly.

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
    app.UseHttpsRedirection();
}

app.UseServiceStack(new AppHost())
app.Run();

The application AppHost hooks into startup using HostingStartup assembly attribute. In ServiceStack templates, this uses the file name prefix of Configure.*.cs to help identify these startup modules.

All ServiceStack's features are loaded using .NET's HostingStartup, including ServiceStack's AppHost itself that's now being configured in Configure.AppHost.cs, e.g:

[assembly: HostingStartup(typeof(MyApp.AppHost))]

namespace MyApp;

public class AppHost() : AppHostBase("MyApp"), IHostingStartup
{
    public void Configure(IWebHostBuilder builder) => builder
        .ConfigureServices(services => {
            // Configure ASP.NET Core IOC Dependencies
        });

    public override void Configure()
    {
        // Configure ServiceStack, Run custom logic after ASP.NET Core Startup
        SetConfig(new HostConfig {
        });
    }
}

The use of Modular Startup does not change the AppHost declaration, but enables the modular grouping of configuration concerns. Different features are encapsulated together allowing them to be more easily updated or replaced, e.g. each feature could be temporarily disabled by commenting out its assembly HostingStartup's attribute:

//[assembly: HostingStartup(typeof(MyApp.AppHost))]

Module composition using mix

This has enabled ServiceStack Apps to be easily composed with the features developers need in mind. Either at project creation with servicestack.net/start page or after a project's creation where features can easily be added and removed using the command-line mix tool where you can view all available mix gists that can be added to projects with:

x mix

.NET 6's idiom is incorporated into the mix gist config files to adopt its HostingStartup which is better able to load modular Startup configuration without assembly scanning.

This is a standard ASP .NET Core feature that we can use to configure Mongo DB in any ASP .NET Core App with:

x mix mongodb

Which adds the mongodb gist file contents to your ASP .NET Core Host project:

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using MongoDB.Driver;

[assembly: HostingStartup(typeof(MyApp.ConfigureMongoDb))]

namespace MyApp;

public class ConfigureMongoDb : IHostingStartup
{
    public void Configure(IWebHostBuilder builder) => builder
        .ConfigureServices((context, services) => {
            var mongoClient = new MongoClient();
            IMongoDatabase mongoDatabase = mongoClient.GetDatabase("MyApp");
            services.AddSingleton(mongoDatabase);
        });
}    

As it's not a ServiceStack feature it can be used to configure ASP .NET Core Apps with any feature, e.g. we could also easily configure Marten in an ASP .NET Core App with:

x mix marten

The benefit of this approach is entire modules of features can be configured in a single command, e.g. An empty ServiceStack App can be configured with MongoDB, ServiceStack Auth and a MongoDB Auth Repository with a single command:

x mix auth auth-mongodb mongodb

Likewise, you can replace MongoDB with a completely different PostgreSQL RDBMS implementation by running:

x mix auth auth-db postgres

Services and App Customizations

Modular Startup configurations are flexible enough to encapsulate customizing ASP.NET Core's IOC and the built WebApplication by registering a IStartupFilter which is required by the Open API v3 Modular Configuration:

x mix openapi3

[assembly: HostingStartup(typeof(MyApp.ConfigureOpenApi))]

namespace MyApp;

public class ConfigureOpenApi : IHostingStartup
{
    public void Configure(IWebHostBuilder builder) => builder
        .ConfigureServices((context, services) =>
        {
            if (context.HostingEnvironment.IsDevelopment())
            {
                services.AddEndpointsApiExplorer();
                services.AddSwaggerGen();

                services.AddServiceStackSwagger();
                services.AddBasicAuth<Data.ApplicationUser>();
                //services.AddJwtAuth();

                services.AddTransient<IStartupFilter, StartupFilter>();
            }
        });

    public class StartupFilter : IStartupFilter
    {
        public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next) => app =>
        {
            app.UseSwagger();
            app.UseSwaggerUI();
            next(app);
        };
    }
}

ConfigureAppHost

Looking deeper, we can see where we're plugins are able to configure ServiceStack via the .ConfigureAppHost() extension method to execute custom logic on AppHost Startup:

[assembly: HostingStartup(typeof(MyApp.ConfigureAutoQuery))]

namespace MyApp;

public class ConfigureAutoQuery : IHostingStartup
{
    public void Configure(IWebHostBuilder builder) => builder
        .ConfigureServices(services => {
            // Enable Audit History
            services.AddSingleton<ICrudEvents>(c =>
                new OrmLiteCrudEvents(c.GetRequiredService<IDbConnectionFactory>()));

            // For TodosService
            services.AddPlugin(new AutoQueryDataFeature());

            // For Bookings https://docs.servicestack.net/autoquery-crud-bookings
            services.AddPlugin(new AutoQueryFeature
            {
                MaxLimit = 1000,
                //IncludeTotal = true,
            });
        })
        .ConfigureAppHost(appHost => {
            appHost.Resolve<ICrudEvents>().InitSchema();
        });
}

Customize AppHost at different Startup Lifecycles

By default, any AppHost configuration is called before AppHost.Configure() is run, but to cater for all plugins, AppHost configurations can be registered at different stages within the AppHost's initialization:

public void Configure(IWebHostBuilder builder) => builder
    .ConfigureAppHost(
        beforeConfigure:    appHost => /* fired before AppHost.Configure() */, 
        afterConfigure:     appHost => /* fired after AppHost.Configure() */,
        afterPluginsLoaded: appHost => /* fired after plugins are loaded */,
        afterAppHostInit:   appHost => /* fired after AppHost has initialized */);

Removing Features

The benefits of adopting a modular approach to AppHost configuration is the same as general organizational code structure which results in better decoupling and cohesion where it's easier to determine all the dependencies of a feature, easier to update, less chance of unintended side effects, easier to share standard configuration amongst multiple projects and easier to remove the feature entirely, either temporarily if needing to isolate & debug a runtime issue by:

// [assembly: HostingStartup(typeof(MyApp.ConfigureAuth))]

Or easier to permanently replace or remove features by either directly deleting the isolated *.cs source files or by undoing mixing in the feature using mix -delete, e.g:

x mix -delete auth auth-db postgres

Which works similar to package managers where it removes all files contained within each mix gist.

INFO

Please see the Mix HowTo to find out how you can contribute your own gist mix features

Migrating to HostingStartup

As we'll be using the new HostingStartup model going forward we recommend migrating your existing configuration to use them.

To help with this you can refer to the mix diff showing how each of the existing mix configurations were converted to the new model.

As a concrete example, lets take a look at the steps used to migrate our Chinook example application from NET5 using the previous Startup : ModularStartup, to .NET 6 HostingStartup.

Step 1

Migrate your existing ConfigureServices and Configure(IApplicationBuilder) from Startup : ModularStartup to the top-level host builder in Program.cs. Eg

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // The default HSTS value is 30 days. 
    // You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
    app.UseHttpsRedirection();
}

app.Run();

Step 2

Move your AppHost class to a new Configure.AppHost.cs file.

Step 3

Implement IHostingStartup on your AppHost with automatic initialization. Eg:

public void Configure(IWebHostBuilder builder)
{
    builder.ConfigureServices(services => {
        // Configure ASP.NET Core IOC Dependencies
    });
}

Step 4

Declare assembly: HostingStartup for your AppHost in the same Configure.AppHost.cs. Eg:

[assembly: HostingStartup(typeof(Chinook.AppHost))]

Step 5

Migrate each existing modular startup class that implements IConfgiureServices and/or IConfigureApp to use IHostingStartup. Eg:

// net5.0 modular startup
using ServiceStack;

namespace Chinook;

public class ConfigureAutoQuery : IConfigureAppHost
{
    public void Configure(IAppHost appHost)
    {
        appHost.Plugins.Add(new AutoQueryFeature {
            MaxLimit = 1000,
            IncludeTotal = true
        });
    }
}
// net8.0 modular startup using IHostingStartup
using Microsoft.AspNetCore.Hosting;
using ServiceStack;

[assembly: HostingStartup(typeof(Chinook.ConfigureAutoQuery))]

namespace Chinook;

public class ConfigureAutoQuery : IHostingStartup
{
    public void Configure(IWebHostBuilder builder) => builder
        .ConfigureServices(services => {
            services.AddPlugin(new AutoQueryFeature {
                MaxLimit = 1000,
                IncludeTotal = true
            });
        });
}

Remembering also that infrastructure like your Dockerfile or host will likely need the runtimes/SDKs updated as well.