1. 程式人生 > 程式設計 >在.net core中實現欄位和屬性注入的示例程式碼

在.net core中實現欄位和屬性注入的示例程式碼

簡單來說,使用Ioc模式需要兩個步驟,第一是把服務註冊到容器中,第二是從容器中獲取服務,我們一個一個討論並演化。這裡不會考慮使用如Autofac等第三方的容器來代替預設容器,只是提供一些簡單實用的小方法用於簡化應用層的開發。

將服務注入到容器

asp.netcore官方給出的在容器中註冊服務方法是,要在Startup類的ConfigureServices方法中新增服務,如下所示:

public void ConfigureServices(IServiceCollection services)
{
  services.AddMvc();

  services.AddSingleton(typeof(UserService));
  services.AddSingleton(typeof(MsgService));
  services.AddSingleton(typeof(OrderService));
}

AddMvc方法添加了mvc模組內部用到的一些服務,這個是封裝好的,一句話就行了,其他第三方元件也都提供了類似的Add方法,把自己內部需要的服務都封裝好註冊進去了。但是我們應用開發人員使用的類,還是需要一個一個寫進去的,大家最常見的三層架構中的資料訪問層和業務邏輯層便是此類服務,上面程式碼中我加入了三個業務服務類。這顯然不是長久之計,我想大家在開發中也會針對此問題做一些處理,這裡說下我的,僅供參考吧。

解決方法就是批量註冊!說到批量,就需要一個東西來標識一批東西,然後用這一個東西來控制這一批東西。在.net程式的世界中,有兩個可選的角色,一個是介面Interface,另一個是特性Attribute。

如果使用介面作為標識來使用,限制就太死板了,一個標識的資訊不是絕對的單一,是不推薦使用介面的,因為可能需要引入多個接口才能共同完成,所以我選擇特性作為標識。特性相較與介面有什麼特點呢?特性在執行時是類的例項,所以可以儲存更多的資訊。

下面我們簡單實現一個AppServiceAttribute:

/// <summary>
/// 標記服務
/// </summary>
[AttributeUsage(AttributeTargets.Class,Inherited = false)]
public class AppServiceAttribute : Attribute
{
}

這個特性類取名AppService有兩個理由,一是指定是應用層的服務類,二是避免使用Service這樣的通用命名和其他類庫衝突。

有了標識,就可以批量處理了,我們在一個新的類中給IServiceCollection提供一個擴充套件方法,用來批量新增標記有AppService特性的服務到容器中。

public static class AppServiceExtensions
{
  /// <summary>
  /// 註冊應用程式域中所有有AppService特性的服務
  /// </summary>
  /// <param name="services"></param>
  public static void AddAppServices(this IServiceCollection services)
  {
    foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
    {
      foreach (var type in assembly.GetTypes())
      {
        var serviceAttribute = type.GetCustomAttribute<AppServiceAttribute>();

        if (serviceAttribute != null)
        {
          services.AddSingleton(type);
        }
      }
    }
  }
}

我們遍歷應用程式中所有程式集,然後巢狀遍歷每個程式集中的所有型別,判斷型別是否有AppService特性,如果有的話就新增到容器中,這裡有點不自信哦,為什麼呢,因為我是使用AddSingleton方法以單例模式將服務新增到容器中的,雖然三層中的資料訪問層和業務邏輯層絕大部分都可以使用單例,但是我們希望更通用一些,大家都知道netcore自帶的Ioc容器支援三種生命週期,所以我們修改AppServiceAttribute,新增一個Lifetime屬性:

[AttributeUsage(AttributeTargets.Class,Inherited = false)]
public class AppServiceAttribute : Attribute
{
  /// <summary>
  /// 生命週期
  /// </summary>
  public ServiceLifetime Lifetime { get; set; } = ServiceLifetime.Singleton;
}

Lifetime的預設值我們設定成ServiceLifetime.Singleton是比較合適的,因為大部分服務我們都希望使用單例註冊,一個合理的預設設定可以節省使用者很多程式碼,新手可能還會樂於複製貼上,但老同志肯定都深有體會。

有了Lifetime這個資訊,我們就可以改進AddAppServices方法了,在判斷serviceAttribute不為null後,使用下面的程式碼替換services.AddSingleton(type):

  switch (serviceAttribute.Lifetime)
  {
    case ServiceLifetime.Singleton:
      services.AddSingleton(serviceType,type);
      break;
    case ServiceLifetime.Scoped:
      services.AddScoped(serviceType,type);
      break;
    case ServiceLifetime.Transient:
      services.AddTransient(serviceType,type);
      break;
    default:
      break;
  }

現在我們可以註冊不同生命週期的服務了,只是該控制是在類的定義中,按理說,服務物件註冊到容器中的生命週期,是不應該在類的定義中確定的,因為一個類的定義是獨立的,定義好之後,使用者可以用任何一種容器支援的生命週期來註冊例項。但是此時這樣的設計是比較合理的,因為我們要解決的是應用層服務的批量註冊,這類服務一般在定義的時候就已經確定了使用方式,而且很多時候服務的開發者就是該服務的使用者!所以我們可以把這個當成合理的反正規化設計。

目前這樣子,對於我來說,基本已經夠用了,因為在應用層,我都是依賴實現程式設計的😀(哈哈,會不會很多人說咦......呢?)。設計模式說:“要依賴於抽象,不要依賴於具體”,這點我還沒做到,我抽空檢討(呵呵,誰信呢!)。所以呢,我們的批量注入要支援那些優秀的同學。

從上面的程式碼不難發現,如果定義介面IA和其實現A:IA,並在A上新增AppService特性是不行的:

  public interface IA { }

  [AppService]
  public class A : IA { }

這個時候我們並不能依賴IA程式設計,因為我們註冊的服務類是A,實現類是A,我們需要註冊成服務類是IA,實現類是A才可:

public class HomeController : Controller
{
  private IA a;
  public HomeController(IA a)
  {
    this.a = a; //這裡a是null,不能使用
  }
}

讓我繼續改進,在AppServiceAttribute中,我們加入服務型別的資訊:

[AttributeUsage(AttributeTargets.Class,Inherited = false)]
public class AppServiceAttribute : Attribute
{
  /// <summary>
  /// 生命週期
  /// </summary>
  public ServiceLifetime Lifetime { get; set; } = ServiceLifetime.Singleton;
  /// <summary>
  /// 指定服務型別
  /// </summary>
  public Type ServiceType { get; set; }
  /// <summary>
  /// 是否可以從第一個介面獲取服務型別
  /// </summary>
  public bool InterfaceServiceType { get; set; } = true;
}

我們從兩個方面入手來解決服務型別的問題,一個是指定ServiceType,這個就毫無疑問了,在A的AppService中可以明確指定IA為其服務類:

[AppService(ServiceType = typeof(IA))]
public class A : IA { }

另一個是從服務類自身所繼承的介面中獲取服務類形,這一點要在AddAppServices方法中體現了,再次改進AddAppServices方法,還是替換最開始services.AddSingleton(type)的位置:

  var serviceType = serviceAttribute.ServiceType;
  if (serviceType == null && serviceAttribute.InterfaceServiceType)
  {
    serviceType = type.GetInterfaces().FirstOrDefault();
  }
  if (serviceType == null)
  {
    serviceType = type;
  }
  switch (serviceAttribute.Lifetime)
  {
    case ServiceLifetime.Singleton:
      services.AddSingleton(serviceType,type);
      break;
    default:
      break;
  }

我們首先檢查serviceAttribute.ServiceType,如果有值的話,它就是註冊服務的型別,如果沒有的話,看是否允許從介面中獲取服務型別,如果允許,便嘗試獲取第一個作為服務型別,如果還沒獲取到,就把自身的型別作為服務型別。

  • 第一種情況不常見,特殊情況才會指定ServiceType,因為寫起來麻煩;
  • 第二種情況適用於依賴抽象程式設計的同學,注意這裡只取第一個介面的型別;
  • 第三種情況就是適用於像我這種有不良習慣的患者(依賴實現程式設計)!

到此為止我們的服務註冊已經討論完了,下面看看如何獲取。

欄位和屬性注入

這裡我們說的獲取,不是框架預設容器提供的構造器注入,而是要實現欄位和屬性注入,先看看構造器注入是什麼樣的:

public class HomeController : Controller
{
  UserService userService;
  OrderService orderService;
  MsgService msgService;
  OtherService otherService;
  OtherService2 otherService2;

  public HomeController(UserService userService,OrderService orderService,MsgService msgService,OtherService otherService,OtherService2 otherService2)
  {
    this.userService = userService;
    this.orderService = orderService;
    this.msgService = msgService;
    this.otherService = otherService;
    this.otherService2 = otherService2;
  }
}

如果引用的服務不再新增還好,如果編寫邊新增就太要命了,每次都要定義欄位、在構造器方法簽名中些新增引數、在構造器中賦值,便捷性和Spring的@autowired註解沒法比,所以我們要虛心學習,創作更便捷的操作。
首先我們再定義個特性,叫AutowiredAttribute,雖然也是個標識,但是由於這個特性是用在欄位或者屬性上,所以只能用特性Attribute,而不能使用介面Interface,到這裡我們又發現一點,使用介面作為標識的話,只能用在類、介面和結構中,而不能用在他們的成員上,畢竟介面的主要作用是定義一組方法契約(即抽象)!

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class AutowiredAttribute : Attribute
{
}

這個特性裡面什麼也沒有,主要是下面這個類,裝配操作都在這裡:

/// <summary>
/// 從容器裝配service
/// </summary>
[AppService]
public class AutowiredService
{
  IServiceProvider serviceProvider;
  public AutowiredService(IServiceProvider serviceProvider)
  {
    this.serviceProvider = serviceProvider;
  }
  public void Autowired(object service)
  {
    var serviceType = service.GetType();
    //欄位賦值
    foreach (FieldInfo field in serviceType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
    {
      var autowiredAttr = field.GetCustomAttribute<AutowiredAttribute>();
      if (autowiredAttr != null)
      {
        field.SetValue(service,serviceProvider.GetService(field.FieldType));
      }
    }
    //屬性賦值
    foreach (PropertyInfo property in serviceType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
    {
      var autowiredAttr = property.GetCustomAttribute<AutowiredAttribute>();
      if (autowiredAttr != null)
      {
        property.SetValue(service,serviceProvider.GetService(property.PropertyType));
      }
    }
  }
}

我們剛剛寫的[AppService]特性在這裡已經用上了,並且這個類使用構造器注入了IServiceProvider。Autowired(object service)方法的引數是要裝配的服務例項,首先獲取服務型別,再使用反射查詢有AutowiredAttribute特性的欄位和屬性,我們在構造器注入了serviceProvider,這裡便可以使用serviceProvider的GetService方法從容器中獲取對應型別的例項來給欄位和屬性賦值。 整個過程就是這樣,簡單明瞭。開始的時候我想使用靜態類來編寫AutowiredService,但是靜態類沒法注入IServiceProvider,解決方法也有,可以使用定位器模式全域性儲存IServiceProvider:

/// <summary>
/// 服務提供者定位器
/// </summary>
public static class ServiceLocator
{
  public static IServiceProvider Instance { get; set; }
}

在Setup的Configure方法中賦值:

ServiceLocator.Instance = app.ApplicationServices;

這樣在靜態的AutowiredService中也就可以訪問IServiceProvider了,但是使其自己也註冊成服務能更好的和其他元件互動,java有了spring框架,大家都認可spring,一切都在容器中,一切都可注入,spring提供了統一的物件管理,非常好,我感覺netcore的將來也將會是這樣。

Autowired(object service)方法的實現雖然簡單,但是使用了效率底下的反射,這個美中不足需要改進,以前可以使用晦澀難懂的EMIT來編寫,現在有Expression,編寫和閱讀都簡單了好多,並且效率也不比EMIT差,所以我們使用表示式+快取來改進。Autowired方法要做的就是從容器中取出合適的物件,然後賦值給service要自動裝配的欄位和屬性,據此我們先編寫出委託的虛擬碼:

(obj,serviceProvider)=>{
  ((TService)obj).aa=(TAAType)serviceProvider.GetService(aaFieldType);
  ((TService)obj).bb=(TBBType)serviceProvider.GetService(aaFieldType);
  ...
}

注意虛擬碼中的型別轉換,Expression表示式在編譯成委託時是非常嚴格的,所有轉換都不能省。寫表示式的時候我習慣先寫虛擬碼,我希望大家也能養成這個習慣!有了虛擬碼我們可以開始改造AutowiredService類了:

  /// <summary>
  /// 從容器裝配service
  /// </summary>
  [AppService]
  public class AutowiredService
  {
    IServiceProvider serviceProvider;
    public AutowiredService(IServiceProvider serviceProvider)
    {
      this.serviceProvider = serviceProvider;
    }

    Dictionary<Type,Action<object,IServiceProvider>> autowiredActions = new Dictionary<Type,IServiceProvider>>();

    public void Autowired(object service)
    {
      Autowired(service,serviceProvider);
    }
    /// <summary>
    /// 裝配屬性和欄位
    /// </summary>
    /// <param name="service"></param>
    /// <param name="serviceProvider"></param>
    public void Autowired(object service,IServiceProvider serviceProvider)
    {
      var serviceType = service.GetType();
      if (autowiredActions.TryGetValue(serviceType,out Action<object,IServiceProvider> act))
      {
        act(service,serviceProvider);
      }
      else
      {
        //引數
        var objParam = Expression.Parameter(typeof(object),"obj");
        var spParam = Expression.Parameter(typeof(IServiceProvider),"sp");

        var obj = Expression.Convert(objParam,serviceType);
        var GetService = typeof(IServiceProvider).GetMethod("GetService");
        List<Expression> setList = new List<Expression>();

        //欄位賦值
        foreach (FieldInfo field in serviceType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
        {
          var autowiredAttr = field.GetCustomAttribute<AutowiredAttribute>();
          if (autowiredAttr != null)
          {
            var fieldExp = Expression.Field(obj,field);
            var createService = Expression.Call(spParam,GetService,Expression.Constant(field.FieldType));
            var setExp = Expression.Assign(fieldExp,Expression.Convert(createService,field.FieldType));
            setList.Add(setExp);
          }
        }
        //屬性賦值
        foreach (PropertyInfo property in serviceType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
        {
          var autowiredAttr = property.GetCustomAttribute<AutowiredAttribute>();
          if (autowiredAttr != null)
          {
            var propExp = Expression.Property(obj,property);
            var createService = Expression.Call(spParam,Expression.Constant(property.PropertyType));
            var setExp = Expression.Assign(propExp,property.PropertyType));
            setList.Add(setExp);
          }
        }
        var bodyExp = Expression.Block(setList);
        var setAction = Expression.Lambda<Action<object,IServiceProvider>>(bodyExp,objParam,spParam).Compile();
        autowiredActions[serviceType] = setAction;
        setAction(service,serviceProvider);
      }
    }
  }

程式碼一下子多了不少,不過由於我們前面的鋪墊,理解起來也不難,至此自動裝配欄位和屬性的服務已經寫好了,下面看看如何使用:

編寫服務類,並新增[AppService]特性

[AppService]
public class MyService
{
  //functions
}

在Setup的ConfigureServices方法中註冊應用服務

public void ConfigureServices(IServiceCollection services)
{
  services.AddMvc();
  //註冊應用服務
  services.AddAppServices();
}

在其他類中注入使用,比如Controller中

public class HomeController : Controller
{
  [Autowired]
  MyUserService myUserService;

  public HomeController(AutowiredService autowiredService)
  {
    autowiredService.Autowired(this);
  }
}

HomeController的建構函式是不是簡潔了許多呢!而且再有新的服務要注入,只要定義欄位(屬性也可以,不過欄位更方便)就可以了,注意:我們定義的欄位不能是隻讀的,因為我們要在AutowiredService中設定。我們還用上面的例子,看一下它的威力吧!

public class HomeController : Controller
{
  [Autowired]
  UserService userService;
  [Autowired]
  OrderService orderService;
  [Autowired]
  MsgService msgService;
  [Autowired]
  OtherService otherService;
  [Autowired]
  OtherService2 otherService2;

  public HomeController(AutowiredService autowiredService)
  {
    autowiredService.Autowired(this);
  }
}

感謝您的觀看!全文已經完了,我們沒有使用第三方容器,也沒有對自帶的容器大肆修改和破壞,只是在服務類的構造器中選擇性的呼叫了AutowiredService.Autowired(this)方法,為什麼是選擇性的呢,因為你還可以使用在構造器中注入的方式,甚至混用,一切都好,都不會錯亂。

nuget安裝:

PM> Install-Package Autowired.Core

git原始碼:

[Autowired.Core] https://gitee.com/loogn/Autowired.Core

更新:

  • 支援多個AppServiceAttribute,
  • 支援服務唯一標識,通過Identifier指定服務實現

到此這篇關於在.net core中實現欄位和屬性注入的示例程式碼的文章就介紹到這了,更多相關.net core 欄位和屬性注入內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!