Переход с ASP.NET Core 5.0 на 6.0
В этой статье объясняется, как обновить существующий проект ASP.NET Core 5.0 до ASP.NET Core 6.0. Инструкции по миграции с ASP.NET Core 3.1 на ASP.NET Core 6.0 см. в разделе «Миграция с ASP.NET Core 3.1 на 6.0».
Необходимые компоненты
- Visual Studio
- Visual Studio Code
- Visual Studio для Mac
- Visual Studio 2022 с рабочей нагрузкой ASP.NET и веб-разработка.
- Пакет SDK для .NET 6.0
- Visual Studio Code
- C# для Visual Studio Code (последняя версия)
- Пакет SDK для .NET 6.0
Для функций разработки ASP.NET Core, таких как создание проекта, в инструкциях Visual Studio Code используется .NET CLI. Эти инструкции можно выполнять на macOS, Linux или Windows и в любом редакторе кода. При использовании редактора, отличного от Visual Studio Code, может потребоваться внести незначительные изменения.
- Visual Studio Code с набором средств разработки C# и связанными расширениями, такими как .NET MAUI и Unity.
- Интегрированная среда разработки Visual Studio, запущенная в Windows на виртуальной машине Mac.
- Интегрированная среда разработки Visual Studio, запущенная в Windows на виртуальной машине в облаке.
Дополнительные сведения см. в Visual Studio для Mac объявлении о выходе на пенсию.
Обновление версии пакета SDK для .NET в global.json
Если вы используете global.json файл для конкретной версии пакета SDK для .NET, обновите version свойство до установленной версии пакета SDK для .NET 6.0. Например:
Обновление целевой платформы
Обновите moniker целевой платформы проекта (TFM) файла проекта следующим: net6.0
- net5.0 + net6.0
Обновление ссылок на пакеты
В файле проекта обновите атрибут каждой Microsoft.AspNetCore.* ссылки и Microsoft.Extensions.* пакета Version до версии 6.0.0 или более поздней версии. Например:
Новая модель размещения
Новая модель размещения .NET 6 для приложений ASP.NET Core требует только одного файла и нескольких строк кода. Приложения, перенесенные на версию 6.0, не должны использовать новую модель минимального размещения. Дополнительные сведения см. в разделе «Приложения, перенесенные на 6.0», не нужно использовать новую модель минимального размещения в следующем разделе.
Следующий код из пустого шаблона ASP.NET Core создает приложение с помощью новой модели минимального размещения:
var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.MapGet("/", () => "Hello World!"); app.Run();
Минимальная модель размещения:
- Значительно сокращают количество файлов и строк кода, необходимых для создания приложения. Требуется только один файл с четырьмя строками кода.
- Объединяют Startup.cs и Program.cs в один файл Program.cs .
- Используют инструкции верхнего уровня для минимизации кода, необходимого для приложения.
- Используют глобальные директивы using , чтобы исключить или минимизировать числу требуемых строк инструкций using .
В следующем коде отображаются Startup.cs и Program.cs файлы из шаблона веб-приложения ASP.NET Core 5 (Razor Pages) с неиспользуемых using операторов удалены:
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; // Unused usings removed. namespace WebAppRPv5 < public class Startup < public Startup(IConfiguration configuration) < Configuration = configuration; >public IConfiguration Configuration < get; >// This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) < services.AddRazorPages(); >// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) < if (env.IsDevelopment()) < app.UseDeveloperExceptionPage(); >else < app.UseExceptionHandler("/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); >app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => < endpoints.MapRazorPages(); >); > > >
using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; // Unused usings removed. namespace WebAppRPv5 < public class Program < public static void Main(string[] args) < CreateHostBuilder(args).Build().Run(); >public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => < webBuilder.UseStartup(); >); > >
В ASP.NET Core 6 предыдущий код заменяется следующим кодом:
var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddRazorPages(); var app = builder.Build(); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) < app.UseExceptionHandler("/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); >app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); app.MapRazorPages(); app.Run();
В предыдущем примере ASP.NET Core 6 показано, как:
- Значение ConfigureServices заменяется на WebApplication.Services .
- builder.Build() возвращает настроенную WebApplication переменную app . Configureзаменяется вызовами конфигурации для одних и того же служб. app
Подробные примеры переноса кода ASP.NET Core 5 Startup на ASP.NET Core 6 с использованием минимальной модели размещения приведены далее в этом документе.
Существует несколько изменений в других файлах, созданных для шаблона веб-приложения:
- Index.cshtml и Privacy.cshtml удалите неиспользуемые using инструкции.
- RequestId in Error.cshtml объявлен как ссылочный тип, допускающий значение NULL (NRT):
- public string RequestId < get; set; >+ public string? RequestId
- Значения по умолчанию уровня журнала изменились и appsettings.json appsettings.Development.json :
- "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" + "Microsoft.AspNetCore": "Warning"
В предыдущем ASP.NET код «Microsoft»: «Warning» шаблона Core был изменен «Microsoft.AspNetCore»: «Warning» на . Это изменение приводит к ведению журнала всех информационных сообщений из Microsoft пространства имен, кроме Microsoft.AspNetCore . Например, Microsoft.EntityFrameworkCore теперь заносится в журнал на информационном уровне.
Дополнительные сведения о новой модели размещения см. в разделе » Часто задаваемые вопросы «. Дополнительные сведения о внедрении NRTs и анализа состояния компилятора .NET см . в разделе «Типы ссылок, допускающие значение NULL» (NRTs) и статический анализ состояния .NET.
Приложения, перенесенные на 6.0 и более поздние версии, не должны использовать новую модель минимального размещения
Использование Startup и универсальный узел , используемый шаблонами ASP.NET Core 3.1 и 5.0, полностью поддерживается.
Использование Запуска с новой минимальной моделью размещения
ASP.NET приложения Core 3.1 и 5.0 могут использовать свой Startup код с новой минимальной моделью размещения. Использование Startup с минимальной моделью размещения имеет следующие преимущества:
- Для вызова класса не используется скрытое Startup отражение.
- Асинхронный код можно записать, так как разработчик управляет вызовом Startup .
- Код можно написать, что пересекает ConfigureServices и Configure .
Одно из незначительных ограничений при использовании Startup кода с новой минимальной моделью размещения заключается в том, что для внедрения зависимостей Configure в Program.cs службу необходимо разрешить вручную.
Рассмотрим следующий код, созданный шаблоном страниц ASP.NET Core 3.1 или 5.0 Razor Pages:
public class Program < public static void Main(string[] args) < CreateHostBuilder(args).Build().Run(); >public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => < webBuilder.UseStartup(); >); >
public class Startup < public Startup(IConfiguration configuration) < Configuration = configuration; >public IConfiguration Configuration < get; >public void ConfigureServices(IServiceCollection services) < services.AddRazorPages(); >public void Configure(IApplicationBuilder app, IWebHostEnvironment env) < if (env.IsDevelopment()) < app.UseDeveloperExceptionPage(); >else < app.UseExceptionHandler("/Error"); app.UseHsts(); >app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseEndpoints(endpoints => < endpoints.MapRazorPages(); >); > >
Предыдущий код перенесен на новую минимальную модель размещения:
using Microsoft.AspNetCore.Builder; var builder = WebApplication.CreateBuilder(args); var startup = new Startup(builder.Configuration); startup.ConfigureServices(builder.Services); var app = builder.Build(); startup.Configure(app, app.Environment); app.Run();
public class Startup < public Startup(IConfiguration configuration) < Configuration = configuration; >public IConfiguration Configuration < get; >public void ConfigureServices(IServiceCollection services) < services.AddRazorPages(); >public void Configure(IApplicationBuilder app, IWebHostEnvironment env) < if (!env.IsDevelopment()) < app.UseExceptionHandler("/Error"); app.UseHsts(); >app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseEndpoints(endpoints => < endpoints.MapRazorPages(); >); > >
В приведенном выше коде блок удаляется, if (env.IsDevelopment()) так как в режиме разработки по промежуточному слоям страницы исключений разработчика включен по умолчанию. Дополнительные сведения см. в разделе «Различия между моделями размещения ASP.NET Core 5 и 6» в следующем разделе.
При использовании контейнера пользовательской внедрения зависимостей (DI) добавьте следующий выделенный код:
using Autofac; using Autofac.Extensions.DependencyInjection; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Hosting; var builder = WebApplication.CreateBuilder(args); var startup = new Startup(builder.Configuration); startup.ConfigureServices(builder.Services); // Using a custom DI container. builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory()); builder.Host.ConfigureContainer(startup.ConfigureContainer); var app = builder.Build(); startup.Configure(app, app.Environment); app.Run();
using Autofac; public class Startup < public Startup(IConfiguration configuration) < Configuration = configuration; >public IConfiguration Configuration < get; >public void ConfigureServices(IServiceCollection services) < services.AddRazorPages(); >// Using a custom DI container public void ConfigureContainer(ContainerBuilder builder) < // Configure custom container. >public void Configure(IApplicationBuilder app, IWebHostEnvironment env) < if (!env.IsDevelopment()) < app.UseExceptionHandler("/Error"); app.UseHsts(); >app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseEndpoints(endpoints => < endpoints.MapRazorPages(); >); > >
При использовании минимальной модели размещения ПО промежуточного слоя маршрутизации конечных точек упаковывает весь конвейер ПО промежуточного слоя, поэтому нет необходимости явно вызывать UseRouting UseEndpoints или регистрировать маршруты. UseRouting можно использовать для указания того, где происходит сопоставление маршрутов, но UseRouting не требуется явно вызывать, если маршруты должны соответствовать в начале конвейера по промежуточного слоя.
В следующем коде вызовы UseRouting и UseEndpoints удаляются из Startup него. MapRazorPages вызывается в Program.cs :
public class Startup < public Startup(IConfiguration configuration) < Configuration = configuration; >public IConfiguration Configuration < get; >public void ConfigureServices(IServiceCollection services) < services.AddRazorPages(); >public void Configure(IApplicationBuilder app, IWebHostEnvironment env) < if (!env.IsDevelopment()) < app.UseExceptionHandler("/Error"); app.UseHsts(); >app.UseHttpsRedirection(); app.UseStaticFiles(); //app.UseRouting(); //app.UseEndpoints(endpoints => //< // endpoints.MapRazorPages(); //>); > >
using Microsoft.AspNetCore.Builder; var builder = WebApplication.CreateBuilder(args); var startup = new Startup(builder.Configuration); startup.ConfigureServices(builder.Services); var app = builder.Build(); startup.Configure(app, app.Environment); app.MapRazorPages(); app.Run();
При использовании Startup с новой минимальной моделью размещения следует учитывать следующее различие:
- Program.cs управляет экземпляром и временем существования Startup класса.
- Любые дополнительные службы, внедренные в Configure метод, должны быть вручную разрешены классом Program .
Различия между моделями размещения ASP.NET Core 5 и 6
- В режиме разработки по промежуточному слоям страницы исключений разработчика включен по умолчанию.
- Имя приложения по умолчанию — имя сборки точки входа: Assembly.GetEntryAssembly().GetName().FullName При использовании WebApplicationBuilder библиотеки явно измените имя приложения на сборку библиотеки, чтобы разрешить обнаружение частей приложения MVC. Подробные инструкции см. в разделе «Изменение корневого каталога содержимого», имени приложения и среды в этом документе.
- ПО промежуточного слоя маршрутизации конечных точек упаковывает весь конвейер ПО промежуточного слоя, поэтому нет необходимости явно вызывать UseRouting маршруты или UseEndpoints регистрировать их. UseRouting можно использовать для указания того, где происходит сопоставление маршрутов, но UseRouting не требуется явно вызывать, если маршруты должны соответствовать в начале конвейера по промежуточного слоя.
- Конвейер создается перед выполнениемIStartupFilter, поэтому исключения, вызванные при создании конвейера, не видны цепочке IStartupFilter вызовов.
- Некоторые средства, такие как миграция EF, используются Program.CreateHostBuilder для доступа к приложению IServiceProvider для выполнения пользовательской логики в контексте приложения. Эти средства были обновлены, чтобы использовать новый метод для выполнения пользовательской логики в контексте приложения. Entity Framework Migrations — это пример средства, которое используется Program.CreateHostBuilder таким образом. Мы работаем над тем, чтобы убедиться, что средства обновлены для использования новой модели.
- Startup В отличие от класса, минимальный узел не настраивает автоматически область di при создании экземпляра поставщика услуг. Для контекстов, в которых требуется область, необходимо вызвать IServiceScope с помощью IServiceScopeFactory.CreateScope, чтобы создать экземпляр новой область. Дополнительные сведения см. в статье о том, как разрешить службу при запуске приложения.
- Изменить параметры узла, например имя приложения, среду или корневой каталог содержимого после создания, WebApplicationBuilderневозможно. Подробные инструкции по изменению параметров узла см. в разделе «Настройка» или IWebHostBuilder «Настройка IHostBuilder «. Следующие выделенные API вызывают исключение:
var builder = WebApplication.CreateBuilder(args); builder.Services.AddRazorPages(); // WebHost try < builder.WebHost.UseContentRoot(Directory.GetCurrentDirectory()); >catch (Exception ex) < Console.WriteLine(ex.Message); >try < builder.WebHost.UseEnvironment(Environments.Staging); >catch (Exception ex) < Console.WriteLine(ex.Message); >try < builder.WebHost.UseSetting(WebHostDefaults.ApplicationKey, "ApplicationName2"); >catch (Exception ex) < Console.WriteLine(ex.Message); >try < builder.WebHost.UseSetting(WebHostDefaults.ContentRootKey, Directory.GetCurrentDirectory()); >catch (Exception ex) < Console.WriteLine(ex.Message); >try < builder.WebHost.UseSetting(WebHostDefaults.EnvironmentKey, Environments.Staging); >catch (Exception ex) < Console.WriteLine(ex.Message); >// Host try < builder.Host.UseEnvironment(Environments.Staging); >catch (Exception ex) < Console.WriteLine(ex.Message); >try < // TODO: This does not throw builder.Host.UseContentRoot(Directory.GetCurrentDirectory()); >catch (Exception ex) < Console.WriteLine(ex.Message); >var app = builder.Build(); if (!app.Environment.IsDevelopment()) < app.UseExceptionHandler("/Error"); app.UseHsts(); >app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); app.MapRazorPages(); app.Run();
- Класс Startup нельзя использовать из WebApplicationBuilder.Host или WebApplicationBuilder.WebHost . Следующий выделенный код вызывает исключение:
var builder = WebApplication.CreateBuilder(args); try < builder.Host.ConfigureWebHostDefaults(webHostBuilder =>< webHostBuilder.UseStartup(); >); > catch (Exception ex) < Console.WriteLine(ex.Message); throw; >builder.Services.AddRazorPages(); var app = builder.Build();
var builder = WebApplication.CreateBuilder(args); try < builder.WebHost.UseStartup(); > catch (Exception ex) < Console.WriteLine(ex.Message); throw; >builder.Services.AddRazorPages(); var app = builder.Build();
using Microsoft.Extensions.DependencyInjection.Extensions; var builder = WebApplication.CreateBuilder(args); builder.Host.ConfigureServices(services => < services.TryAddSingleton(); >); builder.Services.TryAddSingleton(); var app = builder.Build(); // Displays Service1 only. Console.WriteLine(app.Services.GetRequiredService()); app.Run(); class Service1 : IService < >class Service2 : IService < >interface IService
В приведенном выше коде builder.Host.ConfigureServices обратный вызов вызывается встроенным, а не откладывается до builder.Build вызова. Это означает, что Service1 он добавляется к IServiceCollection параметру до Service2 и приводит к Service1 разрешению IService .
Создание библиотек для ASP.NET Core 6
Существующая экосистема .NET построила расширяемость вокруг IServiceCollection, IHostBuilderи IWebHostBuilder. Эти свойства доступны как WebApplicationBuilder Services , Host и WebHost .
Мы ожидаем, что авторы библиотеки будут продолжать использовать целевые IHostBuilder компоненты , IWebHostBuilder IApplicationBuilder и IEndpointRouteBuilder при создании ASP.NET основных компонентов. Это гарантирует, что по промежуточному слоям, обработчику маршрутов или другим точкам расширяемости будут работать в разных моделях размещения.
Часто задаваемые вопросы
- Является ли новая минимальная модель размещения менее способной? Нет. Новая модель размещения функционально эквивалентна 98% сценариев, поддерживаемых IHostBuilder ими IWebHostBuilder . Существуют некоторые сложные сценарии, требующие конкретных обходных IHostBuilder решений, но мы ожидаем, что они будут чрезвычайно редкими.
- Не рекомендуется ли использовать универсальную модель размещения? Нет. Универсальная модель размещения — это альтернативная модель, которая поддерживается бесконечно. Универсальный узел лежит в основе новой модели размещения и по-прежнему является основным способом размещения рабочих приложений.
- Нужно ли перенести новую модель размещения? Нет. Новая модель размещения — это предпочтительный способ размещения новых приложений с помощью .NET 6 и более поздних версий, но вы не вынуждены изменять макет проекта в существующих приложениях. Это означает, что приложения могут обновляться с .NET 5 до .NET 6, изменив целевую платформу в файле проекта на net5.0 net6.0 . Дополнительные сведения см. в разделе «Обновление целевой платформы » в этой статье. Однако мы рекомендуем приложениям перейти на новую модель размещения, чтобы воспользоваться новыми функциями, доступными только для новой модели размещения.
- Нужно ли использовать инструкции верхнего уровня? Нет. Новые шаблоны проектов используют инструкции верхнего уровня, но новые API размещения можно использовать в любом приложении .NET 6 для размещения веб-сервера или веб-приложения.
- Где поместить состояние, которое хранилось в качестве полей в моем Program или Startup классе? Настоятельно рекомендуется использовать внедрение зависимостей (DI) для потока состояния в приложениях ASP.NET Core. Существует два подхода к хранению состояния за пределами DI:
- Сохраните состояние в другом классе. При хранении в классе предполагается статическое состояние, к которому можно получить доступ в любом месте приложения.
- Используйте класс, Program созданный операторами верхнего уровня, для хранения состояния. Использование Program для хранения состояния — это семантический подход:
var builder = WebApplication.CreateBuilder(args); ConfigurationValue = builder.Configuration["SomeKey"] ?? "Hello"; var app = builder.Build(); app.MapGet("/", () => ConfigurationValue); app.Run(); partial class Program < public static string? ConfigurationValue < get; private set; >>Blazor
После выполнения инструкций, описанных ранее в этой статье, чтобы обновить приложение до версии 6.0, получите конкретные функции, следуя ссылкам, приведенным в статье «Новые возможности» ASP.NET Core 6.0.
Чтобы внедрить все новые функции версии 6.0 для Blazor приложений, рекомендуется выполнить следующий процесс:
- Создайте проект версии 6.0 Blazor из одного из Blazor шаблонов проектов. Дополнительные сведения см. в статье Инструментарий для ASP.NET Core Blazor.
- Переместите компоненты и код приложения в приложение 6.0, внося изменения в новые функции 6.0.
Перенос проектов SPA
Перенос приложений Angular из расширений SPA
Перенос приложений React из расширений SPA
Сведения о переносе приложений React из расширений Spa в этой проблеме GitHub
Обновление образов Docker
Для приложений с помощью Docker обновите инструкции и скрипты Dockerfile FROM . Используйте базовый образ, включающий среду выполнения ASP.NET Core 6.0. Рассмотрим следующее docker pull различие между ASP.NET Core 5.0 и 6.0:
- docker pull mcr.microsoft.com/dotnet/aspnet:5.0 + docker pull mcr.microsoft.com/dotnet/aspnet:6.0Изменения пакета SDK для ASP.NET Core Razor
Теперь Razor компилятор использует новую функцию генераторов источников для создания скомпилированных файлов C# из Razor представлений и страниц проекта. В предыдущих версиях:
- Компиляция зависит от целевых RazorGenerate объектов и RazorCompile создает созданный код. Эти целевые объекты больше не допустимы. В .NET 6 создание кода и компиляция поддерживаются одним вызовом компилятора. RazorComponentGenerateDependsOn по-прежнему поддерживается для указания зависимостей, необходимых перед выполнением сборки.
- Была создана отдельная Razor сборка, AppName.Views.dll содержащая скомпилированные типы представлений в приложении. Это поведение устарело, и создается одна сборка AppName.dll , содержащая как типы приложений, так и созданные представления.
- Типы приложений были AppName.Views.dll общедоступными. В .NET 6 типы приложений находятся только internal sealed в AppName.dll . Приложения, выполняя обнаружение типов, не смогут выполнять обнаружение AppName.Views.dll типов. AppName.dll Ниже показано изменение API:
- public class Views_Home_Index : global::Microsoft.AspNetCore.Mvc.Razor.RazorPage + internal sealed class Views_Home_Index : global::Microsoft.AspNetCore.Mvc.Razor.RazorPage
Выполните следующие изменения:
- Следующие свойства больше не применимы к модели одношаговой компиляции.
- RazorTargetAssemblyAttribute
- RazorTargetName
- EnableDefaultRazorTargetAssemblyInfoAttributes
- UseRazorBuildServer
- GenerateRazorTargetAssemblyInfo
- GenerateMvcApplicationPartsAssemblyAttributes
Шаблоны проектов используют сервер Duende Identity
Duende Identity Server — это открытый код продукт с соглашением о взаимной лицензии. Если вы планируете использовать Duende Server в рабочей среде, вам может потребоваться получить коммерческую лицензию от Duende Identity Software и оплатить плату за лицензию. Дополнительные сведения см. в разделе Duende Software: License.
DbSet Добавьте свойство с именем Keys для каждого IdentityDbContext для удовлетворения нового требования из обновленной IPersistedGrantDbContext версии. Ключи необходимы в рамках контракта с магазинами Duende Identity Server.
public DbSet Keys
Существующие миграции необходимо повторно создать для сервера Duende Identity .
Примеры кода, перенесенные в ASP.NET Core 6.0
Просмотр критических изменений
См. следующие ресурсы.
- Identity: изменена версия начальной загрузки пользовательского интерфейса по умолчанию
- Критические изменения для миграции с версии 5.0 на 6.0: включает ASP.NET Core и Entity Framework Core.
- Репозиторий объявлений GitHub (aspnet/Announcements, 6.0.0 label): включает критические и не критически важные сведения.
Ссылочные типы, допускающие значение NULL (NRT), и статический анализ состояния NULL компилятора .NET
ASP.NET Шаблоны проектов Core используют ссылочные типы, допускающие значение NULL(NRT), а компилятор .NET выполняет статический анализ состояния NULL. Эти функции были выпущены в C# 8 и включены по умолчанию для приложений, созданных с помощью ASP.NET Core 6.0 (C# 10) или более поздних версий.
Предупреждения статического анализа состояния .NET компилятора .NET могут служить руководством по обновлению примера документации или примера приложения локально или игнорироваться. Статический анализ состояния NULL можно отключить, задав значение Nullable для параметра disable в файле проекта приложения, который мы рекомендуем только для примеров документации и примеров приложений, если при изучении .NET возникли ненужные предупреждения компилятора. Не рекомендуется отключить проверка состояния NULL в рабочих проектах.
Дополнительные сведения о NRT, свойстве MSBuild Nullable и обновлении приложений (включая рекомендации #pragma ) см. в следующих ресурсах в документации по C#:
- Ссылочные типы, допускающие значение NULL
- Ссылочные типы, допускающие значение NULL (справочник по C#)
- Подробнее о методах разрешения предупреждений, допускающих значения NULL
- Обновление базы кода с помощью ссылочных типов, допускающих значение NULL, для улучшения предупреждений диагностики NULL
- Атрибуты для статического анализа состояния со значением NULL
- ! — оператор (допускающий значение NULL) (справочник по C#)
модуль ASP.NET Core (ANCM)
Если модуль ASP.NET Core (ANCM) не был выбранным компонентом при установке Visual Studio или если в системе установлена предварительная версия ANCM, скачайте последнюю версию установщика пакета размещения .NET Core (прямая загрузка) и запустите установщик. Дополнительные сведения см. в разделе «Пакет размещения».
Изменение имени приложения
В .NET 6 WebApplicationBuilder нормализует корневой путь содержимого, чтобы завершиться с DirectorySeparatorCharпомощью . Большинство приложений, перенесенных из HostBuilder или WebHostBuilder не имеющих того же имени приложения, так как они не нормализованы. Дополнительные сведения см. в разделе SetApplicationName
Дополнительные ресурсы
Совместная работа с нами на GitHub
Источник этого содержимого можно найти на GitHub, где также можно создавать и просматривать проблемы и запросы на вытягивание. Дополнительные сведения см. в нашем руководстве для участников.
Реализация Web API OData в ASP.NET Core 3 и ASP.NET 5 (часть 1). Связи «многие-ко-многим»
Наша команда занимается развитием корпоративной системы электронного документооборота. В команде часть приложений разрабатывается на текущей LTS версии .NET Core 3.1, в частности, бэкэнд для SPA, а также ряд Worker Service’ов, которые с определенным интервалом взаимодействуют с СЭД.
Со временем, возникла необходимость использования этими приложениями общих мастер-данных. Для их хранения решили использовать БД PostgreSQL, так как имели свежий опыт и почти готовое окружение для его развертывания. Непосредственно для получения (а в будущем – и для записи) данных приложениями, решили реализовать Web API на .NET Core 3.1, чтобы инкапсулировать взаимодействие с БД в одном приложении и заложить возможность взаимодействия с любой системой. В качестве ORM, исходя из сложившихся практик и опыта, использовали EF Core. При этом, нужна была возможность фильтрации и получения связанных данных. Чтобы не придумывать велосипед в этой части, пришли к необходимости реализации API на основе стандартов OData.
В сети есть ряд хороших статей по реализации API OData на .NET Core, однако информация в них весьма разрозненна и зачастую авторы упускают важные нюансы, имеющиеся в реализации. В первой статье нами описана общая реализация API OData с использованием EF Core. Особое внимание при этом уделено неочевидным моментам при реализации типа связи «многие-ко-многим».
Реализация на ASP.NET Core 3.1
Вначале в Visual Studio 2019 создадим проект по шаблону ASP.NET Core Web API. Для взаимодействия с БД Postgres в проект добавим пакеты Microsoft.EntityFrameworkCore, Npgsql.EntityFrameworkCore.PostgreSQL, а также, для применения рекомендованного для Postgres нэйминга объектов БД, используем пакет EFCore.NamingConventions. Для реализации требований OData добавляем пакет Microsoft.AspNetCore.OData:
all runtime; build; native; contentfiles; analyzers; buildtransitive Основная проблема с типом связи «многие-ко-многим» заключалась в том, что EF Core 3.1 не умеет самостоятельно создавать таблицу, связывающую два справочника. Эта опция доступна только в версиях под .NET Framework, либо начиная с .NET 5.
Для примера реализуем модель для справочников Систем и Шаблонов загружаемых файлов: каждая система может использовать несколько шаблонов загрузки, а каждый шаблон может быть использован в нескольких системах. Каждый из классов модели имеет навигационное свойство, указывающее на другую модель. Навигационные свойства обязательно инициализируются в конструкторе класса пустым списком.
// Базовый класс public class BaseDictionaryEntry < [Column(Order = 1)] [Key] public long Id < get; set; >public string Name < get; set; >public string Description < get; set; >> // Модель записи справочника использующих систем public class UsingSystem : BaseDictionaryEntry < public ListUploadTemplates < get; set; >public UsingSystem() < UploadTemplates = new List(); > > // Модель записи справочника шаблонов загрузки public class UploadTemplate : BaseDictionaryEntry < public string ProcessName < get; set; >public List UsingSystems < get; set; >public UploadTemplate() < UsingSystems = new List(); > >Контекст БД определим следующим образом:
// Контекст БД public class MyDbContext : DbContext < public virtual DbSetUploadTemplates < get; set; >public virtual DbSet UsingSystems < get; set; >public MyDbContext(DbContextOptions options) : base(options) < >public MyDbContext() < >>Startup.cs будет выглядеть следующим образом:
// Startup public class Startup < public IConfiguration Configuration < get; >public Startup(IConfiguration configuration) < Configuration = configuration; >public void ConfigureServices(IServiceCollection services) < // Конфигурируем контекст БД services.AddDbContext(options => options .UseNpgsql(Configuration.GetValue("ConString"), assembly => assembly.MigrationsAssembly(typeof(MyDbContext).Assembly.FullName)) .UseSnakeCaseNamingConvention()); services.AddControllers(); // Конфигурируем OData services.AddOData(); > public void Configure(IApplicationBuilder app, IWebHostEnvironment env) < if (env.IsDevelopment()) < app.UseDeveloperExceptionPage(); >app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => < endpoints.MapControllers(); endpoints.Select().Filter().OrderBy().Count().MaxTop(10).Expand(); // Добавляем пути OData endpoints.MapODataRoute("odata", "odata", GetEdmModel()); >); > // Настройка модели OData private IEdmModel GetEdmModel() < var odataBuilder = new ODataConventionModelBuilder(); odataBuilder.EntitySet("UsingSystems"); odataBuilder.EntitySet("UploadTemplates"); return odataBuilder.GetEdmModel(); > >Наконец, добавим простой контроллер для одного из справочников. Реализацию методов контроллера подробно рассмотрим в следующей статье.
// Контроллер public class UsingSystemsController : BaseDictionaryController < public UsingSystemsController(DbContext dbContext) : base(dbContext) < >[EnableQuery] public IActionResult Get() < return Ok(_dbContext.UsingSystems .Include(x =>x.UploadTemplates)); > [EnableQuery] public IActionResult Get(long key) < return Ok(_dbContext.UsingSystems .Where(x =>x.Id == key) .Include(x => x.UploadTemplates)); > >После запуска проекта получим следующее исключение:
System.InvalidOperationException: Unable to determine the relationship represented by navigation property ‘UsingSystem.UploadTemplates’ of type ‘List’. Either manually configure the relationship, or ignore this property using the ‘[NotMapped]’ attribute or by using ‘EntityTypeBuilder.Ignore’ in ‘OnModelCreating’.
Это означает, что Entity Framework Core не может понять, как ему связать две наших модели.
Решение проблемы со связью «многие-ко-многим» для .NET Core 3.1
Создавать связующую таблицу придется самостоятельно, определив отдельный класс. В классах UsingSystem и UploadTemplates, необходимо переписать навигационное свойство и его инициализацию пустым списком. Модель и контекст БД теперь выглядят так:
// Базовый класс public class BaseDictionaryEntry < [Column(Order = 1)] [Key] public long Id < get; set; >public string Name < get; set; >public string Description < get; set; >> // Модель записи справочника использующих систем public class UsingSystem : BaseDictionaryEntry < public ListUploadTemplateUsingSystems < get; set; >public UsingSystem() < UploadTemplateUsingSystems = new List(); > > // Модель записи справочника шаблонов загрузки public class UploadTemplate : BaseDictionaryEntry < public string ProcessName < get; set; >public List UploadTemplateUsingSystems < get; set; >public UploadTemplate() < UploadTemplateUsingSystems = new List(); > > // Модель для связывания сиситем и шаблонов public class UploadTemplateUsingSystem < public long UploadTemplateId < get; set; >public UploadTemplate UploadTemplate < get; set; >public long UsingSystemId < get; set; >public UsingSystem UsingSystem < get; set; >> // Контекст БД public class MyDbContext : DbContext < public virtual DbSetUploadTemplates < get; set; >public virtual DbSet UsingSystems < get; set; >public virtual DbSet UploadTemplateUsingSystems < get; set; >public MyDbContext(DbContextOptions options) : base(options) < >public MyDbContext() < >protected override void OnModelCreating(ModelBuilder modelBuilder) < modelBuilder.Entity() .HasKey(x => new ); modelBuilder.Entity() .HasOne(x => x.UploadTemplate) .WithMany(x => x.UploadTemplateUsingSystems) .HasForeignKey(x => x.UploadTemplateId); modelBuilder.Entity() .HasOne(x => x.UsingSystem) .WithMany(x => x.UploadTemplateUsingSystems) .HasForeignKey(x => x.UsingSystemId); > >Обратите внимание, что в связывающем классе следует определить свойства как для хранения внешнего ключа, так и для хранения самого объекта. Причем о том, что UploadTemplateUsingSystem имеет составной ключ, нужно сообщить и Edm модели. Это необходимо, чтобы использовать параметр $expand при обращении к нашему API. Метод GetEdmModel в Startup изменим следующим образом:
private IEdmModel GetEdmModel() < var odataBuilder = new ODataConventionModelBuilder(); odataBuilder.EntitySet("UsingSystems"); odataBuilder.EntitySet("UploadTemplates"); odataBuilder.EntityType() .HasKey(x => new ); return odataBuilder.GetEdmModel(); >Изменятся и методы контроллера:
[EnableQuery] public IActionResult Get() < return Ok(_dbContext.UsingSystems .Include(x =>x.UploadTemplateUsingSystems) .ThenInclude(x => x.UploadTemplate)); > [EnableQuery] public IActionResult Get(long key) < return Ok(_dbContext.UsingSystems .Where(x =>x.Id == key) .Include(x => x.UploadTemplateUsingSystems) .ThenInclude(x => x.UploadTemplate)); >После применения миграций в базе появится третья таблица с двумя полями – соответствующими внешними ключами. Кажется, проблема решена. Но давайте попробуем вытащить первую систему и из нее получить список разрешенных шаблонов. Вызовем наше API с параметром $expand следующим запросом:
OData выдаст ошибку, потому что не сможет найти навигационное свойство UploadTemplates в типе UsingSystem. Запрос нужно поправить следующим образом:
GET http://localhost:61268/odata/UsingSystems(1)?$expand= UploadTemplateUsingSystems($expand=UploadTemplates)
В ответ мы получим JSON следующего вида:
Видим, что идентификаторы передаются дважды, да и навигация по такому объекту усложняется. Это значит, что на вызывающей стороне также придется усложнять вызов API.
Обновление проекта до .NET 5
Готовый проект пришлось перетаскивать на .NET 5. Для этого мы изменили файл проекта следующим образом:
net5.0 all runtime; build; native; contentfiles; analyzers; buildtransitive Модель, контекст БД и контроллеры вернули в тот вид, в котором они были приведены в начале статьи. А вот в Startup с переходом к 8 версии (превью) Microsoft.AspNetCore.OData разрешенные методы манипуляции переехали в метод ConfigureServices:
public class Startup < public IConfiguration Configuration < get; >public Startup(IConfiguration configuration) < Configuration = configuration; >public void ConfigureServices(IServiceCollection services) < services.AddDbContext(options => options .UseNpgsql(Configuration.GetValue("ConStrings:Mdm"), assembly => assembly.MigrationsAssembly(typeof(MyDbContext).Assembly.FullName)) .UseSnakeCaseNamingConvention()); services.AddControllers(); // OData services.AddOData(opt => opt .AddModel("odata", GetEdmModel()) .Select() .Filter() .OrderBy() .Count() .Expand() ); > public void Configure(IApplicationBuilder app, IWebHostEnvironment env) < if (env.IsDevelopment()) < app.UseDeveloperExceptionPage(); >app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => < endpoints.MapControllers(); >); > private IEdmModel GetEdmModel() < var odataBuilder = new ODataConventionModelBuilder(); odataBuilder.EntitySet("UsingSystems"); odataBuilder.EntitySet("UploadTemplates"); return odataBuilder.GetEdmModel(); > >При применении миграций в базе создалась третья таблица для связывания. Результирующий JSON при выполнении запроса
стал выглядеть так:
Таким образом, реализация Entity FrameworkCore для .NET 5 позволила нам не только избавиться от ручного создания таблиц связей, но и упростить EDM модель и облегчить взаимодействие с OData на стороне клиента. Поэтому для создания подобных решений считаем .NET 5+ более предпочтительным выбором, чем .NET Core 3.1.
Getting the Web Root Path and the Content Root Path in ASP.NET Core
In ASP.NET Core, the physical paths to both the content root and the web root directories can be retrieved via the IWebHostEnvironment service. Here’s an example of a HomeController that uses constructor dependency injection to get an IWebHostEnvironment :
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; namespace AspNetCorePathMapping public class HomeController : Controller private readonly IWebHostEnvironment _env; public HomeController(IWebHostEnvironment env) _env = env; > public ActionResult Index() string contentRootPath = _env.ContentRootPath; string webRootPath = _env.WebRootPath; return Content(contentRootPath + "\n" + webRootPath); > > >This controller is part of a demo ASP.NET Core application that I scaffolded using the dotnet CLI. I ran the following command within the ~/aspnetcore-path-demo/src directory:
dotnet new mvc --name AspNetCorePathDemo.WebHere’s the default structure of the created project:
. └── aspnetcore-path-demo └── src └── AspNetCorePathDemo.Web ├── Controllers ├── Models ├── Properties ├── Views ├── bin ├── obj ├── wwwroot ├── Program.cs ├── Startup.cs ├── appsettings.Development.json ├── appsettings.json └── AspNetCorePathDemo.Web.csprojIf I open https://localhost:5001 in my browser, the above HomeController.Index() action returns the following two directory paths:

You can see that the content root path points to the root directory of the AspNetCorePathDemo.Web project, whereas the web root path points to the wwwroot directory. To summarize, here’s the distinction between the two root paths:
- The content root path is the absolute path to the directory that contains the application content files.
- The web root path is the absolute path to the directory that contains the web-servable application content files.
You can use either path in conjunction with the Path.Combine() method to construct a physical file path to a specific file or directory.
#ASP.NET Core
Prior to ASP.NET Core 3.0, we were using the IHostingEnvironment service instead of the IWebHostEnvironment service. Aside from the name difference, both service are used the same way:
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; namespace AspNetCorePathMapping public class HomeController : Controller private readonly IHostingEnvironment _env; public HomeController(IHostingEnvironment env) _env = env; > public ActionResult Index() string contentRootPath = _env.ContentRootPath; string webRootPath = _env.WebRootPath; return Content(contentRootPath + "\n" + webRootPath); > > >#ASP.NET Core RC1
Before ASP.NET Core RC2 — that is, up until ASP.NET Core RC1 — the application base path was available via IApplicationEnvironment.ApplicationBasePath :
using Microsoft.AspNet.Mvc; using Microsoft.Extensions.PlatformAbstractions; namespace AspNetCorePathMapping public class HomeController : Controller private readonly IApplicationEnvironment _env; public HomeController(IApplicationEnvironment appEnvironment) _env = env; > public ActionResult Index() return Content(_env.ApplicationBasePath); > > >#Classic ASP.NET
If you wanted to map relative or virtual paths to physical directories on the server in classic ASP.NET applications, you were always able to use the Server.MapPath() method to find the physical path of the web root directory like this:
public class HomeController : Controller public ActionResult Index() string physicalWebRootPath = Server.MapPath("~/"); return Content(physicalWebRootPath); > >In ASP.NET Core, however, the Server.MapPath() method doesn’t exist anymore. In fact, the Controller base class doesn’t even have a Server property.
Интеграционные тесты в ASP.NET Core

Тестирование — это очень важный аспект в написании кода. Интеграционные тесты, это «контроль качества» нашего приложения. Очень важно понимать как писать интаграционные тесты правильно. Сегодня мы рассмотрим как «запустить» колесо интеграционных тестов в ASP.NET Core для REST API.
Для начала создадим наш проект:

Как тип проекта выберем API

.NET нам создаст готовый класс, который будет прогнозировать погоду.

Давайте запустим и протестируем его в постмене:

Теперь приступим непосредственно к созданию тестов. Создадим проект с тестами:

Перед тем как писать сами тесты, нам нужно внести несколько изменений в Startup класс. В моем конкретном примере. я не использую базу данных. Но покажу вам как создать inmemory database для тестирования.
Первым делом установим Entity Framework пакеты с помощью команды:
install-package Microsoft.EntityFrameworkCore install-package Microsoft.EntityFrameworkCore.InMemory install-package Microsoft.EntityFrameworkCore.SqlServerТеперь модифицируем ConfigureServices для работы в тестах:
public void ConfigureServices(IServiceCollection services) < services.AddControllers(); AddDb(services); ConfigureDependencies(services); >public virtual void ConfigureDependencies(IServiceCollection services) < >private void AddDb(IServiceCollection services) < if (_currentEnvironment.IsEnvironment("Testing")) < services.AddDbContextPool(options => options.UseInMemoryDatabase("TestingDB")); > else < services.AddDbContextPool(options => options.UseSqlServer( Configuration.GetConnectionString("DefaultConnection"))); > >Полностью наш Startup класс будет выглядеть так:
public class Startup < private readonly IWebHostEnvironment _currentEnvironment; public IConfiguration Configuration < get; >public Startup(IConfiguration configuration, IWebHostEnvironment currentEnvironment) < Configuration = configuration; _currentEnvironment = currentEnvironment; >// This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) < services.AddControllers(); AddDb(services); ConfigureDependencies(services); >public virtual void ConfigureDependencies(IServiceCollection services) < >// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) < if (env.IsDevelopment()) < app.UseDeveloperExceptionPage(); >app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => < endpoints.MapControllers(); >); > private void AddDb(IServiceCollection services) < if (_currentEnvironment.IsEnvironment("Testing")) < services.AddDbContextPool(options => options.UseInMemoryDatabase("TestingDB")); > else < services.AddDbContextPool(options => options.UseSqlServer( Configuration.GetConnectionString("DefaultConnection"))); > > > public class ApplicationDbContext : DbContext < public ApplicationDbContext(DbContextOptionsoptions) : base(options) < >>Далее возвращаемся к проекту тестов который мы создали и добавим туда TestStartup
using System; using System.Collections.Generic; using System.Text; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Web; namespace Tests < public class TestStartup : Startup < public TestStartup(IConfiguration configuration, IWebHostEnvironment currentEnvironment) : base(configuration, currentEnvironment) < >public override void ConfigureDependencies(IServiceCollection services) < base.ConfigureDependencies(services); >> >ConfigureDependencies сейчас у меня не переопределяет никаких сервисов, но в реальном приложении, там бы мы переопределяли реальные сервисы нашими моками.
Для начала поставим тест хост для нашего проекта с тестами
install-package Microsoft.AspNetCore.TestHost -v 3.00Теперь добавим базовую фикстуру для XUnit тестов:
public class BaseTestServerFixture < public TestServer TestServer < get; >public ApplicationDbContext DbContext < get; >public HttpClient Client < get; >public BaseTestServerFixture() < var builder = new WebHostBuilder() .UseEnvironment("Testing") .UseStartup(); TestServer = new TestServer(builder); Client = TestServer.CreateClient(); DbContext = TestServer.Host.Services.GetService(); > public void Dispose() < Client.Dispose(); TestServer.Dispose(); >>Далее пишем сам тест для нашего «прогнозатора» погоды:
public class WeatherForecastControllerTests : IClassFixture < private readonly BaseTestServerFixture _fixture; public WeatherForecastControllerTests(BaseTestServerFixture fixture) < _fixture = fixture; >[Fact] public async Task Get_ShouldReturnListResult() < // Arrange var response = await _fixture.Client.GetAsync("/WeatherForecast/"); response.EnsureSuccessStatusCode(); var models = JsonConvert.DeserializeObject>(await response.Content.ReadAsStringAsync()); // Assert Assert.NotEmpty(models); > >Отмечу, что если вы не можете правильно выбрать именование для тестов, у нас была статья на эту тему.
Исходный код проекта можно найти тут.