Extracting route parameters from MVC 5 route templates - c#

My project is using the following code to generate MVC routed URLs for the given routeName and routeValues:
public class RouteBuilder
{
public string GetUrl(string routeName, object routeValues)
{
return RouteTable.Routes.GetVirtualPath(
new RequestContext(new MockHttpContextBase(string.Empty), new RouteData()),
routeName,
new RouteValueDictionary(routeValues)
)?.VirtualPath;
}
}
For example, given the following route definition:
public MyController : Controller
{
[Route("Foo", "{bar}/{quux}")]
public IActionResult MyControllerMethod(MyModel model) { }
}
we call:
// yields "zod/baz"
var routedUrl = RouteBuilder.GetUrl("Foo", new
{
bar = "zod",
quux = "baz",
});
The problem comes in with the route values object passed to GetUrl. Out of necessity it can be any type of object, including an anonymous one as demonstrated in the example. But this is problematic because if there are any typos in the object's definition, the route template won't match and the URL won't be built:
// yields null!
var routedUrl = RouteBuilder.GetUrl("Foo", new
{
baz = "zod", // oops, typo in 1st param name!
quux = "baz",
});
This is even more of an issue because it can only be caught at runtime.
A potential solution is this:
public class MyControllerMethodRouteParameters
{
string bar { get; set; }
string quux { get; set; }
}
public MyController : Controller
{
[Route("Foo", "{" + nameof(MyControllerMethodRouteParameters.bar) + "}/{" + nameof(MyControllerMethodRouteParameters.quux ) + "}")]
public IActionResult MyControllerMethod(MyModel model) { }
}
public class RouteBuilder
{
private string GetUrl(string routeName, object routeValues) { /* as before */ }
public string GetUrl(MyControllerMethodRouteParameters params)
{
return GetUrl(GetRouteNameFor(params.GetType(), params));
}
}
But it has a large drawback in terms of developer effort: when adding a new controller method, you also have to remember to add a route parameters class, a GetUrl overload for it, and a routename => route parameters type mapping. Easy to forget to do, and it also feels like unnecessary repetition since all of the necessary parameters and their types are already defined in the RouteAttribute.
So my idea was to generate the needed code via a T4 template, by reflecting over the assembly, grabbing all RouteAttributes and pulling the route name and template out of them, then parsing the template for the property names of the route parameters class. It's this last hurdle that I've fallen at, because while I know I can probably write a regex(es) to match and extract the route params from the template, I would prefer to use existing functionality to do this.
Problem is, I can't find anything that seems like it'll do this for me. The closest appears to be the internal class System.Web.Mvc.Routing.InlineRouteTemplateParser but my experiments with it were not particularly fruitful. Is there anything I can or should be using to achieve this, or should I just give in to the dark regex god?

Related

Null Models with default arguments need to be instantiated as such

