AOP реализован с использованием шаблона Decorator в общем хранилище

Я пытаюсь создать прототип, который применяет Аспектно-ориентированное программирование к моему проекту с использованием Декораторов. Некоторая часть моего проекта будет использовать общий репозиторий (для простого CRUD), но в конечном итоге я также включу обработчики команд и запросов (они будут выполнять конкретные задачи, такие как ProcessCustomerOrders и т. Д.). Кроме того, сквозными проблемами, которые я хотел бы привести здесь, являются безопасность и ведение журнала.

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

Я понимаю, что существуют другие способы реализации AOP (или сквозные вопросы), такие как шаблоны Proxy или Code Weaving, но я не знаком с этими шаблонами и поэтому не знаю компромиссов между ними.

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

Мои вопросы:

(1) Как мне подключить это с помощью Simple Injector (в классе начальной загрузки) и при этом сохранить порядок?

(2) Является ли это правильным использованием шаблона Decorator (поскольку я не использую базовый реферат или интерфейсный класс или базу декоратора)?

(3) Есть ли чистый способ использовать более одной реализации службы ILogger (например, DatabaseLogger и ConsoleLogger) в одном и том же репозитории без внедрения двух разных версий?

(4) Фактическое ведение журнала реализовано в методе Repository, а служба ILogger внедряется в класс Repository, но есть ли лучший способ сделать это, чем жестко подключить регистратор и все еще использовать универсальные репозитории?

(5) Должен ли я использовать шаблоны Proxy или Code Weaving, основанные на том, как я использую репозиторий в этом прототипе?

Также приветствуются общие критические замечания по этому проекту.

Код прототипа:

public class Program
{
    public static void Main(string[] args)
    {
        var e = new Entity
        {
            Id = 1,
            Name = "Example Entity",
            Description = "Used by Decorators",
            RowGuild = Guid.NewGuid()
        };

        Controller controller = 
            new Controller(
                new GenericRepository<Entity>(
                    new ClientManagementContext(), 
                    new ConsoleLogger()
                ), 
                new WebUser()
            );

        controller.Create(e);
    }
}

public static class RepositoryBoostrapper
{
    public static void Bootstrap(Container container)
    {
        container.RegisterOpenGeneric(typeof(IGenericRepository<>), typeof(GenericRepository<>));
    }
}

public class Entity
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public Guid RowGuild { get; set; }
    public byte[] RowVersion { get; set; }
}

public class Controller
{
    private readonly IGenericRepository<Entity> _repository;
    private readonly IUserSecurity _userSecurity;

    public Controller(IGenericRepository<Entity> repository, IUserSecurity userSecurity)
    {
        _repository = repository;
        _userSecurity = userSecurity;
    }

    // Displays all Entities on a web page view
    public ActionResult Index() {
        IEnumerable<Entity> e = null;
        User user = User.Identity.Name;

        if (_userSecurity.ValidateUser(user))
        {
            e = _repository.ReadTs();
        }
        return View(e);
    }

    public ActionResult Create(Entity e) {
        User user = User.Identity.Name;

        if (_userSecurity.ValidateUser(user))
        {
            if (ModelState.IsValid)
            {
                _repository.CreateT(e);
                return RedirectToAction("Index");
            }
        }
        return View(e);
    }
}

public interface IGenericRepository<T>
{
    T ReadTById(object id);
    IEnumerable<T> ReadTs();
    void UpdateT(T entity);
    void CreateT(T entity);
    void DeleteT(T entity);
}

public class GenericRepository<T> : IGenericRepository<T> where T : class
{
    private readonly ClientManagementContext _context;
    private readonly ILogger _logger;

    public GenericRepository(ClientManagementContext context, ILogger logger)
    {
        _context = context;
        _logger = logger;
    }

    public T ReadTById(object id) {
        return _context.Set<T>().Find(id);
    }

    public IEnumerable<T> ReadTs() {
        return _context.Set<T>().AsNoTracking().AsEnumerable(); 
    }

