ServiceStack Endpoint Routing

ServiceStack Endpoint Routing Background
15 min read

In an effort to reduce friction and improve integration with ASP.NET Core Apps, we've continued the trend from last year for embracing ASP.NET Core's built-in features and conventions which saw the latest ServiceStack v8 release converting all its newest .NET 8 templates to adopt ASP.NET Core Identity Auth.

This is a departure from building upon our own platform-agnostic abstractions which allowed the same ServiceStack code-base to run on both .NET Core and .NET Framework. Our focus going forward will be to instead adopt De facto standards and conventions of the latest .NET platform which also means ServiceStack's new value-added features are only available in the latest .NET 8+ release.

ServiceStack Middleware

Whilst ServiceStack integrates into ASP.NET Core Apps as custom middleware into ASP.NET Core's HTTP Request Pipeline, it invokes its own black-box of functionality from there, implemented using its own suite of overlapping features.

Whilst this allows ServiceStack to have full control over how to implement its features, it's not as integrated as it could be, with there being limits on what ServiceStack Functionality could be reused within external ASP .NET Core MVC Controllers, Razor Pages, etc. and inhibited the ability to apply application-wide authorization policies across an Application entire surface area, using and configuring different JSON Serialization implementations.

Areas for tighter integration

The major areas we've identified that would benefit from tighter integration with ASP.NET Core include:

ServiceStack v8.1 is fully integrated!

We're happy to announce the latest release of ServiceStack v8.1 now supports utilizing the optimal ASP.NET Core's standardized features to reimplement all these key areas - fostering seamless integration and greater reuse which you can learn about below:

Better yet, this new behavior is enabled by default in all of ServiceStack's new ASP .NET Identity Auth .NET 8 templates!

Migrating to ASP.NET Core Endpoints

To assist ServiceStack users in upgrading their existing projects we've created a migration guide walking through the steps required to adopt these new defaults:

ASP .NET Core IOC

The primary limitation of ServiceStack using its own Funq IOC is that any dependencies registered in Funq are not injected into Razor Pages, Blazor Components, MVC Controllers, etc.

That's why our Modular Startup configurations recommend utilizing custom IHostingStartup configurations to register application dependencies in ASP .NET Core's IOC where they can be injected into both ServiceStack Services and ASP.NET Core's external components, e.g:

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

namespace MyApp;

public class ConfigureDb : IHostingStartup
{
    public void Configure(IWebHostBuilder builder) => builder
        .ConfigureServices((context, services) => {
            services.AddSingleton<IDbConnectionFactory>(new OrmLiteConnectionFactory(
                context.Configuration.GetConnectionString("DefaultConnection"),
                SqliteDialect.Provider));
        });
}

But there were fundamental restrictions on what could be registered in ASP .NET Core's IOC as everything needed to be registered before AspNetCore's WebApplication was built and before ServiceStack's AppHost could be initialized, which prohibited being able to register any dependencies created by the AppHost including Services, AutoGen Services, Validators and internal functionality like App Settings, Virtual File System and Caching providers, etc.

Switch to use ASP .NET Core IOC

To enable ServiceStack to switch to using ASP .NET Core's IOC you'll need to move registration of all dependencies and Services to before the WebApplication is built by calling the AddServiceStack() extension method with the Assemblies where your ServiceStack Services are located, e.g:

builder.Services.AddServiceStack(typeof(MyServices).Assembly);

var app = builder.Build();

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

Which now registers all ServiceStack dependencies in ASP .NET Core's IOC, including all ServiceStack Services prior to the AppHost being initialized which no longer needs to specify the Assemblies where ServiceStack Services are created and no longer needs to use Funq as all dependencies should now be registered in ASP .NET Core's IOC.

Registering Dependencies and Plugins

Additionally ASP.NET Core's IOC requirement for all dependencies needing to be registered before the WebApplication is built means you'll no longer be able to register any dependencies or plugins in ServiceStack's AppHost.Configure() method.

