simpleinjector / SimpleInjector

An easy, flexible, and fast Dependency Injection library that promotes best practice to steer developers towards the pit of success.
https://simpleinjector.org
MIT License
1.21k stars 156 forks source link

Suggestion: Update documentation for Web Api Integration #265

Closed seangwright closed 8 years ago

seangwright commented 8 years ago

Hello,

I use Simple Injector for my Web Api projects and I love it. Thanks for all the work on the project.

I wanted to make a recommendation for the docs (which I would be happy to create a PR for).

In the Web Api Integration docs you explain how DI does/doesn't work with filter attributes. While the service locator pattern does work, I think there is a much better pattern (and I think this is actually how filter attributes are supposed to be built).

A good explanation can be found at this SO answer.

Basically the attribute class should be pretty dumb, with no external dependencies and only using props to hold internal settings and configuration.

A action filter should then be created that looks for this attribute on the controller or action method based on the current context. If the attribute is found, then the action filter can go to work performing all the business logic and returning some sort of response.

In some of my projects I have the attributes act as custom Authorization attributes and in my action filters I check for some condition and throw a special NotAuthorized exception (which my GlobalExceptionHandler) which causes a 401 to be returned.

These action filters can have dependencies injected through the constructor unlike the attributes, because they are not singletons.

What do you think of making this recommendation to users and would it be helpful to have a small example in the documentation or should I create a couple gists/repo to host the files?

Here is an example action filter

public class UserAuthorizationFilter : IActionFilter
{
    private readonly IUserRepository userRepository;

    public bool AllowMultiple { get { return false; } }

    public UserAuthorizationFilter(IUserRepository userRepository)
    {
        if (userRepository == null)
        {
            throw new ArgumentNullException("userRepository");
        }

        this.userRepository = userRepository;
    }

    public Task<HttpResponseMessage> ExecuteActionFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
    {
        var userAuthAttribute = actionContext
            .ActionDescriptor
            .GetCustomAttributes<UserAuthorizationAttribute>()
            .Concat(actionContext
                .ControllerContext
                .ControllerDescriptor
                .GetCustomAttributes<UserAuthorizationAttribute>())
            .SingleOrDefault();

        // The attribute was not found or the user is authenticated
        if (userAuthAttribute == null || userRepository.IsCurrentUserAuthenticated())
        {
            return continuation();
        }

        throw new NotAuthorizedException("Not Authorized");
    }
}

And here is the corresponding attribute in use

[RoutePrefix("accounts")]
public class AccountsController : ApiController
{
    private IAccountRepository accountRepository;

    public AccountsController(IAccountRepository accountRepository)
    {
        this.accountRepository = accountRepository;
    }

    [UserAuthorization]
    [Route("{id:guid}")]
    [ResponseType(typeof(AccountResponseDTO))]
    public IHttpActionResult Get(Guid id)
    {
        var account = accountRepository.GetAccount(id);

        return Ok(account);
    }
}

The filter would be registered in the Web Api configuration class like so

config.Filters.Add(container.GetInstance<UserAuthorizationFilter>());

Thanks again!

seangwright commented 8 years ago

Nevermind - didn't see the links to this approach below the service locator example. Closing!