Asynchronous MVC controller and HttpTaskAsyncHandler - c#

I'm trying to implemented a custom HttpTaskAsyncHandler for my custom content management solution. The idea is to route /100/my-little-pony to /Details/my-little-pony. I hope to achieve this with the following HttpTaskAsyncHandler:
public override async Task ProcessRequestAsync(HttpContext context)
{
try
{
var id = GetContentIdFromRouteData();
// Retrieve the content identified by the specified ID
var contentRepository = new ContentRepository();
var content = await contentRepository.GetAsync(id);
if (content == null)
throw new ContentNotFoundException(id);
// Initialize an instance of the content controller
var factory = ControllerBuilder.Current.GetControllerFactory();
var controller = (IContentController) factory.CreateController(_requestContext, content.ControllerName);
if (controller == null)
throw new ControllerNotFoundException(content.ControllerName);
try
{
// Retrieve all content type values and pass them on the the method for index pages
var action = _requestContext.RouteData.GetRequiredString("action");
if (action == "Index")
{
ContentType data = null;
if (controller.ContentType != null)
{
data = BusinessHost.Resolve<ContentType>(controller.ContentType);
data.Values = content.Parameters.ToDictionary(p => p.Name, p => p.Value);
}
_requestContext.RouteData.Values.Add("data", data);
}
var values = _requestContext.RouteData.Values;
values.Add("name", content.Name);
values.Add("controllerId", id);
values.Add("controller", content.ControllerName);
controller.Execute(_requestContext);
}
finally
{
factory.ReleaseController(controller);
}
}
catch (ContentNotFoundException ex)
{
Trace.TraceWarning($"404: {ex.Message}");
_requestContext.HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
}
}
This works wonderfully well for synchronous requests, but when I try to invoke asynchronous methods ...
#using (Html.BeginForm("Save", Html.ControllerId(), FormMethod.Post, new { #class = "form-horizontal" }))
... and this being the method ...
[HttpPost]
public async Task<ActionResult> Save(NewsViewModel model)
{ }
Edit I've changed the name of the method to Save as Async isn't inferred, I receive a new error:
The asynchronous action method 'Login' returns a Task, which cannot be executed synchronously.

Action name is SaveAsync, but code that refers to it uses Save as the name. There is no magical renaming for any actions, including async once.
Your options:
use SaveAsync to refer to the action
use ActionName attribute to rename action
rename method to Save (but that would be against convention that all async methods have ...Async suffix)
Side note: using routing may be better option for redirects than some custom handler.

There's more to MvcHandler than meets the eye, and I've come to the realization that one cannot simply hope to replicate it with a clean conscience. I've therefore decided to change the way I approach this problem entirely: instead of trying to implement my own MvcHandler, I extend my IRouteHandler instead.
This is my solution:
private static ConcurrentDictionary<string, Type> _contentTypes = new ConcurrentDictionary<string, Type>();
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
// Retrieve the page ID from the route data
var id = GetContentIdFromRouteData(requestContext);
// Retrieve the content identified by the specified ID
var contentRepository = new ContentRepository();
var content = contentRepository.Get(id);
if (content == null)
throw new ContentNotFoundException(id);
// Retrieve all content type values and pass them on the the method for index pages
var action = requestContext.RouteData.GetRequiredString("action");
if (action == "Index")
{
var data = CreateContentType(requestContext, content);
requestContext.RouteData.Values.Add("data", data);
}
var values = requestContext.RouteData.Values;
values.Add("name", content.Name);
values.Add("controllerId", id);
values.Add("controller", content.ControllerName);
return new MvcHandler(requestContext);
}
private static int GetContentIdFromRouteData(RequestContext context)
{
var idString = context.RouteData.GetRequiredString("id");
int id;
if (!int.TryParse(idString, out id))
throw new ArgumentException("Content can't be loaded due to an invalid route parameter.", "id");
return id;
}
private static ContentType CreateContentType(RequestContext context, Content content)
{
Type type;
if (!_contentTypes.ContainsKey(content.ControllerName) ||
!_contentTypes.TryGetValue(content.ControllerName, out type))
{
var factory = ControllerBuilder.Current.GetControllerFactory();
var controller = (IContentController)factory.CreateController(context, content.ControllerName);
if (controller == null)
throw new ControllerNotFoundException(content.ControllerName);
type = controller.ContentType;
factory.ReleaseController(controller);
_contentTypes.TryAdd(content.ControllerName, type);
}
ContentType data = null;
if (type != null)
{
data = BusinessHost.Resolve<ContentType>(type);
data.Values = content.Parameters.ToDictionary(p => p.Name, p => p.Value);
}
return data;
}

Related

Asp.net single controller action with different model type depending on route data

I want to create a generic action in my controller. The model that I would read from the body depends on other route data.
For example I have a resources controller with a create action. the route is something like this /api/[controller]/[action]/{resource} where resource is a route parameter.
So, POST: /api/resources/create/book should create a book resource in the repository. Each resource has it's own CreateModel. For instace book may use
class BookCreateModel
{
[Required]
public string Title {get; set;}
[Required]
public Guid AuthorId {get; set;}
... // etc
}
I would like my action to have a signature like the one below
public Task<IActionResult> Create([FromRoute] resource, [FromBody] object model)
{
if(!ModelState.IsValid)
return BadRequest(ModelState);
...
}
The actual model type should depend on the resource parameter and the action name (create in this example)
I should probably create a model binder, but I want to have all the funtionality of the default model binder (ModelState, validations etc). The only different thing I want to do is to choose which model type it should bind to. The rest should be kept the same.
Is there a way to do it, or should I implement the whole binding logic myself?
How about setting route like below and register startup use endpoints to map controller
[Produces("application/json")]
[Route("api/[controller]/[action]/{resource}")]
[ApiController]
public class ResourceController : ControllerBase
{
[HttpPost]
public Task<IActionResult> Create([FromRoute] string resource, [FromBody] object model)
{
if(!ModelState.IsValid)
return BadRequest(ModelState);
}
}
Startup
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
After a lot of googling, the only solution I could find and address my problem was to create my own binder that would mimic the default BodyModelBinder.
Then, in the place were the InputFormatterContext was instantiated, instead of passing bindingContext.ModelMetadata as the 4th argument, I pass bindingContext.ModelMetadata.GetMetadataForType([desired type here]), where the type I pass depends on other context values (such as route parameters, controller and action context etc).
Then I use the binder using a binder attribute in my action like this
[HttpPut("{resource}"), ActionName("Create")]
public async Task<IActionResult> CreateResource(
[ModelBinder(BinderType = typeof(BodyResourceModelBinder))] object model
)
{
...
}
By executing .GetType() in the model instance, I get the actual type I had set in the binder's code.
The final binder code is this:
public class BodyResourceModelBinder: IModelBinder
{
private readonly IList<IInputFormatter> _formatters;
private readonly Func<Stream, Encoding, TextReader> _readerFactory = (s, e) => new StreamReader(s, e);
private readonly ILogger _logger;
public BodyResourceModelBinder(IOptions<MvcOptions> mvcOptions, ILogger<BodyResourceModelBinder> logger = null)
{
if(mvcOptions == null || mvcOptions.Value == null)
throw new ArgumentNullException(nameof(mvcOptions));
_formatters = mvcOptions.Value.InputFormatters.ToList();
_logger = logger;
}
internal bool AllowEmptyBody { get; set; }
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
if(bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
string modelBindingKey;
if(bindingContext.IsTopLevelObject)
{
modelBindingKey = bindingContext.BinderModelName ?? string.Empty;
}
else
{
modelBindingKey = bindingContext.ModelName;
}
var httpContext = bindingContext.HttpContext;
var formatterContext = new InputFormatterContext(
httpContext,
modelBindingKey,
bindingContext.ModelState,
// THIS IS THE ACTUAL CHANGE. I CREATE NEW METADATA BASED ON THE TYPE I WANT TO BIND TO
bindingContext.ModelMetadata.GetMetadataForType(typeof(<PUT YOUR TYPE HERE>)),
_readerFactory,
AllowEmptyBody);
var formatter = (IInputFormatter)null;
for(var i = 0; i < _formatters.Count; i++)
{
if(_formatters[i].CanRead(formatterContext))
{
formatter = _formatters[i];
break;
}
}
if(formatter == null)
{
var message = $"Unsupported content type: {httpContext.Request.ContentType}";
var exception = new UnsupportedContentTypeException(message);
bindingContext.ModelState.AddModelError(modelBindingKey, exception, bindingContext.ModelMetadata);
return;
}
try
{
var result = await formatter.ReadAsync(formatterContext);
if(result.HasError)
return;
if(result.IsModelSet)
{
// The actual type of result.Model here is the type you provided in the Metadata of the IInputFormatter above
var model = result.Model;
bindingContext.Result = ModelBindingResult.Success(model);
}
else
{
var message = bindingContext
.ModelMetadata
.ModelBindingMessageProvider
.MissingRequestBodyRequiredValueAccessor();
bindingContext.ModelState.AddModelError(modelBindingKey, message);
}
}
catch(Exception exception) when(exception is InputFormatterException || ShouldHandleException(formatter))
{
bindingContext.ModelState.AddModelError(modelBindingKey, exception, bindingContext.ModelMetadata);
}
}
private bool ShouldHandleException(IInputFormatter formatter)
{
// Any explicit policy on the formatters overrides the default.
var policy = (formatter as IInputFormatterExceptionPolicy)?.ExceptionPolicy ??
InputFormatterExceptionPolicy.MalformedInputExceptions;
return policy == InputFormatterExceptionPolicy.AllExceptions;
}
}

