How to cache response in Asp.Net Core Middleware? - asp.net-core

I have created a Middleware in ASP.NET Core 3.1 to return a XML Sitemap from route /sitemap.xml.
public class SitemapMiddleware {
private readonly RequestDelegate _next;
public SitemapMiddleware(RequestDelegate next) {
_next = next;
}
public async Task InvokeAsync(HttpContext context, ISitemapService sitemapService) {
if (context.Request.Path.StartsWithSegments("/sitemap.xml)) {
Sitemap sitemap = sitemapService.GetSitemap();
context.Response.ContentType = "application/xml";
await context.Response.WriteAsync(sitemap.ToString(), Encoding.UTF8);
} else {
await _next(context);
}
}
And I register it as follows:
public static class SitemapMiddlewareExtensions {
public static IApplicationBuilder UseSitemap(this IApplicationBuilder builder) {
return builder.MapWhen(x => x.Request.Path.StartsWithSegments("/sitemap.xml"),
x => x.UseMiddleware<SitemapMiddleware>(route));
}
}
Question
How to cache the middleware's response and call the SitemapService only if the cached response has more than 24 hours?

Related

override default asp.net core cancelationtoken or change default timeout for requests

In my asp.net core 5.0 app, I call an async method that might take some time to be processed.
await someObject.LongRunningProcess(cancelationToken);
However, I want that method to be timeout after 5 seconds. I know that instead of "cancelationToken" passed by asp.net core action, I can use "CancellationTokenSource" :
var s_cts = new CancellationTokenSource();
s_cts.CancelAfter(TimeSpan.FromSeconds(5);
await someObject.LongRunningProcess(s_cts );
Is it possible to use "CancellationTokenSource" as a default "Cancelation Token" policy for all asp.net core requests ? I mean override the one which is passed as a parameter of the action ?
Or is it possible to change the default timeout for all request in asp.net core 5.0 ?
[Update]
Customizing the CancellationToken passed to actions
You need to replace the default CancellationTokenModelBinderProvider that binds HttpContext.RequestAborted token to CancellationToken parameters of actions.
This involves creating a custom IModelBinderProvider. Then we can replace the default binding result with our own.
public class TimeoutCancellationTokenModelBinderProvider : IModelBinderProvider
{
public IModelBinder? GetBinder(ModelBinderProviderContext context)
{
if (context?.Metadata.ModelType != typeof(CancellationToken))
{
return null;
}
var config = context.Services.GetRequiredService<IOptions<TimeoutOptions>>().Value;
return new TimeoutCancellationTokenModelBinder(config);
}
private class TimeoutCancellationTokenModelBinder : CancellationTokenModelBinder, IModelBinder
{
private readonly TimeoutOptions _options;
public TimeoutCancellationTokenModelBinder(TimeoutOptions options)
{
_options = options;
}
public new async Task BindModelAsync(ModelBindingContext bindingContext)
{
await base.BindModelAsync(bindingContext);
if (bindingContext.Result.Model is CancellationToken cancellationToken)
{
// combine the default token with a timeout
var timeoutCts = new CancellationTokenSource();
timeoutCts.CancelAfter(_options.Timeout);
var combinedCts =
CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, cancellationToken);
// We need to force boxing now, so we can insert the same reference to the boxed CancellationToken
// in both the ValidationState and ModelBindingResult.
//
// DO NOT simplify this code by removing the cast.
var model = (object)combinedCts.Token;
bindingContext.ValidationState.Clear();
bindingContext.ValidationState.Add(model, new ValidationStateEntry() { SuppressValidation = true });
bindingContext.Result = ModelBindingResult.Success(model);
}
}
}
}
class TimeoutOptions
{
public int TimeoutSeconds { get; set; } = 30; // seconds
public TimeSpan Timeout => TimeSpan.FromSeconds(TimeoutSeconds);
}
Then add this provider to Mvc's default binder provider list. It needs to run before all others, so we insert it at the beginning.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.Configure<MvcOptions>(options =>
{
options.ModelBinderProviders.RemoveType<CancellationTokenModelBinderProvider>();
options.ModelBinderProviders.Insert(0, new TimeoutCancellationTokenModelBinderProvider());
});
// remember to set the default timeout
services.Configure<TimeoutOptions>(configuration => { configuration.TimeoutSeconds = 2; });
}
Now ASP.NET Core will run your binder whenever it sees a parameter of CancellationToken type, which combines HttpContext.RequestAborted token with our timeout token. The combined token is triggered whenever one of its component is cancelled (due to timeout or request abortion, whichever is cancelled first)
[HttpGet("")]
public async Task<IActionResult> Index(CancellationToken cancellationToken)
{
await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken); // throws TaskCanceledException after 2 seconds
return Ok("hey");
}
References:
https://github.com/dotnet/aspnetcore/blob/348b810d286fd2258aa763d6eda667a83ff972dc/src/Mvc/Mvc.Core/src/ModelBinding/Binders/CancellationTokenModelBinder.cs
https://abdus.dev/posts/aspnetcore-model-binding-json-query-params/
https://learn.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-model-binding?view=aspnetcore-5.0#custom-model-binder-sample
One approach to solve this problem is wrapping that logic inside a class. Write a class that runs a task with a configurable timeout.
Then register it in DI, then use it anywhere you want to reuse the configuration.
public class TimeoutRunner
{
private TimeoutRunnerOptions _options;
public TimeoutRunner(IOptions<TimeoutRunnerOptions> options)
{
_options = options.Value;
}
public async Task<T> RunAsync<T>(Func<CancellationToken, Task<T>> runnable,
CancellationToken cancellationToken = default)
{
// cancel the task as soon as one of the tokens is set
var timeoutCts = new CancellationTokenSource();
var token = timeoutCts.Token;
if (cancellationToken != default)
{
timeoutCts.CancelAfter(_options.Timeout);
var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, cancellationToken);
token = combinedCts.Token;
}
return await runnable(token);
}
}
internal static class ServiceCollectionExtensions
{
public static IServiceCollection AddTimeoutRunner(this IServiceCollection services,
Action<TimeoutRunnerOptions> configure = null)
{
if (configure != null)
{
services.Configure<TimeoutRunnerOptions>(configure);
}
return services.AddTransient<TimeoutRunner>();
}
}
public class TimeoutRunnerOptions
{
public int TimeoutSeconds { get; set; } = 10;
public TimeSpan Timeout => TimeSpan.FromSeconds(TimeoutSeconds);
}
you'd then register this in Startup class,
public void ConfigureServices(IServiceCollection services)
{
services.AddTimeoutRunner(options =>
{
options.TimeoutSeconds = 10;
});
}
then consume it wherever you need that global option:
public class MyController : ControllerBase
{
private TimeoutRunner _timeoutRunner;
public MyController(TimeoutRunner timeoutRunner)
{
_timeoutRunner = timeoutRunner;
}
public async Task<IActionResult> DoSomething(CancellationToken cancellationToken)
{
await _timeoutRunner.RunAsync(
async (CancellationToken token) => {
await Task.Delay(TimeSpan.FromSeconds(20), token);
},
cancellationToken
);
return Ok();
}
}
Running a task before every action dispatch
Method 1: Action filters
We can use action filters to run a task before/after every request.
public class ApiCallWithTimeeotActionFilter : IAsyncActionFilter
{
private TimeoutRunner _runner;
public ApiCallWithTimeeotActionFilter(TimeoutRunner runner)
{
_runner = runner;
}
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var result = await _runner.RunAsync(
async (CancellationToken token) =>
{
await Task.Delay(TimeSpan.FromSeconds(20), token);
return 42;
},
default
);
await next();
}
}
then to use it annotate a class with [TypeFilter(typeof(MyAction))]:
[TypeFilter(typeof(ApiCallWithTimeeotActionFilter))]
public class MyController : ControllerBase { /* ... */ }
Method 2: Middlewares
Another option is to use a middleware
class ApiCallTimeoutMiddleware
{
private TimeoutRunner _runner;
public ApiCallTimeoutMiddleware(TimeoutRunner runner)
{
_runner = runner;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
// run a task before every request
var result = await _runner.RunAsync(
async (CancellationToken token) =>
{
await Task.Delay(TimeSpan.FromSeconds(20), token);
return 42;
},
default
);
await next(context);
}
}
then attach the middleware in Startup.Configure method:
public void Configure(IApplicationBuilder app)
{
app.UseMiddleware<ApiCallTimeoutMiddleware>();
app.UseRouting();
app.UseEndpoints(e => e.MapControllers());
}

