在 ASP.NET MVC 應用中使用 NInject 注入 ASMX 類型的 Web Service


這幾天,有同學問到為什么在 ASP.NET MVC 應用中,無法在 .ASMX 中使用 NInject 進行注入。

現象

比如,我們定義了一個接口,然后定義了一個實現。

public interface IMessageProvider
{
    string GetMessage();
}

定義一個接口的實現。

public class NinjectMessageProvider : IMessageProvider
{
    public string GetMessage()
    {
        return "This message was provided by Ninject";
    }
}

在 ASMX 中進行 NInject 進行注入。

[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.ComponentModel.ToolboxItem(false)]
public class MyService 
{

    [Ninject.Inject]
    public IMessageProvider MessageProvider { set; get; }

    [WebMethod]
    public string HelloWorld()
    {
        var result = MessageProvider.GetMessage();return "Hello World";
    }
}

你會發現,注入失敗!!!

System.NullReferenceException: 未將對象引用設置到對象的實例。

分析

Why?

這需要從 ASP.NET MVC 應用的結構說起了,相對與 WebForm 應用,MVC 是微軟重新打造的嶄新 Web 應用框架,雖然已經誕生多年了,沒有那么新了,但是,從理念到實現確實是革命性的不同。這里面最核心的一個不同,就是在 MVC 中從框架級別全面使用了 DI 容器。在 MVC 中,所有對象的創建都使用了容器來獲取,你自己定義的類就看你自己了,反正系統已經做到了。

在使用 NInject 的時候,一個重要的步驟就是在 global.asax 中的第一行就替換掉系統默認的容器,這樣保證新創建的對象是從 NInject 中獲取的,以便 NInject 完成依賴注入的實現。

System.Web.Mvc.DependencyResolver.SetResolver(new NinjectDependencyResolver());

上面的這行代碼大家應該很熟悉了,這樣就把對象創建的所有權轉移到了 NInject 手中。

但是,這是對 MVC 來說的,對於原來的 WebForm, ASMX 等等,MVC 是不管的,Scott Hanselman 有一篇文章討論了這個問題。

Plug-In Hybrids: ASP.NET WebForms and ASP.MVC and ASP.NET Dynamic Data Side By Side

所以,好消息是在 MVC 應用中,可以繼續使用原有的 ASPX,ASMX 等等類型的特性,壞消息就是,這些類型的對象都不是 MVC 來管理創建和使用的,也就是說,MVC 的 DI 容器不管理這些對象,所以,在使用 NInject 的時候,也就無法實現注入了。

思路

如果我們能夠獲取剛剛創建的 MyService 對象,然后自己使用 NInject 注入一下,不就解決了嗎?只要我們能夠獲取剛剛創建的對象,也能夠獲取 NInject 的容器,調用一下容器提供的注入方法 Inject 就可以了。

//
// Summary:
//     Injects the specified existing instance, without managing its lifecycle.
//
// Parameters:
//   instance:
//     The instance to inject.
//
//   parameters:
//     The parameters to pass to the request.
void Inject(object instance, params IParameter[] parameters);

解決問題

獲取 NInject 容器

在我們  NInject 管理對象中,就可以直接獲取容器對象,我們可以添加一個注入特定對象的方法。

public void Inject(object target)
{
    this.kernel.Inject(target);
}

以后,直接調用這個方法就可以了。完整的類定義如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Ninject;

namespace MvcNinjectAsmx.Models
{
    public class NinjectDependencyResolver
        : System.Web.Mvc.IDependencyResolver
    {
        private Ninject.IKernel kernel;
        public NinjectDependencyResolver()
        {
            this.kernel = new Ninject.StandardKernel();
            this.AddBindings();
        }

        private void AddBindings()
        {
            this.kernel.Bind<IMessageProvider>()
                .To<NinjectMessageProvider>();
        }

        public object GetService(Type serviceType)
        {
            return this.kernel.TryGet(serviceType);
        }

        public IEnumerable<object> GetServices(Type serviceType)
        {
            return this.kernel.GetAll(serviceType);
        }

        public void Inject(object target)
        {
            this.kernel.Inject(target);
        }
    }
}

這是個實例方法,在整個 MVC 中只有一個實例,就是在 Global.asax 中創建的那個,以后,我們可以從 MVC 中直接獲取這個對象,並調用我們的注入方法。

