Access ASP.NET Core HttpContext.Session in Custom RequestCultureProvider - asp.net-core

In our ASP.NET Core web app, I need to be able to access HttpContext.Session within a Custom RequestCultureProvider I've written. There is alot of background here... To be brief, I'll just mention that the business requirement is that I'm trying to meet, is to be able to display data in at least two different cultures, in our web app. A user may navigate to one page, where it should display in fr-FR and navigate to another page where it should display in zh-CN.
I have all of this working, when it comes to the "Response" from the server. By using an ActionFilter on a per-Action basis I can set the current thread's culture to whatever I want. We look up the various cultures from a database and store them in Session and then n the ActionFilter, based on an argument I pass to the ActionFilter method... I can even load ViewComponents via Controller Actions in the various cultures and have each view component display with a different culture setting. Given this, you can see why I can't store session in a Cookie or use the Querystring.
But the PROBLEM comes into play with "Requests" to the server, like when I edit Form data in a view and Submit/post the data back to an Action.
When I do that, my Dates are sent back in as NULL. BTW, I do Model binding and I have my dates marked with this attributes in my ViewModels:
[DataType(DataType.Date)]
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:dd-MMM-yyyy}")]
According to the well written article below, the order in which the Request culture is looked up, is:
https://www.jerriepelser.com/blog/how-aspnet5-determines-culture-info-for-localization/
From the query string
From a cookie
From the Accept-Language HTTP header
From the DefaultRequestCulture property of the RequestLocalizationOptions class
From the thread culture
I can certainly verify the above. If I use a cookie, it works. If I add French fr-FR as the first language in Chrome Settings, it works. If I change my default request culture in my Startup localization to fr-FR, it works...
For example, in the web page a Kendo DatePicker (or even an tag) will contain a French date like "14-août-2019", yet when I post it back to an Action, it is NULL unless I've set the querystring, cookie, Accept-Language or DefaultRequestCulture to fr-FR.
One odd thing is that #5 says it should use the current Thread Culture, and in my Action that I post Form data to I check the current thread's culture, and it is indeed fr-FR, so I don't know why my french dates are not being recognized...
So what I tried to get around this was to clear out all of those default providers and write a custom provider.
In Startup.ConfigureServices, I now do this:
services.Configure<RequestLocalizationOptions>(options =>
{
var supportedCultures = new[]
{
new CultureInfo("en-US"),
new CultureInfo("fr-FR"),
new CultureInfo("zh-CN"),
new CultureInfo("en-IE"),
};
var defaultCultureSettingOverride = this.Configuration.GetSection("appsettings").GetValue<string>("DefaultCultureOverride");
defaultCultureSettingOverride = defaultCultureSettingOverride == null ? "en-US" : defaultCultureSettingOverride;
options.DefaultRequestCulture = new RequestCulture(defaultCultureSettingOverride);
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
options.RequestCultureProviders.Clear();
options.RequestCultureProviders.Insert(0, new MyCustomRequestCultureProvider()
{
Options = options,
});
});
And that works too, as long as my MyCustomRequestCultureProvider returns the correct culture!
My problem now is: I NEED to be able to access Session state in my custom RequestCultureProvider. But the HttpContext sent into DetermineProviderCultureResult has a null Session. Why? Is it the wrong place in the pipeline?
And when I call options.RequestCultureProviders.Insert(0, , new MyCustomRequestCultureProvider() in ConfigureServices, I don't have access yet (as far as I can tell) to the HttpContextAccessor.
If I pass new MyCustomRequestCultureProvider(new HttpContextAccessor()), that does me no good...
And while I have access to IHttpContextAccessor accessor as a parameter passed into Startup.Configure, I can't do the same thing with Startup.ConfigureServices. If I add (IHttpContextAccessor accessor) as an additional parameter to ConfigureServices, .NET Core errors when I run it, telling me that method can only access an IServicesCollection parameter.
I'm at a dead end...

Well, Session being NULL in my custom RequestCultureProvider was my fault.
In Startup.Configure, we had app.UseSession() AFTER app.UseRequestLocalization()
Once I moved app.UseSession() BEFORE app.UseRequestLocalization(), I was able to access Session in my custom RequestCultureProvider.
And now once I get the user's culture from Session and use it to return the proper ProviderCultureResult, my form posts do contain the dates.

Related

Customize Identity Server 4 Login UI for Hosted Blazor WebAssembly

I have a solution based on the Visual Studio template that is successfully using IdentityServer4.
What think I understand is:
The IS4 integration implements endpoints at /authentication/{action}, e.g. authentication/login
IS4 routes that request to host/Identity/Account/Login.cshtml. I have scaffolded identity so I can see that .cshtml file in my project.
The code-behind Login.cshtml.cs takes care of speaking with SignInManager, etc.
In the client project the "LoginDisplay.razor" component redirects to /authentication/login when I click the login button. I presume that this is where step 1 above is invoked, which redirects to the location in step 2.
Fine. Now, I want to customise the whole login UI.
From instructions at: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/additional-scenarios?view=aspnetcore-5.0 I can see that:
I can configure authentication paths to be anything I want. For example:
builder.Services.AddApiAuthorization(options => {
options.AuthenticationPaths.LogInPath = "security/login";
})
So, I have created a razor component to handle security/login:
#page "/security/{action}"
#using Microsoft.AspNetCore.Components.WebAssembly.Authentication
<RemoteAuthenticatorView Action="#Action">
<LoggingIn>
This is the new login page
</LoggingIn>
</RemoteAuthenticatorView>
#code{
[Parameter] public string Action { get; set; }
}
My expectation was that after clicking "Login", I would be routed to the above and presented with a page that just said:
"This is the new login page"
and thought that from there, I would:
Customise the UI within the <LoggingIn> fragment.
Make a call to an API that would then replicate the logic in the scaffolded login.cshtml file that actually logs the user in.
That line of code from the login.cshtml.cs file looks like this:
var result = await _signInManager.PasswordSignInAsync(
Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false);
However, it seems that the razor component I created for /security/login is simply a 'transient' message that appears before being routed to the scaffolded login.csthml file.
So, to me, it seems that I am unable to actually change the physical page used to collect the user's credentials. I am only able to change the transient screen that appears before the originally scaffolded login page is shown.
Does this mean that if I want a customised UI for logging in I need to directly edit the scaffolded page as opposed to creating a whole new UI in the WebAssembly project that calls an APIController that I create to take care of using SignInManager?
It would actually be a lot less work to do that than to take the approach of creating a Client-side UI that calls an API that I create, etc. so, with hindsight, editing the scaffolded cshtml file is probably the best way to go. But am still confused as to what value is really being brought by being able to configure options.AuthenticationPaths.LogInPath.