public class AppHost() : AppHostBase("MyApp"), IHostingStartup
{
    public void Configure(IWebHostBuilder builder) => builder
        .ConfigureServices(services => {
            // Register IOC Dependencies and ServiceStack Plugins
        });

    public override void Configure()
    {
        // DO NOT REGISTER ANY PLUGINS OR DEPENDENCIES HERE
    }
}

Instead anything that needs to register dependencies in ASP.NET Core IOC should now use the IServiceCollection extension methods:

  • Use IServiceCollection.Add* APIs to register dependencies
  • Use IServiceCollection.AddPlugin API to register ServiceStack Plugins
  • Use IServiceCollection.RegisterService* APIs to dynamically register ServiceStack Services in external Assemblies

This can be done whenever you have access to IServiceCollection, either in Program.cs:

builder.Services.AddPlugin(new AdminDatabaseFeature());

Or in any Modular Startup IHostingStartup configuration class, e.g:

public class ConfigureDb : IHostingStartup
{
    public void Configure(IWebHostBuilder builder) => builder
        .ConfigureServices((context, services) => {
            services.AddSingleton<IDbConnectionFactory>(new OrmLiteConnectionFactory(
                context.Configuration.GetConnectionString("DefaultConnection"),
                SqliteDialect.Provider));
            
            // Enable Audit History
            services.AddSingleton<ICrudEvents>(c =>
                new OrmLiteCrudEvents(c.GetRequiredService<IDbConnectionFactory>()));
            
            // Enable AutoQuery RDBMS APIs
            services.AddPlugin(new AutoQueryFeature {
                 MaxLimit = 1000,
            });

            // Enable AutoQuery Data APIs
            services.AddPlugin(new AutoQueryDataFeature());
            
            // Enable built-in Database Admin UI at /admin-ui/database
            services.AddPlugin(new AdminDatabaseFeature());
        })
        .ConfigureAppHost(appHost => {
            appHost.Resolve<ICrudEvents>().InitSchema();
        });
}

The ConfigureAppHost() extension method can continue to be used to execute any startup logic that requires access to registered dependencies.

Authoring ServiceStack Plugins

To enable ServiceStack Plugins to support both Funq and ASP .NET Core IOC, any dependencies and Services a plugin needs should be registered in the IConfigureServices.Configure(IServiceCollection) method as seen in the refactored ServerEventsFeature.cs plugin, e.g:

public class ServerEventsFeature : IPlugin, IConfigureServices
{
    //...
    public void Configure(IServiceCollection services)
    {
        if (!services.Exists<IServerEvents>())
        {
            services.AddSingleton<IServerEvents>(new MemoryServerEvents
            {
                IdleTimeout = IdleTimeout,
                HouseKeepingInterval = HouseKeepingInterval,
                OnSubscribeAsync = OnSubscribeAsync,
                OnUnsubscribeAsync = OnUnsubscribeAsync,
                OnUpdateAsync = OnUpdateAsync,
                NotifyChannelOfSubscriptions = NotifyChannelOfSubscriptions,
                Serialize = Serialize,
                OnError = OnError,
            });
        }
        
        if (UnRegisterPath != null)
            services.RegisterService<ServerEventsUnRegisterService>(UnRegisterPath);

        if (SubscribersPath != null)
            services.RegisterService<ServerEventsSubscribersService>(SubscribersPath);
    }

    public void Register(IAppHost appHost)
    {
        //...
    }
}

All Plugins refactored to support ASP .NET Core IOC

All of ServiceStack's Plugins have been refactored to make use of IConfigureServices which supports registering in both Funq and ASP.NET Core's IOC when enabled.

Funq IOC implements IServiceCollection and IServiceProvider interfaces

To enable this Funq now implements both IServiceCollection andIServiceProvider interfaces to enable 100% source-code compatibility for registering and resolving dependencies with either IOC, which we now recommend using over Funq's native Registration and Resolution APIs to simplify migration efforts to ASP.NET Core's IOC in future.

