主頁 > .NET開發 > 在 ASP.NET Core 專案中使用 MediatR 實作中介者模式

在 ASP.NET Core 專案中使用 MediatR 實作中介者模式

2020-09-20 20:46:22 .NET開發

 一、前言

   最近有在看 DDD 的相關資料以及微軟的 eShopOnContainers 這個專案中基于 DDD 的架構設計,在 Ordering 這個示例服務中,可以看到各層之間的代碼呼叫與我們之前傳統的呼叫方式似乎差異很大,整個專案各個層之間的代碼全部是通過注入 IMediator 進行呼叫的,F12 查看原始碼后可以看到該介面是屬于 MediatR 這個組件的,既然要照葫蘆畫瓢,那我們就先來了解下如何在 ASP.NET Core 專案中使用 MediatR,

  代碼倉儲:https://github.com/Lanesra712/ingos-common/tree/master/sample/aspnetcore/aspnetcore-mediatr-tutorial

 二、Step by Step

  MediatR  從 github 的專案主頁上可以看到作者對于這個專案的描述是基于中介者模式的 .NET 實作,是一種基于行程內的資料傳遞,也就是說這個組件主要實作的是在一個應用中實作資料傳遞,如果想要實作多個應用間的資料傳遞就不太適合了,從作者的 github 個人主頁上可以看到,他還是 AutoMapper 這個 OOM 組件的作者,PS,如果你想要了解如何在 ASP.NET Core 專案中使用 AutoMapper,你可以查看我之前寫的這一篇文章(電梯直達),而對于 MediatR 來說,在具體的學習使用之前,我們先來了解下什么是中介者模式,

  1、什么是中介者模式

  很多舶來詞的中文翻譯其實最終都會與實際的含義相匹配,例如軟體開發程序中的 23 種設計模式的中文名稱,我們其實可以比較容易的從中文名稱中得知出該設計模式具體想要實作的作用,就像這里介紹的中介者模式,

  在我們通過代碼實作實際的業務邏輯時,如果涉及到多個物件類之間的互動,通常我們都是會采用直接參考的形式,隨著業務邏輯變的越來越復雜,對于一個簡單的業務抽象出的實作方法中,可能會被我們添加上各種判斷的邏輯或是對于資料的業務邏輯處理方法,

  例如一個簡單的用戶登錄事件,我們可能最侄訓抽象出如下的業務流程實作,

