How to implement Response Caching with Minimal APIs in .NET 6? - c#

In order to implement response caching, all that is needed to be done was:
inject .AddResponseCaching() into the services,
decorate a controller action with [ResponseCache(Duration = 10)].
Now I'm trying the minimal APIs that come with .NET 6 and I haven't figured out a way to do that except add the header myself cache-control: public,max-age=10.
Is there a more elegant way to do so?

ResponseCacheAttribute is part of MVC and as per docs will be applied to:
Razor Pages: Attributes can't be applied to handler methods.
MVC controllers.
MVC action methods: Method-level attributes override the settings specified in class-level attributes.
So it seems that adding cache header yourself is an only option. If you want you can "prettify" it a little bit using custom middleware. Something along this lines:
class CacheResponseMetadata
{
// add configuration properties if needed
}
class AddCacheHeadersMiddleware
{
private readonly RequestDelegate _next;
public AddCacheHeadersMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext httpContext)
{
if (httpContext.GetEndpoint()?.Metadata.GetMetadata<CacheResponseMetadata>() is { } mutateResponseMetadata)
{
if (httpContext.Response.HasStarted)
{
throw new InvalidOperationException("Can't mutate response after headers have been sent to client.");
}
httpContext.Response.Headers.CacheControl = new[] { "public", "max-age=100" };
}
await _next(httpContext);
}
}
And usage:
app.UseMiddleware<AddCacheHeadersMiddleware>(); // optionally move to extension method
app.MapGet("/cache", () => $"Hello World, {Guid.NewGuid()}!")
.WithMetadata(new CacheResponseMetadata()); // optionally move to extension method
.NET 7 update
For .NET 7 you can configure output caching (see the linked article for differences with response caching):
The output caching middleware can be used in all types of ASP.NET Core apps: Minimal API, Web API with controllers, MVC, and Razor Pages.
Output caching can be done per endpoint via OutputCacheAttribute or CacheOutput method call:
app.MapGet("/cached", Gravatar.WriteGravatar).CacheOutput();
app.MapGet("/attribute", [OutputCache] (context) => Gravatar.WriteGravatar(context));
Or for multiple endpoints via policy:
The following code configures caching for all of the app's endpoints, with expiration time of 10 seconds.
builder.Services.AddOutputCache(options =>
{
options.AddBasePolicy(builder => builder.Expire(TimeSpan.FromSeconds(10)));
});

Related

ASP.NET Core MVC filter analogue for gRPC service

I have an existing REST API running on ASP.NET Core 3.0. It uses MVC filter to perform an authorization check based on a header value and returns error in case of authorization failure so that the request is not passed to the controller.
Now, I am experimenting with gRPC and trying to port this API to a gRPC service. However, I do not see any obvious solutions that might act as an MVC filter replacement.
Is there some way to achieve similar authorization checking functionality, perhaps using metadata?
For MVC and gRpc, they are different. ActionFilter is not exist under gRpc.
If you want to apply checking request header for all actions, you could try implement your custom middleware before app.UseEndpoints and check the request header.
For another way, you could try Policy like below:
GrpcRequireemnt and GrpcHandler
public class GrpcRequireemnt : IAuthorizationRequirement
{
}
public class GrpcHandler : AuthorizationHandler<GrpcRequireemnt>
{
private readonly IHttpContextAccessor _httpContextAccessor;
public GrpcHandler(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, GrpcRequireemnt requirement)
{
var headers = _httpContextAccessor.HttpContext.Request.Headers;
StringValues token;
if (!headers.TryGetValue("token", out token))
{
context.Fail();
return Task.CompletedTask;
}
context.Succeed(requirement);
return Task.CompletedTask;
}
}
Register required services
services.AddAuthorization(options =>
{
options.AddPolicy("TokenAuthorize", policy =>
{
policy.AddRequirements(new GrpcRequireemnt());
});
});
services.AddHttpContextAccessor();
services.AddSingleton<IAuthorizationHandler, GrpcHandler>();
UseCase
[Authorize("TokenAuthorize")]
public override Task<BuyTicketsResponse> BuyTickets(BuyTicketsRequest request, ServerCallContext context)
{
var user = context.GetHttpContext().User;
return Task.FromResult(new BuyTicketsResponse
{
Success = _ticketRepository.BuyTickets(user.Identity.Name!, request.Count)
});
}
This will have slightly different answers depending on if you're using Grpc.Core, which is a wrapper around the C GRPC library initially developed at Google, which has been available for a while and supports a variety of .Net targets (including Framework), or if you're using the new Grpc.AspNetCore which launched with .Net Core 3.0 and is built on Kestrel and ASP.NET Core internals.
Grpc.Core
For Grpc.Core you would want to pass your header value as metadata, and then create a server-side Interceptor to handle the metadata and the request. You can also consider using the AsyncAuthInterceptor, however the core Grpc implementation on the client side will not send credentials over insecure (non-TLS) connections.
Grpc.AspNetCore
Grpc.AspNetCore is built on ASP.NET and can use ASP.NET middleware, including the default ASP.NET authentication. If you can convert your filter into a middleware, you would be able to share the authentication between both implementations.