Generic async try catch on base API - ASP NET CORE

I have created a generic try/catch method on base API on a net core 2.2 project, and I am not sure about perfomance of this generic method. Is this a good way to do it?
This is on base api:
protected async Task<IActionResult> TryReturnOk<TReturn>(Func<Task<TReturn>> function)
{
try
{
var result = await function();
return Ok(result);
}
catch (Exception ex)
{
_fileLogger.LogError(ex.Message);
_fileLogger.LogError(ex.StackTrace);
return BadRequest(ex);
}
}
And it is used on post method in the api-s like:
public async Task<IActionResult> Post([FromBody] LogViewModel log)
{
return await TryReturnOk(() => _writeLogService.WriteLog(log));
}
Instead of cluttering up all your controllers, I would centralized logging to middleware like below.
400 Bad Request should be used when e.g. request model is not valid. When an exception is thrown, 500 is more appropriate.
public class LoggerMiddleware
{
private readonly ILogger _fileLogger;
private readonly RequestDelegate _next;
public LoggerMiddleware(RequestDelegate next, ILogger fileLogger)
{
_next = next;
_fileLogger = fileLogger;
}
public async Task Invoke(HttpContext context)
{
try
{
await _next.Invoke(context);
}
catch (Exception ex)
{
_fileLogger.LogError(ex.Message);
_fileLogger.LogError(ex.StackTrace);
context.Response.Clear();
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
}
}
}
public static class MiddlewareExtensions
{
public static IApplicationBuilder UseLoggerMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<LoggerMiddleware>();
}
}
In Startup#Configure
app.UseLoggerMiddleware()

