Can I pass CancellationToken as parameter in AspNetCore WebAPI - asp.net-core

As far as I know if I use services.AddControllers() or services.AddMvc()extension in my Startup.cs "MVC will automatically bind any CancellationToken parameters in an action method.
I have the following TestController and TestService as Transient service.
According to this informations, when the auto-binded CancellationToken IsCancellationRequested will the tokens that I have passed as parameters be also canceled?
public class TestController : ControllerBase
{
private readonly ITestService _testService;
public TestController(ITestService testService)
{
_testService = testService;
}
[HttpGet, ActionName("Get")]
public async Task<IActionResult> GetAsync(CancellationToken cancellationToken)
{
await _testService.GetAsync(cancellationToken);
return Ok();
}
}
public class TestService : ITestService
{
public async Task GetAsync(CancellationToken cancellationToken)
{
//I also send the cancellationToken as a parameter to external API calls
}
}

when the auto-binded CancellationToken IsCancellationRequested will
the tokens that I have passed as parameters be also canceled?
By injecting a CancellationToken into your action method, which will be automatically bound to the HttpContext.RequestAborted token for the request. After the request is cancelled by the user refreshing the browser or click the "stop" button, the original request is aborted with a TaskCancelledException which propagates back through the MVC filter pipeline, and back up the middleware pipeline. Then, You could check the value of IsCancellationRequested and exit the action gracefully. In this scenario, there is no need to transfer the CancellationToken as parameters.
If you want to Cancel an Async task, since, CancellationTokens are lightweight objects that are created by a CancellationTokenSource. When a CancellationTokenSource is cancelled, it notifies all the consumers of the CancellationToken. So, you could call the Cancel() method to cancel the task.
Check the following sample:
var cts = new CancellationTokenSource();
//cts.CancelAfter(5000);//Request Cancel after 5 seconds
_logger.LogInformation("New Test start");
var newTask = Task.Factory.StartNew(state =>
{
int i = 1;
var token = (System.Threading.CancellationToken)state;
while (true)
{
Console.WriteLine(i);
i++;
if (i == 10)
{
cts.Cancel(); //call the Cancel method to cancel.
}
_logger.LogInformation("thread running " + DateTime.Now.ToString());
Thread.Sleep(1000);
token.ThrowIfCancellationRequested();
}
}, cts.Token, cts.Token);
try
{
newTask.Wait(10000);
}
catch
{
Console.WriteLine("Catch:" + newTask.Status);
}
_logger.LogInformation("Test end");
More detail information about using CancellationToken, please check the following articles:
Using CancellationTokens in ASP.NET Core MVC controllers

Related

SaveChangesOnDbContextAsync has never been called

ABP has added an overridable function called SaveChangesOnDbContextAsync that allow us to intercept any insert/update/delete operation as in this link: https://github.com/abpframework/abp/issues/4659
I tried to override SaveChangesOnDbContextAsync in a class that inherits AbpDbContext like the following but it has never been called whenever I update the data:
public override async Task<int> SaveChangesOnDbContextAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
{
var result = await base.SaveChangesAsync();
return result;
}
That method is provided for your application code to bypass the logic in AbpDbContext.SaveChangesAsync(). ABP does not call it.
This is what the requestor meant:
public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
{
try
{
return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}
catch (AbpDbConcurrencyException)
{
// Handle it
// ...
// Directly call Microsoft.EntityFrameworkCore.DbContext's SaveChangesAsync(bool, CancellationToken) to not repeatedly
// create audit logs, update concurrencystamps, or trigger entity events etc. as AbpDbContext's version would do
return await base.SaveChangesOnDbContextAsync(acceptAllChangesOnSuccess, cancellationToken);
}
}
You probably just want to override SaveChangesAsync instead.

Blazor WASM controller: read request body causes the IIS process to crash

