Error handling in the controller - error-handling

I have this two controllers actions
#1 will throw divide by zero exception
public IActionResult About()
{
ViewData["Message"] = "Your application description page.";
var x = 0;
var y = 5 / x;
return View();
}
#2 after the exception, i want this action to be called in production mode.
public IActionResult Error()
{
var model = new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier };
return View(model);
}
I have two issues
When I am in VS Debug mode, app.UseDeveloperExceptionPage() is used and therefore Error controller is not called (that is fine for development environment).
When I am in VS Production mode, visually app.UseDeveloperExceptionPage() is used but i dont know how to confirm what environment is really used (breakpoints in startup.cs does not work in production mode.)
If somehow Error action was called, how can I get full exception message (for log purpose) ? By default only RequestId is stored inside the model.
I do not want to use try catch syntax in each action or repeat action filter attribute, because i want to have general exception handler implemented like i had before in global.asax before core version.
My startup.cs is very easy.
// 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.UseBrowserLink();
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseStatusCodePages();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
any idea ?

To get the exception details, you just need:
var ex = HttpContext.Features.Get<IExceptionHandlerFeature>();

we can use the below line of code
var feature = HttpContext.Features.Get<IExceptionHandlerFeature>();
var error = feature?.Error;
_logger.LogError("Oops!", error);
return View("~/Views/Shared/Error.cshtml", error);
Can you refer the below link for more details
Click

Related

How to return HTTP 404

I am building an asp.net core Web API and I need to be able to hide some of the actions in a controller.
I use the following code to return HTTP 404 (Not Found):
[HttpGet]
public IActionResult Index()
{
if(!_isEnabled)
{
return NotFound();
}
However, in my browser I get this result:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.4",
"title": "Not Found",
"status": 404,
"traceId": "00-502319d62a6027718d2ee2cb3c9f263f-28de7bfdfb48f2d8-00"
}
I need to make the call as if the controller does not exists and the browser shows this:
How can a Controller returns a "real" HTTP 404 experience as if the controller dos not exists at that route?
Update 1
The answers return a JSON data and response code 404.
I am trying to do something different.
I am trying to hide the controller as if it doesn't exist for security reasons. I like the end user browser see above screenshot (Edge in my example)
Update 2
I changed to the following code:
[HttpGet]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status200OK)]
public IActionResult Index()
{
if(!_isEnabled)
{
return StatusCode(StatusCodes.Status404NotFound);
}
and the controller returns the following result:
{"type":"https://tools.ietf.org/html/rfc7231#section-6.5.4","title":"Not Found","status":404,"traceId":"00-3275026575270e11a4b1a5ab0817776a-a4777e626460faeb-00"}
The behavior is strange. Is it a new feature in aspnet code 6 ?
Update 3
Here is my middleware setup in the Program.c. It is plain oob setup:
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
builder.Logging.AddAzureWebAppDiagnostics();
// Add services to the container.
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddApplicationInsightsTelemetry();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
}
Solution For Update 1:
Middleware could be your savior here thus can be achived what you are trying to implement.
Controller:
[ProducesResponseType(StatusCodes.Status404NotFound)]
public IActionResult GetById(int id)
{
// return Ok(NotFound());
return StatusCode(StatusCodes.Status404NotFound);
}
Note: You can choose either of the status pattern.
Middleware:
public class CustomResponseMiddleware
{
private readonly RequestDelegate _next;
public CustomResponseMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext httpContext)
{
if (httpContext.Response.StatusCode == 404)
{
httpContext.Response.Redirect("/WrongControllerName/WrongAction");
}
await _next(httpContext);
}
}
Note: As you can see, we are checking the controller status code and checking if any 404 decteced. Once the desired status code we will redirect a controller which doesn't exist at all that eventually generate the expected output.
Register Middleware In Program.cs:
app.UseMiddleware<CustomResponseMiddleware>();
Output:

TempData available but not rendering in View after RedirectToAction

