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");
精彩评论