Why ASP.NET Core MVC Route cannot match up "//" like "https://localhost:5001//?id=123"? [duplicate]

I need to handle an incoming request which is of the form:
//ohif/study/1.1/series
Note the exta slash at the front
My controller signature is:
[Route("ohif/study/{studyUid}/series")]
[HttpGet]
public IActionResult GetStudy(string studyUid)
If I modify the incoming request to /ohif/study/1.1/series it works fine
however when I use //ohif/study/1.1/series, the route is not hit
Additionally I also tried: [Route("/ohif/study/{studyUid}/series")]
and [Route("//ohif/study/{studyUid}/series")]
Both fail. I unfortunately cannot change the incoming request as it is from an external application. Is there some trick to handle this route? I am working in .NET Core 3.0.
Update NOTE:
I have logging activated and I see that asp.net core is analyzing the route, I have the message:
No candidates found for the request path '//ohif/study/1.1/series'
for the logger Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware
What about the middleware to handle double slash?
app.Use((context, next) =>
{
if (context.Request.Path.Value.StartsWith("//"))
{
context.Request.Path = new PathString(context.Request.Path.Value.Replace("//", "/"));
}
return next();
});
Rewrite the URL at the web server-level, e.g. for IIS, you can use the URL Rewrite Module to automatically redirect //ohif/study/1.1/series to /ohif/study/1.1/series. This isn't a job for your application.
I took Ravi's answer and fleshed out a middleware. The middleware is nice because it is encapsulated, easily testable, can inject a logger, more readable, etc.
app.UseDoubleSlashHandler();
The code and tests:
public class DoubleSlashMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<DoubleSlashMiddleware> _logger;
public DoubleSlashMiddleware(RequestDelegate next, ILogger<DoubleSlashMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
_logger.LogInformation($"Invoking {nameof(DoubleSlashMiddleware)} on {context.Request.Path}");
context.Request.Path = context.Request.Path.FixDoubleSlashes();
// Call the next delegate/middleware in the pipeline.
await _next(context);
}
}
public static class DoubleSlashMiddlewareExtensions
{
public static IApplicationBuilder UseDoubleSlashHandler(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<DoubleSlashMiddleware>();
}
}
[TestClass()]
public class DoubleSlashMiddlewareTests
{
private DoubleSlashMiddleware _sut;
private ILogger<DoubleSlashMiddleware> _logger;
private bool _calledNextMiddlewareInPipeline;
[TestInitialize()]
public void TestInitialize()
{
_logger = Substitute.For<ILogger<DoubleSlashMiddleware>>();
Task Next(HttpContext _)
{
_calledNextMiddlewareInPipeline = true;
return Task.CompletedTask;
}
_sut = new DoubleSlashMiddleware(Next, _logger);
}
[TestMethod()]
public async Task InvokeAsync()
{
// Arrange
_calledNextMiddlewareInPipeline = false;
// Act
await _sut.InvokeAsync(new DefaultHttpContext());
// Assert
_logger.ReceivedWithAnyArgs(1).LogInformation(null);
Assert.IsTrue(_calledNextMiddlewareInPipeline);
}
}
String method to do the replacement:
public static class RoutingHelper
{
public static PathString FixDoubleSlashes(this PathString path)
{
if (string.IsNullOrWhiteSpace(path.Value))
{
return path;
}
if (path.Value.Contains("//"))
{
return new PathString(path.Value.Replace("//", "/"));
}
return path;
}
}
[TestClass()]
public class RoutingHelperTests
{
[TestMethod()]
[DataRow(null, null)]
[DataRow("", "")]
[DataRow("/connect/token", "/connect/token")]
[DataRow("//connect/token", "/connect/token")]
[DataRow("/connect//token", "/connect/token")]
[DataRow("//connect//token", "/connect/token")]
[DataRow("/connect///token", "/connect/token")]
public void FixDoubleSlashes(string input, string expected)
{
// Arrange
var path = new PathString(input);
// Act
var actual = path.FixDoubleSlashes();
// Assert
Assert.AreEqual(expected, actual.Value);
}
}

