Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dependency Injection in HtmlHelper extension method?

I want to implement property renderers as handlers. I am using Autofac as a DI container in the app. How can I get objects implementing IPropertyHandler in HtmlHelper extension without using globally accessible container (service location)? Is it a way to register own HtmlHelper in Autofac? Maybe MVC framework provide another way?

public static class HtmlHelperExtensions {
    public static MvcHtmlString Editor(this HtmlHelper html, object model) {
        return new Renderer(new List<IPropertyHandler>() /*Where to get these objects?*/ ).Render(html, model);
    }
}

public class Renderer {
    private readonly ICollection<IPropertyHandler> _propertyRenderers;

    public Renderer(ICollection<IPropertyHandler> propertyRenderers) {
        _propertyRenderers = propertyRenderers;
    }

    public MvcHtmlString Render(HtmlHelper html, object model) {
        var result = "";
        foreach(var prop in model.GetType().GetProperties()) {
            var renderers = _propertyRenderers.OrderBy(b => b.Order);
            //impl
        }
        return new MvcHtmlString(result);
    }
}
like image 546
kmasalski Avatar asked Jun 01 '17 13:06

kmasalski


People also ask

What is dependency injection in .NET Core?

ASP.NET Core supports the dependency injection (DI) software design pattern, which is a technique for achieving Inversion of Control (IoC) between classes and their dependencies. For more information specific to dependency injection within MVC controllers, see Dependency injection into controllers in ASP.NET Core.

What is a dependency injection container?

A Dependency Injection Container is an object that knows how to instantiate and configure objects. And to be able to do its job, it needs to knows about the constructor arguments and the relationships between the objects.

What is dependency injection in Web API?

What is Dependency Injection? A dependency is any object that another object requires. For example, it's common to define a repository that handles data access. Let's illustrate with an example.


1 Answers

AFAIK, MVC 5 doesn't provide a way to do this. But that doesn't mean you can't wire up your own solution.

MVC Core now uses view components that are DI friendly, so you don't have to jump through so many hoops.

As per the article DI Friendly Framework by Mark Seemann, you can make a factory interface for your HTML helper that can be used to instantiate it with its dependencies.

DefaultRendererFactory

First there is a default factory that provides the logical default behavior (whatever that is).

public interface IRendererFactory
{
    IRenderer Create();
    void Release(IRenderer renderer);
}

public class DefaultRendererFactory : IRendererFactory
{
    public virtual IRenderer Create()
    {
        return new Renderer(new IPropertyHandler[] { new DefaultPropertyHandler1(), DefaultPropertyHandler2() });
    }
    
    public virtual void Release(IRenderer renderer)
    {
        if (renderer is IDisposable disposable)
        {
            disposable.Dispose();
        }
    }
}

You may wish to make this default factory smarter or even use a fluent builder to supply its dependencies as per the other article DI Friendly Library so it is more flexible without using a DI container.

IRenderer

Then we use an abstraction for Renderer, IRenderer so it can be swapped easily and/or provided via DI.

public interface IRenderer
{
    MvcHtmlString Render(HtmlHelper html, object model);
}

public class Renderer : IRenderer
{
    private readonly ICollection<IPropertyHandler> _propertyRenderers;

    public Renderer(ICollection<IPropertyHandler> propertyRenderers) 
    {
        _propertyRenderers = propertyRenderers;
    }

    public MvcHtmlString Render(HtmlHelper html, object model) 
    {
        var result = "";
        foreach(var prop in model.GetType().GetProperties()) 
        {
            var renderers = _propertyRenderers.OrderBy(b => b.Order);
            //impl
        }
        return new MvcHtmlString(result);
    }
}

Factory Registration

Next, we provide a hook to register the factory. Since the HTML helper is a static extension method, the only option is to make a static field with a static property or method to set it. Its always good practice to make a getter as well in case there is a need to use a decorator pattern on the factory.

public interface IRendererFactory
{
    IRenderer Create();
    void Release(IRenderer renderer);
}

public static class HtmlHelperExtensions {
    private static IRendererFactory rendererFactory = new DefaultRendererFactory();
    
    public static IRendererFactory RendererFactory
    {
        get => rendererFactory;
        set => rendererFactory = value;
    }

    public static MvcHtmlString Editor(this HtmlHelper html, object model) {
        var renderer = rendererFactory.Create();
        try
        {
            return renderer.Render(html, model);
        }
        finally
        {
            rendererFactory.Release(renderer);
        }
    }
}

You could provide some logical place to register all of your factories statically, if that makes more sense for the app. But there will basically need to be a factory per HTML helper to adhere to the SRP. If you try to generalize, you are basically back to a service locator.

AutofacRendererFactory

Now that all of the pieces are in place, this is how you would slip Autofac into the equation. You will need a custom IRendererFactory that you will make part of your composition root that is specific to Autofac.

public class AutofacRendererFactory : IRendererFactory
{
    private readonly Autofac.IContainer container;

    public AutofacRendererFactory(Autofac.IContainer container)
    {
        this.container = container ?? new ArgumentNullException(nameof(container));
    }

    public IRenderer Create()
    {
        return this.container.Resolve(typeof(IRenderer));
    }
    
    public void Release(IRenderer renderer)
    {
        // allow autofac to release dependencies using lifetime management
    }
}

Next, you need to add the type mappings for IRenderer and its dependencies to Autofac.

Last but not least, you will need to add a line to your application startup after creating the Autofac container to resolve the renderer when it is needed by the application.

// Register all of your types with the builder
// ...
// ...
Autofac.IContainer container = builder.Build();
HtmlHelperExtensions.RendererFactory = new AutofacRendererFactory(container);
like image 180
NightOwl888 Avatar answered Sep 30 '22 15:09

NightOwl888