Blazor localization in class library - c#

I'm building up a server side Blazor application. Inside this Blazor app I already have the localization running using the "IStringLocalizer" mechanism.
Now my project has grown and I want to reuse components, so I created a razor class library where I put all my reusable components in.
Solution
|
|-- VISU (the Blazor Project project)
|
|-- UI.Components (RCL project containing the components)
The components contain some text which must be translated dependent on the actual culture.
At the RCL I did following.
a) I create a folder "Resources"
UI.Components (RCL Project)
|
|- Resources
b) inside the "Resource Folder" I create the resource "UIComponentText.resx" file containing the translation
c) inside the "Resource" folder I create a file containing the dummy class "UIComponentText"
Furthermore I create a service class "UIComponentLocalizer"
public class UIComponentText
{
}
public class UIComponentLocalizer
{
private readonly IStringLocalizer _localizer;
public UIComponentLocalizer(IStringLocalizerFactory factory)
{
var type = typeof(UIComponentText);
var assemblyName = new AssemblyName(type.GetTypeInfo().Assembly.FullName);
_localizer = factory.Create("UIComponentText", assemblyName.Name);
var all = _localizer.GetAllStrings(false); // test to see content
}
public LocalizedString this[string key] => _localizer[key];
public LocalizedString GetLocalizedString(string key)
{
return _localizer[key];
}
}
d) Inside startup.cs of the Blazor app, I register the service
services.AddSingleton<UIComponentLocalizer>();
e) Inside a component I inject the service to make use of it
[Inject] public UIComponentLocalizer CompTranslate { get; set; }
--> Finally it does not work. :-(
The UIComponentLocalizer service is started, but the dictionary is empty.
Actually, I'm thinking that the access to the resource file is wrong.
The component resource has the namespace "UI.Components.Resources.UIComponentText" which is not
like the resources inside the Blazor project ("VISU.Resources.xyz")
Questions:
a) is it possible to create a RCL which includes localization which can be "includes" inside a Blazor serve side app.
b) If yes? Where do I make a mistake
Thanks
Wolfgang

Try to add your Resource file via the builder.Services:
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
And go from there. Maybe this helps too: Blazor Localization

Related

Access configuration object (appsettings.json) in library project

I started a .net core web api project.
I have one solution with the main project and many other project like DAL, Common etc.
I want to create a class in the Common project that will share configuration settings(appsettings.json) between my different library project.
So, How can I acces Iconfiguration from that class ?
Is this a good way to do this ? Or should I create a class into the main project to get configuration data from other project ?
One way to do it would be to create a Configuration Model based on your section/subsection from your appsettings.json file, add the configuration section in your Startup.cs class, then you can use the built-in dependency injection to inject the configuration anywhere you would need it.
For Example
appsettings.json :
{
"MyConfiguration" : {
"Configuration1" : "1",
}
}
Your configuration model:
public class MyConfiguration
{
public int Configuration1 { get; set; }
}
Then in the StartupClass in the ConfigureServices method you have to add
services.Configure<MyConfiguration>(configuration.GetSection("MyConfiguration"));
services.AddOptions();
And when you want to use that Configuration somewhere in the code, you would just inject it in the constructor like so:
public class SomeClass
{
private MyConfiguration MyConfiguration { get; }
public SomeClass(IOptions<MyConfiguration> myConfigurationOptions)
{
MyConfiguration = myConfigurationOptions.Value;
}
//... more code
}
Then you can access it
MyConfiguration.Configuration1
You can add the file as a link in yout project.
Right click your project , then select "Add" ->"Existing Item..."
Choose the file that you want to add to the solution
Instead of hitting Enter or clicking the Add button, you need to click the
down-arrow icon at the right edge of the Add button, and select "Add As Link".
Please refer to this link.

is there a way of making use of .json configurations in .net core class library instead of a .config file

