AOP implemented with the Decorator pattern over a Generic Repository

寵の児 提交于 2019-12-05 17:03:25

(1) How do I wire this up using Simple Injector (in the bootstrap class) and still keep the ordering the same,

Simple Injector contains a RegisterDecorator method that can be used to register decorators. Registered decorators are (guaranteed to be) applied in the order in which they are registered. Example:

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

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

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

This configuration ensures that every time an IGenericRepository<T> is requested, an GenericRepository<T> is returned which is wrapped with an LoggingRepository<T> which is wrapped by an SecurityRepository<T>. The last registered decorator will be the outer-most decorator.

(2) Is this the proper use of the Decorator Pattern (since I'm not using the base abstract or interface class or decorator base)

I'm not sure how you're currently doing things; I don't see any decorators in your code. But one thing is wrong. Your GenericRepository<T> uses the ILogger, but logging is a cross-cutting concern. It should be placed in a decorator. That decorator might look like this:

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) Is there a clean way to make use of more than one implementation of a ILogger service (for example DatabaseLogger and ConsoleLogger) in the same Repository without injecting two different versions?

It depends on your needs, but either the Composite Pattern or the Proxy pattern might be of help here. The Composite pattern allows you to hide a collection of 'things' behind an interface of that thing. For instance:

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);
        }        
    }    
}

You can register this as follows:

// 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);

With the proxy pattern on the other hand you could hide some decision about how to root the message inside the proxy. Example:

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;
    }
}

You can register this as follows:

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

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

(4) The actual logging is implemented in the Repository method and the ILogger service is injected into the Repository class, but is there a better way to do this than hard wire up the logger and still use Generic Repositories?

Absolutely: don't inject the logger into the repository, since this is a cross-cutting concern. You will probably be changing the logging logic much sooner than you will change the rest of the generic repository code. So you should write a decorator instead.

Happily, since you created a generic interface for your repositories, you will only have to write one generic decorator for adding logging behavior to repositories. Unfortunately, since the repository interface has 5 members, your decorators will need to implement all of them. But you can't blame decorators for this; it's the Repository pattern itself that violates the Interface Segregation Principle.

UPDATE:

private readonly IGenericRepository securityGenericRepository;

You shouldn't name your repository like this. Security and logging are cross-cutting concerns and the consumer should not have to know about their existence. What if you decide you need an extra cross-cutting concern that should be triggered before the security goes off? Are you going to rename all your securityGenericRepository dependencies to fooGenericRepository? That would defeat the whole purpose of having decorators: they allow you to plug in new cross-cutting concerns dynamically, without having to change a single line of code in your application.

If I want to take some action in the Controller based on a return value

Think hard if that really is what you need. Especially for security. At that level you should usually only want to check and throw an exception. You don't want to catch such exception in your controllers, let alone that you want to return a value.

Such a security decorator is usually meant as safety mechanism to prevent evil doers from doing bad things with your system. Throwing SecurityException is the right thing to do. Such exception will be logged and will be picked up by your team or by support. What you are probably trying to do is to show users a friendly message when they clicked a button that their current role doesn't allow, but instead you should prevent showing this button to the user.

And you might still show the user a friendly message by implementing the Application_Error event and checking whether a SecurityException was thrown and redirecting the user to a page that explains that they unfortunately tried to accessed a page that the system didn't allow access to. But IMO, if the user sees that page, they either are 'hacking' the system, or you made a programming mistake.

Please remember that a decorator implements the same abstraction as it wraps. This means that you can't change the abstraction (and can't return something different) with an decorator. If this is what you need, your consumer will have to depend on something different. But please note that this is not a very common scenario, so you have to think really hard if this really is what you need.

In a system I'm working on right now, my Windows forms classes depend on an IPromptableCommandHandler<TCommand> instead of ICommandHandler<TCommand>. That's because we wanted to show a dialog to the user that explained that the data they entered was invalid (some data can only be validated by the server) and besides the command, we pass in a delegate that allows the 'promptable command handler' to call back in case the command was handled successfully. The promptable command handler implementation itself depends on an ICommandHandler<TCommand> and delegates the work and catches any ValidationException that are returned from the WCF service. This prevents each form from having an ugly try-catch block. Still the solution isn't really nice, and I will change when I got a better solution.

But still, even with such solution, you probably still want to create a decorator that does security and have a proxy (the promptable command handler in my case) that contains the catch statement. Don't try to return something different from a decorator.

What I don't understand above is, where/when does the logger get called?

The registration with the two decorators ensures that when a IGenericRepositotory<Customer> is requested, the following object graph is constructed:

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

When a controller calls the repository Create method, the following call chain will be executed:

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

So, the security decorator calls the logging decorator, because security decorator wraps the logging decorator (and the logging decorator wraps the GenericRepository<T>).

ps. Your method naming for the repository is really ugly. Here are some tips:

  • Call the interface IRepository<T> instead of IGenericRepository<T> (because T implies that it is in fact generic).
  • Remove all the T postfixes from the methods; they have no meaning when you define closed repositories. For instance, what does IRepository<Customer>.CreateT do? What is 'T' in the context of an IRepository<Customer>? A better name would be CreateCustomer, but that is not possible, because IRepository<Order>.CreateCustomer wouldn't make any sense. By naming it IRepository<T>.Create all those problems go away.
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!