So I am trying to simply read the body (with string content) in a Blazor WASM ApiController. My code on the server-side:
[AllowAnonymous]
[ApiController]
[Route("[controller]")]
public class SmartMeterDataController : ControllerBase
{
[HttpPost("UploadData")]
public async void UploadData()
{
string body = null;
if (Request.Body.CanRead && (Request.Method == HttpMethods.Post || Request.Method == HttpMethods.Put))
{
Request.EnableBuffering();
Request.Body.Position = 0;
body = await new StreamReader(Request.Body).ReadToEndAsync();
}
}
}
My app builder in Program.cs is pretty much out of the box:
//enable REST API controllers
var mvcBuillder = builder.Services.AddMvcCore(setupAction: options => options.EnableEndpointRouting = false).ConfigureApiBehaviorOptions(options => //activate MVC and configure error handling
{
options.InvalidModelStateResponseFactory = context => //error 400 (bad request)
{
JsonApiErrorHandler.HandleError400BadRequest(context);
return new Microsoft.AspNetCore.Mvc.BadRequestObjectResult(context.ModelState);
};
});
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
...
app.UseRouting();
app.UseMvcWithDefaultRoute();
app.MapRazorPages();
app.MapControllers();
The request body looks like this:
{"api_key":"K12345667565656", "field1":"1.10", "field2":"0.76",
"field3":"0.65", "field4":"455", "field5":"0", "field6":"1324",
"field7":"433761", "field8":"11815" }
Yes, this is JSON. No, I don't want to parse it with [FromBody] or similar.
POSTing to this endpoint causes the following exception (as seen in the Windows event viewer thingy):
Application: w3wp.exe
CoreCLR Version: 6.0.1222.56807
.NET Version: 6.0.12
Description: The process was terminated due to an unhandled exception.
Exception Info: System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'HttpRequestStream'.
at Microsoft.AspNetCore.Server.IIS.Core.HttpRequestStream.ValidateState(CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.IIS.Core.HttpRequestStream.ReadAsync(Memory`1 destination, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.IIS.Core.WrappingStream.ReadAsync(Memory`1 destination, CancellationToken cancellationToken)
at Microsoft.AspNetCore.WebUtilities.FileBufferingReadStream.ReadAsync(Memory`1 buffer, CancellationToken cancellationToken)
at System.IO.StreamReader.ReadBufferAsync(CancellationToken cancellationToken)
at System.IO.StreamReader.ReadToEndAsyncInternal()
After that, a second error is always logged. It states something like it is described here.
Note that it's usually not the first, but the second or third POST that causes this. After this, the error keeps happening with every POST and after a short while the application stops working and the Windows Server 2019 need to be rebooted.
According to the internet, the code should work. Anyone have a guess why it doesn't?
I use this HttpContext extension method to read the request body and cache it in the context in case needed later in the pipeline. It works for me.
Notice the condition around EnableBuffering. Perhaps adding that condition to your code will help.
public static async Task<string> GetRequestBodyAsStringAsync(
this HttpContext httpContext)
{
if (httpContext.Items.TryGetValue("BodyAsString", out object? value))
return (string)value!;
if (!httpContext.Request.Body.CanSeek)
{
// We only do this if the stream isn't *already* rewindable,
// as EnableBuffering will create a new stream instance
// each time it's called
httpContext.Request.EnableBuffering();
}
httpContext.Request.Body.Position = 0;
StreamReader reader = new(httpContext.Request.Body, Encoding.UTF8);
string bodyAsString = await reader.ReadToEndAsync().ConfigureAwait(false);
httpContext.Request.Body.Position = 0;
httpContext.Items["BodyAsString"] = bodyAsString;
return bodyAsString;
}
EDIT ...
Possibly, your issue could also be related to fact your controller method is returning a void instead of Task?
Finally, I found the original article I used for my extension method. Interestingly, if you that extension method for the FIRST time after model-binding then it won't work (in my project I do call it from middleware).
https://markb.uk/asp-net-core-read-raw-request-body-as-string.html
Adding:
public class EnableRequestBodyBufferingMiddleware
{
private readonly RequestDelegate _next;
public EnableRequestBodyBufferingMiddleware(RequestDelegate next) =>
_next = next;
public async Task InvokeAsync(HttpContext context)
{
context.Request.EnableBuffering();
await _next(context);
}
}
and
app.UseMiddleware<EnableRequestBodyBufferingMiddleware>();
may therefore also help.

How to set custom DelegatingHandler to all HttpClients automatically?

I would like to use the LoggingHttpClientHandler for all the clients in the application. I found the only usable way via named/typed client
startup.cs
services.AddTransient<LoggingHttpClientHandler>();
services.AddHttpClient("clientWithLogging").AddHttpMessageHandler<LoggingHttpClientHandler>();
and then in each service I have to use var client = _clientFactory.CreateClient("clientWithLogging") which is kinda uncomfortable.
LoggingHttpClientHandler.cs
public class LoggingHttpClientHandler : DelegatingHandler
{
public LoggingHttpClientHandler(HttpMessageHandler innerHandler) : base(innerHandler)
{
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (request.Content != null)
{
Logging.Log.Info(await request.Content.ReadAsStringAsync());
}
return await base.SendAsync(request, cancellationToken);
}
}
Is there a way how to do it without naming each client?
If it is just about the name, there is an extension method CreateClient that takes no arguments and uses the default name, which is string.Empty.
So you might be fine by registering the service using string.Empty as name, e.g.:
services.AddHttpClient(string.Empty).AddHttpMessageHandler<LoggingHttpClientHandler>();
and instantiate clients using:
var client = _clientFactory.CreateClient();
The problem was with resolving of params in useless constructor of LoggingHttpClientHandler.
So I removed the constructor.

Running multiple backend services using iHostedService

Currently my web API is able to run on a schedule and trigger another end point in order to sync data. The services that needs to be called are stored in a yml file. I have managed to get it working for one service to run a schedule. What I want is to be able to save multiple endpoints with schedules of their own and for them to be scheduled and executed at the right time.
Here is the code that I have now
I have done this using iHostedService interface.
This is the HostService class that implements iHostedService
public abstract class HostedService : IHostedService
{
private Task _executingTask;
private CancellationTokenSource _cts;
public Task StartAsync(CancellationToken cancellationToken)
{
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_executingTask = ExecuteAsync(_cts.Token);
// If the task is completed then return it, otherwise it's running
return _executingTask.IsCompleted ? _executingTask : Task.CompletedTask;
}
public async Task StopAsync(CancellationToken cancellationToken)
{
// Stop called without start
if (_executingTask == null)
{
return;
}
// Signal cancel
_cts.Cancel();
// Wait until the task completes or the stop token triggers
await Task.WhenAny(_executingTask, Task.Delay(-1, cancellationToken));
cancellationToken.ThrowIfCancellationRequested();
}
// cancel
protected abstract Task ExecuteAsync(CancellationToken cancellationToken);
}
I am then extending this class and implementing what needs to be done in the ExecuteAsync as follows
public class DataRefreshService : HostedService
{
private readonly DataFetchService _dataFetchService;
public DataRefreshService(DataFetchService randomStringProvider)
{
_dataFetchService = randomStringProvider;
}
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
try
{
while (!cancellationToken.IsCancellationRequested)
{
await _dataFetchService.UpdateData(cancellationToken);
TimeSpan span = _dataFetchService.GetNextTrigger();
await Task.Delay(span, cancellationToken);
}
} catch (Exception)
{
await StopAsync(cancellationToken);
throw new Exception("Error trigger Sync service");
}
}
}
This is what I have added to the Startup.cs file
services.AddSingleton<DataFetchService>();
services.AddSingleton<IHostedService, DataRefreshService>();
You could try
services.AddHostedService<DataRefreshService>;
You could also try in making the DataRefreshService inherit from
Microsoft.Extensions.Hosting.BackgroundService
You can read more about that here

Access IUrlHelper inside DelegatingHandler

Following a migration to ASP.Net core, the following handler does not work. I cannot see how to get access to IUrlHelper from the HttpRequestMessage as was previously possible, and can't find a package with relevant extension methods.
The handler is added using config.MessageHandlers.Add(new LinkDecoratorHandler());
Can anyone help?
public class LinkDecoratorHandler : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage
request, CancellationToken cancellationToken)
{
return base.SendAsync(request, cancellationToken)
.ContinueWith(task =>
{
var response = task.Result;
if (!(response.Content is ObjectContent))
{
return response;
}
var entity = (response.Content as ObjectContent).Value as ILinkedEntity;
var enumeration = (response.Content as ObjectContent).Value as IEnumerable<ILinkedEntity>;
if (entity != null || enumeration != null)
{
//no longer available
var helper = request.GetUrlHelper();
//blah
}
return response;
});
}
}
Thanks in advance
If your LinkDecoratorHandler is instantiated via dependency injection then you could inject an instance of IActionContextAccessor to get the current ActionContext. From there, you can create your own UrlHelper instance.