通过 Dapr Actor 包,您可以从 .NET 应用程序与 Dapr 虚拟 Actor 进行交互。
要开始使用,请参阅 Dapr Actors 操作指南。
This is the multi-page printable view of this section. Click here to print.
通过 Dapr Actor 包,您可以从 .NET 应用程序与 Dapr 虚拟 Actor 进行交互。
要开始使用,请参阅 Dapr Actors 操作指南。
在 Actor 类或 ASP.NET Core 项目中,建议使用 IActorProxyFactory 接口来创建 actor 客户端。
AddActors(...) 方法会将 actor 服务注册到 ASP.NET Core 依赖注入中。
IActorProxyFactory 实例作为单例服务通过依赖注入可用。IActorProxyFactory 实例作为属性(this.ProxyFactory)可用。以下是在 actor 内部创建代理的示例:
public Task<MyData> GetDataAsync()
{
var proxy = this.ProxyFactory.CreateActorProxy<IOtherActor>(ActorId.CreateRandom(), "OtherActor");
await proxy.DoSomethingGreat();
return this.StateManager.GetStateAsync<MyData>("my_data");
}
在本指南中,你将学习如何使用 IActorProxyFactory。
ActorProxy 上的静态方法。由于 ActorProxy 方法容易出错,在配置自定义设置时尽量避免使用它们。IActorProxyFactory 上的所有 API 都需要 actor 类型 和 actor id 才能与 actor 通信。对于强类型客户端,你还需要它的接口之一。
如果你没有 actor id 并且想要与一个新实例通信,可以使用 ActorId.CreateRandom() 创建一个随机 id。由于随机 id 是一个加密强度高的标识符,当你与它交互时,运行时会创建一个新的 actor 实例。
你可以使用 ActorReference 类型与其他 actor 交换 actor 类型和 actor id,作为消息的一部分。
actor 客户端支持两种不同的调用风格:
| Actor 客户端风格 | 描述 |
|---|---|
| 强类型 | 强类型客户端基于 .NET 接口,提供强类型的典型好处。它们不适用于非 .NET actor。 |
| 弱类型 | 弱类型客户端使用 ActorProxy 类。建议仅在需要互操作性或其他高级原因时使用这些客户端。 |
以下示例使用 CreateActorProxy<> 方法创建强类型客户端。CreateActorProxy<> 需要一个 actor 接口类型,并将返回该接口的一个实例。
// 为 IOtherActor 创建一个代理,类型为 OtherActor,具有随机 id
var proxy = this.ProxyFactory.CreateActorProxy<IOtherActor>(ActorId.CreateRandom(), "OtherActor");
// 调用接口定义的方法来调用 actor
//
// proxy 是 IOtherActor 的实现,所以我们可以直接调用其方法
await proxy.DoSomethingGreat();
以下示例使用 Create 方法创建弱类型客户端。Create 返回 ActorProxy 的一个实例。
// 为类型 OtherActor 创建一个代理,具有随机 id
var proxy = this.ProxyFactory.Create(ActorId.CreateRandom(), "OtherActor");
// 按名称调用方法来调用 actor
//
// proxy 是 ActorProxy 的一个实例
await proxy.InvokeMethodAsync("DoSomethingGreat");
由于 ActorProxy 是弱类型代理,你需要将 actor 方法名称作为字符串传入。
你还可以使用 ActorProxy 调用具有请求和响应消息的方法。请求和响应消息将使用 System.Text.Json 序列化器进行序列化。
// 为类型 OtherActor 创建一个代理,具有随机 id
var proxy = this.ProxyFactory.Create(ActorId.CreateRandom(), "OtherActor");
// 调用代理上的方法来调用 actor
//
// proxy 是 ActorProxy 的一个实例
var request = new MyRequest() { Message = "Hi, it's me.", };
var response = await proxy.InvokeMethodAsync<MyRequest, MyResponse>("DoSomethingGreat", request);
使用弱类型代理时,你_必须_主动定义正确的 actor 方法名称和消息类型。使用强类型代理时,这些名称和类型作为接口定义的一部分为你定义。
actor 方法调用异常详细信息会向调用方和被调用方公开,提供追踪问题的入口点。异常详细信息包括:
你使用 UUID 在调用方和被调用方之间匹配异常。以下是异常详细信息的示例:
Dapr.Actors.ActorMethodInvocationException: Remote Actor Method Exception, DETAILS: Exception: NotImplementedException, Method Name: ExceptionExample, Line Number: 14, Exception uuid: d291a006-84d5-42c4-b39e-d6300e9ac38b
ActorHost:
internal class MyActor : Actor, IMyActor, IRemindable
{
public MyActor(ActorHost host) // Accept ActorHost in the constructor
: base(host) // Pass ActorHost to the base class constructor
{
}
}
由于 ActorHost 包含 actor 独有的状态,你无需将实例传递到代码的其他部分。建议仅在测试中创建自己的 ActorHost 实例。
Actor 支持将额外参数依赖注入到构造函数中。你定义的任何其他参数都将从依赖注入容器中获取其值。
internal class MyActor : Actor, IMyActor, IRemindable
{
public MyActor(ActorHost host, BankService bank) // Accept BankService in the constructor
: base(host)
{
...
}
}
actor 类型应具有单个 public 构造函数。actor 基础设施使用 ActivatorUtilities 模式来构造 actor 实例。
你可以在 Startup.cs 中注册类型以使其可用于依赖注入。阅读更多关于注册类型的不同方式。
// In Startup.cs
public void ConfigureServices(IServiceCollection services)
{
...
// Register additional types with dependency injection.
services.AddSingleton<BankService>();
}
每个 actor 实例都有自己的依赖注入作用域,并在执行操作后的段时间内保留在内存中。在此期间,与 actor 关联的依赖注入作用域也被视为活动状态。当 actor 被停用时,该作用域将被释放。
如果 actor 在构造函数中注入了 IServiceProvider,则 actor 将接收对其作用域关联的 IServiceProvider 的引用。IServiceProvider 可用于在未来动态解析服务。
internal class MyActor : Actor, IMyActor, IRemindable
{
public MyActor(ActorHost host, IServiceProvider services) // Accept IServiceProvider in the constructor
: base(host)
{
...
}
}
使用此模式时,避免创建许多实现 IDisposable 的瞬态服务实例。由于与 actor 关联的作用域可能在较长时间内被视为有效,因此可能会在内存中累积许多服务。有关更多信息,请参阅依赖注入指南。
Actor 可以实现 IDisposable 或 IAsyncDisposable。建议依赖依赖注入进行资源管理,而不是在应用程序代码中实现 dispose 功能。在确实必要的罕见情况下才提供 dispose 支持。
在 actor 类中,你可以通过基 Actor 类上的属性访问 ILogger 实例。此实例连接到 ASP.NET Core 日志系统,应用于 actor 内的所有日志记录。阅读更多关于日志记录。你可以配置多种不同的日志格式和输出接收器。
使用带有_命名占位符_的_结构化日志记录_,如下所示:
public Task<MyData> GetDataAsync()
{
this.Logger.LogInformation("Getting state at {CurrentTime}", DateTime.UtcNow);
return this.StateManager.GetStateAsync<MyData>("my_data");
}
记录日志时,避免使用格式字符串,如:$"Getting state at {DateTime.UtcNow}"
日志记录应使用命名占位符语法,它提供更好的性能和与日志系统的集成。
默认情况下,客户端看到的 actor 类型_派生自 actor 实现类的_名称。默认名称将是类名(不带命名空间)。
如果需要,可以通过将 ActorAttribute 属性附加到 actor 实现类来指定显式类型名称。
[Actor(TypeName = "MyCustomActorTypeName")]
internal class MyActor : Actor, IMyActor
{
// ...
}
在上面的示例中,名称将是 MyCustomActorTypeName。
无需更改向运行时注册 actor 类型的代码,通过属性提供值就是所需的全部。
Actor 注册是 Startup.cs 中 ConfigureServices 的一部分。你可以通过 ConfigureServices 方法注册具有依赖注入的服务。注册 actor 类型集是 actor 服务注册的一部分。
在 ConfigureServices 内部,你可以:
AddActors)options.Actors.RegisterActor<>)optionsservices)// In Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// Register actor runtime with DI
services.AddActors(options =>
{
// Register actor types and configure actor settings
options.Actors.RegisterActor<MyActor>();
// Configure default settings
options.ActorIdleTimeout = TimeSpan.FromMinutes(10);
options.ActorScanInterval = TimeSpan.FromSeconds(35);
options.DrainOngoingCallTimeout = TimeSpan.FromSeconds(35);
options.DrainRebalancedActors = true;
});
// Register additional services for use with actors
services.AddSingleton<BankService>();
}
actor 运行时使用 System.Text.Json 进行:
默认情况下,actor 运行时使用基于 JsonSerializerDefaults.Web 的设置。
你可以作为 ConfigureServices 的一部分配置 JsonSerializerOptions:
// In Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddActors(options =>
{
...
// Customize JSON options
options.JsonSerializerOptions = ...
});
}
ASP.NET Core 对 actor 的托管支持使用终结点路由系统。.NET SDK 不支持使用早期 ASP.NET Core 版本中的旧路由系统托管 actor。
由于 actor 使用终结点路由,actor HTTP 处理程序是中间件管道的一部分。以下是设置带有 actor 的中间件管道的 Configure 方法的最小示例。
// in Startup.cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseEndpoints(endpoints =>
{
// Register actors handlers that interface with the Dapr runtime.
endpoints.MapActorsHandlers();
});
}
UseRouting 和 UseEndpoints 调用是配置路由所必需的。通过在终结点中间件内添加 MapActorsHandlers 将 actor 配置为管道的一部分。
这是一个最小示例,Actor 功能与以下功能并存是有效的:
某些中间件可能会干扰 Dapr 请求到 actor 处理程序的路由。特别是,UseHttpsRedirection 对 Dapr 的默认配置有问题。Dapr 默认情况下通过未加密的 HTTP 发送请求,UseHttpsRedirection 中间件将阻止这些请求。此中间件目前不能与 Dapr 一起使用。
// in Startup.cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// INVALID - this will block non-HTTPS requests
app.UseHttpsRedirection();
// INVALID - this will block non-HTTPS requests
app.UseRouting();
app.UseEndpoints(endpoints =>
{
// Register actors handlers that interface with the Dapr runtime.
endpoints.MapActorsHandlers();
});
}
Dapr actor 包使您能够在 .NET 应用程序中使用弱类型或强类型客户端来使用 Dapr virtual actors。每种方式使用不同的序列化方法。本文档将回顾这些差异,并传达一些关键的基本规则,以便在任何一种情况下都能理解。
请注意,由于这些不同的序列化方法,不支持可互换地使用弱类型或强类型 actor 客户端。使用一个 Actor 客户端持久化的数据将无法使用另一个 Actor 客户端访问,因此重要的是选择一个并在整个应用程序中一致地使用它。
在本节中,您将学习如何配置 C# 类型,以便在使用弱类型 actor 客户端时在运行时正确序列化和反序列化。这些客户端使用基于字符串的方法名称,以及使用 System.Text.Json 序列化程序序列化的请求和响应负载。请注意,此序列化框架并非特定于 Dapr,而是由 .NET 团队在 .NET GitHub 仓库 中单独维护。
当使用弱类型 Dapr Actor 客户端从各种 actor 调用方法时,不需要独立序列化或反序列化方法负载,因为 SDK 将代表您透明地执行此操作。
客户端将针对您构建的 .NET 版本可用的最新版本的 System.Text.Json,并且序列化遵循相关 .NET 文档中提供的所有固有功能。
序列化程序将配置为使用 JsonSerializerOptions.Web 默认选项,除非使用自定义选项配置覆盖,这意味着应用以下内容:
[JsonPropertyName] 属性覆盖在以下示例中,我们展示了一个名为 Doodad 的简单类,但它也可以是一个记录。
public class Doodad
{
public Guid Id { get; set; }
public string Name { get; set; }
public int Count { get; set; }
}
默认情况下,这将使用类型中使用的成员名称以及其实例化的任何值进行序列化:
{"id": "a06ced64-4f42-48ad-84dd-46ae6a7e333d", "name": "DoodadName", "count": 5}
可以通过将 [JsonPropertyName] 属性应用于所需的属性来覆盖默认属性名称。
通常,对于您持久化到 actor 状态的类型,这不是必需的,因为您不打算独立于 Dapr 相关功能读取或写入它们,但以下内容仅是为了清楚地说明这是可能的。
以下示例演示了使用 JsonPropertyName 更改序列化后第一个属性的名称。请注意,Count 属性上最后一次使用 JsonPropertyName 与预期序列化的名称相匹配。这主要是为了证明应用此属性不会产生任何负面影响——事实上,如果您稍后决定更改默认序列化选项但仍需要在更改之前一致地访问以前序列化的属性,这可能是更可取的,因为 JsonPropertyName 将覆盖这些选项。
public class Doodad
{
[JsonPropertyName("identifier")]
public Guid Id { get; set; }
public string Name { get; set; }
[JsonPropertyName("count")]
public int Count { get; set; }
}
这将序列化为以下内容:
{"identifier": "a06ced64-4f42-48ad-84dd-46ae6a7e333d", "name": "DoodadName", "count": 5}
让我们尝试对 C# 12 或更高版本的记录执行相同的操作:
public record Thingy(string Name, [JsonPropertyName("count")] int Count);
由于在主构造函数中传递的参数(在 C# 12 中引入)可以应用于记录中的属性或字段,因此在某些不明确的情况下,使用 [JsonPropertyName] 属性可能需要指定您打算将属性应用于属性而不是字段。如果有必要,您可以在主构造函数中这样指示:
public record Thingy(string Name, [property: JsonPropertyName("count")] int Count);
如果在不需要的情况下将 [property: ] 应用于 [JsonPropertyName] 属性,则不会对序列化或反序列化产生负面影响,因为操作将正常进行,就好像它是一个属性(通常情况下,如果没有这样标记)。
枚举(包括平面枚举)可以序列化为 JSON,但持久化的值可能会让您感到惊讶。同样,开发人员不应该独立于 Dapr 处理序列化数据,但以下信息可能至少有助于诊断为什么看似微小的版本迁移无法按预期工作。
采用以下 enum 类型提供一年中的各个季节:
public enum Season
{
Spring,
Summer,
Fall,
Winter
}
我们将继续使用一个单独的演示类型来引用我们的 Season,并同时演示它如何与记录一起使用:
public record Engagement(string Name, Season TimeOfYear);
给定以下初始化实例:
var myEngagement = new Engagement("Ski Trip", Season.Winter);
这将序列化为以下 JSON:
{"name": "Ski Trip", "season": 3}
我们的 Season.Winter 值表示为 3 可能是出乎意料的,但这是因为序列化程序将自动使用枚举值的数字表示形式,第一个值从零开始,并递增每个可用附加值的数值。同样,如果正在进行迁移并且开发人员翻转了枚举的顺序,这将对您的解决方案产生重大更改,因为在反序列化时序列化的数值将指向不同的值。
相反,System.Text.Json 提供了一个 JsonConverter,它将选择使用基于字符串的值而不是数值。需要将 [JsonConverter] 属性应用于枚举类型本身以启用此功能,但随后将在任何引用枚举的下游序列化或反序列化操作中实现。
[JsonConverter(typeof(JsonStringEnumConverter<Season>))]
public enum Season
{
Spring,
Summer,
Fall,
Winter
}
使用上面 myEngagement 实例中的相同值,这将生成以下 JSON:
{"name": "Ski Trip", "season": "Winter"}
因此,可以移动枚举成员,而无需担心在反序列化期间引入错误。
System.Text.Json 序列化平台开箱即用,不支持使用 [EnumMember] 来允许您更改序列化或反序列化期间使用的枚举值,但在某些情况下,这可能很有用。同样,假设您负责重构解决方案,以便为各种枚举应用更好的名称。您使用了上面详述的 JsonStringEnumConverter<TType>,因此您将枚举的名称保存为值而不是数值,但如果您更改枚举名称,这将引入重大更改,因为该名称将不再与状态中的名称匹配。
请注意,如果您选择使用此方法,应该用 [EnumMember] 属性装饰所有枚举成员,以便为每个枚举值一致地应用值,而不是杂乱无章。没有任何东西会在构建或运行时验证这一点,但它被视为最佳实践操作。
在这种情况下,如何指定持久化的精确值,同时更改枚举成员的名称?使用自定义 JsonConverter 和扩展方法,该方法可以在提供的情况下从附加的 [EnumMember] 属性中提取值。将以下内容添加到您的解决方案中:
public sealed class EnumMemberJsonConverter<T> : JsonConverter<T> where T : struct, Enum
{
/// <summary>读取并将 JSON 转换为类型 <typeparamref name="T" />。</summary>
/// <param name="reader">读取器。</param>
/// <param name="typeToConvert">要转换的类型。</param>
/// <param name="options">指定要使用的序列化选项的对象。</param>
/// <returns>转换后的值。</returns>
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// 从 JSON 读取器获取字符串值
var value = reader.GetString();
// 遍历所有枚举值
foreach (var enumValue in Enum.GetValues<T>())
{
// 从 EnumMember 属性获取值(如果有)
var enumMemberValue = GetValueFromEnumMember(enumValue);
// 如果值匹配,则返回枚举值
if (value == enumMemberValue)
{
return enumValue;
}
}
// 如果未找到匹配项,则引发异常
throw new JsonException($"Invalid value for {typeToConvert.Name}: {value}");
}
/// <summary>将指定值写入 JSON。</summary>
/// <param name="writer">要写入的写入器。</param>
/// <param name="value">要转换为 JSON 的值。</param>
/// <param name="options">指定要使用的序列化选项的对象。</param>
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
// 从 EnumMember 属性获取值(如果有)
var enumMemberValue = GetValueFromEnumMember(value);
// 将值写入 JSON 写入器
writer.WriteStringValue(enumMemberValue);
}
private static string GetValueFromEnumMember(T value)
{
MemberInfo[] member = typeof(T).GetMember(value.ToString(), BindingFlags.DeclaredOnly | BindingFlags.Static | BindingFlags.Public);
if (member.Length == 0)
return value.ToString();
object[] customAttributes = member.GetCustomAttributes(typeof(EnumMemberAttribute), false);
if (customAttributes.Length != 0)
{
EnumMemberAttribute enumMemberAttribute = (EnumMemberAttribute)customAttributes;
if (enumMemberAttribute != null && enumMemberAttribute.Value != null)
return enumMemberAttribute.Value;
}
return value.ToString();
}
}
现在让我们添加一个示例枚举器。我们将设置一个值,该值使用每个枚举成员的小写版本来演示这一点。不要忘记用 JsonConverter 属性装饰枚举,并引用我们的自定义转换器,而不是上一节中使用的数字到字符串转换器。
[JsonConverter(typeof(EnumMemberJsonConverter<Season>))]
public enum Season
{
[EnumMember(Value="spring")]
Spring,
[EnumMember(Value="summer")]
Summer,
[EnumMember(Value="fall")]
Fall,
[EnumMember(Value="winter")]
Winter
}
让我们使用之前的示例记录。我们还将添加一个 [JsonPropertyName] 属性只是为了增强演示:
public record Engagement([property: JsonPropertyName("event")] string Name, Season TimeOfYear);
最后,让我们初始化一个新的实例:
var myEngagement = new Engagement("Conference", Season.Fall);
这一次,序列化将考虑来自附加的 [EnumMember] 属性的值,为我们提供了一种重构应用程序的机制,而不需要对状态中现有枚举值进行复杂的版本控制方案。
{"event": "Conference", "season": "fall"}
在 Dapr Actor 客户端中使用多态类型时,必须正确处理序列化和反序列化,以确保实例化适当的派生类型。多态序列化允许您序列化基类型的对象,同时保留特定的派生类型信息。
要启用多态反序列化,必须在基类型上使用 [JsonPolymorphic] 属性。此外,至关重要的是包含 [AllowOutOfOrderMetadataProperties] 属性,以确保元数据属性(如 $type)可以被 System.Text.Json 正确处理,即使它们不是 JSON 对象中的第一个属性。
[JsonPolymorphic]
[AllowOutOfOrderMetadataProperties]
public abstract class SampleValueBase
{
public string CommonProperty { get; set; }
}
public class DerivedSampleValue : SampleValueBase
{
public string SpecificProperty { get; set; }
}
在此示例中,SampleValueBase 类标记了 [JsonPolymorphic] 和 [AllowOutOfOrderMetadataProperties] 属性。此设置确保 $type 元数据属性可以在反序列化期间正确识别和处理,无论它在 JSON 对象中的位置如何。
通过遵循此方法,您可以在 Dapr Actor 客户端中有效地管理多态序列化和反序列化,确保实例化和使用正确的派生类型。
在本节中,您将学习如何配置类和记录,以便在使用强类型 actor 客户端时在运行时正确序列化和反序列化。这些客户端使用 .NET 接口实现,与使用其他语言编写的 Dapr Actor 不兼容。
此 actor 客户端使用名为 Data Contract Serializer 的序列化引擎序列化数据,该引擎将 C# 类型转换为 XML 文档并从中转换。此序列化框架并非特定于 Dapr,而是由 .NET 团队在 .NET GitHub 仓库 中单独维护。
发送或接收基元类型(如字符串或整数)时,此序列化会透明地发生,您无需进行任何必要的准备。但是,在处理您自己创建的复杂类型时,需要考虑一些重要规则,以便此过程顺利进行。
使用 Data Contract Serializer 时,有几个重要的注意事项需要牢记:
反序列化使用的方法取决于类型是否使用 DataContractAttribute 属性进行装饰。如果不存在此属性,则使用无参构造函数创建类型的实例。然后,每个属性和字段使用其各自的设置器映射到类型中,并将实例返回给调用者。
如果类型标记了 [DataContract],序列化程序将改为使用反射读取类型的元数据,并根据是否使用 DataMemberAttribute 属性标记来确定应包含哪些属性或字段,因为它是基于选择性加入的方式执行的。然后,它在内存中分配一个未初始化的对象(避免使用任何构造函数,无论是否有参数),然后直接在每个映射的属性或字段上设置值,即使是私有的或使用 init-only 设置器。在此过程中,将根据情况调用序列化回调,然后将对象返回给调用者。
强烈建议使用序列化属性,因为它们提供了更大的灵活性来覆盖名称和命名空间,并且通常使用更多的现代 C# 功能。虽然默认序列化程序可以用于基元类型,但不建议将其用于您自己的任何类型,无论是类、结构体还是记录。如果您使用 DataContractAttribute 属性装饰类型,还建议显式使用 DataMemberAttribute 属性装饰要序列化或反序列化的每个成员。
类在 Data Contract Serializer 中得到完全支持,前提是还遵循本文档和 Data Contract Serializer 文档中详述的其他规则。
这里要记住的最重要的事情是,您必须拥有公共无参构造函数,或者必须使用适当的属性对其进行装饰。让我们查看一些示例来真正阐明什么可行,什么不可行。
在以下示例中,我们展示了一个名为 Doodad 的简单类。这里我们没有提供显式构造函数,因此编译器将提供默认的无参构造函数。因为我们使用的是 支持的基元类型(Guid、string 和 int32),并且我们的所有成员都具有公共 getter 和 setter,所以不需要任何属性,并且我们可以毫无问题地在 Dapr actor 方法中发送和接收此类时使用此类。
public class Doodad
{
public Guid Id { get; set; }
public string Name { get; set; }
public int Count { get; set; }
}
默认情况下,这将使用类型中使用的成员名称以及其实例化的任何值进行序列化:
<Doodad>
<Id>a06ced64-4f42-48ad-84dd-46ae6a7e333d</Id>
<Name>DoodadName</Name>
<Count>5</Count>
</Doodad>
所以让我们调整它——让我们添加自己的构造函数,并且只在成员上使用 init-only 设置器。这将无法序列化和反序列化,不是因为使用了 init-only 设置器,而是因为没有无参构造函数。
// 无法正确序列化!
public class Doodad
{
public Doodad(string name, int count)
{
Id = Guid.NewGuid();
Name = name;
Count = count;
}
public Guid Id { get; set; }
public string Name { get; init; }
public int Count { get; init; }
}
如果我们向类型添加公共无参构造函数,我们就可以开始了,这将无需进一步注释即可工作。
public class Doodad
{
public Doodad()
{
}
public Doodad(string name, int count)
{
Id = Guid.NewGuid();
Name = name;
Count = count;
}
public Guid Id { get; set; }
public string Name { get; set; }
public int Count { get; set; }
}
但是如果我们不想添加这个构造函数呢?也许您不希望您的开发人员意外地使用非预期的构造函数创建此 Doodad 的实例。这就是更灵活的属性有用的地方。如果使用 DataContractAttribute 属性装饰类型,则可以删除无参构造函数,它将再次工作。
[DataContract]
public class Doodad
{
public Doodad(string name, int count)
{
Id = Guid.NewGuid();
Name = name;
Count = count;
}
public Guid Id { get; set; }
public string Name { get; set; }
public int Count { get; set; }
}
在上面的示例中,我们不需要使用 DataMemberAttribute 属性,因为同样,我们使用的是序列化程序支持的 内置基元类型。但是,如果我们使用属性,我们会获得更多的灵活性。通过 DataContractAttribute 属性,我们可以使用 Namespace 参数指定我们自己的 XML 命名空间,并通过 Name 参数,在序列化到 XML 文档时更改类型的名称。
建议的做法是将 DataContractAttribute 属性附加到类型,并将 DataMemberAttribute 属性附加到要序列化的所有成员——如果它们不是必需的并且您没有更改默认值,它们将被忽略,但它们为您提供了一种选择序列化本来不会包含的成员的机制,例如标记为私有的成员或本身就是复杂类型或集合的成员。
请注意,如果您选择序列化私有成员,它们的值将被序列化为纯文本——根据您在序列化后处理数据的方式,它们很有可能被查看、拦截并可能被篡改,因此重要的是在您的用例中考虑是否要标记这些成员。
在以下示例中,我们将查看使用属性来更改某些成员的序列化名称,以及引入 IgnoreDataMemberAttribute 属性。顾名思义,这告诉序列化程序跳过此属性,即使它本来符合序列化条件。此外,因为我使用 DataContractAttribute 属性装饰类型,这意味着我可以在属性上使用 init-only 设置器。
[DataContract(Name="Doodad")]
public class Doodad
{
public Doodad(string name = "MyDoodad", int count = 5)
{
Id = Guid.NewGuid();
Name = name;
Count = count;
}
[DataMember(Name = "id")]
public Guid Id { get; init; }
[IgnoreDataMember]
public string Name { get; init; }
[DataMember]
public int Count { get; init; }
}
序列化时,因为我们更改了序列化成员的名称,我们可以期望使用默认值的新 Doodad 实例序列化为:
<Doodad>
<id>a06ced64-4f42-48ad-84dd-46ae6a7e333d</id>
<Count>5</Count>
</Doodad>
C# 12 为我们带来了类的主构造函数。使用主构造函数意味着编译器将被阻止创建默认的隐式无参构造函数。虽然类上的主构造函数不会生成任何公共属性,但这意味着如果向此主构造函数传递任何参数或在类中有非基元类型,则要么需要指定自己的无参构造函数,要么使用序列化属性。
这是一个示例,我们使用主构造函数将 ILogger 注入到字段并添加我们自己的无参构造函数,而无需任何属性。
public class Doodad(ILogger<Doodad> _logger)
{
public Doodad() {} // 我们的无参构造函数
public Doodad(string name, int count)
{
Id = Guid.NewGuid();
Name = name;
Count = count;
}
public Guid Id { get; set; }
public string Name { get; set; }
public int Count { get; set; }
}
并使用我们的序列化属性(同样,因为我们使用序列化属性,所以选择使用 init-only 设置器):
[DataContract]
public class Doodad(ILogger<Doodad> _logger)
{
public Doodad(string name, int count)
{
Id = Guid.NewGuid();
Name = name;
Count = count;
}
[DataMember]
public Guid Id { get; init; }
[DataMember]
public string Name { get; init; }
[DataMember]
public int Count { get; init; }
}
结构体由 Data Contract 序列化程序支持,前提是它们使用 DataContractAttribute 属性标记,并且您希望序列化的成员使用 DataMemberAttribute 属性标记。此外,为了支持反序列化,结构体还需要具有无参构造函数。即使在 C# 10 中定义了自己的无参构造函数,这也可以工作。
[DataContract]
public struct Doodad
{
[DataMember]
public int Count { get; set; }
}
记录是在 C# 9 中引入的,在序列化方面遵循与类完全相同的规则。我们建议您应该使用 DataContractAttribute 属性装饰所有记录,并使用 DataMemberAttribute 属性装饰要序列化的成员,以免在使用此或其他较新的 C# 功能时遇到任何反序列化问题。因为记录类默认对属性使用 init-only 设置器并鼓励使用主构造函数,所以将这些属性应用于类型可确保序列化程序能够正确地容纳您的类型。
通常,记录使用新的主构造函数概念呈现为简单的单行语句:
public record Doodad(Guid Id, string Name, int Count);
一旦您在 Dapr actor 方法调用中使用它,这将抛出一个错误,鼓励使用序列化属性,因为没有可用的无参构造函数,也没有使用上述属性进行装饰。
这里我们添加一个显式的无参构造函数,它不会抛出错误,但在反序列化期间不会设置任何值,因为它们是使用 init-only 设置器创建的。因为它没有对任何成员使用 DataContractAttribute 属性或 DataMemberAttribute 属性,所以序列化程序将无法在反序列化期间正确映射目标成员。
public record Doodad(Guid Id, string Name, int Count)
{
public Doodad() {}
}
这种方法不需要额外的构造函数,而是依赖于序列化属性。因为我们使用 DataContractAttribute 属性标记类型,并使用自己的 DataMemberAttribute 属性装饰每个成员,所以序列化引擎将能够毫无问题地从 XML 文档映射到我们的类型。
[DataContract]
public record Doodad(
[property: DataMember] Guid Id,
[property: DataMember] string Name,
[property: DataMember] int Count)
.NET 中内置了多种类型,这些类型被视为基元类型,无需开发人员付出额外努力即可进行序列化:
还有其他不是真正基元类型但具有类似内置支持的类型:
同样,如果您想通过 actor 方法传递这些类型,无需额外考虑,因为它们将毫无问题地序列化和反序列化。此外,本身标记了 (SerializeableAttribute)[https://learn.microsoft.com/dotnet/api/system.serializableattribute] 属性的类型也将被序列化。
枚举(包括标志枚举)如果适当标记则是可序列化的。您希望序列化的枚举成员必须使用 EnumMemberAttribute 属性标记才能进行序列化。在此属性的可选 Value 参数中传递自定义值将允许您指定成员在序列化文档中使用的值,而不是让序列化程序从成员名称派生它。
枚举类型不要求使用 DataContractAttribute 属性装饰类型——只需要您希望序列化的成员使用 EnumMemberAttribute 属性标记即可。
public enum Colors
{
[EnumMember]
Red,
[EnumMember(Value="g")]
Green,
Blue, // 即使被类型使用,此值也不会被序列化,因为它没有使用 EnumMember 属性装饰
}
关于数据约定序列化程序,所有实现 IEnumerable 接口的集合类型(包括数组和泛型集合)都被视为集合。实现 IDictionary 或泛型 IDictionary<TKey, TValue> 的类型被视为字典集合;所有其他类型都被视为列表集合。
与其他复杂类型一样,集合类型必须具有可用的无参构造函数。此外,它们还必须具有名为 Add 的方法,以便可以正确序列化和反序列化。这些集合类型使用的类型本身必须标记 DataContractAttribute 属性或按照本文档中的描述可序列化。
由于数据约定序列化程序仅在 Dapr 中用于通过代理方法序列化和反序列化 .NET SDK 中的值与 Dapr actor 实例之间,因此几乎不需要考虑数据约定的版本控制,因为数据不会在使用相同序列化程序的应用程序版本之间持久化。对于那些有兴趣了解有关数据约定版本控制的更多信息的人,请访问此处。
通过使用 DataContractAttribute 属性标记每个类型,可以轻松嵌套您自己的复杂类型。这会通知序列化程序应如何执行反序列化。
但是,如果您正在处理多态类型,并且您的成员之一是具有派生类或其他实现的基类或接口呢?在这里,您将使用 KnownTypeAttribute 属性向序列化程序提供有关如何继续的提示。
当您将 KnownTypeAttribute 属性应用于类型时,您是在通知数据约定序列化程序它可能遇到哪些子类型,允许它正确处理这些类型的序列化和反序列化,即使运行时的实际类型与声明的类型不同。
[DataContract]
[KnownType(typeof(DerivedClass))]
public class BaseClass
{
// 基类的成员
}
[DataContract]
public class DerivedClass : BaseClass
{
// 派生类的其他成员
}
在此示例中,BaseClass 标记了 [KnownType(typeof(DerivedClass))],这告诉数据约定序列化程序 DerivedClass 是它可能需要序列化或反序列化的 BaseClass 的可能实现。如果没有此属性,序列化程序在遇到实际上是 DerivedClass 类型的 BaseClass 实例时将不知道 DerivedClass,这可能会导致序列化异常,因为序列化程序将不知道如何处理派生类型。通过将所有可能的派生类型指定为已知类型,可以确保序列化程序可以正确处理该类型及其成员。
有关使用 [KnownType] 的更多信息和示例,请参阅官方文档。
Dapr Actor 包允许你从 .NET 应用程序与 Dapr 虚拟 Actor 交互。在本指南中,你将学习如何:
MyActor)。MyActor --- MyActor.Interfaces
|
+- MyActorService
|
+- MyActorClient
接口项目 (\MyActor\MyActor.Interfaces)
该项目包含 actor 的接口定义。Actor 接口可以在任何名称的项目中定义。接口定义了由以下各方共享的 actor 契约:
由于客户端项目可能依赖它,最好在独立于 actor 实现的程序集中定义它。
Actor 服务项目 (\MyActor\MyActorService)
该项目实现承载 actor 的 ASP.Net Core Web 服务。它包含 actor 的实现 MyActor.cs。Actor 实现是一个类,它:
MyActor.Interfaces 项目中定义的接口。Actor 类还必须实现一个构造函数,该构造函数接受一个 ActorService 实例和一个 ActorId,并将它们传递给基 Actor 类。
Actor 客户端项目 (\MyActor\MyActorClient)
该项目包含 actor 客户端的实现,该客户端调用 MyActor 的在 Actor Interfaces 中定义的方法。
由于我们将创建 3 个项目,请选择一个空目录作为起点,并在你选择的终端中打开它。
Actor 接口定义了由 actor 实现和调用 actor 的客户端共享的 actor 契约。
Actor 接口定义需满足以下要求:
Dapr.Actors.IActor 接口Task 或 Task<object># Create Actor Interfaces
dotnet new classlib -o MyActor.Interfaces
cd MyActor.Interfaces
# Add Dapr.Actors nuget package. Please use the latest package version from nuget.org
dotnet add package Dapr.Actors
cd ..
定义 IMyActor 接口和 MyData 数据对象。将以下代码粘贴到 MyActor.Interfaces 项目的 MyActor.cs 中。
using Dapr.Actors;
using Dapr.Actors.Runtime;
using System.Threading.Tasks;
namespace MyActor.Interfaces
{
public interface IMyActor : IActor
{
Task<string> SetDataAsync(MyData data);
Task<MyData> GetDataAsync();
Task RegisterReminder();
Task UnregisterReminder();
Task<IActorReminder> GetReminder();
Task RegisterTimer();
Task UnregisterTimer();
}
public class MyData
{
public string PropertyA { get; set; }
public string PropertyB { get; set; }
public override string ToString()
{
var propAValue = this.PropertyA == null ? "null" : this.PropertyA;
var propBValue = this.PropertyB == null ? "null" : this.PropertyB;
return $"PropertyA: {propAValue}, PropertyB: {propBValue}";
}
}
}
Dapr 使用 ASP.NET Web 服务来承载 Actor 服务。本节将实现 IMyActor actor 接口并将 Actor 注册到 Dapr Runtime。
# Create ASP.Net Web service to host Dapr actor
dotnet new web -o MyActorService
cd MyActorService
# Add Dapr.Actors.AspNetCore nuget package. Please use the latest package version from nuget.org
dotnet add package Dapr.Actors.AspNetCore
# Add Actor Interface reference
dotnet add reference ../MyActor.Interfaces/MyActor.Interfaces.csproj
cd ..
实现 IMyActor 接口并派生自 Dapr.Actors.Actor 类。以下示例还展示了如何使用 Actor Reminders。要使 Actors 使用 Reminders,它必须派生自 IRemindable。如果你不打算使用 Reminder 功能,可以跳过实现 IRemindable 和下面代码中显示的 reminder 特定方法。
将以下代码粘贴到 MyActorService 项目的 MyActor.cs 中:
using Dapr.Actors;
using Dapr.Actors.Runtime;
using MyActor.Interfaces;
using System;
using System.Threading.Tasks;
namespace MyActorService
{
internal class MyActor : Actor, IMyActor, IRemindable
{
// The constructor must accept ActorHost as a parameter, and can also accept additional
// parameters that will be retrieved from the dependency injection container
//
/// <summary>
/// Initializes a new instance of MyActor
/// </summary>
/// <param name="host">The Dapr.Actors.Runtime.ActorHost that will host this actor instance.</param>
public MyActor(ActorHost host)
: base(host)
{
}
/// <summary>
/// This method is called whenever an actor is activated.
/// An actor is activated the first time any of its methods are invoked.
/// </summary>
protected override Task OnActivateAsync()
{
// Provides opportunity to perform some optional setup.
Console.WriteLine($"Activating actor id: {this.Id}");
return Task.CompletedTask;
}
/// <summary>
/// This method is called whenever an actor is deactivated after a period of inactivity.
/// </summary>
protected override Task OnDeactivateAsync()
{
// Provides Opporunity to perform optional cleanup.
Console.WriteLine($"Deactivating actor id: {this.Id}");
return Task.CompletedTask;
}
/// <summary>
/// Set MyData into actor's private state store
/// </summary>
/// <param name="data">the user-defined MyData which will be stored into state store as "my_data" state</param>
public async Task<string> SetDataAsync(MyData data)
{
// Data is saved to configured state store implicitly after each method execution by Actor's runtime.
// Data can also be saved explicitly by calling this.StateManager.SaveStateAsync();
// State to be saved must be DataContract serializable.
await this.StateManager.SetStateAsync<MyData>(
"my_data", // state name
data); // data saved for the named state "my_data"
return "Success";
}
/// <summary>
/// Get MyData from actor's private state store
/// </summary>
/// <return>the user-defined MyData which is stored into state store as "my_data" state</return>
public Task<MyData> GetDataAsync()
{
// Gets state from the state store.
return this.StateManager.GetStateAsync<MyData>("my_data");
}
/// <summary>
/// Register MyReminder reminder with the actor
/// </summary>
public async Task RegisterReminder()
{
await this.RegisterReminderAsync(
"MyReminder", // The name of the reminder
null, // User state passed to IRemindable.ReceiveReminderAsync()
TimeSpan.FromSeconds(5), // Time to delay before invoking the reminder for the first time
TimeSpan.FromSeconds(5)); // Time interval between reminder invocations after the first invocation
}
/// <summary>
/// Get MyReminder reminder details with the actor
/// </summary>
public async Task<IActorReminder> GetReminder()
{
await this.GetReminderAsync("MyReminder");
}
/// <summary>
/// Unregister MyReminder reminder with the actor
/// </summary>
public Task UnregisterReminder()
{
Console.WriteLine("Unregistering MyReminder...");
return this.UnregisterReminderAsync("MyReminder");
}
// <summary>
// Implement IRemindeable.ReceiveReminderAsync() which is call back invoked when an actor reminder is triggered.
// </summary>
public Task ReceiveReminderAsync(string reminderName, byte[] state, TimeSpan dueTime, TimeSpan period)
{
Console.WriteLine("ReceiveReminderAsync is called!");
return Task.CompletedTask;
}
/// <summary>
/// Register MyTimer timer with the actor
/// </summary>
public Task RegisterTimer()
{
return this.RegisterTimerAsync(
"MyTimer", // The name of the timer
nameof(this.OnTimerCallBack), // Timer callback
null, // User state passed to OnTimerCallback()
TimeSpan.FromSeconds(5), // Time to delay before the async callback is first invoked
TimeSpan.FromSeconds(5)); // Time interval between invocations of the async callback
}
/// <summary>
/// Unregister MyTimer timer with the actor
/// </summary>
public Task UnregisterTimer()
{
Console.WriteLine("Unregistering MyTimer...");
return this.UnregisterTimerAsync("MyTimer");
}
/// <summary>
/// Timer callback once timer is expired
/// </summary>
private Task OnTimerCallBack(byte[] data)
{
Console.WriteLine("OnTimerCallBack is called!");
return Task.CompletedTask;
}
}
}
Actor 运行时通过 ASP.NET Core Startup.cs 进行配置。
运行时使用 ASP.NET Core 依赖注入系统来注册 actor 类型和基本服务。此集成通过 ConfigureServices(...) 中的 AddActors(...) 方法调用提供。使用传递给 AddActors(...) 的委托来注册 actor 类型并配置 actor 运行时设置。你可以在 ConfigureServices(...) 内部注册其他类型以进行依赖注入。这些类型将可用于注入到 Actor 类型的构造函数中。
Actors 通过与 Dapr 运行时的 HTTP 调用来实现。此功能是应用程序 HTTP 处理管道的一部分,并在 Configure(...) 内部的 UseEndpoints(...) 中注册。
将以下代码粘贴到 MyActorService 项目的 Startup.cs 中:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace MyActorService
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddActors(options =>
{
// Register actor types and configure actor settings
options.Actors.RegisterActor<MyActor>();
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
// Register actors handlers that interface with the Dapr runtime.
app.MapActorsHandlers();
}
}
}
创建一个简单的控制台应用程序来调用 actor 服务。Dapr SDK 提供 Actor Proxy 客户端来调用 Actor Interface 中定义的 actor 方法。
# Create Actor's Client
dotnet new console -o MyActorClient
cd MyActorClient
# Add Dapr.Actors nuget package. Please use the latest package version from nuget.org
dotnet add package Dapr.Actors
# Add Actor Interface reference
dotnet add reference ../MyActor.Interfaces/MyActor.Interfaces.csproj
cd ..
你可以使用 ActorProxy.Create<IMyActor>(..) 创建强类型客户端并调用 actor 上的方法。
将以下代码粘贴到 MyActorClient 项目的 Program.cs 中:
using System;
using System.Threading.Tasks;
using Dapr.Actors;
using Dapr.Actors.Client;
using MyActor.Interfaces;
namespace MyActorClient
{
class Program
{
static async Task MainAsync(string[] args)
{
Console.WriteLine("Startup up...");
// Registered Actor Type in Actor Service
var actorType = "MyActor";
// An ActorId uniquely identifies an actor instance
// If the actor matching this id does not exist, it will be created
var actorId = new ActorId("1");
// Create the local proxy by using the same interface that the service implements.
//
// You need to provide the type and id so the actor can be located.
var proxy = ActorProxy.Create<IMyActor>(actorId, actorType);
// Now you can use the actor interface to call the actor's methods.
Console.WriteLine($"Calling SetDataAsync on {actorType}:{actorId}...");
var response = await proxy.SetDataAsync(new MyData()
{
PropertyA = "ValueA",
PropertyB = "ValueB",
});
Console.WriteLine($"Got response: {response}");
Console.WriteLine($"Calling GetDataAsync on {actorType}:{actorId}...");
var savedData = await proxy.GetDataAsync();
Console.WriteLine($"Got response: {savedData}");
}
}
}
你现在可以测试你创建的项目。
运行 MyActorService
由于 MyActorService 承载着 actors,它需要使用 Dapr CLI 运行。
cd MyActorService
dapr run --app-id myapp --app-port 5000 --dapr-http-port 3500 -- dotnet run
你将在此终端中看到来自 daprd 和 MyActorService 的命令行输出。你应该会看到类似以下内容,这表明应用程序已成功启动。
...
ℹ️ Updating metadata for app command: dotnet run
✅ You're up and running! Both Dapr and your app logs will appear here.
== APP == info: Microsoft.Hosting.Lifetime[0]
== APP == Now listening on: https://localhost:5001
== APP == info: Microsoft.Hosting.Lifetime[0]
== APP == Now listening on: http://localhost:5000
== APP == info: Microsoft.Hosting.Lifetime[0]
== APP == Application started. Press Ctrl+C to shut down.
== APP == info: Microsoft.Hosting.Lifetime[0]
== APP == Hosting environment: Development
== APP == info: Microsoft.Hosting.Lifetime[0]
== APP == Content root path: /Users/ryan/actortest/MyActorService
运行 MyActorClient
MyActorClient 充当客户端,可以使用 dotnet run 正常运行。
打开一个新终端并导航到 MyActorClient 目录。然后使用以下命令运行项目:
dotnet run
你应该会看到类似以下的命令行输出:
Startup up...
Calling SetDataAsync on MyActor:1...
Got response: Success
Calling GetDataAsync on MyActor:1...
Got response: PropertyA: ValueA, PropertyB: ValueB
💡 此示例依赖于一些假设。ASP.NET Core Web 项目的默认监听端口为 5000,该端口作为
--app-port 5000传递给dapr run。Dapr sidecar 的默认 HTTP 端口为 3500。我们告诉MyActorService的 sidecar 使用 3500,以便MyActorClient可以依赖默认值。
现在你已成功创建了 actor 服务和客户端。请参阅相关链接部分以了解更多信息。