Проверка: как внедрить оболочку состояния модели с помощью Ninject?

Я просматривал этот учебник http://asp-umb.neudesic.com/mvc/tutorials/validating-with-a-service-layer--cs о том, как обернуть мои данные проверки вокруг оболочки.

Однако я хотел бы использовать инъекцию зависимостей. Я использую нинжект 2.0

namespace MvcApplication1.Models
{
    public interface IValidationDictionary
    {
        void AddError(string key, string errorMessage);
        bool IsValid { get; }
    }
}

// обертка

using System.Web.Mvc;

namespace MvcApplication1.Models
{
    public class ModelStateWrapper : IValidationDictionary
    {

        private ModelStateDictionary _modelState;

        public ModelStateWrapper(ModelStateDictionary modelState)
        {
            _modelState = modelState;
        }

        #region IValidationDictionary Members

        public void AddError(string key, string errorMessage)
        {
            _modelState.AddModelError(key, errorMessage);
        }

        public bool IsValid
        {
            get { return _modelState.IsValid; }
        }

        #endregion
    }
}

// контроллер

private IProductService _service;

public ProductController() 
{
    _service = new ProductService(new ModelStateWrapper(this.ModelState),
        new ProductRepository());
}

// сервисный слой

private IValidationDictionary _validatonDictionary;
private IProductRepository _repository;

public ProductService(IValidationDictionary validationDictionary,
    IProductRepository repository)
{
    _validatonDictionary = validationDictionary;
    _repository = repository;
}

public ProductController(IProductService service)
{
    _service = service;
}

person chobo2    schedule 23.01.2011    source источник
comment
Любой, кто хочет сделать это с помощью Autofac (в основном для ядра asp.net), см. stackoverflow.com/questions/45172019/   -  person Martin Dawson    schedule 19.07.2017


Ответы (2)


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

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

public class ProductController : Controller
{
    private readonly IProductService service;

    public ProductController(IProductService service) => this.service = service;

    public ActionResult Create(
        [Bind(Exclude = "Id")] Product productToCreate)
    {
        try
        {
            this.service.CreateProduct(productToCreate);
        }
        catch (ValidationException ex)
        {
            this.ModelState.AddModelErrors(ex);
            return View();
        }

        return RedirectToAction("Index");
    }
}

public static class MvcValidationExtension
{
    public static void AddModelErrors(
        this ModelStateDictionary state, ValidationException exception)
    {
        foreach (var error in exception.Errors)
        {
            state.AddModelError(error.Key, error.Message);
        }
    }
}

Класс ProductService не должен сам иметь никакой проверки, но должен делегировать это классу, специализирующемуся на проверке, т.е. IValidationProvider:

public interface IValidationProvider
{
    void Validate(object entity);
    void ValidateAll(IEnumerable entities);
}

public class ProductService : IProductService
{
    private readonly IValidationProvider validationProvider;
    private readonly IProductRespository repository;

    public ProductService(
        IProductRespository repository,
        IValidationProvider validationProvider)
    {
        this.repository = repository;
        this.validationProvider = validationProvider;
    }

    // Does not return an error code anymore. Just throws an exception
    public void CreateProduct(Product productToCreate)
    {
        // Do validation here or perhaps even in the repository...
        this.validationProvider.Validate(productToCreate);

        // This call should also throw on failure.
        this.repository.CreateProduct(productToCreate);
    }
}

Этот IValidationProvider, однако, не должен проверять сам себя, а скорее должен делегировать проверку классам проверки, которые специализируются на проверке одного конкретного типа. Когда объект (или набор объектов) недействителен, поставщик проверки должен выдать ValidationException, который может быть обнаружен выше в стеке вызовов. Реализация провайдера может выглядеть так:

sealed class ValidationProvider : IValidationProvider
{
    private readonly Func<Type, IValidator> validatorFactory;

    public ValidationProvider(Func<Type, IValidator> validatorFactory)
    {
        this.validatorFactory = validatorFactory;
    }

    public void Validate(object entity)
    {
        IValidator validator = this.validatorFactory(entity.GetType());
        var results = validator.Validate(entity).ToArray();        

        if (results.Length > 0)
            throw new ValidationException(results);
    }

    public void ValidateAll(IEnumerable entities)
    {
        var results = (
            from entity in entities.Cast<object>()
            let validator = this.validatorFactory(entity.GetType())
            from result in validator.Validate(entity)
            select result)
            .ToArray();

        if (results.Length > 0)
            throw new ValidationException(results);
    }
}