Unable to Retain ActionContext When Running HTML.Action in .Net Core 3.1

I understand that in .Net Core 3.1 that the html.action was removed in favor of ViewComponents. Unfortunately the code I have does not lend itself for a ViewComponent since it is a custom PeoplePicker control that will have user interaction. Keep in mind that this PeoplePicker control works correctly in .Net 4.7.2. I've looked on line and found methods on how to re-implement the html.action functionality. The problem I'm having is that when the code hits the await invoker.InvokeAsync(); line in the code the ActionContext that was set gets overwritten by subsequent calls to the underlying model's get/set properties. I'll walk through the code and what is happening. Here is the line that calls the PeoplePicker:
#Html.Action("PeoplePicker", "PeoplePicker", new EDAD.Models.PeoplePickerViewModel { PickerId = 20, UserProfile = Model.CurrentUser })
The next step that happens is the HTMLHelperViewExtensions that I implemented to allow the html.Action are called:
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.IO;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Mvc.Rendering
{
public static class HtmlHelperViewExtensions
{
public static IHtmlContent Action(this IHtmlHelper helper, string action, object parameters = null)
{
var controller = (string)helper.ViewContext.RouteData.Values["controller"];
return Action(helper, action, controller, parameters);
}
public static IHtmlContent Action(this IHtmlHelper helper, string action, string controller, object parameters = null)
{
var area = (string)helper.ViewContext.RouteData.Values["area"];
return Action(helper, action, controller, area, parameters);
}
public static IHtmlContent Action(this IHtmlHelper helper, string action, string controller, string area, object parameters = null)
{
if (action == null)
throw new ArgumentNullException("action");
if (controller == null)
throw new ArgumentNullException("controller");
var task = RenderActionAsync(helper, action, controller, area, parameters);
return task.Result;
}
private static async Task<IHtmlContent> RenderActionAsync(this IHtmlHelper helper, string action, string controller, string area, object parameters = null)
{
// fetching required services for invocation
var serviceProvider = helper.ViewContext.HttpContext.RequestServices;
var actionContextAccessor = helper.ViewContext.HttpContext.RequestServices.GetRequiredService<IActionContextAccessor>();
var httpContextAccessor = helper.ViewContext.HttpContext.RequestServices.GetRequiredService<IHttpContextAccessor>();
var actionSelector = serviceProvider.GetRequiredService<IActionSelector>();
// creating new action invocation context
var routeData = new RouteData();
foreach (var router in helper.ViewContext.RouteData.Routers)
{
routeData.PushState(router, null, null);
}
routeData.PushState(null, new RouteValueDictionary(new { controller = controller, action = action, area = area }), null);
routeData.PushState(null, new RouteValueDictionary(parameters ?? new { }), null);
//get the actiondescriptor
RouteContext routeContext = new RouteContext(helper.ViewContext.HttpContext) { RouteData = routeData };
var candidates = actionSelector.SelectCandidates(routeContext);
var actionDescriptor = actionSelector.SelectBestCandidate(routeContext, candidates);
var originalActionContext = actionContextAccessor.ActionContext;
var originalhttpContext = httpContextAccessor.HttpContext;
try
{
var newHttpContext = serviceProvider.GetRequiredService<IHttpContextFactory>().Create(helper.ViewContext.HttpContext.Features);
if (newHttpContext.Items.ContainsKey(typeof(IUrlHelper)))
{
newHttpContext.Items.Remove(typeof(IUrlHelper));
}
newHttpContext.Response.Body = new MemoryStream();
var actionContext = new ActionContext(newHttpContext, routeData, actionDescriptor);
actionContextAccessor.ActionContext = actionContext;
var invoker = serviceProvider.GetRequiredService<IActionInvokerFactory>().CreateInvoker(actionContext);
await invoker.InvokeAsync();
newHttpContext.Response.Body.Position = 0;
using (var reader = new StreamReader(newHttpContext.Response.Body))
{
return new HtmlString(reader.ReadToEnd());
}
}
catch (Exception ex)
{
return new HtmlString(ex.Message);
}
finally
{
actionContextAccessor.ActionContext = originalActionContext;
httpContextAccessor.HttpContext = originalhttpContext;
if (helper.ViewContext.HttpContext.Items.ContainsKey(typeof(IUrlHelper)))
{
helper.ViewContext.HttpContext.Items.Remove(typeof(IUrlHelper));
}
}
}
}
}
Everything is working at this point. The code gets to the following line where it then calls the People Picker model
routeData.PushState(null, new RouteValueDictionary(parameters ?? new { }), null);
This goes to the model and correctly gets the 2 variables with the data that was passed in:
public class PeoplePickerViewModel
{
public int? PickerId { get; set; }
public UserModel UserProfile { get; set; }
}
The code continues through the HTMLHelper code. On the line just prior to await invoker.InvokeAsync() I can view the data in both of the 2 variables (PickerID and UserProfile). This is where the problem happens. When it hits the await invoker.InvokeAsync() it goes BACK to the model and gets the UserProfile (which is now NULL), gets the PickerID which has retained the value, then gets the UserProfile AGAIN for a third time (it is still null). It then passes the info to the PeoplePicker controller where the "model" variable is used to set the PeoplePicker. Since the UserProfile was set to null by the second/third calls the model.UserProfile is set to a new UserModel() instead of using the one that was started with.
public PartialViewResult PeoplePicker(PeoplePickerViewModel model)
{
model.UserProfile = model.UserProfile ?? new UserModel();
model.PickerId = model.PickerId ?? 0;
return PartialView(model);
}
Let me add that the PeoplePicker works in all other aspects of its functionality. It's just not working when a userprofile is passed in at the start.
So here are my questions:
Why is it calling the model more than once?
Is there a way I can troubleshoot this other than what I've done to this point?
Is there a better way to do this in Core 3.1?
the UserProfile was set to null
I did a test and can reproduce same issue. In your code we can find that the UserProfile property of your PeoplePickerViewModel class is a complex type, which seems cause this issue.
To fix it, you can try the following workaround.
routeData.PushState(null, new RouteValueDictionary(new { controller = controller, action = action, area = area }), null);
if (parameters == null)
{
routeData.PushState(null, new RouteValueDictionary(new { }), null);
}
else
{
var type = parameters.GetType();
if (parameters.GetType() == typeof(PeoplePickerViewModel))
{
//dynamically generate and populate values based on your model class
var mdata = parameters as PeoplePickerViewModel;
var routeValDict = new RouteValueDictionary();
routeValDict.Add("PickerId", mdata.PickerId);
routeValDict.Add("UserProfile.Id", mdata.UserProfile.Id);
routeValDict.Add("UserProfile.Name", mdata.UserProfile.Name);
routeData.PushState(null, routeValDict, null);
}
else
{
routeData.PushState(null, new RouteValueDictionary(parameters), null);
}
}
Testing code of UserModel class
public class UserModel
{
public int Id { get; set; }
public string Name { get; set; }
}