I have the following asp.net WebApi2 route using .NET 4.6 that illustrates the problem I am having:
[Route("books/{id}")]
[HttpGet]
public JsonResponse GetBooks(string id, [FromUri]DescriptorModel model)
With the following model:
public class DescriptorModel
{
public bool Fiction { get; set; } = false;
// other properties with default arguments here
}
I am trying to allow Fiction property to be set to a default value (if not specified during the get request).
When I specify the Fiction property explicitly it works correctly:
curl -X GET --header 'Accept: application/json' 'http://127.0.0.1:11000/api/v1/books/516.375/?Fiction=false'
However, when doing the following test (omitting the property with the default argument):
curl -X GET --header 'Accept: application/json' 'http://127.0.0.1:11000/api/v1/books/516.375'
The value of "model" is bound as null which is not what I am looking for. My question is how to simply allow models defined with default values to be instantiated as such during/after the model binding process but prior to the controller's "GetBooks" action method being called.
NOTE. the reason I use models with GET requests is that documenting in swagger is much easier as then my GET/POST actions can reuse the same models in many case via inheritance.
Since you are using id as FromUri, the only way you can use a model with get is to use url with a query string
[Route("~/GetBooks/{id?}")]
[HttpGet]
public IActionResult GetBooks(string id, [FromQuery] DescriptorModel model)
in this case you url should be
'http://127.0.0.1:11000/api/v1/books/?Name=name&&fiction=true'
//or if fiction==false just
'http://127.0.0.1:11000/api/v1/books/?Name=name'
//or if want to use id
'http://127.0.0.1:11000/api/v1/books/123/?Name=name&&fiction=true'
using model your way will be working only with [FromForm] or [FromBody].
To use it as MVC recomends try this
[Route("books/{id}/{param1}/{param2}/{fiction?}")]
[HttpGet]
public JsonResponse GetBooks(string id, string param1, string param2, bool fiction)
By the way, you don't need to make bool false as default since it is false by default any way
if you want to use ID and DescriptorModel from uri you can do this only if you add Id to DescriptorModel too
[Route("books/{id}/{param1}/{param2}/{fiction?}")]
[HttpGet]
public JsonResponse GetBooks(DescriptorModel model)
UPDATE
If your mvc doesnt support [FromQuery], you can use RequestQuery inside of action like this
var value= context.Request.Query["value"];
but is better to update to MVC 6.
I wasn't able to figure out how to do this via model-binding but I was able to use Action Filters to accomplish the same thing.
Here's the code I used (note it only supports one null model per action but this could easily be fixed if needed):
public class NullModelActionFilter : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext context)
{
object value = null;
string modelName = string.Empty;
// are there any null models?
if (context.ActionArguments.ContainsValue(null))
{
// Yes => iterate over all arguments to find them.
foreach (var arg in context.ActionArguments)
{
// Is the argument null?
if (arg.Value == null)
{
// Search the parameter bindings to find the matching argument....
foreach (var parameter in context.ActionDescriptor.ActionBinding.ParameterBindings)
{
// Did we find a match?
if (parameter.Descriptor.ParameterName == arg.Key)
{
// Yes => Does the type have the 'Default' attribute?
var type = parameter.Descriptor.ParameterType;
if (type.GetCustomAttributes(typeof(DefaultAttribute), false).Length > 0)
{
// Yes => need to instantiate it
modelName = arg.Key;
var constructor = parameter.Descriptor.ParameterType.GetConstructor(new Type[0]);
value = constructor.Invoke(null);
// update the model state
context.ModelState.Add(arg.Key, new ModelState { Value = new ValueProviderResult(value, value.ToString(), CultureInfo.InvariantCulture) });
}
}
}
}
}
// update the action arguments
context.ActionArguments[modelName] = value;
}
}
}
I created a DefaultAttribute class like so:
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class DefaultAttribute : Attribute
{
}
I then added that attribute to my descriptor class:
[Default]
public class DescriptorModel
{
public bool Fiction { get; set; } = false;
// other properties with default arguments here
}
And finally registered the action filter in
public void Configure(IAppBuilder appBuilder)
{
var config = new HttpConfiguration();
// lots of configuration here omitted
config.Filters.Add(new NullModelActionFilter());
appBuilder.UseWebApi(config);
}
I definitely consider this a hack (I think I really should be doing this via model binding) but it accomplishes what I needed to do with the constraints that I was given of ASP.NET (not Core) / WebApi2 / .NET Framework so hopefully some else will benefit from this.

'Multiple actions were found' error with RESTful Web API Routing

