Nuget - publishing d.ts files in ASP.Net Core Nuget packages
Are you trying to create a Nuget package to distribute both static files (CSS, Javascript etc) and your C# assemblies to your ASP.Net Core project? Officially, this isn’t supported, but I’ve come up with a pretty good workaround…
I’ve been re-working Blackball’s internal front-end architecture and one of the technologies I wanted to use was the new TagHelpers in ASP.Net Core. This will be a great mechanism for standardizing our HTML across our clients, while keeping things 100% flexible should our client use a third-party designer.
In addition to the Tag Helpers, our library also includes a number of Typescript Definition files. All of these work together to provide our framework as a whole.
The problem
So far so good, but when it came to rolling things up in a Nuget package, I hit a wall - Nuget will include my (compiled) Tag Helpers without problem, but the static files .d.ts files display only as links in the project that consumes the Nuget package - even if I have packed them as “content files” in Nuget.
This is a problem because the Typescript compiler requires the physical presence of files on disk - a virtual link will not suffice.
Here is a screenshot of a CSS file, for example:
In particular, note that:
there is a tiny blue arrow on the icon, indicating that this is a virtual linked file
the full path actually refers to the Nuget repo hosted on my computer
What this means is that the project compiles properly, but when the browser makes a request to /css/foundation/foundation.css, we get a 404.
Now, with a CSS file, you can use ASP.Net Core’s ManifestEmbeddedFileProvider to serve the content, but with .d.ts this won’t do because, again, the Typescript compiler does not route through the ASP.Net Core pipeline - it requires the physical files on disk.
This meant that I simply could not deploy my .d.ts files via Nuget.
The fix
I tried all combinations of PackageReference, Nuget, Nuspec, dotnet pack, nuget pack, contentFiles tag, files tag - everything I could find online. But it got me nowhere and more and more I kept seeing that Nuget is not intended to deploy content files - for that you should use npm or similar.
The problem with using npm is:
my architecture specifically includes both server-side and client-side modules. If I reversion something, my developers would need to remember to upgrade both their npm and Nuget dependencies - keeping them precisely in sync
I can’t be bothered maintaining an npm package in addition to my Nuget packages
So, I came up with this handy piece of code which does the trick…
Step one - indicate the files you want to publish by setting their Build Action to ‘Embedded resource’
Step two - exclude the files you do not want, by setting their Build Action to ‘None’
Step three - use the built-in Project -> Properties -> Pack to configure your Nuget package
You do not need to create a .nuspec file.
Step four - create your Nuget package using dotnet pack
Here is the command line I used, yours may differ. Note that we are referring to the .csproj file - not a .nuspec file:
dotnet.exe pack "Foundation.UI.Web.Core.csproj" --configuration Release
At this point, you now have a Nuget package file which contains all your static files embedded in the assemblies. So, how do we get them out?
Extracting the embedded content in your consuming package
The basic idea is that you install the Nuget package in your consuming project, then on Startup, you interrogate the Nuget DLL, extract the Embedded resources and save them to disk - right into your project structure. Here is the code that does it:
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; namespace Foundation.UI.Web.Core.Code { public class EmbeddedResourceUnpacker { /// <summary> /// Examines the Foundation DLL and creates files on disk for each of them /// </summary> /// <returns></returns> public async Task UnpackFiles(IHostingEnvironment env) { // We only need to do this in Development mode. The assumption being that the developer will have unpacked the correct Foundation // version and these files will be committed to source control etc, just like normal files if (!env.IsDevelopment()) return; var foundationAssembly = typeof(Foundation.UI.Web.Core.Code.Library).GetTypeInfo().Assembly; var assemblyName = foundationAssembly.GetName().Name; // Iterate over each embedded resource var names = foundationAssembly.GetManifestResourceNames(); foreach (var name in names) { var filePath = name; // Embedded files are prefixed with the full namespace of the assembly, so your file is stored at wwwroot/foundation.css, then // your embedded name is Foundation.UI.Web.Core.wwwroot.foundation.css // Here, we strip the assembly name from the start - note the following '.' too filePath = filePath.Replace(assemblyName + ".", ""); // Parse file path filePath = filePath.Replace(".", "\\"); // Reset files - order is important!! filePath = this.ResetFileExtension(filePath, ".cshtml"); filePath = this.ResetFileExtension(filePath, ".min.css"); filePath = this.ResetFileExtension(filePath, ".css"); filePath = this.ResetFileExtension(filePath, ".d.ts"); filePath = this.ResetFileExtension(filePath, ".min.js"); filePath = this.ResetFileExtension(filePath, ".js"); filePath = this.ResetFileExtension(filePath, ".otf"); filePath = this.ResetFileExtension(filePath, ".eot"); filePath = this.ResetFileExtension(filePath, ".svg"); filePath = this.ResetFileExtension(filePath, ".ttf"); filePath = this.ResetFileExtension(filePath, ".woff"); filePath = this.ResetFileExtension(filePath, ".png"); filePath = this.ResetFileExtension(filePath, ".jpg"); filePath = this.ResetFileExtension(filePath, ".gif"); filePath = this.ResetFileExtension(filePath, ".ico"); // Now prepend the root path of this application, on disk filePath = System.IO.Path.Combine(env.ContentRootPath, filePath); var directory = System.IO.Path.GetDirectoryName(filePath); System.IO.Directory.CreateDirectory(directory); // Copy using (var resource = Assembly.GetExecutingAssembly().GetManifestResourceStream(name)) { using (var file = new FileStream(filePath, FileMode.Create, FileAccess.ReadWrite)) { resource.CopyTo(file); } } } } /// <summary> /// Helper routine /// </summary> /// <param name="fileName"></param> /// <param name="requiredExtension"></param> /// <returns></returns> private string ResetFileExtension(string fileName, string requiredExtension) { var encodedExtension = requiredExtension.Replace(".", "\\"); if (!fileName.EndsWith(encodedExtension)) return fileName; fileName = fileName.Substring(0, fileName.Length - encodedExtension.Length) + requiredExtension; return fileName; } } }
It’s actually a pretty simple class. Of note:
there’s lots of room for improvement. You could customize the file location you save to, for example
I recommend including this file in the Nuget package itself. After all, it is deployed to your consuming project along with the static files. That’s what I have done.
it only adds files. With a bit more time, you could track what files were added, and then remove them if an updated Nuget package no longer contained them
it overwrites files every time, which adds a big performance hit. With a small project, you don’t notice, but ultimately I’ll need to add a CRC file or similar to avoid constantly rewriting over existing
it only needs to be called when in developer mode.
Finally, you can execute this class wherever you like. For now, I’ve created an extension method (again, in my Nuget package)…
public static void AddFoundation(this IServiceCollection services, IHostingEnvironment environment) { var unpack = new EmbeddedResourceUnpacker(); var task = unpack.UnpackFiles(environment); Task.WaitAll(task); }
…and then call it in your Startup class:
using Foundation.UI.Web.Core.Code.Config; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; namespace Web.Core { public class Startup { private IHostingEnvironment Environment; public Startup(IHostingEnvironment environment) { this.Environment = environment; } public void ConfigureServices(IServiceCollection services) { services.AddFoundation(this.Environment); services.AddMvc(); } } }
And that’s it!
It’s frustrating and weird that Nuget doesn’t seem to want to support content files and assemblies. But oh well, here we are. I hope this class helps some other people in my situation.