public bool Login(AppUserLoginDto dto, out string msg)
{
    bool flag = false;
    try
    {
        // 1、驗證碼是否正確
        flag = _redisLogic.GetValueByKey(dto.VerificationCode);
        if (!flag)
        {
            msg = "驗證碼錯誤,請重試";
            return false;
        }

        // 2、驗證賬戶密碼是否正確
        flag = _userLogic.GetAppUser(dto.Account.Trim(), dto.Password.Trim(), out AppUserDetailDto appUser);
        if (!flag)
        {
            msg = "賬戶或密碼錯誤,請重試";
            return false;
        }

        // 3、驗證賬戶是否可以登錄當前的站點(未被鎖定 or 具有登錄當前系統的權限...)
        flag = _authLogic.CheckIsAvailable(appUser);
        if (!flag)
        {
            msg = "用戶被禁止登錄當前系統,請重試";
            return false;
        }

        // 4、設定當前登錄用戶資訊
        _authLogic.SetCurrentUser(appUser);

        // 5、記錄登錄記錄
        _userLogic.SaveLoginRecord(appUser);

        msg = "";
        return true;
    }
    catch (Exception ex)
    {
        // 記錄錯誤資訊
        msg = $"用戶登錄失敗:{ex.Message}";
        return false;
    }
}

  這里我們假設對于登錄事件的實作方法存在于 UserAppService 這個類中,對于 redis 資源的操作在 RedisLogic 類中,對于用戶相關資源的操作在 UserLogic 中,而對于權限校驗相關的資源操作位于 AuthLogic 類中,

  可以看到,為了實作 UserAppService 類中定義的登錄方法,我們至少需要依賴于 RedisLogic、UserLogic 以及 AuthLogic,甚至在某些情況下可能在 UserLogic 和 AuthLogic 之間也存在著某種依賴關系,因此我們可以從中得到如下圖所示的類之間的依賴關系,

  一個簡單的登錄業務尚且如此,如果我們需要對登錄業務添加新的需求,例如現在很多網站的登錄和注冊其實是放在一起的,當登錄時如果判斷沒有當前的用戶資訊,其實會催生創建新用戶的流程,那么,對于原本的登錄功能實作,是不是會存在繼續添加新的依賴關系的情況,同時對于很多本身就很復雜的業務,最終實作出來的方法是不是會有更多的物件類之間存在各種的依賴關系,牽一發而動全身,后期修改測驗的成本會不會變得更高,

  那么,中介者模式是如何解決這個問題呢?

  在上文有提到,對于舶來詞的中文名稱,中文更多的會根據實際的含義進行命名,試想一下我們在現實生活中提到中介,是不是更多的會想到房屋中介這一角色,當我們來到一個新的城市,面臨著租房的問題,絕大多數的情況下,我們最終需要通過中介去達成我們租房的目的,在租房這個案例中,房屋中介其實就是一個中介者,他承接我們對于想要租的房子的各種需求,從自己的房屋資料庫中去尋找符合條件的,最終以一個橋梁的形式,連接我們與房東,最終就房屋的租住達成一致,

  而在軟體開發中,中介者模式則是要求我們根據實際的業務去定義一個包含各種物件之間互動關系的物件類,之后,所有涉及到該業務的物件都只關聯于這一個中介物件類,不再顯式的呼叫其它類,采用了中介者模式之后設計的登錄功能所涉及到的類依賴如下圖所示,這里的 AppUserLoginEventHandler 其實就是我們的中介類,

  當然,任何事都會有利有弊,不會存在百分百完美的事情,就像我們通過房租中介去尋找合適的房屋,最終我們需要付給中介一筆費用去作為酬勞,采用中介者模式設計的代碼架構也會存在別的問題,因為在代碼中引入了中介者這一物件,勢必會增加我們代碼的復雜度,可能會使原本很輕松就實作的代碼變得復雜,同時,我們引入中介者模式的初衷是為了解決各個物件類之間復雜的參考關系,對于某些業務來說,本身就很復雜,最終必定會導致這個中介者物件例外復雜,

  畢竟,軟體開發的程序中不會存在銀彈去幫我們解決所有的問題,

  那么,在本篇文章的示例代碼中,我將使用 MediatR 這一組件,通過引入中介者模式的思想來完成上面的用戶登錄這一案例,

  2、組件加載

  在使用 MediatR 之前,這里簡單介紹下這篇文章的示例 demo 專案,這個示例專案的架構分層可以看成是介于傳統的多層架構與采用 DDD 的思想的架構分層,嗯,你可以理解成四不像,屬于那種傳統模式下的開發人員在往 DDD 思想上進行遷移的成品,具體的代碼分層說明解釋如下,

  01_Infrastructure:基礎架構層,這層會包含一些對于基礎組件的配置或是幫助類的代碼,對于每個新建的服務來說,該層的代碼幾乎都是差不多的,所以對于基礎架構層的代碼其實最好是發布到公有 or 私有的 Nuget 倉庫中,然后我們直接在專案中通過 Nuget 去參考,

  對于采用 DDD 的思想構建的專案來說,很多人可能習慣將一些物體的配置也放置在基礎架構層,我的個人理解還是應該置于領域層,對于基礎架構層,只做一些基礎組件的封裝,如果有什么不對的地方,歡迎在評論區提出,

  02_Domain:領域層,這層會包含我們根據業務劃分出的領域的幾乎所有重要的部分,有領域物件(Domain Object)、值物件(Value Object)、領域事件(Domain Event)、以及倉儲(Repository)等等領域組件,

  這里雖然我創建了 AggregateModels(聚合物體)這個檔案夾,其實在這個專案中,我創建的還是不包含任何業務邏輯的貧血模型,同時,對于倉儲(Repository)在領域分層中是置于 Infrastructure(基礎架構層)還是位于 Domain(領域層),每個人都會有自己的理解,這里我還是更傾向于放在 Domain 層中更符合其定位,

  03_Application:應用層,這一層會包含我們基于領域所封裝出的各種實際的業務邏輯,每個封裝出的服務應用之間并不會出現互相呼叫的情況,

  Sample.Api:API 介面層,這層就很簡單了,主要是通過 API 介面暴露出我們基于領域對外提供的各種服務,

  整個示例專案的分層結構如下圖所示,

  與使用其它的第三方組件的使用方式相同,在使用之前,我們需要在專案中通過 Nuget 添加對于 MediatR 的程式集參考,

  這里需要注意,因為我們主要是通過參考 MediatR 來實作中介者模式,所以我們只需要在領域層和應用層加載 MediatR 即可,而對于 Sample.Api 這個 Web API 專案,因為需要通過依賴注入的方式來使用我們基于 MediatR 所構建出的各種服務,所以這里我們還要添加 MediatR.Extensions.Microsoft.DependencyInjection 這個程式集到 Sample.Api 中,