I have the following class:
public class GetLogsRequestDto
{
public LogLevel Level { get; set; }
public LogSortOrder SortOrder { get; set; }
}
I have a Web API Controller (LogsController) with the following 2 actions:
async Task<IHttpActionResult> Get( [FromUri]int id )
async Task<IHttpActionResult> Get( [FromUri]GetLogsRequestDto dto )
The first for retrieving a specific log, and the second for retrieving a list of logs. When I make a GET request for a specific log via: /logs/123, it calls the 1st action correctly, and likewise if I make a GET request for /logs it calls the 2nd action correctly (the properties defined in that class are optional and don't need to always be provided).
However, I wanted to change the first GET method so it uses a class instead of the int id parameter, like this (note it's specifying a different (singular) type to the 2nd action above):
async Task<IHttpActionResult> Get( [FromUri]GetLogRequestDto dto )
This GetLogRequestDto class looks like this:
public class GetLogRequestDto
{
[Required]
[Range( 100, int.MaxValue )]
public int Id { get; set; }
}
My reasoning behind this approach was so that I can have validation of the model go through my standard ModelStateValidationActionFilter, and also put any specific validation attributes inside this class, rather than when using the 'int id' parameter approach, then having to perform validation.
When I implement this approach though and attempt to call /logs/1, I get the following error:
Multiple actions were found that match the request
It's not differentiating between the 2 different types used as params in these 2 methods.
The default route I have configured is:
config.Routes.MapHttpRoute(
name: "controller-id",
routeTemplate: "{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
I can't figure out why there is a problem - why it works one way but not the other.
Using a complex type for handling a single basic type parameter (that is also part of the route) in GET requests is not a great idea.
By using this approach the framework will not be able to bind your route parameter to that complex type (the route definition requires an id parameter that must be a simple type).
I strongly suggest you to revert your changes and make the id parameter again an int.
As an alternative approach you may follow this great post and implement an action filter that may validate your method parameters decorated by validation attributes even if they are simple types.
Here it is an excerpt from Mark Vincze's blog post representing the action filter attribute used to validate action parameters:
public class ValidateActionParametersAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
var descriptor = context.ActionDescriptor as ControllerActionDescriptor;
if (descriptor != null)
{
var parameters = descriptor.MethodInfo.GetParameters();
foreach (var parameter in parameters)
{
var argument = context.ActionArguments[parameter.Name];
EvaluateValidationAttributes(parameter, argument, context.ModelState);
}
}
base.OnActionExecuting(context);
}
private void EvaluateValidationAttributes(ParameterInfo parameter, object argument, ModelStateDictionary modelState)
{
var validationAttributes = parameter.CustomAttributes;
foreach (var attributeData in validationAttributes)
{
var attributeInstance = CustomAttributeExtensions.GetCustomAttribute(parameter, attributeData.AttributeType);
var validationAttribute = attributeInstance as ValidationAttribute;
if (validationAttribute != null)
{
var isValid = validationAttribute.IsValid(argument);
if (!isValid)
{
modelState.AddModelError(parameter.Name, validationAttribute.FormatErrorMessage(parameter.Name));
}
}
}
}
}

How to have a Route which points to two different controller end points which accepts different arguments in WEB Api 2

How to have a Route which points to two different controller end points which accepts different arguments in WEB Api 2
I have two different end points declared in controller and as for the REST perspective I have to use the alpha/{aplhaid}/beta format for both the end points ,
[Authorize]
[HttpPost]
[Route("alpha/{aplhaid}/beta")]
public async Task<HttpResponseMessage> CreateAlpha(Beta beta, string projectId, [FromHeader] RevisionHeaderModel revision)
[Authorize]
[HttpPost]
[Route("alpha/{aplhaid}/beta")]
public async Task<HttpResponseMessage> CreateAlpha(List<Beta> betas, string projectId, [FromHeader] RevisionHeaderModel revision)
Is it possible to use the same router with different parameters which points to 2 different end points in Web API 2?
If you really need to have the same route and the same ActionName, you could do it with an IHttpActionSelector.
public class CustomActionSelector : ApiControllerActionSelector, IHttpActionSelector
{
public new HttpActionDescriptor SelectAction(HttpControllerContext controllerContext)
{
var context = HttpContext.Current;
// Read the content. Probably a better way of doing it?
var stream = new StreamReader(context.Request.InputStream);
var input = stream.ReadToEnd();
var array = new JavaScriptSerializer().Deserialize<List<string>>(input);
if (array != null)
{
// It's an array
//TODO: Choose action.
}
else
{
// It's not an array
//TODO: Choose action.
}
// Default.
var action = base.SelectAction(controllerContext);
return action;
}
public override ILookup<string, HttpActionDescriptor> GetActionMapping(HttpControllerDescriptor controllerDescriptor)
{
var lookup = base.GetActionMapping(controllerDescriptor);
return lookup;
}
}
In your WebApiConfig:
config.Services.Replace(
typeof(IHttpActionSelector),
new CustomActionSelector());
Example for your an Controller:
public class FooController: ApiController
{
[HttpPost]
public string Post(string id)
{
return "String";
}
[HttpPost]
public string Post(List<string> id)
{
return "some list";
}
}
The solution has some big downsides, if you ask me. First of, you should look for a solution for using this CustomActionSelector only when needed. Not for all controllers as it will create an overhead for each request.
I think you should reconsider why you really need two have to identical routes. I think readability will be suffering if the same route accepts different arguments. But that's just my opinion.
I would use different routes instead.
Overload web api action method based on parameter type is not well supported.
But what about attribute based routing ?
You can find out a good example here
Route constraints let you restrict how the parameters in the route template are matched. The general syntax is "{parameter:constraint}". For example:
[Route("users/{id:int}"]
public User GetUserById(int id) { ... }
[Route("users/{name}"]
public User GetUserByName(string name) { ... }
And I think this link must be helpful
Use one route and call the other controller inside from the first controller.

Passing Complex Objects to ASP.NET Web API Using FromUri

I want to bind the URL parameters to my Point object using attribute routing and the [FromUri] attribute such that the following URL is possible:
/foo-1,2
public IHttpActionResult PostFoo(
[FromBody] string content,
[FromUri] Point point)
{
}
public class Point
{
public int A { get; set; }
public int B { get; set; }
// ...Other properties omitted for simplicity
}
I have tried the following Route attributes but none of these work:
[Route("foo-{a},{b}")]
[Route("foo-{A},{B}")]
[Route("foo-{point.A},{point.B}")]
Note that I can't use query string parameters because a badly built third party service does not accept ampersands in their URL's (Yes it's that bad). So I'm trying to build all query string parameters into the URL itself.
The two Options I'm aware of is:
Use URL Rewriter to Globally take care of every and all routes. The advantage is that (I would hope) your publisher does have some type of standard url you can transform into a friendly MVC route.
If not then you'll probably have to write your own RouteHandler. Not sure if you could use this globally, but you'd have to register it a lot (not that hard really).
public class CustomRouteHandler : MvcRouteHandler
{
protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
{
var acceptValue = requestContext.HttpContext.Request.Headers["Accept"];
if( /* do something with the accept value */)
{
// Set the new route value in the
// requestContext.RouteData.Values dictionary
// e.g. requestContext.RouteData.Values["action"] = "Customer";
}
return base.GetHttpHandler(requestContext);
}
}
Then register it:
RouteTable.Routes.MapRoute(
name: "Custom",
url: "{controller}/{action}",
defaults: new { controller = "Home", action = "Index" }
).RouteHandler = new CustomRouteHandler();

Attribute Routing and Query Strings

I am new to web api coming from a WCF background and as prep I watched Shawn Wildermuth's Pluralsight course on the subject before diving in. His course material was designed around more traditional routing. One of the subjects the course dives into is HATEOAS and how easy it is to achieve this with a base api controller and model factory.
One of the first things I hit when implementing against attribute routing was the need for the UrlHelper to have a route name as the first argument of the Link() method, something that was inherited in the conventional routing configured in the WebApiConfig.cs.
I worked around this by decorating one of my controllers route attributes with the Name property and it appears that all methods in that controller have access to the name property regardless of which method I put it on (see code below). While I find this a bit odd, it works. Now that I had HATEOAS implemented, I noticed the URL's it was generating were in the query string format and not "url" formatted (I know the term is wrong but bear with me). Instead of .../api/deliverables/1 I am getting .../api/deliverables?id=1.
This is "ok" but not the desired output. While I still have not figured out how to adjust the formatting the of the return value of the URL, I figured I would test the query string against my controller and found that in the query string format my controller does not work but in the "url" format it does.
I then spent an hour trying to figure out why. I have attempted different decorations (i.e. [FromUri] which from my reading should only be necessary for complex objects which default to the message body) to setting default values, constraints and making it optional (i.e. {id?}).
Below is the code in question, both for the controller, the base api controller and the model factory that makes the HATEOAS implementation possible.
The 3 questions I have are:
1) How to make the controller accept the "id" on the querystring AND in the url format (.../deliverables/1 and .../deliverables?id=1.
2) How to make the Link method of the URL helper return the value in the url format (it is currently returning it as a query string.
3) Proper way to name routes in WebAPI 2. What I am doing (assigning a name to a single method and the others appear to inherit it simply smells and I have to believe this would crumble as I actually start to implement more complex code. Is Shawn's implementation flawed in some way? I like not having to hard code a URL for test/development purposes but maybe UrlHelper is not the best way to achieve this. It seems to carry with it a lot of baggage that may not be necessary.
Controller:
[RoutePrefix("api/deliverables")]
public class DeliverablesController : BaseApiController
{
private readonly IDeliverableService _deliverableService;
private readonly IUnitOfWork _unitOfWork;
public DeliverablesController(IDeliverableService deliverableService, IUnitOfWorkAsync unitOfWork)
{
_deliverableService = deliverableService;
_unitOfWork = unitOfWork;
}
[Route("", Name = "Deliverables")]
public IHttpActionResult Get()
{
return Ok(_deliverableService.Get().Select(TheModelFactory.Create));
}
[Route("{id}")]
public IHttpActionResult Get(int id)
{
return Ok(TheModelFactory.Create(_deliverableService.Find(id)));
}
[Route("")]
public IHttpActionResult Post([FromBody]DeliverableModel model)
{
try
{
var entity = TheModelFactory.Parse(model);
if (entity == null)
{
return BadRequest("Could not parse Deliverable entry in body.");
}
_deliverableService.Insert(entity);
_unitOfWork.SaveChanges();
return Created(Request.RequestUri + "/" + entity.Id.ToString(CultureInfo.InvariantCulture),TheModelFactory.Create(entity));
}
catch (Exception exception)
{
return BadRequest(exception.Message);
}
}
}
Base API Controller:
public abstract class BaseApiController : ApiController
{
private ModelFactory _modelFactory;
protected ModelFactory TheModelFactory
{
get
{
return _modelFactory ?? (_modelFactory = new ModelFactory(Request));
}
}
}
Model Factory:
public class ModelFactory
{
private readonly UrlHelper _urlHelper;
public ModelFactory(HttpRequestMessage request)
{
_urlHelper = new UrlHelper(request);
}
public DeliverableModel Create(Deliverable deliverable)
{
return new DeliverableModel
{
Url = _urlHelper.Link("deliverables", new { id = deliverable.Id }),
Description = deliverable.Description,
Name = deliverable.Name,
Id = deliverable.Id
};
}
public Deliverable Parse(DeliverableModel model)
{
try
{
if (string.IsNullOrEmpty(model.Name))
return null;
var entity = new Deliverable
{
Name = model.Name,
Description = !string.IsNullOrEmpty(model.Description)
? model.Description
: string.Empty
};
return entity;
}
catch (Exception)
{
return null;
}
}
}
As a point of clarification, non-attribute (traditional) routing works without an issue for both the URI and query string formats:
config.Routes.MapHttpRoute(
name: "deliverables",
routeTemplate: "api/deliverables/{id}",
defaults: new { controller = "deliverables", id = RouteParameter.Optional }
);
In my opinion, this is one of the problems with Attributed routing. That's why I use it for exceptional cases only. I use route tables for the majority of routing then drop down into attributed routing for exceptional cases.
To solve this your way, have you thought about multiple routes on the Get(id)? (I don't actually think this would work, but its worth a try).

Categories

Resources