Localization not working in .NET6 Razor Pages project - asp.net-core

I was trying to get localization to work in my 'real' project, but was not able to do so. So I created a new stock .NET6 Razor Pages project to test localization in a fresh environment. However I am not quite able to do that there as well. At this point I have read about 6 articles, watched 2 tutorials and read the official documentation which says that it is up to date with .NET6, but I'm not sure about that (partly because the examples use old syntax, i.e. not things that came with .NET6). Every single one of those resources more or less did/said the same things, which is what I have right now, but it just doesn't work.
This is my Program.cs file:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
//builder.Services.AddRazorPages(); builder.Services.AddMvc().AddDataAnnotationsLocalization();
builder.Services.AddRazorPages().AddDataAnnotationsLocalization();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
var supportedCultures = new[] { "en" };
var localizationOptions = new RequestLocalizationOptions().SetDefaultCulture(supportedCultures[0])
.AddSupportedCultures(supportedCultures)
.AddSupportedUICultures(supportedCultures);
app.UseRequestLocalization(localizationOptions);
app.UseAuthorization();
app.MapRazorPages();
app.Run();
I have tested the "things I'll mention bellow" with ...AddRazorPages().AddData... and (AddMvc().AddData... with ...AddRazorPages();) as well. That's why one of those lines is commented.
The way I am testing the localization is just with an OnGet call on the Index page, and restarting the (local) server after every change just to not miss some dumb thing. On the OnGet, I just log the value to the console. But I'm always getting the key. Here is the IndexModel class:
public class IndexModel : PageModel
{
private readonly IStringLocalizer<IndexModel> _localizer;
public IndexModel(IStringLocalizer<IndexModel> localizer)
{
_localizer = localizer;
}
public void OnGet()
{
Console.WriteLine(_localizer["Test"].Value);
}
}
This is how my (relevant) folder structure looks like in the project's directory:
And finally the resx file:
Things I have tested are the .resx file names and the 2 possible locations. I am sure that both are correct, because when both files were active (neither had the .Dup (for duplicate) extension) at the end, I got an error from the compiler that multiple resources point to the same location or something like that.
The names I have tested are the following:
IndexModel.en.resx
Index.en.resx
IndexModel.cshtml.cs.en.resx
Index.cshtml.cs.en.resx
IndexModel.cs.en.resx
Index.cs.en.resx

Related

Blazor Server Side Localization without cookie

So Im trying to add Localization without a cookie to my Blazor Server Side project.
In the documentaition https://learn.microsoft.com/en-us/aspnet/core/blazor/globalization-localization?view=aspnetcore-5.0#provide-ui-to-choose-the-culture it says:
Use of a cookie ensures that the WebSocket connection can correctly propagate the culture. If localization schemes are based on the URL path or query string, the scheme might not be able to work with WebSockets, thus fail to persist the culture. Therefore, use of a localization culture cookie is the recommended approach.
So I know its not recommended, but I would like it to be path-based anyway if possible. Can it be done with the standard services.AddLocalization(); and IStringLocalizer, or do I have to build a custom one?
For Localization, I prefer a direct, clear, and readable link that contains the language's first 2 letters (like: mysite.com/en/mypage ), which allows having the correct link using exactly the language needed.
The only solution I found that works perfectly with Blazor Server without the need for Cookies was using the package "BlazorServerUrlRequestCultureProvider" created by "Pier-Luc Bonneville"; it is effortless to use and gives excellent results.
Usage:
Install the package BlazorServerUrlRequestCultureProvider (worked perfectly for me with .Net 5.0):
Install-Package BlazorServerUrlRequestCultureProvider -Version 1.0.0
Startup.cs:
using BlazorServerUrlRequestCultureProvider;
// ...
//------------------------------
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// ...
// Set the Resources folder name:
services.AddLocalization(options => options.ResourcesPath = "Resources");
// ...
}
}
//------------------------------
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
}
app.UseHttpsRedirection();
app.UseStaticFiles();
#region Localization
var supportedCultures = new[]
{
new CultureInfo("en"),
new CultureInfo("ar"),
};
var options = new RequestLocalizationOptions
{
// Select a default language:
DefaultRequestCulture = new RequestCulture("en"),
// For Numbers, Dates, etc:
SupportedCultures = supportedCultures,
// For strings that we have localized in .resx files:
SupportedUICultures = supportedCultures
};
app.UseUrlRequestLocalization(options);
#endregion
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
app.UseRequestLocalization();
}
That was just all you need! No Cookies nor Controllers!
Now use StringLocalizer as usual (in my case, the resource files names were App.resx and App.ar.resx inside the Resource folder):
MyPage.razor:
#page "/ar/MyPage"
#page "/en/MyPage"
#inject IStringLocalizer<App> localizer
<h1>#localizer["My Translated Text Here !"]</h1>
All Thanks & Credits goes to "Pier-Luc Bonneville" for his wonderful work. Project Website , NuGet .
--
Bonus:
To make your life much easier, use the tool ResXManager to easily manage and translate resource files, it's a free and great tool, and it will save you a lot of time.

Asp.net core, when in debug, bypass Authorization

I have a test web site which uses the aspnetCore [AuthorizeAttribute] at the entire controller level to ensure only Authenticated Users can hit this site.
While we debug and test new features, we constantly have to comment out the attribute in each controller, which I know will be easy to forget and might get merged some day.
We've had good success with checking to see if a Debugger is attached before...I am wondering which AuthenticationScheme I should specify to allow anonymous, only if debugging.
I extend the base AuthorizeAttribute so I have an easy place to shim in some code.
public class MyAppAuthorizeAttribute : AuthorizeAttribute
{
public MyAppAuthorizeAttribute()
: base(Policies.MyAppAuthorize)
{
if (System.Diagnostics.Debugger.IsAttached)
{
Console.WriteLine("Skipping auth for debug"); //we hit this line but...
this.AuthenticationSchemes = AllowAnonymousAttribute //this setting does not work
}
else
{
this.AuthenticationSchemes = "IntegratedWindowsAuthentication";
}
}
}
Seems like a good candidate for IWebHostingEnvironment.IsDevelopment():
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (!env.IsDevelopment())
{
app.UseAuthentication();
app.UseAuthorization(); // maybe optional, depends on your case
}
...
Your requirement may be necessary in some cases where the user is required to be authenticated BUT not actually referenced in the code (e.g: the code does not access any info on the current identity, especially related to the business model of the user).
So I assume that you are aware of that because the following will just simply remove the requirement to check for authenticated user when the code runs in the development environment. There is another way to auto sign-in a dummy user which can be better in some scenarios.
Here is the first solution, it configures a default policy only which does not include the DenyAnonymousAuthorizationRequirement (which is the only requirement contained in the default policy). That means if you have multiple policies used somewhere (with AuthorizeAttribute), only the default will be ignored (while debugging). The second solution (shown later) may suit that scenario better.
//inject IHostingEnvironment in the Startup constructor
public Startup(IConfiguration configuration, IHostingEnvironment env){
HostingEnvironment = env;
}
public IHostingEnvironment HostingEnvironment { get; }
//in the ConfigureServices method in Startup.cs
services.AddAuthorization(o => {
if (HostingEnvironment.IsDevelopment()){
o.DefaultPolicy = new AuthorizationPolicyBuilder(CookieAuthenticationDefaults.AuthenticationScheme)
//there should be at least 1 requirement
//here we add a simple always-passed assertion
.RequireAssertion(e => true).Build();
}
//...
});
We need to use IHostingEnvironment (in .net core 2.2, since 3.0 we have 2 alternatives IWebHostEnvironment and IHostEnvironment) so we inject it in the Startup constructor and store it in a readonly property (as you see above). There is another way is try to get the ASPNETCORE_ENVIRONMENT variable directly like this:
var isDevelopment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development";
Here is the second solution in which you use a custom global IAsyncAuthorizationFilter to auto sign-in a dummy user (so it's always authenticated for all requests).
public class AllowAnonymousFilterAttribute : Attribute, IAsyncAuthorizationFilter
{
readonly IHostingEnvironment _hostingEnvironment;
public AllowAnonymousFilterAttribute(IHostingEnvironment env){
_hostingEnvironment = env;
}
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
if (_hostingEnvironment.IsDevelopment() && !context.HttpContext.User.Identity.IsAuthenticated)
{
//prepare a dummy user to auto sign-in
HttpContext.User = new ClaimsPrincipal(new[] {
new ClaimsIdentity(new []{ new Claim(ClaimTypes.NameIdentifier,"admin")},
CookieAuthenticationDefaults.AuthenticationScheme)
});
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
HttpContext.User);
}
}
}
Register the filter globally, here I write code for .net core 2.2:
services.AddMvc(o => {
//register the filter only for development (should be debugging)
if (HostingEnvironment.IsDevelopment()){
o.Filters.Add<AllowAnonymousFilterAttribute>();
}
//...
});
I'm sure there are still some other solutions but what I've introduced here are fairly simple and good enough for your purpose.
P/S: the first solution I've introduced above suits for .net core 2.2 (actually currently I do not have access to newer versions of .net core, it's a pity). For the newer versions, the Authorization middleware is separate so you may just simply not call .UseAuthorization() middleware in the Configure method (of course for development environment only) as one other answer suggests.

Swashbuckle/Swagger on .NET Core 2.1 has stopped working since upgrade

I have a .NET Core 2.0 application, using Swashbuckle/Swagger to generate API documentation. When we were on 2.1.0-preview, Swagger was working fine. Then we did the big upgrade to 2.1.0 release and SDK 2.1.300. We didn't notice exactly when things broke, but now our Swagger docs won't load. Here's what we see:
Project has a reference to Swashbuckle.AspNetCore version 2.5.0. The relevant code in Startup.cs is below. In ConfigureServices():
services.AddSwaggerGen(swaggerOptions =>
{
// Register a swagger doc
swaggerOptions.SwaggerDoc("v1", new Info
{
// Optional descriptive info that will be included in the Swagger output
Contact = new Contact
{
Name = "LightSail",
Url = "https://myurl.com/"
},
Description = "A description of the API can go here",
Title = "My API",
Version = "v1"
});
// Xml file to get comment information from
swaggerOptions.IncludeXmlComments("App_Data/Api.xml");
});
And in Configure():
app.UseSwagger();
app.UseSwaggerUI(swaggerUiOptions => swaggerUiOptions.SwaggerEndpoint("/swagger/v1/swagger.json", "My API v1"));
I found lots of other similar questions, one of which suggested that there might be duplicate endpoints; I tried adding a call to .ResolveConflictingEndpoints() but that made no difference. I have searched through my project folders and there is no file called swagger.json, so I'm guessing that's the problem.
Any ideas why this is not working, or how to fix?
This is usually indicative of controllers/actions that Swashbuckle doesn't support for one reason or another.
It's expected that you don't have a swagger.json file in your project. Swashbuckle creates and serves that dynamically using ASP.NET Core's ApiExplorer APIs. What's probably happening here is that Swashbuckle is unable to generate Swagger.json and, therefore, the UI is failing to display.
As HelderSepu said, it's hard to know exactly what caused the failure, so the best way to debug is probably just to remove half your controllers (just move the files to a temporary location) and check whether the issues persists. Then you'll know which half of your controllers contains the troublesome action. You can 'binary search' removing controllers (and then actions) until you figure out which action method is causing Swashbuckle to not be able to generate Swagger.json. Once you know that, it should be obvious whether this is some issue in your code or an issue that should be filed in the Swashbuckle repo.
For example, Swashbuckle appears to not support open generics, so having a response type attribute like [ResponseType(typeof(IEnumerable<>))] could cause this sort of behavior. It could also be an issue with ambiguous routes or something like that tripping Swashbuckle up. Once you've narrowed down the cause of failure to something more specific like that, it can either be fixed or filed, as appropriate.
Today I found out that I could just go to the json url in the browser and get some error information
for example
myapiurl/api/vi/swagger.json
I was able to solve this error by explicitly adding the http verb attribute to my asp.net core 2.x controller method. The convention of prefixing the method name with the http verb is not enough for Swashbuckle apparently.
[HttpPost]
public async Task<IActionResult> AddNewData([FromBody] MyType myType) { … }
In my case I can reproduce your error by omitting "." from the end point as you have done.
I don't get the error if I include "." at the start of the path.
Here is more of my code in case it is relevant.
In ConfigureServices I have
services.AddSwaggerGen(c =>
{
c.OperationFilter<AuthorizationHeaderParameterOperationFilter>();
c.SwaggerDoc("v1", new Info
{
Version = "v1",
Title = "My API",
Description = "ASP.NET Core Web API",
TermsOfService = "None",
Contact = new Contact
{
Name = "my name",
Email = "me#myemail.com"
}
});
});
In configure
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseAuthentication();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseRewriter(new RewriteOptions()
.AddRedirectToHttpsPermanent());
app.UseSwagger(c =>
{
c.RouteTemplate =
"api-docs/{documentName}/swagger.json";
});
app.UseSwaggerUI(c =>
{
//Include virtual directory if site is configured so
c.RoutePrefix = "api-docs";
c.SwaggerEndpoint("./v1/swagger.json", "Api v1");
});
app.UseMvc(routes =>
{
routes.MapRoute(
"default",
"{controller=Home}/{action=Index}/{id?}");
});
Also there is
public class AuthorizationHeaderParameterOperationFilter : IOperationFilter
{
public void Apply(Operation operation, OperationFilterContext context)
{
var filterPipeline = context.ApiDescription.ActionDescriptor.FilterDescriptors;
var isAuthorized = filterPipeline.Select(filterInfo => filterInfo.Filter).Any(filter => filter is AuthorizeFilter);
var allowAnonymous = filterPipeline.Select(filterInfo => filterInfo.Filter).Any(filter => filter is IAllowAnonymousFilter);
if (isAuthorized && !allowAnonymous)
{
if (operation.Parameters == null)
operation.Parameters = new List<IParameter>();
operation.Parameters.Add(new NonBodyParameter
{
Name = "Authorization",
In = "header",
Description = "access token",
Required = true,
Type = "string"
});
}
}
My dependencies are
Microsoft.AspNetCore.App (2.1.0)
Swashbuckle.AspNetCore (2.5.0)
Microsoft.NETCore.App (2.1.0)
Personally I was a bit quick and forgot to add this line to the method ConfigureServices in Startup.cs.
services.AddSwaggerDocument();
In my case, I missed the 'HttpAttribute':
public async Task<IEnumerable<ClientesListDto>> GetAll()
{
return await _service.GetAllAsync();
}
Then I put it and swagger likes it:
[HttpGet]
public async Task<IEnumerable<ClientesListDto>> GetAll()
{
return await _service.GetAllAsync();
}
In my case, I had this:
[HttpGet("CleanUpSnoozedLeads")]
public async Task<ActionResult<bool>> CleanUpSnoozedLeads()
[HttpGet("CleanUpSnoozedLeads")]
public async Task<ActionResult<bool>> DoSomethingElse()
Notice the HttpGet() had the same name. That causes the undefined error as well.
A very common case is ambiguity. Just use the same signature for two PUT or POST operations for example and you will get the error.
Others answers did not worked for me.
I was able to fix and understand my issue when I tried to go to the swagger.json URL location:
https://localhost:XXXXX/swagger/v1/swagger.json
The page will show the error and reason why it is not found.
In my case, I saw that there was a misconfigured XML definition of one of my methods based on the error it returned:
NotSupportedException: HTTP method "GET" & path "api/Values/{id}" overloaded by actions - ...
...
...
In my case, i just forgot to add the HttpPostAttribute annotation to the method.
[HttpPost]
public ActionResult Post()
{
return Ok();
}
In my case there was a conflict in the schemaId. Apparently every class in the swagger JSON must have a unique schemaId. If you have two classes in different namespaces with the same name this will not work. We have to configure "UseFullTypeNameInSchemaIds" in the startup class.
Add "options.CustomSchemaIds(x => x.FullName);" in "services.AddSwaggerGen"
I found the trace by enabling Output window in VS, selecting the main project from Show output from dropdown list then visit http://{yourapiendpoint}/swagger/v1/swagger.json
If your api have same two or more [HttpGet] its not working swagger.
You should be specify [HttpGet] , [HttpGet ("{id}")]
simple solution

AspNetCore Swagger/Swashbuckle and Virtual Directories

I'm struggling with getting the configuration for Swagger/Swashbuckle correct in an Asp.Net core 2.0 web api. I've followed the examples, which all work brilliantly when working at the root folder/localhost. As many others have pointed out, I too am seeing different behavior when working in a virtual folder on the server. I've examined this question - IIS site within virtual directory Swagger UI end point which is similar, but the solution provided there is not working.
My startup.cs file has the following block for configuring services:
public void ConfigureServices(IServiceCollection services)
{
services.AddCors();
services.AddSwaggerGen(c =>
{
c.DescribeAllEnumsAsStrings();
c.IncludeXmlComments(string.Format(#"{0}\EmployeeService.xml", System.AppDomain.CurrentDomain.BaseDirectory));
c.SwaggerDoc("v1", new Info
{
Version = "v1",
Title = "Employee Service"
});
});
...
}
And my Configure method looks like this:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Employee Service API");
});
app.UseMvc();
}
I've tried this with and without adding the RoutePrefix to the SwaggerUI section.
As I mentioned, I'm running .Net Core 2.0.3, I have the Nuget package Swashbuckle.AspNetCore 2.3.0 referenced.
What I get in the app regardless of what I do with the path is a 404 on the /swagger/v1/swagger.json file when I try to access {server}/{virtualdirectory}/swagger. The UI loads, but it won't load the json file, as it always tries to find it at server root.
Can someone point me in the right direction?
You must check your [http] route , dont use [routes] before http`s tag.
you must add a api route on the top and remove all routes before them Http:
[Route("api/[controller]")]
public class asteriskAPI:Controller
{ ........
}
and like this:
[HttpGet]
public ActionResult<List<ast_cel>> GetAll()
{
...
}
You need to change your app.UseSwaggerUI method to this
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("./swagger/v1/swagger.json", "Employee Service API");
});
Note the period.
Source: https://learn.microsoft.com/en-us/samples/aspnet/aspnetcore.docs/getstarted-swashbuckle-aspnetcore/

Localize an ASP.NET Core MVC app

I'm struggling with getting my app to localize strings properly. Feel that I've searched every corner of the web without finding something that works that I expect it to.
I use the RouteDataRequestCultureProvider and first problem I'm trying to solve is to be able to use "short hand version" of the culture, e.g. sv instead of sv-SE and that sv is treated as sv-SE when the culture is created. This doesn't happen automatically.
Second is just getting the app to show a localized string. Here is how I configure the localization
public static IServiceCollection ConfigureLocalization(this IServiceCollection services)
{
services.AddLocalization(options =>
{
options.ResourcesPath = "Resources";
});
services.Configure<RequestLocalizationOptions>(options =>
{
var supportedCultures = new[]
{
new System.Globalization.CultureInfo("sv-SE"),
new System.Globalization.CultureInfo("en-US")
};
options.DefaultRequestCulture = new RequestCulture(culture: "sv-SE", uiCulture: "sv-SE");
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
options.RequestCultureProviders = new[]
{
new RouteDataRequestCultureProvider
{
Options = options
}
};
});
return services;
}
And then in Configure I do app.UseRequestLocalizationOptions(); which inject the options previously configured.
In the folder Resources I've created the resources files Resource.resx and Resource.sv.resx.
In my view file I've tried both injecting IStringLocalizer (which fails since no default implementation is registered) and IStringLocalizer<My.Namespace.Resources.Resource> but none of the options works. I've also tried to old fashion way #My.Namespace.Resources.Resource.StringToLocalize
Is it impossible to have a shared resource file? Don't want to resort to Resource\Views\ViewA.resx and Resource\Controllers\AController.resx.
Thanks
So I got this to work. Inside my Resource folder I have my Lang.resx-files with public access modifier.
I found this and added a class Lang (named to file LangDummy not to conflict with resource files) that belongs to the root namespace of the project.
Then in my view imports file #inject Microsoft.Extensions.Localization.IStringLocalizer<My.Namespace.Lang> StringLocalizer and use it #StringLocalizer["PropertyInResxFile"]. Ideally I would like to use the generated static properties for type safety and easier refactoring and using nameof everywhere just feels bloated.
This works for localization via data annotation attributes as well but here I use ResourceType = typeof(My.Namespace.Resources.Lang), i.e. the class for the resx file