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

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());
}

Related

Differ IOutputFormatter per endpoint in ASP.NET Core 6

I have a legacy ASP.NET Web API 2 app which must be ported to ASP.NET Core 6 and it has the following behaviour:
Some controllers return responses in Pascal-case Json
Some controllers return responses in camel-case Json
All controllers have the same authentication/authorization, but they return different objects using different serializers for 401/403 cases.
In ASP.NET Web API 2 it was easily solved with IControllerConfiguration (to set the formatter for a controller), AuthorizeAttribute (to throw exceptions for 401/403), ExceptionFilterAttribute to set 401/403 status code and response which will be serialized using correct formatter.
In ASP.NET Core, it seems that IOutputFormatter collection is global for all controllers and it is not available during UseAuthentication + UseAuthorization pipeline where it terminates in case of failure.
Best I could come up with is to always "succeed" in authentication / authorization with some failing flag in claims and add IActionFilter as first filter checking those flags, but it looks very hacky.
Is there some better approach?
Update1:
Implementing different output formatters for IActionResult from controller or IFilter (including IExceptionFilter) is not very difficult.
What I want is to be able to either set IActionResult or use IOutputFormatter related to Action identified by UseRouting for Authentication/Authorization error or IAuthorizationHandler, but looks like all those auth steps are invoked before either ActionContext or IOutputFormatter is invoked.
So 2 approaches I see now:
hack auth code to "always pass" and handle HttpContext.Items["MyRealAuthResult"] object in IActionFilter
expose V1OutputFormatter/V2OutputFormatter in a static field and duplicate selection logic in HandleChallengeAsync/HandleForbiddenAsync based on to what controller/action it was routed from UseRouting step.
Here is sample app that uses auth and has 2 endpoints:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IConfigureOptions<MvcOptions>, MvcOptionsSetup>();
builder.Services.AddAuthentication(options =>
{
options.AddScheme<DefAuthHandler>("defscheme", "defscheme");
});
builder.Services.AddAuthorization(options =>
options.DefaultPolicy = new AuthorizationPolicyBuilder("defscheme")
.RequireAssertion(context =>
// false here should result in Pascal case POCO for WeatherForecastV1Controller
// and camel case POCO for WeatherForecastV2Controller
context.User.Identities.Any(c => c.AuthenticationType == "secretheader"))
.Build())
.AddSingleton<IAuthorizationMiddlewareResultHandler, AuthorizationResultHandler>();
builder.Services.AddControllers();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
public class AuthorizationResultHandler : IAuthorizationMiddlewareResultHandler
{
private readonly AuthorizationMiddlewareResultHandler _handler;
public AuthorizationResultHandler()
{
_handler = new AuthorizationMiddlewareResultHandler();
}
public async Task HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult)
{
// Can't set ActionContext.Response here or use IOutputFormatter
await _handler.HandleAsync(next, context, policy, authorizeResult);
}
}
public class DefAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public DefAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock) { }
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new List<ClaimsIdentity>();
if (Request.Headers.ContainsKey("secretheader")) claims.Add(new ClaimsIdentity("secretheader"));
return AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(claims), "defscheme"));
}
}
public class MvcOptionsSetup : IConfigureOptions<MvcOptions>
{
private readonly ArrayPool<char> arrayPool;
private readonly MvcNewtonsoftJsonOptions mvcNewtonsoftJsonOptions;
public MvcOptionsSetup(ArrayPool<char> arrayPool, IOptions<MvcNewtonsoftJsonOptions> mvcNewtonsoftJsonOptions)
{
this.arrayPool = arrayPool;
this.mvcNewtonsoftJsonOptions = mvcNewtonsoftJsonOptions.Value;
}
public void Configure(MvcOptions options)
{
options.OutputFormatters.Insert(0, new V1OutputFormatter(arrayPool, options, mvcNewtonsoftJsonOptions));
options.OutputFormatters.Insert(0, new V2OutputFormatter(arrayPool, options, mvcNewtonsoftJsonOptions));
}
}
public class V1OutputFormatter : NewtonsoftJsonOutputFormatter
{
public V1OutputFormatter(ArrayPool<char> charPool, MvcOptions mvcOptions, MvcNewtonsoftJsonOptions? jsonOptions)
: base(new JsonSerializerSettings { ContractResolver = new DefaultContractResolver() }, charPool, mvcOptions, jsonOptions) { }
public override bool CanWriteResult(OutputFormatterCanWriteContext context)
{
var controllerDescriptor = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata<Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor>();
return controllerDescriptor?.ControllerName == "WeatherForecastV1";
}
}
public class V2OutputFormatter : NewtonsoftJsonOutputFormatter
{
public V2OutputFormatter(ArrayPool<char> charPool, MvcOptions mvcOptions, MvcNewtonsoftJsonOptions? jsonOptions)
: base(new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }, charPool, mvcOptions, jsonOptions) { }
public override bool CanWriteResult(OutputFormatterCanWriteContext context)
{
var controllerDescriptor = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata<Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor>();
return controllerDescriptor?.ControllerName == "WeatherForecastV2";
}
}
[ApiController]
[Authorize]
[Route("v1/weatherforecast")]
public class WeatherForecastV1Controller : ControllerBase
{
[HttpGet]
public IActionResult Get()
{
// This must be Pascal case
return Ok(new WeatherForecast() { Summary = "summary" });
}
}
[ApiController]
[Authorize]
[Route("v2/weatherforecast")]
public class WeatherForecastV2Controller : ControllerBase
{
[HttpGet]
public IActionResult Get()
{
// This must be camel case
return Ok(new WeatherForecast() { Summary = "summary" });
}
}
If there is no way to configure controllers independently, then you could use some middleware to convert output from selected controllers that meet a path-based predicate.
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapWhen(ctx => ctx.Request.Path.Containes("v2/"), cfg =>
{
app.UseMiddleware<JsonCapitalizer>();
});
app.Run();
And then create a JsonCapitalizer class to convert output from any path that contains "v2/". Note, this middleware will not run if the predicate in MapWhen is not satisfied.
public class JsonCapitalizer
{
readonly RequestDelegate _nextRequestDelegate;
public RequestLoggingMiddleware(
RequestDelegate nextRequestDelegate)
{
_nextRequestDelegate = nextRequestDelegate;
}
public async Task Invoke(HttpContext httpContext)
{
await _nextRequestDelegate(httpContext);
// Get the httpContext.Response
// Capitalize it
// Rewrite the response
}
}
There may be better ways, but that's the first that comes to mind.
The following link will help with manipulation of the response body:
https://itecnote.com/tecnote/c-how-to-read-asp-net-core-response-body/
I also faced such a problem in ASP Core 7 and ended up with writing an attribute.
So the attribute will be applied on each Action where the response type has to be converted. You can write many an attribute for camelcase response and another attribute for pascalcase. The attribute will look like below for CamelCase
public class CamelCaseAttribute : ActionFilterAttribute
{
private static readonly SystemTextJsonOutputFormatter formatter = new SystemTextJsonOutputFormatter(new()
{
ReferenceHandler = ReferenceHandler.IgnoreCycles,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
public override void OnActionExecuted(ActionExecutedContext context)
{
if (context.Result is ObjectResult objectResult)
{
objectResult.Formatters
.RemoveType<NewtonsoftJsonOutputFormatter>();
objectResult.Formatters.Add(formatter);
}
else
{
base.OnActionExecuted(context);
}
}
}
And on the Contoller Action you can use it like below
[CamelCase]
public async IAsyncEnumerable<ResponseResult<IReadOnlyList<VendorBalanceReportDto>>> VendorBalanceReport([FromQuery] Paginator paginator, [FromQuery] VendorBalanceReportFilter filter, [EnumeratorCancellation] CancellationToken token)
{
var response = _reportService.VendorBalanceReport(paginator, filter, token);
await foreach (var emailMessage in response)
{
yield return emailMessage;
}
}

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);
}
}

IdentityServer4 Reject Token Request If Custom Parameter Not Valid

I have this test client sending RequestToken:
var tokenResponse = await client.RequestTokenAsync(new TokenRequest
{
Address = disco.TokenEndpoint,
GrantType = "password",
ClientId = "My_Client",
ClientSecret = "mysecret",
Parameters =
{
{ "username", "user#entity.com" },
{ "password", "userpassword" },
{ "logged_entity_id", "143" },
{ "scope", "MyAPI" }
}
});
Now each user has a list of entity and I want to reject the token request if the value in the parameter "logged_entity_id" does not exist in the user's list of entity.
I was initially planning on checking it via IsActiveSync in my CustomProfileService but I can't seem to access the raw parameters in IsActiveSync method.
public class CustomProfileService : IProfileService
{
protected UserManager<User> _userManager;
public CustomProfileService(UserManager<User> userManager)
{
_userManager = userManager;
}
public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var claims = new List<Claim>
{
new Claim("LoggedEntityId", context.ValidatedRequest.Raw["logged_entity_id"])
};
context.IssuedClaims.AddRange(claims);
return Task.FromResult(0);
}
public Task IsActiveAsync(IsActiveContext context)
{
var user = _userManager.GetUserAsync(context.Subject).Result;
// var entityId = Can't access logged_entity_id parameter here
context.IsActive = user != null && user.DeletingDate == null && user.entities.Contains(entityId);
return Task.FromResult(0);
}
}
I'm not really sure if this is where I should check and reject it.
In asp.net core you can register a dependency using the built-in dependency injection container. The dependency injection container supplies the IHttpContextAccessor to any classes that declare it as a dependency in their constructors:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddHttpContextAccessor();
...
}
Then in your class ,for example , in the implement of IProfileService :
private readonly IHttpContextAccessor _httpContextAccessor;
public CustomProfileService(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
Then in IsActiveAsync method get the value by :
var id = _httpContextAccessor.HttpContext.Request.Form["logged_entity_id"].ToString();
You can implement ICustomTokenValidator to validate token's request on your own way
You can run custom code as part of the token issuance pipeline at the token endpoint. This allows e.g. for
adding additional validation logic
changing certain parameters (e.g.token lifetime) dynamically
public class CustomValidator : ICustomTokenRequestValidator
{
public Task<TokenValidationResult> ValidateAccessTokenAsync(TokenValidationResult result)
{
throw new NotImplementedException();
}
public Task<TokenValidationResult> ValidateIdentityTokenAsync(TokenValidationResult result)
{
throw new NotImplementedException();
}
}
and in your startup.cs:
services.AddIdentityServer(options =>
{
...
})
.AddCustomTokenRequestValidator<CustomValidator>();

How do I push data from hub to client every second using SignalR

Working on building signalR hub, I'm able to get data from hub to the client but I'm, not sure how do I push it every 1 second.
I'm not sure where do I set the timer in the controller where getApps method exists or in the hub?
Hub:
public class nphub : Hub
{
public readonly sbController _sbcontroller;
public nphub(sbController sbcontroller)
{
_sbcontroller = sbcontroller;
}
public async Task NotifyConnection()
{
IActionResult result = await _sbcontroller.getApps();
await Clients.All.SendAsync("TestBrodcasting", result);
}
}
In Controller:
public async Task<IActionResult> getApps()
{
// var request = new HttpRequestMessage(HttpMethod.Get, "apps");
// var response = await _client_NP.SendAsync(request);
// var json = await response.Content.ReadAsStringAsync();
return Ok($"Testing a Basic HUB at {DateTime.Now.ToLocalTime()}");
}
Client:
let connection = new signalR.HubConnectionBuilder()
.withUrl("/nphub").build();
connection.start().then(function () {
TestConnection();
}).catch(function (err) {
return console.error(err.toString());
});
function TestConnection() {
connection.invoke("NotifyConnection").catch(function (err) {
return console.error(err.toString());
});
}
connection.on("TestBrodcasting", function (time) {
document.getElementById('broadcastDiv').innerHTML = time.value;
document.getElementById('broadcastDiv').style.display = "block";
});
Just for the test purpose to see realtime changes, I'm trying to return time. I'm able to see time on the client but it's not changing.
You need to use a hosted service, as described in the docs. Add a class like:
internal class SignalRTimedHostedService : IHostedService, IDisposable
{
private readonly IHubContext<nphub> _hub;
private readonly ILogger _logger;
private Timer _timer;
public SignalRTimedHostedService(IHubContext<nphub> hub, ILogger<SignalRTimedHostedService> logger)
{
_hub = hub;
_logger = logger;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Timed Background Service is starting.");
_timer = new Timer(DoWork, null, TimeSpan.Zero,
TimeSpan.FromSeconds(1));
return Task.CompletedTask;
}
private void DoWork(object state)
{
_logger.LogInformation("Timed Background Service is working.");
// send message using _hub
}
public Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Timed Background Service is stopping.");
_timer?.Change(Timeout.Infinite, 0);
return Task.CompletedTask;
}
public void Dispose()
{
_timer?.Dispose();
}
}
Note: A hosted service lives in singleton scope. You can inject IHubContext<T> directly, though, because it too is in singleton scope.
Then in ConfigureServices:
services.AddHostedService<SignalRTimedHostedService>();

ASP .NET Core webapi set cookie in middleware

I'm trying to set a cookie after the action is executed, struggling to get this working. I managed to see the cookie if I set it from a controller, but not from a middleware.
I have played with the order of the configuration and nothing.
The code sample is from a clean webapi created project, so if someone wants to play with it is simple, just create an empty webapi, add the CookieSet class and replace the Startup class with the one below (only added are the cookie policy options)
Here is my middleware
public class CookieSet
{
private readonly RequestDelegate _next;
public CookieSet(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
await _next.Invoke(context);
var cookieOptions = new CookieOptions()
{
Path = "/",
Expires = DateTimeOffset.UtcNow.AddHours(1),
IsEssential = true,
HttpOnly = false,
Secure = false,
};
context.Response.Cookies.Append("test", "cookie", cookieOptions);
}
}
I have added the p assignment and checked that the execution never gets there, on the Cookies.Append line it stops the execution, so there is something going on I can't figure it out.
And here is my Startup class
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => false;
options.MinimumSameSitePolicy = SameSiteMode.None;
options.HttpOnly = HttpOnlyPolicy.None;
options.Secure = CookieSecurePolicy.None;
// you can add more options here and they will be applied to all cookies (middleware and manually created cookies)
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseCookiePolicy(new CookiePolicyOptions
{
CheckConsentNeeded = c => false,
HttpOnly = HttpOnlyPolicy.None,
Secure = CookieSecurePolicy.None,
MinimumSameSitePolicy = SameSiteMode.None,
});
app.UseMiddleware<CookieSet>();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMvc();
}
}
I have set all the options to the minimum requirements, tested with chrome and fiddler and nothing.
Ok, I'm talking to myself, but this is for the community...
Got this working after digging into the AspNetCore code.
Basically the cookie must be set on the callback OnStarting of the context response.
Here is the code of the middleware that makes the trick
public class CookieSet
{
private readonly RequestDelegate _next;
private readonly ASessionOptions _options;
private HttpContext _context;
public CookieSet(RequestDelegate next, IOptions<ASessionOptions> options)
{
_next = next;
_options = options.Value;
}
public async Task Invoke(HttpContext context)
{
_context = context;
context.Response.OnStarting(OnStartingCallBack);
await _next.Invoke(context);
}
private Task OnStartingCallBack()
{
var cookieOptions = new CookieOptions()
{
Path = "/",
Expires = DateTimeOffset.UtcNow.AddHours(1),
IsEssential = true,
HttpOnly = false,
Secure = false,
};
_context.Response.Cookies.Append("MyCookie", "TheValue", cookieOptions);
return Task.FromResult(0);
}
}
The AspNetCore team uses an internal class for that.
Checking the SessionMiddleware class, part of the code is as follows (removed a lot of things just for the sake of the answer):
public class SessionMiddleware
{
public async Task Invoke(HttpContext context)
{
// Removed code here
if (string.IsNullOrWhiteSpace(sessionKey) || sessionKey.Length != SessionKeyLength)
{
// Removed code here
var establisher = new SessionEstablisher(context, cookieValue, _options);
tryEstablishSession = establisher.TryEstablishSession;
isNewSessionKey = true;
}
// Removed code here
try
{
await _next(context);
}
// Removed code here
}
//Now the inner class
private class SessionEstablisher
{
private readonly HttpContext _context;
private readonly string _cookieValue;
private readonly SessionOptions _options;
private bool _shouldEstablishSession;
public SessionEstablisher(HttpContext context, string cookieValue, SessionOptions options)
{
_context = context;
_cookieValue = cookieValue;
_options = options;
context.Response.OnStarting(OnStartingCallback, state: this);
}
private static Task OnStartingCallback(object state)
{
var establisher = (SessionEstablisher)state;
if (establisher._shouldEstablishSession)
{
establisher.SetCookie();
}
return Task.FromResult(0);
}
private void SetCookie()
{
var cookieOptions = _options.Cookie.Build(_context);
var response = _context.Response;
response.Cookies.Append(_options.Cookie.Name, _cookieValue, cookieOptions);
var responseHeaders = response.Headers;
responseHeaders[HeaderNames.CacheControl] = "no-cache";
responseHeaders[HeaderNames.Pragma] = "no-cache";
responseHeaders[HeaderNames.Expires] = "-1";
}
// Returns true if the session has already been established, or if it still can be because the response has not been sent.
internal bool TryEstablishSession()
{
return (_shouldEstablishSession |= !_context.Response.HasStarted);
}
}
}
.NET 5
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// ........
app.Use(async (context, next) =>
{
var cookieOptions = new CookieOptions()
{
Path = "/",
Expires = DateTimeOffset.UtcNow.AddHours(1),
IsEssential = true,
HttpOnly = false,
Secure = false,
};
context.Response.Cookies.Append("MyCookie", "TheValue", cookieOptions);
await next();
});
// ........
}