ASP.NET core 2.2: what is the expected behaviour of ChallengeResult when there are multiple authentication schemes configured?

We are trying to understand what is the expected handling of a ChallengeResult when there are multiple authentication schemes registered.
We need to handle such a scenario because we have an ASP.NET core 2.2 app exposing some action methods (we use the MVC middleware) that must be used by an angularjs SPA which relies on cookies authentication and some third parties applications which use an authentication mechanism based on the Authorization HTTP request header. Please notice that the involved action methods are the same for both the users, this means that each one of them must allow authentication using both the cookie and the custom scheme based on Authorization HTTP request header. We know that probably this is not an optimal design but we cannot modify the overall architecture.
This documentation seems to confirm that what we would like to achieve is entirely possible using ASP.NET core 2.2. Unfortunately, the cookie authentication used by the UI app and the custom authentication used by the third parties must behave differently in case of an authentication challenge and their expected behaviors are not compatible with each other: the UI app should redirect the user to a login form, while a thir party application expects a raw 401 status code response. The documentation linked above does not offer a clear explanation of the ChallengeResult handling, so we decided to experiment with a test application.
We created two fake authentication handlers:
public class FooAuthenticationHandler : IAuthenticationHandler
{
private HttpContext _context;
public Task<AuthenticateResult> AuthenticateAsync()
{
return Task.FromResult(AuthenticateResult.Fail("Foo failed"));
}
public Task ChallengeAsync(AuthenticationProperties properties)
{
_context.Response.StatusCode = StatusCodes.Status403Forbidden;
return Task.CompletedTask;
}
public Task ForbidAsync(AuthenticationProperties properties)
{
return Task.CompletedTask;
}
public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
{
_context = context;
return Task.CompletedTask;
}
}
public class BarAuthenticationHandler : IAuthenticationHandler
{
private HttpContext _context;
public Task<AuthenticateResult> AuthenticateAsync()
{
return Task.FromResult(AuthenticateResult.Fail("Bar failed"));
}
public Task ChallengeAsync(AuthenticationProperties properties)
{
_context.Response.StatusCode = StatusCodes.Status500InternalServerError;
return Task.CompletedTask;
}
public Task ForbidAsync(AuthenticationProperties properties)
{
return Task.CompletedTask;
}
public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
{
_context = context;
return Task.CompletedTask;
}
}
We registered the authentication schemas inside ConfigureServices method as follows:
public void ConfigureServices(IServiceCollection services)
{
services
.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddAuthentication(options =>
{
options.DefaultChallengeScheme = "Bar";
options.AddScheme<FooAuthenticationHandler>("Foo", "Foo scheme");
options.AddScheme<BarAuthenticationHandler>("Bar", "Bar scheme");
});
}
This is our middleware pipeline:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// 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.UseAuthentication();
app.UseMvc();
}
and finally we created a controller with an action method requiring authentication:
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
// GET api/values/5
[HttpGet("{id}")]
[Authorize(AuthenticationSchemes = "Foo,Bar")]
public ActionResult<string> Get(int id)
{
return "value";
}
}
We noticed that:
both the FooAuthenticationHandler and BarAuthenticationHandler are called to handle the ChallengeResult
the order is FooAuthenticationHandler before BarAuthenticationHandler and depends on the Authorize attribute (if you swap the authentication schemes inside the Authorize attribute then BarAuthenticationHandler is called first)
the caller gets a raw 500 status code response, but this only depends on the order in which the authorization handlers are called
the call to options.DefaultChallengeScheme = "Bar"; matters if and only if inside the [Authorize] attribute the property AuthenticationSchemes is not set. If you do so, only the BarAuthenticationHandler is called and FooAuthenticationHandler never gets a chance to authenticate the request or handle an authentication challenge.
So, the question basically is: when you have such a scenario, how are you expected to handle the possible "incompatibility" of different authentication schemes regarding ChallengeResult handling since they get both called ?
In our opinion is fine that both have a chance to authenticate the request, but we would like to know if it is possible to decide which one should handle the authentication challenge.
Thanks for helping !
You should not specify the schemes on the Authorize attribute.
Instead, specify one scheme as the default, and setup a forward selector.
The implementation of the selector depends on your case, but usually you can somehow figure out which scheme was used in a request.
For example, here is an example from the setup of an OpenID Connect scheme.
o.ForwardDefaultSelector = ctx =>
{
// If the current request is for this app's API
// use JWT Bearer authentication instead
return ctx.Request.Path.StartsWithSegments("/api")
? JwtBearerDefaults.AuthenticationScheme
: null;
};
So what it does is forward challenges (and well, everything) to the JWT handler if the route starts with /api.
You can do any kind of checks there, headers etc.
So in this case OpenID Connect and Cookies are setup as defaults for everything, but if a call is received that is going to the API, use JWT authentication.
The example here forwards all the "actions" you can do with authentication (challenge, forbid etc.).
You can also setup forward selectors for just challenges etc.

NSwag .NET Core API Versioning configuration

I'd like to prepare my .NET Core Web API project so that multiple versions of the API can be managed and documented, according to the REST services standards.
I'm using .NET Core 2.1 with NSwag (v11.18.2). I also installed the Microsoft.AspNetCore.Mvc.Versioning NuGet package.
I already searched with Google for some configuration examples, but the only useful link I found is this.
I'm now able to get Swagger pages for both API versions but with some problems:
Please note that none of the last config settings (Title, Description, etc.) takes effect on any of the 2 routes. It only works if I add them on each of the individual configuration. So I'd also like to know if it possible to avoid that, since the general configuration of the API can be version indipendent (title, description and so on...).
Since the issue with NSwag and Microsoft API Versioning package discussed in the above link, was opened 2-3 months (and NSwag versions too) ago, I'd like to know if it is now truly fixed and in this case, which is the right configuration to set.
Although the version is explicit in the configuration of the controllers, it is still required as a mandatory input parameter of the controller methods and of course I don't want that! See image:
So, my actual configuration, by following that example, is looking like this:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddApiVersioning(options =>
{
options.AssumeDefaultVersionWhenUnspecified = true;
options.DefaultApiVersion = new ApiVersion(1, 0);
options.ReportApiVersions = true;
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseSwaggerWithApiExplorer(config =>
{
config.GeneratorSettings.OperationProcessors.TryGet<ApiVersionProcessor>().IncludedVersions = new[] { "1.0" };
config.SwaggerRoute = "v1.0.json";
});
app.UseSwaggerWithApiExplorer(config =>
{
config.GeneratorSettings.OperationProcessors.TryGet<ApiVersionProcessor>().IncludedVersions = new[] { "2.0" };
config.SwaggerRoute = "v2.0.json";
});
app.UseSwaggerUi3(typeof(Startup).GetTypeInfo().Assembly, config =>
{
config.SwaggerRoutes.Add(new SwaggerUi3Route("v1.0", "/v1.0.json"));
config.SwaggerRoutes.Add(new SwaggerUi3Route("v2.0", "/v2.0.json"));
config.GeneratorSettings.Title = "My API";
config.GeneratorSettings.Description = "API functionalities.";
config.GeneratorSettings.DefaultUrlTemplate = "{v:apiVersion}/{controller}/{action}/{id?}";
config.GeneratorSettings.DefaultPropertyNameHandling = PropertyNameHandling.CamelCase
});
}
And these are my actual controllers:
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]/[action]")]
[SwaggerTag("Test1", Description = "Core operations on machines (v1.0).")]
public class MachinesController : Controller
{
[HttpGet("{id}")]
[ProducesResponseType((int)HttpStatusCode.OK)]
public async Task<ActionResult<Machine>> Get(int id)
{
return await ...
}
}
[ApiController]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/[controller]/[action]")]
[SwaggerTag("Test2", Description = "Core operations on machines (v2.0).")]
public class MachinesController : Controller
{
[HttpGet("{id}")]
[ProducesResponseType((int)HttpStatusCode.OK)]
public async Task<ActionResult<Machine>> Get(int id)
{
return await ...
}
}
They are ignored in the middleware because they are inferred from the settings or do not apply for api explorer (template). However title and description should work...
Please create an issue with the specific issue and a repro, also check out the existing tests in the repo
Fixed with v11.18.3
I believe starting in NSwag 12.0.0, there is significantly improved support for the API Explorer. It's important that the complementary API Explorer package for API versioning is also referenced so that the proper information is provided to NSwag.
The Swagger sample application provided by API Versioning uses Swashbuckle, but the setup will be very similar to NSwag. You can use the IApiVersionDescriptionProvider service to enumerate all of the API versions defined in your application. That should significantly simplify your NSwag configuration.
You're versioning by URL segment; therefore, to address Problem 3 you simply need to configure the API Explorer a la:
services.AddVersionedApiExplorer( options => options.SubstituteApiVersionInUrl = true );
This will replace the {version} route parameter in the route template with the corresponding API version value and remove the API version parameter from the API description.

