Reface.AppStarter 類型掃描 —— 獲得系統中所有的實體類型

類型掃描Reface.AppStarter 提供的最基本、最核心的功能。

AutoConfig , ComponentScan 等功能都是基於該功能完成的。

每一個使用 Reface.AppStarter 的人都可以訂製自己的掃描類型掃描邏輯。

例如

收集系統中所有的 實體 類型,並在系統啟動後執行 Code-First 的相關操作。

我們現在就以該示例為需求,開發一個能夠 掃描實體,並藉助第三方框架實現 CodeFirst 的示常式序。

1. 創建程式

創建一個名為 Reface.AppStarter.Demos.ScanEntities 的控制台項目用於承載我們的示常式序。

2. 添加 Reface.AppStarter 的 nuget 依賴

點擊訪問 Reface.AppStarter @ nuget 可以複製最新版本的 Reface.AppStarter 的命令行到 Package Manager 中。

3. 創建專屬的 Attribute

Reface.AppStarter 對類型的掃描是通過 Attribute 識別的。

Reface.AppStarter.Attributes.ScannableAttribute 表示該特徵允許被 AppSetup 掃描並記錄。

因此只要將我們的 Attribute 繼承於 ScannableAttribute 就可以被 AppSetup 掃描並記錄。

我們先創建一個目錄 Attributes,並在該目錄中創建特徵類型 EntityAttribute 讓其繼承於 ScannableAttribute

using Reface.AppStarter.Attributes;

namespace Reface.AppStarter.Demos.ScanEntities.Attributes
{
    public class EntityAttribute : ScannableAttribute
    {
    }
}

所有標記了 EntityAttribute 的類型都會被 AppSetup 發現、記錄。

我們使用這些記錄就可以收集到系統內的所有實體,並進一步根據這些實體類型進行 Code-First 操作。

4. 創建專屬 AppModule

Reface.AppStarter 中對模組的依賴和增加都是通過 AppModule 完成的。

我們希望使用者添加對我們的 AppModule 依賴就可以掃描指定模組中的所有實體類型。

在應用時,我們希望形如下面的程式碼:

[ComponentScanAppModule] // IOC 組件掃描與註冊模組
[AutoConfigAppModule] // 自動配置模組
[EntityScanAppModule] // 實體操作模組
public class UserAppModule : AppModule 
{

}

很明顯,我們需要創建一個名為 EntityScanAppModule 的類型。

我們創建一個目錄名為 AppModules,並將 EntityScanAppModule 創建在該目錄下。

此時,我們的目錄如下

- Reface.AppStarter.Demos.ScanEntities
    - AppModules
        EntityScanAppModule.cs
    - Attributes
        EntityAttribute.cs
    Program.cs

此時的 EntityScanAppModule 是一個空白的 AppModule
還不具有找到所有標記了 EntityAttribute 的類型的能力。

我們可以通過重寫 OnUsing 方法賦予此功能。

OnUsing 方法具備一個類型為 AppModuleUsingArguments 的參數。

  • AppSetup , 此時啟動過程的 AppSetup 實例
  • TargetAppModule , 以之前的 UserAppModule 為例,TargetAppModule 就是 UserAppModule 的實例
  • UsingAppModule , 以之前的 UserAppModule 為例,UsingAppModule 就是 EntityScanAppModule 的實例,也就是 this 指向的實例
  • ScannedAttributeAndTypeInfos , 從 TargetAppModule 中掃描所到的全部類型資訊

看到最後一個屬性,應該一切就簡單了。

ScannedAttributeAndTypeInfos 找到所有 AttributeEntityAttribute 的類型,就能解決了 :

using Reface.AppStarter.AppModules;
using Reface.AppStarter.Demos.ScanEntities.Attributes;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Reface.AppStarter.Demos.ScanEntities.AppModules
{
    public class EntityScanAppModule : AppModule
    {
        public override void OnUsing(AppModuleUsingArguments args)
        {
            IEnumerable<Type> entityTypes = args
                .ScannedAttributeAndTypeInfos
                .Where(x => x.Attribute is EntityAttribute)
                .Select(x => x.Type);
        }
    }
}

我們現在需要考慮的是,如何將 entityTypes 保存下來,以便在系統啟動後使用它們建表。

除了使用 靜態類 等常見手段保存這些資訊,Reface.AppStarter 也提供了兩種方式。

4.1 App 上下文

AppAppSetup.Start() 得到的實例。

App.Context 屬性是一個 Dictionary<String, Object> 對象,允許開發者自定義一些資訊掛載在上下文中,以便在其它位置使用它們。

AppSetup 在構建期間也可以預置一些資訊到 App.Context 中。

public override void OnUsing(AppModuleUsingArguments args)
{
    IEnumerable<Type> entityTypes = args
        .ScannedAttributeAndTypeInfos
        .Where(x => x.Attribute is EntityAttribute)
        .Select(x => x.Type);
    args.AppSetup
        .AppContext
        .GetOrCreate<List<Type>>("EntityTypes", key =>
            {
                return new List<Type>();
            })
        .AddRange(entityTypes);
}

通過這種方式,當系統啟動後,我們可以通過 App.Context 得到所有掃描到的實體類型:

var app = AppSetup.Start<XXXAppModule>();
List<Type> entityTypes = app.Context
    .GetOrCreate<List<Type>>("EntityTypes", key => new List<Type>());
// code-first here

4.2 AppContainerBuilder / AppContainer

AppContainerApp 對象的構成要素,

App 的本質只有兩樣

  • 字典類型的上下文
  • 所有 AppContainer

每一種 AppContainer 都會管理某一類的類型,
而這些類型都是通過 ScannableAttribute 掃描得到的。

比如,在 Reface.AppStarter 中,有負責 IOC 和負責 AutoConfig 的兩個 AppContainer
它們分別對標有 ComponentConfig 的類型進行不同的管理和處理。

根據這個分門別類管理的思想,所有的實體類型也應當由專門的 AppContainer 管理所有的 實體類型

5. 創建 EntityAppContainer

創建目錄 AppContainers
並在目錄內創建 EntityAppContainer
EntityAppContainer 需要繼承 BaseAppContainer ,
添加構造函數,傳入所有的 實體類型 ,
重寫 OnAppStarted 方法 , 實現 Code-First 功能。

using Reface.AppStarter.AppContainers;
using System;
using System.Collections.Generic;

namespace Reface.AppStarter.Demos.ScanEntities.AppContainers
{
    public class EntityAppContainer : BaseAppContainer
    {
        // all entity type here
        private readonly IEnumerable<Type> entityType;

        public EntityAppContainer(IEnumerable<Type> entityType)
        {
            this.entityType = entityType;
        }

        public override void OnAppStarted(App app)
        {
            entityType.ForEach(x => Console.WriteLine("Do CODE-FIRST from type {0}", x));
        }
    }
}

很明顯,沒有 entityType ,我們無法直接構造出 EntityAppContainer

Reface.AppStarter 要求所有的 AppContainer 都是通過 AppContainerBuilder 創建得到的。

AppContainer 不同,AppContainerBuilder 是被託管在 AppSetup 實例中的,
開發者可以通過 AppSetup 的實例 訪問 指定類型的 AppContainerBuilder

AppContainerBuilder 一旦被 訪問 ,就會立刻創建,並在最終生成 App 實例時,構建成相應的 AppContainer 並移交給 App

6. 創建 EntityAppContainerBuilder

創建目錄 AppContainerBuilders,
在目錄內創建類型 EntityAppContainerBuilder 繼承 BaseAppContainerBuilder,
並重寫 Build 方法。

using Reface.AppStarter.AppContainerBuilders;
using Reface.AppStarter.AppContainers;
using System;

namespace Reface.AppStarter.Demos.ScanEntities.AppContainerBuilders
{
    public class EntityAppContainerBuilder : BaseAppContainerBuilder
    {
        public override IAppContainer Build(AppSetup setup)
        {
            throw new NotImplementedException();
        }
    }
}

很明顯,我們知道需要在 Build 中寫下這樣的程式碼

return new EntityAppContainer(entityTypes);

entityTypes 從何而來?

這個就簡單了,為 EntityAppContainerBuilder 添加一個 AddEntityType(Type type) 就行了。

using Reface.AppStarter.AppContainerBuilders;
using Reface.AppStarter.AppContainers;
using Reface.AppStarter.Demos.ScanEntities.AppContainers;
using System;
using System.Collections.Generic;

namespace Reface.AppStarter.Demos.ScanEntities.AppContainerBuilders
{
    public class EntityAppContainerBuilder : BaseAppContainerBuilder
    {
        private readonly ICollection<Type> entityTypes;

        public EntityAppContainerBuilder()
        {
            entityTypes = new List<Type>();
        }

        public void AddEntityType(Type type)
        {
            this.entityTypes.Add(type);
        }

        public override IAppContainer Build(AppSetup setup)
        {
            return new EntityAppContainer(entityTypes);
        }
    }
}

從這個類型不難發現,我們需要創建一個 EntityAppContainerBuilder ,並使用 AddEntityType 將實體類型加入。

最後的 Build 方法會由 AppSetup 內部執行。

7. 使用 EntityScanAppModule 操作 EntityAppContainerBuilder

現在回到之前的 EntityScanAppModule ,
從前向 App.Context 內預置資訊的程式碼可以刪除掉了。

我們先從 AppSetup 中獲取 EntityAppContainerBuilder 的實例,
配合上 AddEntityType,然後就一氣呵成了。

using Reface.AppStarter.AppModules;
using Reface.AppStarter.Demos.ScanEntities.AppContainerBuilders;
using Reface.AppStarter.Demos.ScanEntities.Attributes;
using System.Linq;

namespace Reface.AppStarter.Demos.ScanEntities.AppModules
{
    public class EntityScanAppModule : AppModule
    {
        public override void OnUsing(AppModuleUsingArguments args)
        {
            EntityAppContainerBuilder builder = args.AppSetup.GetAppContainerBuilder<EntityAppContainerBuilder>();

            args
                .ScannedAttributeAndTypeInfos
                .Where(x => x.Attribute is EntityAttribute)
                .Select(x => x.Type)
                .ForEach(x => builder.AddEntityType(x));

        }
    }
}

8. 準備我們的啟動程式

創建 DemoAppModule
添加一些 Entity,
添加 EntityScanAppModule 的依賴,
啟動,即可測試我們的程式碼了。

using Reface.AppStarter.AppModules;
using Reface.AppStarter.Demos.ScanEntities.AppModules;

namespace Reface.AppStarter.Demos.ScanEntities
{
    [EntityScanAppModule]
    public class DemoAppModule : AppModule
    {
    }
}

在 Program.cs 中編寫啟動程式

using System;

namespace Reface.AppStarter.Demos.ScanEntities
{
    class Program
    {
        static void Main(string[] args)
        {
            AppSetup.Start<DemoAppModule>();
            Console.ReadLine();
        }
    }
}

控制台中就可以得到所有的實體類型

Do CODE-FIRST from type Reface.AppStarter.Demos.ScanEntities.Entities.Role
Do CODE-FIRST from type Reface.AppStarter.Demos.ScanEntities.Entities.User

文中項目程式碼可以從這裡下載 : Reface.AppStarter.Demos.ScanEntities @ Github