Reload routes on runtime in dotnetcore - c#

I have a custom route that reads URLs from a no-SQL database (MongoDB) and add them to the route pipeline at moment that the application starts, which is "pretty standard"
something like this (in my startup.cs file):
app.UseMvc(routes =>
{
routes.MapRoute(
"default",
"{controller=Home}/{action=Index}/{id?}");
routes.Routes.Add(
new LandingPageRouter(routes, webrequest, memoryCache, Configuration));
// this custom routes adds URLs from database
}
the issue is that if I add another route to the database after the application has started I basically get a 404 since the routing system isn't aware of this new route, I think that what I need is add the missing routes at runtime or (less convenient) restart the application pool from another web application (which has been developed on framework 4.5 and obviously it runs on a different pool)
Any other thoughts on this?
thanks.

The first question is what does database mean when you say: I add another route to the database and wether you can keep your routes in a JSON, XML or INI file.
If you can, for example, keep the routes in a JSON file, then there is possible for the routes to be dynamically available on runtime (as you can read in the ASP.NET Core Documentation)
You can find a full blog post about this here.
Assuming that routes.json is a JSON file with structure similar to the following, and is in the same folder as Startup:
{
"route": "This is a route",
"another-route": "This is another route",
"default": "This is default!"
}
You can configure the Startup class in the following way:
Note that this example does not use MVC but the separate routing package, although I assume you can transpose it to work with MVC.
public class Startup
{
public IConfiguration Configuration {get;set;}
public Startup(IHostingEnvironment env)
{
var configurationBuilder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("routes.json", optional: false, reloadOnChange: true);
Configuration = configurationBuilder.Build();
}
public void ConfigureServices(IServiceCollection services)
{
services.AddRouting();
}
public void Configure(IApplicationBuilder app)
{
var routeBuilder = new RouteBuilder(app);
routeBuilder.MapGet("{route}", context =>
{
var routeMessage = Configuration.AsEnumerable()
.FirstOrDefault(r => r.Key == context.GetRouteValue("route")
.ToString())
.Value;
var defaultMessage = Configuration.AsEnumerable()
.FirstOrDefault(r => r.Key == "default")
.Value;
var response = (routeMessage != null) ? routeMessage : defaultMessage;
return context.Response.WriteAsync(response);
});
app.UseRouter(routeBuilder.Build());
}
}
At this point, while the application is running, you can modify the JSON file, save it, then because of the reloadOnChange: true parameter, the framework will reinject the new configuration into the Configuration property.
The implementation of the reload is based on a file watcher, so if you want to use a database for this - a SQL Server, then I think you have to implement this yourself.
A (not pretty at all and reminded here just for completeness) solution could be to create a console application that adds database changes in a file, then use that file in the application configuration.
Best regards!

In ASP.NET Core 3 you can use Dynamic Routing. In Startup.cs add:
app.UseEndpoints(endpoints =>
{
endpoints.MapDynamicControllerRoute<SearchValueTransformer>("{**url}");
});
And create new class SearchValueTransformer
class SearchValueTransformer : DynamicRouteValueTransformer
{
public override async ValueTask<RouteValueDictionary> TransformAsync(HttpContext httpContext, RouteValueDictionary values)
{
var url = values["url"] as string;
if (String.IsNullOrEmpty(url))
return values;
values["controller"] = "Controller";
values["action"] = "Action";
values["name"] = url;
return values;
}
}
Also in method TransformAsync you can search in your MongoDB for proper Controller, Action and Name values. More info: https://weblogs.asp.net/ricardoperes/dynamic-routing-in-asp-net-core-3

Related

How to manage Cache control options in a single place instead of having it at action method level using asp.net core 3.1?

I have ASP.NETCORE 3.1 WEBAPI , Azure SQL server as the database for an application.
Instead of repeating the code ([ResponseCache(CacheProfileName = "Never")]) as an attribute across multiple methods as shown below, I want to put it in Starup.cs ConfigureServices method:
TestController.cs
[HttpGet("{param1:long}", Name = "GetData")]
[ResponseCache(CacheProfileName = "Never")]
[ProducesResponseType(typeof(TestModel), 200)]
public IActionResult GetData(long param1)
=> Ok(_testService.GetData(param1));
Startup.cs
public static IServiceCollection AddFrameworkServices(this IServiceCollection services) => services.AddMemoryCache().AddRouting().Configure<ApiBehaviorOptions>(options => options.SuppressModelStateInvalidFilter = true).AddMvc(op =>
{
op.CacheProfiles.Add("Never", new CacheProfile()
{Location = ResponseCacheLocation.None, NoStore = true});
op.Filters.Add(new ProducesResponseTypeAttribute(StatusCodes.Status400BadRequest));
op.Filters.Add(new ProducesResponseTypeAttribute(StatusCodes.Status204NoContent));
op.Filters.Add(new ProducesResponseTypeAttribute(typeof(ErrorMsg), StatusCodes.Status500InternalServerError));
}).SetCompatibilityVersion(CompatibilityVersion.Version_3_0).Services;
I removed [ResponseCache(CacheProfileName = "Never")] from the action method and validated and found cache headers are not displayed in the response even though it is mentioned in the Startup.cs.
Can anyone help me here by providing their guidance?
The below code resolved the issue:
services
.AddMvc(o =>
{
op.Filters.Add(new ResponseCacheAttribute { NoStore = true, Location = ResponseCacheLocation.None });
});
I validated the change and found working as expected

How to authorize access to static files based on claims

Static files can require the user to be authenticated as per documentation
I have not been able to find any info on restricting authorized access to static files, according to specific claims.
E.g. users with claims "A" and "B" have access to folder A and B, where as users with only claim "B" only have access to folder B
How would I accomplish this "as easy as possible" with .NET 6.0 / webAPI / static files?
Currently there is no built-in way to secure wwwroot directories, I think you can expose an endpoint, and then make judgments in the endpoint, This is a very simple method as you expected, in your question, you want to access static file A only user with claims A,I write a similar demo here, hope it can help you to solve your problem.
First I have a static file named "AAA" in wwwroot.
I use Asp.Net Core Identity here, Now I am logged in as a user, Then I add claim to this user.
//the claim's type and value is the same with static file name
Claim claim = new Claim("AAA", "AAA");
await _userManager.AddClaimAsync(user,claim);
Then I expose an endpoint to get the static path then do judgments :
//Add [Authorize] attribute, the controller can only be accessed when the user is logged in
[Authorize]
public class TestController : Controller
{
//Pass in the name of the static file that needs to be accessed, and then use claim to authorize
public IActionResult Find(string path)
{
var value = IHttpContextAccessor.HttpContext.User.Claims.Where(e => e.Type == path ).Select(e => e.Value).FirstOrDefault();
if(value !=null && value == path) {
//authorize success
//read the static file and do what you want
}else{
//authorize fail
}
}
}
View
//use asp-route-path="AAA" to pass the value of path
<a asp-controller="Test" asp-action="Find" asp-route-path="AAA">AAA</a>
<a asp-controller="Test" asp-action="Find" asp-route-path="BBB">BBB</a>
//.......
From the linked example;
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
You could build any policy you want, by calling any of the .Require... methods. eg;
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireClaim("name", "value")
.Build();
});
Note that the fallback policy applies to all endpoints that don't have any [Authorize] metadata.
Instead, you will probably need to write some middleware to check your authorization rule for each path. Perhaps based on this sample.
The linked example demonstrates an interesting concept. Authorisation is based on endpoints, but the static file middleware just takes over the response without using endpoint routing. So what if we generated our own endpoint metadata based on the file provider;
.Use((context, next) => { SetFileEndpoint(context, files, null); return next(context); });
That's doable, but what if we just defined a fake endpoint?
app.UseAuthentication();
app.UseAuthorization();
app.UseStaticFiles();
app.UseEndpoints(endpoints => {
endpoints.MapGet("static/pathA/**",
async (context) => context.Response.StatusCode = 404)
.RequireAuthorization("PolicyA");
});
Of course you could map that dummy path to a controller.