I have a class library i want end up packaging into a nuget package, i have some configurations i wish to use which are in a json file (apsettings.json).
I have added this file to my class library project, and at the moment i just want to read the values in the configurations in a constructor without using a .config file, i will like to use a .json similar to how i'll do it in a web project.
I am trying something like this, but i get an error stating that the settings.json file does not exist in \bin\Debug\netcoreapp2.0. It is not in that folder, it is in the root of my project.
public TokenGenerator()
{
var builder = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("settings.json");
_configuration = builder.Build();
_tokenServiceSettings = new TokenServiceSettings()
{
Audience = _configuration.GetSection("JwtBearer:Audience").Value
};
}
At the moment, i have just hard coded the directory as a string inside the SetBasePath method
If you are writing a library that other teams might use in other projects, you should not bind it to another package (Microsoft.Extensions.Configuration) just for configuration.
You should have your own Option class which is a simple class with bunch of properties
public class TokenServiceOptions
{
public TokenServiceOptions()
{
Audience = "Audience_Default_Value!";
}
public string Audience { set; get; }
public string OtherOption1 { set; get; }
public string OtherOption2 { set; get; }
///
}
and let your library users pass an instance of TokenServiceOptions to your method.
It should not be important to a library how options are gathered.
Because this is in a library, this class cannot know where and what json file to read.
Pass the full path to the file to the library or,
Pass a structure as an argument to the library so the calling assembly can pass in the data and store it anywhere and any way it likes or
Get the location of the CallingAssembly (Assembly.GetCallingAssembly().Location) and construct a relative path to that.

Dependency Registrar for other assembly

I have a simple question about dependecy registration.
I'm developing a brand new web application that use Engine Context paradigm with Autofac container. For any library on the solution I have one class implementing IDependencyRegistrar that implement a common Register method, due to add one the container some specific implementation of some interfaces and components.
In this way, a base Core library (running at application startup) provide a RegisterDependencies method that lookup on every Executing Assembly to discover all the DDL's used by the application and registering them on Autofac Container.
The code that provide this behavior is:
builder = new ContainerBuilder();
var drTypes = typeFinder.FindClassesOfType<IDependencyRegistrar>();
var drInstances = new List<IDependencyRegistrar>();
foreach (var drType in drTypes)
drInstances.Add((IDependencyRegistrar) Activator.CreateInstance(drType));
//sort
drInstances = drInstances.AsQueryable().OrderBy(t => t.Order).ToList();
foreach (var dependencyRegistrar in drInstances)
dependencyRegistrar.Register(builder, typeFinder, config);
builder.Update(container);
Where the FindClassOfType<IDependencyRegistrar> works thanks to a Method implementation like that:
public virtual IList<Assembly> GetAssemblies()
{
var addedAssemblyNames = new List<string>();
var assemblies = new List<Assembly>();
if (LoadAppDomainAssemblies)
AddAssembliesInAppDomain(addedAssemblyNames, assemblies);
AddConfiguredAssemblies(addedAssemblyNames, assemblies);
return assemblies;
}
And, AddAssemblyInAppDomain is:
private void AddAssembliesInAppDomain(List<string> addedAssemblyNames, List<Assembly> assemblies)
{
foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
{
if (Matches(assembly.FullName))
{
if (!addedAssemblyNames.Contains(assembly.FullName))
{
assemblies.Add(assembly);
addedAssemblyNames.Add(assembly.FullName);
}
}
}
}
The problem is: when I end up on adding in mysolution the MVC project (the front-end), I've referenced on it only direct accessing library (service layer and some infrastructure components) but no DataLayer components and some other DLL. Due to the fact that MVC not referencing directly some libraries of deep layers, my Engine Context doesn't see the others sub-components and not registering them on the Autofac container, causing a
'no registered services'
exception when execution make explicit request on them.
The whole system just works if I add reference to any library from the MVC project but, for layered architectured application, this is not a best practice: my MVC need to know nothing about DataLayer or others low-layered services.
However, in this way, no ExecutingAssembly are discovered, so, not dependency are registered anymore.
Wich is the best approch to resolve this situation without referencing all assemblies directly from main MVC project?
What you are trying to do is described in Autofac documentation as Assembly Scanning, take a look here. Basically, to get all assemblies in IIS-hosted application you need this piece of code:
var assemblies = BuildManager.GetReferencedAssemblies().Cast<Assembly>();
EDIT:
Ok, so I understand the situation is like this:
Project Web is a MVC web app.
Project Model is a class library where you have your contracts (interfaces) defined, e.g. for DAL, but also for Web.
Project DAL contains some implementations of contracts from Model.
There might be some additional class libraries, but they all uses Model for contracts.
So to sum up - all projects have reference to Model, but they have no references to each other.
I think for every library (except Model) you should create a module. To do so, create a class implementing Module type from Autofac library and override Load method - put all your module registration in there. Then, in Web app start you should load all assemblies and register their modules. But, as you mentioned, assemblies other than Web are not present in bin directory; you should copy them there "manually", for example in Post-Build action (Project Properties -> Build Events -> Post-Build action). The following command should do the work:
xcopy /Y "$(TargetDir)*.dll" "$(ProjectDir)..\{Your Web App}\bin"
Also, in your solution properties you should set, that Web project "depends" on all other projects. It would assure all other libraries would be build before Web. It does not add any reference between these assemblies.
Then, during application startup, you should search for you assemblies in bin folder and register each assembly module, like this:
var containerBuilder = new ContainerBuilder();
containerBuilder.RegisterControllers(typeof(MvcApplication).Assembly);
var libFolder = new DirectoryInfo(HostingEnvironment.MapPath("~/bin"));
var libFiles = libFolder.GetFiles("*.dll", SearchOption.AllDirectories);
foreach (var lib in libFiles)
{
var asm = Assembly.LoadFrom(lib.FullName);
containerBuilder.RegisterAssemblyModules(asm);
}
var container = containerBuilder.Build();
You might want to add some filter to libFolder.GetFiles() to retreive only your assemblies, not all from bin.
If your other assemblies contains Mvc Controllers, you should take a look how to manage the situation here (see Initializer class). Basically, in pre-start of application you would need to add assemblies to BuildManager. Otherwise, the code above should work just fine.
If you are working on a non-web project then my answer might help?
To your Ioc class add a method i.e:
public static void SetIocForTesting(bool forUnitTesting)
{
_testContext = forUnitTesting;
}
Sample container set-up code, delegate out the responsibility of getting the assemblies to load into the builder. i.e GetModules():
public static IContainer Container
{
get
{
if (_container != null)
{
return _container;
}
var builder = new ContainerBuilder();
foreach (var lib in GetModules())
{
builder.RegisterAssemblyModules(lib);
}
_container = builder.Build();
return _container;
}
}
When scanning for Assemblies, switch on the testContext variable:
private static IEnumerable<Assembly> GetModules()
{
if (_testContext)
{
return AppDomain.CurrentDomain.GetAssemblies();
}
var currentPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
if (currentPath == null)
{
throw new NullReferenceException("Unable to build the container because currentPath variable is null.");
}
// XXXX = assign a wild card
var libFolder = new DirectoryInfo(currentPath);
var libFiles = libFolder.GetFiles("XXXX.*.dll", SearchOption.TopDirectoryOnly);
return libFiles.Select(lib => Assembly.LoadFrom(lib.FullName)).ToList();
}
When unit testing your IoC provider and a registration:
protected virtual void GivenThat()
{
IocProvider.SetIocForTesting(true);
}
.. you have a method that switches the IoC to ensure it works correctly with all assemblies referenced and loaded by your test project. The above method lives inside an abstract base class I use for BDD style unit testing.
Your test project usually ends up referencing a lot of assemblies which means resolving services have a higher success rate.
Finally, for non UnitTesting code add a static constructor:
static IocProvider()
{
_testContext = false;
}
This will ensure a default work flow for production code.
Feel free to play with the above format to suit your needs; I hope it helps someone in the way the above question and answer helped me.

Strongly typed Settings class in a services library getting settings from web.config in mvc project

I have a solution with multiple projects. One of them is a Services project and within that most of my backend code resides and thus, the settings for most of those services reside in its app.config file.
I created a class to access all those settings, so they would be available outside the Services project.
public static class ServicesAppSettings
{
/// <summary>
/// Simple App Settings classes for strong typing
/// When creating a new class please ENSURE THAT THE PROPERTY NAME EXACTLY MATCHES THE APPSETTINGS KEY!
/// </summary>
public static class SmsSettings
{
public static string SmsProvider => ConfigurationManager.AppSettings["SmsProvider"];
public static string SmsAccountId => ConfigurationManager.AppSettings["SmsAccountId"];
public static string SmsPassword => ConfigurationManager.AppSettings["SmsAccountPassword"];
public static string SmsFromPhone => ConfigurationManager.AppSettings["SmsFromPhone"];
}
public static class EmailSettings
{
public static string EmailProvider => ConfigurationManager.AppSettings["EmailProvider"];
public static string EmailProviderHost => ConfigurationManager.AppSettings["EmailProviderHost"];
public static string EmailAccount => ConfigurationManager.AppSettings["EmailAccount"];
public static string EmailAccountPassword => ConfigurationManager.AppSettings["EmailAccountPassword"];
public static string GetEmailAddressFromApplication()
{
return string.Format(EmailAccount + "#" + EmailProvider);
}
}
public static class UserIcons
{
public static string MaleUserIconSource => ConfigurationManager.AppSettings["MaleUserIcon"];
public static string FemaleUserIconSource => ConfigurationManager.AppSettings["FemaleUserIcon"];
}
}
and it works perfectly fine EXCEPT when the settings aren't in the WEB.CONFIG file. Why on earth is this drawing from the mvc's web.config file and NOT its own libraries app.config file. Trust me, this is the case. If I remove one setting from web.config and then try to access it in a test it's null, if i put it back in web.config it works.
Anyone know why this is? Can you specify to the ConfigurationManager the name of the file you want to retrieve from?
It happens because only one configuration file is actually accessible - config of your executable project. That's why in mvc application you can read settings from web.config of your executable mvc application. If you would have service application (ms service) it will be app.config of you startup service.
As I know, you can not read settings from another config file.
One the possible solutions is to store your custom settings in .xml file and read it. This is also useful if you want to change your custom settings at a runtime.
Another one (in case you don't want to mess up your settings in appSettings section) is to create your custom config section and use it for your settings
Hope that helps
Web.config is the place to put your settings. Dlls that are part of a .net web solution will use it.

ASP.NET MVC 6: view components in a separate assembly

I'd like to define view components (which are new in ASP.NET MVC 6) in a separate assembly from the MVC 6 web startup project so that I can reuse them in multiple web projects. A sample solution might look like this:
BookStore.Components (houses common view components)
BookStore.Web1 (references BookStore.Components)
BookStore.Web2 (references BookStore.Components)
I created a new Class Library (Package) and created a view component inside. I also created the view following the nested folder convention. My BookStore.Components project looks like this:
When I try to invoke this view component from my web project:
#Component.Invoke("BookOfTheMonth")
...I get a 500 error with an empty content body. It seems like the ViewComponent class is discovered, but the razor view for the component isn't.
I also tried to extend DefaultViewComponentDescriptorProvider so that view components from the BookStore.Components assembly can be discovered:
Defined an AssemblyProvider
public class AssemblyProvider : IAssemblyProvider
{
public IEnumerable<Assembly> CandidateAssemblies
{
get
{
yield return typeof(AssemblyProvider).Assembly;
yield return typeof(BookStore.Components.BookOfTheMonthViewComponent).Assembly;
}
}
}
Registered AssemblyProvider using Autofac
builder.RegisterType<AssemblyProvider>()
.AsImplementedInterfaces();
builder.RegisterType<DefaultViewComponentDescriptorProvider>()
.AsImplementedInterfaces();
I'm not sure if the registration of DefaultViewComponentDescriptorProvider above is needed or not, so I tried with and without it, but I still get a 500 error on a page where the view component is invoked.
How can I invoke a view component that lives in a separate assembly from the MVC6 web project?
Update 2017-03-09
Things have changed a bit in Visual Studio 2017 using MS Build. Luckily it's much simpler. Here's how to get this to work:
In the external assembly, add this to the csproj file:
<ItemGroup>
<EmbeddedResource Include="Views/**/*.cshtml" />
</ItemGroup>
In the main web project, add this NuGet package:
Microsoft.Extensions.FileProviders.Embedded
Then in Startup, add the external assembly to the list of File Providers:
services.Configure<RazorViewEngineOptions>(options =>
{
options.FileProviders.Add(new EmbeddedFileProvider(
typeof(SampleClassInAssembly).Assembly
# Prior to .Net Standard 2.0
# typeof(SampleClassInAssembly).GetTypeInfo().Assembly
));
});
I'll leave the original answer below for now, in case people are still trying to get this to work with older versions of .Net Core and project.json.
================================================================
Here are the steps to make this work.
Make sure your view structure in the components assembly is the same as your web project. Note that there was a mistake in the screenshot that I posted along with my question.
Register CompositeFileProvider in Startup.cs of the web project:
services.Configure<RazorViewEngineOptions>(options =>
{
options.FileProvider = new CompositeFileProvider(
new EmbeddedFileProvider(
typeof(BookOfTheMonthViewComponent).GetTypeInfo().Assembly,
"BookStore.Components"
),
options.FileProvider
);
});
Both CompositeFileProvider and EmbeddedFileProvider are new, so you'll need to get these from the aspnetvnext NuGet feed. I did this by adding this source:
Add the dependencies in project.json:
"Microsoft.AspNet.FileProviders.Composite": "1.0.0-*",
"Microsoft.AspNet.FileProviders.Embedded": "1.0.0-*",
Lastly, add this to the project.json of the Components assembly:
"resource": "Views/**"
That should be enough to get this working.
Here is a working demo:
https://github.com/johnnyoshika/mvc6-view-components/tree/master
This answer was formulated from this discussion here: https://github.com/aspnet/Mvc/issues/3750
Update 2016-01-15
There is currently one painful problem with external view components. Any changes you make to the view cshtml file does not automatically get recompiled. Even a forced Visual Studio clean and rebuild doesn't do it. You need to change a .cs file in the components assembly in order to trigger a view recompilation, but it looks like this is something that will be corrected in the future. The reason for this problem is explained here: https://github.com/aspnet/Mvc/issues/3750#issuecomment-171765303
I have done some researching on Github and found that PhysicalFileProvider (link) IFileInfo GetFileInfo(string subpath) method is used by Razor engine (link) for getting real files to compile.
Current implementation of this method
public IFileInfo GetFileInfo(string subpath)
{
if (string.IsNullOrEmpty(subpath))
{
return new NotFoundFileInfo(subpath);
}
// Relative paths starting with a leading slash okay
if (subpath.StartsWith("/", StringComparison.Ordinal))
{
subpath = subpath.Substring(1);
}
// Absolute paths not permitted.
if (Path.IsPathRooted(subpath))
{
return new NotFoundFileInfo(subpath);
}
var fullPath = GetFullPath(subpath);
if (fullPath == null)
{
return new NotFoundFileInfo(subpath);
}
var fileInfo = new FileInfo(fullPath);
if (FileSystemInfoHelper.IsHiddenFile(fileInfo))
{
return new NotFoundFileInfo(subpath);
}
return new PhysicalFileInfo(_filesWatcher, fileInfo);
}
private string GetFullPath(string path)
{
var fullPath = Path.GetFullPath(Path.Combine(Root, path));
if (!IsUnderneathRoot(fullPath))
{
return null;
}
return fullPath;
}
We can see here that absolute paths nor permitted and the GetFullPath method combines path with Root which is your main web application root path.
So I assume that u can't open ViewComponent from other folder than the current one.
As of .NetCore v3.x:
[Optional] Remove Microsoft.Extensions.FileProviders.Embedded nuget package
Install Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation nuget package
Call .AddRazorRuntimeCompilation(), e.g: services.AddMvc().AddRazorRuntimeCompilation()
Instead of
services.Configure<RazorViewEngineOptions>(options =>
{
options.FileProviders.Add(new EmbeddedFileProvider(
typeof(SampleClassInAssembly).Assembly
));
});
Add this:
services.Configure<MvcRazorRuntimeCompilationOptions>(options =>
{
options.FileProviders.Add(new EmbeddedFileProvider(
typeof(SampleClassInAssembly).Assembly
));
});
And you are good to go.
Related github issue

Categories