Инъекция зависимостей в классах моделей (объектов)

Я строю приложение ASP.NET Core MVC с помощью Entity Framework Code-First. Я решил реализовать простой шаблон репозитория, обеспечивающий базовые операции CRUD для всех классов моделей, которые я создал. Я решил следовать всем рекомендациям, представленным на http://docs.asp.net, и DI - одна из них.

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

Мы просто внедряем их через конструктор и регистрируем сопоставления в классе Startup приложения:

// Some repository class
public class MyRepository : IMyRepository
{
    private readonly IMyDependency _myDependency;
    public MyRepository(IMyDependency myDependency)
    {
        _myDependency = myDependency;
    }
}

// In startup.cs :
services.AddScoped<IMyDependency, MyDependency>();
services.AddScoped<IMyRepository, MyRepository>();

У меня проблема в том, что в некоторых моих модельных классах я хотел бы добавить некоторые из объявленных мной зависимостей.

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

Итак, мой вопрос: есть ли другой способ, кроме инъекции конструктора, чтобы внедрить зависимости, и как? Например, я думал о шаблоне атрибута или о чем-то подобном.

Ответ 1

Как я уже объяснил в комментарии, при создании объекта с использованием new ничего из структуры внедрения зависимостей не участвует в процессе. Таким образом, структура DI не может волшебным образом вводить вещи в этот объект, она просто не знает об этом.

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

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

Конечно, в зависимости от количества зависимостей это может стать немного сложнее, так как вам нужно передать их все конструктору. Таким образом, одной из альтернатив будет иметь некоторую "фабрику моделей", которая заботится о создании объекта модели. Другой альтернативой также может быть использование шаблона локатора службы, передавая IServiceCollection модели, которая затем может запрашивать любые необходимые ей зависимости. Обратите внимание, что, как правило, это плохая практика, а не инверсия контроля.

У обеих этих идей есть проблема в том, что они изменяют способ создания объекта. А некоторые модели, особенно те, которые обрабатываются Entity Framework, нуждаются в пустом конструкторе, чтобы EF мог создавать объект. Таким образом, в этот момент вы, вероятно, столкнетесь с некоторыми случаями, когда зависимости вашей модели не разрешаются (и у вас нет простого способа сказать).

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

Другим решением было бы убрать логику из модели. Например, модели ASP.NET Identity действительно глупы. Они ничего не делают. Вся логика выполняется в UserStore, который является сервисом и поэтому может иметь сервисные зависимости.

Ответ 2

Шаблон, часто используемый в проектировании, управляемом доменом (конкретная конкретная модель домена), заключается в передаче необходимых услуг в метод, который вы вызываете.

Например, если вы хотите вычислить vat, вы передадите службу vat в метод CalculateVat.

В вашей модели

    public void CalculateVat(IVatCalculator vatCalc) 
    {
        if(vatCalc == null)
            throw new ArgumentNullException(nameof(vatCalc));

        decimal vatAmount = vatcalc.Calculate(this.TotalNetPrice, this.Country);
        this.VatAmount = new Currency(vatAmount, this.CurrencySymbol);
    }

Ваш класс обслуживания

    // where vatCalculator is an implementation IVatCalculator 
    order.CalculateVat(vatCalculator);

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

public class VatCalculator : IVatCalculator
{
    private readonly IVatRepository vatRepository;

    public VatCalculator(IVatRepository vatRepository)
    {
        if(vatRepository == null)
            throw new ArgumentNullException(nameof(vatRepository));

        this.vatRepository = vatRepository;
    }

    public decimal Calculate(decimal value, Country country) 
    {
        decimal vatRate = vatRepository.GetVatRateForCountry(country);

        return vatAmount = value * vatRate;
    }
}

Ответ 3

Есть ли другой способ, чем инъекция конструктора для инъекций зависимостей и как?

Ответ "нет", это невозможно сделать с "инъекцией зависимостей". Но, "да", вы можете использовать "шаблон локатора службы" для достижения своей конечной цели.

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

public class MyRepository : IMyRepository
{
    public IMyDependency { get; } =
        CallContextServiceLocator.Locator
                                 .ServiceProvider
                                 .GetRequiredService<IMyDependency>();
}

CallContextServiceLocator.Locator.ServiceProvider - глобальный поставщик услуг, в котором все живет. На самом деле это не рекомендуется. Но если у вас нет другого выбора, вы можете. Рекомендуется вместо этого использовать DI и никогда не создавать экземпляр объекта вручную, т.е. избегайте new.

Ответ 4

Встроенные встраивающие устройства жалуются, что не могут найти значение по умолчанию ctor. Поэтому вам нужен пользовательский.

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

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

    public class DiModelBinder : ComplexTypeModelBinder
    {
        public DiModelBinder(IDictionary<ModelMetadata, IModelBinder> propertyBinders) : base(propertyBinders)
        {
        }

        /// <summary>
        /// Creates the model with one (or more) injected service(s).
        /// </summary>
        /// <param name="bindingContext"></param>
        /// <returns></returns>
        protected override object CreateModel(ModelBindingContext bindingContext)
        {
            var services = bindingContext.HttpContext.RequestServices;
            var modelType = bindingContext.ModelType;
            var ctors = modelType.GetConstructors();
            foreach (var ctor in ctors)
            {
                var paramTypes = ctor.GetParameters().Select(p => p.ParameterType).ToList();
                var parameters = paramTypes.Select(p => services.GetService(p)).ToArray();
                if (parameters.All(p => p != null))
                {
                    var model = ctor.Invoke(parameters);
                    return model;
                }
            }

            return null;
        }
    }

