Is there a way to handle asp.net core odata errors - asp.net-core

Is there a way to handle asp.net core odata errors?
I have a model class DimDateAvailable with one property, a primary key of int DateId, and I make a call like /data/DimDateAvailable?$select=test.
Other calls work as expected and return what I'm after - this is a deliberate call to generate a fault, and it fails because there is no property named test on the model. The response comes back as expected, like so: {"error":{"code":"","message":"The query specified in the URI is not valid. Could not find a property named 'test' on type 'DimDateAvailable'... followed by a stack trace.
This response is fine when env.IsDevelopment() is true but I don't want to expose the stack trace when not in development.
I've looked at wrapping the code in the controllers' get method in a try-catch, but I think there's an action filter running over the results so it never gets called. On the other hand, I can't see where to inject any middleware and/or add any filters to catch errors. I suspect there might be a way to override an output formatter to achieve what I want but I can't see how.
Here's what I have at the moment:
In Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<TelemetryDbContext>();
services.AddOData();
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMvc(routeBuilder =>
{
routeBuilder.MapODataServiceRoute("odata", "data", GetEdmModel());
routeBuilder.Select().Expand().Filter().OrderBy().MaxTop(null).Count();
// insert special bits for e.g. custom MLE here
routeBuilder.EnableDependencyInjection();
});
}
private static IEdmModel GetEdmModel()
{
var builder = new ODataConventionModelBuilder();
builder.EntitySet<DimDateAvailable>("DimDateAvailable");
return builder.GetEdmModel();
}
In TelemetryDbContext.cs:
public virtual DbSet<DimDateAvailable> DimDateAvailable { get; set; }
In DimDateAvailable.cs
public class DimDateAvailable
{
[Key]
public int DateId { get; set; }
}
My controller:
public class DimDateAvailableController : ODataController
{
private readonly TelemetryDbContext data;
public DimDateAvailableController(TelemetryDbContext data)
{
this.data = data;
}
[EnableQuery(AllowedQueryOptions = AllowedQueryOptions.Supported, PageSize = 2000)]
public IActionResult Get()
{
return Ok(this.data.DimDateAvailable.AsQueryable());
}
}
This is in an asp.net core 2 web app with the Microsoft.AspNetCoreOData v7.0.1 and EntityFramework 6.2.0 packages.

Investigating Ihar's suggestion lead me down the rabbit hole, and I ended up inserting an ODataOutputFormatter into the MVC options to intercept ODataPayloadKind.Error responses and reformat them.
It was interesting to see that context.Features held an instance of IExceptionHandlerFeature in app.UseExceptionHandler() but not in the ODataOutputFormatter. That lack was pretty much what prompted me to pose this question in the first place, but was solved by translating the context.Object in the ODataOutputFormatter which is something I saw done in the OData source as well. I don't know if the changes below are good practice in asp.net core or when using the AspNetCoreOData package, but they do what I want for now.
Changes to Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<TelemetryDbContext>();
services.AddOData();
services.AddMvc(options =>
{
options.OutputFormatters.Insert(0, new CustomODataOutputFormatter(this.Environment.IsDevelopment()));
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// Added this to catch errors in my own code and return them to the client as ODataErrors
app.UseExceptionHandler(appBuilder =>
{
appBuilder.Use(async (context, next) =>
{
var error = context.Features[typeof(IExceptionHandlerFeature)] as IExceptionHandlerFeature;
if (error?.Error != null)
{
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
context.Response.ContentType = "application/json";
var response = error.Error.CreateODataError(!env.IsDevelopment());
await context.Response.WriteAsync(JsonConvert.SerializeObject(response));
}
// when no error, do next.
else await next();
});
});
app.UseMvc(routeBuilder =>
{
routeBuilder.MapODataServiceRoute("odata", "data", GetEdmModel());
routeBuilder.Select().Expand().Filter().OrderBy().MaxTop(null).Count();
// insert special bits for e.g. custom MLE here
routeBuilder.EnableDependencyInjection();
});
}
New classes CustomODataOutputFormatter.cs and CommonExtensions.cs
public class CustomODataOutputFormatter : ODataOutputFormatter
{
private readonly JsonSerializer serializer;
private readonly bool isDevelopment;
public CustomODataOutputFormatter(bool isDevelopment)
: base(new[] { ODataPayloadKind.Error })
{
this.serializer = new JsonSerializer { ContractResolver = new CamelCasePropertyNamesContractResolver() };
this.isDevelopment = isDevelopment;
this.SupportedMediaTypes.Add("application/json");
this.SupportedEncodings.Add(new UTF8Encoding());
}
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
{
if (!(context.Object is SerializableError serializableError))
{
return base.WriteResponseBodyAsync(context, selectedEncoding);
}
var error = serializableError.CreateODataError(this.isDevelopment);
using (var writer = new StreamWriter(context.HttpContext.Response.Body))
{
this.serializer.Serialize(writer, error);
return writer.FlushAsync();
}
}
}
public static class CommonExtensions
{
public const string DefaultODataErrorMessage = "A server error occurred.";
public static ODataError CreateODataError(this SerializableError serializableError, bool isDevelopment)
{
// ReSharper disable once InvokeAsExtensionMethod
var convertedError = SerializableErrorExtensions.CreateODataError(serializableError);
var error = new ODataError();
if (isDevelopment)
{
error = convertedError;
}
else
{
// Sanitise the exposed data when in release mode.
// We do not want to give the public access to stack traces, etc!
error.Message = DefaultODataErrorMessage;
error.Details = new[] { new ODataErrorDetail { Message = convertedError.Message } };
}
return error;
}
public static ODataError CreateODataError(this Exception ex, bool isDevelopment)
{
var error = new ODataError();
if (isDevelopment)
{
error.Message = ex.Message;
error.InnerError = new ODataInnerError(ex);
}
else
{
error.Message = DefaultODataErrorMessage;
error.Details = new[] { new ODataErrorDetail { Message = ex.Message } };
}
return error;
}
}
Changes to the controller:
[EnableQuery(AllowedQueryOptions = AllowedQueryOptions.Supported, PageSize = 2000)]
public IQueryable<DimDateAvailable> Get()
{
return this.data.DimDateAvailable.AsQueryable();
}

If you want a customization of responses, including customization of error responses try to use ODataQueryOptions instead of using
[EnableQuery(AllowedQueryOptions = AllowedQueryOptions.Supported, PageSize = 2000)]
Check some samples at https://learn.microsoft.com/en-us/aspnet/web-api/overview/odata-support-in-aspnet-web-api/supporting-odata-query-options#invoking-query-options-directly
It would allow you to cache validation errors and build custom response.

I have had this issue in the past and the only one way I got this working without having to write a middleware was like:
Try this:
catch (ODataException ex)
{
HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;//This line is important, if not it will return 500 Internal Server Error.
return BadRequest(ex.Message);//Just respond back the actual error which is 100% correct.
}
Then the error will look like:
{
"#odata.context": "http://yourendpoint.com$metadata#Edm.String",
"value": "The property 'test' cannot be used in the $select query option."
}
Hope this helps.
Thanks

Related

How to make "One or more validation errors occurred" raise an exception?

I'm running a WebAPI on Core 3.1 and one of my endpoints excepts JSON with a model that has fields with [Required] attribute like so:
public class Vendor
{
public int ID { get; set; }
[Required(ErrorMessage = "UID is required")] <<<------ required attribute
public string UID { get; set; }
}
When i call this endpoint without setting UID I get the following output as expected:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "|7ced8b82-4aa34d65daa99a12.",
"errors": {
"Vendor.UID": [
"UID is required"
]
}
}
Altho this output is pretty informative and clear it is not consistent with other error outputs that my API produces by means of ExceptionFilter. Is there any way this errors can be routed to the exception filter as well?
Here is my Startup.cs:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
Common.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.AddControllers();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_3_0);
services.AddMvc().AddXmlDataContractSerializerFormatters();
services.AddMvc().AddMvcOptions(options =>
{
options.EnableEndpointRouting = false;
});
services.AddMvcCore(options => options.OutputFormatters.Add(new XmlSerializerOutputFormatter()));
services.AddOptions();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger<Startup> logger)
{
//Middleware for exception filtering
app.UseMiddleware<ErrorHandlingMiddleware>(new ErrorHandlingMiddlewareOptions { logger = logger });
app.UseStaticFiles();
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
app.UseMvc(routes =>
{
routes.MapRoute("EndpointNotFound", "{*url}", new { controller = "Error", action = "EndpointNotFound" });
});
}
}
You can add filter in your mvc service or controller service
this filter return badrequest
services.AddControllers(option =>
{
option.Filters.Add<ValidationFilter>();
});
to create your filter you can add this class
also you can customize this filter to what ever you want
public class ValidationFilter : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
//before contrller
if(!context.ModelState.IsValid)
{
var errorsInModelState = context.ModelState
.Where(x => x.Value.Errors.Count > 0)
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Errors.Select(x => x.ErrorMessage).ToArray());
var errorResponse = new ErrorResponse();
foreach (var error in errorsInModelState)
{
foreach (var subError in error.Value)
{
var errorModel = new ErrorModel
{
FieldName = error.Key,
Message = subError
};
errorResponse.Error.Add(errorModel);
}
context.Result = new BadRequestObjectResult(errorResponse);
return;
}
await next();
//after controller
}
}
}
I have created error model just like this
public class ErrorModel
{
public string FieldName { get; set; }
public string Message { get; set; }
}
and error response like below
public class ErrorResponse
{
public List<ErrorModel> Error { get; set; } = new List<ErrorModel>();
public bool Successful { get; set; }
}
In order to achive this functionality you need to implement your own model validator described in this question: Model validation in Web API - Exception is thrown with out a throw statement?
You might want to take a look at the great library FluentValidation!
Sample:
Build a validator module binding your DTO and create a set of rules.
public class CustomerValidator : AbstractValidator<Customer> {
public CustomerValidator() {
RuleFor(x => x.Surname).NotEmpty();
RuleFor(x => x.Forename).NotEmpty().WithMessage("Please specify a first name");
RuleFor(x => x.Discount).NotEqual(0).When(x => x.HasDiscount);
RuleFor(x => x.Address).Length(20, 250);
RuleFor(x => x.Postcode).Must(BeAValidPostcode).WithMessage("Please specify a valid postcode");
}
private bool BeAValidPostcode(string postcode) {
// custom postcode validating logic goes here
}
}
Inject it in your controllers through DI:
services.AddControllers()
.AddFluentValidation(s =>
{
s.ValidatorOptions.CascadeMode = CascadeMode.Stop;
s.RunDefaultMvcValidationAfterFluentValidationExecutes = false;
s.ValidatorOptions.LanguageManager.Culture = new CultureInfo("en-US");
s.RegisterValidatorsFromAssemblyContaining<Customer>();
...
// more validators
});
That way your code looks well organized;
You get rid of Data Annotations spread all over your code.
Personalize error messages and validations.
You also may want to check why implementing ControllerBase on your controllers might be the way to go while using web APIs.
You want to output the JSON like this:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "|7ced8b82-4aa34d65daa99a12.",
"errors": {
"Vendor.UID": [
"UID is required"
]
}
}
the type and the status field, are from here https://github.com/dotnet/aspnetcore/blob/v3.1.17/src/Mvc/Mvc.Core/src/DependencyInjection/ApiBehaviorOptionsSetup.cs#L54-L108, the ClientErrorMapping will be configured when the dotnet core project setup.
the JSON was a ValidationProblemDetails which created by the DefaultProblemDetailsFactory, https://github.com/dotnet/aspnetcore/blob/v3.1.17/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsFactory.cs#L45
we can use this ProblemDetailsFactory to create the ValidationProblemDetails https://github.com/dotnet/aspnetcore/blob/v3.1.17/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs#L261
I have just change in http methos and work fine for the same issue
[HttpPost("authenticate")]
public IActionResult Authenticate([FromBody]AuthenticateModel model)