How to store session data using Owin hosting?

I have problem with creating a session in application hosted using Owin. I have tried using RedisSession, but I didn't know how to configure it so it gave me an error.
I was looking for a solution for some time, tried different things and finally decided to ask here for help.
Scenario:
I'm logging in the application using HTTP POST request,
User login and password should be stored in session,
For each next GET/POST request which need previous login session is
empty (login and password are null).
Object HTTPContext is empty.
I'm using Ninject for dependency injections.
I tried something like that: Can OWIN middleware use the http session?
Does anybody have an idea how to store login data in Owin session?
Below is Owin configuration file, included in it things are from link posted above.
[assembly: OwinStartup(typeof(Service.Startup))]
namespace Service
{
public class Startup
{
public void Configuration(IAppBuilder appBuilder)
{
var config = new HttpConfiguration();
config.Routes.MapHttpRoute("DefaultApi", "api/{controller}/{action}/{id}", new { id = RouteParameter.Optional }
);
appBuilder.RequireAspNetSession();
appBuilder.UseNinjectMiddleware(CreateKernel).UseNinjectWebApi(config);
}
public static StandardKernel CreateKernel()
{
var kernel = new StandardKernel(new Module());
return kernel;
}
}
public static class AspNetSessionExtensions
{
public static IAppBuilder RequireAspNetSession(this IAppBuilder app)
{
app.Use((context, next) =>
{
// Depending on the handler the request gets mapped to, session might not be enabled. Force it on.
HttpContextBase httpContext = context.Get<HttpContextBase>(typeof(HttpContextBase).FullName);
httpContext.SetSessionStateBehavior(SessionStateBehavior.Required);
return next();
});
// SetSessionStateBehavior must be called before AcquireState
app.UseStageMarker(PipelineStage.MapHandler);
return app;
}
}
}
I've had some struggling with sessions as well.
Here is the solution, which works for me:
1) Add NuGet Microsoft.AspNetCore.Session
2) Call .AddSession on your IServiceCollection. Note, it might need configuration.
In my case it is:
3) Use your session. Keep in mind, that if there are no values set to a session, on each request SessionID is different.
So you'd have to add some value to a session. This is how it would stay the same across multiple requests.
And here is my session pinning middleware:
Hope it helps.
For anyone wanting to do this with ASP.net Framework, instead of Core, read on.
First, make sure Owin.Extensions is installed.
Next, add this code to your Owin Startup.cs before the middleware that you want to use session.
app.Use((context, next) =>
{
var httpContext = context.Get<HttpContextBase>(typeof(HttpContextBase).FullName);
httpContext.SetSessionStateBehavior(SessionStateBehavior.Required);
return next();
}).UseStageMarker(PipelineStage.MapHandler);
By default, OWIN Middleware run at the last event (PipelineStage.PreHandlerExecute) which is too late for accessing session state.
.UseStageMarker tells OWIN where in the execution pipeline to execute the function passed to .Use().
To use sessions, you need to execute middleware that runs after the session has been initialized by the ASP.net runtime. This middleware must be run in the PostAquireState phase, like so:
.Use((context, next) =>
{
HttpContext.Current.Session["mykey"] = "myvalue";
return next();
}).UseStageMarker(PipelineStage.PostAcquireState);

