I wrote a code but for some reason it doesn't work...can you tell me what's wrong?
I want the app not to stop when I get an exception, only to send that exception back as a json message.
Startup.cs Configure method:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v2/swagger.json", "My API");
});
}
//this is the question...?
app.UseExceptionHandler(c => c.Run(async context =>
{
var exception = context.Features.Get<IExceptionHandlerPathFeature>().Error;
var response = new { Msg = exception.Message };
await context.Response.WriteAsJsonAsync(response);
}));
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseCors(x => x
.AllowAnyMethod()
.AllowAnyHeader()
.SetIsOriginAllowed(origin => true)
.AllowCredentials());
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHub<EventHub>("/events");
});
}
Logic the method where I throw an exception:
public IEnumerable<object> Search(string text)
{
if (text.Length >= 3)
{
var result = new List<object>
{
clubRepository.GetAll().Where(club => club.ClubName.Contains(text)),
playerRepository.GetAll().Where(player => player.PlayerName.Contains(text)),
managerRepository.GetAll().Where(manager => manager.ManagerName.Contains(text)),
stadiumRepository.GetAll().Where(stadium => stadium.StadiumName.Contains(text))
};
return result;
}
else
{
throw new ArgumentException("The text is not long enough!");
}
}
So I would like to get this exception message as json!
Now it is happening --> Image1
I want that to happen --> Image2
You can extract exception elements as a key value in a dictionary.
And serialize that into JSON.
Inspired by this answer, here is my method to extract key/value from exception:
public static Dictionary<string, string> GetExceptionDetails(Exception exception)
{
var properties = exception.GetType()
.GetProperties();
var fields = properties
.Select(property => new
{
Name = property.Name,
Value = property.GetValue(exception, null)
})
.Select(x => $"{x.Name} = {(x.Value != null ? x.Value.ToString() : string.Empty)}")
.ToDictionary(k => k, v => v);
return fields;
}
For my test I have done this:
private static void CallSome()
{
throw new Exception("xx");
}
in your try/catch you do the following:
try
{
CallSome();
}
catch (Exception e)
{
string str = JsonConvert.SerializeObject(GetExceptionDetails(e));
Console.WriteLine(str);
}
This will return you a JSON payload.
I use dotnet 6 console app. I have also installed the Newtonsoft.Json package. You can also you dotnet JsonSerializer:
var str = JsonSerializer.Serialize(GetExceptionDetails(e));
Note: it also worth reading this also this.
You can use Middleware to handle any exception.
First: Create a ErrorHandlerMiddleware.cs like below.
public class ErrorHandlerMiddleware
{
private readonly RequestDelegate _next;
public ErrorHandlerMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception error)
{
var response = context.Response;
//Set response ContentType
response.ContentType = "application/json";
//Set custome error message for response model
var responseContent = new ResponseContent()
{
error = error.Message
};
//handler many Exception types
switch (error)
{
case ArgumentException _ae:
response.StatusCode = StatusCodes.Status400BadRequest;
break;
default:
response.StatusCode = StatusCodes.Status500InternalServerError;
break;
}
//Using Newtonsoft.Json to convert object to json string
var jsonResult = JsonConvert.SerializeObject(responseContent);
await response.WriteAsync(jsonResult);
}
}
//Response Model
public class ResponseContent
{
public string error { get; set; }
}
}
Next: In Startup.cs, use the middleware
app.UseMiddleware<ErrorHandlerMiddleware>();
Here is project structure of my simple example :
Goodluck!
I don't know if you misunderstood but your app is only stopping because you are running it inside VS Code (Debug Mode). If you run your app externally (in command, run "dotnet run") you'll see that the app will not stop.
Now, it's just an advice. Your app is already sending back the json but with status code 500 (internal server error). A better practice for validation errors, is returning as bad request (status code 400). You can add one line like below.
app.UseExceptionHandler(c => c.Run(async context =>
{
var exception = context.Features.Get<IExceptionHandlerPathFeature>().Error;
var response = new { Msg = exception.Message };
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
await context.Response.WriteAsJsonAsync(response);
}));
Then, if you want to improve a little more. You can replace exceptions with notification pattern. Here are some links if you are interested.
https://martinfowler.com/articles/replaceThrowWithNotification.html
https://timdeschryver.dev/blog/creating-a-new-csharp-api-validate-incoming-requests
Related
I'm migrating a .Net Core 2.2 web API application having API Controllers only with no views. I have a custom API response set in my project using SuperJsonOutputFormatter. Now, I'm using NewtonsoftJsonOutputFormatter for creating custom response for the APIs. But as per microsoft document services.AddMvc() is obsolete in .Net Core 3.1. So, how to call the customformatter in startup.cs. I'm using the below code
services.AddControllers().AddNewtonsoftJson(options =>
{
options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
options.SerializerSettings.Formatting = Formatting.Indented;
});
services.AddScoped<SuperJsonOutputFormatterFilter>();
services.AddMvc(opts =>
{
opts.EnableEndpointRouting = false;
var oldFormatter = opts.OutputFormatters.OfType<CustomOutputFormatter>().Single();
opts.OutputFormatters.Remove(oldFormatter);
var replacementJsonOutputFormatter =
new CustomOutputFormatter(oldFormatter.serializerSettings, ArrayPool<char>.Shared);
opts.OutputFormatters.Add(replacementJsonOutputFormatter);
}).SetCompatibilityVersion(CompatibilityVersion.Version_3_0);
The configure services is as below
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseRouting();
app.UseExceptionHandler("/Error");
app.UseAuthentication();
app.UseStaticFiles();
app.UseHttpsRedirection();
app.UseMvc();
}
The above code gives a runtime error that Some services are not able to be constructed, Error while validating the service descriptor. How to call customformatter without using Services.AppMvc()
My Error_Helper is as below
ErrorDescription Class
public class ErrorDescription
{
public ErrorDescription(HttpStatusCode statusCode)
{
this.Code = (int)statusCode;
this.Description = GetDescription((int)statusCode);
}
string GetDescription(int statusCode)
{
return statusCode switch
{
404 => "Employee ID not found",
500 => "Internal server error",
400 => "Device token already registered",
406 => "No data found in table",
_ => "",
};
}
[JsonProperty("errorCode")]
public int Code { get; set; }
[JsonProperty("errorDescription")]
public string Description { get; set; }
}
FormatterFilter Class
public class CustomJsonOutputFormatterFilter : IAsyncActionFilter
{
private readonly CustomOutputFormatter _formatter;
// inject SuperJsonOutputFormatter service
public CustomJsonOutputFormatterFilter(CustomOutputFormatter formatter)
{
this._formatter = formatter;
}
// a helper method that provides an ObjectResult wrapper over the raw object
private ObjectResult WrapObjectResult(ActionExecutedContext context, object obj)
{
var wrapper = new ObjectResult(obj);
wrapper.Formatters.Add(this._formatter);
context.Result = wrapper;
return wrapper;
}
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
ActionExecutedContext resultContext = await next();
// in case we get a 500
if (resultContext.Exception != null && !resultContext.ExceptionHandled)
{
var ewrapper = this.WrapObjectResult(resultContext, new { });
ewrapper.StatusCode = (int)HttpStatusCode.InternalServerError;
resultContext.ExceptionHandled = true;
return;
}
else
{
switch (resultContext.Result)
{
case BadRequestObjectResult b: // 400 with an object
var bwrapper = this.WrapObjectResult(resultContext, b.Value);
bwrapper.StatusCode = b.StatusCode;
break;
case NotFoundObjectResult n: // 404 with an object
var nwrapper = this.WrapObjectResult(resultContext, n.Value);
nwrapper.StatusCode = n.StatusCode;
break;
case ObjectResult o: // plain object
this.WrapObjectResult(resultContext, o.Value);
break;
case JsonResult j: // plain json
this.WrapObjectResult(resultContext, j.Value);
break;
case StatusCodeResult s: // other statusCodeResult(including NotFound,NoContent,...), you might want to custom this case
var swrapper = this.WrapObjectResult(resultContext, new { result="" });
swrapper.StatusCode = s.StatusCode;
break;
}
}
}
}
Custom Outputformatter Class, this class calls the customformatterfilter
public class CustomOutputFormatter : NewtonsoftJsonOutputFormatter
{
public CustomOutputFormatter(JsonSerializerSettings serializerSettings,
ArrayPool<char> charPool) : base (serializerSettings, charPool)
{
}
public JsonSerializerSettings serializerSettings { get; private set; }
public override async Task WriteResponseBodyAsync(
OutputFormatterWriteContext context,
Encoding selectedEncoding)
{
if (context == null)
throw new ArgumentNullException(nameof(context));
if (selectedEncoding == null)
if (selectedEncoding == null)
throw new ArgumentNullException(nameof(selectedEncoding));
using TextWriter writer = context.WriterFactory(context.HttpContext.Response.Body, selectedEncoding);
var statusCode = context.HttpContext.Response.StatusCode;
var rewrittenValue = new
{
status = IsSucceeded(statusCode),
error = IsSucceeded(statusCode) ? null : new ErrorDescription((HttpStatusCode)statusCode),
data = context.Object,
};
writer.Write(rewrittenValue);
this.CreateJsonWriter(writer);
await writer.FlushAsync();
}
private bool IsSucceeded(int statusCode)
{
// 204 is not an error but handled
if (statusCode >= 400 || statusCode == 204) { return false; }
return true;
}
}
I haven't looked in detail at your implementation (it seems quite convoluted, what are you trying to achieve?), but you can use UseControllers() in the same way as you were using UseMvc() previously to configure the MvcOptions instance. e.g:
services.AddControllers(options =>
{
options.InputFormatters.Insert(0, new VcardInputFormatter());
options.OutputFormatters.Insert(0, new VcardOutputFormatter());
})
That might solve your problem - there's no need to call AddMvc.
However, the error "Some services are not able to be constructed" suggests you are missing a service dependency. The error message will tell you which one. This is a new feature in .NET Core 3.1, service provider validation, that you can read about in this blog post.
I have this ErrorHandlingMiddleware that looks like this:
public class ErrorHandlingMiddleware
{
private readonly RequestDelegate _next;
public ErrorHandlingMiddleware(RequestDelegate next)
{
this._next = next;
}
public async Task Invoke(HttpContext context /* other dependencies */)
{
try
{
await _next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private static Task HandleExceptionAsync(HttpContext context, Exception ex)
{
var statusCode = (int)HttpStatusCode.InternalServerError;
if (ex is NotFoundError) statusCode = (int)HttpStatusCode.NotFound;
//else if (ex is MyUnauthorizedException) code = HttpStatusCode.Unauthorized;
//else if (ex is MyException) code = HttpStatusCode.BadRequest;
var error = new AttemptError(statusCode, ex.Message, ex);
context.Response.ContentType = "application/json";
context.Response.StatusCode = statusCode;
return context.Response.WriteAsync(error.ToString());
}
}
And I have added this to my Startup class:
public void Configure(IApplicationBuilder app)
{
app.UseMiddleware<ErrorHandlingMiddleware>();
app.SeedIdentityServerDatabase();
app.UseDeveloperExceptionPage();
app.UseIdentityServer();
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "r3plica Identity Server v1");
c.OAuthClientId("swagger");
c.OAuthAppName("Swagger Api UI");
});
app.UseMvc();
}
I would expect that if I was anywhere in my application and I throw an exception, it would be caught and it would execute this line:
await HandleExceptionAsync(context, ex);
So, I set up a test:
throw new Exception();
Which is thrown in my controller. When I run my application and then call the endpoint that has that exception thrown, it does indeed get to the Invoke method of my ErrorHandlingMiddleware, but instead of an exception being caught, it just goes to the await _next(context)....
Does anyone know what I am doing wrong?
Your middleware will work if you place the call to use it after app.UseDeveloperExceptionPage(); in Startup.
Handle errors in ASP.NET Core says
Place the call to UseDeveloperExceptionPage before any middleware that you want to catch exceptions.
I fixed this by creating a filter:
public class HttpResponseExceptionFilter : IActionFilter, IOrderedFilter
{
public int Order { get; set; } = int.MaxValue - 10;
public void OnActionExecuting(ActionExecutingContext context) { }
public void OnActionExecuted(ActionExecutedContext context)
{
if (context.Exception == null) return;
var attempt = Attempt<string>.Fail(context.Exception);
if (context.Exception is AttemptException exception)
{
context.Result = new ObjectResult(attempt)
{
StatusCode = exception.StatusCode,
};
}
else
{
context.Result = new ObjectResult(attempt)
{
StatusCode = (int)HttpStatusCode.InternalServerError,
};
}
context.ExceptionHandled = true;
}
}
And registering it like this:
services.AddControllers(options => options.Filters.Add(new HttpResponseExceptionFilter()));
I'm trying to setup exception handling in a new .net Core 2.2 Web application project.
Currently I tried two approaches - being
Use the built-in ExceptionHandlers: app.UseExceptionHandler, app.UseStatusCodePages, etc...
Created my own middleware to handle exceptions which I've added to the bottom of this question.
Now, the main issue is that I can use my own custom middleware, but (of course) this is used on all requests, and not only on API calls.
What I would like to achieve is have separated exception logic for Web API and Web Pages, so that I can use my custom middleware and return format for API, and use app.UseDeveloperExceptionPage() for example for web pages.
Exception middleware for reference:
public static class ExceptionMiddleware
{
public static void ConfigureExceptionHandler(this IApplicationBuilder app, ILoggerFactory loggerFactory,
bool includeStackTrace = false)
{
app.UseExceptionHandler(builder =>
builder.Run(async context =>
{
var logger = loggerFactory.CreateLogger(nameof(ExceptionMiddleware));
// Set the response status code
context.Response.StatusCode = (int) HttpStatusCode.InternalServerError;
context.Response.ContentType = "application/json";
var contextFeature = context.Features.Get<IExceptionHandlerFeature>();
if (contextFeature != null)
{
// Get the current exception
var currentException = contextFeature.Error;
logger.LogError(currentException, $"Something went wrong: {currentException}");
ErrorDetails errorDetails;
if (includeStackTrace)
errorDetails = new ExtendedErrorDetails
{
StatusCode = (HttpStatusCode) context.Response.StatusCode,
Message = currentException.Message,
StackTrace = currentException.StackTrace
};
else
errorDetails = new ErrorDetails
{
StatusCode = (HttpStatusCode) context.Response.StatusCode,
Message = "Internal Server Error."
};
// Write the response
await context.Response.WriteAsync(JsonConvert.SerializeObject(errorDetails));
}
}));
}
private class ErrorDetails
{
public HttpStatusCode StatusCode { [UsedImplicitly] get; set; }
public string Message { [UsedImplicitly] get; set; }
}
private class ExtendedErrorDetails : ErrorDetails
{
public string StackTrace { [UsedImplicitly] get; set; }
}
}
You could not identify the error from exception, I suggest you try to make different route for mvc and web api. For web api, add attribute route with api, and then check the request path in the middleware or exception handler like
app.UseExceptionHandler(builder =>
{
builder.Run(async context =>
{
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
var error = context.Features.Get<IExceptionHandlerFeature>() as ExceptionHandlerFeature;
var requestPath = error.Path;
});
});
Update:
app.UseDeveloperExceptionPage();
app.UseWhen(context => context.Request.Path.StartsWithSegments("/api"), subApp =>
{
subApp.UseExceptionHandler(builder =>
{
builder.Run(async context =>
{
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
await context.Response.WriteAsync("Error");
});
});
});
I have a MediatR Pipeline behavior for validating commands with the FluentValidation library. I've seen many examples where you throw a ValidationException from the behavior, and that works fine for me. However in my scenario I want to update my response object with the validation errors.
I am able to build and run the following code. When I set a break point within the if statement the CommandResponse is constructed with the validation errors as expected - but when the response is received by the original caller it is null:
public class RequestValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse>
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
public RequestValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
{
_validators = validators;
}
public Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
{
var context = new ValidationContext(request);
// Run the associated validator against the request
var failures = _validators
.Select(v => v.Validate(context))
.SelectMany(result => result.Errors)
.Where(f => f != null)
.ToList();
if(failures.Count != 0)
{
var commandResponse = new CommandResponse(failures) { isSuccess = false };
return commandResponse as Task<TResponse>;
}
else
{
return next();
}
}
}
I think it has to do with my attempt to cast it as Task - but without this I get compiler errors. I'm returning the same type that my command handler would if validation passes so I am at a loss as to why it returns a null instance of the expected response. I feel like there is a better way to handle this, but I've tried a number of variations to no avail. Any suggestions? Is there a better pattern to use? I'd prefer to keep this in the pipeline as it will be reused a lot.
I ended up adding exception handling middleware to the MVC project. Instead of trying to pass back the validation errors as an object I throw a ValidationException inside of the pipeline behavior and the middleware handles any and all exceptions across the entire project. This actually worked out better as I handle all exceptions in one place higher up in the processing chain.
Here is the updated portion of the code I posted:
if(failures.Count != 0)
{
// If any failures are found, throw a custom ValidationException object
throw new ValidationException(failures);
}
else
{
// If validation passed, allow the command or query to continue:
return next();
}
Here is the exception handling middleware:
public class ErrorHandlingMiddleware
{
private readonly RequestDelegate next;
public ErrorHandlingMiddleware(RequestDelegate next)
{
this.next = next;
}
public async Task Invoke(HttpContext context /* other dependencies */)
{
try
{
await next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private static Task HandleExceptionAsync(HttpContext context, Exception exception)
{
// Log issues and handle exception response
if (exception.GetType() == typeof(ValidationException))
{
var code = HttpStatusCode.BadRequest;
var result = JsonConvert.SerializeObject(((ValidationException)exception).Failures);
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)code;
return context.Response.WriteAsync(result);
}
else
{
var code = HttpStatusCode.InternalServerError;
var result = JsonConvert.SerializeObject(new { isSuccess = false, error = exception.Message });
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)code;
return context.Response.WriteAsync(result);
}
}
}
You then register the middleware in your Startup before MVC is added:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseMiddleware(typeof(ErrorHandlingMiddleware));
app.UseMvc();
}
Note: You can also create an extension method for your middleware:
public static class ErrorHandlingMiddlewareExtension
{
public static IApplicationBuilder UseErrorHandlingMiddleware(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<ErrorHandlingMiddleware>();
}
}
Which allows you to register it like this:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseErrorHandlingMiddleware();
app.UseMvc();
}
I am using .Net core 3.1 and I was not able to catch the exceptions when I added the middleware before the following block in Configure function of Startup
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
check in the configure method. Make sure to register it after the above statement like this. It is quite obvious but may help someone like me.
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMiddleware<ErrorHandlingMiddleware>();
This is my Startup() method in Startup.cs:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
if (env.IsDevelopment())
{
loggerFactory.AddNLog();
}
app.UseApplicationInsightsRequestTelemetry(); // Needs to be first
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
context.Response.StatusCode = 500;
context.Response.ContentType = "application/json";
var error = context.Features.Get<IExceptionHandlerFeature>();
if (error != null)
{
var ex = error.Error;
await context.Response.WriteAsync(new ErrorResponse
{
Code = 500,
Message = ex.Message
}.ToString(), Encoding.UTF8);
}
});
});
app.UseApplicationInsightsExceptionTelemetry(); // After error page
app.UseMvc();
}
This is my route in a controller.
[HttpGet]
[Route("Test")]
public string Test()
{
if(!string.IsNullOrEmpty(Request.Query["test"]))
throw new Exception("Testing..");
return "Hello.";
}
ErrorResponse.cs
public class ErrorResponse
{
public int Code { get; set; }
public string Message { get; set; }
public override string ToString()
{
return JsonConvert.SerializeObject(this);
}
}
When I go to http://website/test it shows "Hello." but when I go to http://website/test?test=asd it throws my exception (the one in the route) instead of catching it with UseExceptionHandler and stops the program.
My goal is to make it show the following json instead:
{
"Error": 500,
"Message": "Testing..."
}
I am using asp.net core.
Old post, but I just hit the same issue.
There is likely some exception filter applied globally. Look up the MVC configuration in Startup.ConfigureServices. I had services.AddMvcOptions(o => o.Filters.Add<ErrorHandlerAttribute>()) which was the problem.
Your app is stopped on exception cause you run it in debug mode in VS and Debugger is attached. Run app from console for example (or disable "break on exception" option in VS.
In my case it was that app.UseDeveloperExceptionPage() was already catching the exceptions and conflicting in some way with this middleware.
}