Skip to content

Instantly share code, notes, and snippets.

@jjxtra
Last active July 11, 2019 15:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jjxtra/10ca9170e83525ea007b17bacb214ad8 to your computer and use it in GitHub Desktop.
Save jjxtra/10ca9170e83525ea007b17bacb214ad8 to your computer and use it in GitHub Desktop.
The Microsoft embedded file provider is buggy and difficult to use, requires arcane project settings and fails in unit tests, especially with class libraries and multiple projects. This embedded file provider has no such problems.
// MIT license, https://github.com/jjxtra
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Primitives;
namespace BetterEmbeddedFileProviderNamespace
{
public class BetterEmbeddedFileProvider : IFileProvider, IChangeToken
{
private static readonly string[] extensions = new string[]
{
".min.js",
".min.css",
".json",
".js",
".css",
".xml",
".htm",
".html",
".zip",
".cshtml",
".txt",
".ico",
".png",
".jpg",
".jpeg",
".webp",
".gif",
".svg",
".less",
".scss"
};
private static readonly DummyDisposable dummyDisposable = new DummyDisposable();
private class DummyDisposable : IDisposable
{
public void Dispose() { }
}
private class EmbeddedDirectory : IDirectoryContents
{
private static readonly List<EmbeddedFile> emptyList = new List<EmbeddedFile>(0);
private readonly List<EmbeddedFile> files;
public EmbeddedDirectory(List<EmbeddedFile> files)
{
this.files = files;
}
public bool Exists => true;
public IEnumerator<IFileInfo> GetEnumerator()
{
return files.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return files.GetEnumerator();
}
}
private class EmbeddedFile : IFileInfo
{
private static readonly DateTime lastModified = DateTime.UtcNow;
private readonly Assembly assembly;
private readonly string resourcePath;
private readonly string filePath;
private readonly string name;
private readonly long length;
private readonly bool isDirectory;
private readonly bool exists;
public EmbeddedFile(Assembly assembly, string resourcePath, string filePath, bool isDirectory)
{
this.assembly = assembly;
this.resourcePath = resourcePath;
this.filePath = filePath;
this.isDirectory = isDirectory;
if (resourcePath == null)
{
exists = false;
name = "?";
}
else
{
exists = true;
if (!isDirectory)
{
using (Stream s = assembly.GetManifestResourceStream(resourcePath))
{
length = s.Length;
}
}
int pos = filePath.LastIndexOf('/');
if (pos >= 0)
{
name = filePath.Substring(++pos);
}
else
{
name = filePath;
}
}
}
bool IFileInfo.Exists => exists;
long IFileInfo.Length => length;
string IFileInfo.PhysicalPath => filePath;
string IFileInfo.Name => name;
DateTimeOffset IFileInfo.LastModified => lastModified;
bool IFileInfo.IsDirectory => isDirectory;
Stream IFileInfo.CreateReadStream()
{
return assembly.GetManifestResourceStream(resourcePath);
}
}
private readonly Assembly assembly;
private readonly string contentRoot;
private readonly Dictionary<string, List<EmbeddedFile>> folders = new Dictionary<string, List<EmbeddedFile>>(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, EmbeddedFile> pathsToResources = new Dictionary<string, EmbeddedFile>(StringComparer.OrdinalIgnoreCase);
private readonly EmbeddedFile notFoundFile;
private readonly IDirectoryContents notFoundDirectory = NotFoundDirectoryContents.Singleton;
bool IChangeToken.HasChanged => false;
bool IChangeToken.ActiveChangeCallbacks => false;
/// <summary>
/// Constructor
/// </summary>
/// <param name="assembly">Assembly containing resources</param>
/// <param name="ns">Namespace of the resources</param>
/// <param name="contentRoot">Content root, i.e. empty string or www</param>
public BetterEmbeddedFileProvider(Assembly assembly, string ns, string contentRoot = "")
{
this.assembly = assembly;
this.contentRoot = (contentRoot.Length == 0 || contentRoot == "/" ? string.Empty : ("/" + contentRoot.Trim('/')));
string[] allResources = assembly.GetManifestResourceNames();
string path;
string dir;
string extension;
int prefixLength = ns.Length + 1;
int pos;
EmbeddedFile file;
foreach (string s in allResources)
{
extension = null;
foreach (string ext in extensions)
{
if (s.EndsWith(ext, StringComparison.OrdinalIgnoreCase))
{
extension = ext;
break;
}
}
if (extension == null)
{
throw new InvalidDataException("Invalid extension for embedded resource " + s);
}
path = "/" + s.Substring(prefixLength);
path = path.Substring(0, path.Length - extension.Length);
path = path.Replace('.', '/');
path += extension;
pos = path.LastIndexOf('/');
pathsToResources[path] = file = new EmbeddedFile(assembly, s, path, false);
if (pos >= 0)
{
dir = path.Substring(0, pos);
if (!folders.TryGetValue(dir, out List<EmbeddedFile> files))
{
folders[dir] = files = new List<EmbeddedFile>();
}
files.Add(file);
}
}
notFoundFile = new EmbeddedFile(assembly, null, null, false);
}
public IDirectoryContents GetDirectoryContents(string subPath)
{
string path = contentRoot + subPath;
if (folders.TryGetValue(path, out List<EmbeddedFile> files))
{
return new EmbeddedDirectory(files);
}
return notFoundDirectory;
}
public IFileInfo GetFileInfo(string subPath)
{
pathsToResources.TryGetValue(contentRoot + subPath, out EmbeddedFile resource);
return (resource ?? notFoundFile);
}
public IChangeToken Watch(string filter)
{
return this;
}
IDisposable IChangeToken.RegisterChangeCallback(Action<object> callback, object state)
{
return dummyDisposable;
}
}
}
@jjxtra
Copy link
Author

jjxtra commented Jun 5, 2019

Example usage:

var filesProvider = new BetterEmbeddedFileProvider(Assembly.GetAssembly(typeof(TypeInAssemblyWithEmbeddedFiles)), "NamespaceOfYourEmbeddedFiles", contentRoot); // contentRoot can be empty or wwwroot, etc.
options.FileProvider = new CompositeFileProvider(options.FileProvider, filesProvider);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment