▲?點擊上方“DotNet NB”關注公眾號
回復“1”獲取開發者路線圖
學習分享?丨作者?/?鄭?子?銘????
這是DotNet?NB?公眾號的第202篇原創文章
目錄
為什么我們用 Orleans
Dapr VS Orleans
Actor 模型
Orleans 的核心概念
結合 OP Storming 的實踐
結合 OP Storming 的實踐
業務模型
設計模型
代碼實現
業務模型
我們可以把關鍵對象(職位、客戶行為記錄、線索)參考為 actor
獵頭顧問一邊尋找職位,一邊尋找候選人,撮合之后匹配成線索,然后推薦候選人到客戶公司,進行面試,發放 offer,候選人入職
設計模型
我們新建職位的時候需要一個參數對象 CreateJobArgument,相當于錄入數據
創建了 Job 之后,它有三個行為:瀏覽、點贊、投遞
投遞之后會直接產生一個意向的 Thread,可以繼續去推進它的狀態:推薦 -> 面試 -> offer -> 入職
針對瀏覽和點贊會產生兩種不同的活動記錄:ViewActivity 和 StarActivity
代碼實現
HelloOrleans.Host
HelloOrleans.Host
新建一個空白解決方案 HelloOrleans
創建一個 ASP .NET Core 空項目 HelloOrleans.Host
分別創建 BaseEntity、Job、Thread、Activity 實體
namespace HelloOrleans.Host.Contract.Entity
{public class BaseEntity{public string Identity { get; set; }}
}namespace HelloOrleans.Host.Contract.Entity
{public class Job : BaseEntity{public string Title { get; set; }public string Description { get; set; }public string Location { get; set; }}
}namespace HelloOrleans.Host.Contract.Entity
{public class Thread : BaseEntity{public string JobId { get; set; }public string ContactId { get; set; }public EnumThreadStatus Status { get; set; }}
}namespace HelloOrleans.Host.Contract
{public enum EnumThreadStatus : int{Recommend,Interview,Offer,Onboard,}
}namespace HelloOrleans.Host.Contract.Entity
{public class Activity : BaseEntity{public string JobId { get; set; }public string ContactId { get; set; }public EnumActivityType Type { get; set; }}
}namespace HelloOrleans.Host.Contract
{public enum EnumActivityType : int{View = 1,Star = 2,}
}
給 Job 添加 View 和 Star 的行為
public async Task View(string contactId)
{}public async Task Star(string contactId)
{}
這里就只差 Grain 的 identity,我們添加 Orleans 的 nuget 包
<PackageReference Include="Microsoft.Orleans.Core" Version="3.6.5" />
<PackageReference Include="Microsoft.Orleans.Server" Version="3.6.5" />
<PackageReference Include="Microsoft.Orleans.CodeGenerator.MSBuild" Version="3.6.5" />
<PackageReference Include="Microsoft.Orleans.OrleansTelemetryConsumers.Linux" Version="3.6.5" />
Microsoft.Orleans.Core 是核心
Microsoft.Orleans.Server 做 Host 就需要用到它
Microsoft.Orleans.CodeGenerator.MSBuild 會在編譯的時候幫我們生成客戶端或者訪問代碼
Microsoft.Orleans.OrleansTelemetryConsumers.Linux 是監控
安裝完后我們就可以繼承 Grain 的基類了
using Orleans;namespace HelloOrleans.Host.Contract.Entity
{public class Job : Grain{public string Title { get; set; }public string Description { get; set; }public string Location { get; set; }public async Task View(string contactId){}public async Task Star(string contactId){}}
}
如果我們需要用它來做持久化是有問題的,因為持久化的時候它會序列化我們所有的公有屬性,然而在 Grain 里面會有一些公有屬性你沒有辦法給它序列化,所以持久化的時候會遇到一些問題,除非我們把持久化的東西重新寫一遍
public abstract class Grain : IAddressable, ILifecycleParticipant<IGrainLifecycle>
{public GrainReference GrainReference { get { return Data.GrainReference; } }/// <summary>/// String representation of grain's SiloIdentity including type and primary key./// </summary>public string IdentityString{get { return Identity?.IdentityString ?? string.Empty; }}...
}
理論上你的狀態和行為是可以封裝在一起的,這樣更符合 OO 的邏輯
我們現在需要分開狀態和行為
定義一個 IJobGrain 接口,繼承 IGrainWithStringKey,用 string 作為它的 identity 的類型
using Orleans;namespace HelloOrleans.Host.Contract.Grain
{public interface IJobGrain : IGrainWithStringKey{Task View(string contactId);}
}
定義 JobGrain 繼承 Grain,實現 IJobGrain 接口
using HelloOrleans.Host.Contract.Entity;
using HelloOrleans.Host.Contract.Grain;
using Orleans;namespace HelloOrleans.Host.Grain
{public class JobGrain : Grain<Job>, IJobGrain{public Task View(string contactId){throw new NotImplementedException();}}
}
這是使用 DDD 來做的區分開狀態和行為,變成貧血模型,是不得已而為之,因為持久化的問題
在 Orleans 的角度而言,它的 Actor 綁定了一個外部的狀態,但是實際上我們更希望它們兩在一起
它的實體就變成這樣
namespace HelloOrleans.Host.Contract.Entity
{public class Job{public string Title { get; set; }public string Description { get; set; }public string Location { get; set; }}
}
Job 不是 Actor 實例,JobGrain 才是 Actor 實例
接下來我們需要做一個 Host 讓它跑起來
添加 nuget 包
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
在 Program 中需要通過 WebApplication 的 Builder 配置 Orleans
builder.Host.UseOrleans(silo =>
{silo.UseLocalhostClustering();silo.AddMemoryGrainStorage("hello-orleans");
});
在 JobGrain 中使用 hello-orleans 這個 Storage 標識一下
[StorageProvider(ProviderName = "hello-orleans")]
public class JobGrain : Grain<Job>, IJobGrain
添加 JobController,這屬于前面講的 silo 內模式,可以直接使用 IGrainFactory,因為這是在同一個項目里
using Microsoft.AspNetCore.Mvc;
using Orleans;namespace HelloOrleans.Host.Controllers
{[Route("job")]public class JobController : Controller{private IGrainFactory _factory;public JobController(IGrainFactory grainFactory){_factory = grainFactory;}}
}
添加一個創建方法 CreateAsync,它的入參叫做 CreateJobViewModel,包含我們需要的 Job 的數據
[Route("")]
[HttpPost]
public async Task<IActionResult> CreateAsync([FromBody] CreateJobViewModel model)
{var jobId = Guid.NewGuid().ToString();var jobGrain = _factory.GetGrain<IJobGrain>(jobId);
}
創建的時候 Grain 是不存在的,必須有 identity,不然 Actor 獲取不到,所以需要先 new 一個 identity,就是 jobId
通過 IGrainFactory 獲取到 jobGrain 之后我們是無法獲取到它的 state,只能看到它的行為,所以我們需要在 Grain 里面添加一個 Create 的方法方便我們調用
using HelloOrleans.Host.Contract.Entity;
using Orleans;namespace HelloOrleans.Host.Contract.Grain
{public interface IJobGrain : IGrainWithStringKey{Task<Job> Create(Job job);Task View(string contactId);}
}
所以這個 Create 方法并不是真正的 Create,只是用來設置 state 的對象,再通過 WriteStateAsync 方法保存
using HelloOrleans.Host.Contract.Entity;
using HelloOrleans.Host.Contract.Grain;
using Orleans;
using Orleans.Providers;namespace HelloOrleans.Host.Grain
{[StorageProvider(ProviderName = "hello-orleans")]public class JobGrain : Grain<Job>, IJobGrain{public async Task<Job> Create(Job job){job.Identity = this.GetPrimaryKeyString();this.State = job;await this.WriteStateAsync();return this.State;}public Task View(string contactId){throw new NotImplementedException();}}
}
new 一個 job,調用 Create 方法設置 State,得到一個帶 identity 的 job,然后返回 OK
[Route("")]
[HttpPost]
public async Task<IActionResult> CreateAsync([FromBody] CreateJobViewModel model)
{var jobId = Guid.NewGuid().ToString();var jobGrain = _factory.GetGrain<IJobGrain>(jobId);var job = new Job(){Title = model.Title,Description = model.Description,Location = model.Location,};job = await jobGrain.Create(job);return Ok(job);
}
因為我們現在采用的是內存級別的 GrainStorage,所以我們沒有辦法去查看它
我們再加一個 Get 的方法去查詢它
[Route("{jobId}")]
[HttpGet]
public async Task<IActionResult> GetAsync(string jobId)
{var jobGrain = _factory.GetGrain<IJobGrain>(jobId);
}
這個時候我們需要去 Grain 的接口里面加一個 Get 方法
using HelloOrleans.Host.Contract.Entity;
using Orleans;namespace HelloOrleans.Host.Contract.Grain
{public interface IJobGrain : IGrainWithStringKey{Task Create(Job job);Task<Job> Get();Task View(string contactId);}
}
Get 方法是不需要傳 id 的,因為這個 id 就是 Grain 的 id,你激活的時候就已經有了,直接返回 this.State
using HelloOrleans.Host.Contract.Entity;
using HelloOrleans.Host.Contract.Grain;
using Orleans;
using Orleans.Providers;namespace HelloOrleans.Host.Grain
{[StorageProvider(ProviderName = "hello-orleans")]public class JobGrain : Grain<Job>, IJobGrain{public async Task Create(Job job){this.State = job;await this.WriteStateAsync();}public Task<Job> Get(){return Task.FromResult(this.State);}public Task View(string contactId){throw new NotImplementedException();}}
}
這個地方所有你的行為都不是直接去查數據庫,而是利用這個 State,它不需要你自己去讀取,跟 DDD 的 repository 不同
直接通過 Grain 的 Get 方法獲取 Job 返回 OK
[Route("{jobId}")]
[HttpGet]
public async Task<IActionResult> GetAsync(string jobId)
{var jobGrain = _factory.GetGrain<IJobGrain>(jobId);return Ok(await jobGrain.Get());
}
這里我們可以再加點校驗邏輯
[Route("{jobId}")]
[HttpGet]
public async Task<IActionResult> GetAsync(string jobId)
{if (string.IsNullOrEmpty(jobId)){throw new ArgumentNullException(nameof(jobId));}var jobGrain = _factory.GetGrain<IJobGrain>(jobId);return Ok(await jobGrain.Get());
}
要注意如果你傳入的 jobId 是不存在的,因為不管你傳什么,只要是一個合法的字符串,并且不重復,它都會幫你去激活,只不過在于它是否做持久化而已,如果你隨便傳了一個 jobId,這個時候不是調了 Get 方法,它可能也會返回給你一個空的 state,所以這個 jobId 沒有這種很強的合法性的約束,在調 Get 的時候要特別的注意,不管是 Create 還是 Get,其實都是調用了 GetGrain,傳了一個 identity 進去,這樣的一個行為
在 Program 中添加 Controller 的配置
using Orleans.Hosting;var builder = WebApplication.CreateBuilder(args);builder.Host.UseOrleans(silo =>
{silo.UseLocalhostClustering();silo.AddMemoryGrainStorage("hello-orleans");
});
builder.Services.AddControllers();var app = builder.Build();
app.UseRouting();
app.UseEndpoints(endpoints =>
{endpoints.MapControllers();
});app.MapGet("/", () => "Hello World!");app.Run();
我們啟動項目測試一下
Create 方法入參
{"title": "第一個職位","description": "第一個職位"
}
可以看到方法調用成功,返回的 job 里面包含了 identity
接著我們使用 Create 方法返回的 identity 作為入參調用 Get 方法
可以看到方法調用成功,返回同一個 job
這種基于內存的存儲就很適合用來做單元測試
推薦閱讀:
.NET周報【12月第1期 2022-12-08】.NET 7 新增的 IParsable 接口介紹.NET 云原生架構師訓練營(基于 OP Storming 和 Actor 的大型分布式架構一)--學習筆記一個.NetCore前后端分離、模塊化、插件式的通用框架.NET 為什么推薦Kestrel作為網絡開發框架用最少的代碼打造一個Mini版的gRPC框架
點擊下方卡片關注DotNet NB
一起交流學習▲?點擊上方卡片關注DotNet NB,一起交流學習請在公眾號后臺
回復?【路線圖】獲取.NET 2021開發者路線圖
回復?【原創內容】獲取公眾號原創內容
回復?【峰會視頻】獲取.NET Conf開發者大會視頻
回復?【個人簡介】獲取作者個人簡介
回復?【年終總結】獲取作者年終總結
回復?【加群】加入DotNet NB?交流學習群長按識別下方二維碼,或點擊閱讀原文。和我一起,交流學習,分享心得。