var resolver = System.Web.Mvc.DependencyResolver.Current
    as NinjectDependencyResolver;
resolver.Inject(this);

 

獲取新創建的服務對象

現在的問題變成了如何獲取剛剛創建的 MyService 服務對象了。

最為簡單的方式,是在使用之前,調用我們的注入方法。比如在調用需要注入的對象之前,手工完成注入。

[Ninject.Inject]
public IMessageProvider MessageProvider { set; get; }

[WebMethod]
public string HelloWorld()
{
    var resolver = System.Web.Mvc.DependencyResolver.Current
        as NinjectDependencyResolver;
    resolver.Inject(this);

    var result = MessageProvider.GetMessage();
    return "Hello World";
}

這樣有點太笨了。

在 ASP.NET 中服務對象都是從 HandlerFactory 中創建的,我們應該可以替換掉 .asmx 的處理器工廠,如何能夠獲取到剛剛創建的 MyService 對象,就可以完美處理這個問題了。

打開系統的 web.config 文件,可以找到 .asmx 的處理器管理配置信息。

<add path="*.asmx" verb="*" type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" validate="False" />

StackOverflow 上的一篇文章,描述了如何獲取 ScriptHandlerFactory 創建的處理器。

Getting ScriptHandlerFactory handler

public class WebServiceFactory : IHttpHandlerFactory
{
    public IHttpHandler GetHandler(HttpContext context, string requestType, string url, string pathTranslated)
    {
        PrivilegedCommand cmd = new PrivilegedCommand();
        SecurityCritical.ExecutePrivileged(new PermissionSet(PermissionState.Unrestricted), new SecurityCritical.PrivilegedCallback(cmd.Execute));
        var handlerFactory = cmd.Result;
        var handler = handlerFactory.GetHandler(context, context.Request.RequestType, url, pathTranslated);

        // Inject
        var resolver = System.Web.Mvc.DependencyResolver.Current
            as NinjectDependencyResolver;
        resolver.Inject(handler);

        return handler;
    }

    public void ReleaseHandler(IHttpHandler handler)
    {
            
    }

    private class PrivilegedCommand
    {
        public IHttpHandlerFactory Result = null;

        public void Execute()
        {
            Type handlerFactoryType = typeof(System.Web.Services.WebService).Assembly.GetType("System.Web.Services.Protocols.WebServiceHandlerFactory");
            Result = (IHttpHandlerFactory)Activator.CreateInstance(handlerFactoryType, true);
        }
    }
}

實際上,還是注入失敗了,如果檢查一下,可以發現,我們獲取的 handler 並不是 MyService,而是下面的類型。

System.Web.Services.Protocols.SyncSessionlessHandler

在這個類的內部通過反射來創建 MyService。我們還是沒有拿到剛剛創建的 MyService 對象來實現我們的注入。

所以,這個方法就算了。

換一個思路,我們可以給 MyService 對象提供一個構造函數,這個構造函數總是要被調用的,我們在這里來實現注入不就可以了嗎?

另一篇文章提到這個思路:

Ninject w/ ASMX web service in a MVC3/Ninject 3 environment

public class MyService
{

    [Ninject.Inject]
    public IMessageProvider MessageProvider { set; get; }

    public MyService()
    {
        var resolver = System.Web.Mvc.DependencyResolver.Current
            as NinjectDependencyResolver;
        resolver.Inject(this);
    }

    [WebMethod]
    public string HelloWorld()
    {
        var result = MessageProvider.GetMessage();
        return "Hello World";
    }
}

如果我們定義了多個 WebService ,這樣的話,在每個構造函數中都要寫上注入的這兩行,還是再優化一下。

定義一個支持 NInject 注入的基類來完成這個工作。

public class NInjectWebService
{
    public NInjectWebService()
    {
        var resolver = System.Web.Mvc.DependencyResolver.Current
            as NinjectDependencyResolver;
        resolver.Inject(this);
    }
}

this 就是我們剛剛創建的對象實例。

然后,將我們的服務類定義成派生自這個類的基類。

public class MyService : NInjectWebService
{
    [Ninject.Inject]
    public IMessageProvider MessageProvider { set; get; }

    [WebMethod]
    public string HelloWorld()
    {
        var result = MessageProvider.GetMessage();
        return "Hello World";
    }
}

這樣,以后的 WebServe 只要從這個基類派生就可以了。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM