MinimalAp IEndpointFilter MapGet Get Pattern - c#

In the IEndpointFilter would like to get the route used.
app.MapGet("/get/{productId:guid}", ......
public class MyFilter : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
{
//Getting it here /get/{productId:guid}
return await next.Invoke(context);
}
}

You can use HttpContext.GetEndpoint() method and then check if endpoint is RouteEndpoint:
class MyFilter : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
{
var endpoint = context.HttpContext.GetEndpoint();
if (endpoint is RouteEndpoint re1)
{
Console.WriteLine(re1.RoutePattern.RawText);
}
return await next(context);
}
}
Or do manually what GetEndpoint does itself:
var endpointFeature = context.HttpContext.Features.Get<IEndpointFeature>();
if (endpointFeature?.Endpoint is RouteEndpoint re)
{
Console.WriteLine(re.RoutePattern.RawText);
}

Related

Blazor Server http headers authorisation throws 400 on attempt to start hub connection

I`ve made a simple Blazor server hub with [Authorize] attribute. Authorisation process is described in specific AuthorizationRequirement and is considered successful only if incoming HTTP request has specified headers.
[Authorize(Policy = EnterpriseServiceBusHub.EsbHubAuthorizationPolicy)]
public class EnterpriseServiceBusHub : Hub
{
public const string EsbHubPath = "/esb-hub";
public const string EsbHubMethod = "InvokeIntegration";
public const string EsbHubAuthorizationPolicy = "EsbHubAuthorizationPolicy";
public const string EsbIntegrationID = "ESB-Integration-ID";
public const string EsbIntegrationName = "ESB-Integration-Channel";
private readonly IEntepriseServiceBus _entepriseServiceBus;
public EnterpriseServiceBusHub(IEntepriseServiceBus entepriseServiceBus)
{
_entepriseServiceBus = entepriseServiceBus;
}
[HubMethodName(EsbHubMethod)]
public async Task InvokeIntegration(string integrationChannel, string encryptedMessage)
{
await Clients.Others.SendAsync(integrationChannel, encryptedMessage);
}
}
public class EsbAuthorizationRequirement : AuthorizationHandler<EsbAuthorizationRequirement>, IAuthorizationRequirement
{
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, EsbAuthorizationRequirement requirement)
{
try
{
await EsbAuthorizationChecker.CheckAuthorizationAsync();
context.Succeed(requirement);
}
catch (Exception ex)
{
context.Fail(new AuthorizationFailureReason(this, ex.Message));
}
}
}
static class EsbAuthorizationChecker
{
public static async Task<bool> CheckAuthorizationAsync()
{
var httpContextAccessor = ServicesHolder.GetService<IHttpContextAccessor>();
var requestHeaders = httpContextAccessor.HttpContext.Request.Headers;
if (!requestHeaders.TryGetValue(EnterpriseServiceBusHub.EsbIntegrationID, out var integrationID) ||
!requestHeaders.TryGetValue(EnterpriseServiceBusHub.EsbIntegrationName, out var integrationUrl))
{
var message = $"Headers {EnterpriseServiceBusHub.EsbIntegrationID} or {EnterpriseServiceBusHub.EsbIntegrationName} were not provided";
throw new Exception(message);
}
return await Task.FromResult(true);
}
}
The problem is that even specifying those headers in HubConnectionBuilder, attempt to start hub connection throws 400 (Bad request) exception.
_hubConnection = new HubConnectionBuilder().WithUrl($#"{Url}/{EnterpriseServiceBusHub.EsbHubPath}",
(HttpConnectionOptions options) =>
{
options.Headers.Add(EnterpriseServiceBusHub.EsbIntegrationID, "Value-1");
options.Headers.Add(EnterpriseServiceBusHub.EsbIntegrationName, "Value-2");
}).Build();
await _hubConnection.StartAsync(); // throws 400 Bad request
The specified authorisation policy is included in Program.cs
builder.Services.AddAuthorization(options =>
{
options.AddPolicy(EnterpriseServiceBusHub.EsbHubAuthorizationPolicy, policy =>
{
policy.Requirements.Add(new EsbAuthorizationRequirement());
});
});
builder.Services.AddSignalR(config =>
{
config.EnableDetailedErrors = true;
config.ClientTimeoutInterval = null;
});
What can be the reason for that? Am I specifying headers wrong? Thank you.

How to call async from sync method in blazor and wait for result?

In Blazor webassembly I have javascript function in js file:
function AksMessage(message) {
return confirm(message);
}
In razor file:
[Inject]
public IJSRuntime JSRuntime { get; set; }
public async Task<bool> askMessage(msg)
{
await JSRuntime.InvokeVoidAsync("AskMessage", msg);
}
Now in some not async function I want to call
askMessage and get result if user clicked and returned false or true.
How can I run it and wait for result from synchronous part of code?
If I do:
var askmsg = Task.Run(async () => await askMessage("question"));
and askmsg.Result I have exception that monitors can not wait on this runtime.
I just solved this for setting bearer tokens on a gRPC interceptor which required some async code in WASM, you do it like this:
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
TRequest request,
ClientInterceptorContext<TRequest, TResponse> context,
AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
{
var call = continuation(request, context);
return new AsyncUnaryCall<TResponse>(
call.ResponseAsync,
GetMetadata(),
call.GetStatus,
call.GetTrailers,
call.Dispose);
}
private async Task<Metadata> GetMetadata()
{
try
{
if (!string.IsNullOrEmpty(await _authenticationManager.TryRefreshToken()))
_snackbar.Add("Refreshed Token.", Severity.Success);
var token = _authenticationManager.Token;
var metadata = new Metadata();
if (!string.IsNullOrEmpty(token))
{
metadata.Add("Authorization", $"Bearer {token}");
var userIdentity = (await _authenticationStateProvider.GetAuthenticationStateAsync()).User.Identity;
if (userIdentity!.IsAuthenticated)
metadata.Add("User", userIdentity.Name!);
}
else
{
_authenticationManager.Logout().GetAwaiter().GetResult();
_navigationManager.NavigateTo("/");
}
return metadata;
}
catch (Exception ex)
{
throw new InvalidOperationException("Failed to add token to request metadata", ex);
}
}
EDIT: For some reason, although this async code runs, it doesn't actually set the headers onto the request so you have to do the following hack:
public class AuthenticationInterceptor : Interceptor
{
private readonly IAuthenticationManager _authenticationManager;
private readonly ISnackbar _snackbar;
private readonly NavigationManager _navigationManager;
private readonly AuthenticationStateProvider _authenticationStateProvider;
private Metadata? _metadata;
public AuthenticationInterceptor(
IAuthenticationManager authenticationManager,
ISnackbar snackbar,
NavigationManager navigationManager,
AuthenticationStateProvider authenticationStateProvider)
{
_authenticationManager = authenticationManager;
_snackbar = snackbar;
_navigationManager = navigationManager;
_authenticationStateProvider = authenticationStateProvider;
}
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
TRequest request,
ClientInterceptorContext<TRequest, TResponse> context,
AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
{
_ = new AsyncUnaryCall<TResponse>(
null!,
GetMetadata(context),
null!,
null!,
null!); // Doesn't actually send the request but runs the Task
// that provides the metadata to the field which can
// then be used to set the request headers
var newOptions = context.Options.WithHeaders(_metadata!);
var newContext = new ClientInterceptorContext<TRequest, TResponse>(
context.Method,
context.Host,
newOptions);
return base.AsyncUnaryCall(request, newContext, continuation);
}
private async Task<Metadata> GetMetadata<TRequest, TResponse>(ClientInterceptorContext<TRequest, TResponse> context)
where TRequest : class
where TResponse : class
{
try
{
if (!string.IsNullOrEmpty(await _authenticationManager.TryRefreshToken()))
_snackbar.Add("Refreshed Token.", Severity.Success);
var token = _authenticationManager.Token;
Console.WriteLine($"Token: {token}");
var headers = new Metadata();
if (!string.IsNullOrEmpty(token))
{
headers.Add(new Metadata.Entry("Authorization", $"Bearer {token}"));
Console.WriteLine("Set metadata");
var userIdentity = (await _authenticationStateProvider.GetAuthenticationStateAsync()).User.Identity;
if (userIdentity!.IsAuthenticated)
headers.Add(new Metadata.Entry("User", userIdentity.Name!));
}
else
{
await _authenticationManager.Logout();
_navigationManager.NavigateTo("/");
}
var callOptions = context.Options.WithHeaders(headers);
return _metadata = callOptions.Headers!;
}
catch (Exception ex)
{
throw new InvalidOperationException("Failed to add token to request headers", ex);
}
}
}
You can call async method from synchronous method and wait for it like this :
var askmsg = Task.Run(async () => await askMessage("question"));
var result = Task.WaitAndUnwrapException();
another solution is like this (sync method backs to its context):
var result = AsyncContext.RunTask(askMessage("question")).Result;

Selecting Injected Instance with Conditional

I have several classes that have inherited from one interface. I want the desired service to be loaded and used in the controller depending on the conditions.
Controller
public class GatewayController
{
private readonly IAction action;
public GatewayController(IAction action)
{
this.action = action;
}
[HttpPost("gateway")]
public async Task<ApiResult> Gateway(GatewayRequest gatewayRequest, CancellationToken cancellationToken)
{
try
{
var comId = gatewayRequest.CommandId;
switch (comId)
{
case (int) GuaranteeItemStatus.End:
return await action.Perform(gatewayRequest, cancellationToken); //must be use EndActionService
case (int) GuaranteeItemStatus.SendProduct:
return await action.Perform(gatewayRequest, cancellationToken); //must be use SendProductActionService
default:
return null;
}
}
catch (Exception ex)
{
return ApiResult.ToErrorModel("error");
}
}
}
Parent interface:
public interface IAction
{
Task<ApiResult> Perform(GatewayRequest gatewayRequest, CancellationToken cancellationToken);
}
Services:
1-SendProductActionService:
public class SendProductActionService:IAction
{
public async Task<ApiResult> Perform(GatewayRequest gatewayRequest, CancellationToken cancellationToken)
{
return ApiResult.ToSuccessModel("SendProduct");
}
}
2-EndActionService:
public class EndActionService:IAction
{
public async Task<ApiResult> Perform(GatewayRequest gatewayRequest, CancellationToken cancellationToken)
{
return ApiResult.ToSuccessModel("EndAction");
}
}
It's possible to register, and inject, an IEnumerable that contains all your IActions.
First, to identify which IAction reacts to which command, you can add a CommandId property:
public interface IAction
{
int CommandId { get; }
}
public class SendProductActionService : IAction
{
public int CommandId => (int)GuaranteeItemStatus.SendProduct;
}
public class EndActionService : IAction
{
public int CommandId => (int)GuaranteeItemStatus.End;
}
In your Startup.cs, you register all your actions:
services.AddScoped<IAction, SendProductActionService>();
services.AddScoped<IAction, EndActionService>();
Then in your controller, you inject all the IAction, and select the appropriate one when needed:
public class GatewayController
{
// map the command ID to the proper IAction
private readonly Dictionary<int, IAction> actions;
// inject all the services registered that implement IAction
public GatewayController(IEnumerable<IAction> actions)
{
this.actions = actions.ToDictionary(_ => _.CommandId);
}
[HttpPost("gateway")]
public async Task<ApiResult> Gateway(GatewayRequest gatewayRequest, CancellationToken cancellationToken)
{
// find the appropriate IAction
if (!actions.TryGetValue((int)gatewayRequest.CommandId, out var action)
return BadRequest();
return await action.Perform(gatewayRequest, cancellationToken);
}
}
In your startup.cs:
services.AddScoped<SendProductActionService>();
services.AddScoped<EndActionService>();
services.AddScoped<Func<GuaranteeItemStatus, IAction>>(serviceProvider => status =>
{
switch (status)
{
case GuaranteeItemStatus.SendProduct:
return serviceProvider.GetService<SendProductActionService>();
case GuaranteeItemStatus.End:
return serviceProvider.GetService<EndActionService>();
default:
throw new InvalidOperationException();
}
});
Your controller should be similar to this:
public class GatewayController
{
private readonly Func<GuaranteeItemStatus, IAction> actionProvider;
public GatewayController(Func<GuaranteeItemStatus, IAction> actionProvider)
{
this.actionProvider = actionProvider;
}
[HttpPost("gateway")]
public async Task<ApiResult> Gateway(GatewayRequest gatewayRequest, CancellationToken cancellationToken)
{
try
{
return await actionProvider((GuaranteeItemStatus)gatewayRequest.CommandId)
.Perform(gatewayRequest, cancellationToken);
}
catch (Exception ex)
{
return ApiResult.ToErrorModel("error");
}
}
}

Get route to an action from outside the method

Similar to Get the full route to current action, but I want to get the route from outside of the controller method.
[ApiController]
public class TestController : ControllerBase {
public IActionResult OkTest() {
return Ok(true);
}
}
Then a test class:
public class TestControllerTests {
private readonly HttpClient _client;
public TestControllerTests() {
_client = TestSetup.GetTestClient();
}
[Test]
public async Task OkTest() {
var path = GetPathHere(); // should return "/api/test/oktest". But what is the call?
var response = await _client.GetAsync(path);
response.EnsureSuccessStatusCode();
}
}
This approach seems to provide desired result. But this basically instantiates the whole application in order to get to the configured services:
private string GetPathHere(string actionName)
{
var host = Program.CreateWebHostBuilder(new string[] { }).Build();
host.Start();
IActionDescriptorCollectionProvider provider = (host.Services as ServiceProvider).GetService<IActionDescriptorCollectionProvider>();
return provider.ActionDescriptors.Items.First(i => (i as ControllerActionDescriptor)?.ActionName == actionName).AttributeRouteInfo.Template;
}
[TestMethod]
public void OkTestShouldBeFine()
{
var path = GetPathHere(nameof(ValuesController.OkTest)); // "api/Values" in my case
}
However I suspect more complex cases will require a bit more massaging.

Creating new IHttpActionResult action result methods

Is there a way I can use the new IHttpActionResult interface to return a HttpStatusCode.NoContent response message?
I am currently using return new HttpResponseMessage( HttpStatusCode.NoContent );
and would like to convert this into return NoContent();.
IHttpActionResult has already got Ok(), Conflict() and NotFound() but I cannot find any for Forbidden() and NoContent() which I need to use in my project.
How easy is it to add other result types?
There's no convenience method for no-content result because, by default, when a action returns void, the response will have the HTTP status 204.
If you wish to explicitly indicate that on the action, you could also return a StatusCode(HttpStatusCode.NoContent) from your action or a
ResponseMessage(new HttpResponseMessage(HttpStatusCode.NoContent)).
The Unauthorized() convenience method gives you a 401 status so, for Forbidden (403), you would also have to use StatusCode(HttpStatusCode.Forbidden) or
ResponseMessage(new HttpResponseMessage(HttpStatusCode.Forbidden))
I found this example site that shows how to add a custom IHttpActionResult method and I've used this to create the Forbidden() and NoContent() methods with great success.
public abstract class CommonApiController : ApiController
{
public class ForbiddenResult : IHttpActionResult
{
private readonly HttpRequestMessage _request;
private readonly string _reason;
public ForbiddenResult(HttpRequestMessage request,string reason)
{
_request = request;
_reason = reason;
}
public ForbiddenResult(HttpRequestMessage request)
{
_request = request;
_reason = "Forbidden";
}
public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
var response = _request.CreateResponse(HttpStatusCode.Forbidden,_reason);
return Task.FromResult(response);
}
}
public class NoContentResult : IHttpActionResult
{
private readonly HttpRequestMessage _request;
private readonly string _reason;
public NoContentResult(HttpRequestMessage request,string reason)
{
_request = request;
_reason = reason;
}
public NoContentResult(HttpRequestMessage request)
{
_request = request;
_reason = "No Content";
}
public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
var response = _request.CreateResponse(HttpStatusCode.NoContent,_reason);
return Task.FromResult(response);
}
}
}
And then I can use it like this:
public class InvoiceController : CommonApiController
{
public async Task<IHttpActionResult> Post([FromBody]Invoice invoice)
{
if(User.IsInRole("Readonly"))
{
return Forbidden();
}
// Rest of code
}
}
I tried the #Intrepid implementation and I ran into some problems. I see two solutions here:
Solution 1:
The part: return Forbidden(); should not work.
The compiler would not recognize this.
Instead it should be: return new ForbiddenResult(Request, "my reason");
UPDATE 1
Solution 2:
I think this is what #Interpid intended in his implementation, but he was missing a few things.
In order to use return Forbidden(); the CommonApiController should be updated with the functions that return the custom IHttpActionResult for Forbidden and NoContent
The class should look like this:
public abstract class CommonApiController: ApiController {
protected ForbiddenResult Forbidden() {
return new ForbiddenResult(this.Request);
}
protected ForbiddenResult Forbidden(string reason) {
return new ForbiddenResult(this.Request, reason);
}
protected NoContentResult NoContent() {
return new NoContentResult(this.Request);
}
public class ForbiddenResult: IHttpActionResult {
private readonly HttpRequestMessage _request;
private readonly string _reason;
public ForbiddenResult(HttpRequestMessage request, string reason) {
_request = request;
_reason = reason;
}
public ForbiddenResult(HttpRequestMessage request) {
_request = request;
_reason = "Forbidden";
}
public Task < HttpResponseMessage > ExecuteAsync(CancellationToken cancellationToken) {
var response = _request.CreateResponse(HttpStatusCode.Forbidden, _reason);
return Task.FromResult(response);
}
}
public class NoContentResult: IHttpActionResult {
private readonly HttpRequestMessage _request;
private readonly string _reason;
public NoContentResult(HttpRequestMessage request, string reason) {
_request = request;
_reason = reason;
}
public NoContentResult(HttpRequestMessage request) {
_request = request;
_reason = "No Content";
}
public Task < HttpResponseMessage > ExecuteAsync(CancellationToken cancellationToken) {
var response = _request.CreateResponse(HttpStatusCode.NoContent, _reason);
return Task.FromResult(response);
}
}
}
Anyway, if I am wrong and #Interpid's answer is correct. What am I missing here to make his implementation work?
You can now use the following (.Net Standard):
return StatusCode(HttpStatusCode.NoContent);
or (.Net Core 2.1+)
return NoContent();
If you want to include a reason phrase with your response without adding a sub-class to ApiController, build a ResponseMessage object and return it from the action by the ResponseMessage() method. Try this:
public class InvoiceController : ApiController
{
public async Task<IHttpActionResult> Post([FromBody]Invoice invoice)
{
if(User.IsInRole("Readonly"))
{
var response = new HttpResponseMessage(HttpStatusCode.Forbidden);
response.ReasonPhrase = "User has the Readonly role";
return ResponseMessage(response);
}
// Rest of code
}
}
This worked well for me:
public class CodeAndReason : IHttpActionResult
{
private readonly HttpStatusCode code;
private readonly string reason;
public CodeAndReason(HttpStatusCode code, string reason)
{
this.code = code;
this.reason = reason;
}
public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
var response = new HttpResponseMessage(code)
{
ReasonPhrase = reason,
Content = new StringContent(reason),
};
return Task.FromResult(response);
}
public static IHttpActionResult NotFound(string reason)
{
return new CodeAndReason(HttpStatusCode.NotFound, reason);
}
public static IHttpActionResult Conflict(string reason)
{
return new CodeAndReason(HttpStatusCode.Conflict, reason);
}
public static IHttpActionResult Unauthorized(string reason)
{
return new CodeAndReason(HttpStatusCode.Unauthorized, reason);
}
}
Used as:
return CodeAndReason.NotFound("Record {blah} not found");

Categories

Resources