Этот пост является второй частью серии из трех частей. Возможно, вы захотите сначала прочитать часть 1.

Из предыдущего поста этой серии мы знаем, что для того, чтобы сделать зависимость доступной для внедрения в другой объект кода, зависимость должна:

  • Иметь абстракцию (чаще всего интерфейс)
  • Быть вставленной в .Net контейнер

Мы знаем, как сделать первую часть; нам нужно создать абстракцию (чаще всего интерфейс) для любого класса, который мы хотим внедрить как зависимость. В этой серии мы используем класс MovieRepository и интерфейс IMovieRepository со следующими методами:

public interface IMovieRepository
{
    List<Movie> GetAll();
    Movie GetByID(int id);
}

public class MovieRepository : IMovieRepository
{
    public List<Movie> GetAll()
    {
        //Implementation
    }

    public Movie GetByID(int id)
    {
        //Implementation
    }
}

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

Анатомия файла Program.cs

Регистрация зависимостей в контейнере происходит в файле Program.cs в приложении .NET 6. Вот файл Program.cs по умолчанию, созданный Visual Studio 2022, когда я создал приложение Net .NET 6 Razor Pages:

var builder = WebApplication.CreateBuilder(args);

// Добавляет сервис в контейнер.
builder.Services.AddRazorPages();

var app = builder.Build();

// Настраивает конвейер Http-запросов.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

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

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

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

Добавление зависимостей к контейнеру

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

Объект также определяет большой набор методов, которые добавляют в контейнер общие объекты .NET, например:

builder.Services.AddMvc(); //Добавляет базовый функционал MVC 
builder.Services.AddControllers(); //Добавляет поддержку для  MVC контроллеров (представления и маршрутизация должны быть добавлены отдельно)
builder.Services.AddLogging(); //Добавлояет поддержку логирования
builder.Services.AddSignalR(); //Добавляет поддержку дял SignalR

Эти методы часто принимают классы «параметров», которые определяют, как будут вести себя эти части приложения. Например, предположим, что мы хотим добавить поддержку защиты от подделки в наше приложение, чтобы предотвратить атаки межсайтового скриптинга (XSRF). Мы можем сделать это следующим образом, используя шаблон Options для указания имени поля формы и имени заголовка HTTP.

builder.Services.AddAntiforgery(options =>
{
    options.FormFieldName = "AntiforgeryFieldname";
    options.HeaderName = "X-CSRF-TOKEN-HEADERNAME";
});

В нашем примере приложения мы используем шаблон параметров аналогичным способом, чтобы изменить страницу Razor по умолчанию:

builder.Services.AddRazorPages().AddRazorPagesOptions(options =>
{
    options.Conventions.AddPageRoute("/Movies", "");
});

Но как насчет нашего пользовательского класса MovieRepository? Чтобы добавить это в контейнер, нам нужно выбрать для него время жизни службы. В следующем посте более подробно будет обсуждаться время жизни службы; пока все, что вам нужно знать, это то, что следующий код добавит экземпляр MovieRepository в контейнер.

builder.Services.AddTransient<IMovieRepository, MovieRepository>();

Используем методы расширения для регистрации зависимостей

В реальных приложениях чаще всего у нас гораздо больше зависимостей, чем один репозиторий. Чтобы наш файл Program.cs оставался чистым и удобочитаемым, мы часто создаем методы расширения, которые регистрируют группы связанных зависимостей с контейнером .NET.

using DependencyInjectionNET6Demo.Repositories;
using DependencyInjectionNET6Demo.Repositories.Interfaces;

namespace DependencyInjectionNET6Demo.Extensions;

public static class ServiceExtensions
{
    public static void RegisterRepos(this IServiceCollection collection)
    {
        collection.AddTransient<IMovieRepository, MovieRepository>();
        //Добавляем другие репозитарии
    }

    public static void RegisterLogging(this IServiceCollection collection)
    {
        //Регистрируем логирование
    }

    public static void RegisterAuth(this IServiceCollection collection)
    {
        //Регистрируем сервис аутентификации.
    }
}

Затем в файле Program.cs мы можем вызвать их так:

var builder = WebApplication.CreateBuilder(args);

// Добавляем сервисы в контейнер.
builder.Services.RegisterAuth();
builder.Services.RegisterRepos();
builder.Services.RegisterLogging();

Таким образом, наш файл Program.cs остается чистым и легко модифицируемым.

Независимо от того, используем ли мы методы расширения или только базовые методы в Program.cs, мы достигнем точки, в которой все наши службы теперь находятся в нашем контейнере. Но как нам внедрить их в зависимые классы?

Внедрение зависимостей (Injecting Dependencies)

В .NET 6 зависимости должны вводиться через конструктор. Допустим, у нас есть Razor Page с именем Movies.cshtml, который использует класс MoviesPageModel в качестве файла кода программной части:

@page "/Movies"
@model MoviesPageModel
@{
    ViewData["Title"] = "Movies";
}

<div class="text-center">
    <h1 class="display-4">Movies</h1>
</div>

<table class="table">
    <thead>
        <tr>
            <th>ID</th>
            <th>Title</th>
            <th>Release Date</th>
            <th>Runtime</th>
            <th></th>
        </tr>
    </thead>

    @foreach(var movie in Model.Movies)
    {
        <tr>
            <td>@movie.ID</td>
            <td>@movie.Title</td>
            <td>@movie.ReleaseDate</td>
            <td>@movie.RuntimeMinutes min</td>
            <td><a asp-page="/MovieDetails" asp-route-id="@movie.ID">Details</a></td>
        </tr>
    }
