I have ASP.NET Web Api 2 application which already has controllers. Now, we have new controllers that need to be added but with prefix (v10)
/api/products/1 // Old controller
/api/v1/proucts/1 // the new controller
I tried to version the API with ApiVersion attribute:
[ControllerName("Products")]
[ApiVersion("1.0")]
[RoutePrefix("api/v10/[controller]")]
public class ProductsV1Controller : ApiController
{
...
}
And the old controller is:
public class ProductsController : ApiController
{
...
}
The routing without the version is still working and accessing the old controller, but when I call this routing:
api/v10/products/1
It returns 404 Page not found. The same get method is written in both controllers just for testing purposes.
In my Startup config:
httpConfig.MapHttpAttributeRoutes();
httpConfig.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional },
constraints: null);
does anyone know how to configure the routing to navigate correctly?
#Sajid was mostly correct. When you version by URL segment, you need to use the API version route constraint. There's a couple of issues at play. It looks like you are migrating from an unversioned API to a versioned API, which is a supported scenario. The example values you gave are incongruent. It looks like you are going from v1 to v10. The values are irrelevant, but realize that your existing API does have some logical name, even if you never previously assigned it a value.
Issue 1
You didn't specify your setup, but it should be something like:
var constraintResolver = new DefaultInlineConstraintResolver()
{
ConstraintMap = { ["apiVersion"] = typeof( ApiVersionRouteConstraint ) }
};
configuration.MapHttpAttributeRoutes( constraintResolver );
configuration.AddApiVersioning(
options =>
{
// required because the original API doesn't have a version in the URL
options.AssumeDefaultVersionWhenUnspecified = true;
// this is already the default value, but you might need to change it.
// this is the value that will be 'assumed' by the original, unversioned
// API. it is addressable by /api/products. if you meant to start at 1.0,
// then you can use a lower value such as 0.9
options.DefaultApiVersion = new ApiVersion(1, 0);
});
Issue 2
The token [controller] is not supported in Web API. This is a concept, along with [action], were introduced in ASP.NET Core routing.
You didn't fully elaborate on how you solved the issue, but your changes almost certainly highlight how mixing styles can be confusing. It's unclear if the API was matched by an attribute route or a convention-based route.
Issue 3
Your route template does not include the API Version route constraint (see Issue 1 for registration). This is required so API Versioning knows how to extract the API Version from the URL. API Versioning does not do magic string parsing with regular expressions, etc.
The template should be: [RoutePrefix("api/v{version:apiVersion}/products")] as mentioned by #Sajid. Keep in might that the literal v is not part of an API Version, but is common in the URL segment versioning method. version is the name of the route parameter, but can be any value you want. apiVersion is the key of the registered route constraint, but you can change it in the template if you also change it in the registration (see Issue 1).
Issue 4
The annotated API version is 1.0, but the route template seems to imply that you meant 10.0. You can use any value you want, but they need to be congruent. If you do mean to use 1.0 don't forget to the change the options.DefaultApiVersion or you'll get a runtime exception because the original API and your new API will be considered duplicates as they have the same API version. There is no concept of no API Version once you opt in.
For example:
// ~/api/v10/products
[ApiVersion("10.0")]
[RoutePrefix("api/v{version:apiVersion}/products")]
public class ProductsV1Controller : ApiController { }
Issue 5
I strongly advise against mixing routing styles. It is supported, but it can be confusing to troubleshoot and for maintainers. I would choose either Direct Routing (aka Attribute Routing) or Convention-Based Routing. There are edge cases where mixing them will not work, but they are rare.
This means you should choose one of the following:
Attribute Routing
// setup (see Issue 1)
configuration.MapHttpAttributeRoutes( constraintResolver );
// old controller
[RoutePrefix("api/products")]
public class ProductsController : ApiController { }
// new controller; ControllerNameAttribute isn't need because it's not used
[ApiVersion("10.0")]
[RoutePrefix("api/v{version:apiVersion}/products")]
public class ProductsV1Controller : ApiController { }
Convention-Based Routing
// setup
httpConfig.Routes.MapHttpRoute(
name: "VersionedApi",
routeTemplate: "api/v{apiVersion}/{controller}/{id}",
defaults: new { id = RouteParameter.Optional },
constraints: new { apiVersion = new ApiVersionRouteConstraint() });
httpConfig.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional },
constraints: null);
// old controller
public class ProductsController : ApiController { }
// new controller
[ApiVersion("10.0")]
[ControllerName("Products")] // required because the convention-based name is "ProductsV1"
public class ProductsV1Controller : ApiController { }
Warning: You can mix these styles, but troubleshooting will become more difficult. You need extensive test coverage to ensure things are correct. One-off manual testing may yield false positives.
I hope that helps
Related
In our RouteConfig.cs file we have the following default route:
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Original", action = "Index", id = UrlParameter.Optional },
namespaces: new[] { "Path.To.Controllers" }
);
Our application is split into several different "Areas". This particular route works perfectly fine.
We were asked to change one of our URLs, however the underlying codebase is the same. In an effort to avoid breaking existing links out there I'd like to setup my controller to support two different routes:
Here's an example of what the original URL looks like:
website.com/MyArea/Original
With the aforementioned "Default" route in place, this will be directed to the OriginalController in the MyArea Area, and will hit the default Index action since none was specified.
My goal is to setup another URL that will also direct itself to the OriginalController. That is,
website.com/MyArea/Other
Should route to the OriginalController in the MyArea Area, and hit the default Index action.
I've added the following to the RouteConfig.cs file:
routes.MapRoute(
name: "Other",
url: "Other/{action}/{id}",
defaults: new { controller = "Original", action = "Index", id = UrlParameter.Optional },
namespaces: new[] { "Path.To.Controllers" }
);
What I'm finding is that the Default route config is always used in favor of the Other route config, which causes a binding issue stating "OtherController could not be found". I suspect this is because they have a very similar signature in the url template, but I'm not entirely sure how to get around that.
I'm aware that there's a Route attribute also, which I'm not opposed to using. I unfortunately was unsuccessful in getting that setup correctly though.
After researching and attempting several different combinations I still can't get both URLs to route to one controller.
What am I doing wrong?
I was able to get the expected result using RouteAttribute on the controller itself (thank you #Michael for the resources and making me take another look at the RouteAttribute), rather than conventional MapConfig routing. As I described in my question above, I was having difficulties when attempting the Route approach in that I was receiving 404 errors stating the "resource could not be found".
It turns out the above errors were due to the fact that my attribute routing wasn't being wired up in the correct order, which forced the conventional route to be used (e.g. Default MapConfig) over my Route attributes.
I stumbled upon this SO post which states:
You are probably combining convention based routing with attribute
routing, and you should register your areas after you map the
attribute routes.
The line
AreaRegistration.RegisterAllAreas(); should be called AFTER this line:
routes.MapMvcAttributeRoutes();
When working with Areas, you must register those areas after you register the attribute routing. I was originally registering my areas in the Application_Start method of Globas.asax.cs, which is called before the RouteConfig.RegisterRoutes. I moved this registration to right below my MapMvcAttributeRoutes call in the RouteConfig.cs file, which allowed the following route attribute on the controller to work as expected:
[RouteArea("MyArea")]
[Route("Original/{action=index}", Order = 1)]
[Route("Other/{action=index}", Order = 0)]
public class OriginalController : Controller {
...
...
public async Task<ActionResult> Index() { ... }
}
With the above in place, I can now navigate to either of the below URLs which will properly route to the "Index" action of my OriginalController:
website.com/MyArea/Original
website.com/MyArea/Other
This works. However, I do have another action defined that choked up the attribute routing and caused the conventional Default route (defined via the MapConfig function) to be used. My action signature:
public async Task<ActionResult> Details(int id) {
...
}
The route to this action is: website.com/MyArea/Original/Details/123, which also satisfies the default conventional route of {area}/{controller}/{action}/{id}.
The way around this was to go a step further with defining route attributes at the action level:
[Route("Original/Details/{id:int}")]
[Route("Other/Details/{id:int}")]
public async Task<ActionResult> Details(int id) {
...
}
Now my Route Attributes are found first, then the conventional route is used.
I am trying to configure Audit.net and define my custom logic for saving logs.
Is there a way to configure included entities within context?
I tried this
`
public ResidentMasterContext(DbContextOptions options) : base(options)
{
AuditDataProvider = new DynamicDataProvider();
Mode = AuditOptionMode.OptIn;
IncludeEntityObjects = true;
EntitySettings = new Dictionary<Type, EfEntitySettings>
{
{typeof(Apartment), new EfEntitySettings()}
};
}
`
but OnScopeSaving is not firing. And when I change mode to OptOut it takes all entities
I guess you are referring to the Audit.NET EntityFramework extension.
if you use OptIn you need to mark the included entities with [AuditInclude] attribute, or use the Include methods of the fluent API. You can check the documentation here.
An example using the fluent API for the EF configuration, to include only the entities User and UserDetail:
Audit.EntityFramework.Configuration.Setup()
.ForContext<ResidentMasterContext>(config => config
.IncludeEntityObjects())
.UseOptIn()
.Include<User>()
.Include<UserDetail>();
An example of the output configuration:
Audit.Core.Configuration.Setup()
.UseDynamicProvider(_ => _.OnInsertAndReplace(auditEvent =>
{
Console.WriteLine(auditEvent.ToJson());
}));
It is possible to add a MessageHandler only for a specific controller that is using Route Attributes?
I want to cut the request earlier in the pipeline if it doesn't contain certain headers. I want to
mention that:
I can't add another route in WebApiConfig, we must use the Routing Attributes from the controller.
I don't want to add the MessageHandler globally.
It has to be a MessageHandler (early in the pipeline). We have alternatives for this but we are trying to do this more efficient.
For example, I've decorated the controller with the following RoutePrefix: api/myapicontroller and one action with Route(""). (I know it is strange, we are selecting a different action based on querystring)
Then, I've added
config.Routes.MapHttpRoute(
name: "CustomRoute",
routeTemplate: "api/myapicontroller/{id}",
defaults: new { id = RouteParameter.Optional },
constraints: null,
handler: new myMessageHandler()
);
If I put this code before config.MapHttpAttributeRoutes(); the myMessageHandler is executing but I get this Message:
No action was found on the controller 'myapicontroller' that matches
the request
If I put config.MapHttpAttributeRoutes(); first, the myMessageHandler is never executed but the my action inside myapicontroller is called.
Unfortunately, you can not set any handlers via AttributeRouting. If you want to assign handler to specific routes you have to register it via MapHttpRoute only. Except you need to add your controller name in defaults section like in Ajay Aradhya's answer and remove Route attribute from your action, because you are allowed to register routes either with Route attribute or MapHttpRoute method, but not both at the same time.
Also note that you need to create pipeline, otherwise you handler will work but request will not hit the controller action.
See my answer on similar question for details.
This article from MS docs explains the same. At last they provide a way to have controller specific handlers.But thats for the conventional routing. I don't know if this helps you or not.
config.Routes.MapHttpRoute(
name: "MyCustomHandlerRoute",
routeTemplate: "api/MyController/{id}",
defaults: new { controller = "MyController", id = RouteParameter.Optional },
constraints: null,
handler: HttpClientFactory.CreatePipeline(new HttpControllerDispatcher(config), new MyCustomDelegatingMessageHandlerA());
);
i followed this to learn how ODataController works, everything is OK but when i changed the request uri
from
"localhost:49292/odata/Employees" //result: 200
to
"localhost:49292/odata/employees" //result: 404
to say one word: "odata" or "Odata" and "Employee" are all ok, but lowercase "employee" return 404. any explanation about this. Moreover, the routes in asp.net mvc is not case-sensitive afaik.
how about including a Route attribute and direct it to lower case. for Upper case web api will take care about it
[Route("odata/employees")]
add this on the top of the controller
if odata is common for every action then you can include [RoutePrefix] attribute
You can manually do it using the ODataModelBuilder instead of the ODataConventionModelBuilder
e.g
var builder = new ODataModelBuilder();
builder.EntitySet<Order>("Employees");
builder.EntitySet<Order>("employees");
this will work but your metadata will show 2 entity sets:
{
#odata.context: "http://localhost:62881/$metadata",
value: [
{
name: "Employees",
kind: "EntitySet",
url: "Employees"
},
{
name: "employees",
kind: "EntitySet",
url: "employees"
}
]
}
lowercase "employee" return 404.
I hope you probably didn't have the typo like that.
AFAIK, there is a case limitation on filter and properties. (You can vote there https://aspnetwebstack.codeplex.com/workitem/366 ) but not sure about the controller name..
You can create the REST server using web api without having oData as well..
I'm creating a rest API in ASP.NET MVC4, and am having a problem with routing. For reference, I've already read these questions but they haven't answered my problem:
Web API Routing - api/{controller}/{action}/{id} "dysfunctions" api/{controller}/{id}
Web Api Routing for multiple Get methods in ASP.NET MVC 4
Routing in Asp.net Mvc 4 and Web Api
The urls I'm looking to craft can be as follows:
GET /account/id (where id is a Guid) - equivalent to GET /account/?id=x
GET /account/first%20last%20name (where name is a
string) - equivalent to GET /account/?name=x
GET /pendingregistrations/?page=y (here the action is omitted)
POST /pendingregistrations/denyregistration?denyId=x (here an action is specified)
As you can see, in some cases the URL after the controller name maps to a parameter (id and name in #1,2 above), and sometimes the action name (#4 above). In addition, it may be not present at all (#3 above), in which case I assume a default action. Here is the routing that is working for almost all cases:
// Match for an id next.
config.Routes.MapHttpRoute(
name: "WithIdApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { action = "Index" },
constraints: new
{
id = #"^(\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1})$"
}
);
// Match for a name next.
config.Routes.MapHttpRoute(
name: "WithNameApi",
routeTemplate: "api/{controller}/{name}",
defaults: new { action = "Index" }
);
// Match for an action last.
config.Routes.MapHttpRoute(
name: "WithActionApi",
routeTemplate: "api/{controller}/{action}",
defaults: new { action = "Index" }
);
This code works for everything except example #4 above, because MVC can't tell the difference between a 'name' parameter, and an 'action' binding. If I change the order (i.e. put the match for action above), then the 'name' parameter example will never work.
Does anyone know of any way I can accomplish this?
Well for anyone else trying to do this, the answer is it isn't possible. Best thing to do is to move the 'name' search into it's own action. For example:
GET /account/id (where id is Guid)
GET /account/name/first%20last
I would have assumed MVC would have attempted to match parameters, or actions, if one or the other failed and other bindings were available, but sadly this is not the case.