Multiple levels in MVC custom routing

I am trying to build my own little cms. I created an abstract pageBase class that is inherited by Static, Reviews, Articles, News. Each having there own controller methods.
My problem is that I need to allow the admin to define his own custom path levels. E.g. news\local\mynewdog or Articles\events\conventions\mycon. So I would like a way to pass an array of strings and also set the custom routing.
You can make CMS-style routes seamlessly with a custom RouteBase subclass.
public class PageInfo
{
// VirtualPath should not have a leading slash
// example: events/conventions/mycon
public string VirtualPath { get; set; }
public Guid Id { get; set; }
}
public class CustomPageRoute
: RouteBase
{
private object synclock = new object();
public override RouteData GetRouteData(HttpContextBase httpContext)
{
RouteData result = null;
// Trim the leading slash
var path = httpContext.Request.Path.Substring(1);
// Get the page that matches.
var page = GetPageList(httpContext)
.Where(x => x.VirtualPath.Equals(path))
.FirstOrDefault();
if (page != null)
{
result = new RouteData(this, new MvcRouteHandler());
// Optional - make query string values into route values.
this.AddQueryStringParametersToRouteData(result, httpContext);
// TODO: You might want to use the page object (from the database) to
// get both the controller and action, and possibly even an area.
// Alternatively, you could create a route for each table and hard-code
// this information.
result.Values["controller"] = "CustomPage";
result.Values["action"] = "Details";
// This will be the primary key of the database row.
// It might be an integer or a GUID.
result.Values["id"] = page.Id;
}
// IMPORTANT: Always return null if there is no match.
// This tells .NET routing to check the next route that is registered.
return result;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
VirtualPathData result = null;
PageInfo page = null;
// Get all of the pages from the cache.
var pages = GetPageList(requestContext.HttpContext);
if (TryFindMatch(pages, values, out page))
{
if (!string.IsNullOrEmpty(page.VirtualPath))
{
result = new VirtualPathData(this, page.VirtualPath);
}
}
// IMPORTANT: Always return null if there is no match.
// This tells .NET routing to check the next route that is registered.
return result;
}
private bool TryFindMatch(IEnumerable<PageInfo> pages, RouteValueDictionary values, out PageInfo page)
{
page = null;
Guid id = Guid.Empty;
// This example uses a GUID for an id. If it cannot be parsed,
// we just skip it.
if (!Guid.TryParse(Convert.ToString(values["id"]), out id))
{
return false;
}
var controller = Convert.ToString(values["controller"]);
var action = Convert.ToString(values["action"]);
// The logic here should be the inverse of the logic in
// GetRouteData(). So, we match the same controller, action, and id.
// If we had additional route values there, we would take them all
// into consideration during this step.
if (action == "Details" && controller == "CustomPage")
{
page = pages
.Where(x => x.Id.Equals(id))
.FirstOrDefault();
if (page != null)
{
return true;
}
}
return false;
}
private void AddQueryStringParametersToRouteData(RouteData routeData, HttpContextBase httpContext)
{
var queryString = httpContext.Request.QueryString;
if (queryString.Keys.Count > 0)
{
foreach (var key in queryString.AllKeys)
{
routeData.Values[key] = queryString[key];
}
}
}
private IEnumerable<PageInfo> GetPageList(HttpContextBase httpContext)
{
string key = "__CustomPageList";
var pages = httpContext.Cache[key];
if (pages == null)
{
lock(synclock)
{
pages = httpContext.Cache[key];
if (pages == null)
{
// TODO: Retrieve the list of PageInfo objects from the database here.
pages = new List<PageInfo>()
{
new PageInfo()
{
Id = new Guid("cfea37e8-657a-43ff-b73c-5df191bad7c9"),
VirtualPath = "somecategory/somesubcategory/content1"
},
new PageInfo()
{
Id = new Guid("9a19078b-2d7e-4fc6-ae1d-3e76f8be46e5"),
VirtualPath = "somecategory/somesubcategory/content2"
},
new PageInfo()
{
Id = new Guid("31d4ea88-aff3-452d-b1c0-fa5e139dcce5"),
VirtualPath = "somecategory/somesubcategory/content3"
}
};
httpContext.Cache.Insert(
key: key,
value: pages,
dependencies: null,
absoluteExpiration: System.Web.Caching.Cache.NoAbsoluteExpiration,
slidingExpiration: TimeSpan.FromMinutes(15),
priority: System.Web.Caching.CacheItemPriority.NotRemovable,
onRemoveCallback: null);
}
}
}
return (IEnumerable<PageInfo>)pages;
}
}
You can register the route with MVC like this.
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
// Case sensitive lowercase URLs are faster.
// If you want to use case insensitive URLs, you need to
// adjust the matching code in the `Equals` method of the CustomPageRoute.
routes.LowercaseUrls = true;
routes.Add(
name: "CustomPage",
item: new CustomPageRoute());
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
The above assumes you have a CustomPageController with a Details action method.
public class CustomPageController : Controller
{
public ActionResult Details(Guid id)
{
// Do something with id
return View();
}
}
You can change the route if you want it to go to a different controller action (or even make them constructor parameters).

