Разработка приложений MVC ASP.NET Core

Совет

Это фрагмент из книги, архитектор современных веб-приложений с ASP.NET Core и Azure, доступный в документации .NET или в виде бесплатного скачиваемого PDF-файла, который можно читать в автономном режиме.

Архитектор современных веб-приложений с эскизами ASP.NET Core и azure eBook.

"Неважно, если что-то не получается вначале. Это жизненно важно, чтобы получить его правильно в последний раз". - Эндрю Хант и Дэвид Томас

ASP.NET Core является кроссплатформенной средой с открытым исходным кодом для создания современных приложений, оптимизированных для работы в облаке. Приложения ASP.NET Core отличаются простотой и модульностью, а также реализуют встроенную поддержку внедрения зависимостей, что позволяет расширить возможности тестирования и сопровождения. В сочетании с моделью MVC, которая поддерживает создание современных веб-API в дополнение к приложениям на основе представлений, ASP.NET Core представляет собой эффективную платформу для разработки веб-приложений корпоративного уровня.

MVC и Razor Pages

ASP.NET Core MVC предлагает множество функций, которые могут быть полезны для создания веб-API и приложений. Термин MVC означает "модель — представление — контроллер", шаблон пользовательского интерфейса, который разбивает обязанности по ответу на запросы пользователей на несколько частей. В дополнение к использованию этого шаблона вы также можете реализовать функции в приложениях ASP.NET Core как Razor Pages.

Razor Pages встроены в ASP.NET Core MVC и используют те же функции для маршрутизации, привязки модели, фильтров, авторизации и т. д. Но вместо отдельных папок и файлов для контроллеров, моделей, представлений и т. д., а также использования маршрутизации на основе атрибутов, Razor Pages помещаются в одну папку ("/Pages"), выстраивают маршрут на основе своего относительного расположения в этой папке и обрабатывают запросы с помощью обработчиков, а не действий контроллера. В результате при работе с Razor Pages все необходимые файлы и классы обычно размещаются рядом, а не распределяются по всему веб-проекту.

Узнайте больше о том, как шаблоны MVC, Razor Pages и другие применяются в примере приложения eShopOnWeb.

При создании нового приложения ASP.NET Core необходимо мысленно составить план приложения, которое вы хотите создать. При создании проекта в интегрированной среде разработки или с помощью команды CLI dotnet new вы выбираете один из нескольких шаблонов. Наиболее распространенные шаблоны проектов: "Пусто", "Веб-API", "Веб-приложение" и "Web App (Model-View-Controller)" (Веб-приложение (MVC)). Хотя вы принимаете это решение только при создании проекта, вы можете впоследствии передумать. В проекте веб-API используются стандартные контроллеры шаблона "модель — представление — контроллер", просто по умолчанию там нет представлений. Аналогичным образом шаблон веб-приложения по умолчанию использует Razor Pages, поэтому там тоже нет папки с представлениями. Вы можете добавить папку с представлениями в эти проекты позже для поддержки поведения на основе представлений. Проекты веб-API и "модель — представление — контроллер" не включают папку страниц по умолчанию, но ее можно добавить позже для поддержки поведения на основе Razor Pages. Эти три шаблона поддерживают три различных типа взаимодействия с пользователем по умолчанию: данные (веб-API), на базе страниц и на базе представлений. Но при желании вы можете сочетать некоторые или все эти шаблоны в одном проекте.

Почему Razor Pages?

Razor Pages — это подход по умолчанию для новых веб-приложений в Visual Studio. Razor Pages предлагает более простой способ создания функций приложения на основе страниц, например, формы в не одностраничных приложениях. При использовании контроллеров и представлений у приложений обычно были очень большие контроллеры, которые работали со множеством различных зависимостей и моделей представлений и возвращали множество различных представлений. Это приводило к массе сложностей, и контроллеры часто не могли эффективно следовать принципу единой ответственности или принципам открытости/закрытости. Razor Pages решает эту проблему за счет инкапсуляции логики на стороне сервера для данной логической "страницы" в веб-приложение с разметкой Razor. Страница Razor без серверной логики может просто состоять из файла Razor (например, Index.cshtml). Но большинство нетривиальных страниц Razor Pages будет иметь соответствующий класс модели страницы, которая по соглашению называется так же, как файл Razor с расширением .cs (например, Index.cshtml.cs).

Модель страницы Razor сочетает в себе обязанности контроллера MVC и модели представления. Вместо обработки запросов с помощью методов действий контроллера выполняются обработчики модели страницы, такие как OnGet(), подготавливая к просмотру соответствующую страницу по умолчанию. Razor Pages упрощает процесс создания отдельных страниц в приложения ASP.NET Core, предоставляя при этом все архитектурные компоненты ASP.NET Core MVC. Это неплохой выбор по умолчанию для новых функциональных возможностей на основе страниц.

Когда использовать MVC

При создании веб-API шаблон MVC подходит больше, чем Razor Pages. Если проект будет предоставлять только конечные точки веб-API, в идеале следует начать с шаблона проекта веб-API. В противном случае вы можете легко добавить контроллеры и связанные конечные точки API для любого приложения ASP.NET Core. Шаблон MVC на основе представления следует использовать в том случае, если вы переносите имеющееся приложение ASP.NET MVC 5 или более ранней версии в ASP.NET Core MVC и хотите затратить минимум усилий. После завершения первоначальной миграции вы можете оценить, нужно ли использовать Razor Pages для новых функций или для миграции в целом. Дополнительные сведения о переносе приложений .NET 4.x в .NET 8 см. в статье Перенос существующих ASP.NET приложений в ASP.NET Core eBook.

Независимо от выбранного подхода (Razor Pages или представления MVC) ваше приложение будет иметь схожую производительность и будет включать поддержку внедрения зависимостей, фильтров, привязки, проверки модели и т. д.

Сопоставление запросов с ответами

Суть приложений ASP.NET Core заключается в сопоставлении входящих запросов с исходящими ответами. На низком уровне это сопоставление реализуется за счет ПО промежуточного слоя, и простые приложения и микрослужбы ASP.NET Core могут полностью состоять из пользовательского ПО промежуточного слоя. При использовании модели MVC ASP.NET Core вы можете работать на более высоком уровне на основе маршрутов, контроллеров и действий. Каждый входящий запрос сравнивается с таблицей маршрутизации приложения. При обнаружении подходящего маршрута для обработки запроса вызывается связанный метод действия, который принадлежит контроллеру. Если подходящий маршрут не обнаружен, вызывается обработчик ошибок (в этом случае он возвращает результат NotFound).

Приложения MVC ASP.NET Core могут использовать обычные маршруты, маршруты с атрибутами или оба вида маршрутов. Обычные маршруты определяются в коде путем указания соглашений о маршрутизации с использованием показанного в следующем примере синтаксиса:

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");
});

В этом примере в таблицу маршрутизации добавлен маршрут "default". Он определяет шаблон маршрута с заполнителями controller, action и id. Заполнители controller и action имеют значения по умолчанию (Home и Index соответственно). Заполнитель id является необязательным (что указывает примененный к нему знак "?"). В определенном здесь соглашении указывается, что первая часть запроса должна соответствовать имени контроллера, вторая часть — действию, а третья (если требуется) представляет параметр идентификатора. Обычно стандартные маршруты определяются для приложения в одном расположении, например в файле Program.cs, где настроен конвейер ПО промежуточного слоя для запросов.

Маршруты с атрибутами применяются напрямую к контроллерам и действиям, а не задаются глобально. Это делает их более доступными для обнаружения в рамках конкретного метода, однако при этом сведения о маршрутизации могут храниться в разных местах в приложении. С помощью маршрутов с атрибутами можно с легкостью задавать несколько маршрутов для конкретного действия, а также комбинировать маршруты между контроллерами и действиями. Например:

[Route("Home")]
public class HomeController : Controller
{
    [Route("")] // Combines to define the route template "Home"
    [Route("Index")] // Combines to define route template "Home/Index"
    [Route("/")] // Does not combine, defines the route template ""
    public IActionResult Index() {}
}

Маршруты можно задать с использованием [HttpGet] и схожих атрибутов, что позволяет отказаться от добавления отдельных атрибутов [Route]. Маршруты с атрибутами также могут использовать маркеры, что позволяет сократить объем использования имен контроллера или действия, как показано ниже:

[Route("[controller]")]
public class ProductsController : Controller
{
    [Route("")] // Matches 'Products'
    [Route("Index")] // Matches 'Products/Index'
    public IActionResult Index() {}
}

Razor Pages не использует маршрутизацию атрибутов. Вы можете указать дополнительные сведения о шаблоне маршрута для страницы Razor в директиве @page:

@page "{id:int}"

В предыдущем примере рассматриваемая страница будет сопоставлять маршрут с целочисленным параметром id. Например, страница Products.cshtml, расположенная в корне /Pages, будет отвечать на запросы, подобные этому:

/Products/123

После того как нужный запрос сопоставлен с маршрутом, но до вызова метода действия, модель MVC ASP.NET Core выполняет привязку и проверку модели по запросу. Привязка модели необходима для преобразования входящих данных HTTP в типы .NET, которые задаются в виде параметров вызываемого метода действия. Например, если метод действия ожидает параметр int id, привязка модели попытается получить этот параметр из значения, предоставленного в рамках запроса. Для этого привязка модели будет искать значения в опубликованной форме, в самом маршруте, а также в строке запроса. Если значение id найдено, оно преобразовывается в целое число, а затем передается в метод действия.

После привязки модели, но перед вызовом метода действия выполняется проверка модели. При проверке используются необязательные атрибуты типа модели, что позволяет убедиться, что предоставленный объект модели соответствует определенным требованиям к данным. Некоторые значения могут быть указаны как обязательные или ограничены определенной длиной или числовым диапазоном и т. д. Если указаны атрибуты проверки, но модель не соответствует их требованиям, свойство ModelState.IsValid будет false, а набор правил проверки сбоя будет доступен для отправки клиенту, выполняющего запрос.

Если вы используете проверку модели, необходимо всегда проверять допустимость модели, прежде чем выполнять какие-либо изменяющие состояние команды. Это позволит предотвратить повреждение приложения из-за недопустимых данных. Чтобы не добавлять код для этой проверки в каждое действие, можно использовать фильтр. Фильтры MVC ASP.NET Core предназначены для перехвата групп запросов с той целью, чтобы применить к ним общие политики или сквозную функциональность. Фильтры могут применяться к отдельным действиям, ко всему контроллеру или глобально на уровне приложения.

Для веб-API модель MVC ASP.NET Core поддерживает согласование содержимого, что позволяет указывать в запросах требуемый формат ответа. На основании представленных в запросе заголовков действия, возвращающие данные, будут использовать для ответа XML, JSON или другой поддерживаемый формат. Эта возможность позволяет использовать один и тот же API нескольким клиентам с разными требованиями к формату.

Проекты веб-API могут использовать атрибут [ApiController], который может применяться к отдельным контроллерам, базовому классу контроллера или ко всей сборке. Этот атрибут добавляет автоматическую проверку модели, и действие с недопустимой моделью вернет ошибку BadRequest с подробными сведениями об ошибках проверки. Атрибут также требует, чтобы все действия имели маршрут атрибута, а не использовали маршрут на основе соглашения, и возвращает более подробные сведения ProblemDetails в ответ на ошибки.

Управление контроллерами

Для приложений на основе Razor Pages важно избегать слишком большого размера контроллеров. Каждой отдельной странице присваиваются собственные файлы и классы, выделенные только для своих обработчиков. До появления Razor Pages многие приложения, ориентированные на представление, будут иметь большие классы контроллеров, отвечающие за множество различных действий и представлений. Эти классы естественным образом увеличиваются с учетом множества обязанностей и зависимостей, что затрудняет их обслуживание. Если вы обнаружите, что контроллеры на основе представления слишком велики, рассмотрите возможность их рефакторинга для использования Razor Pages или введения шаблона, такого как медиатор.

Шаблон проектирования медиатора используется для сокращения взаимосвязей между классами и разрешает обмен данными между ними. В приложениях MVC ASP.NET Core этот шаблон часто используется для разбиения контроллеров на небольшие части с помощью обработчиков для выполнения методов действия. Для решения этой задачи часто используется популярный пакет MediatR NuGet. Как правило, контроллеры включают множество различных методов действий, каждый из которых может требовать определенных зависимостей. Набор всех зависимостей, необходимых для любого действия, должен быть передан в конструктор контроллера. При использовании MediatR контроллер обычно имеет единственную зависимость — от экземпляра медиатора. Затем каждое действие использует экземпляр медиатора для отправки сообщения, обрабатываемого обработчиком. Этот обработчик относится только к одному действию и поэтому требует только необходимые для этого действия зависимости. Пример контроллера, использующего MediatR, показан здесь:

public class OrderController : Controller
{
    private readonly IMediator _mediator;

    public OrderController(IMediator mediator)
    {
        _mediator = mediator;
    }

    [HttpGet]
    public async Task<IActionResult> MyOrders()
    {
        var viewModel = await _mediator.Send(new GetMyOrders(User.Identity.Name));
        return View(viewModel);
    }
    // other actions implemented similarly
}

В действии MyOrders сообщение GetMyOrders для вызова Send обрабатывается этим классом:

public class GetMyOrdersHandler : IRequestHandler<GetMyOrders, IEnumerable<OrderViewModel>>
{
    private readonly IOrderRepository _orderRepository;
    public GetMyOrdersHandler(IOrderRepository orderRepository)
    {
        _orderRepository = orderRepository;
    }