    public void UpdateT(T entity) {
        var watch = Stopwatch.StartNew();

        _context.Entry(entity).State = EntityState.Modified;
        _context.SaveChanges();

        _logger.Log(typeof(T).Name +
        " executed in " +
        watch.ElapsedMilliseconds + " ms.");
    }

    public void CreateT(T entity) {
        var watch = Stopwatch.StartNew();

        _context.Entry(entity).State = EntityState.Added;
        _context.SaveChanges();

        _logger.Log(typeof(T).Name +
        " executed in " +
        watch.ElapsedMilliseconds + " ms.");
    }


    public void DeleteT(T entity) {
        _context.Entry(entity).State = EntityState.Deleted;
        _context.SaveChanges();
    }
}



public class Logger
{
    private readonly ILogger _logger;

    public Logger(ILogger logger)
    {
        _logger = logger;
    }

    public void Log(string message)
    {
        _logger.Log(message);
    }
}

public interface ILogger
{
    void Log(string message);
}

public class ConsoleLogger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine(message);
    }
}

public class DatabaseLogger : ILogger
{
    public void Log(string message)
    {
        // database logging
    }
}

public interface IUserSecurity
{
    bool ValidateUser(User user);
}

public class UserSecurity
{
    private readonly IUserSecurity _userSecurity;

    public UserSecurity(IUserSecurity userSecurity)
    {
        _userSecurity = userSecurity;
    }

    public bool ValidateUser(User user)
    {
        return _userSecurity.ValidateUser(user);
    }
}

public class WebUser : IUserSecurity
{
    public bool ValidateUser(User user)
    {
        // validate MVC user

        return true;
    }
}

ОБНОВЛЕНИЕ На основании ответа @Steven:

Простой инжектор DI декораторов и репозитория:

public static class RepositoryBoostrapper
{
public static void Bootstrap(Container container)
{
    container.RegisterOpenGeneric(
        typeof(IGenericRepository<>),
        typeof(GenericRepository<>));

    container.RegisterDecorator(
        typeof(IGenericRepository<>),
        typeof(LoggingRepositoryDecorator<>));

    container.RegisterDecorator(
        typeof(IGenericRepository<>),
        typeof(SecurityRepositoryDecorator<>));

}
}

Порядок цепочки Декоратор, как вызывается Контроллером, должен быть "Контроллер (проверки)"> "Безопасность" (если "ОК", чтобы продолжить, разрешить вызов) > "Репо" (обновить слой постоянства и затем) > "Журнал" (для некоторого средства) > и вернуться назад. к контроллеру.

Новый класс контроллеров:

public class Controller
{
private readonly IGenericRepository<Entity> securityGenericRepository;

public Controller(
    IGenericRepository<Entity> securityGenericRepository)
{
    this.securityGenericRepository = securityGenericRepository;
}

// Displays all Entities on a web page view
public bool Index() {
    var e = new Entity
    {
        Id = 1,
        Name = "Example Entity",
        Description = "Used by Decorators",
        RowGuild = Guid.NewGuid()
    };
    this.securityGenericRepository.CreateT(e);
    return false;
}

public ActionResult Create(Entity e) {
    if (ModelState.IsValid)
    {
        this.securityGenericRepository.CreateT(e);
        return RedirectToAction("Index");
    }
    return View(e);
}
}

Вопрос о приведенном выше фрагменте кода:

Если я хочу выполнить какое-либо действие в Контроллере на основе возвращаемого значения (например, вернуть bool из Security Decorator), нужно ли мне тогда изменять интерфейс IGenericRepository (и, следовательно, класс GenericRepository)? В некотором смысле это означает, что поскольку классы Repo и Security Decorator оба реализуют один и тот же интерфейс, если я хочу внести изменения в возвращаемое значение или параметры методов Security, мне также нужно будет изменить методы Repository?

Кроме того, я только теперь передаю в реализации безопасности IGenericRepository к контроллеру?

Кроме того, журнал был изменен, чтобы выглядеть следующим образом:

