▲点击上方“DotNet NB”关注公众号
回复“1”获取开发者路线图
(相关资料图)
学习分享丨作者/郑子铭
这是DotNetNB公众号的第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 包
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{ public GrainReference GrainReference { get { return Data.GrainReference; } } /// /// String representation of grain"s SiloIdentity including type and primary key. /// 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
using HelloOrleans.Host.Contract.Entity;using HelloOrleans.Host.Contract.Grain;using Orleans;namespace HelloOrleans.Host.Grain{ public class JobGrain : Grain, 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 包
在 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, 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 TaskCreateAsync([FromBody] CreateJobViewModel model){ var jobId = Guid.NewGuid().ToString(); var jobGrain = _factory.GetGrain (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 { TaskCreate(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, IJobGrain { public async Task 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 TaskCreateAsync([FromBody] CreateJobViewModel model){ var jobId = Guid.NewGuid().ToString(); var jobGrain = _factory.GetGrain (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 TaskGetAsync(string jobId){ var jobGrain = _factory.GetGrain (jobId);}
这个时候我们需要去 Grain 的接口里面加一个 Get 方法
using HelloOrleans.Host.Contract.Entity;using Orleans;namespace HelloOrleans.Host.Contract.Grain{ public interface IJobGrain : IGrainWithStringKey { Task Create(Job job); TaskGet(); 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, IJobGrain { public async Task Create(Job job) { this.State = job; await this.WriteStateAsync(); } public Task 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 TaskGetAsync(string jobId){ var jobGrain = _factory.GetGrain (jobId); return Ok(await jobGrain.Get());}
这里我们可以再加点校验逻辑
[Route("{jobId}")][HttpGet]public async TaskGetAsync(string jobId){ if (string.IsNullOrEmpty(jobId)) { throw new ArgumentNullException(nameof(jobId)); } var jobGrain = _factory.GetGrain (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交流学习群长按识别下方二维码,或点击阅读原文。和我一起,交流学习,分享心得。