Dependency Injection

The primary difference between the IOC's is that ASP.NET Core's IOC does not support property injection by default, which will require you to refactor your ServiceStack Services to use constructor injection of dependencies, although this has become a lot more pleasant with C# 12's Primary Constructors which now requires a lot less boilerplate to define, assign and access dependencies, e.g:

public class TechStackServices(IAutoQueryDb autoQuery) : Service
{
    public async Task<object> Any(QueryTechStacks request)
    {
        using var db = autoQuery.GetDb(request, base.Request);
        var q = autoQuery.CreateQuery(request, Request, db);
        return await autoQuery.ExecuteAsync(request, q, db);
    }
}

This has become our preferred approach for injecting dependencies in ServiceStack Services which have all been refactored to use constructor injection utilizing primary constructors in order to support both IOC's.

To make migrations easier we've also added support for property injection convention in ServiceStack Services using ASP.NET Core's IOC where you can add the [FromServices] attribute to any public properties you want to be injected, e.g:

public class TechStackServices : Service
{
    [FromServices]
    public required IAutoQueryDb AutoQuery { get; set; }

    [FromServices]
    public MyDependency? OptionalDependency { get; set; }
}

This feature can be useful for Services wanting to access optional dependencies that may or may not be registered.

NOTE

[FromServices] is only supported in ServiceStack Services (i.e. not other dependencies)

Built-in ServiceStack Dependencies

This integration now makes it effortless to inject and utilize optional ServiceStack features like AutoQuery and Server Events in other parts of ASP.NET Core inc. Blazor Components, Razor Pages, MVC Controllers, Minimal APIs, etc.

Whilst the Built-in ServiceStack features that are registered by default and immediately available to be injected, include:

  • IVirtualFiles - Read/Write Virtual File System, defaults to FileSystemVirtualFiles at ContentRootPath
  • IVirtualPathProvider - Multi Virtual File System configured to scan multiple read only sources, inc WebRootPath, In Memory and Embedded Resource files
  • ICacheClient and ICacheClientAsync - In Memory Cache, or distributed Redis cache if ServiceStack.Redis is configured
  • IAppSettings - Multiple AppSettings configuration sources

With ASP.NET Core's IOC now deeply integrated we moved onto the next area of integration: API Integration and Endpoint Routing.

Endpoint Routing

Whilst ASP.NET Core's middleware is a flexible way to compose and execute different middleware in a HTTP Request pipeline, each middleware is effectively their own island of functionality that's able to handle HTTP Requests in which ever way they see fit.

In particular ServiceStack's middleware would execute its own Request Pipeline which would execute ServiceStack API's registered at user-defined routes with its own ServiceStack Routing.

We're happy to announce that ServiceStack .NET 8 Apps support an entirely new and integrated way to run all of ServiceStack requests including all APIs, metadata and built-in UIs with support for ASP.NET Core Endpoint Routing - enabled by calling the MapEndpoints() extension method when configuring ServiceStack, e.g:

app.UseServiceStack(new AppHost(), options => {
    options.MapEndpoints();
});

Which configures ServiceStack APIs to be registered and executed along-side Minimal APIs, Razor Pages, SignalR, MVC and Web API Controllers, etc, utilizing the same routing, metadata and execution pipeline.

View ServiceStack APIs along-side ASP.NET Core APIs

Amongst other benefits, this integration is evident in endpoint metadata explorers like the Swashbuckle library which can now show ServiceStack APIs in its Swagger UI along-side other ASP.NET Core APIs in ServiceStack's new Open API v3 support.

Routing

Using Endpoint Routing also means using ASP.NET Core's Routing System which now lets you use ASP.NET Core's Route constraints for defining user-defined routes for your ServiceStack APIs, e.g:

[Route("/users/{Id:int}")]
[Route("/users/{UserName:string}")]
public class GetUser : IGet, IReturn<User>
{
    public int? Id { get; set; }
    public int? UserName { get; set; }
}

For the most part ServiceStack Routing implements a subset of ASP.NET Core's Routing features so your existing user-defined routes should continue to work as expected.

Wildcard Routes

The only incompatibility we found was when using wildcard paths which in ServiceStack Routing would use an '*' suffix, e.g: [Route("/wildcard/{Path*}")] which will need to change to use a ASP.NET Core's Routing prefix, e.g:

[Route("/wildcard/{*Path}")]
[Route("/wildcard/{**Path}")]
public class GetFile : IGet, IReturn<byte[]>
{
    public string Path { get; set; }
}

ServiceStack Routing Compatibility

To improve compatibility with ASP.NET Core's Routing, ServiceStack's Routing (when not using Endpoint Routing) now supports parsing ASP.NET Core's Route Constraints but as they're inert you would need to continue to use Custom Route Rules to distinguish between different routes matching the same path at different specificity:

[Route("/users/{Id:int}", Matches = "**/{int}")]
[Route("/users/{UserName:string}")]
public class GetUser : IGet, IReturn<User>
{
    public int? Id { get; set; }
    public int? UserName { get; set; }
}

It also supports defining Wildcard Routes using ASP.NET Core's syntax which we now recommend using instead for compatibility when switching to use Endpoint Routing:

[Route("/wildcard/{*Path}")]
[Route("/wildcard/{**Path}")]
public class GetFile : IGet, IReturn<byte[]>
{
    public string Path { get; set; }
}

Primary HTTP Method

Another difference is that an API will only register its Endpoint Route for its primary HTTP Method, if you want an API to be registered for multiple HTTP Methods you can specify them in the Route attribute, e.g:

[Route("/users/{Id:int}", "GET,POST")]
public class GetUser : IGet, IReturn<User>
{
    public required int Id { get; set; }
}

As such we recommend using the IVerb IGet, IPost, IPut, IPatch, IDelete interface markers to specify the primary HTTP Method for an API. This isn't needed for AutoQuery Services which are implicitly configured to use their optimal HTTP Method.

If no HTTP Method is specified, the Primary HTTP Method defaults to HTTP POST.

Authorization

Using Endpoint Routing also means ServiceStack's APIs are authorized the same way, where ServiceStack's Declarative Validation attributes are converted into ASP.NET Core's [Authorize] attribute to secure the endpoint:

[ValidateIsAuthenticated]
[ValidateIsAdmin]
[ValidateHasRole(role)]
[ValidateHasClaim(type,value)]
[ValidateHasScope(scope)]
public class Secured {}

Authorize Attribute on ServiceStack APIs

Alternatively you can now use ASP.NET Core's [Authorize] attribute directly to secure ServiceStack APIs should you need more fine-grained Authorization:

[Authorize(Roles = "RequiredRole")]
[Authorize(Policy = "RequiredPolicy")]
[Authorize(AuthenticationSchemes = "Identity.Application,Bearer")]
public class Secured {}

Configuring Authentication Schemes

ServiceStack will default to using the major Authentication Schemes configured for your App to secure the APIs endpoint with, this can be overridden to specify which Authentication Schemes to use to restrict ServiceStack APIs by default, e.g:

app.UseServiceStack(new AppHost(), options => {
    options.AuthenticationSchemes = "Identity.Application,Bearer";
    options.MapEndpoints();
});

Hidden ServiceStack Endpoints

Whilst ServiceStack Requests are registered and executed as endpoints, most of them are marked with builder.ExcludeFromDescription() to hide them from polluting metadata and API Explorers like Swagger UI and API Explorer.

To also hide your ServiceStack APIs you can use [ExcludeMetadata] attribute to hide them from all metadata services or use [Exclude(Feature.ApiExplorer)] to just hide them from API Explorer UIs:

[ExcludeMetadata]
[Exclude(Feature.ApiExplorer)]
public class HiddenRequest {}

Content Negotiation

