Overriding RouteValueDictionary in a Constraint in ASP.NET Core - asp.net-core

I've just started using ASP.NET Core MVC and I want one route (global slug) to go to multiple controllers and actions depending on what type of page I'm serving to the user. For example, I want to use {*slug} for category and product pages.
I'm trying to override the default controller and action in a constraint.
In past versions of MVC, you could change either values["controller"] or values["action"] in a constraint and it would re-route to the appropriate controller and action.
public class Startup
{
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseMvc(routes =>
{
routes.MapRoute(
name: "Category",
template: "{*slug}",
defaults: new { controller = "Page", action = "Home" },
constraints: new { slug = new PageConstraint() }
);
});
}
}
public partial class PageConstraint : IRouteConstraint
{
public virtual bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
{
if (routeDirection == RouteDirection.UrlGeneration)
{
return true;
}
var slug = values["slug"] != null ? values["slug"].ToString() : null;
if (string.IsNullOrWhiteSpace(slug))
{
// Homepage
return true;
}
// Get category service
var categoryService = httpContext.RequestServices.GetRequiredService<ICategoryService>();
var category = categoryService.GetBySlug(slug);
if (category == null)
{
return false;
}
values["controller"] = "Category";
values["action"] = "Listing";
values["category"] = category;
return true;
}
}
In PageConstraint, I look to see if the Category exists, and if it does, it changes the Controller to Category and the Action to Listing in the RouteValueDictionary. However, when I debug the code through, it still goes to the Controller Page and the Action Home.
Anyone know why this is happening, or whether there is a better way of doing this? I know I could have one action in a controller doing all the work that the constraint is, but I would prefer to house the code in separate controllers and actions.

For your requirement, you could try to implement custom IRouter
public class RouterFromAppSettings : IRouter
{
private readonly IRouter _defaulRouter;
private readonly IConfiguration _config;
public RouterFromAppSettings(IRouter defaulRouter
, IConfiguration config)
{
_defaulRouter = defaulRouter;
_config = config;
}
public async Task RouteAsync(RouteContext context)
{
var controller = _config.GetSection("Router").GetValue<string>("Controller");
var action = _config.GetSection("Router").GetValue<string>("Action");
context.RouteData.Values["controller"] = controller;
context.RouteData.Values["action"] = action;
await _defaulRouter.RouteAsync(context);
}
public VirtualPathData GetVirtualPath(VirtualPathContext context)
{
return _defaulRouter.GetVirtualPath(context);
}
}
And then register it in Startup.cs like
app.UseMvc(routes =>
{
routes.Routes.Insert(0, new RouterFromAppSettings(routes.DefaultHandler,Configuration));
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});

Related

.NET Core Audit Logging

I want to implement audit logging in my .NET Core application.
Something like
[HttpPost, Auditing]
public dynamic SomeApiAction()
{
// API code here
...
}
The Attribute should be able to intercept the API call before and after execution in order to log.
Is there any such mechanism available in .net core as a part of the framework? I don't want to use any third-party component.
Please advise.
You can try Audit.WebApi library which is part of Audit.NET framework. It provides a configurable infrastructure to log interactions with your Asp.NET Core Web API.
For example using attributes:
using Audit.WebApi;
public class UsersController : ApiController
{
[HttpPost]
[AuditApi(IncludeHeaders = true)]
public IHttpActionResult Post()
{
//...
}
}
You can use CustomActionFilter for it like
public class CustomDemoActionFilter : Attribute, IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
var controller = context.Controller as Controller;
if (controller == null) return;
var controllerName = context.RouteData.Values["controller"];
var actionName = context.RouteData.Values["action"];
var message = String.Format("{0} controller:{1} action:{2}", "onactionexecuting", controllerName, actionName);
var CurrentUrl = "/" + controllerName + "/" + actionName;
bool IsExists = false;
if(CurrentUrl=="/Home/Index")
{
IsExists=true;
}
else
{
IsExists=false;
}
if (IsExists)
{
//do your conditional coding here.
//context.Result = new RedirectToRouteResult(new RouteValueDictionary { { "controller", "Home" }, { "action", "Index" } });
}
else
{
//else your error page
context.Result = new RedirectToRouteResult(new RouteValueDictionary { { "controller", "Home" }, { "action", "Error" } });
}
//base.OnActionExecuting(context);
}
public void OnActionExecuted(ActionExecutedContext context)
{
}
}
and just use this customactionfilter as attribute over your action method like
[HttpGet]
[CustomHMISActionFilter]
public IActionResult Registration()
{
//your code here
}

