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).
Related
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?
My problem: I have multiple controller classes and I want that their route change depending on some value (let's call it ID) that is in external config file(can change). That ID is not constant it is generated on application start up.
[Route("api/projects/" + idForTest1FromConfigFile]
public class Test1Controller : Controller
{
public IActionResult Index()
{
return View();
}
}
UPDATE
Then I Have Test2Controller which is basically same as Test1Controller but returns different views
[Route("api/projects/" + idForTest2FromConfigFile]
public class Test2Controller : Controller
{
public IActionResult Index()
{
return View();
}
}
So lets say in my config file I have:
Test1 : 123
Test2 : 456
So when I call https://localhost:44391/api/projects/123/Index I want to get the index page from Test1Controller and when I call https://localhost:44391/api/projects/456/Index I want to get Index page from Test2Controller
is there any way that this could be done?
Thanks
If that identifier is generated at startup but then it’s constant, you can just generate a dynamic route mapping when you call UseMvc() inside of the Configure method:
var id1 = GetIdFromSomewhere();
var id2 = GetIdFromSomewhere();
app.UseMvc(routes =>
{
// Test1Controller
routes.MapRoute("test-route-1",
"/api/projects/" + id1 + "/{action}",
new { controller = "Test1", action = "Index" });
// Test2Controller
routes.MapRoute("test-route-2",
"/api/projects/" + id2 + "/{action}",
new { controller = "Test2", action = "Index" });
// …
});
If you want to use an approach that's a bit more flexible, you could consider using a custom controller convention, which can be implemented using the IControllerModelConvention interface. Using this, you could pass a configuration object into a custom convention and apply the routes using that. There are number of ways to tackle this, but here's a sample implementation:
public class RoutingControllerModelConvention : IControllerModelConvention
{
private readonly IConfiguration _configuration;
public RoutingControllerModelConvention(IConfiguration configuration)
{
_configuration = configuration;
}
public void Apply(ControllerModel controllerModel)
{
const string RouteTemplate = "/api/projects/<id>/[action]";
var routeId = _configuration["RouteIds:" + controllerModel.ControllerName];
var firstSelector = controllerModel.Selectors[0];
if (firstSelector.AttributeRouteModel == null)
firstSelector.AttributeRouteModel = new AttributeRouteModel();
firstSelector.AttributeRouteModel.Template = RouteTemplate.Replace("<id>", routeId);
}
}
In this example, I'm taking an instance of IConfiguration into the constructor, which is populated from the following appsettings.json:
{
"RouteIDs": {
"Test1": 123,
"Test2": 234
}
}
I realise you may well be using something else for your configuration, but using this approach in this example should help explain things more simply.
In the RoutingControllerModelConvention.Apply method, which gets called for each controller in your project, we look up the corresponding value from our IConfiguration instance, where we use controllerModel.ControllerName to get e.g. Test1. In this example, this gives us a value of 123. Next, we grab the first selector (there's always at least one) and, ultimately, set its route template to be /api/projects/123/[action].
With this approach, you don't need to apply a [Route] attribute to the controller itself and you don't need to use MapRoute in Startup. All you'd need to do when adding new controllers is create the controller and add an entry to (in this example) appsettings.json, accordingly.
In order to use this custom convention, you'd need to configure it in Startup.ConfigureServices:
services.AddMvc(options =>
{
options.Conventions.Add(new RoutingControllerModelConvention(Configuration));
});
For more information, the application model and conventions are documented here: Work with the application model in ASP.NET Core.
I appreciate that the implementation above isn't perfect: You'd want to check for nulls and for controller names that aren't found in the configuration, etc. This should at least serve as something to get you started on an approach that's quite flexible.
I have ProductsController and OwnersController:
public class ProductsController : ApiController
{
//constructor is here
// GET /api/products
public IHttpActionResult GetProducts()
{
return Ok(new ApiResponse());
}
// GET /api/products/{productCode}
[HttpGet, Route("api/products/{productCode}")]
public IHttpActionResult GetProductByCode(string productCode)
{
return Ok(new ApiResponse());
}
// POST /api/products
public IHttpActionResult PostProduct(Product product /*my class*/)
{
return CreatedAtRoute("DefaultApi", new { id = product.Id }, product);
}
}
It works perfectly.
But now I create second controller and do the same things but I get the error when trying POST method. Another methods work well!
Lets take a look at the code first:
public class OwnersController : ApiController
{
// constructor
// GET /api/owners/{label}
// GET /api/owners/{label}?skip=1&take=4
[Route("api/owners/{label}")]
public IHttpActionResult GetOwnersExamples(string label, int skip=0, int take=10)
{
return Ok(new ApiResponse());
}
// POST /api/owners/{productCode}
//[HttpPost]
[Route("api/owners/{productCode}"/*, Name = "CreateOwner"*/)]
public IHttpActionResult PostOwner(string productCode, Owner owner)
{
return CreatedAtRoute("DefaultApi", new { id = Owner.Id }, owner);
}
}
Error message:
UrlHelper.Link must not return null
My RouteConfig:
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
As I understand problem that CreateAtRoute method must get another RouteName. As you see I can solve the problem by adding Route Name parameter (commented now) and replace "DefaultApi" with "CreateOwner" but it looks like a hack. I believe there is another method to do it avoiding Name Property.
P.S. Looks like my Web API can see only first controller (ProductsController) - any other methods doesn't work if I delete explicit Route [Route("...")]...
As I understand problem that CreateAtRoute method must get another
RouteName. As you see I can solve the problem by adding Route Name
parameter (commented now) and replace "DefaultApi" with "CreateOwner"
but it looks like a hack. I believe there is another method to do it
avoiding Name Property.
Your understanding is almost correct. However you should specify a name not for the current route, but for the one that points to created resource. CreatedAtRoute fills response Location header that should contain a GET-able URI for newly created resource.
Here is a working sample:
[HttpGet]
[Route("api/owners/{id}", Name = "GetOwnerById")]
public IHttpActionResult GetOwner(int id)
{
// Obtain the owner by id here
return Ok(new ApiResponse());
}
[HttpPost]
[Route("api/owners/{productCode}"/*, Name = "CreateOwner"*/)]
public IHttpActionResult PostOwner(string productCode, Owner owner)
{
return CreatedAtRoute("GetOwnerById", new { id = owner.Id }, owner);
}
(Note: For get this example working, you should comment GetOwnersExamples action, otherwise multiple action will match your GET request.)
You said that it looks like a hack, but it is not. CreatedAtRoute takes a route name as argument and you should provide it. How otherwise the proper action will be selected and Location header will be built?
I solve the problem using next steps:
Delete all [RoutePrefix] for controller - let them work by default - it works perfectly for simple requests.
IMPORTANT: check all methods for duplicates! Problem was I had 2 methods with routes api/controller/{label} and api/controller/{parameter} - it can't understand which of them to use by default. (Or use explicit uri: api/controller?label=1)
IMPORTANT: avoid to put into api methods a lot of complex types - create Wrapper for them and put only one parameter!
All this actions let me delete excess attributes and make methods more readable.
Here is the result:
public IHttpActionResult PostOwner(OwnerWrapper modelWrapper)
{
string productCode = modelWrapper.Product.Code;
Owner owner = modelWrapper.Owners[0];
return CreatedAtRoute("DefaultApi", new { id = Owner.Id }, owner);
}
It is just test case, so we can see productCode is never used, but my real realization is more difficult.
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
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.