public class LoggingRepositoryDecorator<T> : IGenericRepository<T>
{
private readonly IGenericRepository<T> decoratee;
private readonly ILogger logger;

public LoggingRepositoryDecorator(IGenericRepository<T> decoratee, ILogger logger)
{
    this.decoratee = decoratee;
    this.logger = logger;
}

// ...

public void CreateT(T entity)
{
    var watch = Stopwatch.StartNew();

    this.decoratee.CreateT(entity);

    this.logger.Log(typeof(T).Name + " executed in " +
        watch.ElapsedMilliseconds + " ms.");
}
// ...
}

Выше я просто звоню Decoratee и добавляю функциональность Decorator сверху.

И наконец Декоратор безопасности:

public class SecurityRepositoryDecorator<T> : IGenericRepository<T>
{
  private readonly IGenericRepository<T> decoratee;
  private readonly IUserSecurity userSecurity;
  private User user;

  public SecurityRepositoryDecorator(
  IGenericRepository<T> decoratee,
  IUserSecurity userSecurity)
  {
    this.decoratee = decoratee;
    this.userSecurity = userSecurity;
    this.user = User.Identity.Name;
  }

  // ...

  public void CreateT(T entity)
  {
    if (userSecurity.ValidateUser(user))
      this.decoratee.CreateT(entity);
  }
  // ...
  }

Что я не понимаю выше, так это где и когда вызывается регистратор?

ОБНОВЛЕНИЕ 2:

Кажется, работает как шаблон Decorator сейчас; спасибо Стивену за все великолепные ответы.

Прототип Основная функция:

public static void Main(string[] args)
{
    var container = new Container();
    PrototypeBoostrapper.Bootstrap(container);

    IRepository<Entity> repository = 
        new ValidateUserDecorator<Entity>(
            new LoggingDecorator<Entity>(
                new Repository<Entity>(
                    new PrototypeContext()), 
                new ConsoleLogger()), 
            new ClaimsPrincipal());

    var controller = new Controller(repository);

    var e = new Entity
    {
        Id = 1,
        Name = "Example Entity",
        Description = "Used by Decorators",
        RowGuild = Guid.NewGuid()
    };

    controller.Create(e);
}

Валидация (Безопасность) Декоратор:

public class ValidateUserDecorator<T> : IRepository<T>
{
    private readonly IRepository<T> decoratee;
    //private readonly IUserSecurity userSecurity;
    private IPrincipal User { get; set; }

    public ValidateUserDecorator(
        IRepository<T> decoratee,
        IPrincipal principal)
    {
        this.decoratee = decoratee;
        User = principal;
    }

    //..
    public void CreateT(T entity)
    {
        if (!User.IsInRole("ValidRoleToExecute"))
            throw new ValidationException();
        this.decoratee.CreateT(entity);
    }
    //..

Логгинг Декоратор:

public class LoggingDecorator<T> : IRepository<T>
{
    private readonly IRepository<T> decoratee;
    private readonly ILogger logger;

    public LoggingDecorator(IRepository<T> decoratee, ILogger logger)
    {
        this.decoratee = decoratee;
        this.logger = logger;
    }

    // ..
    public void CreateT(T entity)
    {
        var watch = Stopwatch.StartNew();

        this.decoratee.CreateT(entity);

        this.logger.Log(typeof(T).Name + " executed in " +
                        watch.ElapsedMilliseconds + " ms.");
    }
    // ..

Общий репозиторий:

public class Repository<T> : IRepository<T> where T : class
{
    private readonly PrototypeContext _context;

    public Repository(PrototypeContext context)
    {
        _context = context;
    }
    //..
    public void CreateT(T entity) {
        _context.Entry(entity).State = EntityState.Added;
        _context.SaveChanges();
    }
    //..

Контроллер:

public class Controller
{
    private readonly IRepository<Entity> repository;

