In my application there are certain "friendly" messages that the services layer returns to me through a custom exception "LimsDataException" and that I want to show in the corresponding view.
I solve them with a try/catch in the controller actions, generating a lot of repetitive code, could I solve it with an exception filter, with a custom middleware or in some other way?
[HttpPost]
public async Task<IActionResult> Create(PriorityVM vm)
{
if (ModelState.IsValid)
{
try
{
var priority = _mapper.Map<PriorityDto>(vm);
priority.Id = await _priorityService.Create(priority);
return RedirectToAction(nameof(Details), new { id = priority.Id });
}
catch (LimsDataException ex)
{
ModelState.AddModelError("", _dbLocalizer[ex.Message]);
}
}
return View(vm);
}
public class LimsDataExceptionFilter : IExceptionFilter
{
public void OnException(ExceptionContext context)
{
if (context.Exception is LimsDataException)
{
context.ModelState.AddModelError("", context.Exception.Message);
context.ExceptionHandled = true;
// Can I continue with the execution of the view? Do I need a Middleware?
}
}
}
screen
Related
I have domain service that throws a custom DomainServiceValidationException for a business validation. I want to globally capture the exception and return in ModelState. My current working solution is using ExceptionFilterAttribute
public class ExceptionHandlerAttribute : ExceptionFilterAttribute
{
private readonly ILogger<ExceptionHandlerAttribute> _logger;
public ExceptionHandlerAttribute(ILogger<ExceptionHandlerAttribute> logger)
{
_logger = logger;
}
public override void OnException(ExceptionContext context)
{
if (context == null || context.ExceptionHandled)
{
return;
}
if(context.Exception is DomainServiceValidationException)
{
context.ModelState.AddModelError("Errors", context.Exception.Message);
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
context.Result = new BadRequestObjectResult(context.ModelState);
}
else
{
handle exception
}
}
}
I want to know if there is any way to access ModelState in UseExceptionHandler middleware
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime appLifetime)
{
app.UseExceptionHandler(new ExceptionHandlerOptions()
{
ExceptionHandler = async (context) =>
{
var ex = context.Features.Get<IExceptionHandlerFeature>().Error;
// how to access to ModelState here
}
});
}
}
Shorten answer:
No, you cannot access ModelState in UseExceptionHandler middleware.
Explanation:
Firstly you need to know ModelState is only available after Model Binding.
Then Model Binding invokes before Action Filters and after Resource Filters(See Figure 1). But Middleware invokes before Filters(See Figure 2).
Figure 1:
Figure 2:
Reference:
How Filters work
Conclusion:
That is to say, you can not get the ModelState in UseExceptionHandler middleware.
Workaround:
You can only store the ModelState automatically in Filters(Action Filters or Exception Filters or Result Filters), then you can use it within middleware.
app.UseExceptionHandler(new ExceptionHandlerOptions()
{
ExceptionHandler = async (context) =>
{
var ex = context.Features.Get<IExceptionHandlerFeature>().Error;
// how to access to ModelState here
var data = context.Features.Get<ModelStateFeature>()?.ModelState;
}
});
Reference:
How to store ModelState automatically
hello i write this code in asp.net core. for api request. so i will write return keyword in this code. what should i do to access a return keyword in below code?
[HttpPost]
[Route("UserDelete")]
public async Task UserDelete(string Id)
{
try
{
await _ICcontext.UserRegistrationDelete(Id);
}
catch(Exception Ex)
{
throw Ex;
}
}
Simply return with method OK() or Json() with the object you want to serialize.
[HttpPost]
[Route("UserDelete")]
public async Task<IActionResult> UserDelete(string Id)
{
try
{
return Ok(await _ICcontext.UserRegistrationDelete(Id));
}
catch(Exception Ex)
{
LogException(Ex);
return StatusCode(500);
}
}
I have .net core web api and able to access the get endpoint but not the delete. How do i access the delete endpoint. I am not sure what the problem is ?
I have tried the following
http://localhost:53538/api/cities/delete-city/1
I have been using for the get endpoint.
http://localhost:53538/api/cities
controller
public class CitiesController : Controller
{
private readonly ICityInfoService _cityInfoService;
public CitiesController(ICityInfoService cityInfoService)
{
_cityInfoService = cityInfoService;
}
[HttpGet]
public IActionResult GetCities()
{
var cities = _cityInfoService.GetCities();
if (!cities.Any())
return NoContent();
var citiesMapped = cities.Select(MapCity);
return Ok(citiesMapped);
}
[HttpGet("{cityId:int}")]
public IActionResult GetCity(int cityId)
{
try
{
var city = _cityInfoService.GetCity(cityId);
var cityMapped = MapCity(city);
return Ok(cityMapped);
}
catch (CityNotFoundException e)
{
return BadRequest(e.Message);
}
catch (Exception ex)
{
return StatusCode(500, ex.Message);
}
}
[HttpDelete("delete-city/{cityId:int}")]
public IActionResult DeleteCity(int cityId)
{
try
{
_cityInfoService.DeleteCity(cityId);
return Ok();
}
catch (CityNotFoundException e)
{
return BadRequest(e.Message);
}
catch (Exception ex)
{
return StatusCode(500, ex.Message);
}
}
private static CityDto MapCity(City city)
{
return new CityDto
{
Id = city.Id,
Description = city.Description,
Name = city.Name
};
}
}
Your delete method is a HttpDelete, so it needs DELETE request. If you a trying to hit the endpoint via a browser, it won't work as it will just do a GET.
You can use Curl or Postman to issue the DELETE request to your URL.
How can I implement exception handling in my view component?
Wrapping the logic from my action method into try/catch blocks doesn't catch any exceptions thrown within a view component itself, and I don't want the app to stop functioning regardless of any errors. This is what I'm doing so far and trying to accomplish:
Action Method
public IActionResult LoadComments(int id)
{
try
{
return ViewComponent("CardComments", new { id });
}
catch (SqlException e)
{
return RedirectToAction("Error", "Home");
}
}
To reiterate, this does not catch a SqlException that occurs inside the view component itself, and thus it fails to redirect.
View Component
public class CardCommentsViewComponent : ViewComponent
{
public async Task<IViewComponentResult> InvokeAsync(int id)
{
try
{
IEnumerable<CardCommentData> comments = await DbHelper.GetCardCommentData(id);
return View(comments);
}
catch (SqlException e)
{
//Redirect from here if possible?
}
}
}
Can I accomplish this from the controller's action method? If not, how can I redirect from the view component itself? I've tried researching this problem and came up empty. Any information would be helpful.
You can try to redirect to another page using HttpContextAccessor.HttpContext.Response.Redirect:
public class CardCommentsViewComponent : ViewComponent
{
private readonly IHttpContextAccessor _httpContextAccessor;
public CardCommentsViewComponent( IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public async Task<IViewComponentResult> InvokeAsync(int id)
{
try
{
IEnumerable<CardCommentData> comments = await DbHelper.GetCardCommentData(id);
return View(comments);
}
catch (SqlException e)
{
_httpContextAccessor.HttpContext.Response.Redirect("/About");
return View(new List<CardCommentData>());
}
}
}
Register in DI :
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
But the preferred way is using global exception handler /filter to trace the exception and redirect to related error page :
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling?view=aspnetcore-2.2
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