ValidationProvider зависит от IValidator экземпляров, которые выполняют фактическую проверку. Сам провайдер не знает, как создавать эти экземпляры, но использует для этого внедренный делегат Func<Type, IValidator>. Этот метод будет иметь специфичный для контейнера код, например, для Ninject:

var provider = new ValidationProvider(type =>
{
    var valType = typeof(Validator<>).MakeGenericType(type);
    return (IValidator)kernel.Get(valType);
});

Этот фрагмент показывает класс Validator<T> — я покажу этот класс через секунду. Во-первых, ValidationProvider зависит от следующих классов:

public interface IValidator
{
    IEnumerable<ValidationResult> Validate(object entity);
}

public class ValidationResult
{
    public ValidationResult(string key, string message)
    {
        this.Key = key;
        this.Message = message; 
    }
    public string Key { get; }
    public string Message { get; }
}

public class ValidationException : Exception
{
    public ValidationException(ValidationResult[] r) : base(r[0].Message)
    {
        this.Errors = new ReadOnlyCollection<ValidationResult>(r);
    }

    public ReadOnlyCollection<ValidationResult> Errors { get; }            
}    

Весь вышеприведенный код — это сантехника, необходимая для проведения проверки. Теперь вы можете определить класс проверки для каждой сущности, которую вы хотите проверить. Однако, чтобы немного помочь вашему DI-контейнеру, вы должны определить общий базовый класс для валидаторов. Это позволит вам зарегистрировать типы проверки:

public abstract class Validator<T> : IValidator
{
    IEnumerable<ValidationResult> IValidator.Validate(object entity)
    {
        if (entity == null) throw new ArgumentNullException("entity");

        return this.Validate((T)entity);
    }

    protected abstract IEnumerable<ValidationResult> Validate(T entity);
}

Как видите, этот абстрактный класс наследуется от IValidator. Теперь вы можете определить класс ProductValidator, производный от Validator<Product>:

public sealed class ProductValidator : Validator<Product>
{
    protected override IEnumerable<ValidationResult> Validate(
        Product entity)
    {
        if (entity.Name.Trim().Length == 0)
            yield return new ValidationResult(
                nameof(Product.Name), "Name is required.");

        if (entity.Description.Trim().Length == 0)
            yield return new ValidationResult(
                nameof(Product.Description), "Description is required.");

        if (entity.UnitsInStock < 0)
            yield return new ValidationResult(
                nameof(Product.UnitsInStock), 
                "Units in stock cnnot be less than zero.");
    }
}

Как видите, класс ProductValidator использует оператор C# yield return, который делает возврат ошибок проверки более быстрым.

Последнее, что вам нужно сделать, чтобы все это заработало, — настроить конфигурацию Ninject:

kernel.Bind<IProductService>().To<ProductService>();
kernel.Bind<IProductRepository>().To<L2SProductRepository>();

Func<Type, IValidator> validatorFactory = type =>
{
    var valType = typeof(Validator<>).MakeGenericType(type);
    return (IValidator)kernel.Get(valType);
};

kernel.Bind<IValidationProvider>()
    .ToConstant(new ValidationProvider(validatorFactory));

kernel.Bind<Validator<Product>>().To<ProductValidator>();

Мы действительно закончили? По-разному. Недостатком приведенной выше конфигурации является то, что для каждого объекта в нашем домене вам потребуется реализация Validator<T>. Даже если, возможно, большинство реализаций будут пустыми.

Вы можете решить эту проблему, выполнив две вещи:

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

Такая реализация по умолчанию может выглядеть так:

sealed class NullValidator<T> : Validator<T>
{
    protected override IEnumerable<ValidationResult> Validate(T entity)
    {
        return Enumerable.Empty<ValidationResult>();
    }
}

Вы можете настроить этот NullValidator<T> следующим образом:

kernel.Bind(typeof(Validator<>)).To(typeof(NullValidator<>));

После этого Ninject будет возвращать NullValidator<Customer>, когда запрашивается Validator<Customer>, и для него не зарегистрирована конкретная реализация.

Последнее, чего сейчас не хватает, так это авторегистрации. Это избавит вас от необходимости добавлять регистрацию для каждой реализации Validator<T> и позволит Ninject динамически искать ваши сборки для вас. Я не смог найти никаких примеров этого, но я предполагаю, что Ninject может это сделать.

ОБНОВЛЕНИЕ: см. ответ Кайесса, чтобы узнать, как автоматически регистрировать эти типы.

И последнее замечание: чтобы это сделать, вам нужно довольно много сантехники, поэтому, если ваш проект (и остается) довольно маленьким, этот подход может дать вам слишком много накладных расходов. Однако когда ваш проект разрастется, вы будете очень рады такому гибкому дизайну. Подумайте, что вам нужно сделать, если вы хотите изменить валидацию (скажем, блок приложения валидации или аннотации данных). Единственное, что вам нужно сделать, это написать реализацию для NullValidator<T> (в этом случае я бы переименовал ее в DefaultValidator<T>. Кроме того, все еще возможно иметь свои собственные классы проверки для дополнительных проверок, которые трудно реализовать с другими проверками). технологии.

Обратите внимание, что использование абстракций, таких как IProductService и ICustomerService, нарушает принципы SOLID, и вам может быть полезно перейти от этого шаблона к шаблон, который абстрагирует варианты использования.

Обновление: также взгляните на этот q/a; в нем обсуждается дополнительный вопрос по той же статье.

person Steven    schedule 31.01.2011
comment
@Steven - в итоге я сделал что-то похожее на то, что вы описали, но не такое безумное. Я предполагаю, что суть вопросов заключается в том, что вы делаете, когда вам нужно что-то связать и передать. Например, скажем, мне нужно передать адрес электронной почты на мой сервисный уровень, поскольку почти каждый метод использует его. Однако я получаю электронное письмо от файла cookie (Request.User.UserName), как это будет работать с ninject? - person chobo2; 03.02.2011
comment
@chobo2: Вставка самого адреса электронной почты часто не очень хорошая вещь, особенно когда она нужна на многих занятиях. Если вы считаете, что вам нужно использовать его во многих классах, хорошо подумайте о своем дизайне. Соблюдаете ли вы принцип единой ответственности? Мне нужно больше контекста, чтобы сказать об этом что-то полезное. Пожалуйста, разместите новый вопрос с подробной информацией о вашей проблеме здесь, на SO, и оставьте ссылку на него в поле для комментариев ниже. Это позволяет не только мне, но и другим помогать. Ваше здоровье - person Steven; 04.02.2011
comment
@chobo2: но не настолько сумасшедший. хехехе... это немного абстрактно, не так ли? :-) - person Steven; 04.02.2011
comment
@Steven: Спасибо за ссылку на эту тему, теперь вы заставили меня переосмыслить весь мой подход к проверке. Слишком много сантехники, но мне нравится тот факт, что это чрезвычайно масштабируемо! Однако у меня есть вопрос: если класс обслуживания (скажем, ProductService) не отвечает за проверку, то зачем мне вообще нужен этот класс? Если внутри него нет никакой логики проверки, то это не что иное, как обращения к репозиторию, вам так не кажется? - person Kassem; 01.03.2011
comment
@Kassem, если ваше приложение выполняет только операции CRUD, класс обслуживания действительно может быть излишним. Однако когда вы делаете больше, чем вставляете или обновляете одну запись (например, вставляете и обновляете несколько записей нескольких объектов, отправляете почту, взаимодействуете с другой службой, как вы это называете), уровень службы становится чрезвычайно полезным. - person Steven; 01.03.2011
comment
@Стивен: понятно. В настоящее время я работаю над приложением для онлайн-аукциона. У меня возникли проблемы с принятием решения о том, где должна быть какая-то логика и какие услуги мне нужны. Я думаю, мне лучше опубликовать вопрос об этом здесь, в StackOverflow, и дать ссылку на него. - person Kassem; 01.03.2011
comment
@Steven: У меня есть несколько вопросов, пожалуйста... Не могли бы вы объяснить, что именно делает блок кода Ninject? Кроме того, если у вас есть некоторый опыт работы со StructureMap, не могли бы вы преобразовать этот код в синтаксис StructureMap, пожалуйста? Кроме того, мне все еще нужно предоставить сопоставление каждого IValidator‹Type›, или фабрика все просчитывает? - person Kassem; 19.04.2011
comment
@Kassem: Вы видите строку kernel.Bind<Validator<Product>>().To<ProductValidator>(); где-то в моем ответе? Вам нужно сделать это для каждого созданного вами валидатора или выполнить пакетную регистрацию, но опять же; Я не знаю, как это сделать в Ninject. На этот и другие вопросы, которые у вас есть: попробуйте спросить здесь, в SO, потому что у меня нет всех ответов. Удачи. - person Steven; 19.04.2011
comment
Просто самое безумное дерьмо, которое я видел за весь день. Я еще не готов к этому типу IoC. - person Denis Valeev; 21.06.2011
comment
@Steven: Должны ли быть какие-либо реализации валидатора, которые полагаются на другие службы / репозитории, также внедренные с этими зависимостями? Как бы вы рекомендовали передавать ошибки уровня доступа к базе данных обратно в контроллер? Спасибо за потрясающий пост! - person LeffeBrune; 25.07.2011
comment
@see: я не уверен, что полностью понимаю ваш вопрос, но позвольте мне все же попробовать: если в базе данных возникла ошибка (например, ограничение FK), приложение должно просто быстро выйти из строя. Либо есть ошибка (и вам нужно ее исправить), либо ваш пользователь столкнулся с редким состоянием гонки. В любом случае не переводите эти ошибки в удобные для пользователя сообщения об ошибках проверки, потому что вы не сможете объяснить на простом английском языке, в чем проблема. Сообщения часто зашифрованы, например, вы стали жертвой взаимоблокировки или ограничение внешнего ключа FK_Users_Name не удалось. Это помогает? - person Steven; 30.07.2011
comment
@Steven: У вас должен быть собственный тег SO для вопросов, адресованных вам :) Хорошие вещи! - person Ben; 30.12.2011
comment
Эй, Стивен. Хороший ответ выше. Единственная проблема, с которой я столкнулся бы, заключается в том, что вы выдаете исключение при ошибке проверки. Вы не считаете это излишеством? Я всегда думал, что исключение должно создаваться только в исключительных обстоятельствах, и модель, не прошедшая проверку, не для меня. Что ты думаешь? - person Kevin M; 21.01.2012
comment
В Руководстве по проектированию фреймворка говорится, что "исключительный случай ' - это больше, чем просто ошибки. FDG советует не генерировать исключения в обычном (счастливом) потоке приложения, а использовать его в исключительном потоке. Ошибка проверки — это не «нормальный поток», а «исключительный поток», поскольку нормальным потоком будет случай, когда пользователь правильно введет все данные, поэтому в этом смысле можно выбрасывать. Но что еще более важно, когда какой-либо метод не делает то, что обещает, он должен генерировать исключение (об этом сообщает FDG)... - person Steven; 22.01.2012
comment
Метод service.CreateProduct обещает создать продукт, но когда он не может этого сделать (например, из-за ошибок валидации), единственно правильное решение — выдать исключение. Кроме того, было бы обременительно возвращать коды ошибок, и не говоря уже о том, чтобы этот метод не работал автоматически. Однако это не означает, что вы не можете вызвать какой-либо метод проверки (который возвращает ошибки проверки) перед вызовом службы, поэтому существует больше способов содрать шкуру с кошки. Тем не менее, ИМО - это, по крайней мере, сервис, который должен подтвердить себя... - person Steven; 22.01.2012
comment
Все остальные проверки (например, проверка на стороне клиента) являются необязательными (с точки зрения архитектуры). Когда вы вызываете validate перед вызовом службы, возможно, нет необходимости перехватывать исключение, выдаваемое службой. Кроме того, если вы имеете в виду производительность «излишней», накладные расходы на создание одного исключения во время (например) веб-запроса незначительны. - person Steven; 22.01.2012
comment
@ Стивен +1, это отличный ответ. У меня есть одна проблема с ним, но я не знаю, как решить. не могли бы вы взглянуть на этот вопрос и дать совет? - person Benjamin Gale; 28.05.2013
comment
@Steven - Большое спасибо за уровень детализации, который вы описали. Это помогло мне значительно структурировать приложение, над которым я работаю. Надеюсь, вы не возражаете, но у меня есть быстрый вопрос, используя описанный выше подход. Где вы видите валидацию, требующую доступа к сервису или репозиторию? Придерживаясь приведенного выше примера продукта, как ваш ProductValidator проверит, что имя продукта еще не существует в базе данных? - person Dave Hogan; 25.07.2013
comment
@DaveHogan: я не могу дать прямой ответ на этот вопрос, и эти типы проверки могут получить очень сложно и довольно быстро. Но основная идея заключалась бы в том, чтобы запросить это имя в базе данных, не принимая во внимание рассматриваемый проверенный продукт. - person Steven; 25.07.2013
comment
Спасибо @Steven - я это обнаружил! Я просто пытаюсь разделить все проблемы в приложении MVC (презентация, сервис и репозиторий) с помощью DI/IoC. Служба, отвечающая за обеспечение достоверности объекта (хотя и не напрямую) перед передачей объекта в репо. ProductValidator в этом случае требует ProductService для выполнения этого запроса к базе данных. Я не хочу создавать конкретный экземпляр службы продукта в самом валидаторе. Тем более что от этого зависит создание сервиса. Что вы думаете об управлении проверкой на уровне репозитория? - person Dave Hogan; 25.07.2013
comment
Я не вижу проблем в наличии валидатора, который взаимодействует с базой данных, но ваш ProductValidator не должен ничего знать о вашем ProductService. Скорее наоборот, но название ProductService попахивает классом со слишком большим количеством обязанностей. Вместо этого вас может заинтересовать модель, в которой все операции являются сообщениями и у вас есть валидатор для каждого сообщения. - person Steven; 25.07.2013
comment
Это настолько безумно, насколько невероятно. Спасибо, что поделились, в некотором роде. - person Antrim; 03.03.2016
comment
Ошибки проверки являются исключительными. Только представьте, пользователь может отправить на ваш сервер буквально что угодно, и у вас действительно нет времени, чтобы осмыслить все возможности. Таким образом, в основном, если что-то не подходит, это недействительно и является исключительным, это намного лучше, чем это недействительно, потому что... - person Worthy7; 17.10.2016
comment
@Steven Как нам сделать часть ninject, если мы просто используем внедрение зависимостей ядра Asp.net без ninject? Я застрял на динамической привязке. IValidator ValidationProvider(Type type) { var valType = typeof(Validator<>).MakeGenericType(type); return (IValidator)kernel.Get(valType); } Что эквивалентно этому без использования ninject? - person Martin Dawson; 18.07.2017
comment
@MartinDawson нет поддержки пакетной регистрации в реализации OOTB контейнера DI .NET Core. Этот контейнер действительно ограничен в возможностях. Вы должны использовать стороннее расширение поверх этой библиотеки или использовать сторонний контейнер, такой как Autofac, Simple Injector или Ninject, чтобы иметь возможность сделать это. - person Steven; 18.07.2017
comment
@Стивен Хорошо. Не могли бы вы опубликовать пример с использованием Autofac? Ninject нельзя использовать с ядром Asp.net, и я недостаточно знаю об этом, чтобы исправить это вместе. Спасибо. - person Martin Dawson; 18.07.2017
comment
@MartinDawson: Ваше утверждение неверно. Ninject можно использовать с ASP.NET Core. См. этот вопрос/а и взгляните на этот пример репозитория для примера того, как подключить Ninject к ASP.NET Core. Если вы хотите использовать Autofac, здесь тому пример. - person Steven; 18.07.2017
comment
@Стивен Не удалось заставить это работать. Недостаточно знаю о Ninject, чтобы заставить его работать, так как мои настройки немного отличаются от примера. Возможно ли это с AutoFac? Мне просто нужно, чтобы validatorFactory работал. - person Martin Dawson; 18.07.2017
comment
@MartinDawson: Конечно, это возможно с Autofac, но я не могу вам с этим помочь. - person Steven; 18.07.2017

Я хотел бы расширить фантастический ответ Стивенса, где он написал:

Последнее, чего сейчас не хватает, — это автоматическая регистрация (или пакетная регистрация). Это избавит вас от необходимости добавлять регистрацию для каждой реализации Validator и позволит Ninject динамически искать ваши сборки для вас. Я не смог найти никаких примеров этого, но я предполагаю, что Ninject может это сделать.

Он ссылается на то, что этот код не может быть автоматическим:

kernel.Bind<Validator<Product>>().To<ProductValidator>();

А теперь представьте, что у вас есть десятки подобных:

...
kernel.Bind<Validator<Product>>().To<ProductValidator>();
kernel.Bind<Validator<Acme>>().To<AcmeValidator>();
kernel.Bind<Validator<JohnDoe>>().To<JohnDoeValidator>();
...

Итак, чтобы преодолеть это, я нашел способ сделать это автоматически:

kernel.Bind(
    x => x.FromAssembliesMatching("Fully.Qualified.AssemblyName*")
    .SelectAllClasses()
    .InheritedFrom(typeof(Validator<>))
    .BindBase()
);

Где вы можете заменить Fully.Qualified.AssemblyName на ваше фактическое полное имя сборки, включая ваше пространство имен.

ОБНОВЛЕНИЕ: чтобы все это заработало, вам нужно установить пакет NuGet и используйте Ninject.Extensions.Conventions и используйте пространство имен Bind(), который принимает делегата в качестве параметра.

person kayess    schedule 27.10.2016
comment
Я не мог заставить его работать, кажется, kernel.Bind не принимает тип делегата в качестве параметра? - person Okan Kocyigit; 07.12.2017
comment
вы используете какое-то расширение? Вот IKernel реализация, и нет любой метод Bind, который принимает делегат в качестве параметра. Не могли бы вы проверить свой модуль? - person Okan Kocyigit; 07.12.2017
comment
Да Ninject.Extensions.Conventions, вам нужно использовать это .Bind() - person kayess; 07.12.2017