Copy routes to new HttpContext - asp.net-core

I use the following code to create a new DefaultHttpContext and use it to render views to strings.
var httpContext = new DefaultHttpContext();
if (serviceProvider.GetService<IHttpContextAccessor>() is { } accessor)
{
accessor.HttpContext = httpContext;
}
var website = page.WebsitePageModel as WebsitePageModel;
var domain = website?.PrimaryDomain;
if (website != null && domain != null)
{
httpContext.Request.Host = "host"
httpContext.Request.Scheme = "https";
httpContext.Request.Path = "/my-page"
}
httpContext.RequestServices = serviceProvider;
var routeData = httpContext.GetRouteData();
routeData.Routers.Add(new RouteCollection());
This works well until I try to render a view component that uses routes/endpoints, e.g.
string url = Url.Action("MyAction", "MyController");
The url will be null because the controller does not know any routes. How can I access the routes of my web application and copy them to the controller?

If you just want to get the route, I found another way, but I don't know if it helps you.
I get all registered routes using IActionDescriptorCollectionProvider:
#using Microsoft.AspNetCore.Mvc.Infrastructure
#inject IActionDescriptorCollectionProvider provider
#{
var urls = provider.ActionDescriptors.Items
.Select(descriptor => "/" + string.Join('/', descriptor.RouteValues.Values
.Where(v => v != null)
.Select(c => c.ToLower())
.Reverse()))
.Distinct()
.ToList();
}
#foreach (var item in #urls)
{
#item
}

Related

Redirect to same page with an extra parameter

I am using Request Localization in a NET Core 7 and Razor Pages application:
builder.Services.AddRazorPages();
builder.Services.Configure<RequestLocalizationOptions>(options => {
options.DefaultRequestCulture = new RequestCulture("pt");
options.SupportedCultures = new List<CultureInfo> { new CultureInfo("en"), new CultureInfo("pt") };
options.RequestCultureProviders.Insert(0, new RouteDataRequestCultureProvider {
RouteDataStringKey = "culture",
UIRouteDataStringKey = "culture",
Options = options
});
});
WebApplication application = builder.Build();
application.UseRouting();
application.MapRazorPages();
application.UseRequestLocalization();
// ...
A few of my razor pages are:
Index: #page "/{culture?}" Example: /pt
About: #page "/{culture?}/about" Example: /pt/about
When the culture is null or invalid I want to redirect to same page with default culture.
I am trying to do this using a middleware:
public class RedirectUnsupportedCulturesMiddleware {
private readonly RequestDelegate _next;
private readonly string _routeDataStringKey;
public RedirectUnsupportedCulturesMiddleware(
RequestDelegate next,
IOptions<RequestLocalizationOptions> monitor) {
RequestLocalizationOptions options = monitor.Value;
_next = next;
var provider = options.RequestCultureProviders
.Select(x => x as RouteDataRequestCultureProvider)
.Where(x => x != null)
.FirstOrDefault();
_routeDataStringKey = provider.RouteDataStringKey;
}
public async Task Invoke(HttpContext context) {
var requestedCulture = context.GetRouteValue(_routeDataStringKey)?.ToString();
var cultureFeature = context.Features.Get<IRequestCultureFeature>();
var actualCulture = cultureFeature?.RequestCulture.Culture.Name;
if (string.IsNullOrEmpty(requestedCulture) ||
!string.Equals(requestedCulture, actualCulture, StringComparison.OrdinalIgnoreCase)) {
var newCulturedPath = GetNewPath(context, actualCulture);
context.Response.Redirect(newCulturedPath);
return;
}
await _next.Invoke(context);
}
private string GetNewPath(HttpContext context, string newCulture) {
var routeData = context.GetRouteData();
var router = routeData.Routers[0];
var virtualPathContext = new VirtualPathContext(
context,
routeData.Values,
new RouteValueDictionary { { _routeDataStringKey, newCulture } });
return router.GetVirtualPath(virtualPathContext).VirtualPath;
}
}
When I access the home page, e.g "/", I get an exception in the following:
var router = routeData.Routers[0];
Questions
How to solve this?
Can I use a 301 redirect from thee middleware?
Can I use the Net Core UseRewriter to accomplish the same objective?
Here is a whole working demo you could follow:
1.Custom IPageRouteModelConvention to map the route and then no need modify page route like #page "/{culture?}/xxx"
public class CustomCultureRouteRouteModelConvention : IPageRouteModelConvention
{
public void Apply(PageRouteModel model)
{
List<SelectorModel> selectorModels = new List<SelectorModel>();
foreach (var selector in model.Selectors.ToList())
{
var template = selector.AttributeRouteModel.Template;
selectorModels.Add(new SelectorModel()
{
AttributeRouteModel = new AttributeRouteModel
{
Template = "/{culture}" + "/" + template
}
});
}
foreach (var m in selectorModels)
{
model.Selectors.Add(m);
}
}
}
2.Add this page conventions in Program.cs
builder.Services.AddRazorPages().AddRazorPagesOptions(opts =>
{
opts.Conventions.Add(new CustomCultureRouteRouteModelConvention()); //add this...
});
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
builder.Services.Configure<RequestLocalizationOptions>(
opt =>
{
var supportCulteres = new List<CultureInfo>
{
new CultureInfo("pt"),
new CultureInfo("fr")
};
opt.DefaultRequestCulture = new RequestCulture("pt");
opt.SupportedCultures = supportCulteres;
opt.SupportedUICultures = supportCulteres;
opt.RequestCultureProviders.Insert(0, new RouteDataRequestCultureProvider()); //add this...
});
3.The order of the middleware
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
//app.UseRequestLocalization();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseRequestLocalization(); //be sure add here......
app.UseAuthorization();
app.MapRazorPages();
app.Run();
Result:

Custom parameter with Microsoft Identity Platform and Azure AD B2C - how to add information using the 'State' paramater?

I'm following this tutorial: https://learn.microsoft.com/en-us/azure/active-directory/develop/scenario-web-app-sign-user-overview?tabs=aspnetcore
According to other docs, I can use the 'state' parameter to pass in custom data and this will be returned back to the app once the user is logged in
However, OIDC also uses this state param to add its own encoded data to prevent xsite hacking - I cant seem to find the correct place in the middleware to hook into this and add my custom data
There's a similar discussion on this thread: Custom parameter with Microsoft.Owin.Security.OpenIdConnect and AzureAD v 2.0 endpoint but I'm using AddMicrosoftIdentityWebApp whereas they're using UseOpenIdConnectAuthentication and I don't know how to hook into the right place in the middleware to add my custom data then retrieve it when on the return.
I'd like to be able to do something like in the code below - when I set break points state is null outgoing and incoming, however the querystring that redirects the user to Azure has a state param that is filled in by the middleware, but if i do it like this, then I get an infinite redirect loop
public static class ServicesExtensions
{
public static void AddMicrosoftIdentityPlatformAuthentication(this IServiceCollection services, IConfigurationSection azureAdConfig)
{
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(options =>
{
options.ClientId = azureAdConfig["ClientId"];
options.Domain = azureAdConfig["Domain"];
options.Instance = azureAdConfig["Instance"];
options.CallbackPath = azureAdConfig["CallbackPath"];
options.SignUpSignInPolicyId = azureAdConfig["SignUpSignInPolicyId"];
options.Events.OnRedirectToIdentityProvider = context =>
{
//todo- ideally we want to be able to add a returnurl to the state parameter and read it back
//however the state param is maintained auto and used to prevent xsite attacks so we can just add our info
//as we get an infinite loop back to az b2 - see https://blogs.aaddevsup.xyz/2019/11/state-parameter-in-mvc-application/
//save the url of the page that prompted the login request
//var queryString = context.HttpContext.Request.QueryString.HasValue
// ? context.HttpContext.Request.QueryString.Value
// : string.Empty;
//if (queryString == null) return Task.CompletedTask;
//var queryStringParameters = HttpUtility.ParseQueryString(queryString);
//context.ProtocolMessage.State = queryStringParameters["returnUrl"]?.Replace("~", "");
return Task.CompletedTask;
};
options.Events.OnMessageReceived = context =>
{
//todo read returnurl from state
//redirect to the stored url returned
//var returnUrl = context.ProtocolMessage.State;
//context.HandleResponse();
//context.Response.Redirect(returnUrl);
return Task.CompletedTask;
};
options.Events.OnSignedOutCallbackRedirect = context =>
{
context.HttpContext.Response.Redirect(context.Options.SignedOutRedirectUri);
context.HandleResponse();
return Task.CompletedTask;
};
});
}
}
Use AAD B2C docs
https://learn.microsoft.com/en-us/azure/active-directory-b2c/enable-authentication-web-application-options#support-advanced-scenarios
Then follow this
https://learn.microsoft.com/en-us/azure/active-directory-b2c/enable-authentication-web-application-options#pass-an-id-token-hint
Just change context.ProtocolMessage.IdTokenHint to context.ProtocolMessage.State.
Ok, I've got it to work
Couple of things I discovered, but not sure why - I managed to pass a guid in state and get it back without getting that infinite loop, so I thought I'd try the url again but base64 encode, which worked. I did have some further issues which was solved by doing the following:
public static void AddMicrosoftIdentityPlatformAuthentication(this IServiceCollection services, IConfigurationSection azureAdConfig)
{
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(options =>
{
options.ClientId = azureAdConfig["ClientId"];
options.Domain = azureAdConfig["Domain"];
options.Instance = azureAdConfig["Instance"];
options.CallbackPath = azureAdConfig["CallbackPath"];
options.SignUpSignInPolicyId = azureAdConfig["SignUpSignInPolicyId"];
options.Events.OnRedirectToIdentityProvider = context =>
{
var queryString = context.HttpContext.Request.QueryString.HasValue
? context.HttpContext.Request.QueryString.Value
: string.Empty;
if (queryString == null) return Task.CompletedTask;
var queryStringParameters = HttpUtility.ParseQueryString(queryString);
var encodedData = queryStringParameters["returnUrl"]?.Replace("~", "").Base64Encode();
context.ProtocolMessage.State = encodedData;
return Task.CompletedTask;
};
options.Events.OnTokenValidated = context =>
{
var url = context.ProtocolMessage.State.Base64Decode();
var claims = new List<Claim> { new Claim("returnUrl", url) };
var appIdentity = new ClaimsIdentity(claims);
context.Principal?.AddIdentity(appIdentity);
return Task.CompletedTask;
};
options.Events.OnTicketReceived = context =>
{
if (context.Principal == null) return Task.CompletedTask;
var url = context.Principal.FindFirst("returnUrl")?.Value;
context.ReturnUri = url;
return Task.CompletedTask;
};
options.Events.OnSignedOutCallbackRedirect = context =>
{
context.HttpContext.Response.Redirect(context.Options.SignedOutRedirectUri);
context.HandleResponse();
return Task.CompletedTask;
};
});
}
so now it all works nicely - the user can hit a protected route, get bumped to the login then redirected on return
Maybe not the most elegant soln and I'm not 100% sure of the how or why, but it works