An example of these hidden routes is the support for invoking and returning ServiceStack APIs in different Content Types via hidden Endpoint Routes mapped with the format /api/{Request}.{format}, e.g:

Query String Format

That continues to support specifying the Mime Type via the ?format query string, e.g:

Predefined Routes

Endpoints are only created for the newer /api/{Request} pre-defined routes, which should be easier to use with less conflicts now that ServiceStack APIs are executed along-side other endpoint routes APIs which can share the same /api base path with non-conflicting routes, e.g: app.MapGet("/api/minimal-api").

As a result clients configured to use the older /json/reply/{Request} pre-defined route will need to be configured to use the newer /api base path.

No change is required for C#/.NET clients using the recommended JsonApiClient JSON Service Client which is already configured to use the newer /api base path.

var client = new JsonApiClient(baseUri);

Older .NET clients can be configured to use the newer /api pre-defined routes with:

var client = new JsonServiceClient(baseUri) {
    UseBasePath = "/api"
};
var client = new JsonHttpClient(baseUri) {
    UseBasePath = "/api"
};

To further solidify that /api as the preferred pre-defined route we've also updated all generic service clients of other languages to use /api base path by default:

JavaScript/TypeScript

const client = new JsonServiceClient(baseUrl)

Dart

var client = ClientFactory.api(baseUrl);

Java/Kotlin

JsonServiceClient client = new JsonServiceClient(baseUrl);

Python

client = JsonServiceClient(baseUrl)

PHP

$client = new JsonServiceClient(baseUrl);

Revert to Legacy Predefined Routes

You can unset the base path to revert back to using the older /json/reply/{Request} pre-defined route, e.g:

JavaScript/TypeScript

client.basePath = null;

Dart

var client = ClientFactory.create(baseUrl);

Java/Kotlin

client.setBasePath();

Python

client.set_base_path()

PHP

$client->setBasePath();

Customize Endpoint Mapping

You can register a RouteHandlerBuilders to customize how ServiceStack APIs endpoints are registered which is also what ServiceStack uses to annotate its API endpoints to enable its new Open API v3 support:

options.RouteHandlerBuilders.Add((builder, operation, method, route) =>
{
    builder.WithOpenApi(op => { ... });
});

Endpoint Routing Compatibility Levels

The default behavior of MapEndpoints() is the strictest and recommended configuration that we want future ServiceStack Apps to use, however if you're migrating existing App's you may want to relax these defaults to improve compatibility with existing behavior.

The configurable defaults for mapping endpoints are:

app.UseServiceStack(new AppHost(), options => {
    options.MapEndpoints(use:true, force:true, useSystemJson:UseSystemJson.Always);
});
  • use - Whether to use registered endpoints for executing ServiceStack APIs
  • force - Whether to only allow APIs to be executed through endpoints
  • useSystemJson - Whether to use System.Text.Json for JSON API Serialization

So you could for instance register endpoints and not use them, where they'll be visible in endpoint API explorers like Swagger UI but continue to execute in ServiceStack's Request Pipeline.

force disables fallback execution of ServiceStack Requests through ServiceStack's Request Pipeline for requests that don't match registered endpoints. You may need to disable this if you have clients calling ServiceStack APIs through multiple HTTP Methods, as only the primary HTTP Method is registered as an endpoint.

When enabled force ensures the only ServiceStack Requests that are not executed through registered endpoints are IAppHost.CatchAllHandlers and IAppHost.FallbackHandler handlers.

useSystemJson is a new feature that lets you specify when to use System.Text.Json for JSON API Serialization, which is our next exciting feature to standardize on using
ASP.NET Core's fast async System.Text.Json Serializer.

Endpoint Routing Everywhere

Whilst the compatibility levels of Endpoint Routing can be relaxed, we recommend new projects use the strictest and most integrated defaults that's now configured on all ASP.NET Core Identity Auth .NET 8 Projects.

For additional testing we've also upgraded many of our existing .NET Example Applications, which are now all running with our latest recommended Endpoint Routing configuration: