Can Razor Class Library pack static files (js, css etc) too? - c#

Maybe duplicate of this already, but since that post does not have any answer, I am posting this question.
The new Razor Class Library is awesome, but it cannot pack libraries files (like jQuery, shared CSS).
Can I somehow reuse the CSS across multiple Razor Page projects, either using Razor Class Library or anything else (my purpose is that, multiple websites use the same CSS, and a single change applies to all projects).
I have tried creating the folder wwwroot in the Razor Class Library project, but it does not work as expected (I can understand why it should not work).

Ehsan answer was correct at the time of asking (for .NET Core 2.2), for .NET Core 3.0 onwards (including .NET 7 when I update this link), RCL can include static assets without much effort:
To include companion assets as part of an RCL, create a wwwroot folder in the class library and include any required files in that folder.
When packing an RCL, all companion assets in the wwwroot folder are automatically included in the package.
The files included in the wwwroot folder of the RCL are exposed to the consuming app under the prefix _content/{LIBRARY NAME}/. For example, a library named Razor.Class.Lib results in a path to static content at _content/Razor.Class.Lib/.

You need to embed your static assets into your Razor Class Library assembly. I think the best way to get how to do it is to take a look at ASP.NET Identity UI source codes.
You should take the following 4 steps to embed your assets and serve them.
Edit the csproj file of your Razor Class Library and add the following lines.
<PropertyGroup>
....
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
....
</PropertyGroup>
<ItemGroup>
....
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.1.2" />
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="2.1.1" />
<PackageReference Include="Microsoft.NET.Sdk.Razor" Version="$(MicrosoftNETSdkRazorPackageVersion)" PrivateAssets="All" />
.....
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="wwwroot\**\*" />
<Content Update="**\*.cshtml" Pack="false" />
</ItemGroup>
In your Razor Class Library, create the following class to serve and route the assets. (it assumes your assets are located at wwwroot folder)
public class UIConfigureOptions : IPostConfigureOptions<StaticFileOptions>
{
public UIConfigureOptions(IHostingEnvironment environment)
{
Environment = environment;
}
public IHostingEnvironment Environment { get; }
public void PostConfigure(string name, StaticFileOptions options)
{
name = name ?? throw new ArgumentNullException(nameof(name));
options = options ?? throw new ArgumentNullException(nameof(options));
// Basic initialization in case the options weren't initialized by any other component
options.ContentTypeProvider = options.ContentTypeProvider ?? new FileExtensionContentTypeProvider();
if (options.FileProvider == null && Environment.WebRootFileProvider == null)
{
throw new InvalidOperationException("Missing FileProvider.");
}
options.FileProvider = options.FileProvider ?? Environment.WebRootFileProvider;
var basePath = "wwwroot";
var filesProvider = new ManifestEmbeddedFileProvider(GetType().Assembly, basePath);
options.FileProvider = new CompositeFileProvider(options.FileProvider, filesProvider);
}
}
Make the dependent web application to use your Razor Class Library router. In the ConfigureServices method of Startup Class, add the following line.
services.ConfigureOptions(typeof(UIConfigureOptions));
So, now you can add a reference to your file. ( let's assume it's located at wwwroot/js/app.bundle.js).
<script src="~/js/app.bundle.js" asp-append-version="true"></script>

In .NET Core 3.1, RCL includes assets inside wwwroot folder to consuming app under _content/{LIBRARY NAME}.
We can change _content/{LIBRARY NAME} path to different path name by editing RCL project propeties and placing StaticWebAssetBasePath.
PropertyGroup>
<StaticWebAssetBasePath Condition="$(StaticWebAssetBasePath) == ''">/path</StaticWebAssetBasePath>
</PropertyGroup>
Now you can access files with /path/test.js.

Thanks for the helpful information Ehsan.
Here is an expanded version to allow for debugging javascript and typescript as well has being able to make changes without recompile. TypeScript debugging isn't working in Chrome but is in IE. If you happen to know why please post a response. Thanks!
public class ContentConfigureOptions : IPostConfigureOptions<StaticFileOptions>
{
private readonly IHostingEnvironment _environment;
public ContentConfigureOptions(IHostingEnvironment environment)
{
_environment = environment;
}
public void PostConfigure(string name, StaticFileOptions options)
{
// Basic initialization in case the options weren't initialized by any other component
options.ContentTypeProvider = options.ContentTypeProvider ?? new FileExtensionContentTypeProvider();
if (options.FileProvider == null && _environment.WebRootFileProvider == null)
{
throw new InvalidOperationException("Missing FileProvider.");
}
options.FileProvider = options.FileProvider ?? _environment.WebRootFileProvider;
if (_environment.IsDevelopment())
{
// Looks at the physical files on the disk so it can pick up changes to files under wwwroot while the application is running is Visual Studio.
// The last PhysicalFileProvider enalbles TypeScript debugging but only wants to work with IE. I'm currently unsure how to get TS breakpoints to hit with Chrome.
options.FileProvider = new CompositeFileProvider(options.FileProvider,
new PhysicalFileProvider(Path.Combine(_environment.ContentRootPath, $"..\\{GetType().Assembly.GetName().Name}\\wwwroot")),
new PhysicalFileProvider(Path.Combine(_environment.ContentRootPath, $"..\\{GetType().Assembly.GetName().Name}")));
}
else
{
// When deploying use the files that are embedded in the assembly.
options.FileProvider = new CompositeFileProvider(options.FileProvider,
new ManifestEmbeddedFileProvider(GetType().Assembly, "wwwroot"));
}
_environment.WebRootFileProvider = options.FileProvider; // required to make asp-append-version work as it uses the WebRootFileProvider. https://github.com/aspnet/Mvc/issues/7459
}
}
public class ViewConfigureOptions : IPostConfigureOptions<RazorViewEngineOptions>
{
private readonly IHostingEnvironment _environment;
public ViewConfigureOptions(IHostingEnvironment environment)
{
_environment = environment;
}
public void PostConfigure(string name, RazorViewEngineOptions options)
{
if (_environment.IsDevelopment())
{
// Looks for the physical file on the disk so it can pick up any view changes.
options.FileProviders.Add(new PhysicalFileProvider(Path.Combine(_environment.ContentRootPath, $"..\\{GetType().Assembly.GetName().Name}")));
}
}
}

Please take note that this solutions provided will only work for server side applications. If you are using Blazor client side it will not work. To include static assets on Blazor client side from a razor class library you need to reference directly the assets like this:
<script src="_content/MyLibNamespace/js/mylib.js"></script>
I wasted hours trying to figure out this. Hope this helps someone.

There's a simpler solution: in your RCL's project, you can flag the wwwroot to be copied to the publish directory:
<ItemGroup>
<Content Include="wwwroot\**\*.*" CopyToPublishDirectory="Always" />
</ItemGroup>
When you deploy an app that depends on the RCL, all the files are accessible as expected. You just need to be careful that there's no naming conflict.
Caveat: this only works when deploying on Azure, but not on your local machine (you'll need a provider for that).

Addition to the answer of #revobtz
The name of my project file is different from my namespace, so I found out that is should be the name of the project file instead of the namespace:
<script src="_content/<Name of project file>/js/mylib.js"></script>

Related

ABP localization resources does not return the intended value inside a unit test

After creating and adding a resource in a module:
Configure<AbpLocalizationOptions>(options =>
{
options.Resources
.Add<TestResource>("en")
.AddVirtualJson("/Path/To/Resource");
});
the json:
{
"culture": "en",
"texts": {
"WelcomeMessage": "Hello {0}! welcome to this test application!",
"TestText": "A test text in another resource defined in some module"
}
}
using it inside an app service or a class is working fine and returns the specified value inside the json file when I run the application through HttpApi.Host project:
//injecting IStringLocalizer<TestResource> from ctor
string text = _localizer["TestText"];
//text will be: "A test text in another resource defined in some module"
but when I try to use it inside a unit test, it returns default value (the given key) as if the resource is not even registered:
[Fact]
public void should_get_text_in_TestResource()
{
var localizer = GetRequiredService<IStringLocalizer<TestResource>>();
string text = localizer["TestText"];
text.ShouldBeEquivalentTo("A test text in another resource defined in some module");
}
But the weird thing is that the default resource that comes with template, it works just fine inside the unit tests:
[Fact]
public void should_get_text_in_AbpTestLocalizationResource()
{
var localizer = GetRequiredService<IStringLocalizer<AbpTestLocalizationResource>>();
string text = localizer["TestText"];
text.ShouldBeEquivalentTo("A test text in default localization resource");
}
So I think it has to do with adding resources inside the unit test modules but I could not figure it out.
I created a sample of the problem that I have in the github
(Unit tests are inside the AbpTestLocalization.Application.Tests project)
Note:
localized resources are not something that I want to test directly and it is not even necessary, my main question is how to make a class testable if it depends on resources other than the default resource? (An acutal use case is the TestTemplateProvider class inside the sample project). How come that the default resource gets recognized inside unit tests and not the others?
It might be that the content of your "ProjectName.Domain.Shared.csproj" is erroneous.
We had the same issue, and fixed it by making sure the localization JSON files are embedded, and the reference to "Microsoft.Extensions.FileProviders.Embedded" is included:
<ItemGroup>
<EmbeddedResource Include="Localization\ProjectName\*.json" />
<Content Remove="Localization\ProjectName\*.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="6.0.5" />
</ItemGroup>

Blazor localization in class library

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

How to separate asp.net core mvc project into multiple assembly (.dll)?

How to separate asp.net core mvc project into multiple assembly (.dll)?
I have 3 projects
MyApp Project
Controllers
HomeController.cs
Models
Views
Home
Index.cshtml
HR Project
Controllers
EmployeeController.cs
Models
EMPLOYEE.cs
Views
Employee
Index.cshtml
ACC Project
Controllers
ChartAccountController.cs
Models
ACCOUNT.cs
Views
ChartAccount
Index.cshtml
I want to compile into dll
HR Project
HR.dll
HR.Views.dll
ACC Project
ACC.dll
ACC.Views.dll
I want to add reference those dll (HR.dll, HR.Views.dll, ACC.dll, ACC.Views.dll) into MyApp Project.
And then run MyApp project can access Employee & Chart Account module too.
*** Original Answer (Update below)
If you want to do this you'll need to do the following 2 steps:
You need to load the controllers from the external dlls
There is already a solution for this on stackoverflow: How to use a controller in another assembly in ASP.NET Core MVC 2.0?
It says the following:
Inside the ConfigureServices method of the Startup class you have
to call the following:
services.AddMvc().AddApplicationPart(assembly).AddControllersAsServices();
You need to load the your compiled Razor views, I guess this is what you have in your HR.Views.dll and ACC.Views.dll.
There is also already a solution for this on stackoverflow:
How CompiledRazorAssemblyPart should be used to load Razor Views?
Loading Razor Class Libraries as plugins
This is one possible solution from the links above:
What you need to do is:
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
.ConfigureApplicationPartManager(ConfigureApplicationParts);
and configure the parts like this
private void ConfigureApplicationParts(ApplicationPartManager apm)
{
var rootPath = HostingEnvironment.ContentRootPath;
var pluginsPath = Path.Combine(rootPath, "Plugins");
var assemblyFiles = Directory.GetFiles(pluginsPath, "*.dll", SearchOption.AllDirectories);
foreach (var assemblyFile in assemblyFiles)
{
try
{
var assembly = Assembly.LoadFile(assemblyFile);
if (assemblyFile.EndsWith(".Views.dll"))
apm.ApplicationParts.Add(new CompiledRazorAssemblyPart(assembly));
else
apm.ApplicationParts.Add(new AssemblyPart(assembly));
}
catch (Exception e) { }
}
}
If you also have javascript and css files in our separate MVC projects, you will need to embed this to your dll otherwise your main app wont see it. So in your HR and ACC project, you'll need to add this in your .csproj file:
<ItemGroup>
<EmbeddedResource Include="wwwroot\**" />
</ItemGroup>
And just to be clear, I agree with the other comments, I don't think it is a good architecture, but it is possible to do it if you want.
*** Updated (Worked)
Just a step:
Edit Startup.cs in ConfigureServices method
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2).ConfigureApplicationPartManager(ConfigureApplicationParts);
and method:
private void ConfigureApplicationParts(ApplicationPartManager apm)
{
var rootPath = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
var assemblyFiles = Directory.GetFiles(rootPath , "*.dll");
foreach (var assemblyFile in assemblyFiles)
{
try
{
var assembly = Assembly.LoadFile(assemblyFile);
if (assemblyFile.EndsWith(this.GetType().Namespace + ".Views.dll") || assemblyFile.EndsWith(this.GetType().Namespace + ".dll"))
continue;
else if (assemblyFile.EndsWith(".Views.dll"))
apm.ApplicationParts.Add(new CompiledRazorAssemblyPart(assembly));
else
apm.ApplicationParts.Add(new AssemblyPart(assembly));
}
catch (Exception e) { }
}
}
What you're wanting is not possible, or perhaps better said: it's not a good idea, for sure.
In general, you need to use Razor Class Libraries. All common functionality would then go into those RCLs. Your entire HR and ACC projects could be RCLs, unless you need to run those independently, as full web apps, as well. In which case, you'd need a structure more like:
HR RCL
HR Web App (depends on HR RCL)
ACC RCL
ACC Web App (depends on ACC RCL)
Main App (depends on HR RCL and ACC RCL)
In either case, you'd put all the controllers, views, and static resources that need to be shared in the RCL, so if you do have actual HR/ACC web apps, those would be pretty light: mostly just consisting of Program and Startup and a dependency on their respective RCLs.
For more information, see the documentation on Razor Class Libraries.

ASPNet Core 2.1 correct way to load precompiled views

I am trying to add a plugin like functionality to my application and having hard time making the precompiled views to be found.
So lets say i have a Razor Class Library that compiled to plugin.dll and plugin.views.dll
I am successfully load and add plugin.dll
Assembly PLUGIN_ASSEMBLY = null;
try
{
PLUGIN_ASSEMBLY = Assembly.LoadFile(PLUGIN.PluginFileName);
Assembly.LoadFile(PLUGIN.PluginViewsFileName);
}
catch (FileLoadException)
{
throw;
}
Then the assembly is added with
MVC_BUILDER.AddApplicationPart(PLUGIN_ASSEMBLY);
Then i add the plugin base path so its normal views would be discovered
MVC_BUILDER.AddRazorOptions(o =>
{
IFileProvider physicalProvider = new PhysicalFileProvider(PLUGIN.BasePath);
IFileProvider compositeProvider = new CompositeFileProvider(physicalProvider);
o.FileProviders.Add(compositeProvider);
});
All above works fine except that i can only use the physically located views and not the ones from plugin.views.dll
What would be correct approach to add the views.dll and make the views discovered?
I spent all day to make it work.. and it worked.
In web app razor knew where from take precompiled views, but in console app it doesn't (maybe it my fault). Let's help him :)
At first, we need name of assembly with views:
var viewAssembly = Assembly.Load(PLUGIN_ASSEMBLY.GetName().Name + ".Views");
Second, we should create provider, that will extract all compiled views from assembly:
var viewAssemblyPart = new CompiledRazorAssemblyPart(viewAssembly);
And last, but not least - add it to collection of other providers:
MVC_BUILDER.PartManager.ApplicationParts.Add(viewAssemblyPart);
Enjoy!
Special thanks to sources from github :)

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