ASP.Net Core SignalR authentication always responding with 403 - Forbidden

Summary
I am trying to add security/authentication to my SignalR hubs, but no matter what I try the client requests keep getting a 403 - Forbidden responses (despite the requests successfully authenticating).
Setup
My project is based on Microsoft's SignalRChat example from:
https://learn.microsoft.com/en-us/aspnet/core/tutorials/signalr?view=aspnetcore-3.1&tabs=visual-studio
Basically I have an ASP.Net Core web application with Razor Pages. The project is targeting .Net Core 3.1.
The client library being used is v3.1.0 of Microsoft's JavaScript client library.
I also referenced their authentication and authorization document for the security side:
https://learn.microsoft.com/en-us/aspnet/core/signalr/authn-and-authz?view=aspnetcore-3.1
The key difference is rather than using the JWT Bearer middleware, I made my own custom token authentication handler.
Code
chat.js:
"use strict";
var connection = new signalR.HubConnectionBuilder()
.withUrl("/chatHub", { accessTokenFactory: () => 'mytoken' })
.configureLogging(signalR.LogLevel.Debug)
.build();
//Disable send button until connection is established
document.getElementById("sendButton").disabled = true;
connection.on("ReceiveMessage", function (user, message) {
var msg = message.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
var encodedMsg = user + " says " + msg;
var li = document.createElement("li");
li.textContent = encodedMsg;
document.getElementById("messagesList").appendChild(li);
});
connection.start().then(function () {
document.getElementById("sendButton").disabled = false;
}).catch(function (err) {
return console.error(err.toString());
});
document.getElementById("sendButton").addEventListener("click", function (event) {
var user = document.getElementById("userInput").value;
var message = document.getElementById("messageInput").value;
connection.invoke("SendMessage", user, message).catch(function (err) {
return console.error(err.toString());
});
event.preventDefault();
});
Startup.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using SignalRChat.Hubs;
using SignalRChat.Security;
namespace SignalRChat
{
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)
{
// Found other cases where CORS caused authentication issues, so
// making sure that everything is allowed.
services.AddCors(options =>
{
options.AddPolicy("AllowAny", policy =>
{
policy
.WithOrigins("http://localhost:44312/", "https://localhost:44312/")
.AllowCredentials()
.AllowAnyHeader()
.AllowAnyMethod();
});
});
services
.AddAuthentication()
.AddHubTokenAuthenticationScheme();
services.AddAuthorization(options =>
{
options.AddHubAuthorizationPolicy();
});
services.AddRazorPages();
services.AddSignalR(options =>
{
options.EnableDetailedErrors = true;
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
// 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.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapHub<ChatHub>("/chatHub");
});
}
}
}
ChatHub.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using SignalRChat.Security;
using System.Threading.Tasks;
namespace SignalRChat.Hubs
{
[Authorize(HubRequirementDefaults.PolicyName)]
public class ChatHub : Hub
{
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}
}
HubTokenAuthenticationHandler.cs
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
namespace SignalRChat.Security
{
public class HubTokenAuthenticationHandler : AuthenticationHandler<HubTokenAuthenticationOptions>
{
public IServiceProvider ServiceProvider { get; set; }
public HubTokenAuthenticationHandler(
IOptionsMonitor<HubTokenAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
IServiceProvider serviceProvider)
: base(options, logger, encoder, clock)
{
ServiceProvider = serviceProvider;
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
bool isValid = TryAuthenticate(out AuthenticationTicket ticket, out string message);
if (isValid) return Task.FromResult(AuthenticateResult.Success(ticket));
return Task.FromResult(AuthenticateResult.Fail(message));
}
private bool TryAuthenticate(out AuthenticationTicket ticket, out string message)
{
message = null;
ticket = null;
var token = GetToken();
if (string.IsNullOrEmpty(token))
{
message = "Token is missing";
return false;
}
bool tokenIsValid = token.Equals("mytoken");
if (!tokenIsValid)
{
message = $"Token is invalid: token={token}";
return false;
}
var claims = new[] { new Claim("token", token) };
var identity = new ClaimsIdentity(claims, nameof(HubTokenAuthenticationHandler));
ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), Scheme.Name);
return true;
}
#region Get Token
private string GetToken()
{
string token = Request.Query["access_token"];
if (string.IsNullOrEmpty(token))
{
token = GetTokenFromHeader();
}
return token;
}
private string GetTokenFromHeader()
{
string token = Request.Headers["Authorization"];
if (string.IsNullOrEmpty(token)) return null;
// The Authorization header value should be in the format "Bearer [token_value]"
string[] authorizationParts = token.Split(new char[] { ' ' });
if (authorizationParts == null || authorizationParts.Length < 2) return token;
return authorizationParts[1];
}
#endregion
}
}
HubTokenAuthenticationOptions.cs
using Microsoft.AspNetCore.Authentication;
namespace SignalRChat.Security
{
public class HubTokenAuthenticationOptions : AuthenticationSchemeOptions { }
}
HubTokenAuthenticationDefaults.cs
using Microsoft.AspNetCore.Authentication;
using System;
namespace SignalRChat.Security
{
public static class HubTokenAuthenticationDefaults
{
public const string AuthenticationScheme = "HubTokenAuthentication";
public static AuthenticationBuilder AddHubTokenAuthenticationScheme(this AuthenticationBuilder builder)
{
return AddHubTokenAuthenticationScheme(builder, (options) => { });
}
public static AuthenticationBuilder AddHubTokenAuthenticationScheme(this AuthenticationBuilder builder, Action<HubTokenAuthenticationOptions> configureOptions)
{
return builder.AddScheme<HubTokenAuthenticationOptions, HubTokenAuthenticationHandler>(AuthenticationScheme, configureOptions);
}
}
}
HubRequirement.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;
namespace SignalRChat.Security
{
public class HubRequirement : AuthorizationHandler<HubRequirement, HubInvocationContext>, IAuthorizationRequirement
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, HubRequirement requirement, HubInvocationContext resource)
{
// Authorization logic goes here. Just calling it a success for demo purposes.
context.Succeed(requirement);
return Task.CompletedTask;
}
}
}
HubRequirementDefaults.cs
using Microsoft.AspNetCore.Authentication;
using System;
namespace SignalRChat.Security
{
public static class HubTokenAuthenticationDefaults
{
public const string AuthenticationScheme = "HubTokenAuthentication";
public static AuthenticationBuilder AddHubTokenAuthenticationScheme(this AuthenticationBuilder builder)
{
return AddHubTokenAuthenticationScheme(builder, (options) => { });
}
public static AuthenticationBuilder AddHubTokenAuthenticationScheme(this AuthenticationBuilder builder, Action<HubTokenAuthenticationOptions> configureOptions)
{
return builder.AddScheme<HubTokenAuthenticationOptions, HubTokenAuthenticationHandler>(AuthenticationScheme, configureOptions);
}
}
}
The Results
On the client side, I see the following errors in the browser's developer console:
POST https://localhost:44312/chatHub/negotiate?negotiateVersion=1 403
Error: Failed to complete negotiation with the server: Error
Error: Failed to start the connection: Error
On the server side, all I see is:
SignalRChat.Security.HubTokenAuthenticationHandler: Debug: AuthenticationScheme: HubTokenAuthentication was successfully authenticated.
SignalRChat.Security.HubTokenAuthenticationHandler: Information:
AuthenticationScheme: HubTokenAuthentication was forbidden.
Next Steps
I did see that others had issues with CORS preventing them from security from working, but I believe it usually said that explicitly in the client side errors. Despite that, I added the CORS policies in Startup.cs that I believe should have circumvented that.
I also experimented around with changing the order of service configurations in Startup, but nothing seemed to help.
If I remove the Authorize attribute (i.e. have an unauthenticated hub) everything works fine.
Finally, I found the server side messages to be very interesting in that the authentication succeeded, yet the request was still forbidden.
I'm not really sure where to go from here. Any insights would be most appreciated.
Update
I have been able to debug this a little bit.
By loading system symbols and moving up the call stack, I found my way to Microsoft.AspNetCore.Authorization.Policy.PolicyEvaluator:
As can be seen, the authentication succeeded but apparently the authorization did not. Looking at the requirements, there are two: a DenyAnonymousAuthorizationRequirement and my HubRequirement (which automatically succeeds).
Because the debugger never hit my breakpoint in my HubRequirement class, I am left to assume that the DenyAnonymousAuthorizationRequirement is what is failing. Interesting, because based on the code listing on github (https://github.com/dotnet/aspnetcore/blob/master/src/Security/Authorization/Core/src/DenyAnonymousAuthorizationRequirement.cs) I should be meeting all the requirements:
There is a User defined on the context, the user has an identity, and there are no identities that are unauthenticated.
I have to be missing something, because this isn't adding up.
Turns out the failure was actually happening in my HubRequirement class, and not DenyAnonymousAuthorizationRequirement.
While my HubRequirement class implemented HandleRequirementAsync(), it did not implement HandleAsync(), which is what happened to be what was called instead.
If I update my HubRequirement class to the following, everything works as expected:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;
namespace SignalRChat.Security
{
public class HubRequirement : AuthorizationHandler<HubRequirement, HubInvocationContext>, IAuthorizationRequirement
{
public override Task HandleAsync(AuthorizationHandlerContext context)
{
foreach (var requirement in context.PendingRequirements)
{
// TODO: Validate each requirement
}
// Authorization logic goes here. Just calling it a success for demo purposes.
context.Succeed(this);
return Task.CompletedTask;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, HubRequirement requirement, HubInvocationContext resource)
{
// Authorization logic goes here. Just calling it a success for demo purposes.
context.Succeed(requirement);
return Task.CompletedTask;
}
}
}
Thank you, saved me a lot of debugging hours!
Looks like the problem is that HandleAsync is also being called with a RouteEndpoint resource for the signalr root and negotiation urls, a case the base class does not handle and since no authorization handler signals success it fails.
public override async Task HandleAsync(AuthorizationHandlerContext context)
{
if (context.Resource is HubInvocationContext)
{
foreach (var req in context.Requirements.OfType<RealtimeHubSecurityAuthorizationHandler>())
{
await HandleRequirementAsync(context, req, (HubInvocationContext)context.Resource);
}
} else if (context.Resource is Microsoft.AspNetCore.Routing.RouteEndpoint) {
//allow signalr root and negotiation url
context.Succeed(this);
}
}
(posted as answer since comment length is limited, sorry)

Controller and Action from Global Error Handler

I'm migrating an web api from .Net to .NetCore.
We had a custom ExceptionFilterAttribute to handle errors in a centralized way. Something like this:
public class HandleCustomErrorAttribute : ExceptionFilterAttribute
{
public override void OnException(HttpActionExecutedContext filterContext)
{
// Error handling routine
}
}
With some search, I managed to create something similar on .Net Core
public static class ExceptionMiddlewareExtensions
{
public static void ConfigureExceptionHandler(this IApplicationBuilder app)
{
app.UseExceptionHandler(appError =>
{
appError.Run(async context =>
{
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
context.Response.ContentType = "application/json";
var contextFeature = context.Features.Get<IExceptionHandlerFeature>();
if (contextFeature != null)
{
//logger.LogError($"Something went wrong: {contextFeature.Error}");
await context.Response.WriteAsync(new ErrorDetails()
{
StatusCode = context.Response.StatusCode,
Message = "Internal Server Error."
}.ToString());
}
});
});
}
}
I need to find a way to access these 3 info that where avaiable in .Net in .Net Core version:
filterContext.ActionContext.ActionDescriptor.ActionName;
filterContext.ActionContext.ControllerContext.ControllerDescriptor.ControllerName;
HttpContext.Current.Request.Url.ToString();
Is it possible ?
For a complete solution with registering ExceptionFilter and get request path, you could try like
ExceptinoFilter
public class ExceptinoFilter : IExceptionFilter
{
public void OnException(ExceptionContext context)
{
string controllerName = context.RouteData.Values["controller"].ToString();
string actionName = context.RouteData.Values["action"].ToString();
var request = context.HttpContext.Request;
var requestUrl = request.Scheme + "://" + request.Host + request.Path;
}
}
Register
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews(options => {
options.Filters.Add(new ExceptinoFilter());
});
}

How to return indented json content from an OData controller in asp core web api?

I can retrieve intended json result from normal WebApi using following way.
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
.AddJsonOptions(x=>
{
x.SerializerSettings.Formatting = Newtonsoft.Json.Formatting.Indented;
});
But I cannot find a way to output json like this when using ODataController as opposed to ControllerBase when web api is used. ODataController always sends a minified json.
public class EmployeeController : ODataController
{
[EnableQuery()]
public IActionResult Get()
{
return Ok(new BOContext().Employees.ToList());
}
}
Also, startup.cs
public class Startup
{
private static IEdmModel GetModel()
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Employee>("Employee");
return builder.GetEdmModel();
}
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.AddOData();
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
.AddJsonOptions(x=>
{
x.SerializerSettings.Formatting = Newtonsoft.Json.Formatting.None;
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc(routes =>
{
routes.MapODataServiceRoute("odata", "odata", GetModel());
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
The route is working and I am receiving correct data.
Is there a way to control and output indented json from an OData controller?
I suggest you to make the transfer with minified jsonm, but use json beutifier to show formatted json. dont do this on the data flow phase.
If you are using javascript on the front-end side. You can simple use
JSON.stringify(jsObj, null, "\t"); // stringify with tabs inserted at each level
JSON.stringify(jsObj, null, 2); // stringify with 2 spaces at each level
Not sure if this is still actual, but you can specify formatter when returning the data
// [...]
public IActionResult Get()
{
var res = Ok(_db.Employees);
res.Formatters.Add(new Microsoft.AspNetCore.Mvc.Formatters.JsonOutputFormatter(
new Newtonsoft.Json.JsonSerializerSettings() { Formatting = Newtonsoft.Json.Formatting.Indented },
System.Buffers.ArrayPool<char>.Create()));
return res;
}
And of course, if you want more generalized solution (or you just have a lot of code that is already written), you can create interim abstract class and inherit from that class instead of just ODataController:
public abstract class AbstractFormattedOdataController : ODataController
{
public override OkObjectResult Ok(object value)
{
var res = base.Ok(value);
res.Formatters.Add(new Microsoft.AspNetCore.Mvc.Formatters.JsonOutputFormatter(
new Newtonsoft.Json.JsonSerializerSettings() { Formatting = Newtonsoft.Json.Formatting.Indented },
System.Buffers.ArrayPool<char>.Create()));
return res;
}
}
// [...]
public class EmployeesController : AbstractFormattedOdataController
{
[EnableQuery()]
public IActionResult Get()
{
return Ok(new BOContext().Employees.ToList());
}
}

ASP .NET Entity Framework Core Cannot access a disposed object

Wanting to get into .NET Core, I created a WEB API that takes a file upload and then saves the transactions in the file into a DB table. I'm using .NET Core 2 with Entity Framework Core. I created my context using the example from here.
My problem is that I get the error "System.ObjectDisposedException Cannot access a disposed object" when it tries to save to the context object in my repository. It's a simple stack, so I'm hoping someone can help me out.
My container setup looks like this:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddDbContext<SyncFinContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddScoped<ITransactionProcessor, TransactionProcessor>();
services.AddScoped<ITransactionRepository, TransactionRepository>();
}
My DBInitializer which I also got from the link above:
public static class DbInitializer
{
public static async Task Initialize(SyncFinContext context)
{
await context.Database.EnsureCreatedAsync();
// Look for any students.
if (context.Transactions.Any())
{
return; // DB has been seeded
}
var ts = new Transaction[]
{
// insert test data here
};
await context.SaveChangesAsync();
}
}
My DB Context:
public class SyncFinContext : DbContext
{
public SyncFinContext(DbContextOptions<SyncFinContext> options) : base(options)
{
}
public DbSet<Transaction> Transactions { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Transaction>().ToTable("Transactions");
}
}
My Controller looks like this:
[Produces("application/json")]
public class TransactionController : Controller
{
ITransactionRepository _transactionRepository { get; set; }
ITransactionProcessor _transactionProcessor { get; set; }
public TransactionController(ITransactionRepository m, ITransactionProcessor p) : base()
{
_transactionRepository = m;
_transactionProcessor = p;
}
// POST: transaction/import
[HttpPost]
public async void Import(List<IFormFile> files)
{
if (files == null || files.Count == 0)
{
throw new FileNotFoundException("No file was received.");
}
// copy file to temp location so that it can be processed
var filepath = Path.GetTempFileName();
using (var stream = new FileStream(filepath, FileMode.Create))
{
await files[0].CopyToAsync(stream);
}
ImportTransactionRequest input = new ImportTransactionRequest
{
FilePath = filepath
};
var transactions = await _transactionProcessor.ReadDocument(filepath);
await _transactionRepository.AddBulk(transactions);
}
}
And my repository looks like this:
public class TransactionRepository : ITransactionRepository
{
// TODO: move the context
private SyncFinContext _context;
public TransactionRepository(SyncFinContext context)
{
_context = context;
}
public async Task AddBulk(List<Transaction> transactions)
{
foreach(var t in transactions)
{
await _context.Transactions.AddAsync(t);
}
_context.SaveChanges();
}
}
For full transparency, the transaction Processor just gets a list of rows from a csv:
public async Task<List<Transaction>> ReadDocument(string filepath)
{
try
{
var ret = new List<Transaction>();
var lines = await File.ReadAllLinesAsync(filepath);
foreach (var line in lines)
{
var parts = line.Split(',');
var tx = new Transaction
{
PostedDate = DateTime.Parse(parts[0]),
TransactionDate = DateTime.Parse(parts[1]),
Description = parts[2],
Deposit = ParseDecimal(parts[3]),
Withdrawal = ParseDecimal(parts[4]),
Balance = ParseDecimal(parts[5])
};
ret.Add(tx);
}
return ret;
}
catch(Exception e)
{
throw;
}
}
I've read where the whole stack must be async in order for the db context instance to be available, and, unless I'm doing it wrong, I seem to be doing that, as you can see above.
My expectations are that AddDbContext() will indeed properly scope the context to be available throughout the stack unless I explicitly dispose of it. I have not found anything to make me think otherwise.
I've tried hard-coding data in my DB Initializer also, as I read that may be a factor, but that does not solve the problem. Not sure what else to try. If someone can give me some ideas I would appreciate it.
The Import() action method needs to have a return type of Task. MVC will await execute an action method with a return type of Task.
Also, probably best to get in the habit of returning an IActionResult on your action methods. The task based equivalent is Task<IActionResult>. This makes your controllers easier to test.
Since the AddBulk(List<Transaction> transactions) method is public async Task, the DbContext will be disposed if any part returns void (not awaited) at any point.
Try changing _context.SaveChanges();
To await _context.SaveChangesAsync();
This would ensure a Task is being returned and not void.
https://stackoverflow.com/a/46308661/3062956
https://learn.microsoft.com/en-us/ef/core/saving/async