How to remove .html extension in .NET Web API [duplicate]

ASP.NET Core hapily serves up files from the wwwroot folder based on the mime type of the file. But how do I get it serve up a file with no extension?
As an example, Apple require that you have an endpoint in your app /apple-app-site-association for some app-intergration. If you add a text file called apple-app-site-association into your wwwroot it won't work.
Some things I've tried:
1) Provide a mapping for when there's no extension:
var provider = new FileExtensionContentTypeProvider();
provider.Mappings[""] = "text/plain";
app.UseStaticFiles(new StaticFileOptions
{
ContentTypeProvider = provider
});
2) Adding an app rewrite:
var options = new RewriteOptions()
.AddRewrite("^apple-app-site-association","/apple-app-site-association.txt", false)
Neither work, the only thing that does work is a .AddRedirect which I'd rather not use if possible.
Adding an alternative solution. You have to set ServeUnknownFileTypes to true and after that set the default content type.
app.UseStaticFiles(new StaticFileOptions
{
ServeUnknownFileTypes = true,
DefaultContentType = "text/plain"
});
Rather than fighting with static files, I think you'd be better off just creating a controller for it:
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using System.IO;
namespace MyApp.Controllers {
[Route("apple-app-site-association")]
public class AppleController : Controller {
private IHostingEnvironment _hostingEnvironment;
public AppleController(IHostingEnvironment environment) {
_hostingEnvironment = environment;
}
[HttpGet]
public async Task<IActionResult> Index() {
return Content(
await File.ReadAllTextAsync(Path.Combine(_hostingEnvironment.WebRootPath, "apple-app-site-association")),
"text/plain"
);
}
}
}
This assumes your apple-app-site-association file is in your wwwroot folder.
An easier option may be to put a file with a proper extension on the server, and then use URL rewrite as follows.
app.UseRewriter(new RewriteOptions()
.AddRewrite("(.*)/apple-app-site-association", "$1/apple-app-site-association.json", true));
I think the easiest way is to add the apple-app-site-association file in a .well-known folder in the root folder of the application as described here: [https://developer.apple.com/documentation/safariservices/supporting_associated_domains] and then allow access to it from your code, like this (Startup.cs):
// to allow access to apple-app-site-association file
app.UseStaticFiles(new StaticFileOptions()
{
FileProvider = new PhysicalFileProvider(Path.Combine(env.ContentRootPath, #".well-known")),
RequestPath = new PathString("/.well-known"),
DefaultContentType = "application/json",
ServeUnknownFileTypes = true,
});
Tested in AWS Serverless Application (.NET Core 3.1)

Empty href after upgrading to asp.net core 2.2

We have built an ASP.NET Core 2.1 website where URLs like www.example.org/uk and www.example.org/de determine what resx file and content to show. After upgrading to ASP.NET Core 2.2, pages load but all links generated produce blank/empty href's.
For example, a link this:
<a asp-controller="Home" asp-action="Contact">#Res.ContactUs</a>
will in 2.2 produce an empty href like so:
Contact us
But in 2.1 we get correct href:
Contact us
We are using a Constraint Map to manage the URL-based language feature - here is the code:
Startup.cs
// configure route options {lang}, e.g. /uk, /de, /es etc
services.Configure<RouteOptions>(options =>
{
options.LowercaseUrls = true;
options.AppendTrailingSlash = false;
options.ConstraintMap.Add("lang", typeof(LanguageRouteConstraint));
});
...
app.UseMvc(routes =>
{
routes.MapRoute(
name: "LocalizedDefault",
template: "{lang:lang}/{controller=Home}/{action=Index}/{id?}");
}
LanguageRouteConstraint.cs
public class LanguageRouteConstraint : IRouteConstraint
{
private readonly AppLanguages _languageSettings;
public LanguageRouteConstraint(IHostingEnvironment hostingEnvironment)
{
var builder = new ConfigurationBuilder()
.SetBasePath(hostingEnvironment.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
IConfigurationRoot configuration = builder.Build();
_languageSettings = new AppLanguages();
configuration.GetSection("AppLanguages").Bind(_languageSettings);
}
public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
{
if (!values.ContainsKey("lang"))
{
return false;
}
var lang = values["lang"].ToString();
foreach (Language lang_in_app in _languageSettings.Dict.Values)
{
if (lang == lang_in_app.Icc)
{
return true;
}
}
return false;
}
}
I narrowed down the problem but can't find a way to solve it;
Basically in 2.2. some parameters are not set in the above IRouteConstraint Match method, e.g.
httpContext = null
route = {Microsoft.AspNetCore.Routing.NullRouter)
In 2.1
httpContext = {Microsoft.AspNetCore.Http.DefaultHttpContext}
route = {{lang:lang}/{controller=Home}/{action=Index}/{id?}}
The only difference I made between 2.1 and 2.2 is that I changed
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
to the following (due to https://github.com/aspnet/AspNetCore/issues/4206)
var builder = new ConfigurationBuilder()
.SetBasePath(hostingEnvironment.ContentRootPath) // using IHostingEnvironment
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
Any ideas?
Update
According to https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-2.2#parameter-transformer-reference ASP.NET Core 2.2 uses EndpointRouting whereas 2.1 uses IRouter basic logic. That explains my problem. Now, my question would then what will code look like for 2.2 to use the new EndpointRouting?
// Use the routing logic of ASP.NET Core 2.1 or earlier:
services.AddMvc(options => options.EnableEndpointRouting = false)
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
Differences from earlier versions of routing explains what is happening here (emphasis mine):
The link generation ambient value invalidation algorithm behaves differently when used with endpoint routing.
Ambient value invalidation is the algorithm that decides which route values from the currently executing request (the ambient values) can be used in link generation operations. Conventional routing always invalidated extra route values when linking to a different action. Attribute routing didn't have this behavior prior to the release of ASP.NET Core 2.2. In earlier versions of ASP.NET Core, links to another action that use the same route parameter names resulted in link generation errors. In ASP.NET Core 2.2 or later, both forms of routing invalidate values when linking to another action.
...
Ambient values aren't reused when the linked destination is a different action or page.
In your example, lang is an ambient value and so it is not being reused when going from Home/Index to Home/About (different action). Without a value specified for lang, there is no matching action and so an empty href is generated. This is also described in the docs as an endpoint-routing difference:
However, endpoint routing produces an empty string if the action doesn't exist. Conceptually, endpoint routing doesn't assume that the endpoint exists if the action doesn't exist.
If you want to continue to use endpoint routing, it looks like you're going to need to pass the lang value from your controller into your view and then set it explicitly. Here's an example:
public class HomeController : Controller
{
public IActionResult Index(string lang)
{
ViewData["lang"] = lang; // Using ViewData just for demonstration purposes.
return View();
}
}
<a asp-controller="Home" asp-action="Contact"
asp-route-lang="#ViewData["lang"]">#Res.ContactUs</a>
You can make this a little less repetitive with e.g. an Action Filter, but the concepts are still the same. I can't see that there's another way to handle this (e.g. being able to mark a specific value as being ambient), but perhaps someone else will be able to chime in on that.
You need to explicitly pass the values from the route data:
#using Microsoft.AspNetCore.Routing;
<a ... asp-route-storeId="#this.Context.GetRouteValue("storeId")">Pay Button</a>

OData v4 on .Net Core 1.1 missing /$metadata

Using .net Core 1.1, with the Microsoft.AspNetCore.OData libraries, I am able to get an OData endpoint working with my simple controller to perform get, $expand, and other queries. However, I can't get it to return the $metadata to be returned. This question ($Metadata with WebAPi OData Attribute Routing Not Working) is for the same problem, however the .Net APIs have changed since this was posted.
Is there a setting, flag, or something else I need to enable?
This (http://localhost:52315/odata) seems to return the meta data,
{
"#odata.context":"http://localhost:52315/odata/$metadata","value":[
{
"name":"Users","kind":"EntitySet","url":"Users"
},{
"name":"HelloComplexWorld","kind":"FunctionImport","url":"HelloComplexWorld"
}
]
}
this (http://localhost:52315/odata/$metadata) gives me the error:
An unhandled exception occurred while processing the request.
NotSupportedException: No action match template '$metadata'
in 'MetadataController'
Microsoft.AspNetCore.OData.Routing.Conventions.DefaultODataRoutingConvention.SelectAction(RouteContext routeContext)
My Startup.cs looks like this:
public void Configure(IApplicationBuilder app) {
app.UseDeveloperExceptionPage();
app.UseOData("odata");
app.UseMvcWithDefaultRoute();
}
public void ConfigureServices(IServiceCollection services) {
services.AddMvc().AddWebApiConventions();
services.AddSingleton<ISampleService, ApplicationDbContext>();
services.AddOData<ISampleService>(builder =>
{
builder.Namespace = "Sample";
builder.EntityType<ApplicationUser>();
builder.EntityType<Product>();
builder.Function("HelloComplexWorld").Returns<Permissions>();
});
}
NOTE: I can work around it by adding this at the start of my ConfigureServices(...) method, though it seems wrong given $metadata support should be part of the core platform.
app.Use(async (context, next) => {
if (0 == string.Compare(context.Request.Path, #"/odata/$metadata", true)) {
context.Request.Path = "/odata";
}
await next.Invoke();
});
I revisited this today and had more success using the Microsoft.AspNetCore.OData.vNext 6.0.2-alpha-rtm package. Referencing this example, the metadata and default OData routing worked as expected. My minimal configuration is:
public void ConfigureServices(IServiceCollection services)
{
services.AddOData();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
var modelBuilder = new ODataConventionModelBuilder();
modelBuilder.EntitySet<Document>("Documents");
app.UseMvc(builder =>
{
builder.MapODataRoute("odata", modelBuilder.GetEdmModel());
});
}
The /odata/$metadata route should return "an XML representation of the service’s data model" (EDMX) (according to the OData v4 Web API documentation). This is not the same as the service root /odata/, which returns the top level description of resources published by the OData service (as shown in your example).
I encountered the same issue using the pre-release Microsoft.AspNetCore.OData 1.0.0-rtm-00015, since an official release is not yet available (see open issue on OData Web API repo).
To illustrate the point, you could manually emit the metadata, as in the crude example below. (You can find the InMemoryMessage class in the OData/odata.net GitHub repo.)
However, I would suggest waiting for an official release of OData ASP.NET Core as, quoting from the above issue, the "branch is still in its early stages and we may take a different approach once we have our architecture finalized". So things may well change... and $metadata should definitely work "out of the box"!
app.Use((context, func) =>
{
if (context.Request.Path.StartsWithSegments(new PathString("/data/v1/$metadata"), StringComparison.OrdinalIgnoreCase))
{
var model = app.ApplicationServices.GetService<IEdmModel>();
MemoryStream stream = new MemoryStream();
InMemoryMessage message = new InMemoryMessage() {Stream = stream};
ODataMessageWriterSettings settings = new ODataMessageWriterSettings();
ODataMessageWriter writer = new ODataMessageWriter((IODataResponseMessage)message, settings, model);
writer.WriteMetadataDocument();
string output = Encoding.UTF8.GetString(stream.ToArray());
return context.Response.WriteAsync(output);
}
return func();
});

Categories

Resources