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.
Related
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.
If we ask the MVC framework to generate URLs for us, for example by using UrlHelper inside a controller, the route segments in the generated URL will be upper case.
[Route("[controller]")]
public class PeopleController : Controller
{
[HttpGet]
public IActionResult Get()
{
var url = this.Url.Action("Get", "People"); // Returns "/People"
...
}
}
How is it possible to tell MVC to generate lower case routes instead, so in the above example, return "/people"?
It's simple to achieve this, in the ConfigureServices method of our Startup class we just have to configure routing by setting the LowerCaseUrls property to true.
public class Startup
{
...
public void ConfigureServices(IServiceCollection services)
{
services.AddRouting(routeOptions => routeOptions.LowercaseUrls = true);
...
}
}
I am trying to add a Get() function in a MVC 6 (Asp .Net 5) Web Api to pass a configuration option as a query string. Here are the two functions that I already have:
[HttpGet]
public IEnumerable<Project> GetAll()
{
//This is called by http://localhost:53700/api/Project
}
[HttpGet("{id}")]
public Project Get(int id)
{
//This is called by http://localhost:53700/api/Project/4
}
[HttpGet()]
public dynamic Get([FromQuery] string withUser)
{
//This doesn't work with http://localhost:53700/api/Project?withUser=true
//Call routes to first function 'public IEnumerable<Project> GetAll()
}
I've tried several different ways to configure the routing, but MVC 6 is light on documentation. What I really need is a way to pass some configuration options to the list of Projects for sorting, custom filtering etc.
You can't have two [HttpGet]s with the same template in a single controller. I'm using asp.net5-beta7 and in my case it even throws the following exception:
Microsoft.AspNet.Mvc.AmbiguousActionException
Multiple actions matched. The following actions matched route data and had all constraints satisfied:
The reason for this is that [From*] attributes are meant for binding, not routing.
The following code should work for you:
[HttpGet]
public dynamic Get([FromQuery] string withUser)
{
if (string.IsNullOrEmpty(withUser))
{
return new string[] { "project1", "project2" };
}
else
{
return "hello " + withUser;
}
}
Also consider using Microsoft.AspNet.Routing.IRouteBuilder.MapRoute() instead of attribute routing. It may give you more freedom defining the routes.
Accessing the query string is very doable by using either the RESTful routing conventions (enforced by ASP.NET 5 / MVC 6 by default) or by defining custom routes, as explained in this answer.
Here's quick example using custom, attribute-based routes:
[HttpGet("GetLatestItems/{num}")]
public IEnumerable<string> GetLatestItems(int num)
{
return new string[] { "test", "test2" };
}
For more info about custom routing, read the following article on my blog.
In our application users can create objects and give them a name. This data is stored in the database with a typeId. I have an api controller that can currently be called, with a name like
api/data
This controller has the basic Get, Put, Post and Delete methods.
Now i would like users to be able to call this controller by object type. So if they setup 3 different objects and call them company, contact and project, i would like them to be able to call the api/data controller, using these names, similar to this
api/company
api/contact
api/project
I do not know these object types unitl runtime, so i cannot code these manually.
Now i also have other controllers that i do not want effected by this, so if i have a normal controller called page, then i still want to be able to call it by
api/page
Is there a way i can do this? Almost intercept a call to a controller, see if the name is equal to a name i have in the database, and if so pass it to the data controller, else let it process as normal.
As far as you are using api controller, you may play with RoutePrefix and Route annotations. For example:
[RoutePrefix("api")]
public class DataController : ApiController
{
[Route("company")]
public IEnumerable<CompanyViewModel> GetCompanies()
{
....
}
}
But this may conflict with default route definition. If so, try to specifically define your routes for this controller:
config.Routes.MapHttpRoute(
name: "CompanyApi",
routeTemplate: "api/company/{id}",
defaults: new {controller = "DataController" , id = RouteParameter.Optional}
);
You would need to add a route for each object in your db, or rather each object not from the db, and have all others route to a default path. You may want to look at using regex with your routing, if you are able to prefix the object names at all.
I think you should
HomeController:
ActionResult Index(string name)
{
// check if name exists in DB
}
I didn't see other way to do what you want even by editing routes... or maybe if you do routes with DB but well...
Web API routing uses fall-through logic, so start off by declaring your known routes first, then have a generic handler to catch all other requests and handle them accordingly. For example, use the following code when setting up your HttpConfiguration:
// Define all known routes using attributes.
config.MapHttpAttributeRoutes();
// Generic route to handle all other requests
config.Routes.MapHttpRoute(
name: "DynamicRoute",
routeTemplate: "api/{dataType}",
defaults: new { controller = "data" }
);
Then in your DataController you can have actions that take the dataType as a parameter:
public async Task<IHttpActionResult> Get(string dataType, ...)
{ ... }
I have managed to do what i want by using RoutePrefix attribute on controller with an extra MapRoute, with a constraint applied to it.
Here is the code, if someone else is trying to do something similar
First add a routePrefix to the controller that you want to call
[RoutePrefix("api/{myCustomName}")]
public MyController : ApiController {
}
Now in my WebApiConfig file, i needed to add the following lines
// Web API routes
config.MapHttpAttributeRoutes();
// handle calling MyController using custom name
config.Routes.MapHttpRoute(
name: "ModuleDataApi",
routeTemplate: "api/{myCustomName}/{id}",
defaults: new { controller = "MyController" },
constraints: new { moduleType = new MyCustomRouteConstraint() }
);
Now create the constraint that checks if there is a valid match. In my code, i have populated a text file with the details if the file doesnt exist, and i will delete/refresh this when a new custom name is added. I believe this would perform better then a database query everytime, since i would probably have about 20 name/value pairs in here at the very most.
So create your constraint like so
public class MyCustomRouteConstraint : IHttpRouteConstraint
{
private DbContext _context;
private static object fileLockObject = new object();
public MyCustomRouteConstraint ()
{
_context = new DbContext();
}
public bool Match(System.Net.Http.HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection)
{
var appDataPath = System.Web.Hosting.HostingEnvironment.MapPath("~/App_Data/CustomNameRouteCache.txt");
// lock variable so no 2 or more threads can try to create file at once
lock (fileLockObject)
{
var list = new List<MyRouteModel>();
// check if our cache file exists, if not, lets create it
if (!File.Exists(appDataPath))
{
using (var writer = new System.IO.StreamWriter(appDataPath))
{
// get all modules
var names = _context.CustomNames.ToList();
foreach (var item in names)
{
list.Add(Mapper.Map<MyRouteModel>(item));
}
// serialize the list to the file in json format
var json = JsonConvert.SerializeObject(list);
// write to file
writer.Write(json);
};
}
else
{
// open file and get json contents
using (var reader = new System.IO.StreamReader(appDataPath))
{
var json = reader.ReadToEnd();
list = JsonConvert.DeserializeObject<List<MyRouteModel>>(json);
}
}
// lets search our list and see if this value is in our modules
foreach (var item in list)
{
if (item.Name.Equals(values[parameterName].ToString()))
{
return true;
}
}
}
return false;
}
}
Now this seems to work perfectly, i can call MyController using the following paths, or any others that are created by the user at runtime
api/company/
api/contact/
api/project/
api/whateveryouwant/
Hope this help someone else trying to do something similar
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).