How to prevent the flashing of content in a prerendered Blazor WebAssembly app?

The following code snippet can illustrate the issue present in Blazor applications which are rendered on the server side:
App.Client/Pages/Test.razor
#page "/test"
<p>#text</p>
#code
{
private string text;
protected override async Task OnInitializedAsync()
{
await Task.Delay(1000); // wait 1000 milliseconds
text = "foo";
}
}
When the component is first initailized on the server, the text variable is set to foo and a HTML document containing the string is sent over to the client. However, the client Blazor app isn't aware of the value that the server has assigned to the variable, which becomes null again. OnInitializedAsync is called again on the client and text is set back to foo, but only after a one second period during which there's nothing on the screen - a situation server-side rendering aims to avoid in the first place.
Do you know of any way to send components already populated with data?
Ok -- just being a bit picky -- this is a Blazor page....it isn't a component.
Yes, the pre-render they use isn't great, but assuming you're using Blazor for a SPA it should only happen once. Be careful to avoid a hard reload if you use the Microsoft.AspNetCore.Components.NavigationManager NavigateTo method. A hard re-load triggers pre-rendering because you refresh the SPA.
To avoid the whiplash, you want to avoid needing 1 second to load the text value. So you could use a singleton controller and inject it into the page. On the pre-render pass the value isn't yet known so we need 1 second to load it. Then on the client side pass the data could be accessed via the controller immediately.
Granted this isn't perfect if there is a ton of data to display. But in cases like that I generally show a 'loading' spinner and it gives the customer a feeling of controlled motion.

injecting changing view engine list

I'm trying to develop a multi-tenancy project wherein each client can have their own specific view engines.
Specifically I'd like to compile the views to a DLL (using RazorEngine) and have an individual RazorViewEngine for each client but also provide a fallback to the standard RazorViewEngine if no matching views are found, just as the MVC framework does if you have multiple view engines specified.
I have found I can inject view engines using autofac in the Global.asax of my MVC 4 project using:
ViewEngines.Engines.Clear();
var builder = new ContainerBuilder();
builder.RegisterType<WebFormViewEngine>().As<IViewEngine>();
Now I also want to provide tenant specific overrides as mentioned above which I can do with the following code:
var mtc = new MultitenantContainer(tenantIdStrategy, builder.Build());
mtc.ConfigureTenant("Client1", b => b.RegisterType<RazorViewEngine>().As<IViewEngine>());
DependencyResolver.SetResolver(new AutofacDependencyResolver(mtc));
In this example code I just wanted to see if I could set the WebFormsViewEngine as a fallback and then enable the RazorViewEngine for a specific tenant.
Upon loading and browsing to a non-tenant url, mvc will resolve just the WebFormsViewEngine as expected (through calling the DependencyResolver and in turn Autofac), this works as expected however when I then visit a url that would also include the RazorViewEngine no views are found, even though a valid razor view exists.
Conversely if I stop IISExpress or do something to generate an app pool recycle and visit a tenantable url first both view engines are registered.
From what I can tell MVC caches the list of view engines retrieved after the first call to the MultiServiceResolver.
internal IViewEngine[] CombinedItems
{
get
{
IViewEngine[] combinedItems = _combinedItems;
if (combinedItems == null)
{
combinedItems = MultiServiceResolver.GetCombined<IViewEngine>(Items, _dependencyResolver);
_combinedItems = combinedItems;
}
return combinedItems;
}
}
Is there a way to override this caching or another better way to achieve what I'm trying to get here?

How do I set push-state in durandaljs 2.0 the works on refresh?

I'm using durandaljs 2.0. I've installed the durandal starter-kit as suggested and explained here. In the shell I'm returning router.activate({ pushState: true } ); as explained in the relevant documentation (see the bottom of the page).
Happily, the URL is indeed in a 'push state' format, e.g. http://localhost:61285/flickr - the problem is that when I refresh the page I get a 404 error telling me "the resource cannot be found". If I set push-state to false ({pushState: false} above) I get a hashed URL, e.g. http://localhost:61285/#flickr - and now a refresh does work. So, how do I set up a push state mode in durandaljs 2.0 that will work with refresh?
Thanks,
Elior
Maybe to late...but
just change the routes config.
simple as this :
routes.MapRoute(
name: "Default",
url: "{*url}",
defaults: new { controller = "Home", action = "Index" }
);
When you refresh the page, the browser will make a request to the server with that URL http://localhost:61285/flickr.
What's probably happening is that if you are using ASP.NET MVC, the server is trying to locate a Controller called flickr and it throws an exception because obviously there isn't any resource with that name.
In order to get rid of this exception you should configure the server to serve the same HTML of the APP but for unknown URL's. This can be achieved using IIS URL Rewrite in ASP.NET.
So after setting up properly the server, by requesting an unknown URL it would return the initial view for the app plus whatever you pass in the query string parameters so the router can do its job at client side.
In this blog post you will find more information about how to configure ASP.NET to handle this scenarios. In the article the author uses AngularJS, however it will be the same for Durandal.
RainerAtSpirit and margabit, you're both right, thank you. Here is how I implemented the server side:
First I should note that all the interaction with the server is done via WebApi controllers.
so, for example, if the URL is:
http://localhost:61285/home/category2/subCategory22 (for a localhost), the server tries to look for a controller called 'home' and an action in it called 'category2'. Since there's no such action, I get a 404 error.
What I wanted is that the server WILL call the 'home' controller, but send the rest of the URL as parameters to the client. My solution was to add a hash after the controller's name, so that the URL will look like this: http://localhost:61285/home/#/category2/subCategory22. If this would happen then the client will take care of the hashed part with no 404 error.
For this to happen:
I added the following to 'web.config':
<customErrors mode="On" defaultRedirect="Error">
<error statusCode="404" redirect="Error" />
</customErrors>
Then I create a controller named 'ErrorController' with the following class in it:
public class ErrorController : ApiController
{
[HttpGet, HttpPost, HttpPut, HttpDelete, HttpHead, HttpOptions, AcceptVerbs("PATCH"), AllowAnonymous]
public HttpResponseMessage Handle404()
{
string [] parts = Request.RequestUri.OriginalString.Split(new[] { '?' }, StringSplitOptions.RemoveEmptyEntries);
string parameters = parts[ 1 ].Replace("aspxerrorpath=","");
var response = Request.CreateResponse(HttpStatusCode.Redirect);
response.Headers.Location = new Uri(parts[0].Replace("Error","") + string.Format("#{0}", parameters));
return response;
}
}
what happens is that when the server get a URL with no relevant action as I mentioned above, it redirects it to this controller in the following format: http://localhost:61285/Error?aspxerrorpath=home/category2/subCategory22
as you can see, I manipulate this to add the hash and remove the unnecessary info: http://localhost:61285/home/#/category2/subCategory22 and the redirect the server to the 'home' controller.
You might wonder why I do all of this - the reason is that Durandal, a wonderful platform, enables me to use push state, but in order for that to happen I have to work-around the server getting a non-hashed URL and pass it to the client despite the fact there's no relevant controller/action; Durandal get's a hashed URL but removes the hash automatically and the user eventually sees a hash-less URL while Durandal provides all the necessary push state functionality.
Elior

Change Locale in MVC application on click

How can I apply new locale in MVC application ?
I've created an Action that sets
Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-US");
Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");
then redirects to Index.chtml , but this is not working.
How to make this thing work please ?
Setting the culture is only valid until the thread terminates, which happens after all page processing has finished. In this case, after you issue the redirect the server will send the Location HTTP header to the new address and close the response.
The browser will then initiate a new request to the new location and the value you set in Thread.CurrentThread.CurrentCulture will reset to the default one.
You have to persist the language selection (session, cookie...) and then apply it at start of your page logic.