简体   繁体   中英

C# ASP.NET Dependency Injection with IoC Container Complications

I apologise for the length, and I know there are some answers on this but I searched a lot and haven't found the right solution, so please bear with me.

I am trying to create a framework for legacy applications to use DI in ASP.NET webforms. I will probably use Castle Windsor as the framework.

These legacy applications will use in part an MVP pattern in some places.

A presenter would look something like this:

class Presenter1
{
    public Presenter1(IView1 view, 
        IRepository<User> userRepository)
    {
    }
}

Now the ASP.NET Page would look something like this:

public partial class MyPage1 : System.Web.UI.Page, IView1
{
    private Presenter1 _presenter;
}

Before using DI I would instantiate the Presenter as follows in the OnInit of the page:

protected override void OnInit(EventArgs e)
{
    base.OnInit(e);
    _presenter = new Presenter1(this, new UserRepository(new SqlDataContext()));
}

So now I want to use DI.

First I must create a handler factory to override the construction of my page. I found THIS really good answer to help: How to use Dependency Injection with ASP.NET Web Forms

Now I can easily set up my containers in my composition root as Mark Seeman suggest to use the Global.asax (This means though to create a static container that must be thread safe and sealed not to be able to add further registrations)

Now I can go and declare the constructor injection on the page

public MyPage1() : base()
{
}

public MyPage1(Presenter1 presenter) : this()
{
    this._presenter =  presenter;
}

Now we run into the first problem, I have a circular dependency. Presenter1 depends on IView1, But the page depends on the presenter.

I know what some will say now that the design is probably incorrect when you have circular dependencies. First I dont think the Presenter design is incorrect, by it taking a dependency in the constructor to the View, and I can say this by looking at plenty of MVP implementations.

Some may suggest changing the Page to a design where Presenter1 becomes a property and then using Property injection

public partial class MyPage1 : System.Web.UI.Page, IView1
{
    [Dependency]
    public Presenter1 Presenter
    {
        get; set;
    }
}

Some may even suggest removing the dependency to presenter completely and then simply Wiring up via a bunch of events, But this is not the design I wanted and frankly dont see why I need to make this change to accomodate it.

Anyway regardless of the suggestion, another problem exists:

When the Handler factory gets a page request only a type is available (NOT THE VIEW INTERFACE):

Type pageType = page.GetType().BaseType;

now using this type you can resolve the Page via IoC and its dependencies:

container.Resolve(pageType)

This will then know that there is a property called Presenter1 and be able to inject it. But Presenter1 needs IView1, but we never resolved IView1 through the container, so the container won't know to provide the concrete instance the handler factory just created as it was created outside of container.

So we need to hack our handler factory and replace the view interface: So where the handler factory resolves the page:

private  void InjectDependencies(object page)
{
    Type pageType = page.GetType().BaseType;
    // hack
    foreach (var intf in pageType.GetInterfaces())
    {
        if (typeof(IView).IsAssignableFrom(intf))
        {
            _container.Bind(intf, () => page); 
        }
    }

    // injectDependencies to page...    
} 

This poses another problem, most containers like Castle Windsor will not allow you to reregister this interface to the instance it is pointing to now. Also with the container being registered in the Global.asax, it is not thread-safe to do as the container should be read only at this point.

The other solution is to create a function to rebuild the container on each web request, and then check to see if the container contains the component IView if not set the instance. But this seems wasteful and goes against suggested use.

The other solution is to create a special Factory called IPresenterFactory and put the dependency in the page constructor:

public MyPage1(IPresenter1Factory factory) : this()
{
    this._presenter = factory.Create(this);
}

The problem is that you now need to create a factory for each presenter and then make a call to the container to resolve other dependencies:

class Presenter1Factory : IPresenter1Factory 
{
    public Presenter1Factory(Container container)
    {
        this._container = container;
    }
    public Presenter1 Create(IView1 view)
    {
        return new Presenter1(view, _container.Resolve<IUserRepository>,...)
    }
}

This design also seems cumbersome and over complicated, does any one have ideas for a more elegant solution?

Perhaps I misunderstand your problems, but the solution seems fairly simple to me: promote the IView to a property on the Presenter1 :

class Presenter1
{
    public Presenter1(IRepository<User> userRepository)
    {
    }

    public IView1 View { get; set; }            
}

This way you can set the presenter on the view like this:

public Presenter1 Presenter { get; set; }

public MyPage1() 
{
    ObjectFactory.BuildUp(this);
    this.Presenter.View = this;
}

Or without property injection, you can do it as follows:

private Presenter1 _presenter;

public MyPage1() 
{
    this._presenter = ObjectFactory.Resolve<Presenter1>();
    this._presenter.View = this;
}

Constructor injection in Page classes and user controls will never really work. You can get it to work in full trust ( as this article shows ), but it will fail in partial trust. So you will have to call the container for this.

All DI containers are thread-safe, as long as you don't manually add registrations yourself after the initialization phase and with some containers even that is thread-safe ( some containers even forbid registering types after initialization). There would never be a need to do this (except for unregistered type resolution, which most containers support). With Castle however, you need to register all concrete types upfront, which means it needs to know about your Presenter1 , before you resolve it. Either register this, change this behavior, or move to a container that allows resolving concrete types by default.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM