Is there a way to add two resource folders to aspnet core localization? - asp.net-core

I'm currently developing the SDK for one project and as a requirement I need to add two resources locations. One will be provided with the SDK lib and another to be provided by the consumer app.
Currently, according to docs, this is how to add localization:
services.AddLocalization(options => options.ResourcesPath = "Resources");
I'm calling this method from my BaseStartup class that will be inherited by the consumer app's Startup class. So I need to be able to setup the location of the SDK's resources folder and the consumer app's one as well.
Maybe something like:
services.AddLocalization(options =>
{
options.ResourcesPath = "SDKResources";
options.FromAssembly = sdkResourcesAssembly;
});
services.AddLocalization(options =>
{
options.ResourcesPath = "AppResources";
options.FromAssembly = appResourcesAssembly;
});
Is this possible? If so, how? If not, is there a workaround?
Checking online and even the source code (https://github.com/aspnet/Localization) wasn't of much help. The only thing I can think of is using IStringLocalizerFactory which accepts an assembly and the name of the file. Would it work? For instance, adding services.AddLocalization() and then just creating a wrapper class that would provide the consumer app with the strings using the factories created using IStringLocalizerFactory?
Thanks!

I found out about two ways it can be done, first by adding resources from different assemblies:
I created a base startup class to handle all source assemblies containing my resource classes and then I load them.
serviceCollection.AddLocalization();
var resourceTypes = typeof(BaseResource<>).Assembly.GetDerivedGenericTypes(typeof(BaseResource<>));
if (typeFromResourceAssembly != null)
resourceTypes.AddRange(typeFromResourceAssembly.Assembly.GetDerivedGenericTypes(typeof(BaseResource<>)));
foreach (var resourceType in resourceTypes)
{
serviceCollection.AddScoped(resourceType, resourceType);
}
return serviceCollection;
Second, by adding different resource folders:
services.Configure<ClassLibraryLocalizationOptions>(
options => options.ResourcePaths = new Dictionary<string, string>
{
{ "ResourceClass", "ResourcesFolder" },
{ "Localization.CustomResourceClass", "Folder1/Folder2" }
}
);

Related

Migration to Minimal API - Test Settings Json not overriding Program

Thanks to this answer: Integration test and hosting ASP.NET Core 6.0 without Startup class
I have been able to perform integration tests with API.
WebApplicationFactory<Program>? app = new WebApplicationFactory<Program>()
.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
});
});
HttpClient? client = app.CreateClient();
This has worked using the appsettings.json from the API project. Am now trying to use integrationtestsettings.json instead using:
IConfiguration configuration = new ConfigurationBuilder()
.SetBasePath(ProjectDirectoryLocator.GetProjectDirectory())
.AddJsonFile("integrationtestsettings.json")
.Build();
WebApplicationFactory<Program>? app = new WebApplicationFactory<Program>()
.WithWebHostBuilder(builder =>
{
builder.ConfigureAppConfiguration(cfg => cfg.AddConfiguration(configuration));
builder.ConfigureServices(services =>
{
});
});
_httpClient = app.CreateClient();
I have inspected the configuration variable and can see the properties loaded from my integrartiontestsettings.json file. However, the host is still running using the appsettings.json from the server project.
Previously, in .Net5, I was using WebHostBuilder and the settings were overridden by test settings.
WebHostBuilder webHostBuilder = new();
webHostBuilder.UseStartup<Startup>();
webHostBuilder.ConfigureAppConfiguration(cfg => cfg.AddConfiguration(_configuration));
But cannot get the test settings to apply using the WebApplicationFactory.
It seems the method has changed.
Changing:
builder.ConfigureAppConfiguration(cfg => cfg.AddConfiguration(configuration));
To:
builder.UseConfiguraton(configuration);
has done the trick.
builder.ConfigureAppConfiguration, now it's configuring the app (after your WebApplicationBuilder.Build() is called) and your WebApplication is created.
You need to "inject" your configurations before the .Build() is done. This is why you need to call UseConfiguraton instead of ConfigureAppConfiguration.

Prevent ASP.NET Core discovering Controller in separate assembly

I've got an ASP.NET Core API that references a nuget package that contains a Controller.
By default, this controller is registered and can respond to requests. However, I only want to add this in certain circumstances - e.g. if it's in the DEV environment.
My Startup looks like this:
services.AddControllers()
.AddMvcOptions(cfg => {
cfg.Filters.Add(new CustomExceptionFilterAttribute())
});
I expected I'd need to call AddApplicationPart(typeof(ClassInPackage).Assembly) after calling AddCointrollers to register this controller?
Can someone advise a way I can enable / disable the registration of this controller?
Ok, I've found a solution - remove the ApplicationPart that contains the Controller. Any other dependencies in the assembly can still be used.
In Startup.cs / wherever you do your IoC:
if(hideControllerFromOtherAssembly)
{
var appPartManager = (ApplicationPartManager)services.FirstOrDefault(a => a.ServiceType == typeof(ApplicationPartManager)).ImplementationInstance;
var mockingPart = appPartManager.ApplicationParts.FirstOrDefault(a => a.Name == "MyMockLibrary.Namespace");
if(mockingPart != null)
{
appPartManager.ApplicationParts.Remove(mockingPart);
}
}
You can also manipulate ApplicationParts via the extension method:
AddMvc().ConfigureApplicationPartManager()
This wasn't suitable for me as I'd written an extension method in my nuget package
https://learn.microsoft.com/en-us/aspnet/core/mvc/advanced/app-parts?view=aspnetcore-5.0

Integrate MiniProfiler with .NetCore 3.1

I want to integrate MiniProfiler is a WebApi or View /XX/results-index.
The WebApi is authenticated with Bearer Tokens. I only want Group Users in Active Directory can see the results, but I don't get it.
I have this code in ServicesCollection:
services.AddMiniProfiler(options =>
{
options.RouteBasePath = "/profiler";
options.ResultsAuthorizeAsync = async request => await GetAuthorization(request); }).AddEntityFramework();
private static async Task<bool> GetAuthorization(HttpRequest request)
{
// var user = request.HttpContext.User.Identity.Name; --> Is null
return true;
}
In Configure Method in StartUp:
app.UseSwagger().UseSwaggerUI(options =>
{
options.SwaggerEndpoint($"/swagger/v1/swagger.json", $"{env.ApplicationName} V1");
options.OAuthClientId("TestApiswaggerui");
options.OAuthAppName("TestApi Swagger UI");
options.IndexStream = () => GetType().GetTypeInfo().Assembly.GetManifestResourceStream(
"TestApi.SwaggerMiniProfiler.html");
})
.UseMiniProfiler();
I want to see mini profiler information through some options:
http://localhost:5050/profiler/results-index --> Show the list methods called
http://localhost:5050/swagger/index.html --> Show the MiniProfiler in the same page
Environment:
.NET Core version: 3.1
MiniProfiler version: MiniProfiler.AspNetCore.Mvc v.4.2.1
Operative system: Windows 10
The piece you're probably missing here is that MiniProfiler shows your results. What's "you" is determined by the UserIdProvider option. When recording and viewing profiles, ensure that these are the same "user ID" (defaults to IP address). It looks like this in options:
services.AddMiniProfiler(options =>
{
options.UserIdProvider = request => ConsistentUserId(request);
});
If your swagger has zero server-side processing at all (e.g. it does not include the MiniProfiler <script> tag from .RenderInludes() or the <mini-profiler /> tag helper, then the issue isn't viewing the profiles so much as not even attempting to view. There are some ideas I have around a static tag without profiles to currently view, but I do not know how to get them into Swagger in it's generation phase (just not familiar enough). Note that it's a blatant hack, but you could work around the issue at the moment with a manual script tag. You'll want to follow https://github.com/MiniProfiler/dotnet/issues/326 for this.
I just want to leave the option of having the traces read for that group from the active directory:
services.AddMiniProfiler(options =>
{
// (Optional) Path to use for profiler URLs, default is /mini-profiler-resources
options.RouteBasePath = "/profiler";
options.ColorScheme = StackExchange.Profiling.ColorScheme.Light;
options.PopupRenderPosition = StackExchange.Profiling.RenderPosition.BottomLeft;
options.PopupShowTimeWithChildren = true;
options.PopupShowTrivial = true;
options.ShouldProfile = ShowProfile;
options.SqlFormatter = new StackExchange.Profiling.SqlFormatters.InlineFormatter();
options.ResultsAuthorize = request => request.HttpContext.User.IsInRole("S-INFORMATICA");
})
.AddEntityFramework();

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

Dynamically loading service providers

I am creating an application with a control panel for three different versions of a server, and they may have different database layouts (that's the reason I am modularizing). I am following this tutorial.
How can I, dynamically, load only the service provider of the corrent server version? Every version has it's own folder, and the version chosen is stored is accessible with Config::get, is it already loaded when the service providers are loaded?
Also, if I use the HMVC architecture, can I still use the default folders (not modules) for application-wide, server-common controllers? (like news, which are not server-dependent).
If I didn't make myself clear, please ask.
As far as I can tell, you cannot dinamically load a Service Provider without doing something odd in your code, but Service Providers are class loaders themselves and you should be using one Service Provider to load, dynamically, your services:
<?php
class ServiceProvider extends Illuminate\Support\ServiceProvider {
protected $defer = true;
public function register()
{
$this->app->bind('server', function($app) {
switch ($this->app['config']['serverVersion']) {
case 'versionX':
return new ServiceClassForServerX();
break;
case 'versionY':
return new ServiceClassForServerY();
break;
default:
return new ServiceClassForDefaultServer();
break;
}
});
}
public function provides()
{
return array('sever');
}
}
About folders, Laravel gives you 100% freedom to choose whatever folder structure you want to use in app/*.