Web Api - Request Parameters Outside Controller

I'm working on a ASP.NET Web Api project and made it accept version information in the url.
For example:
api/v1/MyController api/v2/MyController
Now I would like to get the request version v1, v2 inside a custom LayoutRenderer for Nlog. Normally I would do this like the below example.
[LayoutRenderer("Version")]
public class VersionLayoutRenderer : LayoutRenderer
{
protected override void Append(System.Text.StringBuilder builder, NLog.LogEventInfo logEvent)
{
var version = HttpContext.Current.Request.RequestContext.RouteData.Values["Version"];
builder.Append(version);
}
}
The problem: HttpContext.Current is NULL
I believe this is because I use Async wrappers for NLog and some calls before the Logger are also Async.
A example of the logger being called Async inside Ninject.Extensions.WebApi.UsageLogger. At this point the HttpRequestMessage has all info we need to get the Version.
/// <summary>
/// Initializes a new instance of the <see cref="UsageHandler" /> class.
/// </summary>
public UsageHandler()
{
var kernel = new StandardKernel();
var logfactory = kernel.Get<ILoggerFactory>();
this.Log = logfactory.GetCurrentClassLogger();
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var startTime = DateTime.Now;
// Log request
await request.Content.ReadAsStringAsync().ContinueWith(c =>
{
this.Log.Info("{0}: {1} called from {2}", request.Method, HttpUtility.UrlDecode(request.RequestUri.AbsoluteUri), ((HttpContextBase)request.Properties["MS_HttpContext"]).Request.UserHostAddress);
this.Log.Info("Content-Type: {0}, Content-Length: {1}", request.Content.Headers.ContentType != null ? request.Content.Headers.ContentType.MediaType : string.Empty, request.Content.Headers.ContentLength);
this.Log.Info("Accept-Encoding: {0}, Accept-Charset: {1}, Accept-Language: {2}", request.Headers.AcceptEncoding, request.Headers.AcceptCharset, request.Headers.AcceptLanguage);
if (!string.IsNullOrEmpty(c.Result))
{
if (this.MaxContentLength > 0 && c.Result.Length > this.MaxContentLength)
{
this.Log.Info("Data: {0}", HttpUtility.UrlDecode(c.Result).Substring(0, this.MaxContentLength - 1));
}
else
{
this.Log.Info("Data: {0}", HttpUtility.UrlDecode(c.Result));
}
}
});
var response = await base.SendAsync(request, cancellationToken);
// Log the error if it returned an error
if (!response.IsSuccessStatusCode)
{
this.Log.Error(response.Content.ReadAsStringAsync().Result);
}
// Log performance
this.Log.Info("Request processing time: " + DateTime.Now.Subtract(startTime).TotalSeconds + "s");
return response;
}
The question
What would be the best way to make the VersionLayoutRenderer work in a generic way? Could I add a MessageHandler and Bind the HttpRequest to some Async scope? If so any guidelines would be much appreciated cause I'm still getting used to Ninject.
For the time being I add the version information directly to the Log Call in the UsageHandler, but I would really like a more generic solution, where I can always rely on version information inside my logging.
Edit: Updated the question to be more specific and included more details.
Try injecting the context using something like:
kernel.Bind<IDependency>()
.To<Mydependency>()
.InRequestScope()
.WithConstructorArgument("context",c=>HttpContext.Current);
The actual issue is really neutral wrt what you should do with Ninject - you just need to get the phasing of your processing such that any objects that are going be running async have everything they need without relying on the magic HttpContext.Current. Get that working with no DI Container first.
Then, to use Ninject the major steps are:-
Your Bind statements need to be run once. See the Ninject.MV3 wiki for the best approach (until it gets merged in, there is not OOTB with the NuGet-based edition)
as #rickythefox (+1'd) says, your registration should bake the thread/context-relative data into the object and you config the registration such that it can happen early in request processing, when you're still on the thread that's HttpContext.Current
kernel.Bind<ILogger>()
// TODO replace GCCL with something like GetClassLogger(ctx.Request.Service.ReflectedType) - see the wiki for examples
.ToMethod( ctx=> ctx.Get<ILoggerFactory>().GetCurrentClassLogger())
.InRequestScope()
.WithConstructorArgument("context",c=>HttpContext.Current);
Then just make the constructor of the handler take a ILogger, which can be assigned to .Log (which I hope isnt static :D)
NB, the aim is for you never to write a kernel.Get(), ever, period.
The real problem here though, is that proper use of WebApi does not involve using HttpContext.Current or any other magic static methods or anything similar (for testability, to make yourself independent of the hosting context (self hosting, OWIN etc), and many more reasons).
Also, if you are using NLog (or Log4Net) you should also look at the Ninject.Extensions.Logging package (and source).
The GlobalConfiguration class can provide you access to the routing configuration.
// The code below assumes a map routing convention of api/{version}/{controller}/....
// This will give you the configured routes
var routes = GlobalConfiguration.Configuration.Routes;
// This will give you the route templates
var templates = routes
.Select(route => route.RouteTemplate);
// This will give you the distinct versions for all controllers
var versions = routes
.Select(route => route.RouteTemplate)
.Select(template => template.Split("/".ToCharArray(), StringSplitOptions.RemoveEmptyEntries))
.Select(values => values[1])
.Distinct();
// This will give you the distinct versions for a controller with the specified name
var name = "MyController";
var controllerVersions = routes
.Select(route => route.RouteTemplate)
.Select(template => template.Split("/".ToCharArray(), StringSplitOptions.RemoveEmptyEntries))
.Where(values => String.Equals(values[2], name, StringComparison.OrdinalIgnoreCase))
.Select(values => values[1])
.Distinct();
I am not sure if you are trying to resolve the version with a known value (the name of the controller) or if you are trying to dynamically resolve it. If you inject the current HttpContext, you can use the request URI of the context to filter the routes via the route template.
Edit: After your comments I realized the routing configuration isn't what you were after.
If the end goal is to implement logging within your controllers, you may want to take a look at Tracing in ASP.NET Web API as there is support for tracing built-in to the Web API infrastructure.

Categories