GraphQL authentication with Asp.net core using JWT

I am using for GraphQL for .NET package for graphql. But I couldn't understand how can I authentication with JWT in graphql query or mutation.
I read the guide about authorization but I couldn't accomplish.
I need help with GraphQL for .NET authentication.
Any help will be appreciated.
Thanks
The guide is around authorization. The step you're looking for is the authentication and since graphql can be implemented using a ASP.Net API controller, you can implement JWT authentication as you would with any controller.
Here is a sample grapql controller using an Authorize attribute. You could, however, implement this using filter or if you want full control, custom middleware.
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class GraphQLController : ControllerBase
{
private readonly IDocumentExecuter executer;
private readonly ISchema schema;
public GraphQLController(IDocumentExecuter executer, ISchema schema)
{
this.executer = executer;
this.schema = schema;
}
[HttpPost]
public async Task<ActionResult<object>> PostAsync([FromBody]GraphQLQuery query)
{
var inputs = query.Variables.ToInputs();
var queryToExecute = query.Query;
var result = await executer.ExecuteAsync(o => {
o.Schema = schema;
o.Query = queryToExecute;
o.OperationName = query.OperationName;
o.Inputs = inputs;
o.ComplexityConfiguration = new GraphQL.Validation.Complexity.ComplexityConfiguration { MaxDepth = 15};
o.FieldMiddleware.Use<InstrumentFieldsMiddleware>();
}).ConfigureAwait(false);
return this.Ok(result);
}
}
public class GraphQLQuery
{
public string OperationName { get; set; }
public string Query { get; set; }
public Newtonsoft.Json.Linq.JObject Variables { get; set; }
}
In the Startup.cs I have configured JWT bearer token authentication.
Hope this helps.
I myself struggled for two days as well. I'm using https://github.com/graphql-dotnet/authorization now with the setup from this comment (from me): https://github.com/graphql-dotnet/authorization/issues/63#issuecomment-553877731
In a nutshell, you have to set the UserContext for the AuthorizationValidationRule correctly, like so:
public class Startup
{
public virtual void ConfigureServices(IServiceCollection services)
{
...
services.AddGraphQLAuth(_ =>
{
_.AddPolicy("AdminPolicy", p => p.RequireClaim("Role", "Admin"));
});
services.AddScoped<IDependencyResolver>(x => new FuncDependencyResolver(x.GetRequiredService));
services.AddScoped<MySchema>();
services
.AddGraphQL(options => { options.ExposeExceptions = true; })
.AddGraphTypes(ServiceLifetime.Scoped);
...
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IServiceProvider serviceProvider)
{
...
app.UseMiddleware<MapRolesForGraphQLMiddleware>(); // optional, only when you don't have a "Role" claim in your token
app.UseGraphQL<MySchema>();
...
}
}
public static class GraphQLAuthExtensions
{
public static void AddGraphQLAuth(this IServiceCollection services, Action<AuthorizationSettings> configure)
{
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddSingleton<IAuthorizationEvaluator, AuthorizationEvaluator>();
services.AddTransient<IValidationRule, AuthorizationValidationRule>();
services.AddTransient<IUserContextBuilder>(s => new UserContextBuilder<GraphQLUserContext>(context =>
{
var userContext = new GraphQLUserContext
{
User = context.User
};
return Task.FromResult(userContext);
}));
services.AddSingleton(s =>
{
var authSettings = new AuthorizationSettings();
configure(authSettings);
return authSettings;
});
}
}
public class GraphQLUserContext : IProvideClaimsPrincipal
{
public ClaimsPrincipal User { get; set; }
}
public class MapRolesForGraphQLMiddleware
{
private readonly RequestDelegate _next;
public MapRolesForGraphQLMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
// custom mapping code to end up with a "Role" claim
var metadata = context.User.Claims.SingleOrDefault(x => x.Type.Equals("metadata"));
if (metadata != null)
{
var roleContainer = JsonConvert.DeserializeObject<RoleContainer>(metadata.Value);
(context.User.Identity as ClaimsIdentity).AddClaim(new Claim("Role", string.Join(", ", roleContainer.Roles)));
}
await _next(context);
}
}
public class RoleContainer
{
public String[] Roles { get; set; }
}