</table>
public class MoviesPageModel : PageModel
{
    public List<Movie> Movies { get; set; } = new List<Movie>();

    public MoviesPageModel() { }

    public void OnGet()
    {
        //Имплементация
    }
}

Основываясь на разметке страницы Movies.cshtml, мы можем с уверенностью сказать, что эта страница должна отображать коллекцию фильмов. MovieRepository может предоставить эту коллекцию на страницу.

Для этого нам нужно внедрить экземпляр абстракции MovieRepository (интерфейс IMovieRepository) в MoviePageModel, вызвать метод IMovieRepository.GetAll() для получения фильмов и поместить этот результат в свойство MoviePageModel.Movies во время метода OnGet(). . Все это можно сделать так:

public class MoviesPageModel : PageModel
{
    private readonly IMovieRepository _movieRepo;
    public List<Movie> Movies { get; set; } = new List<Movie>();

    public MoviesPageModel(IMovieRepository movieRepo)
    {
        _movieRepo = movieRepo;
    }

    public void OnGet()
    {
        Movies = _movieRepo.GetAll();
    }
}

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

Здорово! Теперь мы можем внедрить наш класс MovieRepository!

Внедрение в другие классы

Если вместо этого вы используете MVC, а не Razor Pages, вы можете внедрить в класс контроллера следующим образом:

public class MovieController : Controller
{
    private readonly IMovieRepository _movieRepo;
    
    public MovieController(IMovieRepository movieRepo)
    {
        _movieRepo = movieRepo;
    }
}

«Подожди ка!», скажете вы, «Это ничем не отличается от примера Razor Pages». И будете абсолютно правы. Любой внедряемый класс можно внедрить в любой другой класс.

Но это может привести нас к потенциальным проблемам.

Циклические зависимости

Представьте, что в дополнение к MovieRepository и IMovieRepository у нас есть класс ActorRepository и интерфейс IActorRepository, первому из которых требуется внедрить в него экземпляр IMovieRepository:

public interface IActorRepository { }

public class ActorRepository : IActorRepository
{
    private readonly IMovieRepository _movieRepo;

    public ActorRepository(IMovieRepository movieRepo)
    {
        _movieRepo = movieRepo;
    }
}

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

Но давайте также скажем, что мы хотим обратного: иногда нам нужен фильм с актерами, которые в нем играли. Для этого мы можем решить, что нам нужно внедрить IActorRepository в MovieRepository:

public class MovieRepository : IMovieRepository
{
    private readonly IActorRepository _actorRepo;

    public MovieRepository(IActorRepository actorRepo)
    {
        _actorRepo = actorRepo;
    }
    
    //...Остальная реализация
}

Добро пожаловать в «Очень плохую идею»! Мы создали циклическую зависимость: MovieRepository зависит от IActorRepository, а ActorRepository зависит от IMovieRepository. Следовательно, контейнер не может фактически создать ни одну из этих зависимостей, потому что они обе зависят друг от друга.

На самом деле, если вы запустите пример проекта с этой круговой зависимостью, вы получите следующее сообщение об ошибке:

«InvalidOperationException: обнаружена циклическая зависимость для службы типа DependencyInjectionNET6Demo.Repositories.Interfaces.IMovieRepository».

Решение простое: только одна из этих зависимостей может зависеть от другой. В этом конкретном случае мы удалим зависимость ActorRepository от MovieRepository.

public class ActorRepository : IActorRepository { }

Эту проблему легко увидеть, когда у вас есть только две зависимости, но в реальных проектах ее может быть гораздо сложнее обнаружить. У вас может быть первая зависимость, которая внедряет вторую, вторая внедряет третью, третья внедряет четвертую, но четвертая внедряет первую!

Это приводит к одному из руководящих правил проектирования программных приложений при использовании внедрения зависимостей: классы не должны зависеть от других классов на том же уровне архитектуры. Для нашего приложения классы уровня репозитория не должны внедрять другие репозитории. Однако классы более высокого уровня (например, модели Razor Pages или контроллеры MVC) могут внедрять классы более низкого уровня.

Зависимости без дополнительных зависимостей

Классная ситуация, с которой вы можете столкнуться, — это внедряемый класс, который сам по себе не имеет зависимостей. Типичным примером этого может быть класс ведения журнала; маловероятно, что класс ведения журнала будет иметь какие-либо другие зависимости.

public interface ILogger 
{
    //Реализация
}

public class MyLogger : ILogger
{
    //Реализация
}

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

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

Вам может быть интересно: если контейнер создает экземпляры зависимостей, как эти экземпляры удаляются или освобождаются? К счастью для нас, контейнер .NET 6 справляется с этим изначально! Экземпляры зависимостей автоматически удаляются или освобождаются либо в конце запроса, либо при завершении работы приложения, в зависимости от срока службы зависимости.

Резюме

Зависимости добавляются в контейнер .NET 6 в файле Program.cs с помощью таких методов, как AddTransient. .NET 6 включает набор функций быстрого доступа для добавления часто используемых реализаций, таких как AddMvc() или AddSignalR(). Мы можем использовать методы расширения для добавления групп связанных зависимостей в контейнер.

Зависимости внедряются в объекты через конструктор этого объекта. Конструктор принимает абстракцию зависимости в качестве параметра и присваивает это значение локальной частной переменной.

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