Full integration with ASP .NET Core 8​
We're excited to announce the most integrated release of ServiceStack ever which continues the trend from last year when we switched our newest .NET 8 templates to adopt ASP.NET Core Identity Auth that continues to replace ServiceStack's platform-agnostic abstractions to use ASP.NET Core's latest default implementations, built-in features and conventions.
This reduces friction for integrating ServiceStack into existing .NET 8 Apps, encourages greater knowledge and reuse and simplifies .NET development as developers have a reduced number of concepts to learn, fewer technology implementations to configure and maintain that are now applied across their entire .NET App.
It's also 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+ LTS 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 now 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:
- ASP.NET Core Identity Auth
- ASP.NET Core IOC
- Endpoint Routing
- System.Text.Json APIs
- Open API v3 and Swagger UI
- ASP.NET Core Identity Auth Admin UI
- JWT Identity Auth
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:
Endpoint Routing Everywhere​
Better yet, this new behavior is enabled by default in all of ServiceStack's new ASP .NET Identity Auth .NET 8 templates that has also been tested against many of ServiceStack's existing .NET Demo Apps which are now all running with the latest recommended integration:
ServiceStack .NET 8 Example Apps​
BlazorDiffusionVue | BlazorDiffusionAuto | TalentBlazor |
TechStacks | TypeChatExamples | Validation |
NorthwindAuto | FileBlazor | Chinook |
TypeChatExamples | Chat |
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 ServiceStack App's Modular Startup configurations utilize
custom IHostingStartup
configurations where application dependencies are registered 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.
This 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 use ASP .NET Core's IOC, all dependency, Plugins and Service registrations need to be moved
to before the WebApplication
is built by calling AddServiceStack()
with the Assemblies where your ServiceStack Service
Implementations are located, e.g:
builder.Services.AddServiceStack(typeof(MyServices).Assembly);
var app = builder.Build();
//...
app.UseServiceStack(new AppHost());
This change 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 reference Funq since it's no longer needed.
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()
:
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:
IServiceCollection.Add*
APIs to register dependenciesIServiceCollection.AddPlugin
API to register ServiceStack PluginsIServiceCollection.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 logic that requires access to
registered dependencies on StartUp.
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 injected, e.g:
public class TechStackServices : Service
{
[FromServices]
public required IAutoQueryDb AutoQuery { get; set; }
[FromServices]
public MyDependency? OptionalDependency { get; set; }
}
This feature is useful for Services needing optional access to 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 toFileSystemVirtualFiles
atContentRootPath
IVirtualPathProvider
- Multi Virtual File System configured to scan multiple read only sources, incWebRootPath
, In Memory and Embedded Resource filesICacheClient
andICacheClientAsync
- In Memory Cache, or distributed Redis if ServiceStack.Redis is configuredIAppSettings
- 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 implementation.
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 MapEndpoints()
when configuring ServiceStack:
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 Syntax​
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:
- /api/QueryBookings
- /api/QueryBookings.jsonl
- /api/QueryBookings.csv
- /api/QueryBookings.xml
- /api/QueryBookings.html
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)
Java/Kotlin​
JsonServiceClient client = new JsonServiceClient(baseUrl);
Python​
client = JsonServiceClient(baseUrl)
PHP​
$client = new JsonServiceClient(baseUrl);
Dart​
var client = ClientFactory.api(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;
Java/Kotlin​
client.setBasePath();
Python​
client.set_base_path()
PHP​
$client->setBasePath();
Dart​
var client = ClientFactory.create(baseUrl);
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 APIsforce
- Whether to only allow APIs to be executed through endpointsuseSystemJson
- 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 existing 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 which enables your App to standardize on using ASP.NET Core's fast async UTF8 JSON Serializer.
System.Text.Json​
The last integration piece we needed to support is utilizing System.Text.Json - the default high-performance async JSON serializer used in .NET Applications, can now be used by ServiceStack APIs to serialize and deserialize its JSON API Responses that's enabled by default when using Endpoint Routing.
This integrates ServiceStack APIs more than ever where just like Minimal APIs and Web API, uses ASP.NET Core's IOC to resolve dependencies, uses Endpoint Routing to Execute APIs that's secured with ASP.NET Core Identity Auth then uses System.Text.Json to deserialize and serialize its JSON payloads.
Enabled by Default when using Endpoint Routing​
app.UseServiceStack(new AppHost(), options => {
options.MapEndpoints();
});
Enhanced System.Text.Json​
ServiceStack uses a custom JsonSerializerOptions
to improve compatibility with existing ServiceStack DTOs and
ServiceStack's rich ecosystem of generic Add ServiceStack Reference
Service Clients, which is configured to:
- Uses
CamelCaseNamingPolicy
for property names - Supports Case Insensitive Properties
- Not serialize
null
properties - Serializes
TimeSpan
andTimeOnly
Data Types with XML Schema Time format - Supports
[DataContract]
annotations - Supports Custom Enum Serialization
Support for DataContract Annotations​
Support for .NET's DataContract
serialization attributes was added using a custom TypeInfoResolver
, specifically it supports:
[DataContract]
- When annotated, only[DataMember]
properties are serialized[DataMember]
- Specify a custom Name or Order of properties[IgnoreDataMember]
- Ignore properties from serialization[EnumMember]
- Specify a custom value for Enum values
Custom Enum Serialization​
Below is a good demonstration of the custom Enum serialization support which matches ServiceStack.Text's behavior:
public enum EnumType { Value1, Value2, Value3 }
[Flags]
public enum EnumTypeFlags { Value1, Value2, Value3 }
public enum EnumStyleMembers
{
[EnumMember(Value = "lower")]
Lower,
[EnumMember(Value = "UPPER")]
Upper,
}
return new EnumExamples {
EnumProp = EnumType.Value2, // String value by default
EnumFlags = EnumTypeFlags.Value2 | EnumTypeFlags.Value3, // [Flags] as int
EnumStyleMembers = EnumStyleMembers.Upper, // Serializes [EnumMember] value
NullableEnumProp = null, // Ignores nullable enums
};
Which serializes to:
{
"enumProp": "Value2",
"enumFlags": 3,
"enumStyleMembers": "UPPER"
}
Custom Configuration​
You can further customize the JsonSerializerOptions
used by ServiceStack by using ConfigureJsonOptions()
to add
any customizations that you can optionally apply to ASP.NET Core's JSON APIs and MVC with:
builder.Services.ConfigureJsonOptions(options => {
options.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
})
.ApplyToApiJsonOptions() // Apply to ASP.NET Core's JSON APIs
.ApplyToMvcJsonOptions(); // Apply to MVC
Control over when and where System.Text.Json is used​
Whilst System.Text.Json
is highly efficient, it's also very strict in the inputs it accepts where you may want to
revert back to using ServiceStack's JSON Serializer for specific APIs, esp. when needing to support non-updatable
external clients.
This can be done by annotating Request DTOs with [SystemJson]
attribute, specifying when to use System.Text.Json,
e.g: you can limit to only use it for serializing an APIs Response with:
[SystemJson(UseSystemJson.Response)]
public class CreateUser : IReturn<IdResponse>
{
//...
}
Or limit to only use System.Text.Json
for deserializing an APIs Request with:
[SystemJson(UseSystemJson.Request)]
public class CreateUser : IReturn<IdResponse>
{
//...
}
Or not use System.Text.Json
at all for an API with:
[SystemJson(UseSystemJson.Never)]
public class CreateUser : IReturn<IdResponse>
{
//...
}
JsonApiClient Support​
When Endpoints Routing is configured, the JsonApiClient
will also be configured to utilize the same System.Text.Json
options to send and receive its JSON API Requests which also respects the [SystemJson]
specified behavior.
Clients external to the .NET App can be configured to use System.Text.Json
with:
ClientConfig.UseSystemJson = UseSystemJson.Always;
Whilst any custom configuration can be applied to its JsonSerializerOptions
with:
TextConfig.ConfigureSystemJsonOptions(options => {
options.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
});
Scoped JSON Configuration​
We've also added partial support for Customized JSON Responses for the following customization options:
Name | Alias |
---|---|
EmitCamelCaseNames | eccn |
EmitLowercaseUnderscoreNames | elun |
EmitPascalCaseNames | epcn |
ExcludeDefaultValues | edv |
IncludeNullValues | inv |
Indent | pp |
These can be applied to the JSON Response by returning a decorated HttpResult
with a custom ResultScope
, e.g:
return new HttpResult(responseDto) {
ResultScope = () =>
JsConfig.With(new() { IncludeNullValues = true, ExcludeDefaultValues = true })
};
They can also be requested by API consumers by adding a ?jsconfig
query string with the desired option or alias, e.g:
/api/MyRequest?jsconfig=EmitLowercaseUnderscoreNames,ExcludeDefaultValues
/api/MyRequest?jsconfig=eccn,edv
SystemJsonCompatible​
Another configuration automatically applied when System.Text.Json
is enabled is:
JsConfig.SystemJsonCompatible = true;
Which is being used to make ServiceStack's JSON Serializer more compatible with System.Text.Json
output so it's easier
to switch between the two with minimal effort and incompatibility. Currently this is only used to override
DateTime
and DateTimeOffset
behavior which uses System.Text.Json
for its Serialization/Deserialization.
OpenAPI v3​
Utilizing the same ASP.NET Core Endpoints that the rest of the ASP.NET Core App uses enables your ServiceStack APIs to integrate with your wider ASP.NET Core application, opening up more opportunities for reuse of your ServiceStack APIs.
This opens up the ability to use common third party tooling. A good example of this is adding OpenAPI v3 specification
generation for your endpoints offered by the Swashbuckle.AspNetCore
package.
Also included in this Release is the ServiceStack.AspNetCore.OpenApi
package to make this integration
as easy as possible, and incorporate additional information from your ServiceStack APIs into Swagger metadata.
Previously, without the ability to map Endpoints, we've maintained a ServiceStack specific OpenAPI specification generation
via the OpenApiFeature
plugin. While this provided a lot of functionality by accurately describing your ServiceStack APIs,
it could be tricky to customize those API descriptions to the way some users wanted to.
In this post we will look at how you can take advantage of the new OpenAPI v3 Swagger support using mapped Endpoints, customizing the generated specification, as well as touch on other related changes to ServiceStack v8.1.
AppHost Initialization​
To use ServiceStack APIs as mapped Endpoints, the way ServiceStack is initialized in .
To convert your App to use Endpoint Routing and ASP.NET Core IOC your ASPNET Core
application needs to be updated to replace any usage of Funq
IoC container to use ASP.NET Core's IOC.
Previously, the following was used to initialize your ServiceStack AppHost
:
Program.cs​
app.UseServiceStack(new AppHost());
The app
in this example is a WebApplication
resulting from an IHostApplicationBuilder
calling builder.Build()
.
Whilst we still need to call app.UseServiceStack()
, we also need to move the discovery of your ServiceStack APIs to earlier
in the setup before the WebApplication
is built, e.g:
// Register ServiceStack APIs, Dependencies and Plugins:
services.AddServiceStack(typeof(MyServices).Assembly);
var app = builder.Build();
//...
// Register ServiceStack AppHost
app.UseServiceStack(new AppHost(), options => {
options.MapEndpoints();
});
app.Run();
Once configured to use Endpoint Routing we can the mix tool to apply the openapi3 Startup Configuration with:
x mix openapi3
Manually Configure OpenAPI v3 and Swagger UI​
This will install the required ASP.NET Core Microsoft, Swashbuckle and ServiceStack Open API NuGet packages:
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.*" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.*" />
<PackageReference Include="ServiceStack.AspNetCore.OpenApi" Version="8.*" />
Then add the Configure.OpenApi.cs
Modular Startup class to your project:
[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(); // Swashbuckle
services.AddServiceStackSwagger();
services.AddBasicAuth<ApplicationUser>(); // Enable HTTP Basic Auth
//services.AddJwtAuth(); // Enable & Use JWT Auth
services.AddTransient<IStartupFilter, StartupFilter>();
}
});
public class StartupFilter : IStartupFilter
{
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
=> app => {
// Provided by Swashbuckle library
app.UseSwagger();
app.UseSwaggerUI();
next(app);
};
}
}
All this setup is done for you in our updated Identity Auth .NET 8 Templates.
More Control​
One point of friction with our previous OpenApiFeature
plugin was the missing customization ability to the OpenAPI spec to somewhat disconnect from the defined ServiceStack service, and related C# Request and Response DTOs since it used Request DTOs property attributes making the structure of the OpenAPI schema mapping quite ridged, preventing the ability for certain customizations.
For example, if we have an UpdateTodo
Request DTO that looks like the following class:
[Route("/todos/{Id}", "PUT")]
public class UpdateTodo : IPut, IReturn<Todo>
{
public long Id { get; set; }
[ValidateNotEmpty]
public string Text { get; set; }
public bool IsFinished { get; set; }
}
Previously, we would get a default Swagger UI that enabled all the properties as Paramters
to populate:
While this correctly describes the Request DTO structure, sometimes as developers we get requirements for how we want to present our APIs to our users from within the Swagger UI.
With the updated SwaggerUI, and the use of the Swashbuckle
library, we get the following UI by default:
These are essentially the same, we have a CRUD Todo API that takes a UpdateTodo
Request DTO, and returns a Todo
Response DTO. ServiceStack needs to have uniquely named Request DTOs, so we can't have a Todo
schema as the Request DTO despite the fact that it is the same structure as our Todo
model.
This is a good thing, as it allows us to have a clean API contract, and separation of concerns between our Request DTOs and our models.
However, it might not be desired to present this to our users, since it can be convenient to think about CRUD services as taking the same resource type as the response.
To achieve this, we use the Swashbuckle library to customize the OpenAPI spec generation. Depending on what you want to customize, you can use the SchemaFilter
or OperationFilter
options. In this case, we want to customize the matching operation to reference the Todo
schema for the Request Body.
First, we create a new class that implements the IOperationFilter
interface.
public class OperationRenameFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
if (context.ApiDescription.HttpMethod == "PUT" &&
context.ApiDescription.RelativePath == "todos/{Id}")
{
operation.RequestBody.Content["application/json"].Schema.Reference =
new OpenApiReference {
Type = ReferenceType.Schema,
Id = "Todo"
};
}
}
}
The above matches some information about the UpdateTodo
request we want to customize, and then sets the Reference
property of the RequestBody
to the Todo
schema.
We can then add this to the AddSwaggerGen
options in the Program.cs
file.
builder.Services.AddSwaggerGen(o =>
{
o.OperationFilter<OperationRenameFilter>();
});
The result is the following Swagger UI:
This is just one simple example of how you can customize the OpenAPI spec generation, and Swashbuckle
has some great documentation on the different ways you can customize the generated spec.
And these customizations impact any of your ASP.NET Core Endpoints, not just your ServiceStack APIs.
Now that ServiceStack APIs can be mapped to standard ASP.NET Core Endpoints, it opens up a lot of possibilities for integrating your ServiceStack APIs into the larger ASP.NET Core ecosystem.
ASP.NET Core Identity Auth Admin UI​
Now that ServiceStack is deeply integrated into ASP.NET Core Apps, we're back to refocusing our efforts on adding value-added features that can benefit all .NET Core Apps.
The new Identity Auth Admin UI is an example of this, which can be enabled when registering the AuthFeature
Plugin:
public class ConfigureAuth : IHostingStartup
{
public void Configure(IWebHostBuilder builder) => builder
.ConfigureServices(services => {
services.AddPlugin(new AuthFeature(IdentityAuth.For<ApplicationUser>(
options => {
options.SessionFactory = () => new CustomUserSession();
options.CredentialsAuth();
options.AdminUsersFeature();
})));
});
}
Which just like the ServiceStack Auth Admin Users UI enables a
Admin UI that's only accessible to Admin Users for managing Identity Auth users at /admin-ui/users
.
Which displays a limited view of a User's info due to the minimal properties on the default IdentityAuth
model:
These User Search results are customizable by specifying the ApplicationUser
properties you want displayed instead:
options.AdminUsersFeature(feature =>
{
feature.QueryIdentityUserProperties =
[
nameof(ApplicationUser.Id),
nameof(ApplicationUser.DisplayName),
nameof(ApplicationUser.Email),
nameof(ApplicationUser.UserName),
nameof(ApplicationUser.LockoutEnd),
];
});
The default display Order of Users is also customizable:
feature.DefaultOrderBy = nameof(ApplicationUser.DisplayName);
As well as the Search behavior which can be replaced to search any custom fields, e.g:
feature.SearchUsersFilter = (q, query) =>
{
var queryUpper = query.ToUpper();
return q.Where(x =>
x.DisplayName!.Contains(query) ||
x.Id.Contains(queryUpper) ||
x.NormalizedUserName!.Contains(queryUpper) ||
x.NormalizedEmail!.Contains(queryUpper));
};
The default Create and Edit Admin Users UI are also limited to editing the minimal IdentityAuth
properties:
Whilst the Edit page includes standard features to lockout users, change user passwords and manage their roles:
By default Users are locked out indefinitely, but this can also be changed to lock users out to a specific date, e.g:
feature.ResolveLockoutDate = user => DateTimeOffset.Now.AddDays(7);
The forms editable fields can also be customized to include additional properties, e.g:
feature.FormLayout =
[
Input.For<ApplicationUser>(x => x.UserName, c => c.FieldsPerRow(2)),
Input.For<ApplicationUser>(x => x.Email, c => {
c.Type = Input.Types.Email;
c.FieldsPerRow(2);
}),
Input.For<ApplicationUser>(x => x.FirstName, c => c.FieldsPerRow(2)),
Input.For<ApplicationUser>(x => x.LastName, c => c.FieldsPerRow(2)),
Input.For<ApplicationUser>(x => x.DisplayName, c => c.FieldsPerRow(2)),
Input.For<ApplicationUser>(x => x.PhoneNumber, c =>
{
c.Type = Input.Types.Tel;
c.FieldsPerRow(2);
}),
];
That can override the new ApplicationUser
Model that's created and any Validation:
feature.CreateUser = () => new ApplicationUser { EmailConfirmed = true };
feature.CreateUserValidation = async (req, createUser) =>
{
await IdentityAdminUsers.ValidateCreateUserAsync(req, createUser);
var displayName = createUser.GetUserProperty(nameof(ApplicationUser.DisplayName));
if (string.IsNullOrEmpty(displayName))
throw new ArgumentNullException(nameof(AdminUserBase.DisplayName));
return null;
};
Admin User Events​
Should you need to, Admin User Events can use used to execute custom logic before and after creating, updating and deleting users, e.g:
feature.OnBeforeCreateUser = (request, user) => { ... };
feature.OnAfterCreateUser = (request, user) => { ... };
feature.OnBeforeUpdateUser = (request, user) => { ... };
feature.OnAfterUpdateUser = (request, user) => { ... };
feature.OnBeforeDeleteUser = (request, userId) => { ... };
feature.OnAfterDeleteUser = (request, userId) => { ... };
JWT Identity Auth​
JWTs enable stateless authentication of clients without servers needing to maintain any Auth state in server infrastructure or perform any I/O to validate a token. As such, JWTs are a popular choice for Microservices as they only need to configured with confidential keys to validate access.
ASP.NET Core JWT Authentication​
ServiceStack's JWT Identity Auth reimplements many of the existing ServiceStack JWT AuthProvider
features but instead of its own implementation, integrates with and utilizes ASP.NET Core's built-in JWT Authentication that's
configurable in .NET Apps with the .AddJwtBearer()
extension method, e.g:
Program.cs​
services.AddAuthentication()
.AddJwtBearer(options => {
options.TokenValidationParameters = new()
{
ValidIssuer = config["JwtBearer:ValidIssuer"],
ValidAudience = config["JwtBearer:ValidAudience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(config["JwtBearer:IssuerSigningKey"]!)),
ValidateIssuerSigningKey = true,
};
})
.AddIdentityCookies(options => options.DisableRedirectsForApis());
Then use the JwtAuth()
method to enable and configure ServiceStack's support for ASP.NET Core JWT Identity Auth:
Configure.Auth.cs​
public class ConfigureAuth : IHostingStartup
{
public void Configure(IWebHostBuilder builder) => builder
.ConfigureServices(services => {
services.AddPlugin(new AuthFeature(IdentityAuth.For<ApplicationUser>(
options => {
options.SessionFactory = () => new CustomUserSession();
options.CredentialsAuth();
options.JwtAuth(x => {
// Enable JWT Auth Features...
});
})));
});
}
Enable in Swagger UI​
Once configured we can enable JWT Auth in Swagger UI by installing Swashbuckle.AspNetCore:
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.*" />
Then enable Open API, Swagger UI, ServiceStack's support for Swagger UI and the JWT Bearer Auth option:
public class ConfigureOpenApi : IHostingStartup
{
public void Configure(IWebHostBuilder builder) => builder
.ConfigureServices((context, services) => {
if (context.HostingEnvironment.IsDevelopment())
{
services.AddEndpointsApiExplorer();
services.AddSwaggerGen();
services.AddServiceStackSwagger();
services.AddJwtAuth();
//services.AddBasicAuth<Data.ApplicationUser>();
services.AddTransient<IStartupFilter,StartupFilter>();
}
});
public class StartupFilter : IStartupFilter
{
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next) => app =>
{
app.UseSwagger();
app.UseSwaggerUI();
next(app);
};
}
}
This will enable the Authorize button in Swagger UI where you can authenticate with a JWT Token:
JWT Auth in Built-in UIs​
This also enables the JWT Auth Option in ServiceStack's built-in API Explorer, Locode and Admin UIs:
Authenticating with JWT​
JWT Identity Auth is a drop-in replacement for ServiceStack's JWT AuthProvider where Authenticating via Credentials
will convert the Authenticated User into a JWT Bearer Token returned in the HttpOnly, Secure ss-tok
Cookie
that will be used to Authenticate the client:
var client = new JsonApiClient(BaseUrl);
await client.SendAsync(new Authenticate {
provider = "credentials",
UserName = Username,
Password = Password,
});
var bearerToken = client.GetTokenCookie(); // ss-tok Cookie
JWT Refresh Tokens​
Refresh Tokens can be used to allow users to request a new JWT Access Token when the current one expires.
To enable support for JWT Refresh Tokens your IdentityUser
model should implement the IRequireRefreshToken
interface
which will be used to store the 64 byte Base64 URL-safe RefreshToken
and its RefreshTokenExpiry
in its persisted properties:
public class ApplicationUser : IdentityUser, IRequireRefreshToken
{
//...
public string? RefreshToken { get; set; }
public DateTime? RefreshTokenExpiry { get; set; }
}
Now after successful authentication, the RefreshToken
will also be returned in the ss-reftok
Cookie:
var refreshToken = client.GetRefreshTokenCookie(); // ss-reftok Cookie
Transparent Server Auto Refresh of JWT Tokens​
To be able to terminate a users access, Users need to revalidate their eligibility to verify they're still allowed access (e.g. deny Locked out users). This JWT revalidation pattern is implemented using Refresh Tokens which are used to request revalidation of their access and reissuing a new JWT Access Token which can be used to make authenticated requests until it expires.
As Cookies are used to return Bearer and Refresh Tokens ServiceStack is able to implement the revalidation logic on the server where it transparently validates Refresh Tokens, and if a User is eligible will reissue a new JWT Token Cookie that replaces the expired Access Token Cookie.
Thanks to this behavior HTTP Clients will be able to Authenticate with just the Refresh Token, which will transparently reissue a new JWT Access Token Cookie and then continue to perform the Authenticated Request:
var client = new JsonApiClient(BaseUrl);
client.SetRefreshTokenCookie(RefreshToken);
var response = await client.SendAsync(new Secured { ... });
There's also opt-in sliding support for extending a User's RefreshToken after usage which allows Users to treat their Refresh Token like an API Key where it will continue extending whilst they're continuously using it to make API requests, otherwise expires if they stop. How long to extend the expiry of Refresh Tokens after usage can be configured with:
options.JwtAuth(x => {
// How long to extend the expiry of Refresh Tokens after usage (default None)
x.ExtendRefreshTokenExpiryAfterUsage = TimeSpan.FromDays(90);
});
Convert Session to Token Service​
Another useful Service that's available is being able to Convert your current Authenticated Session into a Token
with the ConvertSessionToToken
Service which can be enabled with:
options.JwtAuth(x => {
x.IncludeConvertSessionToTokenService = true;
});
This can be useful for when you want to Authenticate via an external OAuth Provider that you then want to convert into a stateless
JWT Token by calling the ConvertSessionToToken
on the client, e.g:
.NET Clients​
await client.SendAsync(new ConvertSessionToToken());
TypeScript/JavaScript​
fetch('/session-to-token', { method:'POST', credentials:'include' })
The default behavior of ConvertSessionToToken
is to remove the Current Session from the Auth Server which will prevent
access to protected Services using our previously Authenticated Session. If you still want to preserve your existing Session
you can indicate this with:
await client.SendAsync(new ConvertSessionToToken {
PreserveSession = true
});
JWT Options​
Other configuration options available for Identity JWT Auth include:
options.JwtAuth(x => {
// How long should JWT Tokens be valid for. (default 14 days)
x.ExpireTokensIn = TimeSpan.FromDays(14);
// How long should JWT Refresh Tokens be valid for. (default 90 days)
x.ExpireRefreshTokensIn = TimeSpan.FromDays(90);
x.OnTokenCreated = (req, user, claims) => {
// Customize which claims are included in the JWT Token
};
// Whether to invalidate Refresh Tokens on Logout (default true)
x.InvalidateRefreshTokenOnLogout = true;
// How long to extend the expiry of Refresh Tokens after usage (default None)
x.ExtendRefreshTokenExpiryAfterUsage = null;
});
Blazor WASM​
Blazor WASM Tailwind
Create a new Blazor WASM Tailwind App
Create a new Blazor WASM Tailwind project with your preferred project name:
Since the release of .NET 8, we have been upgrading our Blazor Templates and example applications to take advantage of some of the new features, including making use of static Server Side Rendering (SSR) for Blazor, which allows for faster initial page loads and better SEO, and our blazor-wasm template uses InteractiveAuto
by default to provide a more Responsive UI.
What is InteractiveAuto?​
Blazor for .NET 8 has four different rendering modes you can take advantage of:
- Static Server (static SSR)
- Interactive Server
- Interactive WebAssembly (WASM)
- Interactive Auto
For non-interactive pages, the static SSR mode is the fastest, as it renders the page on the server and sends the HTML to the client. However, when your page needs to be interactive, you need to use one of the interactive modes.
Prior to .NET 8, there was a trade-off between the two available render modes (static server rendering wasn't yet available).
The Interactive Server
mode was faster to load, but the Interactive WASM
mode was more responsive.
The initial load times for Interactive WASM
could be quite slow, as the entire application and all its dependencies needed to be downloaded before the page could render most of the content.
The initial load time for the
Interactive WASM
mode can be quite slow even for a minimal app
Our templates previously worked around this limitation with a custom Pre-Rendering solution, as the wait times were too long for a good user experience.
.NET 8's new Interactive Auto
mode provides the best of both worlds as pre-rendering is now enabled by default.
When the page is first loaded, it uses the Interactive Server
mode, which is faster than Interactive WASM
as it doesn't need to download WASM resources.
So the user can start interacting with the page straight away, but with a slight delay for each of their interactions due to having to perform round-trips to the server for each interaction.
In the background, the WASM resources are downloaded which can then be used to render the site on the client for subsequent visits.
Using InteractiveAuto in your Blazor application​
In Blazor for .NET 8, render modes can be set on both a per-page and per-component basis.
@page "/counter"
@rendermode InteractiveAuto
<Counter />
<Counter @rendermode="RenderMode.InteractiveAuto" />
ServiceStack.Blazor Components​
The ServiceStack.Blazor Components have been updated for .NET 8 and work with the new InteractiveAuto
render mode.
This means you can focus more on your application logic and less on the UI, as the components provide a high-productivity UI for common tasks such as CRUD operations.
AutoQueryGrid​
The AutoQueryGrid component provides a full-featured data grid that can be used to display and edit data from an AutoQuery service. This is ideal for creating custom admin pages for your application. By integrating your admin screens into your application, you can optimize the user experience for specific workflows and get a huge amount of reuse of your existing AutoQuery services.
<AutoQueryGrid Model="Modifier" Apis="Apis.AutoQuery<QueryModifiers,CreateModifier,UpdateModifier,DeleteModifier>()" />
For BlazorDiffusion, our StableDiffusion example application, we used the AutoQueryGrid to create a custom admin page for managing the modifiers in the application.
This is the simplest and fastest use of the AutoQueryGrid component, but it can also be heavily customized for lots of different use cases.
In BlazorDiffusion we customize the grid to enable easy navigation contextually between separate customized admin screens for each Creative, linking to related table data.
<AutoQueryGrid @ref=@grid Model="Creative" Apis="Apis.AutoQuery<QueryCreatives,UpdateCreative,HardDeleteCreative>()"
ConfigureQuery="ConfigureQuery">
<EditForm>
<div class="relative z-10" aria-labelledby="slide-over-title" role="dialog" aria-modal="true">
<div class="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-10 sm:pl-16">
<CreativeEdit Creative="context" OnClose="grid.OnEditDone" />
</div>
</div>
</EditForm>
<Columns>
<Column Title="User" Field="(Creative x) => x.OwnerId" />
<Column Title="Id" Field="(Creative x) => x.Id" />
<Column Field="(Creative x) => x.Modifiers">
<Template>
@if (context.Modifiers?.Count > 0)
{
<TextLink class="flex" href=@($"/admin/modifiers?Ids={string.Join(",", context.Modifiers.Select(x => x.ModifierId))}")>
<Icon class="w-6 h-6 mr-1" Image=@typeof(Modifier).GetIcon() />
@TextUtils.Pluralize("Modifier", context.Modifiers)
</TextLink>
}
</Template>
</Column>
<Column Field="(Creative x) => x.Artists">
<Template>
@if (context.Artists?.Count > 0)
{
<TextLink class="flex" href=@($"/admin/artists?Ids={string.Join(",", context.Artists.Select(x => x.ArtistId))}")>
<Icon class="w-6 h-6 mr-1" Image=@typeof(Artist).GetIcon() />
@TextUtils.Pluralize("Artist", context.Artists)
</TextLink>
}
</Template>
</Column>
<Column Field="(Creative x) => x.Artifacts">
<Template>
@if (context.Artifacts?.Count > 0)
{
<TextLink class="flex" href=@($"/admin/artifacts?CreativeId={context.Id}")>
<Icon class="w-6 h-6 mr-1" Image=@typeof(Artifact).GetIcon() />
@TextUtils.Pluralize("Artifact", context.Artifacts)
</TextLink>
}
</Template>
</Column>
<Column Field="(Creative x) => x.Key" />
<Column Field="(Creative x) => x.CreatedDate" Format="s" />
<Column Field="(Creative x) => x.UserPrompt" />
</Columns>
</AutoQueryGrid>
In the above example, we use the ConfigureQuery
parameter to customize the query used by the AutoQueryGrid when displaying values.
This is ideal if you want to filter the data for specific workflows, for example, only showing the data that is relevant to the current user.
We combine this with a Tabs
component to provide a navigation bar for the user to switch between the different filters on the same AutoQueryGrid.
<Tabs TabOptions="TabOptions" TabChanged="TabChangedAsync" />
We also use the EditForm
parameter to customize the edit form for the AutoQueryGrid, so the workflow for editing a Creative is optimized using your own completely custom UI.
<AutoQueryGrid @ref=@grid Model="Creative" Apis="Apis.AutoQuery<QueryCreatives,UpdateCreative,HardDeleteCreative>()"
ConfigureQuery="ConfigureQuery">
<EditForm>
<div class="relative z-10" aria-labelledby="slide-over-title" role="dialog" aria-modal="true">
<div class="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-10 sm:pl-16">
<CreativeEdit Creative="context" OnClose="grid.OnEditDone" />
</div>
</div>
</EditForm>
Upgrading to .NET 8​
BlazorDiffusion was an example application we originally developed for .NET 6. We upgraded the production release of this application to use our blazor-vue template, which can be perfect for public-facing web applications and teams that don't mind including a JavaScript framework in their application.
However, to show the flexibility of Blazor for .NET 8, we also upgraded the whole application from our updated blazor-wasm
template to take advantage of the new InteractiveAuto
mode.
Component Compatibility​
Since the ServiceStack.Blazor library has been updated for .NET 8, we just needed to bring over the shared components from the original application and update the references to the new library.
When upgrading your application pages and components, you will need to avoid any JavaScript interop that runs during the InitializeAsync
lifecycle method, as this is not supported in the InteractiveAuto
mode.
Running on both Server vs Client​
When using the InteractiveAuto
mode, first visits will be running on the server, so your pages and components need to be available to both projects, as well as have any required dependencies registered in both projects Program.cs
files.
By placing your shared pages and components in a shared project like the .Client
project in the blazor-wasm
template, you can easily share them between the two projects.
Look for any of your pages or components that use the @injects
directive, as these will need to be registered in both projects.
INFO
Avoid sharing sensitive information via dependency injection, as this will be available to the client at runtime which will be able to be decompiled and inspected.
Source code and live demo​
The source code for the upgraded BlazorDiffusionAuto
application is available on GitHub and you can view a live demo of the application at auto.blazordiffusion.com.
Conclusion​
The new InteractiveAuto
mode in Blazor for .NET 8 provides the best of both worlds for Blazor applications.
A built in pre-rendering solution means that you can have a fast initial load time, but still have a responsive UI for subsequent visits.
And since the ServiceStack.Blazor components have been updated for .NET 8, you can take advantage of the high-productivity UI components to quickly create customizable and professional-looking admin pages in a Blazor application.
Breaking Changes​
In order to promote adoption of ASP.NET Core's APIs the IServiceProvider.TryResolve<T>()
extension method was removed:
services.AddSingleton<ICrudEvents>(c =>
new OrmLiteCrudEvents(c.Resolve<IDbConnectionFactory>()));
Which should be replaced with IServiceProvider's GetService<T>()
and GetRequiredService<T>()
:
services.AddSingleton<ICrudEvents>(c =>
new OrmLiteCrudEvents(c.GetRequiredService<IDbConnectionFactory>()));
Feedback​
If you have any questions or feedback about anything in this release, please feel free to reach out to us on the Customer Forums or GitHub Discussions.