Validating AntiForgery token globally in ASP.NET Core

I want to validate AntiForgery token in ASP.NET Core application. I know i can individually do that by adding [AutoValidateAntiforgeryToken] or [ValidateAntiforgeryToken] attributes on Action methods as suggested in SO post here
I'm looking for global approach to validate token for all POST methods. So i created a middleware to do so. However i could not find suitable method to validate the token. Like in classic asp.net there is AntiForgery.Validate().
What's the equivalent method in ASP.NET Core
public class ValidateAntiForgeryTokenMiddleware
{
private readonly RequestDelegate _next;
public ValidateAntiForgeryTokenMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext httpContext)
{
if (httpContext.Request.Method.ToUpper() == "POST")
{
// where does this mehod exists?
// i could not find it in Microsoft.AspNetCore.Antiforgery namespace
AntiForgery.Validate();
}
await _next(httpContext);
}
}
public static class ValidateAntiForgeryTokenMiddlewareExtensions
{
public static IApplicationBuilder UseValidateAntiForgeryToken(this IApplicationBuilder builder)
{
return builder.UseMiddleware<ValidateAntiForgeryTokenMiddleware>();
}
}
I have to Inject Antiforgery as service
public class ValidateAntiForgeryTokenMiddleware
{
private readonly RequestDelegate _next;
private readonly IAntiforgery _antiforgery;
public ValidateAntiForgeryTokenMiddleware(RequestDelegate next, IAntiforgery antiforgery)
{
_next = next;
_antiforgery = antiforgery;
}
public async Task Invoke(HttpContext httpContext)
{
if (httpContext.Request.Method.ToUpper() == "POST")
{
await _antiforgery.ValidateRequestAsync(httpContext);
}
await _next(httpContext);
}
}
add Antiforgery as service in startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddAntiforgery();
}
Use my middlware
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IApplicationLifetime appLifetime)
{
app.UseValidateAntiForgeryToken();
}