  public async Task<IEnumerable<OrderViewModel>> Handle(GetMyOrders request, CancellationToken cancellationToken)
    {
        var specification = new CustomerOrdersWithItemsSpecification(request.UserName);
        var orders = await _orderRepository.ListAsync(specification);
        return orders.Select(o => new OrderViewModel
            {
                OrderDate = o.OrderDate,
                OrderItems = o.OrderItems?.Select(oi => new OrderItemViewModel()
                  {
                    PictureUrl = oi.ItemOrdered.PictureUri,
                    ProductId = oi.ItemOrdered.CatalogItemId,
                    ProductName = oi.ItemOrdered.ProductName,
                    UnitPrice = oi.UnitPrice,
                    Units = oi.Units
                  }).ToList(),
                OrderNumber = o.Id,
                ShippingAddress = o.ShipToAddress,
                Total = o.Total()
        });
    }
}

Конечный результат этого подхода заключается в том, чтобы сделать контроллеры намного меньше, а также в основном ориентирован на маршрутизацию и привязку модели, в то время как отдельные обработчики отвечают конкретным задачам, необходимым для конкретной конечной точки. Этот подход также можно реализовать без MediatR с помощью пакета ApiEndpoints NuGet, который позволяет реализовать в контроллерах API те же преимущества, которые Razor Pages предоставляет для контроллеров, основанных на представлении.

Ссылки — сопоставление запросов с ответами

Работа с зависимостями

ASP.NET Core реализует встроенную поддержку и использование методики внедрения зависимостей. Методика внедрения зависимостей позволяет обеспечить слабую связанность между разными частями приложения. Такая реализация является желательной, поскольку упрощает изоляцию частей приложения, что делает их тестирование или замену более эффективными. Кроме того, в этом случае снижается вероятность того, что изменения в одной части приложения будут иметь непредвиденное влияние на какую-либо другую часть. Внедрение зависимостей осуществляется на основе принципа инверсии зависимостей и часто является основным фактором, определяющим соблюдение принципа открытости/закрытости. При оценке того, как ваше приложение работает с зависимостями, следует учитывать риск возникновения проблем со статическим сцеплением кода и помнить о том, что ключевое слово new является "клеем" для вашего приложения.

Статическое сцепление происходит тогда, когда классы вызывают статические методы или обращаются к статическим свойствам, в результате чего в инфраструктуре возникают побочные эффекты или зависимости. Например, если вы используете метод, который вызывает статический метод, осуществляющий запись в базу данных, ваш метод будет тесно связан с базой. В таком случае любой сбой в вызове базы данных приведет к нарушению работы вашего метода. Тестирование этих методов представляет собой серьезную проблему, поскольку для проведения таких тестов требуется, чтобы коммерческие библиотеки макетирования имитировали статические вызовы. Иначе подобную проверку можно провести только при наличии тестовой базы данных. Статические вызовы, не имеющие зависимостей от инфраструктуры, особенно те, которые полностью не поддерживают отслеживание состояния, можно вызывать без опаски, так как они не влияют на степень связанности или возможности тестирования (помимо связанности самого кода со статическим вызовом).

Многие разработчики осознают риски статического сцепления и глобального состояния, однако по-прежнему допускают связанность кода с конкретными реализациями из-за прямого создания экземпляров. Об этом напоминает принцип, что ключевое слово new является "клеем" для вашего приложения. Однако это ни в коем случае не значит, что от слова new следует полностью отказаться. Как и в случае с вызовами статических методов, создание новых экземпляров типов, не имеющих внешних зависимостей, как правило, не приводит к тесной привязке кода к деталям реализации и не усложняет тестирование. Однако каждый раз, когда создается экземпляр класса, следует задуматься о том, имеет ли смысл жесткая привязка этого экземпляра к конкретному месту или эффективнее будет реализовать запрос этого экземпляра в качестве зависимости.

Объявление зависимостей

В основе ASP.NET Core лежит принцип объявления методами и классами собственных зависимостей посредством их запроса в виде аргументов. Приложения ASP.NET обычно настраиваются в файле Program.cs или в классе Startup.

Примечание.

Настройка приложений полностью в Program.cs — это подход по умолчанию для приложений .NET 6 (и более поздних версий) и Visual Studio 2022. Мы обновили шаблоны проектов, чтобы помочь вам начать работу с этим новым подходом. При необходимости в проектах ASP.NET Core по-прежнему можно использовать класс Startup.

Настройка служб в файле Program.cs

Для очень простых приложений зависимости можно связать прямо в файле Program.cs с помощью WebApplicationBuilder. После добавления всех необходимых служб для создания приложения используется построитель.

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();

var app = builder.Build();

Настройка служб в файле Startup.cs

Файл Startup.cs сам по себе настроен для поддержки внедрения зависимостей в нескольких точках. Если вы используете класс Startup, можно задать для него конструктор, который может запрашивать зависимости с помощью этого класса, например так:

public class Startup
{
    public Startup(IHostingEnvironment env)
    {
        var builder = new ConfigurationBuilder()
            .SetBasePath(env.ContentRootPath)
            .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
            .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);
    }
}

Класс Startup интересен тем, что для него не определены явные требования к типу. Он не наследуется от конкретного базового класса Startup и не реализует какой-либо конкретный интерфейс. Он может иметь конструктор, для которого вы можете задать любое число параметров, однако при необходимости от конструктора можно отказаться. При запуске веб-узла, который вы настроили для своего приложения, вызывается указанный вами класс Startup, в результате чего все требуемые классом Startup зависимости заполняются посредством внедрения зависимостей. Естественно, если запросить параметры, которые не были настроены в контейнере служб, используемом ASP.NET Core, будет возвращено исключение. Тем не менее, если вы работаете с известными контейнеру зависимостями, вы можете запрашивать все, что угодно.

Ваше приложение ASP.NET Core будет поддерживать внедрение зависимостей с самого начала с момента создания экземпляра Startup. Этим поддержка для класса Startup не ограничивается. Вы также можете запрашивать зависимости в методе Configure:

public void Configure(IApplicationBuilder app,
    IHostingEnvironment env,
    ILoggerFactory loggerFactory)
{

}

Исключением является метод ConfigureServices, который принимает всего один параметр типа IServiceCollection. Он действительно не нуждается в поддержке внедрения зависимостей, так как с одной стороны он несет ответственность за добавление объектов в контейнер служб, а с другой — доступ ко всем настроенным службам через IServiceCollection параметр. Таким образом, вы можете работать с зависимостями, определенными в коллекции служб ASP.NET Core, в любой части класса Startup, запрашивая необходимую службу в виде параметра или используя параметр IServiceCollection метода ConfigureServices.

Примечание.

Если вам необходимо гарантировать доступность определенных служб для класса Startup, вы можете настроить их с помощью IWebHostBuilder и его метода ConfigureServices в вызове CreateDefaultBuilder.

