Этот пост родился из нашего опыта переноса существующего проекта с ASP.NET MVC на ASP.NET Core. Мы постарались собрать в одно целое весь процесс миграции в структурированном виде и описать различные узкие места, чтобы разработчики в дальнейшем могли опираться на этот материал и следовать дорожной карте при решении подобных задач.
Пара слов о нашем проекте. Мы open-source eCommerce платформа на ASP.NET, которая к моменту переноса успешно существовала уже 9 лет. Мы делали миграцию 2 года назад — но руки дошли написать об этом только сейчас. На тот момент мы были одним из первых крупных проектов, кто решился на подобный шаг.
Прежде чем приступить к разбору шагов по переходу с ASP.NET MVC на ASP.NET Core, несколько слов о преимуществах этой платформы.
А теперь давайте поговорим о том, что вы должны учитывать при переносе своего приложения на новую платформу.
В тексте будет присутствовать большое количество ссылок на официальную документацию по ASP.NET Core чтобы помочь получить больше подробной информации по теме. Особенно актуально для разработчиков, которые впервые сталкиваются с подобной задачей.
Первое, что вам необходимо сделать это обновить Visual Studio 2017 до версии 15.3 или выше. И установить последнюю версию .NET Core SDK.
Перед началом переноса рекомендуется воспользоваться инструментом анализа переносимости .NET .Net Portability Analyzer. Это хорошая отправная точка, чтобы понять, насколько трудоемким будет переход от одной платформы к другой. Но, конечно, этот инструмент не решает всех проблем, и в процессе обнаружится много подводных камней. Далее будут изложены основные этапы, которые необходимо будет пройти, и показаны решения, использованные в нашем проекте.
Самое первое что необходимо — обновить ссылки на используемые в проекте библиотеки, которые поддерживали бы .NET Standard.
Если в своем проекте вы используете пакеты NuGet, необходимо проверить, совместимы ли они с .NET Core. Один из способов это сделать — использовать инструмент NuGetPackageExplorer.
Важно использовать новый подход для добавления сторонних ссылок, представленный в .NET Core: когда в решение добавляется новая библиотека классов, вы должны открыть файл основного проекта и заменить его содержимое следующим:
Ссылки из подключенных библиотек будут загружены автоматически. Более подробную информацию о сопоставлении между свойствами project.json и CSPROJ можно найти в официальной документации тут и тут.
Необходимо убрать все использования System.Web и заменить их на Microsoft.AspNetCore.
В ASP.NET Core появился новый механизм для начальной загрузки приложения. Точкой входа в приложения становится
Проблемы Startup.cs
В корне приложения при этом должна размещаться папка с именем Area, внутри которой располагается папка Admin. Теперь для связи контроллера с этой областью будет использоваться атрибут
Остается только создать представления для всех действий описанных в контроллере.
Валидация
Теперь передавать в контроллеры IFormCollection не нужно, так как в таком случае отключается серверная валидация у asp.net — MVC is suppressing further validation if the IFormCollection is found to be not null. Решением проблемы может стать добавление этого свойства в модель, этим мы избежим передачу напрямую в метод контроллера. Это правило справедливо, только если присутствует модель, если же модели нет, то и валидации не будет.
Дочерние свойства теперь автоматически не валидируются. Надо вручную это указывать.
HTTP-обработчики и HTTP-модули по сути очень похожи на концепцию Middleware в ASP.NET Core, но в отличие от модулей, порядок промежуточного программного обеспечения основан на очередности, по которой они вставлены в конвейер запросов. Порядок же модулей по большей части основан на событиях жизненного цикла приложения. Порядок промежуточного программного обеспечения для ответов противоположен порядку для запросов, а порядок модулей для запросов и ответов одинаков.Опираясь на это, можно приступать к обновлению.
Итак, что предстоит обновить:
Аутентификация в нашем проекте не использует встроенную систему удостоверений, для этих целей используется промежуточное программное обеспечение AuthenticationMiddleware, разработанное в соответствии с новой структурой ASP.NET Core.
ASP.NET предоставляет много встроенного промежуточного ПО, которое вы можете использовать в своем приложении, но обратите внимание, что разработчик имеет возможность создать собственное промежуточное программное обеспечение и добавить его в конвейер HTTP-запросов. Для упрощения данного механизма мы добавили специальный интерфейс, и теперь достаточно просто создать класс, который реализует его.
Тут вы можете добавлять и настраивать свое промежуточное программное обеспечение:
Внедрение зависимостей — одна из ключевых возможностей в процессе проектирования приложения в ASP.NET Core. Она позволяет создавать слабосвязанные приложения, которые являются более тестируемыми, модульными и, в результате, обслуживаемыми. Это стало возможным благодаря следованию принципу инверсии зависимостей. Для установки зависимостей используются контейнеры IoC (Inversion of Control). В ASP.NET Core такой контейнер представлен интерфейсом IServiceProvider. Установка сервисов в приложении осуществляется в методе
Любая зарегистрированная служба может быть настроена с тремя областями действия:
Для упрощения миграции существующей реализации Web API рекомендуется использовать NuGet пакет Microsoft.AspNetCore.Mvc.WebApiCompatShim. Поддерживаются такие совместимые функции:
Ранее некоторые настройки были сохранены в файле web.config. Теперь мы используем новый подход, основанный на парах «ключ — значение», установленных поставщиками конфигурации. Это рекомендованный механизм в ASP.NET Core, и мы используем файл appsettings.json.
Вы можете также использовать NuGet пакет
Если вы хотите использовать поставщик конфигурации хранилища ключей Azure, вам следует обратиться к материалам Миграция контента в Azure Key Valut. В нашем проекте такой задачи не стояло.
Для обслуживания статического контента необходимо указать веб-хосту корень содержимого текущего каталога. По умолчанию это wwwroot. Вы можете настроить свою директорию хранения статических файлов, настроив промежуточное программное обеспечение.
Если в проекте используются какие-то специфические возможности Entity Framework 6, которые не поддерживаются в
Пройдем по основным изменениям, которые предстоит учесть:
Для проверки, что
В ходе миграции своего проекта вы обнаружите, что достаточно большое количество классов было переименовано или перемещено, и теперь нужно привести все в соответствие с новыми требованиями. Вот список основных переходов, с которыми вы можете столкнуться:
Замена пространства имен:
Прочее:
Выше я уже писал, что в нашем проекте аутентификация не реализована с помощью встроенной системы удостоверений, а вынесена в отдельный слой промежуточного ПО. Однако в ASP.NET Core существует собственный механизм предоставления учетных данных. Подробнее с ними можно ознакомиться в документации по ссылке.
Что касается защиты данных — мы больше не используем MachineKey. Вместо этого мы используем встроенную функцию защиты данных. По умолчанию ключи генерируются при запуске приложения. В качестве хранилища данных может выступать:
Если встроенные механизмы не подходят, можно указать свой собственный механизм сохранения ключей, предоставив пользовательский IXmlRepository.
Изменился способ работы со статическими ресурсами: теперь все они должны храниться в корневой папке проекта wwwroot, если, конечно, не заданы другие настройки.
Когда вы используете встроенные блоки javascript, рекомендуется переместить их в конец страницы. Просто используйте атрибут asp-location = «Footer» для своих тегов. То же правило относится и к файлам js.
В качестве замены System.Web.Optimization используйте расширение BundlerMinifier — это позволит выполнять связывание и минимизацию JavaScript и CSS во время сборки проекта. Ссылка на документацию.
Дочерние действия (Child Action) больше не используются. Вместо этого ASP.NET Core предлагает новый мощный инструмент — ViewComponents, вызов которого осуществляется асинхронно.
Как получить строку из ViewComponent:
Больше нет необходимости использовать HtmlHelper — в ASP.NET Core встроено большое количество вспомогательных функций тегов (Tag Helpers). При работе приложения они обрабатываются движком Razor на стороне сервера и в конечном счете преобразуются в стандартные html-элементы. Это сильно упрощает разработку приложения. И, конечно же, вы можете реализовать свои собственные tag-helpers.
Мы начали использовать внедрение зависимостей в представления вместо разрешения настроек и служб с помощью
Итак, основные моменты по миграции представлений:
Мы убедились на своем опыте, что процесс миграции крупного веб-приложения — это очень трудоемкая задача, которая вряд ли может быть проведена без подводных камней. Мы планировали переход на новый фреймворк, как только вышла его первая стабильная версия, но сразу завершить его нам не удалось: не хватало некоторых критически важных функций, которые к тому времени ещё не успели перенести в .NET Core, в частности, связанных с EntityFramework. Поэтому нам пришлось сначала выпустить очередной релиз, используя смешанный подход — архитектура .NET Core с зависимостями .NET Framework.
Полностью адаптировать проект мы смогли после выхода .NET Core 2.1, имея к тому моменту уже работающее на новой архитектуре стабильное решение — оставалось лишь заменить некоторые пакеты и переписать работу с EF Core. Таким образом, полное мигрирование на новый фреймворк заняло несколько месяцев работы.
UPD: Продолжение истории обновления нашего проекта читайте в статье «Миграция с .NET Core 2.2 на .NET Core 3.1 на примере реального проекта».
Узнать больше о нашем проекте можно из нашего репозитория на GitHub.
Пара слов о нашем проекте. Мы open-source eCommerce платформа на ASP.NET, которая к моменту переноса успешно существовала уже 9 лет. Мы делали миграцию 2 года назад — но руки дошли написать об этом только сейчас. На тот момент мы были одним из первых крупных проектов, кто решился на подобный шаг.
Почему стоит перейти на ASP.NET Core
Прежде чем приступить к разбору шагов по переходу с ASP.NET MVC на ASP.NET Core, несколько слов о преимуществах этой платформы.
Преимущества ASP.NET Core
Итак, ASP.NET Core уже достаточно известный и развитый фреймворк, который претерпел уже несколько серьезных обновлений, а значит на сегодняшний день он достаточно стабилен, технологичен и устойчив к XSRF/CSRF атакам.
Кроссплатформенность — одна из отличительных черт, позволяющих ему набирать все большую популярность. Отныне ваше веб приложение может запуститься как в Windows среде, так и в Unix.
Модульность — ASP.NET Core поставляется полностью в виде NuGet пакетов, это позволяет оптимизировать приложение, включая выбранные необходимые пакеты. Это повышает производительность решения и сокращает время на обновление отдельных частей. Это вторая важная особенность, позволяющая разработчикам более гибко интегрировать новые функции в свое решение.
Производительность — еще один шаг к построению высокоэффективного приложения, ASP.NET Core обрабатывает на 2300% больше запросов в секунду, чем ASP.NET 4.6, и на 800% больше запросов в секунду, чем node.js. Подробные тесты производительности вы можете изучить самостоятельно тут или тут.
Middleware — это новый легкий высокопроизводительный модульный конвейер для запросов в приложении. Каждая часть промежуточного программного обеспечения обрабатывает HTTP-запрос, а затем либо решает вернуть результат, либо передает следующую часть промежуточного программного обеспечения. Этот подход дает разработчику полный контроль над конвейером HTTP и способствует разработке простых модулей для приложения, что важно для растущего проекта с открытым исходным кодом.
ASP.NET Core MVC предоставляет функции, упрощающие веб-разработку. В nopCommerce уже использовались такие функции, как шаблон Model-View-Controller, синтаксис Razor, привязка и проверка модели, но появились и новые инструменты:
Конечно, ASP.NET Core имеет гораздо больше возможностей, но мы только что рассмотрели самые интересные.
Кроссплатформенность — одна из отличительных черт, позволяющих ему набирать все большую популярность. Отныне ваше веб приложение может запуститься как в Windows среде, так и в Unix.
Модульность — ASP.NET Core поставляется полностью в виде NuGet пакетов, это позволяет оптимизировать приложение, включая выбранные необходимые пакеты. Это повышает производительность решения и сокращает время на обновление отдельных частей. Это вторая важная особенность, позволяющая разработчикам более гибко интегрировать новые функции в свое решение.
Производительность — еще один шаг к построению высокоэффективного приложения, ASP.NET Core обрабатывает на 2300% больше запросов в секунду, чем ASP.NET 4.6, и на 800% больше запросов в секунду, чем node.js. Подробные тесты производительности вы можете изучить самостоятельно тут или тут.
Middleware — это новый легкий высокопроизводительный модульный конвейер для запросов в приложении. Каждая часть промежуточного программного обеспечения обрабатывает HTTP-запрос, а затем либо решает вернуть результат, либо передает следующую часть промежуточного программного обеспечения. Этот подход дает разработчику полный контроль над конвейером HTTP и способствует разработке простых модулей для приложения, что важно для растущего проекта с открытым исходным кодом.
ASP.NET Core MVC предоставляет функции, упрощающие веб-разработку. В nopCommerce уже использовались такие функции, как шаблон Model-View-Controller, синтаксис Razor, привязка и проверка модели, но появились и новые инструменты:
- Tag Helpers. Это серверный код для участия в создании и рендеринге HTML-элементов в файлах Razor.
- View components. Это новый инструмент, похожий на частичные представления (partial views), но гораздо более мощный. nopCommerce использует view components, когда требуется многократное использование логики рендеринга и когда задача слишком сложна для частичного представления.
- DI в представления. Хотя большая часть данных, отображаемых в представлениях, передается из контроллера, nopCommerce имеет представления, в которых внедрение зависимостей более удобно.
Конечно, ASP.NET Core имеет гораздо больше возможностей, но мы только что рассмотрели самые интересные.
А теперь давайте поговорим о том, что вы должны учитывать при переносе своего приложения на новую платформу.
Миграция
В тексте будет присутствовать большое количество ссылок на официальную документацию по ASP.NET Core чтобы помочь получить больше подробной информации по теме. Особенно актуально для разработчиков, которые впервые сталкиваются с подобной задачей.
Шаг 1. Подготовка инструментария
Первое, что вам необходимо сделать это обновить Visual Studio 2017 до версии 15.3 или выше. И установить последнюю версию .NET Core SDK.
Перед началом переноса рекомендуется воспользоваться инструментом анализа переносимости .NET .Net Portability Analyzer. Это хорошая отправная точка, чтобы понять, насколько трудоемким будет переход от одной платформы к другой. Но, конечно, этот инструмент не решает всех проблем, и в процессе обнаружится много подводных камней. Далее будут изложены основные этапы, которые необходимо будет пройти, и показаны решения, использованные в нашем проекте.
Самое первое что необходимо — обновить ссылки на используемые в проекте библиотеки, которые поддерживали бы .NET Standard.
Шаг 2. Анализ совместимости NuGet пакетов для поддержки .Net Standard
Если в своем проекте вы используете пакеты NuGet, необходимо проверить, совместимы ли они с .NET Core. Один из способов это сделать — использовать инструмент NuGetPackageExplorer.
Шаг 3. В .NET Core использован новый формат csproj файла
Важно использовать новый подход для добавления сторонних ссылок, представленный в .NET Core: когда в решение добавляется новая библиотека классов, вы должны открыть файл основного проекта и заменить его содержимое следующим:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.2.6" />
...
</ItemGroup>
...
</Project>
Ссылки из подключенных библиотек будут загружены автоматически. Более подробную информацию о сопоставлении между свойствами project.json и CSPROJ можно найти в официальной документации тут и тут.
Шаг 4. Обновление пространства имен
Необходимо убрать все использования System.Web и заменить их на Microsoft.AspNetCore.
Шаг 5. Необходимо сконфигурировать файл Startup.cs. вместо использования global.asax
В ASP.NET Core появился новый механизм для начальной загрузки приложения. Точкой входа в приложения становится
Startup
, и зависимость от файла Global.asax исчезает. Startup
регистрирует набор ПО промежуточного слоя в приложении. Startup
должен включать метод Configure
. В Configure
добавьте необходимое ПО промежуточного слоя в конвейер.Проблемы Startup.cs
- Настройка middleware для MVC и WebAPI запросов
- Настройка конфигурации для:
- Обработки исключений
Так как в процессе перехода неизбежно придется сталкиваться с разного рода коллизиями необходимо сразу подготовиться и наладить обработку исключений в среде разработки (Development environment). С помощью UseDeveloperExceptionPage добавляется ПО промежуточного слоя, которое должно перехватывать исключения.
- Роутинга MVC
Регистрация новых маршрутов также была изменена. Теперь используется IRouteBuilder вместо RouteCollection, новый способ регистрации ограничений (IActionConstraint)
- MVC/WebAPI фильтры
Необходимо переписать фильтры в соответствии с новой реализацией ASP.NET Core.
- MVC/WebAPI Formatters
- Модели привязки
//add basic MVC feature
var mvcBuilder = services.AddMvc();
//add custom model binder provider (to the top of the provider list)
mvcBuilder.AddMvcOptions(options => options.ModelBinderProviders.Insert(0, new NopModelBinderProvider()));
/// <summary>
/// Represents model binder provider for the creating NopModelBinder
/// </summary>
public class NopModelBinderProvider : IModelBinderProvider
{
/// <summary>
/// Creates a nop model binder based on passed context
/// </summary>
/// <param name="context">Model binder provider context</param>
/// <returns>Model binder</returns>
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
throw new ArgumentNullException(nameof(context));
var modelType = context.Metadata.ModelType;
if (!typeof(BaseNopModel).IsAssignableFrom(modelType))
return null;
//use NopModelBinder as a ComplexTypeModelBinder for BaseNopModel
if (context.Metadata.IsComplexType && !context.Metadata.IsCollectionType)
{
//create binders for all model properties
var propertyBinders = context.Metadata.Properties
.ToDictionary(modelProperty => modelProperty, modelProperty => context.CreateBinder(modelProperty));
return new NopModelBinder(propertyBinders, EngineContext.Current.Resolve<ILoggerFactory>());
}
//or return null to further search for a suitable binder
return null;
}
}
app.UseMvc(routes => { routes.MapRoute("areaRoute", "{area:exists}/{controller=Admin}/{action=Index}/{id?}"); routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); });
В корне приложения при этом должна размещаться папка с именем Area, внутри которой располагается папка Admin. Теперь для связи контроллера с этой областью будет использоваться атрибут
[Area("Admin")] [Route("admin")]
.Остается только создать представления для всех действий описанных в контроллере.
[Area("Admin")]
[Route("admin")]
public class AdminController : Controller
{
public IActionResult Index()
{
return View();
}
}
Валидация
Теперь передавать в контроллеры IFormCollection не нужно, так как в таком случае отключается серверная валидация у asp.net — MVC is suppressing further validation if the IFormCollection is found to be not null. Решением проблемы может стать добавление этого свойства в модель, этим мы избежим передачу напрямую в метод контроллера. Это правило справедливо, только если присутствует модель, если же модели нет, то и валидации не будет.
Дочерние свойства теперь автоматически не валидируются. Надо вручную это указывать.
Шаг 6. Перенос HTTP-обработчиков и HTTP-модулей в Middleware
HTTP-обработчики и HTTP-модули по сути очень похожи на концепцию Middleware в ASP.NET Core, но в отличие от модулей, порядок промежуточного программного обеспечения основан на очередности, по которой они вставлены в конвейер запросов. Порядок же модулей по большей части основан на событиях жизненного цикла приложения. Порядок промежуточного программного обеспечения для ответов противоположен порядку для запросов, а порядок модулей для запросов и ответов одинаков.Опираясь на это, можно приступать к обновлению.
Итак, что предстоит обновить:
- Миграция модулей для Middleware (AuthenticationMiddleware, CultureMiddleware и пр.)
- Обработчики промежуточного программного обеспечения (Handlers to Middleware)
- Использование нового Middleware
Аутентификация в нашем проекте не использует встроенную систему удостоверений, для этих целей используется промежуточное программное обеспечение AuthenticationMiddleware, разработанное в соответствии с новой структурой ASP.NET Core.
public class AuthenticationMiddleware
{
private readonly RequestDelegate _next;
public AuthenticationMiddleware(IAuthenticationSchemeProvider schemes, RequestDelegate next)
{
Schemes = schemes ?? throw new ArgumentNullException(nameof(schemes));
_next = next ?? throw new ArgumentNullException(nameof(next));
}
public IAuthenticationSchemeProvider Schemes { get; set; }
public async Task Invoke(HttpContext context)
{
context.Features.Set<IAuthenticationFeature>(new AuthenticationFeature
{
OriginalPath = context.Request.Path,
OriginalPathBase = context.Request.PathBase
});
var handlers = context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();
foreach (var scheme in await Schemes.GetRequestHandlerSchemesAsync())
{
try
{
if (await handlers.GetHandlerAsync(context, scheme.Name) is IAuthenticationRequestHandler handler && await handler.HandleRequestAsync())
return;
}
catch
{
// ignored
}
}
var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync();
if (defaultAuthenticate != null)
{
var result = await context.AuthenticateAsync(defaultAuthenticate.Name);
if (result?.Principal != null)
{
context.User = result.Principal;
}
}
await _next(context);
}
}
ASP.NET предоставляет много встроенного промежуточного ПО, которое вы можете использовать в своем приложении, но обратите внимание, что разработчик имеет возможность создать собственное промежуточное программное обеспечение и добавить его в конвейер HTTP-запросов. Для упрощения данного механизма мы добавили специальный интерфейс, и теперь достаточно просто создать класс, который реализует его.
public interface INopStartup
{
/// <summary>
/// Add and configure any of the middleware
/// </summary>
/// <param name="services">Collection of service descriptors</param>
/// <param name="configuration">Configuration of the application</param>
void ConfigureServices(IServiceCollection services, IConfiguration configuration);
/// <summary>
/// Configure the using of added middleware
/// </summary>
/// <param name="application">Builder for configuring an application's request pipeline</param>
void Configure(IApplicationBuilder application);
/// <summary>
/// Gets order of this startup configuration implementation
/// </summary>
int Order { get; }
}
Тут вы можете добавлять и настраивать свое промежуточное программное обеспечение:
/// <summary>
/// Represents object for the configuring authentication middleware on application startup
/// </summary>
public class AuthenticationStartup : INopStartup
{
/// <summary>
/// Add and configure any of the middleware
/// </summary>
/// <param name="services">Collection of service descriptors</param>
/// <param name="configuration">Configuration of the application</param>
public void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
//add data protection
services.AddNopDataProtection();
//add authentication
services.AddNopAuthentication();
}
/// <summary>
/// Configure the using of added middleware
/// </summary>
/// <param name="application">Builder for configuring an application's request pipeline</param>
public void Configure(IApplicationBuilder application)
{
//configure authentication
application.UseNopAuthentication();
}
/// <summary>
/// Gets order of this startup configuration implementation
/// </summary>
public int Order => 500; //authentication should be loaded before MVC
}
Шаг 7. Использование встроенной DI
Внедрение зависимостей — одна из ключевых возможностей в процессе проектирования приложения в ASP.NET Core. Она позволяет создавать слабосвязанные приложения, которые являются более тестируемыми, модульными и, в результате, обслуживаемыми. Это стало возможным благодаря следованию принципу инверсии зависимостей. Для установки зависимостей используются контейнеры IoC (Inversion of Control). В ASP.NET Core такой контейнер представлен интерфейсом IServiceProvider. Установка сервисов в приложении осуществляется в методе
Startup.ConfigureServices()
.Любая зарегистрированная служба может быть настроена с тремя областями действия:
- transient
- scoped
- singleton
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddSingleton<Isingleton,MySingleton>();
Шаг 8. Использование оболочек совместимости проектов WebAPI (Shim)
Для упрощения миграции существующей реализации Web API рекомендуется использовать NuGet пакет Microsoft.AspNetCore.Mvc.WebApiCompatShim. Поддерживаются такие совместимые функции:
- Добавляет тип ApiController
- Включает привязку модели в стиле веб-API
- Расширяет привязку модели, чтобы действия контроллера могли принимать параметры типа HttpRequestMessage.
- Добавляет средства форматирования сообщений, позволяющие действиям возвращать результаты типа HttpResponseMessage
services.AddMvc().AddWebApiConventions();
routes.MapWebApiRoute(name: "DefaultApi",
template: "api/{controller}/{id?}"
);
Шаг 9. Перенос Application Configuration
Ранее некоторые настройки были сохранены в файле web.config. Теперь мы используем новый подход, основанный на парах «ключ — значение», установленных поставщиками конфигурации. Это рекомендованный механизм в ASP.NET Core, и мы используем файл appsettings.json.
Вы можете также использовать NuGet пакет
System.Configuration.ConfigurationManager
, если по каким-то причинам хотите продолжать использовать *.config. В этом случае придется отказаться от возможности запуска приложения на Unix платформах и запускать его только под IIS.Если вы хотите использовать поставщик конфигурации хранилища ключей Azure, вам следует обратиться к материалам Миграция контента в Azure Key Valut. В нашем проекте такой задачи не стояло.
Шаг 10. Перенос статического контента в wwwroot
Для обслуживания статического контента необходимо указать веб-хосту корень содержимого текущего каталога. По умолчанию это wwwroot. Вы можете настроить свою директорию хранения статических файлов, настроив промежуточное программное обеспечение.
Шаг 11. Перенос EntityFramework на EF Core
Если в проекте используются какие-то специфические возможности Entity Framework 6, которые не поддерживаются в
EF Core
, то есть смысл запускать приложение на NET Framework
. В таком случае, правда, придется пожертвовать мультиплатформенностью. Приложение будет работать только на Windows и под IIS.Пройдем по основным изменениям, которые предстоит учесть:
- Пространство имен System.Data.Entity заменено на Microsoft.EntityFrameworkCore
- Сигнатура конструктора DbContext была изменена. Теперь необходимо произвести инъекцию (inject) DbContextOptions
- метод HasDatabaseGeneratedOption(DatabaseGeneratedOption.None) заменен на ValueGeneratedNever()
- метод WillCascadeOnDelete(false) заменен на OnDelete(DeleteBehavior.Restrict)
- метод OnModelCreating(DbModelBuilder modelBuilder) заменен на OnModelCreating(ModelBuilder modelBuilder)
- метод HasOptional больше не доступен
- изменена конфигурация объектов, теперь нужно использовать OnModelCreating, т.к. EntityTypeConfiguration больше не доступен
- атрибут ComplexType больше недоступен
- интерфейс IDbSet замен на DbSet
- ComplexType — поддержка комплексного типа появилась в EF Core 2 с типом Owned Entity (https://docs.microsoft.com/en-us/ef/core/modeling/owned-entities), а таблицы без Primary Key с QueryType в EF Core 2.1 (https://docs.microsoft.com/en-us/ef/core/modeling/query-types)
- внешние ключи в EF Core генерируют теневые свойства, используя шаблон [Entity]Id, в отличие от EF6, который использует шаблон [Entity]_Id. Поэтому вначале добавьте внешние ключи в качестве обычного свойства к сущности.
- Для поддержки DI для DbContext сконфигурируйте ваш DbContex в
ConfigureServices
/// <summary>
/// Register base object context
/// </summary>
/// <param name="services">Collection of service descriptors</param>
public static void AddNopObjectContext(this IServiceCollection services)
{
services.AddDbContextPool<NopObjectContext>(optionsBuilder =>
{
optionsBuilder.UseSqlServerWithLazyLoading(services);
});
}
/// <summary>
/// SQL Server specific extension method for Microsoft.EntityFrameworkCore.DbContextOptionsBuilder
/// </summary>
/// <param name="optionsBuilder">Database context options builder</param>
/// <param name="services">Collection of service descriptors</param>
public static void UseSqlServerWithLazyLoading(this DbContextOptionsBuilder optionsBuilder, IServiceCollection services)
{
var nopConfig = services.BuildServiceProvider().GetRequiredService<NopConfig>();
var dataSettings = DataSettingsManager.LoadSettings();
if (!dataSettings?.IsValid ?? true)
return;
var dbContextOptionsBuilder = optionsBuilder.UseLazyLoadingProxies();
if (nopConfig.UseRowNumberForPaging)
dbContextOptionsBuilder.UseSqlServer(dataSettings.DataConnectionString, option => option.UseRowNumberForPaging());
else
dbContextOptionsBuilder.UseSqlServer(dataSettings.DataConnectionString);
}
Для проверки, что
EF Core
генерирует при миграции аналогичную схему базы данных, как Entity Framework
, используйте инструмент SQL Compare.Шаг 12. Удаление всех ссылок на HttpContext, замена устаревших классов и изменение пространства имен
В ходе миграции своего проекта вы обнаружите, что достаточно большое количество классов было переименовано или перемещено, и теперь нужно привести все в соответствие с новыми требованиями. Вот список основных переходов, с которыми вы можете столкнуться:
- HttpPostedFileBase --> IFormFile
- Доступ к access HttpContext теперь через IHttpContextAccessor
- HtmlHelper --> IHtmlHelper
- ActionResult --> IActionResult
- HttpUtility --> WebUtility
- Вместо HttpSessionStateBase — ISession, доступен из HttpContext.Session. из Microsoft.AspNetCore.Http
- Request.Cookies возвращает IRequestCookieCollection: IEnumerable<KeyValuePair<string, string>>, значит вместо HttpCookie — KeyValuePair<string, string> из Microsoft.AspNetCore.Http
Замена пространства имен:
- SelectList --> Microsoft.AspNetCore.Mvc.Rendering
- UrlHelper --> WebUtitlity
- MimeMapping --> FileExtensionContentTypeProvider
- MvcHtmlString --> IHtmlString и HtmlString
- ModelState, ModelStateDictionary, ModelError --> Microsoft.AspNetCore.Mvc.ModelBinding
- FormCollection --> IFormCollection
- Request.Url.Scheme --> this.Url.ActionContext.HttpContext.Request.Scheme
Прочее:
- MvcHtmlString.IsNullOrEmpty(IHtmlString) --> String.IsNullOrEmpty(variable.ToHtmlString())
- [ValidateInput(false)] — ее вообще больше нет и она не нужна
- HttpUnauthorizedResult --> UnauthorizedResult
- [AllowHtml] — директивы больше нет и она не нужна
- заменен метод TagBuilder.SetInnerText — теперь это InnerHtml.AppendHtml
- JsonRequestBehavior.AllowGet при возврате Json больше не нужен
- HttpUtility.JavaScriptStringEncode. --> JavaScriptEncoder.Default.Encode
- Request.RawUrl. Надо отдельно соединять Request.Path + Request.QueryString
- AllowHtmlAttribute — класса больше нет
- XmlDownloadResult — теперь можно использовать просто return File(Encoding.UTF8.GetBytes(xml), «application/xml», «filename.xml»);
- [ValidateInput(false)] — директивы больше нет и она не нужна
Шаг 13. Обновление аутентификации и авторизации
Выше я уже писал, что в нашем проекте аутентификация не реализована с помощью встроенной системы удостоверений, а вынесена в отдельный слой промежуточного ПО. Однако в ASP.NET Core существует собственный механизм предоставления учетных данных. Подробнее с ними можно ознакомиться в документации по ссылке.
Что касается защиты данных — мы больше не используем MachineKey. Вместо этого мы используем встроенную функцию защиты данных. По умолчанию ключи генерируются при запуске приложения. В качестве хранилища данных может выступать:
- File system — хранилище ключей на основе файловой системы
- Azure Storage — ключи защиты данных в хранилище BLOB-объектов Azure
- Redis — ключи защиты данных в кэше Redis
- Registry — необходимо использовать, если приложение не имеет доступа к файловой системе
- EF Core — ключи хранятся в базе данных
Если встроенные механизмы не подходят, можно указать свой собственный механизм сохранения ключей, предоставив пользовательский IXmlRepository.
Шаг 14. Обновление JS/CSS
Изменился способ работы со статическими ресурсами: теперь все они должны храниться в корневой папке проекта wwwroot, если, конечно, не заданы другие настройки.
Когда вы используете встроенные блоки javascript, рекомендуется переместить их в конец страницы. Просто используйте атрибут asp-location = «Footer» для своих тегов. То же правило относится и к файлам js.
В качестве замены System.Web.Optimization используйте расширение BundlerMinifier — это позволит выполнять связывание и минимизацию JavaScript и CSS во время сборки проекта. Ссылка на документацию.
Шаг 15. Миграция представлений
Дочерние действия (Child Action) больше не используются. Вместо этого ASP.NET Core предлагает новый мощный инструмент — ViewComponents, вызов которого осуществляется асинхронно.
Как получить строку из ViewComponent:
/// <summary>
/// Render component to string
/// </summary>
/// <param name="componentName">Component name</param>
/// <param name="arguments">Arguments</param>
/// <returns>Result</returns>
protected virtual string RenderViewComponentToString(string componentName, object arguments = null)
{
if (string.IsNullOrEmpty(componentName))
throw new ArgumentNullException(nameof(componentName));
var actionContextAccessor = HttpContext.RequestServices.GetService(typeof(IActionContextAccessor)) as IActionContextAccessor;
if (actionContextAccessor == null)
throw new Exception("IActionContextAccessor cannot be resolved");
var context = actionContextAccessor.ActionContext;
var viewComponentResult = ViewComponent(componentName, arguments);
var viewData = ViewData;
if (viewData == null)
{
throw new NotImplementedException();
}
var tempData = TempData;
if (tempData == null)
{
throw new NotImplementedException();
}
using (var writer = new StringWriter())
{
var viewContext = new ViewContext(
context,
NullView.Instance,
viewData,
tempData,
writer,
new HtmlHelperOptions());
// IViewComponentHelper is stateful, we want to make sure to retrieve it every time we need it.
var viewComponentHelper = context.HttpContext.RequestServices.GetRequiredService<IViewComponentHelper>();
(viewComponentHelper as IViewContextAware)?.Contextualize(viewContext);
var result = viewComponentResult.ViewComponentType == null ?
viewComponentHelper.InvokeAsync(viewComponentResult.ViewComponentName, viewComponentResult.Arguments):
viewComponentHelper.InvokeAsync(viewComponentResult.ViewComponentType, viewComponentResult.Arguments);
result.Result.WriteTo(writer, HtmlEncoder.Default);
return writer.ToString();
}
}
Больше нет необходимости использовать HtmlHelper — в ASP.NET Core встроено большое количество вспомогательных функций тегов (Tag Helpers). При работе приложения они обрабатываются движком Razor на стороне сервера и в конечном счете преобразуются в стандартные html-элементы. Это сильно упрощает разработку приложения. И, конечно же, вы можете реализовать свои собственные tag-helpers.
Мы начали использовать внедрение зависимостей в представления вместо разрешения настроек и служб с помощью
EngineContext
.Итак, основные моменты по миграции представлений:
- Конвертировать
Views/web.config в Views/_ViewImports.cshtml
— используется для импорта пространств имен и внедрения зависимостей (injection
). Этот файл не поддерживает другие функцииRazor
, такие как определения функций и разделов. - Преобразовать
namespaces.add
в@using
- Перенос любых настроек в основную конфигурацию приложения
Scripts.Render
иStyles.Render
не существует. Заменить ссылками на выходные данныеlibman
илиBundlerMinifier
В качестве заключения
Мы убедились на своем опыте, что процесс миграции крупного веб-приложения — это очень трудоемкая задача, которая вряд ли может быть проведена без подводных камней. Мы планировали переход на новый фреймворк, как только вышла его первая стабильная версия, но сразу завершить его нам не удалось: не хватало некоторых критически важных функций, которые к тому времени ещё не успели перенести в .NET Core, в частности, связанных с EntityFramework. Поэтому нам пришлось сначала выпустить очередной релиз, используя смешанный подход — архитектура .NET Core с зависимостями .NET Framework.
Полностью адаптировать проект мы смогли после выхода .NET Core 2.1, имея к тому моменту уже работающее на новой архитектуре стабильное решение — оставалось лишь заменить некоторые пакеты и переписать работу с EF Core. Таким образом, полное мигрирование на новый фреймворк заняло несколько месяцев работы.
UPD: Продолжение истории обновления нашего проекта читайте в статье «Миграция с .NET Core 2.2 на .NET Core 3.1 на примере реального проекта».
Узнать больше о нашем проекте можно из нашего репозитория на GitHub.