Can Razor Class Library pack static files (js, css etc) too?
Asked Answered
M

7

47

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).

Muticous answered 31/7, 2018 at 9:51 Comment(6)
I dunno if its possible with RCL per see, but its possible - with a bit of effort - with regular class libraries. OpenIddict did that once (embedding static files inside the library). See my answer here (its a bit dated, should still apply or point you to the right direction). Basically Static Files middleware with a specific file provider, the EmbeddedFileProvider)Jamilajamill
@Jamilajamill that's brilliant. Yeah once you mention File middleware, I understand how now. Thank you :)Muticous
Though I should note, I personally wouldn't embed jQuery & js libraries into the class library, for the reason that security updates of the libraries become a pain, you need to update the library, then the project using it rather than doing a simple npm audit fix on a per project basis, so you may end up with a lot of versions "noise" of the that shared class library, when for every security update you need to increment the version of the packageJamilajamill
Thanks. My idea wouldn't really be "embed", but serving static files from a shared folder (let say ../../shared_wwwroot/). This way changing a single place and applies to all websites.Muticous
Assuming your don't intend to use Docker or distributed apps. When they run on different machine, this approach has its flaws. Actually the best way to share js libraries and common css (i.e. bootstrap, jquery-ui css) is using an CDN network, such as Akamai, or Azure CDN). This way you not only have the files available/linkable from everywhere, but also increased performance. if User A visited some other website (not belonging to you) which loaded jQuery 3.0 and then comes to your site which also uses jQuery 3.0 he has no additional download since its cached in his browserJamilajamill
This increases the responsibility of the first time visit of your website as well as reduce the overall traffic and since its CDN always the closest/fastest mirror will be used to ship the static fileJamilajamill
M
39

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/.

Muticous answered 10/9, 2019 at 15:53 Comment(8)
So this means that if we use images inside components in the RCL we also need to add this _content/{LIBRARY NAME} ? That's very non intuitive and unexpected. I would expect the framework to translate this automatically for usages inside the RCL itself.Embarrassment
Correct, but I think it's not too bad. I usually just rename the RCL Assembly name and use short name like "Common"Muticous
Intellisense suggests to use img/foo.png for a MyRazorClassLibrary/wwwroot/img/foo.png. I agree it's not very intuitive having to reference assets with _content/MyRazorClassLibrary/img/foo.png Does anybody have any trick to make intellisense like those paths or to automatically apply that ugly prefix to all assets?Catachresis
For some reason this is not working for me, even though I read and followed the same documentation.Acrocarpous
@Acrocarpous Did you happen to find the cause for this? This solution doesn't seem to work if you are requiring static files inside the RCL, but do not want to explicitly reference them in the consuming project.Akers
@Luke Vo answer is correct, but for me it hadn't worked until I add "/" at the beginning or <base href="/" />Toxicology
@Toxicology that's because without it, browser is requesting files with relative path to current pages, so the file cannot be found.Muticous
@Toxicology Thanks very Mach, I was added <base href="/" /> work for me :).Luxurious
F
28

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.

  1. 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>
    
  2. 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);
        }
    }
    
  3. 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));
    
  4. 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>
    
Folderol answered 27/10, 2018 at 18:13 Comment(9)
Wow this is something new. Sorry the problem was asked a long time ago so now I don't have time to test your answer yet. Can you confirm it works? Will mark it if it is so. Look very nice!Muticous
Finally have another project where I can test this. Worked wonderfully. asp-append-version="true" won't work for obvious reason, but it's a simple problem.Muticous
If I may ask, what did you do to work around asp-append-version="true" not working?Diplegia
@Diplegia I use this one: #53518744Muticous
@LukeVo Thanks! I will check it out.Diplegia
@EhsanMirsaeedi hi I added the answer for .NET Core 3, can you add it to your answer?Muticous
@Diplegia if you are still interested and can use .NET Core 3, asp-append-version works if you use the way written in the document.Muticous
Thanks, it was being excluded in the csproj file, that was the trick!Ticking
@LukeVo that link id now dead :( I don't suppose you recall the content/solution?Heliolatry
S
28

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.

Sundew answered 3/1, 2020 at 7:39 Comment(8)
Hi, welcome to StackOverflow. Thanks for the update, but can you provide a demo code and if any, the documentation of that?Muticous
Just wanted to say this works for me. I bascially added the StaticWebAssetBasePath line to the first PropertyGroup Element in the RCL Project File. Just be aware that the path "_content/{LIBRARY NAME}" will still work as well. Best reference I've found related to this: github.com/dotnet/aspnetcore/issues/14568Reasoned
this sounds great but seems to do nothing for meComp
(well, it does seem to do something since it broke my links, but it doesn't allow me to access resources at the path I specified).Comp
This did not work for me on the first try, but eventually I got it working. There were many things that I had to change from my RCL and consuming app (which I did not even know were wrong) before it started to work. Most of the things I had to remove were things gathered from (now) outdated tutorials for previous versions of NET Core that do not apply/are not needed anymore.Embarrassment
If it helps, I have posted an example of my working .csproj configuration here: github.com/dynamic-vml/dvml/blob/master/src/DynamicVML.csprojEmbarrassment
If it helps, this is the format of my script tag <script src="~/_content/thelibraryname/test.js"></script>Rupee
thanks! worked like a charm: <StaticWebAssetBasePath Condition="$(StaticWebAssetBasePath) == ''">/</StaticWebAssetBasePath> So you dont need any prefix just /myFile.jsFeat
C
3

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}")));
        }
    }
}
Compote answered 23/2, 2019 at 19:49 Comment(0)
O
2

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.

Odrick answered 11/9, 2019 at 16:29 Comment(0)
S
1

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).

Sarcoid answered 31/7, 2019 at 9:2 Comment(0)
V
0

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>
Vedavedalia answered 9/2, 2021 at 6:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.