Multi Tenant Razor Page - asp.net-core

Looking for an easy way build multi tenant razor page. Looking for the url pattern to be like \{Tenant}\{Page} for all pages in an area. Its fairly easy to add a route param at the end via RazorPagesOptions Conventions. How do you add the param at the beginning?

You can use the IPageRouteModelConvention interface to prefix each route with a route parameter representing the tenant. Create a class that implements the interface, and then override the Apply method, something like the following (untested):
public class CustomPageRouteModelConvention : IPageRouteModelConvention
{
public void Apply(PageRouteModel model)
{
foreach (var selector in model.Selectors.ToList())
{
selector.AttributeRouteModel.Template = "{tenant}/" + selector.AttributeRouteModel.Template ;
}
}
}
Then register your implementation in ConfigureServices:
services.AddMvc().AddRazorPagesOptions(options =>
{
options.Conventions.Add(new CustomPageRouteModelConvention());
})

Related

ASP.NET Core 3.1 OData: Cannot Select Entity By Id

I have an OData endpoint (action method) that returns an IQueryable list of items:
[EnableQuery]
public IQueryable<Entity> Get()
The POCO class has the [Key] attribute applied to its id property. It works fine, all entities are returned and I can do filters, etc. But I cannot select an instance by its id, that is:
/odata/entity?$filter=Id eq 1 //works
/odata/entity(1) //does not work
Is there any setup that I need to do? I also tried configuring the key in the model:
var odataBuilder = new ODataConventionModelBuilder();
odataBuilder.EntitySet<Student>("Entity").EntityType.HasKey(x => x.Id);
But it also dit not work. The only way I can achieve this is by adding an action method specifically for this:
public Entity Get(int key) //works
But I was under the impression that this would not be needed, the other action should be enough. Am I wrong?
Based on my research and the examples found, I don't think this is possible, e.g., there is always a need for a dedicated method for this.
TL;DR: use ODataRoutePrefix + ODataRoute because [Route("api/[controller]")] can't handle OData api/todo(10) for single navigable entity.
To get the OData convention for api/todo(10) working:
// Startup - Configure()
app.UseEndpoints(endpoints =>
{
endpoints.Count().Filter().OrderBy().Expand().Select().MaxTop(100);
endpoints.MapODataRoute("odata", "odata", GetEdmModel())
.EnableDependencyInjection();
//[Route("odata/[controller]")] // remove this
[ODataPrefix("todo")] // add this
public class TodoController : ODataController {
// /odata/todo?$filter=id eq 142
[EnableQuery]
public IQueryable<Todo> Get(
[FromServices] MyDataContext dataContext)
{
return dataContext.Todo;
}
[EnableQuery]
[ODataRoute("({id})")] // /odata/todo(142)
public IActionResult GetByKey(
[FromODataUri] long id,
[FromServices] MyDataContext dataContext)
{
return Ok(dataContext.Todo.SingleOrDefault(x => x.Id == id));
}
None of the OData samples for ASP.NET Core use [ODataRoutePrefix] and [ODataRoute] but those are necessary for routing using OData convention api/todo(10).
If you use normal [Route("api/[controller]")] on controller and [Route("({id})"] on GetByKey() then your route is
~/api/todo/(10) - BADBAD
~/api/todo(10) - GOOD
.. the later is the convention used in many samples including https://www.odata.org/getting-started/basic-tutorial/ .
This also seems to break the helper Created(todo) used in POST samples, because it can't find api/todo({newId}) route to return in the Location header.

specify which API is documented by Swagger

I'm new to swagger and have it installed and running but it's picking up far more API files than desired. I have been hunting a way to specify which API is documented.
You can put an ApiExplorerSettings attribute on a controller to remove it from Swagger:
[ApiExplorerSettings(IgnoreApi = true)]
public class TestApiController : ControllerBase
{
}
If you want to apply this on a lot of controllers based on some logic,
it can be done e.g. with an action model convention: https://github.com/juunas11/AspNetCoreHideRoutesFromSwagger/blob/983bad788755b4a81d2cce30f82bc28887b61924/HideRoutesFromSwagger/Controllers/SecondController.cs#L18-L28
public class ActionHidingConvention : IActionModelConvention
{
public void Apply(ActionModel action)
{
// Replace with any logic you want
if (action.Controller.ControllerName == "Second")
{
action.ApiExplorer.IsVisible = false;
}
}
}
The convention is added in ConfigureServices like:
services.AddControllers(o =>
{
o.Conventions.Add(new ActionHidingConvention());
});

A way to set the value of one property after model is bound asp.net core

I am creating an asp.net core 2.2 api. I have several model classes, most of which contains a string property CreatorId:
public class ModelOne
{
public string CreatorId { get; set; }
//other boring properties
}
My controllers' actions accept these models with [FromBody] attribute
[HttpPost]
public IActionResult Create([FromBody] ModelOne model) { }
I don't want my web client to set the CreatorId property. This CreatorId field appears in tens of classes and I don't want to set it in the controlers actions manually like this:
model.CreatorId = User.Claims.First(claim => claim.Type == "id").Value;
I can't pollute the model classes with any custom model-binding related attributes. I don't want to pollute the controllers or actions with any model binding attributes.
Now, the question is: is there a way to add some custom logic after model is bound to check (maybe with the use of reflection) if model class contains CreatorId field and then to update this value. If it is not possible from ControllerBase User property, than maybe by looking at the jwt token. The process should be transparent for the developer, no attributes - maybe some custom model binder registered at the application level or some middleware working transparently.
Ok, I've invented my own - action filter based solution. It seems to work good. I've created a custom action filter:
public class CreatorIdActionFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ActionArguments.ContainsKey("request")) return;
var request = context.ActionArguments["request"];
var creatorIdProperty = request?.GetType().GetProperties().FirstOrDefault(x => x.Name == "CreatorId");
if (creatorIdProperty == null) return;
var user = ((ControllerBase)context.Controller).User;
creatorIdProperty.SetValue(request, user.Claims.First(claim => claim.Type == "id").Value);
}
public void OnActionExecuted(ActionExecutedContext context) { }
}
which was just registered in ConfigureServices
services.AddMvc(options => { options.Filters.Add(typeof(CreatorIdActionFilter)); })
no other boilerplate code is needed. Parameter name is always "request", that's why I take this from ActionArguments, but of course this can be implemented in more universal way if needed.
The solution works. I have only one question to some experts - can we call this solution elegant and efficient? Maybe I used some bad practices? I will be grateful for any comments on this.

Localized page names with ASP.NET Core 2.1

When create a Razor page, e.g. "Events.cshtml", one get its model name set to
#page
#model EventsModel
where the page's name in this case is "Events", and the URL would look like
http://example.com/Events
To be able to use page name's in Norwegian I added the following to the "Startup.cs"
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
.AddRazorPagesOptions(options => {
options.Conventions.AddPageRoute("/Events", "/hvaskjer");
options.Conventions.AddPageRoute("/Companies", "/bedrifter");
options.Conventions.AddPageRoute("/Contact", "/kontakt");
});
With this I can also use an URL like this and still serve the "Events" page
http://example.com/hvaskjer
I'm planning to support many more languages and wonder, is this the recommended way to setup localized page name's/route's?, or is there a more proper, correct way to accomplish the same.
I mean, with the above sample, and having 15 pages in 10 languages it gets/feels messy using options.Conventions.AddPageRoute("/Page", "/side"); 150 times.
You can do this with the IPageRouteModelConvention interface. It provides access to the PageRouteModel where you can effectively add more templates for routes to match against for a particular page.
Here's a very simple proof of concept based on the following service and model:
public interface ILocalizationService
{
List<LocalRoute> LocalRoutes();
}
public class LocalizationService : ILocalizationService
{
public List<LocalRoute> LocalRoutes()
{
var routes = new List<LocalRoute>
{
new LocalRoute{Page = "/Pages/Contact.cshtml", Versions = new List<string>{"kontakt", "contacto", "contatto" } }
};
return routes;
}
}
public class LocalRoute
{
public string Page { get; set; }
public List<string> Versions { get; set; }
}
All it does is provide the list of options for a particular page. The IPageRouteModelConvention implementation looks like this:
public class LocalizedPageRouteModelConvention : IPageRouteModelConvention
{
private ILocalizationService _localizationService;
public LocalizedPageRouteModelConvention(ILocalizationService localizationService)
{
_localizationService = localizationService;
}
public void Apply(PageRouteModel model)
{
var route = _localizationService.LocalRoutes().FirstOrDefault(p => p.Page == model.RelativePath);
if (route != null)
{
foreach (var option in route.Versions)
{
model.Selectors.Add(new SelectorModel()
{
AttributeRouteModel = new AttributeRouteModel
{
Template = option
}
});
}
}
}
}
At Startup, Razor Pages build the routes for the application. The Apply method is executed for every navigable page that the framework finds. If the relative path of the current page matches one in your data, an additional template is added for each option.
You register the new convention in ConfigureServices:
services.AddMvc().AddRazorPagesOptions(options =>
{
options.Conventions.Add(new LocalizedPageRouteModelConvention(new LocalizationService()));
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

OverrideAuthorizationAttribute in ASP.NET 5

I would like to implement the following in MVC6:
[Authorize(Roles = "Shopper")]
public class HomeController
{
[Authorize(Roles = "Editor"), OverrideAuthorization]
public IActionResult EditPage() {}
}
But OverrideAuthorizationAttribute no longer exists. So how do you set it so that a user only needs to be in the Editor role and not Editor and Shopper role to access EditPage in MVC6?
I found this blog post from Filip W that explains how write your own solution using the filter providers.
However the framework has changed a lot and his solution has to be updated to take into account the changes in the framework up to beta8.
First you will create a new attribute where you can specify the type of the filter that you want to override. (In your case this would be the AuthorizeFilter)
public class OverrideFilter : ActionFilterAttribute
{
public Type Type { get; set; }
}
If you want. you could create more specific filters like:
public class OverrideAuthorization : OverrideFilter
{
public OverrideAuthorization()
{
this.Type = typeof(AuthorizeFilter);
}
}
Then you need to create a new IFilterProvider.
This filter provider will be executed after the default providers in
the framework have run.
You can inspect the
FilterProviderContext.Results and search for your OverrideFilter
If found, you can then inspect the rest of the filters, and delete
any filter that is of the filtered type and a lower scope
For example create a new OverrideFriendlyFilterProvider following this idea:
public class OverrideFriendlyFilterProvider : IFilterProvider
{
//all framework providers have negative orders, so ours will come later
public int Order => 1;
public void OnProvidersExecuting(FilterProviderContext context)
{
if (context.ActionContext.ActionDescriptor.FilterDescriptors != null)
{
//Does the action have any OverrideFilter?
var overrideFilters = context.Results.Where(filterItem => filterItem.Filter is OverrideFilter).ToArray();
foreach (var overrideFilter in overrideFilters)
{
context.Results.RemoveAll(filterItem =>
//Remove any filter for the type indicated in the OverrideFilter attribute
filterItem.Descriptor.Filter.GetType() == ((OverrideFilter)overrideFilter.Filter).Type &&
//Remove filters with lower scope (ie controller) than the override filter (i.e. action method)
filterItem.Descriptor.Scope < overrideFilter.Descriptor.Scope);
}
}
}
public void OnProvidersExecuted(FilterProviderContext context)
{
}
}
You need to register it on the ConfigureServices of your startup class:
services.TryAddEnumerable(
ServiceDescriptor.Singleton<IFilterProvider, OverrideFriendlyFilterProvider>());
With all this pieces you will be able to override the authorization filter (or any other filter).
For example in the default HomeController of a new mvc application, any logged in user will be able to access the Home action, but only the ones with the admin role will be able to access the About action:
[Authorize]
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
[Authorize(Roles = "admin"), OverrideAuthorization]
public IActionResult About()
{
return View();
}
I think it would be better to use the new policy based authorization approach instead of using roles directly.
There is not a lot of documentation yet about policy based authorization but this article is a good start