I'm trying to load pages from xaml files and then push them to the navigation stack of prism. For this purpose I added the following methods to my NavigationService:
public class MyNavigationService : PageNavigationService, IMyNavigationService
{
public async Task<INavigationResult> NavigateAsync(Page page, INavigationParameters parameters, bool? useModalNavigation, bool animated)
{
var result = new NavigationResult();
try
{
NavigationSource = PageNavigationSource.NavigationService;
await ProcessNavigation(GetCurrentPage(), page, parameters, useModalNavigation, animated);
result.Success = true;
return result;
}
catch (Exception ex)
{
_logger.Log(ex.ToString(), Category.Exception, Priority.High);
result.Exception = ex;
return result;
}
finally
{
NavigationSource = PageNavigationSource.Device;
}
}
protected virtual async Task ProcessNavigation(Page currentPage, Page nextPage, INavigationParameters parameters, bool? useModalNavigation, bool animated)
{
if (nextPage == null)
return;
if (currentPage == null)
{
throw new NotImplementedException();
//await ProcessNavigationForRootPage(nextPage, parameters, useModalNavigation, animated);
return;
}
if (currentPage is ContentPage)
{
await ProcessNavigationForContentPage(currentPage, nextPage, parameters, useModalNavigation, animated);
}
else if (currentPage is NavigationPage)
{
//await ProcessNavigationForNavigationPage((NavigationPage)currentPage, nextPage, parameters, useModalNavigation, animated);
throw new NotImplementedException();
}
else if (currentPage is TabbedPage)
{
//await ProcessNavigationForTabbedPage((TabbedPage)currentPage, nextPage, parameters, useModalNavigation, animated);
throw new NotImplementedException();
}
else if (currentPage is CarouselPage)
{
//await ProcessNavigationForCarouselPage((CarouselPage)currentPage, nextPage, parameters, useModalNavigation, animated);
throw new NotImplementedException();
}
else if (currentPage is MasterDetailPage)
{
//await ProcessNavigationForMasterDetailPage((MasterDetailPage)currentPage, nextPage, parameters, useModalNavigation, animated);
throw new NotImplementedException();
}
}
/// <summary>
/// Processes a navigation for a content page.
/// </summary>
/// <param name="currentPage">The current page.</param>
/// <param name="nextPage">The page to navigate to.</param>
/// <param name="parameters">Navigation parameters</param>
/// <param name="useModalNavigation">If <c>true</c> uses PopModalAsync, if <c>false</c> uses PopAsync.</param>
/// <param name="animated">If <c>true</c> the transition is animated, if <c>false</c> there is no animation on transition.</param>
/// <returns>Navigation task</returns>
protected virtual async Task ProcessNavigationForContentPage(Page currentPage, Page nextPage, INavigationParameters parameters, bool? useModalNavigation, bool animated)
{
await ProcessNavigation(nextPage, new Queue<string>(), parameters, useModalNavigation, animated);
await DoNavigateAction(currentPage, string.Empty, nextPage, parameters, async () =>
{
await DoPush(currentPage, nextPage, useModalNavigation, animated);
});
}
}
The navigation for this pages works and displays correctly.
But when I try to navigate further (via navigationService.NavigateAsync("ViewName");) it does nothing, even though the NavigationResult.Success is true. The page "ViewName" is a registered ContentPage (registered via container.Register< ViewName, ViewNameViewModel >()).
I think that my navigation with pages, as seen above, is missing something important for navigating further.
Has someone done something similar and could help me?
EDIT:
I noticed that if I want to navigate away from my xaml loaded page, somehow the GetCurrentPage() method and the loacal field _page returns a Page with everything set to null instead of the page I did load from the xaml.
Related
I am learning Blazor, and I have a WebAssembly client application.
I created a WebAPI at the server which does some additional validation over and above the standard data annotation validations. For example, as it attempts to write a record to the database it checks that no other record exists with the same email address. Certain types of validation can't reliably happen at the client, particularly where race conditions could produce a bad result.
The API controller returns a ValidationProblem result to the client, and Postman shows the body of the result as:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "|f06d4ffe-4aa836b5b3f4c9ae.",
"errors": {
"Email": [
"The email address already exists."
]
}
}
Note that the validation error is in the "errors" array in the JSON.
Back in the Blazor Client application, I have the typical HandleValidSubmit function that posts the data to the API and receives a response, as shown here:
private async void HandleValidSubmit()
{
var response = await Http.PostAsJsonAsync<TestModel>("api/Test", testModel);
if (response.StatusCode != System.Net.HttpStatusCode.Created)
{
// How to handle server-side validation errors?
}
}
My question is, how to best process server-side validation errors? The user experience ought to be the same as any other validation error, with the field highlighted, the validation message shown, and the summary at the top of the page.
I ended up solving this by creating a ServerValidator component. I'll post the code here in case it is helpful for others seeking a solution to the same problem.
This code assumes you are calling a Web API endpoint that returns a ValidationProblem result if there are issues.
public class ServerValidator : ComponentBase
{
[CascadingParameter]
EditContext CurrentEditContext { get; set; }
protected override void OnInitialized()
{
base.OnInitialized();
if (this.CurrentEditContext == null)
{
throw new InvalidOperationException($"{nameof(ServerValidator)} requires a cascading " +
$"parameter of type {nameof(EditContext)}. For example, you can use {nameof(ServerValidator)} " +
$"inside an EditForm.");
}
}
public async void Validate(HttpResponseMessage response, object model)
{
var messages = new ValidationMessageStore(this.CurrentEditContext);
if (response.StatusCode == HttpStatusCode.BadRequest)
{
var body = await response.Content.ReadAsStringAsync();
var validationProblemDetails = JsonSerializer.Deserialize<ValidationProblemDetails>(body);
if (validationProblemDetails.Errors != null)
{
messages.Clear();
foreach (var error in validationProblemDetails.Errors)
{
var fieldIdentifier = new FieldIdentifier(model, error.Key);
messages.Add(fieldIdentifier, error.Value);
}
}
}
CurrentEditContext.NotifyValidationStateChanged();
}
// This is to hold the response details when the controller returns a ValidationProblem result.
private class ValidationProblemDetails
{
[JsonPropertyName("status")]
public int? Status { get; set; }
[JsonPropertyName("title")]
public string Title { get; set; }
[JsonPropertyName("type")]
public string Type { get; set; }
[JsonPropertyName("errors")]
public IDictionary<string, string[]> Errors { get; set; }
}
}
To use this new component, you will need to add the component within your EditForm:
<EditForm Model="agency" OnValidSubmit="HandleValidSubmit">
<ServerValidator #ref="serverValidator" />
<ValidationSummary />
... put all your form fields here ...
</EditForm>
Lastly, you can kick off the validation in your #code section:
#code {
private TestModel testModel = new TestModel();
private ServerValidator serverValidator;
private async void HandleValidSubmit()
{
var response = await Http.PostAsJsonAsync<TestModel>("api/TestModels", testModel);
if (response.StatusCode != System.Net.HttpStatusCode.Created)
{
serverValidator.Validate(response, testModel);
}
else
{
Navigation.NavigateTo(response.Headers.Location.ToString());
}
}
}
In theory, this ought to allow you to bypass client validation entirely and rely on your Web API to do it. In practice, I found that Blazor performs client validation when there are annotations on your model, even if you don't include a <DataAnnotationsValidator /> in your form. However, it will still catch any validation issues at the server and return them to you.
how to best process server-side validation errors? The user experience ought to be the same as any other validation error, with the field highlighted, the validation message shown, and the summary at the top of the page.
I don't know what comes in your response, so I made a generic version of a component that do what you need.
Get the CascadingParameter of the EditContext
[CascadingParameter]
public EditContext EditContext { get; set; }
Have a ValidationMessageStore to hold the errors and a function that will display the errors
private ValidationMessageStore _messageStore;
private EventHandler<ValidationRequestedEventArgs> OnValidationRequested => (s, e) =>
{
_messageStore.Clear();
};
private EventHandler<FieldChangedEventArgs> OnFieldChanged => (s, e) =>
{
_messageStore.Clear(e.FieldIdentifier);
};
protected override void OnInitialized()
{
base.OnInitialized();
if (EditContext != null)
{
_messageStore = new ValidationMessageStore(EditContext);
EditContext.OnFieldChanged += OnFieldChanged;
EditContext.OnValidationRequested += OnValidationRequested;
}
}
public override void Dispose()
{
base.Dispose();
if (EditContext != null)
{
EditContext.OnFieldChanged -= OnFieldChanged;
EditContext.OnValidationRequested -= OnValidationRequested;
}
}
private void AddFieldError(ERROR_CLASS_YOU_ARE_USING validatorError)
{
_messageStore.Add(EditContext.Field(validatorError.FIELD_NAME), validatorError.ERROR_MESSAGE);
}
Call the function of the component using it's ref
private async void HandleValidSubmit()
{
var response = await Http.PostAsJsonAsync<TestModel>("api/Test", testModel);
if (response.StatusCode != System.Net.HttpStatusCode.Created)
{
// How to handle server-side validation errors?
// You could also have a foreach or a function that receives an List for multiple fields error display
MyHandleErrorComponent.AddFieldError(response.ERROR_PROPERTY);
}
}
https://learn.microsoft.com/en-us/aspnet/core/blazor/forms-validation has an example of how to handle server-side validation errors:
private async Task HandleValidSubmit(EditContext editContext)
{
customValidator.ClearErrors();
try
{
var response = await Http.PostAsJsonAsync<Starship>(
"StarshipValidation", (Starship)editContext.Model);
var errors = await response.Content
.ReadFromJsonAsync<Dictionary<string, List<string>>>();
if (response.StatusCode == HttpStatusCode.BadRequest &&
errors.Count() > 0)
{
customValidator.DisplayErrors(errors);
}
else if (!response.IsSuccessStatusCode)
{
throw new HttpRequestException(
$"Validation failed. Status Code: {response.StatusCode}");
}
else
{
disabled = true;
messageStyles = "color:green";
message = "The form has been processed.";
}
}
catch (AccessTokenNotAvailableException ex)
{
ex.Redirect();
}
catch (Exception ex)
{
Logger.LogError("Form processing error: {Message}", ex.Message);
disabled = true;
messageStyles = "color:red";
message = "There was an error processing the form.";
}
}
Use two phase validation.
Hook up an event for when the email is entered which calls an "IsEmailUnique" method on your api. This offers your user real time validation information. Perhaps disable the "Save" button until the email has been validated on the server.
You can then handle the Bad Request as you would any other server-side errors.
I created a form where the user can update his data account. In this form the user is also able to change the account password, before doing so, I ask him the current password, this is the field:
<div class="form-group">
<label>Current Password</label>
<input class="form-control" id="oldPassword"
asp-for="#Model.ExistingPassword" type="password" />
<div class="invalid-feedback"></div>
</div>
as you can see the oldPassword input bound the property ExistingPassword which is part of the ViewModel of that View and have the following declaration:
[Required, MinLength(6), MaxLength(50), DataType(DataType.Password)]
public string ExistingPassword { get; set; }
when the form is submitted I call the following ajax function:
$.post(url, user, function (response) {
//Some stuff
}).done(function (response) {
alert("Updated executed");
}).fail(function (jqXHR, textStatus, errorThrown) {
alert("Error happened!");
});
the parameter of the function are taken by the form, in particular:
url: $(this).attr('action');
user: $(this).serialize();
the action of the form will call the following controller: User\UpdateUser.
Inside the UpdateUser method I execute the following check:
public async Task<UserProfileViewModel> UpdateUserAsync(UserProfileViewModel updatedUser)
{
if (!await _userManager.CheckPasswordAsync(originalUser, updatedUser.ExistingPassword))
throw new Exception("Invalid password");
essentially, the condition check if the current password is correct, if not, then an exception will raised.
Now, my question with this is: how can I know which type of exception the method has generated?
I need to know which type of exception the method UpdateUser has generated because there are different exceptions in the method.
Suppose the Invalid Password exceptions is raised, I need to display a message inside invalid-feedback div, next to oldPassword, so the user know why the update has failed.
Thanks in advance for any help.
Normally, I recommend not using an exception except in actual exception circumstances, but given the way you've designed this, you have a few options.
I'd suggest creating a custom "UpdateUserException" that you can throw that will include additional information, which can be provided by an enum or just string.
public class UpdateUserException : Exception {
public UpdateUserError ErrorCondition;
public UpdateUserException(UpdateUserError error, string message)
{
ErrorCondition = error;
Message = message;
}
}
then you would throw it
throw new UpdateUserException(UpdateUserError.BadPassword, "Invalid Password");
then you would catch it
try {}
catch (UpdateUserException e)
{
if (e.ErrorCondition == UpdateUserException.BadPassword)
{
// handle your exception.
}
}
Have a look at the UserManager ChangePassword Method.
You can bind the UserManager to use DependencyInjection like this (in Startup.cs)
public async void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
...
app.UseAuthentication();
app.UseMvc();
var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>();
using (var scope = scopeFactory.CreateScope())
{
UserManager<User> userManager = scope.ServiceProvider.GetRequiredService<UserManager<User>>();
}
}
And then in your Controller s Constructor
private readonly UserManager<User> _userManager;
public AccountController(UserManager<User> userManager)
{
_userManager = userManager;
}
And finally your endpoint:
[HttpPost("ChangePassword")]
public async Task<IActionResult> ChangePassword([FromBody]ChangePasswordRequest changePasswordParams)
{
if (changePasswordParams == null)
return BadRequest($"{nameof(changePasswordParams)} must not be null!");
if (string.IsNullOrWhiteSpace(changePasswordParams.OldPassword) || string.IsNullOrWhiteSpace(changePasswordParams.NewPassword))
return BadRequest("old and new passwords have to be provided, but they both are empty.");
var userId = User.Claims.FirstOrDefault(c => c.Type == "id")?.Value;
var user = await _userManager.FindByIdAsync(userId);
var result = await _userManager.ChangePasswordAsync(user, changePasswordParams.OldPassword, changePasswordParams.NewPassword);
if (result.Succeeded)
return NoContent();
return BadRequest(result.Errors);
}
after that you can handle the errors in a switch statement.
Using Exceptions for handled errors are not recommended since they generally ends up with Internal Server error and actually It is beyond of its purpose.
The best approach would be to send BadRequest as It is stated by #maerlin.
However, If you insist to use Exceptions in your application or your applciation is architected to work in this way. I suggest you to inherit new CustomApplcationException class from ApplicationException and then inherit UpdateUserException and vs. from CustomApplicationException class. After that, I Suggest you to handle your exceptions in ErrorHandlingMiddleware and return HandledExceptions at least with BadRequest (400) status code.
The Example Code would be
public class ExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILoggerManager _logger;
public ExceptionMiddleware(RequestDelegate next, ILoggerManager logger)
{
_logger = logger;
_next = next;
}
public async Task InvokeAsync(HttpContext httpContext)
{
try
{
await _next(httpContext);
}
catch (CustomApplicationException cae)
{
await HandleCustomExceptionAsync(httpContext, cae);
}
catch (Exception ex)
{
_logger.LogError($"Something went wrong: {ex}");
await HandleExceptionAsync(httpContext, ex);
}
}
private static Task HandleExceptionAsync(HttpContext context, Exception exception)
{
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
return context.Response.WriteAsync(new ErrorDetails()
{
StatusCode = context.Response.StatusCode,
Message = "Internal Server Error from the custom middleware."
}.ToString());
}
private static Task HandleCustomExceptionAsync(HttpContext context, Exception exception)
{
context.Response.StatusCode = 400;
return context.Response.WriteAsync(new ErrorDetails()
{
StatusCode = context.Response.StatusCode,
Message = exception.Message
}.ToString());
}
}
then you need to regiter middleware in your Startup.cs
app.UseMiddleware<ExceptionMiddleware>();
please see https://code-maze.com/global-error-handling-aspnetcore/ and http://www.talkingdotnet.com/global-exception-handling-in-aspnet-core-webapi/ for further details.
I am trying to authorize my Action method. Everything works fine but my Challenge() method always redirect to Login Page even if I've already logged in. I try to return Forbid() method and it redirects to Access Denied page as it should. What might be the problem with Challenge() method ?
public async Task<IActionResult> Edit(int id)
{
var project = await _context.Project.Include(p => p.OrganizationsLink).FirstOrDefaultAsync(p => p.Id == id);
if (project == null)
return NotFound();
//AUTHORIZATION
var allowed = await _authz.AuthorizeAsync(User, null, new ProjectEditRequirement(project));
if (!allowed.Succeeded)
return Challenge();
return View(project);
}
For Challenge, it is controlled by IAuthenticationService. AuthenticationService will call AuthenticateAsync to invoke the handler.AuthenticateAsync().
Not sure whether you implement custom IAuthenticationHandler, I will go deep the CookieAuthenticationHandler.
The complete workflow for CookieAuthenticaiton is below:
return Challenge() , for Challenge
public virtual ChallengeResult Challenge()
=> new ChallengeResult();
ChanllengeResult will call ExecuteResultAsync
public override async Task ExecuteResultAsync(ActionContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var loggerFactory = context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger<ChallengeResult>();
logger.ChallengeResultExecuting(AuthenticationSchemes);
if (AuthenticationSchemes != null && AuthenticationSchemes.Count > 0)
{
foreach (var scheme in AuthenticationSchemes)
{
await context.HttpContext.ChallengeAsync(scheme, Properties);
}
}
else
{
await context.HttpContext.ChallengeAsync(Properties);
}
}
context.HttpContext.ChallengeAsync will invoke ChallengeAsync
public static Task ChallengeAsync(this HttpContext context, string scheme, AuthenticationProperties properties) =>
context.RequestServices.GetRequiredService<IAuthenticationService>().ChallengeAsync(context, scheme, properties);
For CookieAuthenticationHandler
protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
{
var redirectUri = properties.RedirectUri;
if (string.IsNullOrEmpty(redirectUri))
{
redirectUri = OriginalPathBase + Request.Path + Request.QueryString;
}
var loginUri = Options.LoginPath + QueryString.Create(Options.ReturnUrlParameter, redirectUri);
var redirectContext = new RedirectContext<CookieAuthenticationOptions>(Context, Scheme, Options, properties, BuildRedirectUri(loginUri));
await Events.RedirectToLogin(redirectContext);
}
As you could find, the HandleChallengeAsync redirect the action.
In my option, you could try return Forbid(), otherwise, you will need to override the HandleChallengeAsync.
I'm writing my first Web API 2. I'm performing authorization using a custom HttpMessageHandler. However, my controller needs to know the username specified in the credentials.
Researching this, it appears that ApiController does not have a Controller.HttpContext property. And I see there are potential issues accessing HttpContext.Current. So while I am actually able to store the username in HttpContext.Current.Items in my HttpMessageHandler and then access that information from my controller, I'm not sure that will always be reliable.
I also saw recommendations to use the RequestContext.Principal property; however, I could not find the current request's username anywhere in this data.
How can my controller reliably get the username for the current request?
NOTE: I refer to the username but in this case the actual user is another piece of software calling the API. The "username" reflects the software that is making the call.
#Win: Well, that part is what I am developing. But currently basic
authentication seems appropriate, where the username identifies the
software contacting us and the password is a special key
Here is the sample code for BasicAuthenticationMessageHandler which uses message handler to support HTTP Basic Authentication.
You can read more at Page 121 of ASP.NET Web API 2: Building a REST Service from Start to Finish.
IBasicSecurityService
public interface IBasicSecurityService
{
bool SetPrincipal(string username, string password);
}
BasicSecurityService
public class BasicSecurityService : IBasicSecurityService
{
public bool SetPrincipal(string username, string password)
{
// Get user from database
var user = GetUser(username);
IPrincipal principal = null;
if (user == null || (principal = GetPrincipal(user)) == null)
{
// System could not validate user
return false;
}
Thread.CurrentPrincipal = principal;
if (HttpContext.Current != null)
{
HttpContext.Current.User = principal;
}
return true;
}
public virtual IPrincipal GetPrincipal(User user)
{
var identity = new GenericIdentity(user.Username, Constants.SchemeTypes.Basic);
identity.AddClaim(new Claim(ClaimTypes.GivenName, user.Firstname));
identity.AddClaim(new Claim(ClaimTypes.Surname, user.Lastname));
// Get authroized roles and add them as Role Claim.
identity.AddClaim(new Claim(ClaimTypes.Role, "Manager"));
return new ClaimsPrincipal(identity);
}
}
BasicAuthenticationMessageHandler
public class BasicAuthenticationMessageHandler : DelegatingHandler
{
public const char AuthorizationHeaderSeparator = ':';
private const int UsernameIndex = 0;
private const int PasswordIndex = 1;
private const int ExpectedCredentialCount = 2;
private readonly IBasicSecurityService _basicSecurityService;
public BasicAuthenticationMessageHandler(IBasicSecurityService basicSecurityService)
{
_basicSecurityService = basicSecurityService;
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
if (HttpContext.Current.User.Identity.IsAuthenticated)
{
// Already authenticated; passing on to next handler...
return await base.SendAsync(request, cancellationToken);
}
if (!CanHandleAuthentication(request))
{
// Not a basic auth request; passing on to next handler...
return await base.SendAsync(request, cancellationToken);
}
bool isAuthenticated;
try
{
isAuthenticated = Authenticate(request);
}
catch (Exception e)
{
// Failure in auth processing
return CreateUnauthorizedResponse();
}
if (isAuthenticated)
{
var response = await base.SendAsync(request, cancellationToken);
return response;
}
return CreateUnauthorizedResponse();
}
public bool CanHandleAuthentication(HttpRequestMessage request)
{
return (request.Headers != null
&& request.Headers.Authorization != null
&& request.Headers.Authorization.Scheme.ToLowerInvariant() == Constants.SchemeTypes.Basic);
}
public bool Authenticate(HttpRequestMessage request)
{
// Attempting to authenticate...
var authHeader = request.Headers.Authorization;
if (authHeader == null)
{
return false;
}
var credentialParts = GetCredentialParts(authHeader);
if (credentialParts.Length != ExpectedCredentialCount)
{
return false;
}
return _basicSecurityService.SetPrincipal(credentialParts[UsernameIndex], credentialParts[PasswordIndex]);
}
public string[] GetCredentialParts(AuthenticationHeaderValue authHeader)
{
var encodedCredentials = authHeader.Parameter;
var credentialBytes = Convert.FromBase64String(encodedCredentials);
var credentials = Encoding.ASCII.GetString(credentialBytes);
var credentialParts = credentials.Split(AuthorizationHeaderSeparator);
return credentialParts;
}
public HttpResponseMessage CreateUnauthorizedResponse()
{
var response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
response.Headers.WwwAuthenticate.Add(new AuthenticationHeaderValue(Constants.SchemeTypes.Basic));
return response;
}
}
I have an HttpModule that I have put together from cobbling a couple of different sources online together into something that (mostly) works with both traditional ASP.NET applications, as well as ASP.NET MVC applications. The largest part of this comes from the kigg project on CodePlex. My problem is in dealing with 404 errors due to a missing image. In the following code, I have had to explicitly look for an image being requested through the AcceptedTypes collection in the HttpContext's Request object. If I don't put in this check, even a missing image is causing a redirect to the 404 page defined in my section in the Web.config.
The problem with this approach is that (beyond the fact it smells) is that this is just for images. I would basically have to do this with every single content type imaginable that I do not want this redirect behavior to happen on.
Looking at the code below, can someone recommend some sort of refactoring that could allow for it to be more lenient with non-page requests? I would still want them in the IIS logs (so I would probably have to remove the ClearError() call), but I do not think that a broken image should impact the user experience to the point of redirecting them to the error page.
The code follows:
/// <summary>
/// Provides a standardized mechanism for handling exceptions within a web application.
/// </summary>
public class ErrorHandlerModule : IHttpModule
{
#region Public Methods
/// <summary>
/// Disposes of the resources (other than memory) used by the module that implements
/// <see cref="T:System.Web.IHttpModule"/>.
/// </summary>
public void Dispose()
{
}
/// <summary>
/// Initializes a module and prepares it to handle requests.
/// </summary>
/// <param name="context">
/// An <see cref="T:System.Web.HttpApplication"/> that provides access to the methods, properties, and events
/// common to all application objects within an ASP.NET application.</param>
public void Init(HttpApplication context)
{
context.Error += this.OnError;
}
#endregion
/// <summary>
/// Called when an error occurs within the application.
/// </summary>
/// <param name="source">The source.</param>
/// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
private void OnError(object source, EventArgs e)
{
var httpContext = HttpContext.Current;
var imageRequestTypes =
httpContext.Request.AcceptTypes.Where(a => a.StartsWith("image/")).Select(a => a.Count());
if (imageRequestTypes.Count() > 0)
{
httpContext.ClearError();
return;
}
var lastException = HttpContext.Current.Server.GetLastError().GetBaseException();
var httpException = lastException as HttpException;
var statusCode = (int)HttpStatusCode.InternalServerError;
if (httpException != null)
{
statusCode = httpException.GetHttpCode();
if ((statusCode != (int)HttpStatusCode.NotFound) && (statusCode != (int)HttpStatusCode.ServiceUnavailable))
{
// TODO: Log exception from here.
}
}
var redirectUrl = string.Empty;
if (httpContext.IsCustomErrorEnabled)
{
var errorsSection = WebConfigurationManager.GetSection("system.web/customErrors") as CustomErrorsSection;
if (errorsSection != null)
{
redirectUrl = errorsSection.DefaultRedirect;
if (httpException != null && errorsSection.Errors.Count > 0)
{
var item = errorsSection.Errors[statusCode.ToString()];
if (item != null)
{
redirectUrl = item.Redirect;
}
}
}
}
httpContext.Response.Clear();
httpContext.Response.StatusCode = statusCode;
httpContext.Response.TrySkipIisCustomErrors = true;
httpContext.ClearError();
if (!string.IsNullOrEmpty(redirectUrl))
{
var mvcHandler = httpContext.CurrentHandler as MvcHandler;
if (mvcHandler == null)
{
httpContext.Server.Transfer(redirectUrl);
}
else
{
var uriBuilder = new UriBuilder(
httpContext.Request.Url.Scheme,
httpContext.Request.Url.Host,
httpContext.Request.Url.Port,
httpContext.Request.ApplicationPath);
uriBuilder.Path += redirectUrl;
string path = httpContext.Server.UrlDecode(uriBuilder.Uri.PathAndQuery);
HttpContext.Current.RewritePath(path, false);
IHttpHandler httpHandler = new MvcHttpHandler();
httpHandler.ProcessRequest(HttpContext.Current);
}
}
}
}
Any feedback would be appreciated. The app that I am currently doing this with is an ASP.NET MVC application, but like I mentioned it is written to work with an MVC handler, but only when the CurrentHandler is of that type.
Edit: I forgot to mention the "hack" in this case would be the following lines in OnError():
var imageRequestTypes =
httpContext.Request.AcceptTypes.Where(a => a.StartsWith("image/")).Select(a => a.Count());
if (imageRequestTypes.Count() > 0)
{
httpContext.ClearError();
return;
}
Ultimately, the problem was being caused by not distinguishing between the different types of Context provided by a traditional ASP.NET Application and an ASP.NET MVC Application. By providing a check to determine the type of context I was dealing with, I was able to respond accordingly.
I have added separate methods for an HttpTransfer and MvcTransfer that allow for me to redirect to the error page, specifically when needed. I also changed the logic around so that I could easily get my YSOD on my local and development machines without the handler swallowing the exception.
With the exception of the code used to log the exception to the database (denoted by a TODO comment), the final code that we are using is:
using System;
using System.Net;
using System.Security.Principal;
using System.Web;
using System.Web.Configuration;
using System.Web.Mvc;
using Diagnostics;
/// <summary>
/// Provides a standardized mechanism for handling exceptions within a web application.
/// </summary>
public sealed class ErrorHandlerModule : IHttpModule
{
#region Public Methods
/// <summary>
/// Disposes of the resources (other than memory) used by the module that implements
/// <see cref="T:System.Web.IHttpModule"/>.
/// </summary>
public void Dispose()
{
}
/// <summary>
/// Initializes a module and prepares it to handle requests.
/// </summary>
/// <param name="context">
/// An <see cref="T:System.Web.HttpApplication"/> that provides access to the methods, properties, and events
/// common to all application objects within an ASP.NET application.</param>
public void Init(HttpApplication context)
{
context.Error += OnError;
}
#endregion
#region Private Static Methods
/// <summary>
/// Performs a Transfer for an MVC request.
/// </summary>
/// <param name="url">The URL to transfer to.</param>
/// <param name="currentContext">The current context.</param>
private static void HttpTransfer(string url, HttpContext currentContext)
{
currentContext.Server.TransferRequest(url);
}
/// <summary>
/// Performs a Transfer for an MVC request.
/// </summary>
/// <param name="url">The URL to transfer to.</param>
/// <param name="currentContext">The current context.</param>
private static void MvcTransfer(string url, HttpContext currentContext)
{
var uriBuilder = new UriBuilder(
currentContext.Request.Url.Scheme,
currentContext.Request.Url.Host,
currentContext.Request.Url.Port,
currentContext.Request.ApplicationPath);
uriBuilder.Path += url;
string path = currentContext.Server.UrlDecode(uriBuilder.Uri.PathAndQuery);
HttpContext.Current.RewritePath(path, false);
IHttpHandler httpHandler = new MvcHttpHandler();
httpHandler.ProcessRequest(HttpContext.Current);
}
#endregion
#region Private Methods
/// <summary>
/// Called when an error occurs within the application.
/// </summary>
/// <param name="source">The source.</param>
/// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
private static void OnError(object source, EventArgs e)
{
var httpContext = HttpContext.Current;
var lastException = HttpContext.Current.Server.GetLastError().GetBaseException();
var httpException = lastException as HttpException;
var statusCode = (int)HttpStatusCode.InternalServerError;
if (httpException != null)
{
if (httpException.Message == "File does not exist.")
{
httpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
httpContext.ClearError();
return;
}
statusCode = httpException.GetHttpCode();
}
if ((statusCode != (int)HttpStatusCode.NotFound) && (statusCode != (int)HttpStatusCode.ServiceUnavailable))
{
// TODO : Your error logging code here.
}
var redirectUrl = string.Empty;
if (!httpContext.IsCustomErrorEnabled)
{
return;
}
var errorsSection = WebConfigurationManager.GetSection("system.web/customErrors") as CustomErrorsSection;
if (errorsSection != null)
{
redirectUrl = errorsSection.DefaultRedirect;
if (httpException != null && errorsSection.Errors.Count > 0)
{
var item = errorsSection.Errors[statusCode.ToString()];
if (item != null)
{
redirectUrl = item.Redirect;
}
}
}
httpContext.Response.Clear();
httpContext.Response.StatusCode = statusCode;
httpContext.Response.TrySkipIisCustomErrors = true;
httpContext.ClearError();
if (!string.IsNullOrEmpty(redirectUrl))
{
var mvcHandler = httpContext.CurrentHandler as MvcHandler;
if (mvcHandler == null)
{
try
{
HttpTransfer(redirectUrl, httpContext);
}
catch (InvalidOperationException)
{
MvcTransfer(redirectUrl, httpContext);
}
}
else
{
MvcTransfer(redirectUrl, httpContext);
}
}
}
#endregion
}
why don't you catch the 404 in your global.asax?
protected void Application_Error(object sender, EventArgs args) {
var ex = Server.GetLastError() as HttpException;
if (ex != null && ex.ErrorCode == -2147467259) {
}
}
If I understand correctly, you only want to handle errors for actions that result in a 404?
You can check if the route for the request is null or stop routed - this is essentially how the url routing handler works to decide if the request should continue into the mvc pipeline.
var iHttpContext = new HttpContextWrapper( httpContext );
var routeData = RouteTable.Routes.GetRouteData( iHttpContext );
if( routeData == null || routeData.RouteHandler is StopRoute )
{
// This is a route that would not normally be handled by the MVC pipeline
httpContext.ClearError();
return;
}
As an aside, redirecting due to a 404 causes a less than ideal user experience and is a hangover from ASP.NET ( where you couldn't separate the view processing from the request processing ). The correct way to manage a 404 is return a 404 status code to the browser and display your custom error page rather than redirect ( which results in a 302 status code sent to the browser ).