ASP.NET Core API: Add custom route token resolver - c#

I'm using https://github.com/ardalis/ApiEndpoints (one action per controller) for my project, and I run into issue that [Route("[controller]")] is not really suitable for me since controllers look like this:
I need something more like [Route("[namespace]")], but that's not supported in ASP.NET Core.
Is there a way, to add custom route token resolution in Startup.cs?
My solutions so far:
Hardcode routes
Create custom attribute that would contain route with custom tokens, and source generator that would resolve custom tokens and generate Route attribute.

Big thanks to #Kahbazi for pointing me in the right direction!
Here's what I came up with:
private class CustomRouteToken : IApplicationModelConvention
{
private readonly string _tokenRegex;
private readonly Func<ControllerModel, string?> _valueGenerator;
public CustomRouteToken(string tokenName, Func<ControllerModel, string?> valueGenerator)
{
_tokenRegex = $#"(\[{tokenName}])(?<!\[\1(?=]))";
_valueGenerator = valueGenerator;
}
public void Apply(ApplicationModel application)
{
foreach (var controller in application.Controllers)
{
string? tokenValue = _valueGenerator(controller);
UpdateSelectors(controller.Selectors, tokenValue);
UpdateSelectors(controller.Actions.SelectMany(a => a.Selectors), tokenValue);
}
}
private void UpdateSelectors(IEnumerable<SelectorModel> selectors, string? tokenValue)
{
foreach (var selector in selectors.Where(s => s.AttributeRouteModel != null))
{
selector.AttributeRouteModel.Template = InsertTokenValue(selector.AttributeRouteModel.Template, tokenValue);
selector.AttributeRouteModel.Name = InsertTokenValue(selector.AttributeRouteModel.Name, tokenValue);
}
}
private string? InsertTokenValue(string? template, string? tokenValue)
{
if (template is null)
{
return template;
}
return Regex.Replace(template, _tokenRegex, tokenValue);
}
}
Configure the token in Startup.cs (this can be wrapped in an extension method):
services.AddControllers(options => options.Conventions.Add(
new CustomRouteToken(
"namespace",
c => c.ControllerType.Namespace?.Split('.').Last()
));
After that custom token can be used for routing:
[ApiController]
[Route("api/[namespace]")]
public class Create : ControllerBase {}
[ApiController]
public class Get : ControllerBase
{
[HttpGet("api/[namespace]/{id}", Name = "[namespace]_[controller]")]
public ActionResult Handle(int id) {}
}

You can achieve that with implementing IApplicationModelConvention. more info on here : Custom routing convention
public class NamespaceRoutingConvention : IApplicationModelConvention
{
public void Apply(ApplicationModel application)
{
foreach (var controller in application.Controllers)
{
controller.Selectors[0].AttributeRouteModel = new AttributeRouteModel()
{
Template = controller.ControllerType.Namespace.Replace('.', '/') + "/[controller]}"
};
}
}
}
And then add it in startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options =>
{
options.Conventions.Add(new NamespaceRoutingConvention());
});
}

Areas are exactly what you need. As mentioned in .net core documentation:
Areas are an ASP.NET feature used to organize related functionality
into a group as a separate:
Namespace for routing.
Folder structure for views and Razor Pages.
To use area routes you need only to add them to the startup.
Like this:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "MyArea",
pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
After that the route will follow your folder structure inside the Areas folder.
For more information visit the Areas in ASP.NET Core documentation.

Related

Add static Routes without visible Identifiers in Url in Razor Pages

We want to migrate a Website from Asp.Net Webforms to Asp.Net Core Webapplication (Razor Pages). We now have readable Urls without visible identifiers (for readability, SEO and to prevent changing Urls after possible database migrations...).
For that i generated dynamically Routes in the Global.Asax at Startup:
RouteTable.Routes.MapPageRoute("myroute1",
"banana/",
"~/content.aspx", false, new RouteValueDictionary { { "id", "4711" }, , { "otherid", "4812" }
RouteTable.Routes.MapPageRoute("myroute2",
"apple/",
"~/content.aspx", false, new RouteValueDictionary { { "id", "4913" }, , { "otherid", "5014" }
That way users could call the content.aspx like this:
https://example.com/banana
https://example.com/apple
In the content.aspx i got the mapped "id" and "otherid" from the RouteValues.
How can i achieve this in Razor Pages?
Now i have a "Content.cshtml" and there i need access to id and otherid. I added this at the top:
#page "/content/{id:int}/{otherid:int}/{title:string}"
The above code allows mit to call Urls like this:
https://example.com/content/4711/4812/banana // still have the ids in it and prefix "content"
Is there a possibility to add all Routes at Startup with fixed Parameters? I have not been able to find anything similar.
Greetings
cpt.oneeye
I found a solution thanks to this thread:
Is there a way to do dynamic routing with Razor Pages?
Another full example can be found here:
https://github.com/dotnet/AspNetCore.Docs/issues/12997
According to my example at the top you first make your own DynamicRouteValueTransformer (see Namespace Microsoft.AspNetCore.Mvc.Routing):
public class NavigationTransformer : DynamicRouteValueTransformer
{
private readonly MyDatabaseContext _repository;
public NavigationTransformer(MyDatabaseContext repository)
{
_repository = repository;
}
public override async ValueTask<RouteValueDictionary> TransformAsync(HttpContext httpContext, RouteValueDictionary values)
{
// all requests which are incoming and fit a specific pattern will go through this method
var slug = values["slug"] as string;
// just get your needed info according to the "slug"
// in my case i have to map according to NavigationNodes that are stored in the Database
var myNavigationNode = _repository.NavigationNodes.FirstOrDefault(nn => nn.Name == slug);
if (node == null)
{
// User will get a 404
return null;
}
// set Path to Page according to Type (or whatever fits your logic)
string pagename = "";
if(node.Type == "ContentPage")
{
pagename = "/Content" // Pages/Content.cshtml
}
else if(node.Type == "ContactForm")
{
pagename = "/Contact" // Pages/Contact.cshtml
}
else
{
pagename = "/Someotherpage"
}
// return all RouteValues you need on your Page
return new RouteValueDictionary()
{
{ "page", pagename },
{ "id", node.Id },
{ "otherid", node.OtherId }
};
}
}
In the Startup.ConfigureServices() you register the new Service:
services.AddScoped<NavigationTransformer>();
In the Startup.Configure() you add the NavigationTransformer to Endpoints:
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapDynamicPageRoute<NavigationTransformer>("{slug}"); // just "slug" so that every request will be handled by NavigationTransformer, you can also add a prefix-folder
});
Now when you call a url like the following you will came through the Transformer and you are able to reroute on the fly:
https://example.com/banana
https://example.com/apple
Be aware the Routings of existing Pages are stronger. So if we have a Apple.cshtml the second Url will still be routed to Apple.cshtml and not to Content.cshtml
To have a static url, you can put your routes in startup class. for example:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default1",
pattern: "{controller=Home}/{action=Index}/{id?}"
);
});
In this case, you can define the endpoints as much as you want. For example, for your routes, you can write like this:
endpoints.MapControllerRoute(
name: "default",
pattern: "{content}/{id?}/{id?}/{banana}"
);
Or you can use Attribute Routing that you can use for each of your controllers. read more about that in
sorry for my English🙏

How can I construct custom WebApi routes like "package/{packageName}/{controller}" to route to Application Parts in a separate assembly?

Our app loads controllers in external assemblies we call packages. I want to create a route that routes to a package using URLs like package/BillingPackage/Invoice rather than api/BillingPackage/Invoice. Here is what I have done:
Startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseEndpointRouting()
.UseMvc(routes =>
{
routes.MapRoute(
name: "package",
template: "package/{package}/{controller}/{id?}");
routes.MapRoute("api", "api/{controller}/{action=Get}/{id?}");
routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
});
app.UseStaticFiles();
}
public IServiceProvider ConfigureServices(IServiceCollection services)
{
var source = new PackageAssemblySource(Configuration);
var packageAssemblies = source.Load();
var builder = new ContainerBuilder();
builder.RegisterModule(new WebApiModule(packageAssemblies));
services
.AddMvc()
.ConfigureApplicationPartManager(manager =>
{
// Add controllers and parts from package assemblies.
foreach (var assembly in packageAssemblies)
{
manager.ApplicationParts.Add(new AssemblyPart(assembly));
}
});
.AddControllersAsServices() // Now that AssemblyParts are loaded.
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);;
builder.Populate(services);
ApplicationContainer = builder.Build();
return new AutofacServiceProvider(ApplicationContainer);
}
Then I define a controller like this:
[Route("package/BillingPackage/[controller]", Name = "Invoice")]
public class InvoiceController : ControllerBase
{
[HttpGet()]
public ActionResult<Invoice> Get()
{
return new SampleInvoice();
}
}
Even with all that, package/BillingPackage/Invoice yields a 404 whereas api/BillingPackage/Invoice does not. How do I get my WebApi to serve endpoints from package rather than api?
You are probably experiencing route conflict with template: "package/{package}/{controller}/{id?}".
If using attribute routing on the controller then remove that convention-based route
To get the desired behavior, you would need to include a template parameter [Route("package/{package}/[controller]", Name = "Invoice")] along with a method/action argument public ActionResult<Invoice> Get(string package) which will be populated from the matched value from the URL.
For example
[Route("package/{package}/[controller]", Name = "Invoice")]
public class InvoiceController : ControllerBase {
//GET package/BillingPackage/Invoice
[HttpGet()]
public ActionResult<Invoice> Get(string package) {
return new SampleInvoice();
}
}
Reference Routing to controller actions in ASP.NET Core

Non-attribute routes in ASP.Net Core WebApi

I need to build project, that implement REST API predefined by vendor application(which will consume it) - there is about thousand of REST-resources with some actions defined by different HTTP-Verb's(POST, GET, PUT, DELETE, etc..).
So, ideally, for each resource i should have single class like this:
public class SomethingController
{
public Something Post(string name, DateTime time)
{
// ...
}
public int PostStrange(string text)
{
// ...
}
public Something Put([FromBody]Something item)
{
// ...
}
public void Delete(int id)
{
// ...
}
}
In previous versions i can just call MapHttpRoute while registering routes, inherit classes like this from ApiController - and ASP.NET Web Api will do as i need... But in .NET Core i can't find anything like MapHttpRoute/ApiController.. Now there is routing and http-verb attributes, and i need to define everything explicitly for each class/method:
[Route("api/[controller]")]
public class SomethingController : Controller
{
[HttpPost]
public Something Post(string name, DateTime time)
{
// ...
}
[HttpPost("api/[controller]/strange")]
public int PostStrange(string text)
{
// ...
}
[HttpPut]
public Something Put([FromBody]Something item)
{
// ...
}
[HttpDelete]
public void Delete(int id)
{
// ...
}
}
Writing this attributes for each of thousands REST-resources is very boring and error prone...
Do i miss something here? Why in pretty new and modern ASP.NET Core that very common and important thing as building REST-Api made so over-complicated, compared to old ASP.NET?
There is nuget package Microsoft.AspNetCore.Mvc.WebApiCompatShim which main goal is to make migration from web api to core easier. It also provides a way to perform convention-based routing to actions you need. So, first install that package, then in startup:
public void ConfigureServices(IServiceCollection services) {
// add conventions here
services.AddMvc().AddWebApiConventions();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
app.UseMvc(routes => {
// map one global route
routes.MapWebApiRoute("WebApi", "api/{controller}");
});
}
After this small configuration you can inherit your controllers either from ApiController, which is added in package above for convenience of migration from web api, or native asp.net core Controller. Example of ApiController:
public class SomeController : ApiController {
// maps to GET /api/Some
// note - no routing attributes anywhere
public HttpResponseMessage Get() {
return new HttpResponseMessage(HttpStatusCode.OK);
}
// maps to POST /api/Some
public HttpResponseMessage Post() {
return new HttpResponseMessage(HttpStatusCode.OK);
}
}
Native asp.net core controller:
// mark with these attributes for it to work
[UseWebApiRoutes]
[UseWebApiActionConventions]
public class TestController : Controller {
// maps to GET /api/Test
// no routing attributes, but two "conventions" attributes
public IActionResult Get(string p) {
return new ObjectResult(new { Test = p });
}
}
You can also mark your base controller with these attributes:
[UseWebApiRoutes]
[UseWebApiActionConventions]
public class BaseController : Controller {
}
public class TestController : BaseController {
// maps to GET /api/Test
// no attributes
public IActionResult Get(string p) {
return new ObjectResult(new { Test = p });
}
}
If you are not migrating from web api - I'd suggest to use native Controller. ApiController has different structure (similar to asp.net web api ApiController), so there is not much reason to use it for anything other than its intended goal (migration from web api).
MapRoute is still there https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing
Attribute routing compliments MapRoute, not replaces it.
Apparently there are quite a few examples which drop the piece about Routing in order to simplify example. So just dig dipper.

No non-OData HTTP route registered

I followed this tutorial to create a WebAPI REST service.
After that, I could load the list of all contacts by pointing at http://baseaddress/api/Contacts.
Then I added the following code in the Register method in WebApiConfig.cs in order to enable an OData endpoint:
config.Count().Filter().OrderBy().Expand().Select().MaxTop(null);
ODataModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Contact>("Contacts");
config.MapODataServiceRoute(
routeName: "OData",
routePrefix: "odata",
model: builder.GetEdmModel());
And also added the [EnableQuery] parameter on the Contact.GetContacts() method. That way, I am able to query for particular contacts like this:
http://baseaddress/odata/Contacts?$filter=startswith(Name,'A')
and it works like charm.
Unfortunately, when I put the [EnableQuery], the WebAPI endpoint stops working, showing instead the following error:
No non-OData HTTP route registered.
in System.Web.OData.Extensions.HttpConfigurationExtensions.GetNonODataRootContainer(HttpConfiguration configuration)
in System.Web.OData.Extensions.HttpRequestMessageExtensions.GetRootContainer(HttpRequestMessage request, String routeName)
in System.Web.OData.Extensions.HttpRequestMessageExtensions.CreateRequestScope(HttpRequestMessage request, String routeName)
in System.Web.OData.Extensions.HttpRequestMessageExtensions.CreateRequestContainer(HttpRequestMessage request, String routeName)
...
What should I do to fix this?
I encountered this problem, and since I'm working with dependency injections I managed to solve this by adding GlobalConfiguration.Configuration.EnableDependencyInjection() to my startup.cs
ex.
using System.Web.OData.Extensions;
public class Startup
{
public void Configuration(IAppBuilder app)
{
GlobalConfiguration.Configuration.EnableDependencyInjection();
}
}
Adding config.EnableDependencyInjection() in Startup.cs worked for me.
var config = new HttpConfiguration();
config.EnableDependencyInjection();
The key with this issue is to use .EnableDependencyInjection() on Configure method in Startup.cs
If you are using ASP.net Core endpoint routing (recommended if you
have at least .net core 3.0 and Microsoft.AspNetCore.OData v7.4.0)
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.Select().Filter().OrderBy().Count().MaxTop(10);
endpoints.EnableDependencyInjection();//This guy solves the problem
endpoints.MapODataRoute("odata", "odata", GetEdmModel());
});
Otherwise if you are using MVC routing (only way available prior .net
core 3.0 and Microsoft.AspNetCore.OData v7.4.0)
app.UseMvc(routeBuilder =>
{
routeBuilder.Select().Filter().OrderBy().Count().MaxTop(10);
routeBuilder.EnableDependencyInjection();//This guy solves the problem
routeBuilder.MapODataServiceRoute("odata", "odata", GetEdmModel());
});
Further reading: https://devblogs.microsoft.com/odata/enabling-endpoint-routing-in-odata/
I encountered this problem before and by the adding the line below it worked for me
protected void Application_Start()
{
GlobalConfiguration.Configuration.EnableDependencyInjection();
.... }
I got this error after updating my WebApi project dependencies (NuGet) to:
Microsoft.AspNet.OData, version="7.0.1"
Microsoft.OData.Core, version="7.5.0"
Microsoft.OData.Edm, version="7.5.0"
Microsoft.Spatial, version="7.5.0"
After downgrading to the versions I used before, the error was gone again:
Microsoft.AspNet.OData, version="5.8.0"
Microsoft.OData.Core, version="6.19.0"
Microsoft.OData.Edm, version="6.19.0"
Microsoft.Spatial, version="6.19.0"
that problem I solved this way:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
.... existing code...
//added:
//(OData need this)
config.EnableDependencyInjection();
config.Expand().Select().OrderBy().Filter();
}
}
In my case, I got the above error even though I had a separate project, which had no Odata code at all. So this was a very strange message to get.
My solution was to delete all the packages from the packages folder. After that it worked again. Some Odata packages were still in the this folder.
In my case, I had a web site with ODATA routes (and controllers), and other API routes (and controllers). What happened was my other routes were simply conflicting with ODATA ones, even with different C# namespaces and classes, etc.
Initially my controller was like this for example:
public class UserController : ApiController
{
[HttpPost]
public void Create([FromBody] string email)
{
}
}
I also had a "User" route in ODATA somewhere (different namespace, different urls, etc.). So I had to add explicitely the Route attribute, like this:
public class UserController : ApiController
{
[HttpPost]
[Route("api/user/create")]
public void Create([FromBody] string email)
{
}
}
I also got this error, and in my case it was an issue with case-sensitivity. I had called
https://www.example.com/odata/MyEntities instead of
https://www.example.com/odata/myentities, as it was registered.
Be sure to check your route configuration and calling url.
the controller name has to match the name you put in builder.EntitySet<SomeType>()
public class Startup
{
public void Configuration(IAppBuilder appBuilder)
{
var config = new HttpConfiguration();
config.Count().Filter().OrderBy().Expand().Select();
var builder = new ODataConventionModelBuilder();
builder.EntitySet<Order>("Order");// now the controller must be named OrderController
config.MapODataServiceRoute(routeName: "odata", routePrefix: null, model: builder.GetEdmModel());
appBuilder.UseWebApi(config);
}
}
public class OrderController : ODataController
{
// test data source
public List<Order> OrdersList { get; set; } = new List<Order>()
{
// put some test data
};
[HttpGet]
[EnableQuery]
public IQueryable<Order> Get() //Get: http://localhost:port/Order
{
return Orders.AsQueryable();
}
[HttpGet]
[EnableQuery]
public Order Get(int key) //Get: http://localhost:port/Order(key)
{
return Orders.FirstOrDefault(x => x.ID == key);
}
[HttpPost]
public bool Post(Order entity) //Post: http://localhost:port/Order
{
Orders.Add(entity);
return true;
}
[HttpPut]
public bool Put(int key, Order entity) //Put: http://localhost:port/Order(key)
{
var idx = Orders.FindIndex(x => x.ID == key);
Orders[idx] = entity;
return true;
}
[HttpPatch]
public bool Patch(int key, Order entity) //Patch: http://localhost:port/Order(key)
{
var idx = Orders.FindIndex(x => x.ID == key);
Orders[idx] = entity;
return true;
}
[HttpDelete]
public bool Delete(int key) //Delete: http://localhost:port/Order(key)
{
var idx = Orders.FindIndex(x => x.ID == key);
Orders.RemoveAt(idx);
return true;
}
}
PS: am using Microsoft.AspNet.OData 7.5.5
I just migrate from core 2.1 to .net core 3.1, and have the same issue. Here is how I fix:
app.UseMvc(routeBuilder =>
{
routeBuilder.EnableDependencyInjection(); //..another config
});

ASP.NET Core read config IOptions controller not triggering

I am trying to read the appsettings.json file with the strongly type class and pass that as a parameter to controller. However its not working. Here is the code.
appsettings.json file:
{
"AppSettings": {
"ApplicationName": "TestApp"
}
}
AppSettings Class:
public class AppSettings
{
public string ApplicationName { get; set; }
}
Injecting in Startup Class:
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddApplicationInsightsTelemetry(Configuration);
services.AddMvc();
services.AddOptions();
services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));
}
Controller:
public class ValuesController : Controller
{
private readonly IOptions<AppSettings> _appsettings;
public ValuesController(IOptions<AppSettings> appsettings)
{
_appsettings = appsettings;
}
[HttpGet]
public string Get()
{
return _appsettings.Options.ApplicationName;
}
}
The startup program is successfully getting executed. However, the controller constructor or default get method is not called.
It is working, If I remove the (IOptions appsettings) from the controller constructor.
What's wrong with my code.
IOptions.Options was renamed to IOptions.Value in beta8. See this question.
Changing your Get action from:
return _appsettings.Options.ApplicationName;
to:
return _appsettings.Value.ApplicationName;
should fix that issue.
UPDATE 3/8/2016
The other issue I see here is that you're calling the Get action the "default" action, but the default routing in ASP.Net Core looks for an Index action on the controller.
You can configure the route in Startup.cs to look for a Get action by default instead of an Index action by modifying the Configure function to include something like this:
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Get}/{id?}");
});
The default implmentation uses the template template: "{controller=Home}/{action=Index}/{id?}" which is why it looks for an Index action.
Your other options would be to change your Get function to an Index function, explicitly specify the Get action in the url when you visit the site (ex. http://localhost/Values/Get), or specify the action name for the Get method in your controller like this:
[HttpGet]
[ActionName("Index")]
public string Get()
{
return _appsettings.Value.ApplicationName;
}

Categories

Resources