Unit Testing Role Access to a Method

How would i go about testing whether an authorize attribute on method or controller in a WebApi/MVC project has a specific role(s)
So i could test a method doing something like the below?
[Test]
[TestCase("Put", new []{"Admin","TeamMember"})]
[TestCase("Post", new []{"Admin","TeamMember"})]
[TestCase("Get", new []{"TeamMember"})]
public void Ensure_Methods_have_Correct_Roles(string methodToTest, List<string> roles)
{
var controller = new myController();
Assert.IsTrue(controller.HasRoles(methodToTest, roles));
}
with the has Roles extension method being stubbed out like this
public static bool HasRoles(this Controller controller, string action, string[] roles)
{
var controllerType = controller.GetType();
var method = controllerType.GetMethod(action);
object[] filters = method.GetCustomAttributes(typeof(AuthorizationAttribute), true);
if(!filters.Any(x => x.GetType() == typeof(AuthorizationAttribute))
{
throw exception()
}
var rolesOnCurrentMethodsAttribute = // This is where i'm stuck
foreach(var role in rolesOnCurrentMethodsAttribute)
{
//pseudo-code
if(!roles.contains(role)
{
return false;
}
}
return true;
}
is this even sensible or should i be testing the controller action directly and testing whether the response is a 401/403? That would require mocking up a context though and would mean more testing code since i would have to test each method separately.
EDIT: Perhaps don't focus on whether it's sensible. Just is it doable?
My thinking was that the unit tests would be the canonical specification of what actions should have what roles (since there is currently no written spec, and probably won't ever have one). If a developer changes a role, then they need to have a good reason for it.
EDIT #2
Based on Con's Answer below, this is what i've ended up with, one method to check aan action, another to check the controller.
public static bool WebApiActionHasRoles(this ApiController controller, string action, string roles)
{
var controllerType = controller.GetType();
var method = controllerType.GetMethod(action);
object[] filters = method.GetCustomAttributes(typeof(System.Web.Http.AuthorizeAttribute), true);
if (!filters.Any())
{
throw new Exception();
}
var rolesOnCurrentMethodsAttribute = filters.SelectMany(attrib => ((System.Web.Http.AuthorizeAttribute)attrib).Roles.Split(new[] { ',' })).ToList();
var rolesToCheckAgainst = roles.Split(',').ToList();
return !rolesOnCurrentMethodsAttribute.Except(rolesToCheckAgainst).Any() && !rolesToCheckAgainst.Except(rolesOnCurrentMethodsAttribute).Any();
}
public static bool WebApiControllerHasRoles(this ApiController controller, string roles)
{
var controllerType = controller.GetType();
object[] filters = controllerType.GetCustomAttributes(typeof(System.Web.Http.AuthorizeAttribute), true);
if (!filters.Any())
{
throw new Exception();
}
var rolesOnCurrentMethodsAttribute = filters.SelectMany(attrib => ((System.Web.Http.AuthorizeAttribute)attrib).Roles.Split(new[] { ',' })).ToList();
var rolesToCheckAgainst = roles.Split(',').ToList();
return !rolesOnCurrentMethodsAttribute.Except(rolesToCheckAgainst).Any() && !rolesToCheckAgainst.Except(rolesOnCurrentMethodsAttribute).Any();
}
If you want to use it with MVC instead of Web Api controllers/Actions just change the System.Web.Http.AuthorizeAttribute to System.Web.MVC.AuthorizeAttribute and in the Method Signature change ApiController to Controller
If you are referring to AuthorizeAttribute vs AuthorizationAttribute, is this what you need:
public static bool HasRoles(this Controller controller, string action, string[] roles)
{
var controllerType = controller.GetType();
var method = controllerType.GetMethod(action);
object[] filters = method.GetCustomAttributes(typeof(AuthorizeAttribute), true);
if(!filters.Any())
{
throw new Exception();
}
var rolesOnCurrentMethodsAttribute = filters.SelectMany(attrib => ((AuthorizeAttribute)attrib).Roles.Split(new[] { ',' })).ToList();
return roles.Except(rolesInMethod).Count() == 0 && rolesInMethod.Except(roles).Count() == 0;
}
Alternatively, if you want to make your tests stricter and enforce only one Authorize attribute per action:
public static bool HasRoles(this Controller controller, string action, string roles)
{
var controllerType = controller.GetType();
var method = controllerType.GetMethod(action);
var attrib = method.GetCustomAttributes(typeof(AuthorizeAttribute), true).FirstOrDefault() as AuthorizeAttribute;
if (attrib == null)
{
throw new Exception();
}
return attrib.Roles == roles;
}

Versioning ASP.NET Web API 2 with Media Types

I'm using ASP.NET Web API 2 with attribute routing but i can't seem to get the versioning using media types application/vnd.company[.version].param[+json] to work.
I get the following error:
The given key was not present in the dictionary.
which originates from testing the key _actionParameterNames[descriptor] in FindActionMatchRequiredRouteAndQueryParameters() method.
foreach (var candidate in candidatesFound)
{
HttpActionDescriptor descriptor = candidate.ActionDescriptor;
if (IsSubset(_actionParameterNames[descriptor], candidate.CombinedParameterNames))
{
matches.Add(candidate);
}
}
Source: ApiControllerActionSelector.cs
After further debugging I've realized that if you have two controllers
[RoutePrefix("api/people")]
public class PeopleController : BaseApiController
{
[Route("")]
public HttpResponseMessage GetPeople()
{
}
[Route("identifier/{id}")]
public HttpResponseMessage GetPersonById()
{
}
}
[RoutePrefix("api/people")]
public class PeopleV2Controller : BaseApiController
{
[Route("")]
public HttpResponseMessage GetPeople()
{
}
[Route("identifier/{id}")]
public HttpResponseMessage GetPersonById()
{
}
}
you can't use your custom ApiVersioningSelector : DefaultHttpControllerSelector because it will test the keys,as stated above, from all controllers having the same [RoutePrefix("api/people")] and obviously an exception will be thrown.
Just to be sure the right controller was selected
I don't know if this is a bug, but using route [RoutePrefix("api/v1/people")] to version API makes me sad.
NOTE: This works great without attribute routing.
UPDATE
public class ApiVersioningSelector : DefaultHttpControllerSelector
{
private HttpConfiguration _HttpConfiguration;
public ApiVersioningSelector(HttpConfiguration httpConfiguration)
: base(httpConfiguration)
{
_HttpConfiguration = httpConfiguration;
}
public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
IDictionary<string, HttpControllerDescriptor> controllers = GetControllerMapping();
var attributedRoutesData = request.GetRouteData().GetSubRoutes();
var subRouteData = attributedRoutesData.LastOrDefault(); //LastOrDefault() will get PeopleController, FirstOrDefault will get People{version}Controller which we don't want
var actions = (ReflectedHttpActionDescriptor[])subRouteData.Route.DataTokens["actions"];
var controllerName = actions[0].ControllerDescriptor.ControllerName;
//For controller name without attribute routing
//var controllerName = (string)routeData.Values["controller"];
HttpControllerDescriptor oldControllerDescriptor;
if (controllers.TryGetValue(controllerName, out oldControllerDescriptor))
{
var apiVersion = GetVersionFromMediaType(request);
var newControllerName = String.Concat(controllerName, "V", apiVersion);
HttpControllerDescriptor newControllerDescriptor;
if (controllers.TryGetValue(newControllerName, out newControllerDescriptor))
{
return newControllerDescriptor;
}
return oldControllerDescriptor;
}
return null;
}
private string GetVersionFromMediaType(HttpRequestMessage request)
{
var acceptHeader = request.Headers.Accept;
var regularExpression = new Regex(#"application\/vnd\.mycompany\.([a-z]+)\.v([0-9]+)\+json",
RegexOptions.IgnoreCase);
foreach (var mime in acceptHeader)
{
var match = regularExpression.Match(mime.MediaType);
if (match != null)
{
return match.Groups[2].Value;
}
}
return "1";
}
}
Thanks for the sharing your code. I have modified your version controller selector like below and tried some scenarios and it seems to work well. Can you try updating your controller selector like below and see if it works?
public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
HttpControllerDescriptor controllerDescriptor = null;
// get list of all controllers provided by the default selector
IDictionary<string, HttpControllerDescriptor> controllers = GetControllerMapping();
IHttpRouteData routeData = request.GetRouteData();
if (routeData == null)
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
//check if this route is actually an attribute route
IEnumerable<IHttpRouteData> attributeSubRoutes = routeData.GetSubRoutes();
var apiVersion = GetVersionFromMediaType(request);
if (attributeSubRoutes == null)
{
string controllerName = GetRouteVariable<string>(routeData, "controller");
if (controllerName == null)
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
string newControllerName = String.Concat(controllerName, "V", apiVersion);
if (controllers.TryGetValue(newControllerName, out controllerDescriptor))
{
return controllerDescriptor;
}
else
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
}
else
{
// we want to find all controller descriptors whose controller type names end with
// the following suffix(ex: CustomersV1)
string newControllerNameSuffix = String.Concat("V", apiVersion);
IEnumerable<IHttpRouteData> filteredSubRoutes = attributeSubRoutes.Where(attrRouteData =>
{
HttpControllerDescriptor currentDescriptor = GetControllerDescriptor(attrRouteData);
bool match = currentDescriptor.ControllerName.EndsWith(newControllerNameSuffix);
if (match && (controllerDescriptor == null))
{
controllerDescriptor = currentDescriptor;
}
return match;
});
routeData.Values["MS_SubRoutes"] = filteredSubRoutes.ToArray();
}
return controllerDescriptor;
}
private HttpControllerDescriptor GetControllerDescriptor(IHttpRouteData routeData)
{
return ((HttpActionDescriptor[])routeData.Route.DataTokens["actions"]).First().ControllerDescriptor;
}
// Get a value from the route data, if present.
private static T GetRouteVariable<T>(IHttpRouteData routeData, string name)
{
object result = null;
if (routeData.Values.TryGetValue(name, out result))
{
return (T)result;
}
return default(T);
}

Categories