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);
}
}
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.
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? 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.
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.
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.
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);
}
}
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.
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);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With