Это связующее будет предоставлено:

public class DiModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context == null) { throw new ArgumentNullException(nameof(context)); }

        if (context.Metadata.IsComplexType && !context.Metadata.IsCollectionType)
        {
            var propertyBinders = context.Metadata.Properties.ToDictionary(property => property, context.CreateBinder);
            return new DiModelBinder(propertyBinders);
        }

        return null;
    }
}

Здесь, как будет зарегистрировано связующее:

services.AddMvc().AddMvcOptions(options =>
{
    // replace ComplexTypeModelBinderProvider with its descendent - IoCModelBinderProvider
    var provider = options.ModelBinderProviders.FirstOrDefault(x => x.GetType() == typeof(ComplexTypeModelBinderProvider));
    var binderIndex = options.ModelBinderProviders.IndexOf(provider);
    options.ModelBinderProviders.Remove(provider);
    options.ModelBinderProviders.Insert(binderIndex, new DiModelBinderProvider());
});

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

И, в конце, вот как вы можете его использовать:

public class MyModel 
{
    private readonly IMyRepository repo;

    public MyModel(IMyRepository repo) 
    {
        this.repo = repo;
    }

    ... do whatever you want with your repo

    public string AProperty { get; set; }

    ... other properties here
}

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

НТН

Ответ 5

Вы можете сделать это, проверить [InjectionMethod] и container.BuildUp(экземпляр);

Пример:

Типичный конструктор DI (НЕ НЕОБХОДИМО ЕСЛИ ВЫ ИСПОЛЬЗУЕТЕ InjectionMethod) public ClassConstructor (DeviceHead pDeviceHead) {     this.DeviceHead = pDeviceHead; }

Этот атрибут вызывает этот метод для вызова DI. [InjectionMethod] public void Инициализировать (DeviceHead pDeviceHead) {     this.DeviceHead = pDeviceHead; }

Ответ 6

Я знаю, что мой ответ опоздал и, возможно, не совсем то, что вы просите, но я хотел поделиться тем, как я это делаю.

Прежде всего: это Antipattern, чтобы использовать ServiceLocator, поэтому старайтесь не использовать его, как вы можете. В моем случае мне нужно было вызвать MediatR внутри моей DomainModel для реализации логики DomainEvents. В любом случае мне нужно было найти способ вызвать статический класс в моей DomainModel, чтобы получить экземпляр некоторой зарегистрированной службы от DI.

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

Давай сделаем это:

1- Я создал интерфейс для упаковки IServiceProvider

public interface IServiceProviderProxy
{
    T GetService<T>();
    IEnumerable<T> GetServices<T>();
    object GetService(Type type);
    IEnumerable<object> GetServices(Type type);
}

2- Затем я создал статический класс для моей точки доступа ServiceLocator.

public static class ServiceLocator
{
    private static IServiceProviderProxy diProxy;

    public static IServiceProviderProxy ServiceProvider => diProxy ?? throw new Exception("You should Initialize the ServiceProvider before using it.");

    public static void Initialize(IServiceProviderProxy proxy)
    {
        diProxy = proxy;
    }
}

3- Я создал реализацию для IServiceProviderProxy которая внутренне использует IHttpContextAccessor

public class HttpContextServiceProviderProxy : IServiceProviderProxy
{
    private readonly IHttpContextAccessor contextAccessor;

    public HttpContextServiceProviderProxy(IHttpContextAccessor contextAccessor)
    {
        this.contextAccessor = contextAccessor;
    }

    public T GetService<T>()
    {
        return contextAccessor.HttpContext.RequestServices.GetService<T>();
    }

    public IEnumerable<T> GetServices<T>()
    {
        return contextAccessor.HttpContext.RequestServices.GetServices<T>();
    }

    public object GetService(Type type)
    {
        return contextAccessor.HttpContext.RequestServices.GetService(type);
    }

    public IEnumerable<object> GetServices(Type type)
    {
        return contextAccessor.HttpContext.RequestServices.GetServices(type);
    }
}

4- Я должен зарегистрировать IServiceProviderProxy в DI, как это

public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpContextAccessor();
    services.AddSingleton<IServiceProviderProxy, HttpContextServiceProviderProxy>();
    .......
}

5- Последний шаг - инициализация ServiceLocator с экземпляром IServiceProviderProxy при запуске приложения.

public void Configure(IApplicationBuilder app, IHostingEnvironment env,IServiceProvider sp)
{
    ServiceLocator.Initialize(sp.GetService<IServiceProviderProxy>());
}

В результате теперь вы можете вызывать ServiceLocator в ваших классах DomainModel "Или и нужное место" и разрешать зависимости, которые вам нужны.

public class FakeModel
{
    public FakeModel(Guid id, string value)
    {
        Id = id;
        Value = value;
    }

    public Guid Id { get; }
    public string Value { get; private set; }

    public async Task UpdateAsync(string value)
    {
        Value = value;
        var mediator = ServiceLocator.ServiceProvider.GetService<IMediator>();
        await mediator.Send(new FakeModelUpdated(this));
    }
}