Edit 2: One thing I failed to mention is that I was making the post request with an onclick event in jQuery. I removed the jQuery and wrapped the button in a form. I'm guessing it has to do with that, because now it is working as I intended with TempData. If anyone has any insight as to why this is the case, I would love to hear it. Should've thought of it sooner.
Edit: I investigated a little more and noticed that the cookie ".AspNetCore.Mvc.CookieTempDataProvider" is added after the redirect, but is removed after the request is complete. If I go back in the browser and then forward again, the message is shown. I also tried removing TempData altogether, adding an "IsSaved" property to my ViewModel, setting it to true in the save method, and checking for it in the redirect action and view. Again, it recognizes it when it creates the view on redirect, but nothing shows up unless I go back in the browser and then forward again. So at this point I am not sure what my issue is.
I am trying to show a message after a successful post request. When I set a breakpoint in the view where I am checking if the TempData exists, it exists in the TempData object and goes into the if statement, but nothing changes on the page.
This is the controller action that renders the view where the saving is initiated
public async Task<IActionResult> City(double id)
{
var city = await _service.GetCity(id);
var currentWeather = await _service.GetCurrentWeather(city.ID);
return View("City", new CityViewModel(city, currentWeather));
}
Here is the post action where I set the TempData and redirect back to the previous action
[HttpPost]
public async Task<IActionResult> SaveCity(double id)
{
var city = await _service.GetCity(id);
var user = User.FindFirst(ClaimTypes.NameIdentifier).Value;
var userCity = new UserCity(user, city.ID);
try
{
var result = await _repo.SaveUserCityAsync(userCity);
TempData["Success"] = "Success";
}
catch (Exception ex)
{
}
return RedirectToAction("City", new { id = id });
}
And here is the portion of the view (with a breakpoint I can see that the temp data exists after redirect).
#if (TempData["Success"] != null)
{
<p>#TempData["Success"]</p>
}
Here are the relevant (as far as I can tell) portions of my startup
public void ConfigureServices(IServiceCollection services)
{
services.ConfigureApplicationCookie(options =>
{
});
services.Configure<CookieTempDataProviderOptions>(options => {
options.Cookie.IsEssential = true;
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}

Catch Exceptions Globally using ElmahCore in Aspnet core 3.0?

I am using Aspnet core 3.0 and I have configured ElmahCore for exception handling. however from their documentation, they advise to catch exceptions using
public IActionResult Test()
{
HttpContext.RiseError(new InvalidOperationException("Test"));
...
}
How can I configure Elmahcore to automatically catch and log all exceptions? or Do I have to write HttpContext.RiseError everytime I want to catch and log an exception?
Like Do I have to put try catch blocks for every ActionResult and call HttpContext.RiseError() in all of my catch blocks?
Is there a way that I can configure catching and logging of exceptions using ElmahCore globally?
Based on #Fei-han's suggestion and this global error handling link, I am able to log exceptions globally in my production environment. In Startup.cs file, I made sure that I have an ExceptionHandler configured when my application is running in production mode like
Startup.cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseElmah();
//Other configurations
}
This ensures that whenever an uncaught exception has occurred, it will call the Error Action Method of Home Controller
Home Controller
using Microsoft.AspNetCore.Diagnostics;
public IActionResult Error()
{
var exceptionFeature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();
if (exceptionFeature != null)
{
// Get the exception that occurred
Exception exceptionThatOccurred = exceptionFeature.Error;
//log exception using ElmahCore
HttpContext.RiseError(exceptionThatOccurred);
}
//Return custom error page (I have modified the default html of
//Shared>Error.cshtml view and showed my custom error page)
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
Now all of my exceptions are getting logged and I am also showing a customized error page in response to an exception.

swagger : Failed to load API definition undefined /swagger/v1/swagger.json

I have tried to configure swagger in my asp.net core api and getting the following error. Failed to load API definition undefined /swagger/v1/swagger.json
I am not sure why I am getting this error. I have added the necessary configuration in the startup file
I have tried the following paths but there has been no difference
/swagger/v1/swagger.json
../swagger/v1/swagger.json
v1/swagger.json
startup.cs
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)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddSwaggerGen(c =>
{
});
services.AddDbContext<NorthwindContext>(item => item.UseSqlServer(Configuration.GetConnectionString("NorthwindDBConnection")));
services.AddCors(option => option.AddPolicy("MyPolicy", builder => {
builder.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod();
}));
var mappingConfig = new MapperConfiguration(mc =>
{
mc.AddProfile(new MappingProfile());
});
IMapper mapper = mappingConfig.CreateMapper();
services.AddSingleton(mapper);
services.AddScoped<ICustomerRepository, CustomerRepository>();
}
// 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
{
// 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.UseCors("MyPolicy");
app.UseHttpsRedirection();
app.UseSwagger();
app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "API name"); });
app.UseMvc();
}
}
CustomerController
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Customer.Repository;
using CustomerService.Models;
using CustomerService.ViewModel;
using Microsoft.AspNetCore.Mvc;
namespace CustomerService.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class CustomersController : Controller
{
ICustomerRepository _customersRepository;
public CustomersController(ICustomerRepository customersRepository)
{
_customersRepository = customersRepository;
}
[HttpGet]
[Route("GetCustomers")]
//[NoCache]
[ProducesResponseType(typeof(List<CustomerViewModel>), 200)]
[ProducesResponseType(typeof(ApiResponse), 400)]
public async Task<IActionResult> Customers()
{
try
{
var customers = await _customersRepository.GetAllCustomers();
if (customers == null)
{
return NotFound();
}
return Ok(customers);
}
catch
{
return BadRequest();
}
}
[HttpGet]
[Route("GetCustomer")]
//[NoCache]
[ProducesResponseType(typeof(List<CustomerViewModel>), 200)]
[ProducesResponseType(typeof(ApiResponse), 400)]
public async Task<IActionResult> Customers(string customerId)
{
if (customerId == null)
{
return BadRequest();
}
try
{
var customer = await _customersRepository.GetCustomer(customerId);
if (customer == null)
{
return NotFound();
}
return Ok(customer);
}
catch
{
return BadRequest();
}
}
[HttpPost]
[Route("AddCustomer")]
public async Task<IActionResult> AddCustomer([FromBody] CustomerViewModel model)
{
if (ModelState.IsValid)
{
try
{
var customerId = await _customersRepository.Add(model);
if (customerId != null)
{
return Ok(customerId);
}
else
{
return NotFound();
}
}
catch(Exception ex)
{
return BadRequest();
}
}
return BadRequest();
}
[HttpPost]
[Route("DeleteCustomer")]
public async Task<IActionResult> DeleteCustomer(string customerId)
{
int result = 0;
if (customerId == null)
{
return BadRequest();
}
try
{
var customer = await _customersRepository.Delete(customerId);
if (customer == null)
{
return NotFound();
}
return Ok(customer);
}
catch
{
return BadRequest();
}
}
[HttpPost]
[Route("UpdateCustomer")]
public async Task<IActionResult> UpdateCustomer([FromBody] CustomerViewModel model)
{
if (ModelState.IsValid)
{
try
{
await _customersRepository.Update(model);
return Ok();
}
catch(Exception ex)
{
if (ex.GetType().FullName == "Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException")
{
return NotFound();
}
return BadRequest();
}
}
return BadRequest();
}
}
}
Swagger also cannot deal with two Classes having the same name (at least, not out of the box). So if you have two name spaces, and two classes having the same name, it will fail to initialize.
If you are on the broken Swashbuckle page, Open Dev Tools ... look at the 500 response that Swagger sends back and you will get some great insight.
Here's the dumb thing I was doing ... had a route in the HTTPGet as well as a ROUTE route.
[HttpGet("id")]
[ProducesResponseType(typeof(string), 200)]
[ProducesResponseType(500)]
[Route("{employeeID:int}")]
You are getting an error. Because of you doubled your action names. Look at this example.
Swagger – Failed To Load API Definition , Change [Route("GetCustomers")] names and try again.
I know this was is resolved, but I had this same problem today.
In my case, the problem was that I had a base controller class I created to other controllers inherit from.
The problem started to happen when I created a public function on the base class. Turning it to protected did the trick
This is usually indicative of controllers/actions that Swashbuckle doesn't support for one reason or another.
It's expected that you don't have a swagger.json file in your project. Swashbuckle creates and serves that dynamically using ASP.NET Core's ApiExplorer APIs. What's probably happening here is that Swashbuckle is unable to generate Swagger.json and, therefore, the UI is failing to display.
It's hard to know exactly what caused the failure, so the best way to debug is probably just to remove half your controllers (just move the files to a temporary location) and check whether the issues persists. Then you'll know which half of your controllers contains the troublesome action. You can 'binary search' removing controllers (and then actions) until you figure out which action method is causing Swashbuckle to not be able to generate Swagger.json. Once you know that, it should be obvious whether this is some issue in your code or an issue that should be filed in the Swashbuckle repo.
You could press F12 to open the chrome browser's developer tools to check the cause of failure ,then enter the failed request path and click on the error file to preview the detailed error .
It could also be an issue with ambiguous routes or something like that tripping Swashbuckle up. Once you've narrowed down the cause of failure to something more specific like that, it can either be fixed or filed, as appropriate.
If you want to access swagger via host:port/swagger/v1/swagger.json then you should add options: SwaggerGenOptions inside
public void ConfigureServices(IServiceCollection services)
{
services.AddSwaggerGen(c =>
c.SwaggerDoc("swagger/v1", new OpenApiInfo { Version = "1.0", Title = "API" });
);
}
It should work properly.