    public Controller(
        IRepository<Entity> repository) {
            this.repository = repository;
    }
    // ..
    public bool Create(Entity e) {
        this.repository.CreateT(e);
        return true;
    }
    // ..

1 ответ

Решение

(1) Как мне подключить это с помощью Simple Injector (в классе начальной загрузки) и при этом сохранить порядок,

Simple Injector содержит метод RegisterDecorator, который можно использовать для регистрации декораторов. Зарегистрированные декораторы применяются (гарантированно) в том порядке, в котором они зарегистрированы. Пример:

container.RegisterOpenGeneric(
    typeof(IGenericRepository<>), 
    typeof(GenericRepository<>));

container.RegisterDecorator(
    typeof(IGenericRepository<>), 
    typeof(LoggingRepositoryDecorator<>));

container.RegisterDecorator(
    typeof(IGenericRepository<>), 
    typeof(SecurityRepositoryDecorator<>));

Эта конфигурация гарантирует, что каждый раз IGenericRepository<T> запрашивается, GenericRepository<T> возвращается, который обернут LoggingRepository<T> который обернут SecurityRepository<T>, Последний зарегистрированный декоратор будет самым внешним декоратором.

(2) Является ли это правильным использованием шаблона Decorator (поскольку я не использую базовый реферат или интерфейсный класс или базу декоратора)

Я не уверен, как ты в настоящее время делаешь вещи; Я не вижу декораторов в вашем коде. Но одна вещь не так. Ваш GenericRepository<T> использует ILogger, но заготовка леса - это сквозная проблема. Это должно быть помещено в декоратор. Этот декоратор может выглядеть так:

public LoggingRepositoryDecorator<T> : IGenericRepository<T> {
    private IGenericRepository<T> decoratee;
    private ILogger _logger;

    public LoggingRepositoryDecorator(IGenericRepository<T> decoratee,
        ILogger logger) {
        this.decoratee = decoratee;
        this._logger = logger;
    }

    public T ReadTById(object id) { return this.decoratee.ReadTById(id); }
    public IEnumerable<T> ReadTs() { return this.decoratee.ReadTs(); }

    public void UpdateT(T entity) {
        var watch = Stopwatch.StartNew();

        this.decoratee.UpdateT(entity);

        _logger.Log(typeof(T).Name + " executed in " + 
            watch.ElapsedMilliseconds + " ms.");    
    }

    public void CreateT(T entity)  {
        var watch = Stopwatch.StartNew();

        this.decoratee.CreateT(entity); 

        _logger.Log(typeof(T).Name + " executed in " + 
            watch.ElapsedMilliseconds + " ms.");    
    }

    public void DeleteT(T entity) { this.decoratee.DeleteT(entity); }
}

(3) Есть ли чистый способ использовать более одной реализации службы ILogger (например, DatabaseLogger и ConsoleLogger) в одном и том же репозитории без внедрения двух разных версий?

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

public class CompositeLogger : ILogger {
    private readonly IEnumerable<ILogger> loggers;

    public CompositeLogger(IEnumerable<ILogger> loggers) {
        this.loggers = loggers;
    }

    public void Log(string message) {
        foreach (var logger in this.loggers) {
            logger.Log(message);
        }        
    }    
}

Вы можете зарегистрировать это следующим образом:

// Register an IEnumerable<ILogger>
container.RegisterCollection<ILogger>(new[] {
    typeof(DatabaseLogger), 
    typeof(ConsoleLogger)
});

// Register an ILogger (the CompositeLogger) that depends on IEnumerable<ILogger>
container.Register<ILogger, CompositeLogger>(Lifestyle.Singleton);

С другой стороны, с помощью шаблона прокси вы можете скрыть какое-то решение о том, как получить root-сообщение внутри прокси. Пример:

public class LoggerSelector : ILogger {
    private readonly ILogger left;
    private readonly ILogger right;

    public LoggerSelector(ILogger left, ILogger right) {
        this.left = left;
        this.right = right;
    }

    public void Log(string message) {
        var logger = this.SelectLogger(message);
        logger.Log(message);
    }

    private ILogger SelectLogger(string message) {
        return message.Contains("fatal") ? this.left : this.right;
    }
}

Вы можете зарегистрировать это следующим образом:

container.Register<ConsoleLogger>();
container.Register<DatabaseLogger>();

container.Register<ILogger>(() => new LoggerSelector(
    left: container.GetInstance<ConsoleLogger>(),
    right: container.GetInstance<DatabaseLogger>());

(4) Фактическое ведение журнала реализовано в методе Repository, а служба ILogger внедряется в класс Repository, но есть ли лучший способ сделать это, чем жестко подключить регистратор и все еще использовать универсальные репозитории?

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

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

ОБНОВИТЬ:

только для чтения, только IGenericRepository securityGenericRepository;

Вы не должны называть свой репозиторий так. Безопасность и ведение журнала являются сквозными проблемами, и потребитель не должен знать об их существовании. Что если вы решите, что вам нужна дополнительная междисциплинарная задача, которая должна быть активирована до того, как сработает система безопасности? Собираетесь ли вы переименовать все свои securityGenericRepository зависимости к fooGenericRepository? Это лишило бы смысла использование декораторов: они позволяют динамически подключать новые сквозные задачи, не изменяя ни одной строки кода в вашем приложении.

Если я хочу предпринять некоторые действия в контроллере на основе возвращаемого значения

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

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

И вы все равно можете показать пользователю дружественное сообщение, внедрив Application_Error событие и проверка, является ли SecurityException был брошен и перенаправил пользователя на страницу, которая объясняет, что он, к сожалению, пытался получить доступ к странице, к которой система не разрешила доступ. Но IMO, если пользователь видит эту страницу, он либо "взламывает" систему, либо вы сделали ошибку в программировании.

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

В системе, над которой я сейчас работаю, классы форм Windows зависят от IPromptableCommandHandler<TCommand> вместо ICommandHandler<TCommand>, Это потому, что мы хотели показать пользователю диалог, в котором объяснялось, что введенные данные были недействительными (некоторые данные могут быть проверены только сервером), и помимо команды мы передаем делегата, который позволяет "обработчику запрашиваемой команды" перезвоните в случае, если команда была обработана успешно. Сама реализация обработчика запрашиваемых команд зависит от ICommandHandler<TCommand> и делегирует работу и ловит любую ValidationException которые возвращаются из WCF оказание услуг. Это препятствует тому, чтобы у каждой формы был уродливый блок try-catch. Тем не менее, решение не очень хорошее, и я изменюсь, когда получу лучшее решение.

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

Что я не понимаю выше, так это где и когда вызывается регистратор?

Регистрация с двумя декораторами гарантирует, что когда IGenericRepositotory<Customer> запрашивается, строится следующий граф объектов:

IGenericRepository<Customer> repository =
    new SecurityRepositoryDecorator<Customer>(
        new LoggingRepositoryDecorator<Customer>(
            new GenericRepository<Customer>(
                new ClientManagementContext()),
            DatabaseLogger(),
        new AspNetUserSecurity());

Когда контроллер вызывает хранилище Create метод, следующая цепочка вызовов будет выполнена:

Begin SecurityRepositoryDecorator<Customer>.Create (calls `userSecurity.ValidateUser`)
    Begin LoggingRepositoryDecorator.Create (calls `Stopwatch.StartNew()`)
        Begin GenericRepository<Customer>.Create
        End GenericRepository<Customer>.Create
    End LoggingRepositoryDecorator.Create (calls ` this.logger.Log`)
End SecurityRepositoryDecorator<Customer>.Create

Таким образом, декоратор безопасности вызывает декоратор логирования, потому что декоратор безопасности оборачивает декоратор логирования (а декоратор логирования оборачивает GenericRepository<T>).

пс. Ваш метод именования репозитория действительно ужасен. Вот несколько советов:

  • Вызвать интерфейс IRepository<T> вместо IGenericRepository<T> (так как T подразумевает, что это на самом деле является общим).
  • Удалить все T постфиксы из методов; они не имеют смысла, когда вы определяете закрытые репозитории. Например, что делает IRepository<Customer>.CreateT делать? Что такое "Т" в контексте IRepository<Customer>? Лучшее имя будет CreateCustomer, но это невозможно, потому что IRepository<Order>.CreateCustomer не имеет никакого смысла. Называя это IRepository<T>.Create все эти проблемы уходят.
Другие вопросы по тегам