基於SqlSugar的開發框架循序漸進介紹(5)-- 在服務層使用介面注入方式實現IOC控制反轉

2022-05-24 12:01:46

在前面隨筆,我們介紹過這個基於SqlSugar的開發框架,我們區分Interface、Modal、Service三個目錄來放置不同的內容,其中Modal是SqlSugar的對映實體,Interface是定義存取介面,Service是提供具體的資料操作實現。在Service層中,往往除了本身的一些增刪改查等處理操作外,也需要涉及到相關業務的服務介面,這些服務介面我們通過利用.net 的介面注入方式,實現IOC控制反轉的處理的。

1、框架Service層的模組

如下面的VS中的專案服務層,包含很多業務表的服務介面實現,如下所示。

 

 我們以其中簡單的Customer業務表為例,它的服務類程式碼如下所示(主要關注服務類的定義即可)。

    /// <summary>
    /// 客戶資訊應用層服務介面實現
    /// </summary>
    public class CustomerService : MyCrudService<CustomerInfo, string, CustomerPagedDto>, ICustomerService
    {
       ...............
    }

它除了在泛型約束中增加SqlSugar實體類,主鍵型別,分頁條件物件外,還繼承介面 ICustomerService ,這個介面就是我們實現IOC的第一步,服務層繼承指定的介面實現,對我們實現IOC控制反轉提供便利。

    /// <summary>
    /// 客戶資訊服務介面
    /// </summary>
    public interface ICustomerService : IMyCrudService<CustomerInfo, string, CustomerPagedDto>, ITransientDependency
    {

    }

這個客戶資訊業務處理,是比較典型的單表處理案例,它沒有涉及到相關服務介面的整合,如果我們在其中服務介面中需要呼叫其他服務介面,那麼我們就需要通過建構函式注入介面物件的方式獲得物件的範例,如下我們說介紹的就是服務呼叫其他相關介面的實現。

 

2、服務層的介面注入

如對於角色服務介面來說,它往往和使用者、機構有關係,因此我們在角色的服務介面層,可以整合使用者、機構的對應服務介面,如下程式碼所示。

    /// <summary>
    /// 角色資訊 應用層服務介面實現
    /// </summary>
    public class RoleService : MyCrudService<RoleInfo,int, RolePagedDto>, IRoleService
    {
        private IOuService _ouService;
        private IUserService _userService;

        /// <summary>
        /// 預設建構函式
        /// </summary>
        /// <param name="ouService">機構服務介面</param>
        /// <param name="userService">使用者服務介面</param>
        public RoleService(IOuService ouService, IUserService userService)
        {
            this._ouService = ouService;
            this._userService = userService;
        }

}

通過建構函式的注入,我們就可以獲得對應介面實現的範例,進行呼叫它的服務層方法使用了。

這樣我們在角色的服務介面實現中,就可以呼叫其他如使用者、機構相關的服務介面了。

 其他模組的處理方式也是類似,如字典專案中,使用字典型別的服務介面。

    /// <summary>
    /// 應用層服務介面實現
    /// </summary>
    public class DictDataService : MyCrudService<DictDataInfo, string, DictDataPagedDto> , IDictDataService
    {
        /// <summary>
        /// 測試字典型別介面
        /// </summary>
        protected IDictTypeService _dictTypeService;

        /// <summary>
        /// 注入方式獲取介面
        /// </summary>
        /// <param name="dictTypeService">字典型別處理</param>
        public DictDataService(IDictTypeService dictTypeService)
        {
            this._dictTypeService = dictTypeService;
        }
}

這裡值得注意的是,由於介面層是同級物件,因此要避免介面的相互參照而導致出錯,依賴關係要清晰,才不會發生這個情況。

 

3、服務介面的範例的容器註冊

在服務層中,我們是通過引數化建構函式的方式,引入對應的介面的,這個操作方式是建構函式的注入處理。

不過在此之前,我們需要在.net 的內建IOC容器中註冊對應的介面範例,否則引數化建構函式會因為找不到介面範例而出錯。

.net 的內建Ioc容器及註冊處理,我們需要在nuget引入下面兩個參照。

1、Microsoft.Extensions.DependencyInjection
2、Microsoft.Extensions.DependencyInjection.Abstractions

.net 中 負責依賴注入和控制反轉的核心元件有兩個:IServiceCollection和IServiceProvider。其中,IServiceCollection負責註冊,IServiceProvider負責提供範例。

在註冊介面和類時,IServiceCollection提供了三種註冊方法,如下所示:

1、services.AddTransient<IDictDataService, DictDataService>();  // 瞬時生命週期
2、services.AddScoped<IDictDataService, DictDataService>();     // 域生命週期
3、services.AddSingleton<IDictDataService, DictDataService>();  // 全域性單例生命週期

如果使用AddTransient方法註冊,IServiceProvider每次都會通過GetService方法建立一個新的範例;

如果使用AddScoped方法註冊, 在同一個域(Scope)內,IServiceProvider每次都會通過GetService方法呼叫同一個範例,可以理解為在區域性實現了單例模式;

如果使用AddSingleton方法註冊, 在整個應用程式生命週期內,IServiceProvider只會建立一個範例。

我們為了在註冊的時候方便通過遍歷方式處理介面範例的註冊,因此我們根據這幾種關係定義了幾個基礎類別介面,便於根據特定的介面方式來構建介面範例。

namespace WHC.Framework.ControlUtil
{
    //用於定義這三種生命週期的標識介面

    /// <summary>
    /// 三種標識介面的基礎類別介面
    /// </summary>
    public interface IDependency
    {
    }
    /// <summary>
    /// 瞬時(每次都重新範例)
    /// </summary>
    public interface ITransientDependency : IDependency
    {
    }
    /// <summary>
    /// 單例(全域性唯一)
    /// </summary>
    public interface ISingletonDependency : IDependency
    {        
    }
    /// <summary>
    /// 一個請求內唯一(執行緒內唯一)
    /// </summary>
    public interface IScopedDependency : IDependency
    {
    }
}

這樣我們在定義註冊型別的時候,通過它的介面指定屬於上面那種型別。如對於字典專案的服務層,我們約定採用瞬時的註冊方式,那麼它的介面定義如下所示。

    /// <summary>
    /// 字典專案服務介面
    /// </summary>
    public interface IDictDataService : IMyCrudService<DictDataInfo, string, DictDataPagedDto>, ITransientDependency
    {
    }

設定自動註冊介面的時候,我們新增如下函數處理即可。

        /// <summary>
        /// 設定依賴注入物件
        /// </summary>
        /// <param name="services"></param>
        public static void ConfigureRepository(IServiceCollection services)
        {
            #region 自動注入對應的服務介面
            //services.AddSingleton<IDictDataService, DictDataService>();//services.AddScoped<IUserService, UserService>();

            var baseType = typeof(IDependency);
            var path = AppDomain.CurrentDomain.RelativeSearchPath ?? AppDomain.CurrentDomain.BaseDirectory;
            var getFiles = Directory.GetFiles(path, "*.dll").Where(Match);  //.Where(o=>o.Match())
            var referencedAssemblies = getFiles.Select(Assembly.LoadFrom).ToList();  //.Select(o=> Assembly.LoadFrom(o))         

            var ss = referencedAssemblies.SelectMany(o => o.GetTypes());

            var types = referencedAssemblies
                .SelectMany(a => a.DefinedTypes)
                .Select(type => type.AsType())
                .Where(x => x != baseType && baseType.IsAssignableFrom(x)).ToList();
            var implementTypes = types.Where(x => x.IsClass).ToList();
            var interfaceTypes = types.Where(x => x.IsInterface).ToList();
            foreach (var implementType in implementTypes)
            {
                if (typeof(IScopedDependency).IsAssignableFrom(implementType))
                {
                    var interfaceType = interfaceTypes.FirstOrDefault(x => x.IsAssignableFrom(implementType));
                    if (interfaceType != null)
                        services.AddScoped(interfaceType, implementType);
                }
                else if (typeof(ISingletonDependency).IsAssignableFrom(implementType))
                {
                    var interfaceType = interfaceTypes.FirstOrDefault(x => x.IsAssignableFrom(implementType));
                    if (interfaceType != null)
                        services.AddSingleton(interfaceType, implementType);
                }
                else
                {
                    var interfaceType = interfaceTypes.FirstOrDefault(x => x.IsAssignableFrom(implementType));
                    if (interfaceType != null)
                        services.AddTransient(interfaceType, implementType);
                }
            }
            #endregion
        }

上面根據我們自定義介面的不同,適當的採用不同的註冊方式來加入Ioc容器中,從而實現了介面的註冊,在服務層中就可以通過建構函式注入的方式獲得對應的介面範例了。

這樣,不管是在WInform的啟動模組中,還是在Web API的啟動模組中,我們在IOC容器中加入對應的介面即可,如下所示。

/// <summary>
/// 應用程式的主入口點。
/// </summary>
[STAThread]
static void Main()
{
    // IServiceCollection負責註冊
    IServiceCollection services = new ServiceCollection();
    //services.AddSingleton<IDictDataService, DictDataService>();
    //services.AddSingleton<IDictTypeService, DictTypeService>();

    //新增IApiUserSession實現類
    services.AddSingleton<IApiUserSession, ApiUserPrincipal>();

    //呼叫自定義的服務註冊
    ServiceInjection.ConfigureRepository(services);

    // IServiceProvider負責提供範例
    IServiceProvider provider = services.BuildServiceProvider();
    services.AddSingleton(provider);//註冊到服務集合中,需要可以在Service中建構函式中注入使用

Web API中的程式碼如下所示

//新增HTTP上下文存取
builder.Services.AddHttpContextAccessor();

//設定依賴注入存取資料庫
ServiceInjection.ConfigureRepository(builder.Services);

//新增IApiUserSession實現類
builder.Services.AddSingleton<IApiUserSession, ApiUserPrincipal>();

var app = builder.Build();

都是類似的處理方式。

同樣在Web API專案中的控制器處理中,也是一樣通過建構函式注入的方式使用介面的,如下所示。

namespace WebApi.Controllers
{
    /// <summary>
    /// 客戶資訊的控制器物件
    /// </summary>
    public class CustomerController : BusinessController<CustomerInfo, string, CustomerPagedDto>
    {
        private ICustomerService _customerService;

        /// <summary>
        /// 建構函式,並注入基礎介面物件
        /// </summary>
        /// <param name="customerService"></param>
        public CustomerController(ICustomerService customerService) :base(customerService)
        {
            this._customerService = customerService;
        }
    }
}

或者登入處理的控制器定義如下。

    /// <summary>
    /// 登入獲取令牌授權的處理
    /// </summary>
    [Route("api/[controller]")]
    [ApiController]
    public class LoginController : ControllerBase
    {
        private readonly IHttpContextAccessor _contextAccessor;
        private readonly IConfiguration _configuration;
        private readonly IUserService _userService;

        /// <summary>
        /// 令牌失效天數,預設令牌7天有效期
        /// </summary>
        protected const int expiredDays = 7;

        /// <summary>
        /// 建構函式,注入所需介面
        /// </summary>
        /// <param name="configuration">設定物件</param>
        /// <param name="httpContext">HTTP上下文物件</param>
        /// <param name="userService">使用者資訊</param>
        public LoginController(IConfiguration configuration, IHttpContextAccessor httpContext, 
            IUserService userService)
        {
            this._configuration = configuration;
            this._contextAccessor = httpContext;
            this._userService = userService;
        }

 

系列文章:

基於SqlSugar的開發框架的循序漸進介紹(1)--框架基礎類的設計和使用

基於SqlSugar的開發框架循序漸進介紹(2)-- 基於中間表的查詢處理

基於SqlSugar的開發框架循序漸進介紹(3)-- 實現程式碼生成工具Database2Sharp的整合開發

基於SqlSugar的開發框架循序漸進介紹(4)-- 在資料存取基礎類別中對GUID主鍵進行自動賦值處理 

基於SqlSugar的開發框架循序漸進介紹(5)-- 在服務層使用介面注入方式實現IOC控制反轉