Plinth ASP.NET Core Services Utilities
$ dotnet add package Plinth.AspNetCoreUtility framework and enhancements for ASP.Net Core Projects
Adds many enhancements and facilities for ASP.Net Core API projects including logging, security, diagnostics, front-end logging, background tasks, swagger/openapi, and more.
services.AddControllers()
.AddPlinthControllerDefaults();
This will enable
IBackgroundTaskQueue for running fire and forget tasksUpgrade() method automatically if an incoming model object is a Plinth.HttpApiClient.Models.BaseApiModel:point_right: It is recommended that APIs be authorized by default. Add the middle line to treat every API by default as if [Authorize] is present. To then have an open API, add [AllowAnonymous].
services.AddControllers()
.AddGlobalAuthorizedFilter()
.AddPlinthControllerDefaults();
services.AddPlinthServices(e =>
{
// use 'e' to enable services
});
Available services:
e.EnableServiceLogging();
/apiLogLevel.Informatione.EnableServiceExceptionHandling();
Plinth.Common.Exceptions.Logical**Exception to their corresponding HTTP error codes, as well as a few other standard exception types.e.EnableLogOnlyExceptionHandling() which will only log and not affect the responsee.EnableWebExceptionHandling() which wraps errors in an easy to use error format for front end frameworks.e.AutoEndpoints.SetComponentName("MyComponent");
e.AutoEndpoints.EnableVersion();
/version which returns information about the build, process, and if configured, the database.SetDisabledFields()e.AutoEndpoints.EnableClient();
/client/webapp which the front end can use to log information and exceptionse.AutoEndpoints.EnableDiagnostics()
/diagnostics/ping which will simply return an HTTP 200 OK, for liveness checks/diagnostics/connectivity which will run through configured dependency checks and return a result containing which are accessible and which are not. Example below. .EnableDiagnostics(c =>
{
c.SetConnectivityTesters(
new ConnectivityTesters.HttpHead("google", "https://www.google.com")
new ConnectivityTesters.SqlDatabase("db", "connectionString")
);
});At the top of Configure(), add this method to verify that all controllers can be instantiated by DI. This allows a missing DI registration to be caught at startup rather than when calling the API.
app.VerifyControllers();:point_right: after app.UseRouting and before app.UseEndpoints, add this to activate the Plinth services added in the prior section.
app.UsePlinthServices();Plinth has its own secure token mechanism which can be used as a bearer token to authorize endpoints. These tokens contain user identity information as well as custom data attributes. They can also be renewed. The tokens are encrypted using AES and authenticated using HMAC.
To enable Plinth secure tokens, import the Plinth.AspNetCore.SecureTokens namespace and add when configuring services:
var secureData = new SecureData("{64 char hex string}")
services.AddSecureTokenAuth(c =>
{
c.SecureData = secureData;
});
services.AddSingleton(new SecureTokenGenerator(secureData, TimeSpan.FromMinutes(15)));:point_right: be sure to add these to Configure()
app.UseAuthentication();
app.UseAuthorization();To generate tokens, inject SecureTokenGenerator into your controller.
.Generate(Guid userGuid, string userName, IEnumerable<string>? roles = null)
.GetBuilder().Refresh(AuthenticationToken origToken)
this.GetAuthenticationToken(), an extension available in the Plinth.AspNetCore.SecureToken namespaceTokens can contain:
These extensions are provided on ControllerBase to retrieve token information:
this.GetAuthenticationToken() returns AuthenticationTokenthis.GetAuthenticatedUserName() returns user name fieldthis.GetAuthenticatedUserGuid() returns user guidthis.GetAuthenticatedUserSessionGuid() returns session guidthis.GetAuthenticatedUserRoles() returns a set of user rolesthis.GetSecureTokenInfo() returns an object containing all of the above:point_right: All of the above will throw if the user is not authenticated. There are also versions prefixed with Try which will return null instead if the user is not authenticated. For example:
this.TryGetAuthenticatedUserName();Plinth also supports standard JWT based authentication using both Signed and Encrypted tokens (JWS and JWE). It supports signing via HMAC and RSA, and encrypting via AES and RSA-OAEP.
To enable Plinth JWT Authentication, import the Plinth.AspNetCore.Jwt namespace and add when configuring services:
services.AddPlinthJwtAuth(c =>
{
c.Audience = "PlinthTest";
c.Issuer = "Plinth.AspNetCore";
c.TokenLifetime = TimeSpan.FromMinutes(10);
c.MaxTokenLifetime = TimeSpan.FromHours(2);
c.SecurityMode = new JwtSecurityModeHmacSignature(
HexUtil.ToBytes("{64-char-hex-string}"));
});Audience and Issuer are optional, but recommended to ensure that the token is used for the purpose that is intended.
:point_right: be sure to add these to Configure()
app.UseAuthentication();
app.UseAuthorization();To generate tokens, inject JwtManager into your controller.
.GetBuilder()
.Refresh(string origToken)
this.GetJwt(), an extension available in the Plinth.AspNetCore.Jwt namespaceThese extensions are provided on ControllerBase to retrieve token information:
this.GetJwt() returns the JWT as a stringthis.GetAuthenticatedUserName() returns user name fieldthis.GetAuthenticatedUserId() returns user idthis.GetAuthenticatedUserSessionGuid() returns session guidthis.GetAuthenticatedUserRoles() returns a set of user rolesthis.GetAuthTokenInfo() returns an object containing all of the above:point_right: All of the above will throw if the user is not authenticated. There are also versions prefixed with Try which will return null instead if the user is not authenticated. For example:
this.TryGetAuthenticatedUserName();If using .AddGlobalAuthorizedFilter(), every controller method by default will behave as if it has [Authorize] on it. If not, you can add it yourself. This will require that the caller provide an authenticated user.
[Authorize]
public async Task<ActionResult> MyApi()
{
}To restrict by role (if you are supplying roles in your tokens)
[Authorize(Roles = "Admin")]
public async Task<ActionResult> MyApi()
{
}:point_right: To restrict an endpoint to users who have one of a set of roles, put them all in the string: I.e. this user is either Admin, Support, or User.
[Authorize(Roles = "Admin,Support,User")]:point_right: To restrict an endpoint to users who have multiple roles, stack the attributes: I.e. this user is both Admin and Support.
[Authorize(Roles = "Admin")]
[Authorize(Roles = "Support")]Plinth has two handy extensions for easy setup of CORS headers to support front-end API callers.
.AddCorsWithOrigins() is an extension method on IServiceCollection for setting up CORS headers to support a list of origins. Simply pass a comma delimited list of origins.
:point_right: If you pass simply "*" as the origin list, it will open up CORS to all origins. See note below.services.AddCorsWithOrigins("http://localhost:4200,https://mysite.com");
services.AddCorsWithOrigins("*");.AddCorsFullOpen() is an extension method on IServiceCollection for setting up CORS headers to support all origins.
:point_right: This is not recommended for anything other than local development.services.AddCorsFullOpen();Both Extensions accept a Action<CorsPolicyBuilder> as an optional parameter for apply custom configuration to the CORS policy such as exposed headers.
To run fire-and-forget tasks that a slight more durable than spawning threads or not awaiting tasks, first inject IBackgroundTaskQueue into your controller or manager class.
The object you will receive has this method, which you can use to queue an async function that will execute in the background.
void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem);If the host begins to shutdown gracefully, the queued tasks will be given as much time as the host allows before shutting down. This is typically about 10 seconds, but could be longer. Note that the cancellation token will be cancelled immediately when the host begins to shut down.
In the Plinth.AspNetCore.DI names is a static class called PlinthActivatorUtilities. It is similar to the Microsoft version (ActivatorUtilities), but is enhanced to create objects which use the most parameters from the DI container and supply the rest from passed in parameters.
For example, this class has four parameters.
class TwoInjectedTwoRuntime
{
public readonly Injected1 _p1;
public readonly Injected2 _p2;
public readonly string _p3;
public readonly int _p4;
public TwoInjectedTwoRuntime(Injected1 p1, Injected2 p2, string p3, int p4) { _p1 = p1; _p2 = p2; _p3 = p3; _p4 = p4; }
}In the example below, you can see how some parameters come from the service provider, and the other two come from the passed parameters.
var sc = new ServiceCollection();
sc.AddSingleton(new Injected1());
sc.AddSingleton(new Injected2());
var sp = sc.BuildServiceProvider();
var x = PlinthActivatorUtil.CreateInstance<TwoInjectedTwoRuntime>(sp, "hello", 8);This facility can be used to instantiate objects with parameters from the container and others at runtime. This mechanism can be used as below to create factories for types that expect parameters (such as this one which expects a connection and a user parameter at runtime, but may have injected parameters as well.
private static Factory<T> NewManagerFactory<T>(this IServiceProvider sp)
=> (conn, user) => PlinthActivatorUtil.CreateInstance<T>(sp, conn, user);
services.AddSingleton(sp => sp.NewManagerFactory<ThingManager>());
class ThingManager
{
public ThingManager(IBackgroundTaskQueue bgQueue, IEmailSender emailer, ISqlConnection conn, string user)
{
}
}