HTTP Context is null inside a asp.net core controller

I'm using ASP.Net Core 2.1.1. I have an issue while calling HttpContext in my controller. When i want to use HttpContext the program returns NullReferenceException and says HttpContext.get returns null.
I'm very confused because it's inside a controller. can you help me with potential reasons for that?
CartController .cs
public class CartController : Controller
{
private readonly IProductServices _productServices;
private readonly ICartServices _cartServices;
public CartController(IProductServices productServices, ICartServices cartServices)
{
_productServices = productServices;
_cartServices = cartServices;
cartServices.Cart = GetCart();
}
public RedirectToActionResult AddToCart(int productID, string returnUrl)
{
ProductViewModel product = _productServices.GetByID(productID);
if (product != null)
{
_cartServices.AddItem(product, 1);
SaveCart(_cartServices.Cart);
}
return RedirectToAction("Index", new { returnUrl });
}
public RedirectToActionResult RemoveFromCart(int productID, string returnUrl)
{
ProductViewModel product = _productServices.GetByID(productID);
if (product != null)
{
_cartServices.RemoveLine(product);
SaveCart(_cartServices.Cart);
}
return RedirectToAction("Index", new { returnUrl });
}
public IActionResult Index(string returnUrl)
{
return View(new CartIndexViewModel()
{
Cart = GetCart(),
ReturnUrl = returnUrl
});
}
private CartViewModel GetCart()
{
return HttpContext.Session.GetJson<CartViewModel>("Cart") ?? new CartViewModel();
}
private void SaveCart(CartViewModel cart)
{
HttpContext.Session.SetJson<CartViewModel>("Cart", cart);
}
}
When this line calls: Cart = GetCart(), it returns null.
Startup.cs
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddSession();
services.AddMemoryCache();
services.AddMvc();
services.RegisterStartupServices(Configuration);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseDeveloperExceptionPage();
app.UseStatusCodePages();
app.UseStaticFiles();
app.UseSession();
app.UseMvc(routes =>
{
routes.MapRoute(
name: null,
template: "{category}/Page{page:int}",
defaults: new { controller = "Product", action = "List" }
);
routes.MapRoute(
name: null,
template: "Page{page:int}",
defaults: new { controller = "Product", action = "List", page = 1 }
);
routes.MapRoute(
name: null,
template: "{category}",
defaults: new { controller = "Product", action = "List", page = 1 }
);
routes.MapRoute(
name: null,
template: "",
defaults: new { controller = "Product", action = "List", page = 1 }
);
routes.MapRoute(
name: "default",
template: "{controller=Product}/{action=List}/{id?}"
);
});
}
}
I wrote application dependency injection codes in another assembly and call it from Sturtup.cs
StartupExtensions.cs
public static class StartupExtensions
{
public static void RegisterStartupServices(this IServiceCollection services, IConfiguration configuration)
{
services.AddDbContext<SportStoreDbContext>(x => x.UseSqlServer(configuration.GetConnectionString("SportStoreDatabase")));
MapperConfiguration mappingConfig = new MapperConfiguration(mc =>
{
mc.AddProfile(new MappingProfile());
});
IMapper mapper = mappingConfig.CreateMapper();
services.AddSingleton(mapper);
services.AddTransient<IProductServices, ProductServices>();
services.AddTransient<ICategoryServices, CategoryServices>();
services.AddTransient<ICartServices, CartServices>();
}
}
Thanks
You call your method GetCart inside your constructor :
public CartController(IProductServices productServices, ICartServices cartServices)
{
_productServices = productServices;
_cartServices = cartServices;
cartServices.Cart = GetCart();
}`
...
private CartViewModel GetCart()
{
return HttpContext.Session.GetJson<CartViewModel>("Cart") ?? new CartViewModel();
}
but the HttpContext property is not yet initialized. You can have a Http context only while processing a request.

Can't change destination controller/action via IRouter

I have simple MyRouter:
public class MyRouter : IRouter
{
private readonly IRouteBuilder _routeBuilder;
public MyRouter(IRouteBuilder routeBuilder)
{
_routeBuilder = routeBuilder;
}
public async Task RouteAsync(RouteContext context)
{
if (ShouldReroute(...))
{
SetNeededPath(context, reroute);
}
await GetDefaultRouter().RouteAsync(context);
}
private bool ShouldReroute(...)
{
return true;
}
public VirtualPathData GetVirtualPath(VirtualPathContext context)
{
return GetDefaultRouter().GetVirtualPath(context);
}
private IRouter GetDefaultRouter()
{
return _routeBuilder.DefaultHandler;
}
private void SetNeededPath(RouteContext context, Reroute reroute)
{
context.RouteData.Values.Clear();
context.RouteData.Values["action"] = "StoreContacts";
context.RouteData.Values["controller"] = "Information";
}
}
As you can see it should change the destination of the request to:
[Route("")]
public class InformationController : Controller
{
[Route("StoreContacts")]
public IActionResult StoreContacts()
{
return View();
}
}
The routers description in Startup.cs is:
app.UseMvc(routes =>
{
routes.MapRoute(
name: "areas",
template: "{area:exists}/{controller=Home}/{action=Index}");
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
routes.Routes.Add(new MyRouter(routes));
});
So in my brain, it should redirect all unmapped requests like mysite.com/unexistingRoute should go to InformationController.StoreContacts, but I get only 404.
Also the mysite.com/StoreContacts is available via the direct URL.
Attribute routing will take over conventional routing , so you can remove the attribute routing :
public class InformationController : Controller
{
public IActionResult StoreContacts()
{
return View();
}
}
And move your logic into custom route via IRouter . mysite.com/unexistingRoute won't map to existed route template config in Startup.cs . So remove attribute should work in your scenario . To map other url like mysite.com/OtherAction , you can write custom logic like :
if (context.HttpContext.Request.Path.Value.StartsWith("/StoreContacts"))
{
context.RouteData.Values["controller"] = "Information";
context.RouteData.Values["action"] = "StoreContacts";
}

Asp.Net core get RouteData value from url

I'm wokring on a new Asp.Net core mvc app. I defined a route with a custom constraint, which sets current app culture from the url. I'm trying to manage localization for my app by creating a custom IRequestCultureProvider which looks like this :
public class MyCustomRequestCultureProvider : IRequestCultureProvider
{
public Task<ProviderCultureResult> DetermineProviderCultureResult(HttpContext httpContext)
{
var language= httpContext.GetRouteValue("language");
var result = new ProviderCultureResult(language, language);
return Task.FromResult(result);
}
}
My MyCustomRequestCultureProvider is hit on every request, which is ok. My problem is that in the MVC pipeline, DetermineProviderCultureResult method from my provider is hit before the routing process, so httpContext.GetRouteValue("language") always return null.
In previous version of MVC, I had the possiblity to manually process my url through the routing process by doing this
var wrapper = new HttpContextWrapper(HttpContext.Current);
var routeData = RouteTable.Routes.GetRouteData(wrapper);
var language = routeData.GetValue("language")
I can't find a way to do the same thing in the new framewrok right now. Also, I want to use the route data to find out my langugae, analysing my url string with some string functions to find the language is not an option.
There isn't an easy way to do this, and the ASP.Net team hasn't decided to implement this functionality yet. IRoutingFeature is only available after MVC has completed the request.
I was able to put together a solution that should work for you though. This will setup the routes you're passing into UseMvc() as well as all attribute routing in order to populate IRoutingFeature. After that is complete, you can access that class via httpContext.GetRouteValue("language");.
Startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
// setup routes
app.UseGetRoutesMiddleware(GetRoutes);
// add localization
var requestLocalizationOptions = new RequestLocalizationOptions
{
DefaultRequestCulture = new RequestCulture("en-US")
};
requestLocalizationOptions.RequestCultureProviders.Clear();
requestLocalizationOptions.RequestCultureProviders.Add(
new MyCustomRequestCultureProvider()
);
app.UseRequestLocalization(requestLocalizationOptions);
// add mvc
app.UseMvc(GetRoutes);
}
Moved the routes to a delegate (for re-usability), same file/class:
private readonly Action<IRouteBuilder> GetRoutes =
routes =>
{
routes.MapRoute(
name: "custom",
template: "{language=fr-FR}/{controller=Home}/{action=Index}/{id?}");
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
};
Add new middleware:
public static class GetRoutesMiddlewareExtensions
{
public static IApplicationBuilder UseGetRoutesMiddleware(this IApplicationBuilder app, Action<IRouteBuilder> configureRoutes)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
var routes = new RouteBuilder(app)
{
DefaultHandler = app.ApplicationServices.GetRequiredService<MvcRouteHandler>(),
};
configureRoutes(routes);
routes.Routes.Insert(0, AttributeRouting.CreateAttributeMegaRoute(app.ApplicationServices));
var router = routes.Build();
return app.UseMiddleware<GetRoutesMiddleware>(router);
}
}
public class GetRoutesMiddleware
{
private readonly RequestDelegate next;
private readonly IRouter _router;
public GetRoutesMiddleware(RequestDelegate next, IRouter router)
{
this.next = next;
_router = router;
}
public async Task Invoke(HttpContext httpContext)
{
var context = new RouteContext(httpContext);
context.RouteData.Routers.Add(_router);
await _router.RouteAsync(context);
if (context.Handler != null)
{
httpContext.Features[typeof (IRoutingFeature)] = new RoutingFeature()
{
RouteData = context.RouteData,
};
}
// proceed to next...
await next(httpContext);
}
}
You may have to define this class as well...
public class RoutingFeature : IRoutingFeature
{
public RouteData RouteData { get; set; }
}
Based on Ashley Lee's answer, here is an optimized approach that prevents duplicate route configuration.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
// setup routes
var mvcRouter = BuildMvcRouter(app, routes =>
{
routes.MapRoute(
name: "custom",
template: "{language=fr-FR}/{controller=Home}/{action=Index}/{id?}");
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
// add route data initialization middleware
app.Use(next => SetRouteData(next, mvcRouter));
// add localization middleware
var requestLocalizationOptions = new RequestLocalizationOptions
{
DefaultRequestCulture = new RequestCulture("en-US")
};
requestLocalizationOptions.RequestCultureProviders.Clear();
requestLocalizationOptions.RequestCultureProviders.Add(
new MyCustomRequestCultureProvider()
);
app.UseRequestLocalization(requestLocalizationOptions);
// add mvc routing middleware
app.UseRouter(mvcRouter);
}
This depends on the following two methods that must be added to the StartUp class:
private static IRouter BuildMvcRouter(IApplicationBuilder app, Action<IRouteBuilder> configureRoutes)
{
if (app == null) throw new ArgumentNullException(nameof(app));
if (configureRoutes == null) throw new ArgumentNullException(nameof(configureRoutes));
app.ApplicationServices.GetRequiredService<MiddlewareFilterBuilder>().ApplicationBuilder = app.New();
var routeBuilder = new RouteBuilder(app)
{
DefaultHandler = app.ApplicationServices.GetRequiredService<MvcRouteHandler>()
};
configureRoutes(routeBuilder);
routeBuilder.Routes.Insert(0, AttributeRouting.CreateAttributeMegaRoute(app.ApplicationServices));
return routeBuilder.Build();
}
private static RequestDelegate SetRouteData(RequestDelegate next, IRouter router)
{
return async context =>
{
var routeContext = new RouteContext(context);
await router.RouteAsync(routeContext);
if (routeContext.Handler != null)
{
context.Features[typeof(IRoutingFeature)] = new RoutingFeature
{
RouteData = routeContext.RouteData
};
}
await next(context);
};
}
This has been made easier with the addition of the Endpoint Routing feature.
This article explains how to do it using the Endpoint Routing feature https://aregcode.com/blog/2019/dotnetcore-understanding-aspnet-endpoint-routing/
var endpointFeature = context.Features[typeof(Microsoft.AspNetCore.Http.Features.IEndpointFeature)]
as Microsoft.AspNetCore.Http.Features.IEndpointFeature;
Microsoft.AspNetCore.Http.Endpoint endpoint = endpointFeature?.Endpoint;
//Note: endpoint will be null, if there was no
//route match found for the request by the endpoint route resolver middleware
if (endpoint != null)
{
var routePattern = (endpoint as Microsoft.AspNetCore.Routing.RouteEndpoint)?.RoutePattern
?.RawText;
Console.WriteLine("Name: " + endpoint.DisplayName);
Console.WriteLine($"Route Pattern: {routePattern}");
Console.WriteLine("Metadata Types: " + string.Join(", ", endpoint.Metadata));
}

Custom Router for Maintenance

I would like my ASP.NET Core MVC site to route all requests to a particular controller/action method whenever a particular file exists. The idea is that I can display a "site down for maintenance" page on an as-needed basis, just by creating a particular empty file.
After playing around with writing some custom middleware, it dawned on me that I should be able to create a custom IRouter which does this. Every route request should map to the "maintenance" route when that particular file exists. But I can't get it to work.
Here's the definition of the custom router:
public class OfflineRouteHandler : IRouter
{
private readonly string _path;
private readonly IPAddress _remote;
public OfflineRouteHandler( string offlinePath, string remoteIP )
{
_path = offlinePath;
_remote = IPAddress.Parse( remoteIP );
}
public VirtualPathData GetVirtualPath( VirtualPathContext context )
{
// this returns a value...but RouteAsync is never called
StringBuilder path = new StringBuilder();
if( context.AmbientValues.ContainsKey( "controller" ) )
path.Append( context.AmbientValues["controller"] );
if( context.AmbientValues.ContainsKey("action") )
{
if( path.Length > 0 ) path.Append( "/" );
path.Append( context.AmbientValues["action"] );
}
return new VirtualPathData( this, "/" + path.ToString() );
}
public async Task RouteAsync( RouteContext context )
{
bool authorized = ( context.HttpContext.Connection.RemoteIpAddress == _remote );
if( File.Exists(_path) && authorized )
{
// just for testing... but the method never gets called
var i = 9;
i++;
}
context.IsHandled = true;
}
}
I invoke it in Configure() in Startup.cs as follows:
string offlinePath = Path.Combine( Directory.GetParent( env.WebRootPath ).FullName, "offline.txt" );
app.UseMvc(routes =>
{
routes.Routes.Add( new TemplateRoute( new OfflineRouteHandler( offlinePath, "50.3.3.2" ), "home/offline", new DefaultInlineConstraintResolver(routeOptions)) );
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
routeOptions gets passed into the call to Configure();
Any help would be greatly appreciated!
This is untested, but I went for the middleware approach.
It uses a similar approach to the UseStatusCodePagesWithReExecute function, in that it modifies the context.Request.Path with the path. I've loaded the settings for the OfflineMiddleware using the Options pattern.
Essentially, when the file exists, the middleware modifies the path to the path you want "home/offline", and continues execution up the mvc pipeline.
As we've added UseStaticFiles() before the middleware, any static files on your offline page will be served - only requests that make it through to the MVC pipeline will be intercepted, and all of these requests will hit home/offline.
public class Startup
{
//Partial Starup class
public void ConfigureServices(IServiceCollection services)
{
services.Configure<OfflineMiddlewareSettings>(settings =>
{
settings.OfflinePath = "thePath";
settings.RemoteIP = "50.3.3.2";
});
// Add framework services.
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IOptionsMonitor<MyValues> monitor)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app. UseStaticFiles();
app.UseMiddleware<OfflineMiddleware>();
app.UseMvc();
}
}
public class OfflineMiddleware
{
private readonly RequestDelegate _next;
private readonly string _path;
private readonly IPAddress _remote;
public OfflineMiddleware(RequestDelegate next, IOptions<OfflineMiddlewareSettings> settings)
{
_next = next;
_path = settings.Value.OfflinePath;
_remote = IPAddress.Parse(settings.Value.OfflinePath);
}
public async Task Invoke(HttpContext context)
{
bool authorized = (context.Connection.RemoteIpAddress == _remote);
if (File.Exists(_path) && authorized)
{
context.Request.Path = "home/offline";
}
await _next.Invoke(context);
}
}
public class OfflineMiddlewareSettings
{
public string OfflinePath { get; set; }
public string RemoteIP { get; set; }
}