Готовим ASP.NET Core: как представить статический контент в виде ресурсов
Мы продолжаем нашу колонку по теме ASP.NET Core очередной публикацией от Дмитрия Сикорского (DmitrySikorsky) — руководителя компании «Юбрейнианс» из Украины. В своей статье Дмитрий рассказывает об опыте работы со статическим контентом в виде ресурсов вне основной сборки проекта в ASP.NET Core. Предыдущие статьи из колонки всегда можно прочитать по ссылке #aspnetcolumn — Владимир Юнев
Иногда необходимо, чтобы статический контент (вроде JS-, CSS-файлов или картинок) располагался, например, вне основной сборки веб-приложения в виде ресурсов. В этой небольшой статье я расскажу о двух подходах к решению этой задачи.
Подготовка проекта с ресурсами
Во-первых, нам необходим проект с ресурсами. Для примера, добавим в ресурсы один CSS-файл (который будет делать весь текст на странице красным) и одну картинку. Для этого нам понадобятся сами файлы, а также, примерно следующая строка в файле project.json нашего проекта:
"resource": [ "Styles/**", "Images/**" ]
Вот и все, теперь после сборки проекта все содержимое папок Styles и Images превратится в ресурсы (очевидно, что можно указать действительно конкретные файлы, а не целые папки, если в этом есть необходимость).
Совет! Вы можете попробовать все самостоятельно или загрузив исходный код из GitHub https://github.com/DmitrySikorsky/AspNet5Resources.
Кстати, при добавлении файлов в ресурсы «древовидность» их расположения становится «плоской», и все символы »\» в пути к файлу превращаются в точки. Т. е. информация об исходном расположении утрачивается (учитывая, что имена файлов могут содержать точки). Например, добавленный в ресурсы файл \Styles\test.css в проекте AspNet5Resources.Resources будет иметь следующее имя (регистр имеет значение):
AspNet5Resources.Resources.Styles.test.css
К счастью, нам не понадобится каждый раз писать имя сборки (в данном случае это AspNet5Resources.Resources) при получении контента из ресурсов. Для этого при создании EmbeddedFileProvider оно указывается в качестве базового пространства имен (об этом ниже).
Использование ресурсов
Мы можем использовать контент, добавленный в проект в виде ресурсов, как минимум двумя способами: реализовать все самостоятельно либо воспользоваться готовой реализацией. Оба способа очень просты.
Чтобы реализовать все самостоятельно необходимо добавить в проект, использующий контент из ресурсов (неважно, расположены ресурсы в этой сборке или в другой), контроллер, который будет извлекать запрошенные ресурсы по их именам и записывать их в выходной поток:
public class ResourceController : Controller
{
public ActionResult Index(string name)
{
Assembly assembly = Assembly.Load(new AssemblyName("AspNet5Resources.Resources"));
string fullName = assembly.GetName().Name + "." + name;
if (assembly.GetManifestResourceNames().Contains(fullName))
{
Stream stream = assembly.GetManifestResourceStream(fullName);
return this.Stream(stream);
}
return this.HttpNotFound();
}
}
Для упрощения работы с выходным потоком тут используется наш собственный класс StreamResult, унаследованный от ActionResult:
public class StreamResult : ActionResult
{
private Stream stream;
public StreamResult(Stream stream)
{
this.stream = stream;
}
public async override Task ExecuteResultAsync(ActionContext actionContext)
{
HttpResponse httpResponse = actionContext.HttpContext.Response;
await this.stream.CopyToAsync(httpResponse.Body);
}
}
Этого достаточно, чтобы иметь возможность отобразить картинку из ресурсов таким вот образом:
Теперь воспользуемся готовой реализацией «из коробки».
Первым делом нам необходимо реализовать интерфейс IFileProvider, чтобы в результате наш класс (назовем его CompositeFileProvider) умел объединять в себе несколько разных провайдеров. Класс целиком можно посмотреть в исходниках (ссылка в конце статьи), но ключевой момент следующий:
public IFileInfo GetFileInfo(string subpath)
{
foreach (IFileProvider fileProvider in this.fileProviders)
{
IFileInfo fileInfo = fileProvider.GetFileInfo(subpath);
if (fileInfo != null && fileInfo.Exists)
return fileInfo;
}
return new NonexistentFileInfo(subpath);
}
Т. е. по сути наш класс при поиске файла просто перебирает все доступные провайдеры в поисках того, в котором этот файл есть.
Чтобы наше приложение могло использовать как физически существующие файлы, так и файлы из ресурсов, создадим экземпляр нашего CompositeFileProvider таким образом:
public IFileProvider GetFileProvider(string path)
{
IEnumerable fileProviders = new IFileProvider[] { new PhysicalFileProvider(path) };
return new CompositeFileProvider(
fileProviders.Concat(
new Assembly[] { Assembly.Load(new AssemblyName("AspNet5Resources.Resources")) }.Select(a => new EmbeddedFileProvider(a, a.GetName().Name))
)
);
}
Далее, нам необходимо «зарегистрировать» наш провайдер при старте приложения в классе Startup:
public Startup(IApplicationEnvironment applicationEnvironment, IHostingEnvironment hostingEnvironment)
{
this.applicationBasePath = applicationEnvironment.ApplicationBasePath;
hostingEnvironment.WebRootFileProvider = this.GetFileProvider(this.applicationBasePath);
}
После этого мы можем использовать контент из ресурсов более привычным способом:
Выводы
Лично мне больше нравится второй вариант, т. к. он сильнее напоминает использование обычных файлов (несмотря на то, что файлы извлекаются из ресурсов). Если, например, не использовать точки в названиях файлов, то можно даже заменять в именах ресурсов все точки, кроме последней, символом »\» и таким образом «восстанавливать» исходное расположение и иметь более наглядный URL, но это не так уж важно.
Как и всегда, я подготовил небольшой тестовый проект, чтобы можно было сразу все запустить и увидеть своими глазами: github.com/DmitrySikorsky/AspNet5Resources.
Авторам
Друзья, если вам интересно поддержать колонку своим собственным материалом, то прошу написать мне на vyunev@microsoft.com для того чтобы обсудить все детали. Мы разыскиваем авторов, которые могут интересно рассказать про ASP.NET и другие темы.
Об авторе
Сикорский Дмитрий Александрович
Компания «Юбрейнианс» (http://ubrainians.com/)
Владелец, руководитель
DmitrySikorsky