How to use a DI / IoC container with the model binder in ASP.NET MVC 2+?
L

3

8

Let's say I have an User entity and I would want to set it's CreationTime property in the constructor to DateTime.Now. But being a unit test adopter I don't want to access DateTime.Now directly but use an ITimeProvider :

public class User {
    public User(ITimeProvider timeProvider) {
        // ...
        this.CreationTime = timeProvider.Now;
    }

    // .....
}

public interface ITimeProvider { 
    public DateTime Now { get; }
}

public class TimeProvider : ITimeProvider {
    public DateTime Now { get { return DateTime.Now; } }
}

I am using NInject 2 in my ASP.NET MVC 2.0 application. I have a UserController and two Create methods (one for GET and one for POST). The one for GET is straight forward but the one for POST is not so straight and not so forward :P because I need to mess with the model binder to tell it to get a reference of an implementation of ITimeProvider in order to be able to construct an user instance.

public class UserController : Controller {

    [HttpGet]
    public ViewResult Create() {
         return View();
    }

    [HttpPost]
    public ActionResult Create(User user) {

         // ...

    }
}

I would also like to be able to keep all the features of the default model binder.

Any chance to solve this simple/elegant/etc? :D

Linet answered 24/5, 2010 at 19:30 Comment(1)
related: Ninject specific thread #9187060Archespore
R
5

How about instead of using an ITimeProvider try this:

public class User 
{
    public Func<DateTime> DateTimeProvider = () => DateTime.Now;

    public User() 
    {
        this.CreationTime = DateTimeProvider();
    }
}

And in your unit test:

var user = new User();
user.DateTimeProvider = () => new DateTime(2010, 5, 24);

I know that this is not very elegant but instead of messing with the model binder this could be a solution. If this doesn't feel like a good solution you could implement a custom model binder and override the CreateModel method where you would inject the dependencies in the constructor of the model.

Restate answered 24/5, 2010 at 19:35 Comment(2)
Yes, this could work. A variant of yours could be a default constructor like so : User() : this(TimeProvider.Instance) { } Still I was hoping for a simple way to inject a [something] between the DefaultModel binder and the ASP.NET MVC 2 Framework..Airiness
Well then overriding the DefaultModelBinder is the route you'll have to take.Restate
M
20

A couple of observations:

Don't inject dependencies just to query them in the constructor

There's no reason to inject an ITimeProvider into a user just to invoke Now immediately. Just inject the creation time directly instead:

public User(DateTime creationTime)
{
     this.CreationTime = creationTime;
}

A really good rule of thumb related to DI is that constructors should perform no logic.

Don't use DI with ModelBinders

An ASP.NET MVC ModelBinder is a really poor place to do DI, particularly because you can't use Constructor Injection. The only remaining option is the static Service Locator anti-pattern.

A ModelBinder translates HTTP GET and POST information to a strongly typed object, but conceptually these types aren't domain objects, but similar to Data Transfer Objects.

A much better solution for ASP.NET MVC is to forego custom ModelBinders completely and instead explicitly embrace that what you receive from the HTTP connection is not your full domain object.

You can have a simple lookup or mapper to retrieve your domain object in your controller:

public ActionResult Create(UserPostModel userPost)
{
    User u = this.userRepository.Lookup(userPost);
    // ...
}

where this.userRepository is an injected dependency.

Muttonchops answered 25/5, 2010 at 7:46 Comment(6)
"A much better solution for ASP.NET MVC is to forego custom ModelBinders completely and instead explicitly embrace that what you receive from the HTTP connection is not your full domain object." Now that's an interesting thought. What if we have multiple entities with common properties like creation date in the above example and we wanted to set them all in one binder for all our entities (we have all entities inheriting one Base entity class and this is where the binder is attached to)? In that case dumping the binders would result in code duplication in every Create on every controller??Constituent
The point is that it doesn't matter. You don't receive an Entity, you receive a HTTP POST (or GET). Embrace the protocol. There are many other ways to avoid code duplication.Muttonchops
As asp.net mvc 3 have support for imodelbinderprovider it shows they were thinking along the correct lines, therefore i disagree with your comments yet understand where you are coming from. Another option could be is to create a base controller which handles the repetitive logic...Izak
Does your answer apply when interfaces are involved? I was trying to have my controller accept an interface parameter, which is implemented in the business adapter (UI-to-domain-adapter layer - which lives in the UI assembly). Then in order to get the controller to accept a POST I'd use a custom modelbinder to 'create an instance of the interface'Lillis
On the protocol level, something very concrete is being posted (a string). It's not even an object, so it's most certainly not an interface. Modeling it as an interface can hardly be said to embrace the protocol.Muttonchops
In 2022 you are now able to do service resolution in the model binder. https://mcmap.net/q/1068920/-how-do-i-use-custom-model-binder-that-supports-dependency-injection-in-asp-net-core. Not to say if it's a good idea or not.Rycca
R
5

How about instead of using an ITimeProvider try this:

public class User 
{
    public Func<DateTime> DateTimeProvider = () => DateTime.Now;

    public User() 
    {
        this.CreationTime = DateTimeProvider();
    }
}

And in your unit test:

var user = new User();
user.DateTimeProvider = () => new DateTime(2010, 5, 24);

I know that this is not very elegant but instead of messing with the model binder this could be a solution. If this doesn't feel like a good solution you could implement a custom model binder and override the CreateModel method where you would inject the dependencies in the constructor of the model.

Restate answered 24/5, 2010 at 19:35 Comment(2)
Yes, this could work. A variant of yours could be a default constructor like so : User() : this(TimeProvider.Instance) { } Still I was hoping for a simple way to inject a [something] between the DefaultModel binder and the ASP.NET MVC 2 Framework..Airiness
Well then overriding the DefaultModelBinder is the route you'll have to take.Restate
V
1

Another option is to create a different class to represent users that haven't been persisted yet that doesn't have a creation date property at all.

Even if CreationDate is one of User's invariants, it can be nullable in your view model - and you can set it farther downstream, in your controller or domain layer.

After all, it probably doesn't matter, but should the creation date attribute really represent the moment you construct a user instance, or would it be more appropriate for it to represent the moment a user submits their data?

Valediction answered 24/5, 2010 at 20:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.