Install-Package MediatR
Install-Package MediatR.Extensions.Microsoft.DependencyInjection

  3、案例實作

  首先我們在 Sample.Domain 這個類別庫的 AggregateModels 檔案夾下添加 AppUser(用戶資訊)類 和 Address(地址資訊) 類,這里雖然并沒有采用 DDD 的思想去劃分領域物件和值物件,我們創建出來的都是不含任何業務邏輯的貧血模型,但是在用戶管理這個業務中,對于用戶所包含的聯系地址資訊,其實是一種無狀態的資料,也就是說對于同一個地址資訊,不會因為置于多個用戶中而出現資料的二義性,因此,對于地址資訊來說,是不需要唯一的標識就可以區分出這個資料的,所以這里的 Address 類就不需要添加主鍵,其實也就是對應于領域建模中的值物件,

  這里我是使用的 EF Core 作為專案的 ORM 組件,當創建好需要使用物體之后,我們在 Sample.Domain 這個類別庫下面新建一個 SeedWorks 檔案夾,添加自定義的 DbContext 物件和用于執行 EF Core 第一次生成資料庫時寫入預置種子資料的資訊類,

  這里需要注意,在 EF Core 中,當我們需要將撰寫的 C# 類通過 Code First 創建出資料庫表時,我們的 C# 類必須包含主鍵資訊,而對應到我們這里的 Address 類來說,它更多的是作為 AppUser 類中的屬性資訊來展示的,所以這里我們需要對 EF Core 生成資料庫表的程序進行重寫,

  這里我們在 SeedWorks 檔案夾下創建一個新的檔案夾 EntityConfigurations,在這里用來存放我們自定義的 EF Core 創建表的規則,新建一個繼承于 IEntityTypeConfiguration<AppUser> 介面的 AppUserConfiguration 配置類,在介面默認 Configure 方法中,我們需要撰寫映射規則,將 Address 類作為 AppUser 類中的欄位進行顯示,最終實作后的代碼如下所示, 

public class AppUserConfiguration : IEntityTypeConfiguration<AppUser>
{
    public void Configure(EntityTypeBuilder<AppUser> builder)
    {
        // 表名稱
        builder.ToTable("appuser");

        // 物體屬性配置
        builder.OwnsOne(i => i.Address, n =>
        {
            n.Property(p => p.Province).HasMaxLength(50)
                .HasColumnName("Province")
                .HasDefaultValue("");

            n.Property(p => p.City).HasMaxLength(50)
                .HasColumnName("City")
                .HasDefaultValue("");

            n.Property(p => p.Street).HasMaxLength(50)
                .HasColumnName("Street")
                .HasDefaultValue("");

            n.Property(p => p.ZipCode).HasMaxLength(50)
                .HasColumnName("ZipCode")
                .HasDefaultValue("");
        });
    }
}

  當創建表的映射規則撰寫完成后,我們就可以對 UserApplicationDbContext 類進行重寫 OnModelCreating 方法,在這個方法中,我們就可以去應用我們自定義設定的物體映射規則,從而讓 EF Core 按照我們的想法去創建資料庫,最終實作的代碼如下所示,

public class UserApplicationDbContext : DbContext
{
    public DbSet<AppUser> AppUsers { get; set; }

    public UserApplicationDbContext(DbContextOptions<UserApplicationDbContext> options)
        : base(options)
    {
    }

    /// <summary>
    ///
    /// </summary>
    /// <param name="modelBuilder"></param>
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // 自定義 AppUser 表創建規則
        modelBuilder.ApplyConfiguration(new AppUserConfiguration());
    }
}

  當我們創建好 DbContext 后,我們需要在 Startup 類的 ConfigureServices 方法中進行注入,在示例代碼中,我使用的是 MySQL 8.0 資料庫,將組態檔寫入到 appsettings.json 檔案中,最終注入 DbContext 的代碼如下所示,

public void ConfigureServices(IServiceCollection services)
{
    // 配置資料庫連接字串
    services.AddDbContext<UserApplicationDbContext>(options =>
        options.UseMySql(Configuration.GetConnectionString("SampleConnection")));
}

  資料庫的連接字串配置如下,

{
  "ConnectionStrings": {
    "SampleConnection": "server=127.0.0.1;database=sample.application;user=root;password=123456@sql;port=3306;persistsecurityinfo=True;"
  }
}

  在上文有提到,除了創建一個 DbContext 物件,我們還創建了一個 DbInitializer 類用于在 EF Core 第一次執行創建資料庫操作時將我們預置的資訊寫入到對應的資料庫表中,這里我們只是簡單的判斷下 AppUser 這張表是否存在資料,如果沒有資料,我們就添加一條新的記錄,最終實作的代碼如下所示,

public class DbInitializer
{
    public static void Initialize(UserApplicationDbContext context)
    {
        context.Database.EnsureCreated();

        if (context.AppUsers.Any())
            return;

        AppUser admin = new AppUser()
        {
            Id = Guid.NewGuid(),
            Name = "墨墨墨墨小宇",
            Account = "danvic.wang",
            Phone = "13912345678",
            Age = 12,
            Password = "123456",
            Gender = true,
            IsEnabled = true,
            Address = new Address("啦啦啦啦街道", "啦啦啦市", "啦啦啦省", "12345"),
            Email = "[email protected]",
        };

        context.AppUsers.Add(admin);
        context.SaveChanges();
    }
}

  當我們完成種子資料植入的代碼,我們需要在程式啟動之前就去執行我們的代碼,因此我們需要修改 Program 類中的 Main 方法,實作在運行 web 程式之前去執行種子資料的植入,

public class Program
{
    public static void Main(string[] args)
    {
        var host = CreateWebHostBuilder(args).Build();

        using (var scope = host.Services.CreateScope())
        {
            // 執行種子資料植入
            //
            var services = scope.ServiceProvider;
            var context = services.GetRequiredService<UserApplicationDbContext>();
            DbInitializer.Initialize(context);
        }
    }

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>();
}

  這時,運行我們的專案,程式就會自動執行創建資料庫的操作,同時會將我們預設好的種子資料寫入到資料庫表中,最終實作的效果如下圖所示,

  基礎的專案代碼已經完成之后,我們就可以開始學習如何通過 MediatR 來實作中介者模式,在這一章的示例專案中,我們會使用到 MediatR 中兩個很重要的介面型別:IRequest 和 INotification,

  在 Github 上,作者針對這兩個介面做了如下的解釋,這里我會按照我的理解去進行使用,同時,為了防止我的理解出現了偏差,從而對各位造成影響,這里貼上作者回復解釋的原文,

Requests are for:
1 request to 1 handler. Handler may or may not return a value
Notifications are for:
1 notification to n handlers. Handler may not return a value.


In practical terms, requests are "commands", notifications are "events".
Command would be directing MediatR to do something like "ApproveInvoiceCommand -> ApproveInvoiceHandler". Event would be
notifications, like "InvoiceApprovedEvent -> SendThankYouEmailToCustomerHandler"

 

  對于繼承于 IRequest 介面的類來說,一個請求(request)只會有一個針對這個請求的處理程式(requestHandler),它可以回傳值或者不回傳任何資訊;

  而對于繼承于 INotification 介面的類來說,一個通知(notification)會對應多個針對這個通知的處理程式(notificationHandlers),而它們不會回傳任何的資料,

  請求(request)更像是一種命令(command),而通知(notification)更像是一種事件(event),嗯,可能看起來更暈了,jbogard 這里給了一個案例給我們進一步的解釋了 request 與 notification 之間的差異性,

  雙十一剛過,很多人都會瘋狂剁手,對于購買大件來說,為了能夠更好地擁有售后服務,我們在購買后肯定會期望商家給我們提供發票,這里的要求商家提供發票就是一種 request,而針對我們的這個請求,商家會做出回應,不管能否開出來發票,商家都應當通知到我們,這里的通知用戶就是一種 notification,

  對于提供發票這個 request 來說,不管最終的結果如何,它只會存在一種處理方式;而對于通知用戶這個 notification 來說,商家可以通過短信通知,可以通過公眾號推送,也可以通過郵件通知,不管采用什么方式,只要完成了通知,對于這個事件來說也就已經完成了,    

  而對應于用戶登錄這個業務來說,用戶的登錄行為其實就是一個 request,對于這個 request 來說,我們可能會去資料庫查詢賬戶是否存在,判斷是不是具有登錄系統的權限等等,而不管我們在這個程序中做了多少的邏輯判斷,它只會有兩種結果,登錄成功或登錄失敗,而對于用戶登錄系統之后可能需要設定當前登錄人員資訊,記錄用戶登錄日志這些行為來說,則是歸屬于 notification 的,

  弄清楚了用戶登錄事件中的 request 和 notification 劃分,那么接下來我們就可以通過代碼來實作我們的功能,這里對于示例專案中的一些基礎組件的配置我就跳過了,如果你想要具體的了解這里使用到的一些組件的使用方法,你可以查閱我之前的文章,

  首先,我們在 Sample.Application 這個類別庫下面創建一個 Commands 檔案夾,在下面存放用戶的請求資訊,現在我們創建一個用于映射用戶登錄請求的 UserLoginCommand 類,它需要繼承于 IRequest<T> 這個泛型介面,因為對于用戶登錄這個請求來說,只會有可以或不可以這兩個結果,所以對于這個請求的回應的結果是 bool 型別的,也就是說,我們具體應該繼承的是 IRequest<bool>,

  對于用戶發起的各種請求來說,它其實只是包含了對于這次請求的一些基本資訊,而對于 UserLoginCommand 這個用戶登錄請求類來說,它可能只會有賬號、密碼、驗證碼這三個資訊,請求類代碼如下所示,

public class UserLoginCommand : IRequest<bool>
{
    /// <summary>
    /// 賬戶
    /// </summary>
    public string Account { get; private set; }

    /// <summary>
    /// 密碼
    /// </summary>
    public string Password { get; private set; }

    /// <summary>
    /// 驗證碼
    /// </summary>
    public string VerificationCode { get; private set; }

    /// <summary>
    /// ctor
    /// </summary>
    /// <param name="account">賬戶</param>
    /// <param name="password">密碼</param>
    /// <param name="verificationCode">驗證碼</param>
    public UserLoginCommand(string account, string password, string verificationCode)
    {
        Account = account;
        Password = password;
        VerificationCode = verificationCode;
    }
}

  當我們擁有了存盤用戶登錄請求資訊的類之后,我們就需要對用戶的登錄請求進行處理,這里,我們在 Sample.Application 這個類別庫下面新建一個 CommandHandlers 檔案夾用來存放用戶請求的處理類,

  現在我們創建一個繼承于 IRequestHandler 介面的 UserLoginCommandHandler 類用來實作對于用戶登錄請求的處理,IRequestHandler 是一個泛型的介面,它需要我們在繼承時宣告我們需要實作的請求,以及該請求的回傳資訊,因此,對于 UserLoginCommand 這個請求來說,UserLoginCommandHandler 這個請求的處理類,最終需要繼承于 IRequestHandler<UserLoginCommand, bool>,

  就像上面提到的一樣,我們需要在這個請求的處理類中對用戶請求的資訊進行處理,在 UserLoginCommandHandler 類中,我們應該在 Handle 方法中去執行我們的判斷邏輯,這里我們會參考到倉儲來獲取用戶的相關資訊,倉儲中的代碼這里我就不展示了,最終我們實作后的代碼如下所示,

public class UserLoginCommandHandler : IRequestHandler<UserLoginCommand, bool>
{
    #region Initizalize

    /// <summary>
    /// 倉儲實體
    /// </summary>
    private readonly IUserRepository _userRepository;

    /// <summary>
    /// ctor
    /// </summary>
    /// <param name="userRepository"></param>
    public UserLoginCommandHandler(IUserRepository userRepository)
    {
        _userRepository = userRepository ?? throw new ArgumentNullException(nameof(userRepository));
    }

    #endregion Initizalize

    /// <summary>
    /// Command Handler
    /// </summary>
    /// <param name="request"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public async Task<bool> Handle(UserLoginCommand request, CancellationToken cancellationToken)
    {
        // 1、判斷驗證碼是否正確
        if (string.IsNullOrEmpty(request.VerificationCode))
            return false;

        // 2、驗證登錄密碼是否正確
        var appUser = await _userRepository.GetAppUserInfo(request.Account.Trim(), request.Password.Trim());
        if (appUser == null)
            return false;

        return true;
    }
}

  當我們完成了對于請求的處理代碼后,就可以在 controller 中提供用戶訪問的入口,當然,因為我們需要采用依賴注入的方式去使用 MediatR,所以在使用之前,我們需要將請求的對應處理關系注入到依賴注入容器中,

  在通過依賴注入的方式使用 MediatR 時,我們需要將所有的事件(請求以及通知)注入到容器中,而 MediatR 則會自動尋找對應事件的處理類,除此之外,我們也需要將通過依賴注入使用到的 IMediator 介面的實作類注入到容器中,而在這個示例專案中,我們主要是在 Sample.Domain、Sample.Application 以及我們的 Web Api 專案中使用到了 MediatR,因此,我們需要將這三個專案中使用到 MediatR 的類全部注入到容器中,

  一個個的注入會比較的麻煩,所以這里我還是采用對指定的程式集進行反射操作,去獲取需要加載的資訊批量的進行注入操作,最終實作后的代碼如下,

public static IServiceCollection AddCustomMediatR(this IServiceCollection services, MediatorDescriptionOptions options)
{
    // 獲取 Startup 類的 type 型別
    var mediators = new List<Type> { options.StartupClassType };

    // IRequest<T> 介面的 type 型別
    var parentRequestType = typeof(IRequest<>);

    // INotification 介面的 type 型別
    var parentNotificationType = typeof(INotification);

    foreach (var item in options.Assembly)
    {
        var instances = Assembly.Load(item).GetTypes();

        foreach (var instance in instances)
        {
            // 判斷是否繼承了介面
            //
            var baseInterfaces = instance.GetInterfaces();
            if (baseInterfaces.Count() == 0 || !baseInterfaces.Any())
                continue;

            // 判斷是否繼承了 IRequest<T> 介面
            //
            var requestTypes = baseInterfaces.Where(i => i.IsGenericType
                && i.GetGenericTypeDefinition() == parentRequestType);

            if (requestTypes.Count() != 0 || requestTypes.Any())
                mediators.Add(instance);

            // 判斷是否繼承了 INotification 介面
            //
            var notificationTypes = baseInterfaces.Where(i => i.FullName == parentNotificationType.FullName);

            if (notificationTypes.Count() != 0 || notificationTypes.Any())
                mediators.Add(instance);
        }
    }

    // 添加到依賴注入容器中
    services.AddMediatR(mediators.ToArray());

    return services;
}

  因為需要知道哪些程式集應該進行反射獲取資訊,而對于 Web Api 這個專案來說,它只會通過依賴注入使用到 IMediator 這一個介面,所以這里需要采用不同的引數的形式去確定具體需要通過反射加載哪些程式集,

public class MediatorDescriptionOptions
{
    /// <summary>
    /// Startup 類的 type 型別
    /// </summary>
    public Type StartupClassType { get; set; }

    /// <summary>
    /// 包含使用到 MediatR 組件的程式集
    /// </summary>
    public IEnumerable<string> Assembly { get; set; }
}

  最終,我們就可以在 Startup 類中通過擴展方法的資訊進行快速的注入,實際使用的代碼如下,這里我是將需要加載的程式集資訊放在 appsetting 這個組態檔中的,你可以根據你的喜好進行調整,

public class Startup
{
    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        // Config mediatr
        services.AddCustomMediatR(new MediatorDescriptionOptions
        {
            StartupClassType = typeof(Startup),
            Assembly = Configuration["Assembly:Mediator"].Split("|", StringSplitOptions.RemoveEmptyEntries)
        });
    }
}

  在這個示例專案中的配置資訊如下所示,

{
  "Assembly": {
    "Function": "Sample.Domain",
    "Mapper": "Sample.Application",
    "Mediator": "Sample.Application|Sample.Domain"
  }
}

  當我們注入完成后,就可以直接在 controller 中進行使用,對于繼承了 IRequest 的方法,可以直接通過 Send 方法進行呼叫請求資訊,MediatR 會幫我們找到對應請求的處理方法,最終登錄 action 中的代碼如下,

[ApiVersion("1.0")]
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
public class UsersController : ControllerBase
{
    #region Initizalize

    /// <summary>
    ///
    /// </summary>
    private readonly IMediator _mediator;

    /// <summary>
    /// ctor
    /// </summary>
    /// <param name="mediator"></param>
    public UsersController(IMediator mediator)
    {
        _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
    }

    #endregion Initizalize

    #region APIs

    /// <summary>
    /// 用戶登錄
    /// </summary>
    /// <param name="login">用戶登錄資料傳輸物件</param>
    /// <returns></returns>
    [HttpPost("login")]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    public async Task<IActionResult> Post([FromBody] AppUserLoginDto login)
    {
        // 物體映射轉換
        var command = new UserLoginCommand(login.Account, login.Password, login.VerificationCode);

        bool flag = await _mediator.Send(command);

        if (flag)
            return Ok(new
            {
                code = 20001,
                msg = $"{login.Account} 用戶登錄成功",
                data = login
            });
        else
            return Unauthorized(new
            {
                code = 40101,
                msg = $"{login.Account} 用戶登錄失敗",
                data = login
            });
    }

    #endregion APIs
}

  當我們完成了對于用戶登錄請求的處理之后,就可以去執行后續的“通知類”的事件,與用戶登錄的請求資訊類相似,對于用戶登錄事件的通知類也只是包含一些通知的基礎資訊,在 Smaple.Domain 這個類別庫下面,創建一個 Events 檔案用來存放我們的事件,我們來新建一個繼承于 INotification 介面的 AppUserLoginEvent 類,用來對用戶登錄事件進行相關的處理,

public class AppUserLoginEvent : INotification
{
    /// <summary>
    /// 賬戶
    /// </summary>
    public string Account { get; }

    /// <summary>
    /// ctor
    /// </summary>
    /// <param name="account"></param>
    public AppUserLoginEvent(string account)
    {
        Account = account;
    }
}

  在上文中有提到過,對于一個通知事件可能會存在著多種處理方式,所以這里我們在 Smaple.Application 這個類別庫的 DomainEventHandlers 檔案夾下面會按照事件去創建對應的檔案夾去存放實際處理方法,

  對于繼承了 INotification 介面的通知類來說,在 MediatR 中我們可以通過創建繼承于 INotificationHandler 介面的類去處理對應的事件,因為一個 notification 可以有多個的處理程式,所以我們可以創建多個的 NotificationHandler 類去處理同一個 notification,一個示例的 NotificationHandler 類如下所示,

public class SetCurrentUserEventHandler : INotificationHandler<AppUserLoginEvent>
{
    #region Initizalize

    /// <summary>
    ///
    /// </summary>
    private readonly ILogger<SetCurrentUserEventHandler> _logger;

    /// <summary>
    ///
    /// </summary>
    /// <param name="logger"></param>
    public SetCurrentUserEventHandler(ILogger<SetCurrentUserEventHandler> logger)
    {
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    #endregion Initizalize

    /// <summary>
    /// Notification handler
    /// </summary>
    /// <param name="notification"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public Task Handle(AppUserLoginEvent notification, CancellationToken cancellationToken)
    {
        _logger.LogInformation($"CurrentUser with Account: {notification.Account} has been successfully setup");

        return Task.FromResult(true);
    }
}

  如何去引發這個事件,對于領域驅動設計的架構來說,一個更好的方法是將各種領域事件添加到事件的集合中,然后在提交事務之前或之后立即調度這些域事件,而對于我們這個專案來說,因為這不在這篇文章考慮的范圍內,只是演示如何去使用 MediatR 這個組件,所以這里我就采取在請求邏輯處理完成后直接觸發事件的方式,

  在 UserLoginCommandHandler 類中,修改我們的代碼,在確認登錄成功后,通過呼叫 AppUser 類的 SetUserLoginRecord 方法來觸發我們的通知事件,修改后的代碼如下所示,

public class UserLoginCommandHandler : IRequestHandler<UserLoginCommand, bool>
{
    #region Initizalize

    /// <summary>
    /// 倉儲實體
    /// </summary>
    private readonly IUserRepository _userRepository;

    /// <summary>
    ///
    /// </summary>
    private readonly IMediator _mediator;

    /// <summary>
    /// ctor
    /// </summary>
    /// <param name="userRepository"></param>
    /// <param name="mediator"></param>
    public UserLoginCommandHandler(IUserRepository userRepository, IMediator mediator)
    {
        _userRepository = userRepository ?? throw new ArgumentNullException(nameof(userRepository));
        _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
    }

    #endregion Initizalize

    /// <summary>
    /// Command Handler
    /// </summary>
    /// <param name="request"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public async Task<bool> Handle(UserLoginCommand request, CancellationToken cancellationToken)
    {
        // 1、判斷驗證碼是否正確
        if (string.IsNullOrEmpty(request.VerificationCode))
            return false;

        // 2、驗證登錄密碼是否正確
        var appUser = await _userRepository.GetAppUserInfo(request.Account.Trim(), request.Password.Trim());
        if (appUser == null)
            return false;

        // 3、觸發登錄事件
        appUser.SetUserLoginRecord(_mediator);

        return true;
    }
}

  與使用 Send 方法去呼叫 request 類的請求不同,對于繼承于 INotification 介面的事件通知類,我們需要采用 Publish 的方法去呼叫,至此,對于一個采用中介者模式設計的登錄流程就結束了,SetUserLoginRecord 方法的定義,以及最終我們實作的效果如下所示,

public void SetUserLoginRecord(IMediator mediator)
{
    mediator.Publish(new AppUserLoginEvent(Account));
}

 三、總結

  這一章主要是介紹了如何通過 MediatR 來實作中介者模式,因為自己也是第一次接觸這種思想,對于 MediatR 這個組件也是第一次使用,所以僅僅是采用案例分享的方式對中介者模式的使用方法進行了一個解釋,如果你想要對中介者模式的具體定義與基礎的概念進行進一步的了解的話,可能需要你自己去找資料去弄明白具體的定義,因為初次接觸,難免會有遺漏或錯誤,如果從文章中發現有不對的地方,歡迎在評論區中指出,先行感謝,

 四、參考

  1、中介者模式— Graphic Design Patterns - 圖說設計模式

  2、MediatR 知多少

 

轉載請註明出處,本文鏈接:https://www.uj5u.com/net/91605.html

標籤:.NET Core

上一篇:.net core的服務器模式和作業站模式

下一篇:IIS 上部署 ASP.NET Core 應用程式

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • WebAPI簡介

    Web體系結構: 有三個核心:資源(resource),URL(統一資源識別符號)和表示 他們的關系是這樣的:一個資源由一個URL進行標識,HTTP客戶端使用URL定位資源,表示是從資源回傳資料,媒體型別是資源回傳的資料格式。 接下來我們說下HTTP. HTTP協議的系統是一種無狀態的方式,使用請求/ ......

    uj5u.com 2020-09-09 22:07:47 more
  • asp.net core 3.1 入口:Program.cs中的Main函式

    本文分析Program.cs 中Main()函式中代碼的運行順序分析asp.net core程式的啟動,重點不是剖析原始碼,而是理清程式開始時執行的順序。到呼叫了哪些實體,哪些法方。asp.net core 3.1 的程式入口在專案Program.cs檔案里,如下。ususing System; us ......

    uj5u.com 2020-09-09 22:07:49 more
  • asp.net網站作為websocket服務端的應用該如何寫

    最近被websocket的一個問題困擾了很久,有一個需求是在web網站中搭建websocket服務。客戶端通過網頁與服務器建立連接,然后服務器根據ip給客戶端網頁發送資訊。 其實,這個需求并不難,只是剛開始對websocket的內容不太了解。上網搜索了一下,有通過asp.net core 實作的、有 ......

    uj5u.com 2020-09-09 22:08:02 more
  • ASP.NET 開源匯入匯出庫Magicodes.IE Docker中使用

    Magicodes.IE在Docker中使用 更新歷史 2019.02.13 【Nuget】版本更新到2.0.2 【匯入】修復單列匯入的Bug,單元測驗“OneColumnImporter_Test”。問題見(https://github.com/dotnetcore/Magicodes.IE/is ......

    uj5u.com 2020-09-09 22:08:05 more
  • 在webform中使用ajax

    如果你用過Asp.net webform, 說明你也算是.NET 開發的老兵了。WEBform應該是2011 2013左右,當時還用visual studio 2005、 visual studio 2008。后來基本都用的是MVC。 如果是新開發的專案,估計沒人會用webform技術。但是有些舊版 ......

    uj5u.com 2020-09-09 22:08:50 more
  • iis添加asp.net網站,訪問提示:由于擴展配置問題而無法提供您請求的

    今天在iis服務器配置asp.net網站,遇到一個問題,記錄一下: 問題:由于擴展配置問題而無法提供您請求的頁面。如果該頁面是腳本,請添加處理程式。如果應下載檔案,請添加 MIME 映射。 WindowServer2012服務器,添加角色安裝完.netframework和iis之后,運行aspx頁面 ......

    uj5u.com 2020-09-09 22:10:00 more
  • WebAPI-處理架構

    帶著問題去思考,大家好! 問題1:HTTP請求和回傳相應的HTTP回應資訊之間發生了什么? 1:首先是最底層,托管層,位于WebAPI和底層HTTP堆疊之間 2:其次是 訊息處理程式管道層,這里比如日志和快取。OWIN的參考是將訊息處理程式管道的一些功能下移到堆疊下端的OWIN中間件了。 3:控制器處理 ......

    uj5u.com 2020-09-09 22:11:13 more
  • 微信門戶開發框架-使用指導說明書

    微信門戶應用管理系統,采用基于 MVC + Bootstrap + Ajax + Enterprise Library的技術路線,界面層采用Boostrap + Metronic組合的前端框架,資料訪問層支持Oracle、SQLServer、MySQL、PostgreSQL等資料庫。框架以MVC5,... ......

    uj5u.com 2020-09-09 22:15:18 more
  • WebAPI-HTTP編程模型

    帶著問題去思考,大家好!它是什么?它包含什么?它能干什么? 訊息 HTTP編程模型的核心就是訊息抽象,表示為:HttPRequestMessage,HttpResponseMessage.用于客戶端和服務端之間交換請求和回應訊息。 HttpMethod類包含了一組靜態屬性: private stat ......

    uj5u.com 2020-09-09 22:15:23 more
  • 部署WebApi隨筆

    一、跨域 NuGet參考Microsoft.AspNet.WebApi.Cors WebApiConfig.cs中配置: // Web API 配置和服務 config.EnableCors(new EnableCorsAttribute("*", "*", "*")); 二、清除默認回傳XML格式 ......

    uj5u.com 2020-09-09 22:15:48 more
最新发布
  • C#多執行緒學習(二) 如何操縱一個執行緒

    <a href="https://www.cnblogs.com/x-zhi/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2943582/20220801082530.png" alt="" /></...

    uj5u.com 2023-04-19 09:17:20 more
  • C#多執行緒學習(二) 如何操縱一個執行緒

    C#多執行緒學習(二) 如何操縱一個執行緒 執行緒學習第一篇:C#多執行緒學習(一) 多執行緒的相關概念 下面我們就動手來創建一個執行緒,使用Thread類創建執行緒時,只需提供執行緒入口即可。(執行緒入口使程式知道該讓這個執行緒干什么事) 在C#中,執行緒入口是通過ThreadStart代理(delegate)來提供的 ......

    uj5u.com 2023-04-19 09:16:49 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    <a href="https://www.cnblogs.com/huangxincheng/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/214741/20200614104537.png" alt="" /&g...

    uj5u.com 2023-04-18 08:39:04 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    一:背景 1. 講故事 前段時間協助訓練營里的一位朋友分析了一個程式卡死的問題,回過頭來看這個案例比較經典,這篇稍微整理一下供后來者少踩坑吧。 二:WinDbg 分析 1. 為什么會卡死 因為是表單程式,理所當然就是看主執行緒此時正在做什么? 可以用 ~0s ; k 看一下便知。 0:000> k # ......

    uj5u.com 2023-04-18 08:33:10 more
  • SignalR, No Connection with that ID,IIS

    <a href="https://www.cnblogs.com/smartstar/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/u36196.jpg" alt="" /></a>...

    uj5u.com 2023-03-30 17:21:52 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:15:33 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:13:31 more
  • C#遍歷指定檔案夾中所有檔案的3種方法

    <a href="https://www.cnblogs.com/xbhp/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/957602/20230310105611.png" alt="" /></a&...

    uj5u.com 2023-03-27 14:46:55 more
  • C#/VB.NET:如何將PDF轉為PDF/A

    <a href="https://www.cnblogs.com/Carina-baby/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2859233/20220427162558.png" alt="" />...

    uj5u.com 2023-03-27 14:46:35 more
  • 武裝你的WEBAPI-OData聚合查詢

    <a href="https://www.cnblogs.com/podolski/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/616093/20140323000327.png" alt="" /><...

    uj5u.com 2023-03-27 14:46:16 more