Класс Startup выступает в качестве модели, определяющей требуемую структуру для других частей вашего приложения ASP.NET Core, включая контроллеры, ПО промежуточного слоя, фильтры и ваши собственные службы. В каждом случае необходимо соблюдать принцип явных зависимостей и запрашивать зависимости, а не создавать их. Для этих целей в приложении следует использовать внедрение зависимостей. Обращайте внимание на то, где и как осуществляется прямое создание экземпляров реализаций, особенно для служб и объектов, которые работают с инфраструктурой или имеют побочные эффекты. Отдавайте предпочтение работе с абстракциями, которые определены в ядре приложения и передаются в качестве аргументов, и не увлекайтесь жестким определением ссылок на конкретные типы реализации.

Структурирование приложения

Монолитные приложения обычно имеют одну точку входа. Для веб-приложения ASP.NET Core точкой входа служит веб-проект ASP.NET Core. Тем не менее это не означает, что решение обязательно должно состоять из одного проекта. В соответствии с принципом разделения задач рекомендуется разбивать приложение на слои. После этого следует рассмотреть возможность разделения проектов по папкам, что позволит достичь более эффективной инкапсуляции. Оптимальным способом достичь этих целей в приложении ASP.NET Core является использование варианта чистой архитектуры, которая обсуждается в главе 5. При таком подходе решение приложения должно состоять из отдельных библиотек для пользовательского интерфейса, инфраструктуры и ядра приложения.

В дополнение к этим проектам также включаются отдельные тестовые проекты (тестирование обсуждается в главе 9).

Объектная модель и интерфейсы приложения должны располагаться в проекте ядра приложения. Этот проект будет содержать минимально возможное количество зависимостей (и ни одной в соответствии с требованиями определенной инфраструктуры), и на него будут ссылаться другие проекты решения. Необходимые бизнес-сущности определяются в проекте ядра приложения, как и службы, которые не зависят напрямую от инфраструктуры.

Детали реализации, в том числе способ реализации сохраняемости или отправки уведомлений пользователю, определяются в проекте инфраструктуры. Этот проект будет ссылаться на зависящие от реализации пакеты, например Entity Framework Core, однако не должен предоставлять детали таких реализаций за пределы проекта. Службы и репозитории инфраструктуры должны реализовывать интерфейсы, которые определены в проекте ядра приложения, а ее реализации сохраняемости обеспечивают извлечение и хранение сущностей, определенных в ядре приложения.

Проект пользовательского интерфейса ASP.NET Core отвечает за выполнение задач уровня пользовательского интерфейса, но не должен включать детали бизнес-логики или инфраструктуры. Фактически, в идеальном случае он не должен иметь зависимостей от проекта инфраструктуры, что позволяет исключить случайное появление зависимостей между этими двумя проектами. Для этого может использоваться сторонний контейнер внедрения зависимостей, например Autofac, который позволяет определять правила внедрения зависимостей в классах модуля в каждом проекте.

Другой подход к ослаблению связанности приложения с деталями реализации заключается в вызове из приложения микрослужб, которые могут быть развернуты в отдельных контейнерах Docker. Таким образом обеспечивается дополнительное разделение задач и ослабление связанности по сравнению с внедрением зависимостей между двумя проектами, однако такой подход связан с определенными сложностями.

Организация компонентов

По умолчанию структура папок приложения ASP.NET Core включает контроллеры, представления и зачастую модели представлений. Код на стороне клиента, обеспечивающий поддержку таких структур на стороне сервера, как правило, хранится отдельно в папке wwwroot. Тем не менее в крупных приложениях при такой организации могут возникать проблемы, поскольку для работы с конкретным компонентом может потребоваться переход между этими папками. С увеличением числа файлов и вложенных папок в каждой папке сложности только возрастают, в связи с чем приходится тратить дополнительное время на просмотр в обозревателе решений. Одним из решений этой проблемы может стать упорядочение кода приложения по компонентам, а не по типу файлов. Этот стиль организации обычно называется папками компонентов или срезами компонентов (см. также: вертикальные срезы).

Для этой цели в модели MVC ASP.NET Core поддерживаются области. С помощью областей можно создавать отдельные наборы папок контролеров и представлений (а также связанных с ними моделей) в каждой папке области. На рис. 7-1 показан пример структуры папок, в которой используются области.

Пример организации области

Рис. 7-1. Пример организации области

При работе с областями необходимо использовать атрибуты, чтобы декорировать контроллеры с указанием имени области, которой они принадлежат:

[Area("Catalog")]
public class HomeController
{}

Также необходимо добавить поддержку областей для маршрутов:

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(name: "areaRoute", pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");
    endpoints.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");
});

Помимо встроенной поддержки областей, вы можете использовать собственную структуру папок и соглашения вместо атрибутов и настраиваемых маршрутов. В этом случае у вас будут папки компонентов, не содержащие отдельных папок для представлений, контроллеров и т. д., благодаря чему иерархия будет иметь меньше уровней, что позволит упростить поиск всех файлов для конкретного компонента в одном месте. Для API-интерфейсов можно использовать папки для замены контроллеров, и каждая папка может содержать все конечные точки API и связанные с ними DTO.

В ASP.NET Core это поведение реализуется с использованием встроенных типов соглашений. При необходимости эти соглашения можно изменить или заменить. Например, вы можете определить соглашение, по которому имя компонента для указанного контроллера будет получаться автоматически на основе его пространства имен (оно обычно связано с папкой, в которой располагается контроллер):

public class FeatureConvention : IControllerModelConvention
{
    public void Apply(ControllerModel controller)
    {
        controller.Properties.Add("feature",
        GetFeatureName(controller.ControllerType));
    }

    private string GetFeatureName(TypeInfo controllerType)
    {
        string[] tokens = controllerType.FullName.Split('.');
        if (!tokens.Any(t => t == "Features")) return "";
        string featureName = tokens
            .SkipWhile(t => !t.Equals("features", StringComparison.CurrentCultureIgnoreCase))
            .Skip(1)
            .Take(1)
            .FirstOrDefault();
        return featureName;
    }
}

Затем это соглашение указывается в качестве варианта при добавлении поддержки модели MVC для приложения в ConfigureServices (или в Program.cs):

// ConfigureServices
services.AddMvc(o => o.Conventions.Add(new FeatureConvention()));

// Program.cs
builder.Services.AddMvc(o => o.Conventions.Add(new FeatureConvention()));

Модель MVC ASP.NET Core также использует соглашение для поиска представлений. Вы можете переопределить его с использованием настраиваемого соглашения, чтобы задать поиск представлений в папках компонентов (будет использоваться имя компонента, предоставленное ранее с помощью FeatureConvention). Узнать больше об этом подходе и скачать рабочий пример можно в статье MSDN Magazine, посвященной срезам компонентов для модели MVC ASP.NET Core.

API и приложения Blazor

Если приложение содержит набор веб-API, которые должны быть защищены, эти API должны быть идеально настроены как отдельный проект из приложения View или Razor Pages. Разделение API, особенно общедоступных API, из веб-приложения на стороне сервера имеет ряд преимуществ. Эти приложения часто будут иметь уникальные характеристики развертывания и загрузки. Кроме того, они, вероятно, применяют различные механизмы безопасности, благодаря приложениям на основе файлов, основанным на файлах cookie, и интерфейсам API, которые, скорее всего, используют проверку подлинности на основе маркеров.

Кроме того, приложения, независимо от того, Blazor используется Blazor ли сервер или BlazorWebAssemblyдолжны создаваться в виде отдельных проектов. Приложения имеют различные характеристики среды выполнения, а также модели безопасности. Они, скорее всего, будут совместно использовать общие типы с веб-приложением на стороне сервера (или проектом API), и эти типы должны быть определены в общем проекте.

BlazorWebAssembly Добавление интерфейса администрирования в eShopOnWeb требует добавления нескольких новых проектов. Сам BlazorWebAssemblyBlazorAdminпроект. Новый набор общедоступных конечных точек API, используемых BlazorAdmin и настроенных для использования проверки подлинности на основе маркеров, определяется в проекте PublicApi. Некоторые общие типы, используемые обоими проектами, хранятся в новом проекте BlazorShared.

Однако зачем добавлять отдельный проект BlazorShared, если уже существует общий проект ApplicationCore, с помощью которого можно совместно использовать типы, необходимые как PublicApi, так и BlazorAdmin? Ответ заключается в том, что этот проект включает в себя всю бизнес-логику приложения. Поэтому он гораздо больше, чем требуется, и с большей вероятностью нужно гарантировать его безопасность на сервере. Помните, что любая библиотека, на которую ссылается BlazorAdmin, будет скачана браузерами пользователей при загрузке приложения Blazor.

В зависимости от того, используется ли этот шаблон серверных интерфейсов (BFF), API, используемые BlazorWebAssembly приложением, могут не совместно использовать их типы 100 % Blazor. В частности, общедоступный API, предназначенный для использования многими разными клиентами, может определять собственные типы запросов и результатов вместо того, чтобы предоставлять общий доступ к ним в общем проекте, ориентированном на клиент. В примере eShopOnWeb предполагается, что в проекте PublicApi на самом деле размещается общедоступный API, поэтому не все типы запросов и ответов берутся из проекта BlazorShared.

Сквозные функции

По мере расширения приложения все большую важность приобретает вопрос вынесения сквозной функциональности, что позволяет исключить дублирование и обеспечить согласованность. В качестве примеров сквозной функциональности в приложениях ASP.NET Core можно привести задачи проверки подлинности, правила проверки моделей, кэширование вывода и обработку ошибок. Фильтры в ASP.NET Core MVC позволяют выполнять код до или после определенных стадий в конвейере обработки запросов. Например, фильтр можно выполнять до и после привязки модели, выполнения действия или получения результата действия. Кроме того, с помощью фильтра авторизации можно управлять доступом к оставшейся части конвейера. На рис. 7-2 показан поток выполнения запроса через настроенные фильтры.

Запрос обрабатывается с помощью фильтров авторизации, фильтров ресурсов, привязки моделей, фильтров действий, преобразования результатов выполнения действий и выполнения действий, фильтров исключений, фильтров результатов и выполнения результатов. В выходе запрос обрабатывается только фильтрами результатов и фильтрами ресурсов, прежде чем стать ответом, отправленным клиенту.

Рис. 7-2. Выполнение запроса через фильтры и конвейер запросов.

Фильтры обычно реализуются в виде атрибутов, поэтому их можно применять к контроллерам или действиям (или даже глобально). При добавлении таким способом фильтры, задаваемые на уровне действия, переопределяют (или используют в качестве основы) фильтры уровня контроллера, которые, в свою очередь, переопределяют глобальные фильтры. Например, [Route] атрибут можно использовать для создания маршрутов между контроллерами и действиями. Аналогичным образом можно настроить авторизацию на уровне контроллера и затем переопределить ее для отдельных действий, как показано в следующем примере:

[Authorize]
public class AccountController : Controller
{
    [AllowAnonymous] // overrides the Authorize attribute
    public async Task<IActionResult> Login() {}
    public async Task<IActionResult> ForgotPassword() {}
}

Первый метод Login использует [AllowAnonymous] фильтр (атрибут) для переопределения набора фильтров авторизации на уровне контроллера. Действие ForgotPassword (и любое другое действие в классе, у которых нет атрибута AllowAnonymous), потребует проверки подлинности запроса.

С помощью фильтров можно исключить дублирование, выражаемое общими политиками обработки ошибок для API. Например, типичная политика API — возврат ответа NotFound на запросы, ссылающиеся на ключи, которые не существуют, и ответ, если проверка модели завершается ошибкой BadRequest . Действие этих двух политик демонстрируется в следующем примере:

[HttpPut("{id}")]
public async Task<IActionResult> Put(int id, [FromBody]Author author)
{
    if ((await _authorRepository.ListAsync()).All(a => a.Id != id))
    {
        return NotFound(id);
    }
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
    author.Id = id;
    await _authorRepository.UpdateAsync(author);
    return Ok();
}

Не перегружайте свои методы действия подобным условным кодом. Вместо этого следует вынести политики в фильтры, которые будут применяться по необходимости. В этом примере проверку модели, которая должна выполняться каждый раз при отправке команды в API, можно заменить следующим атрибутом:

public class ValidateModelAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        if (!context.ModelState.IsValid)
        {
            context.Result = new BadRequestObjectResult(context.ModelState);
        }
    }
}

Вы можете добавить ValidateModelAttribute в проект как зависимость NuGet, включив пакет Ardalis.ValidateModel. Для интерфейсов API можно использовать атрибут ApiController для обеспечения такого поведения без отдельного фильтра ValidateModel.

Аналогичным образом с помощью фильтра можно проверить факт существования записи и вернуть ошибку 404 до выполнения действия. Это позволит исключить необходимость выполнять такие проверки в самом действии. После вынесения общих соглашений и организации решения таким образом, чтобы отделить код инфраструктуры и бизнес-логики от пользовательского интерфейса, метод действия MVC будет иметь минимальный размер:

[HttpPut("{id}")]
[ValidateAuthorExists]
public async Task<IActionResult> Put(int id, [FromBody]Author author)
{
    await _authorRepository.UpdateAsync(author);
    return Ok();
}

Узнать больше о реализации фильтров и скачать рабочий пример можно в статье MSDN Magazine, посвященной применению фильтров MVC для ASP.NET Core в реальном мире.

Если вы обнаружите, что у вас есть несколько распространенных ответов от API на основе типичных сценариев, таких как ошибки проверки ("Недопустимый запрос"),"Ресурс не найден" и ошибки сервера, можно использовать абстракцию result. Результат абстракции будет возвращен службами, используемыми конечными точками API, а действие контроллера или конечная точка будут использовать фильтр для преобразования этих IActionResultsданных.

Ссылки — структурирование приложений

Безопасность

Обеспечение безопасности веб-приложений — это серьезная проблема, требующая решения множества задач. На базовом уровне обеспечение безопасности подразумевает знание источника запроса и предоставление этому запросу доступа только к необходимым ресурсам. Процесс проверки подлинности предусматривает сравнение предоставленных в запросе учетных данных с теми, которые находятся в хранилище доверенных данных. Это позволяет определить, является ли источник запроса известной сущностью. Процесс авторизации предусматривает ограничение доступа к определенным ресурсам на основе удостоверения пользователя. Еще одна задача заключается в защите от перехвата запросов третьими лицами. Для этого в приложении как минимум должен использоваться протокол SSL.

Идентификация

Удостоверение ASP.NET Core — это система членства, с помощью которой обеспечивается поддержка функций входа в ваше приложение. Благодаря этому поддерживаются учетные записи локальных пользователей, а также внешние поставщики служб входа, включая учетные записи Майкрософт, Twitter, Facebook, Google и многие другие. В дополнение к удостоверению ASP.NET Core в приложении может использоваться проверка подлинности Windows или сторонний поставщик служб проверки подлинности, например Identity Server.

Удостоверение ASP.NET Core включается в шаблоны новых проектов в том случае, если выбран параметр "Учетные записи отдельных пользователей". Этот шаблон включает поддержку регистрации, входа, внешних имен входа, забытых паролей и других дополнительных функций.

Выберите

Рис. 7-3. Выбор "Учетные записи отдельных пользователей" для использования предварительно настроенных идентификаторов.

Поддержка удостоверений настраивается в файле Program.cs или в Startup и включает в себя настройку служб и ПО промежуточного слоя.

Настройка удостоверения в Program.cs

В Program.cs вы настраиваете службы из экземпляра WebHostBuilder, а затем после создания приложения настраиваете его ПО промежуточного слоя. Ключевые моменты, на которые следует обратить внимание, — это вызов AddDefaultIdentity для требуемых служб, а также вызовы UseAuthentication и UseAuthorization, которые добавляют необходимое ПО промежуточного слоя.

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
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.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();

app.Run();

Настройка удостоверения при запуске приложения

// Add framework services.
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders();
builder.Services.AddMvc();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();

Важно, чтобы UseAuthentication и UseAuthorization отображались раньше MapRazorPages. При настройке служб удостоверений вы заметите вызов AddDefaultTokenProviders. Этот вызов не связан с маркерами, которые могут использоваться для защиты веб-соединений. Он относится к поставщикам, создающим запросы, которые могут направляться пользователям в сообщениях электронной почты или SMS для подтверждения их личности.

Дополнительные сведения о настройке двухфакторной проверки подлинности и использовании внешних поставщиков служб входа см. в официальной документации по ASP.NET Core.

Проверка подлинности

Проверка подлинности — это процесс установления личности пользователей, обращающихся к системе. Если вы используете ASP.NET Core Identity и методы конфигурации, приведенные в предыдущем разделе, он автоматически настроит некоторые параметры проверки подлинности в приложении. Однако эти значения по умолчанию можно также настроить вручную или переопределить значения, заданные с помощью AddIdentity. Если вы используете удостоверение, настройте проверку подлинности на основе файлов cookie в качестве схемы по умолчанию.

При проверке подлинности на основе веб-служб, как правило, существует до пяти действий, которые можно выполнять для проверки подлинности клиента системы. К ним относятся:

  • Пройти проверку подлинности. Используйте сведения, предоставленные клиентом, чтобы создать удостоверение, которое он сможет использовать в приложении.
  • Задача. Это действие используется, чтобы клиент представлялся самостоятельно.
  • Запрет. Сообщение клиенту, что ему запрещено выполнять действие.
  • Вход. Сохранение входа существующего клиента каким бы то ни было образом.
  • Выход. Окончание сеанса клиента.

Существует ряд распространенных методов выполнения проверки подлинности в веб-приложениях. Они называются схемами. Данная схема определяет действия для некоторых или всех указанных выше вариантов. Некоторые схемы поддерживают только определенное подмножество действий, а для выполнения неподдерживаемых действий может потребоваться отдельная схема. Например, схема OpenID Connect-Connect (OIDC) не поддерживает вход или выход, но обычно она настроена на использование проверки подлинности файлов cookie для сохранения сведений.

В приложении ASP.NET Core можно настроить DefaultAuthenticateScheme, а также дополнительные специальные схемы для каждого из описанных выше действий. Например, DefaultChallengeScheme и DefaultForbidScheme. Вызов AddIdentity настраивает ряд аспектов приложения и добавляет множество необходимых служб. Он также включает этот вызов для настройки схемы проверки подлинности:

builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
    options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
    options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
});

Эти схемы используют файлы cookie для сохранения и перенаправления на страницы входа для проверки подлинности по умолчанию. Эти схемы подходят для веб-приложений, которые взаимодействуют с пользователями через веб-браузеры, но не рекомендуются для интерфейсов API. Вместо этого интерфейсы API обычно используют другую форму проверки подлинности, например токены носителя JWT.

Веб-API используются кодом, например HttpClient в приложениях .NET и эквивалентных типов в других платформах. Эти клиенты предполагают использование ответа от вызова API или кода состояния, указывающего на то, что возникла проблема. Эти клиенты не взаимодействуют через браузер и не отображают или не взаимодействуют с HTML-кодом, который может быть возвращен API. Таким образом, конечные точки API не подходят для перенаправления клиентов на страницы входа в систему, если они не прошли проверку подлинности. Следует использовать другую схему.

Чтобы настроить проверку подлинности для API, можно настроить проверку подлинности следующим образом, который используется проектом PublicApi в эталонном приложении eShopOnWeb:

builder.Services
    .AddAuthentication(config =>
    {
      config.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(config =>
    {
        config.RequireHttpsMetadata = false;
        config.SaveToken = true;
        config.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(key),
            ValidateIssuer = false,
            ValidateAudience = false
        };
    });

Хотя можно настроить несколько различных схем проверки подлинности в рамках одного проекта, гораздо проще настроить одну схему по умолчанию. По этой причине в эталонном приложении eShopOnWeb интерфейсы API разделены по собственным проектам, PublicApiотдельным от главного проекта Web, который включает представления и приложения Razor Pages.

Проверка подлинности в приложениях Blazor

Приложения Blazor Server могут использовать те же функции проверки подлинности, что и любое другое приложение ASP.NET Core. BlazorWebAssembly Приложения не могут использовать встроенные поставщики удостоверений и проверки подлинности, так как они выполняются в браузере. BlazorWebAssembly приложения могут хранить состояние проверки подлинности пользователей локально и получать доступ к утверждениям, чтобы определить, какие действия должны выполнять пользователи. Однако все проверка проверки подлинности и авторизации должны выполняться на сервере независимо от любой логики, реализованной внутри BlazorWebAssembly приложения, так как пользователи могут легко обойти приложение и взаимодействовать с API напрямую.

Ссылки — проверка подлинности

Авторизация

Простейшая форма авторизации предусматривает запрет на доступ анонимных пользователей. Эту функцию можно достичь, применяя [Authorize] атрибут к определенным контроллерам или действиям. При использовании ролей этот атрибут можно расширить, ограничив доступ для пользователей с определенными ролями, как показано ниже:

[Authorize(Roles = "HRManager,Finance")]
public class SalaryController : Controller
{

}

В этом случае доступ к контроллеру SalaryController будут иметь пользователи, которым назначена хотя бы одна роль HRManager или Finance. Если пользователь должен иметь одновременно несколько ролей, этот атрибут можно применить несколько раз, в каждом случае указывая нужную роль.

Если наборы ролей указываются в виде строк в различных контроллерах и действиях, это может привести к нежелательному дублированию. Как минимум, определите константы для этих строковых литералов и используйте константы в любом месте, где нужно указать строку. Вы также можете настроить политики авторизации, которые инкапсулируют правила авторизации, а затем указать политику вместо отдельных ролей при применении атрибута [Authorize] :

[Authorize(Policy = "CanViewPrivateReport")]
public IActionResult ExecutiveSalaryReport()
{
    return View();
}

Такой подход к применению политик позволяет разделить виды действий, доступ к которым ограничивается на основе ролей или применяемых правил. Позже при создании новой роли, необходимой для доступа к определенным ресурсам, можно просто обновить политику, а не обновлять каждый список ролей для каждого [Authorize] атрибута.

Претензии

Утверждения — это пары имен и значений, которые представляют свойства прошедшего проверку подлинности пользователя. Например, в виде утверждения могут храниться номера сотрудников. Утверждения могут использоваться в рамках политик авторизации. Можно создать политику с именем EmployeeOnly, требующую существования вызываемого "EmployeeNumber"утверждения, как показано в следующем примере:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    services.AddAuthorization(options =>
    {
        options.AddPolicy("EmployeeOnly", policy => policy.RequireClaim("EmployeeNumber"));
    });
}

Затем эту политику можно использовать с атрибутом для защиты любого контроллера [Authorize] и /или действия, как описано выше.

Защита веб-API

В большинстве веб-API должна применяться система проверки подлинности на основе маркеров. Проверка подлинности на основе маркеров реализуется без сохранения сведений о состоянии и предусматривает возможность масштабирования. В системе на основе маркеров клиент сначала должен пройти проверку подлинности с использованием соответствующего поставщика. В случае успешного прохождения проверки клиент получает маркер, который представляет собой зашифрованную значащую строку символов. Наиболее распространенным форматом токенов является JSON Web Token или JWT. Впоследствии при отправке запроса к API клиент добавляет этот маркер в заголовок запроса. Перед выполнением запроса сервер проверяет маркер, указанный в его заголовке. Этот процесс показан на рис. 7-4.

Проверка подлинности на основе маркеров

Рис. 7-4. Проверка подлинности на основе маркеров для веб-API.

Вы можете создать собственную службу проверки подлинности, интегрировать ее с Azure AD и OAuth или реализовать службу с помощью средства с открытым исходным кодом, например IdentityServer.

Токены JWT могут внедрять утверждения о пользователе, которые могут быть прочитаны на клиенте или сервере. Для просмотра содержимого маркера JWT можно использовать такие средства, как jwt.io. Не храните конфиденциальные данные, такие как пароли или ключи в маркерах JTW, так как их содержимое легко читается.

При использовании маркеров JWT с SPA или BlazorWebAssembly приложениями необходимо хранить маркер где-то на клиенте, а затем добавить его в каждый вызов API. Обычно это делается в виде заголовка, как показано в следующем коде:

// AuthService.cs in BlazorAdmin project of eShopOnWeb
private async Task SetAuthorizationHeader()
{
      var token = await GetToken();
      _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
}

После вызова метода выше запросы, выполненные с _httpClient, будут содержать маркер, внедренный в заголовки запроса, что позволяет API на стороне сервера выполнять проверку подлинности и авторизацию запроса.

Настраиваемая безопасность

Внимание

Как правило, не следует реализовывать собственные пользовательские реализации безопасности.

Будьте особенно осторожны при самостоятельной реализации криптографии, членства пользователей или системы создания токенов. Есть много платных и бесплатных инструментов, которые почти наверняка гарантируют более высокий уровень безопасности, чем пользовательские реализации.

Ссылки — безопасность

Взаимодействие с клиентом

Помимо обслуживания страниц и ответа на запросы данных через веб-API, приложения ASP.NET Core могут напрямую взаимодействовать с подключенными клиентами. Для исходящего обмена данными в этом случае могут использоваться самые разные транспортные технологии, однако самое широкое распространение получила технология WebSockets. Библиотека ASP.NET Core SignalR упрощает реализацию функций взаимодействия между клиентом и сервером в реальном времени в вашем приложении. SignalR поддерживает широкий спектр транспортных технологий, в том числе WebSockets, и абстрагирует многие детали реализации, освобождая от этого труда разработчика.

Взаимодействие с клиентом в реальном времени с использованием WebSockets или других технологий применяется в самых разных сценариях. Некоторыми примерами могут служить:

  • Приложения комнаты чата в реальном времени

  • Приложения мониторинга

  • Обновления хода выполнения заданий

  • Notifications

  • Приложения интерактивных форм

При реализации взаимодействия с клиентом в приложениях обычно используются два компонента:

  • Диспетчер подключений на стороне сервера (SignalR Hub, WebSocketManager WebSocketHandler)

  • Библиотека на стороне клиента

В качестве клиентов могут выступать не только браузеры, но также мобильные, консольные и другие встроенные приложения, которые поддерживают SignalR/WebSockets. Следующая простая программа, входящая в пример приложения WebSocketManager, выводит на консоль все содержимое, передаваемое в приложение чата:

public class Program
{
    private static Connection _connection;
    public static void Main(string[] args)
    {
        StartConnectionAsync();
        _connection.On("receiveMessage", (arguments) =>
        {
            Console.WriteLine($"{arguments[0]} said: {arguments[1]}");
        });
        Console.ReadLine();
        StopConnectionAsync();
    }

    public static async Task StartConnectionAsync()
    {
        _connection = new Connection();
        await _connection.StartConnectionAsync("ws://localhost:65110/chat");
    }

    public static async Task StopConnectionAsync()
    {
        await _connection.StopConnectionAsync();
    }
}

Рассмотрите способы обеспечения взаимодействия с клиентом напрямую из приложений и оцените, насколько возможность взаимодействия в реальном времени позволит улучшить ваше приложение.

Ссылки — взаимодействие с клиентом

Следует ли применять проблемно-ориентированное проектирование?

Проблемно-ориентированное проектирование предусматривает гибкий подход к разработке программного обеспечения с учетом особенностей области бизнеса. При таком подходе особое внимание уделяется взаимодействию с экспертами в конкретных областях бизнеса, которые предоставляют разработчикам информацию о работе систем в реальном мире. Например, при разработке системы биржевых торгов вам может потребоваться консультация со стороны эксперта в этой области. Проблемно-ориентированное проектирование рассчитано на решение сложных бизнес-проблем и зачастую не подходит для небольших простых приложений, поскольку затраты на понимание и моделирование предметной области попросту не могут окупиться.

При разработке программного обеспечения с соблюдением принципов проблемно-ориентированного проектирования ваша команда (включая нетехнических специалистов и участников) должна использовать единый язык для общего понимания предметной области. Это значит, что необходимо использовать единую терминологию для описания моделируемых концепций реального мира, их программных эквивалентов и любых структур, которые могут применяться для сохранения концепций (например, таблиц баз данных). Таким образом, описываемые на едином языке концепции должны стать основой для вашей модели предметной области.

Модель предметной области состоит из объектов, которые взаимодействуют друг с другом, моделируя поведение системы. Эти объекты можно разделить на следующие категории:

  • Сущности, которые представляют объекты с потоком удостоверений. Для сущностей обычно применяется сохраняемость по ключу, по которому они могут быть впоследствии извлечены.

  • Агрегаты, представляющие группы объектов, которые должны сохраняться как единое целое.

  • Объекты значений, представляющие концепции, которые могут сравниваться на основании суммы значений их свойств. Например, диапазон дат определяется датами начала и окончания.

  • События предметной области, которые представляют происходящие в системе события, интересующие другие части системы.

Модель предметной области должна инкапсулировать комплексное поведение в рамках модели. В частности, сущности должны представлять собой не просто коллекции свойств. Модель предметной области, которая не определяет поведение и представляет лишь состояние системы, будет слабой, что при проблемно-ориентированном проектировании крайне нежелательно.

Помимо этих типов моделей, при проблемно-ориентированном проектировании применяются другие шаблоны:

  • Репозиторий обеспечивает абстрагирование сведений о сохраняемости.

  • Фабрика служит для инкапсуляции создания сложных объектов.

  • Службы инкапсулируют сложное поведение и (или) детали реализации инфраструктуры.

  • Команда обеспечивает разделение выдачи и выполнения команд.

  • Спецификация инкапсулирует детали запроса.

При проблемно-ориентированном проектировании также рекомендуется применять ранее описываемую чистую архитектуру, благодаря которой обеспечиваются слабая связанность, инкапсуляция и простота проверки кода с использованием модульных тестов.

В каких случаях следует применять проблемно-ориентированное проектирование

Проблемно-ориентированное проектирование оптимально подходит для крупных приложений, разработка которых связана с решением как серьезных технических трудностей, так и сложных бизнес-задач. В таких сценариях при разработке приложения требуются знания экспертов в предметной области. При этом требуется значительный объем работы над поведением в самой модели предметной области, что позволяет представить бизнес-правила и взаимодействия в дополнение к возможностям хранения и извлечения текущего состояния различных записей с применением хранилищ данных.

В каких случаях не следует применять проблемно-ориентированное проектирование

Проблемно-ориентированное проектирование предусматривает инвестиции в моделирование, разработку архитектуры и взаимодействия, которые могут быть необоснованно велики для небольших приложений или решений, предназначенных только для создания, чтения, обновления и удаления данных. Если вы выбрали этот вариант, но при этом обнаружили слабость модели предметной области (то есть отсутствие поведения), возможно, следует пересмотреть выбранный подход. Возможно, в таком случае для вашего приложения не требуется применять проблемно-ориентированное проектирование либо вам может потребоваться помощь в рефакторинге приложения с тем, чтобы инкапсулировать бизнес-логику в модели предметной области, а не в базе данных или пользовательском интерфейсе.

Возможен гибридный подход, при котором проблемно-ориентированное проектирование применяется только в отношении транзакционных или более сложных частей приложения, а не для реализации простых функций создания, чтения, обновления и удаления данных. Например, если вы запрашиваете данные для вывода отчета или визуализации данных в панели мониторинга, вам не нужны ограничения агрегата. В этом случае вполне подойдет использование более простой модели чтения.

Ссылки — проблемно-ориентированное проектирование

Развертывание

Независимо от того, где будет размещено ваше приложение ASP.NET Core, процесс его развертывания будет состоять из нескольких шагов. В первую очередь выполняется публикация приложения, например с помощью команды dotnet publish интерфейса командной строки. На этом шаге приложение компилируется, а все необходимые для его работы файлы помещаются в указанную папку. При развертывании из Visual Studio эти действия выполняются автоматически. В папке публикации содержатся EXE- и DLL-файлы приложения, а также его зависимости. Автономное приложение также будет включать версию среды выполнения .NET. Приложения ASP.NET Core также включают файлы конфигурации, статические ресурсы клиента и представления MVC.

Приложения ASP.NET Core являются консольными приложениями и должны запускаться при загрузке или перезагрузке сервера в случае сбоя приложения (или сервера). Для автоматизации этого процесса может использоваться диспетчер процессов. Чаще всего для ASP.NET Core используются такие диспетчеры запросов, как Nginx и Apache в Linux, а также IIS или Windows Service в Windows.

В дополнение к диспетчеру процессов ASP.NET Core приложения могут использовать обратный прокси-сервер. Обратный прокси-сервер получает HTTP-запросы из Интернета и пересылает их в Kestrel после определенной предварительной обработки. Обратные прокси-серверы обеспечивают слой безопасности для приложения. Кроме того, Kestrel не поддерживает размещение нескольких приложений на одном порту и по одному IP-адресу, поэтому такие сценарии нельзя использовать заголовки узла и другие подобные технологии.

Kestrel для Интернета

Рис. 7-5. ASP.NET, размещенный в Kestrel за обратным прокси-сервером

Кроме того, обратный прокси-сервер может применяться для защиты нескольких приложений с использованием протокола SSL/HTTPS. В этом случае настраивать SSL необходимо только на обратном прокси-сервере. Взаимодействие между обратным прокси-сервером и Kestrel может осуществляться по протоколу HTTP, как показано на рис. 7-6.

Приложение ASP.NET, размещенное позади обратного прокси-сервера, защищенного с помощью протокола HTTPS

Рис. 7-6. Приложение ASP.NET, размещенное позади обратного прокси-сервера, защищенного с помощью протокола HTTPS

Также набирает популярность подход с размещением приложений ASP.NET Core в контейнере Docker, который, в свою очередь, может размещаться локально или развертываться в облачной среде Azure. Контейнер Docker может содержать код приложения и выполняться на сервере Kestrel (в этом случае необходимо развертывание позади обратного прокси-сервера, как показано выше).

Если вы размещаете приложение в Azure, можно использовать шлюз приложений Microsoft Azure в качестве выделенного виртуального устройства для предоставления определенных служб. Шлюз приложений не только выступает в качестве обратного прокси-сервера для отдельных приложений, но и реализует следующие возможности:

  • балансировка нагрузки HTTP;

  • Разгрузка SSL (SSL только для Интернета)

  • Сквозное шифрование SSL

  • Распределенная маршрутизация (консолидация до 20 сайтов на одном шлюзе приложений)

  • Брандмауэр веб-приложения

  • Поддержка WebSocket

  • Расширенная диагностика

Дополнительные сведения о вариантах развертывания Azure см. в главе 10.

Ссылки — развертывание