How can I store user id in session for per request in asp net core web api project?

I have middleware that get jwt from header and parse it:
public Task Invoke(HttpContext httpContext)
{
string tokenString = httpContext.Request.Headers["Authorization"].ToString();
if (string.IsNullOrEmpty(tokenString))
{
return _next(httpContext);
}
var jwtEncodedString = tokenString.Substring(7);
var token = new JwtSecurityToken(jwtEncodedString: jwtEncodedString);
string userId = token.Claims.First(c => c.Type == "sub").Value;
string userName = token.Claims.First(c => c.Type == "preferred_username").Value;
string name = token.Claims.First(c => c.Type == "given_name").Value;
string surname = token.Claims.First(c => c.Type == "family_name").Value;
var roles = token.Claims.Where(c => c.Type == "user_roles").ToList();
var lastRole = "";
foreach(var role in roles)
{
lastRole = role.Value;
}
httpContext.Session.SetString("userId", userId);
httpContext.Session.SetString("userName", userName);
httpContext.Session.SetString("name", name);
httpContext.Session.SetString("surname", surname);
httpContext.Session.SetString("role", lastRole);
return _next(httpContext);
}
}
In Configure method of startup I add middleware: app.UseSession().UseCustomMiddleware();
When request comes middleware parse jwt, but in another request session data is enabled yet. I want to make session per request sensitive. How can I do it?

Route localization in ASP.NET Core 2.2

I am developing application using ASP.NET Core 2.2 and I am struggling with how to implement route localization, ex. depending on request I need to redirect to route /en/products, if language is not specified in the route.
If language is not specified then get locale from accept-language header.
Below demo is applied to use twoLetterLanguageName.Refer to this tutorial
1.Create a RouteDataRequestCultureProvider class:
public class RouteDataRequestCultureProvider : RequestCultureProvider
{
public int IndexOfCulture;
public int IndexofUICulture;
public override Task<ProviderCultureResult> DetermineProviderCultureResult(HttpContext httpContext)
{
if (httpContext == null)
throw new ArgumentNullException(nameof(httpContext));
string culture = null;
string uiCulture = null;
var twoLetterCultureName = httpContext.Request.Path.Value.Split('/')[IndexOfCulture]?.ToString();
var twoLetterUICultureName = httpContext.Request.Path.Value.Split('/')[IndexofUICulture]?.ToString();
if (twoLetterCultureName == "de")
culture = "de-DE";
else if (twoLetterCultureName == "en")
culture = uiCulture = "en-US";
if (twoLetterUICultureName == "de")
culture = "de-DE";
else if (twoLetterUICultureName == "en")
culture = uiCulture = "en-US";
if (culture == null && uiCulture == null)
return NullProviderCultureResult;
if (culture != null && uiCulture == null)
uiCulture = culture;
if (culture == null && uiCulture != null)
culture = uiCulture;
var providerResultCulture = new ProviderCultureResult(culture, uiCulture);
return Task.FromResult(providerResultCulture);
}
}
2.And a LanguageRouteConstraintclass
public class LanguageRouteConstraint : IRouteConstraint
{
public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
{
if (!values.ContainsKey("culture"))
return false;
var culture = values["culture"].ToString();
return culture == "en" || culture == "de";
}
}
3.startup.cs ConfigureServices:
services.AddLocalization(options => options.ResourcesPath = "Resources");
services.AddMvc()
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
.AddDataAnnotationsLocalization();
services.Configure<RequestLocalizationOptions>(options =>
{
var supportedCultures = new List<CultureInfo>
{
new CultureInfo("en"),
new CultureInfo("de"),
};
options.DefaultRequestCulture = new RequestCulture(culture: "en", uiCulture: "en-US");
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
options.RequestCultureProviders = new[]{ new RouteDataRequestCultureProvider{
IndexOfCulture=1,
IndexofUICulture=1
}};
});
services.Configure<RouteOptions>(options =>
{
options.ConstraintMap.Add("culture", typeof(LanguageRouteConstraint));
});
4.startup.cs Configure
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
var options = app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>();
app.UseRequestLocalization(options.Value);
//other middlewares
app.UseMvc(routes =>
{
routes.MapRoute(
name: "LocalizedDefault",
template: "{culture:culture}/{controller}/{action}/{id?}",
defaults: new {controller = "Home", action = "Index" });
//constraints: new { culture = new CultureConstraint(defaultCulture: "en", pattern: "[a-z]{2}") });
routes.MapRoute(
name: "default",
template: "{controller}/{action}/{id?}",
defaults: new { controller = "Home", action = "Index" });
});
}
Then you could change culture in browser url directly using /en/Home/Privacy.
If language is not specified then get locale from accept-language header
You could use Url Rewriting Middleware to check the route value and redirect it to a new route with default culture.
1.Create a Redirect rule:
public class RewriteRules
{
public static void RedirectRequests(RewriteContext context)
{
//Your logic
var request = context.HttpContext.Request;
var path = request.Path.Value;
var userLangs = request.Headers["Accept-Language"].ToString();
var firstLang = userLangs.Split(',').FirstOrDefault();
var defultCulture = string.IsNullOrEmpty(firstLang) ? "en" : firstLang.Substring(0,2);
//Add your conditions of redirecting
if ((path.Split("/")[1] != "en") && (path.Split("/")[1] != "de"))// If the url does not contain culture
{
context.HttpContext.Response.Redirect($"/{defultCulture}{ request.Path.Value }");
}
}
}
2..Use the middleware in Startup Configure method:
app.UseAuthentication();//before the Rewriter middleware
app.UseRewriter(new RewriteOptions()
.Add(RewriteRules.RedirectRequests)
);
//MVC middleware
Then if your input /Home/Privacy in browser it will redirect to url like /en/Home/Privacy.

DataAnnotations attributes on custom control

I've used a custom control (HTML Helper) to build an Autocomplete controller.
it works great, the only thing is the validation problem.
on the client side, the validation works fine when jquery.validation.js is out of the picture, (for empty text box it gives an error message).
if the user selects something from the autocomplete, so im fine.
but when the user input is just junk, then the HttpPost needs to handle the junk & return an error message to the user.
HOW??
also, i've seen a DataAnnotation called Remote, which can manage the validation on the client side, is it better ? if so, how can i add DataAnnotaion on a custom control ??
Thank's :)
here is my code:
Index.cshtml
#using (Html.BeginForm("Index", "Create"))
{
#Html.AutocompleteFor(Url.Action("AutoCompleteServiceProviders", "Create"), true, "ex. Shower", c => c.service_id, a => a.name)
<input type="submit" id="search" value="" />
}
AutoComplete.cs
private static MvcHtmlString CreateAutocomplete<TModel>(this HtmlHelper<TModel> helper, string actionUrl, bool? isRequired, string placeholder, params Expression<Func<TModel, object>>[] expression)
{
var builder = new StringBuilder();
foreach (var item in expression)
{
var attributes = new Dictionary<string, object>
{
{ "data-autocomplete", true },
{ "data-action", actionUrl }
};
if (!string.IsNullOrWhiteSpace(placeholder))
{
attributes.Add("placeholder", placeholder);
}
if (isRequired.HasValue && isRequired.Value)
{
attributes.Add("required", "required");
}
Func<TModel, object> method = item.Compile();
var value = (Object)null;
if ((TModel)helper.ViewData.Model != null)
{
value = method((TModel)helper.ViewData.Model);
}
var baseProperty = (string)null;
var hidden = (MvcHtmlString)null;
if (item.Body is MemberExpression)
{
baseProperty = ((MemberExpression)item.Body).Member.Name;
hidden = helper.Hidden(baseProperty, value);
attributes.Add("data-value-name", baseProperty);
}
else
{
var op = ((UnaryExpression)item.Body).Operand;
baseProperty = ((MemberExpression)op).Member.Name;
hidden = helper.Hidden(baseProperty, value);
}
attributes.Add("data-value-id", "service_id");
var automcompleteName = baseProperty + "_autocomplete";
var textBox = (MvcHtmlString)null;
if (value != null)
{
textBox = helper.TextBox(automcompleteName, value, string.Empty, attributes);
}
else
{
textBox = helper.TextBox(automcompleteName, null, string.Empty, attributes);
}
builder.AppendLine(hidden.ToHtmlString());
if (baseProperty == "name")
{
builder.AppendLine(textBox.ToHtmlString());
}
}
return new MvcHtmlString(builder.ToString());
}
You can get your validation from here:
var validation = htmlHelper.ValidationMessageFor(expression, null, new Dictionary<string, object>());
UPDATE:
I use TagBuilder to create tags. What I do with tagbuilder is add that validation to a span or div tag and let the unobtrusive javascript hide/show it when needed. It returns an MVCHtmlString you can just append it to the element you want to display it in