开发者

Injecting a service into a base class using Autofac

TL;DR: How can I consolidate logic shared by two custom ModelBinder implementations into a single base class, when both implementations rely on Autofac to inject a (common) dependency into them?


While reviewing some code in an ASP.NET MVC project I'm working on, I realized that I have two custom model binders that essentially do they same thing. They both inherit from DefaultModelBinder, and they both encode a single property on two separate view model classes, using an IEncodingService that is injected into their constructors.

public class ResetQuestionAndAnswerViewModelBinder : DefaultModelBinder {
    public ResetQuestionAndAnswerViewModelBinder(IEncodingService encodingService) {
        encoder = encodingService;
    }

    private readonly IEncodingService encoder;

    public override object BindModel(ControllerContext controllerContext,
                                     ModelBindingContext bindingContext) {
        var model = base.BindModel(controllerContext, bindingContext) as ResetQuestionAndAns开发者_运维问答werViewModel;

        if (model != null) {
            var answer = bindingContext.ValueProvider.GetValue("Answer");

            if ((answer != null) && !(answer.AttemptedValue.IsNullOrEmpty())) {
                model.Answer = encoder.Encode(answer.AttemptedValue);
            }
        }

        return model;
    }
}

public class ConfirmIdentityViewModelBinder : DefaultModelBinder {
    public ConfirmIdentityViewModelBinder(IEncodingService encodingService) {
        encoder = encodingService;
    }

    private readonly IEncodingService encoder;

    public override object BindModel(ControllerContext controllerContext,
                                     ModelBindingContext bindingContext) {
        var model = base.BindModel(controllerContext, bindingContext) as ConfirmIdentityViewModel;

        if (model != null) {
            var secretKey = bindingContext.ValueProvider.GetValue("SecretKey");

            if ((secretKey != null) && !(secretKey.AttemptedValue.IsNullOrEmpty())) {
                model.SecretKeyHash = encoder.Encode(secretKey.AttemptedValue);
            }
        }

        return model;
    }
}

I wrote a generic base class for both of these classes to inherit from:

public class EncodedPropertyModelBinder<TViewModel> : DefaultModelBinder 
    where TViewModel : class {

    public EncodedPropertyModelBinder(IEncodingService encodingService,
                                      string propertyName) {
        encoder = encodingService;
        property = propertyName;
    }

    private readonly IEncodingService encoder;
    private readonly string property;

    public override object BindModel(ControllerContext controllerContext,
                                     ModelBindingContext bindingContext) {
        var model = base.BindModel(controllerContext, bindingContext) as TViewModel;

        if (model != null) {
            var value = bindingContext.ValueProvider.GetValue(property);

            if ((value != null) && !(value.AttemptedValue.IsNullOrEmpty())) {
                var encodedValue = encoder.Encode(value.AttemptedValue);

                var propertyInfo = model.GetType().GetProperty(property);
                propertyInfo.SetValue(model, encodedValue, null);
            }
        }

        return model;
    }
}

Using Autofac, how would I inject the IEncodingService into the base class constructor, while forcing derived classes to provide the name of the property to encode?


I would actually approach this slightly differently, by favoring composition over inheritance. This means I would encapsulate the details of the property manipulation, and pass different implementations to a single model binder.

First, define an interface which represents binding a single property:

public interface IPropertyBinder
{
    void SetPropertyValue(object model, ModelBindingContext context);
}

Then, implement it using the parameters originally from EncodedPropertyModelBinder:

public sealed class PropertyBinder : IPropertyBinder
{
    private readonly IEncodingService _encodingService;
    private readonly string _propertyName;

    public PropertyBinder(IEncodingService encodingService, string propertyName)
    {
        _encodingService = encodingService;
        _propertyName = propertyName;
    }

    public void SetPropertyValue(object model, ModelBindingContext bindingContext)
    {
        var value = bindingContext.ValueProvider.GetValue(_propertyName);

        if(value != null && !value.AttemptedValue.IsNullOrEmpty())
        {
            var encodedValue = _encodingService.Encode(value.AttemptedValue);

            var property = model.GetType().GetProperty(_propertyName);

            property.SetValue(model, encodedValue, null);
        }
    }
}

Next, implement EncodedPropertyModelBinder using the new interface:

public class EncodedPropertyModelBinder : DefaultModelBinder
{
    private readonly IPropertyBinder _propertyBinder;

    public EncodedPropertyModelBinder(IPropertyBinder propertyBinder)
    {
        _propertyBinder = propertyBinder;
    }

    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var model = base.BindModel(controllerContext, bindingContext);

        if(model != null)
        {
            _propertyBinder.SetPropertyValue(model, bindingContext);
        }

        return model;
    }
}

Finally, register two versions of the view model using Autofac named instances, passing in different configurations of PropertyBinder:

builder.
    Register(c => new EncodedPropertyModelBinder(new PropertyBinder(c.Resolve<IEncodingService>(), "Answer")))
    .Named<EncodedPropertyModelBinder>("AnswerBinder");

builder.
    Register(c => new EncodedPropertyModelBinder(new PropertyBinder(c.Resolve<IEncodingService>(), "SecretKey")))
    .Named<EncodedPropertyModelBinder>("SecretKeyBinder");
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