How to force re authentication between ASP Net Core 2.0 MVC web app and Azure AD

I have an ASP.Net Core MVC web application which uses Azure AD for authentication. I have just received a new requirement to force user to reauthenticate before entering some sensitive information (the button to enter this new information calls a controller action that initialises a new view model and returns a partial view into a bootstrap modal).
I have followed this article which provides a great guide for achieving this very requirement. I had to make some tweaks to get it to work with ASP.Net Core 2.0 which I think is right however my problems are as follows...
Adding the resource filter decoration "[RequireReauthentication(0)]" to my controller action works however passing the value 0 means the code never reaches the await.next() command inside the filter. If i change the parameter value to say 30 it works but seems very arbitrary. What should this value be?
The reauthentication works when calling a controller action that returns a full view. However when I call the action from an ajax request which returns a partial into a bootstrap modal it fails before loading the modal with
Response to preflight request doesn't pass access control check: No
'Access-Control-Allow-Origin' header is present on the requested
resource. Origin 'https://localhost:44308' is therefore not allowed
access
This looks like a CORS issue but I don't know why it would work when going through the standard mvc process and not when being called from jquery. Adding
services.AddCors();
app.UseCors(builder =>
builder.WithOrigins("https://login.microsoftonline.com"));
to my startup file doesn't make any difference. What could be the issue here?
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// Ommitted for clarity...
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddAzureAd(options => Configuration.Bind("AzureAd", options))
.AddCookie();
services.AddCors();
// Ommitted for clarity...
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// Ommitted for clarity...
app.UseCors(builder => builder.WithOrigins("https://login.microsoftonline.com"));
app.UseStaticFiles();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
AzureAdAuthenticationBuilderExtensions.cs
public static class AzureAdAuthenticationBuilderExtensions
{
public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder)
=> builder.AddAzureAd(_ => { });
public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder, Action<AzureAdOptions> configureOptions)
{
builder.Services.Configure(configureOptions);
builder.Services.AddSingleton<IConfigureOptions<OpenIdConnectOptions>, ConfigureAzureOptions>();
builder.AddOpenIdConnect(options =>
{
options.ClaimActions.Remove("auth_time");
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = RedirectToIdentityProvider
};
});
return builder;
}
private static Task RedirectToIdentityProvider(RedirectContext context)
{
// Force reauthentication for sensitive data if required
if (context.ShouldReauthenticate())
{
context.ProtocolMessage.MaxAge = "0"; // <time since last authentication or 0>;
}
else
{
context.Properties.RedirectUri = new PathString("/Account/SignedIn");
}
return Task.FromResult(0);
}
internal static bool ShouldReauthenticate(this RedirectContext context)
{
context.Properties.Items.TryGetValue("reauthenticate", out string reauthenticate);
bool shouldReauthenticate = false;
if (reauthenticate != null && !bool.TryParse(reauthenticate, out shouldReauthenticate))
{
throw new InvalidOperationException($"'{reauthenticate}' is an invalid boolean value");
}
return shouldReauthenticate;
}
// Ommitted for clarity...
}
RequireReauthenticationAttribute.cs
public class RequireReauthenticationAttribute : Attribute, IAsyncResourceFilter
{
private int _timeElapsedSinceLast;
public RequireReauthenticationAttribute(int timeElapsedSinceLast)
{
_timeElapsedSinceLast = timeElapsedSinceLast;
}
public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next)
{
var foundAuthTime = int.TryParse(context.HttpContext.User.FindFirst("auth_time")?.Value, out int authTime);
var ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
if (foundAuthTime && ts - authTime < _timeElapsedSinceLast)
{
await next();
}
else
{
var state = new Dictionary<string, string> { { "reauthenticate", "true" } };
await AuthenticationHttpContextExtensions.ChallengeAsync(context.HttpContext, OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties(state));
}
}
}
CreateNote.cs
[HttpGet]
[RequireReauthentication(0)]
public IActionResult CreateNote(int id)
{
TempData["IsCreate"] = true;
ViewData["PostAction"] = "CreateNote";
ViewData["PostRouteId"] = id;
var model = new NoteViewModel
{
ClientId = id
};
return PartialView("_Note", model);
}
Razor View (snippet)
<a asp-controller="Client" asp-action="CreateNote" asp-route-id="#ViewData["ClientId"]" id="client-note-get" data-ajax="true" data-ajax-method="get" data-ajax-update="#client-note-modal-content" data-ajax-mode="replace" data-ajax-success="ShowModal('#client-note-modal', null, null);" data-ajax-failure="AjaxFailure(xhr, status, error, false);"></a>
All help appreciated. Thanks
The CORS problem is not in your app.
Your AJAX call is trying to follow the authentication redirect to Azure AD,
which will not work.
What you can do instead is in your RedirectToIdentityProvider function, check if the request is an AJAX request.
If it is, make it return a 401 status code, no redirect.
Then your client-side JS needs to detect the status code, and issue a redirect that triggers the authentication.