This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Dapr actors .NET SDK

Get up and running with the Dapr actors .NET SDK

With the Dapr actor package, you can interact with Dapr virtual actors from a .NET application.

To get started, walk through the Dapr actors how-to guide.

1 - The IActorProxyFactory interface

Learn how to create actor clients with the IActorProxyFactory interface

Inside of an Actor class or an ASP.NET Core project, the IActorProxyFactory interface is recommended to create actor clients.

The AddActors(...) method will register actor services with ASP.NET Core dependency injection.

  • Outside of an actor instance: The IActorProxyFactory instance is available through dependency injection as a singleton service.
  • Inside an actor instance: The IActorProxyFactory instance is available as a property (this.ProxyFactory).

The following is an example of creating a proxy inside an actor:

public Task<MyData> GetDataAsync()
{
    var proxy = this.ProxyFactory.CreateActorProxy<IOtherActor>(ActorId.CreateRandom(), "OtherActor");
    await proxy.DoSomethingGreat();

    return this.StateManager.GetStateAsync<MyData>("my_data");
}

In this guide, you will learn how to use IActorProxyFactory.

Identifying an actor

All of the APIs on IActorProxyFactory will require an actor type and actor id to communicate with an actor. For strongly-typed clients, you also need one of its interfaces.

  • Actor type uniquely identifies the actor implementation across the whole application.
  • Actor id uniquely identifies an instance of that type.

If you don’t have an actor id and want to communicate with a new instance, create a random id with ActorId.CreateRandom(). Since the random id is a cryptographically strong identifier, the runtime will create a new actor instance when you interact with it.

You can use the type ActorReference to exchange an actor type and actor id with other actors as part of messages.

Two styles of actor client

The actor client supports two different styles of invocation:

Actor client style Description
Strongly-typed Strongly-typed clients are based on .NET interfaces and provide the typical benefits of strong-typing. They don’t work with non-.NET actors.
Weakly-typed Weakly-typed clients use the ActorProxy class. It is recommended to use these only when required for interop or other advanced reasons.

Using a strongly-typed client

The following example uses the CreateActorProxy<> method to create a strongly-typed client. CreateActorProxy<> requires an actor interface type, and will return an instance of that interface.

// Create a proxy for IOtherActor to type OtherActor with a random id
var proxy = this.ProxyFactory.CreateActorProxy<IOtherActor>(ActorId.CreateRandom(), "OtherActor");

// Invoke a method defined by the interface to invoke the actor
//
// proxy is an implementation of IOtherActor so we can invoke its methods directly
await proxy.DoSomethingGreat();

Using a weakly-typed client

The following example uses the Create method to create a weakly-typed client. Create returns an instance of ActorProxy.

// Create a proxy for type OtherActor with a random id
var proxy = this.ProxyFactory.Create(ActorId.CreateRandom(), "OtherActor");

// Invoke a method by name to invoke the actor
//
// proxy is an instance of ActorProxy.
await proxy.InvokeMethodAsync("DoSomethingGreat");

Since ActorProxy is a weakly-typed proxy, you need to pass in the actor method name as a string.

You can also use ActorProxy to invoke methods with both a request and a response message. Request and response messages will be serialized using the System.Text.Json serializer.

// Create a proxy for type OtherActor with a random id
var proxy = this.ProxyFactory.Create(ActorId.CreateRandom(), "OtherActor");

// Invoke a method on the proxy to invoke the actor
//
// proxy is an instance of ActorProxy.
var request = new MyRequest() { Message = "Hi, it's me.", };
var response = await proxy.InvokeMethodAsync<MyRequest, MyResponse>("DoSomethingGreat", request);

When using a weakly-typed proxy, you must proactively define the correct actor method names and message types. When using a strongly-typed proxy, these names and types are defined for you as part of the interface definition.

Actor method invocation exception details

The actor method invocation exception details are surfaced to the caller and the callee, providing an entry point to track down the issue. Exception details include:

  • Method name
  • Line number
  • Exception type
  • UUID

You use the UUID to match the exception on the caller and callee side. Below is an example of exception details:

Dapr.Actors.ActorMethodInvocationException: Remote Actor Method Exception, DETAILS: Exception: NotImplementedException, Method Name: ExceptionExample, Line Number: 14, Exception uuid: d291a006-84d5-42c4-b39e-d6300e9ac38b

Next steps

Learn how to author and run actors with ActorHost.

2 - Author & run actors

Learn all about authoring and running actors with the .NET SDK

Author actors

ActorHost

The ActorHost:

  • Is a required constructor parameter of all actors
  • Is provided by the runtime
  • Must be passed to the base class constructor
  • Contains all of the state that allows that actor instance to communicate with the runtime
internal class MyActor : Actor, IMyActor, IRemindable
{
    public MyActor(ActorHost host) // Accept ActorHost in the constructor
        : base(host) // Pass ActorHost to the base class constructor
    {
    }
}

Since the ActorHost contains state unique to the actor, you don’t need to pass the instance into other parts of your code. It’s recommended only create your own instances of ActorHost in tests.

Dependency injection

Actors support dependency injection of additional parameters into the constructor. Any other parameters you define will have their values satisfied from the dependency injection container.

internal class MyActor : Actor, IMyActor, IRemindable
{
    public MyActor(ActorHost host, BankService bank) // Accept BankService in the constructor
        : base(host)
    {
        ...
    }
}

An actor type should have a single public constructor. The actor infrastructure uses the ActivatorUtilities pattern for constructing actor instances.

You can register types with dependency injection in Startup.cs to make them available. Read more about the different ways of registering your types.

// In Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    ...

    // Register additional types with dependency injection.
    services.AddSingleton<BankService>();
}

Each actor instance has its own dependency injection scope and remains in memory for some time after performing an operation. During that time, the dependency injection scope associated with the actor is also considered live. The scope will be released when the actor is deactivated.

If an actor injects an IServiceProvider in the constructor, the actor will receive a reference to the IServiceProvider associated with its scope. The IServiceProvider can be used to resolve services dynamically in the future.

internal class MyActor : Actor, IMyActor, IRemindable
{
    public MyActor(ActorHost host, IServiceProvider services) // Accept IServiceProvider in the constructor
        : base(host)
    {
        ...
    }
}

When using this pattern, avoid creating many instances of transient services which implement IDisposable. Since the scope associated with an actor could be considered valid for a long time, you can accumulate many services in memory. See the dependency injection guidelines for more information.

IDisposable and actors

Actors can implement IDisposable or IAsyncDisposable. It’s recommended that you rely on dependency injection for resource management rather than implementing dispose functionality in application code. Dispose support is provided in the rare case where it is truly necessary.

Logging

Inside an actor class, you have access to an ILogger instance through a property on the base Actor class. This instance is connected to the ASP.NET Core logging system and should be used for all logging inside an actor. Read more about logging. You can configure a variety of different logging formats and output sinks.

Use structured logging with named placeholders like the example below:

public Task<MyData> GetDataAsync()
{
    this.Logger.LogInformation("Getting state at {CurrentTime}", DateTime.UtcNow);
    return this.StateManager.GetStateAsync<MyData>("my_data");
}

When logging, avoid using format strings like: $"Getting state at {DateTime.UtcNow}"

Logging should use the named placeholder syntax which offers better performance and integration with logging systems.

Using an explicit actor type name

By default, the type of the actor, as seen by clients, is derived from the name of the actor implementation class. The default name will be the class name (without namespace).

If desired, you can specify an explicit type name by attaching an ActorAttribute attribute to the actor implementation class.

[Actor(TypeName = "MyCustomActorTypeName")]
internal class MyActor : Actor, IMyActor
{
    // ...
}

In the example above, the name will be MyCustomActorTypeName.

No change is needed to the code that registers the actor type with the runtime, providing the value via the attribute is all that is required.

Host actors on the server

Registering actors

Actor registration is part of ConfigureServices in Startup.cs. You can register services with dependency injection via the ConfigureServices method. Registering the set of actor types is part of the registration of actor services.

Inside ConfigureServices you can:

  • Register the actor runtime (AddActors)
  • Register actor types (options.Actors.RegisterActor<>)
  • Configure actor runtime settings options
  • Register additional service types for dependency injection into actors (services)
// 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>();
}

Configuring JSON options

The actor runtime uses System.Text.Json for:

  • Serializing data to the state store
  • Handling requests from the weakly-typed client

By default, the actor runtime uses settings based on JsonSerializerDefaults.Web.

You can configure the JsonSerializerOptions as part of ConfigureServices:

// In Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    services.AddActors(options =>
    {
        ...
        
        // Customize JSON options
        options.JsonSerializerOptions = ...
    });
}

Actors and routing

The ASP.NET Core hosting support for actors uses the endpoint routing system. The .NET SDK provides no support hosting actors with the legacy routing system from early ASP.NET Core releases.

Since actors uses endpoint routing, the actors HTTP handler is part of the middleware pipeline. The following is a minimal example of a Configure method setting up the middleware pipeline with actors.

// 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();
    });
}

The UseRouting and UseEndpoints calls are necessary to configure routing. Configure actors as part of the pipeline by adding MapActorsHandlers inside the endpoint middleware.

This is a minimal example, it’s valid for Actors functionality to existing alongside:

  • Controllers
  • Razor Pages
  • Blazor
  • gRPC Services
  • Dapr pub/sub handler
  • other endpoints such as health checks

Problematic middleware

Certain middleware may interfere with the routing of Dapr requests to the actors handlers. In particular, the UseHttpsRedirection is problematic for Dapr’s default configuration. Dapr sends requests over unencrypted HTTP by default, which the UseHttpsRedirection middleware will block. This middleware cannot be used with Dapr at this time.

// 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();
    });
}

Next steps

Try the Running and using virtual actors example.

3 - Actor serialization in the .NET SDK

Necessary steps to serialize your types using remoted Actors in .NET

Actor Serialization

The Dapr actor package enables you to use Dapr virtual actors within a .NET application with either a weakly- or strongly-typed client. Each utilizes a different serialization approach. This document will review the differences and convey a few key ground rules to understand in either scenario.

Please be advised that it is not a supported scenario to use the weakly- or strongly typed actor clients interchangeably because of these different serialization approaches. The data persisted using one Actor client will not be accessible using the other Actor client, so it is important to pick one and use it consistently throughout your application.

Weakly-typed Dapr Actor client

In this section, you will learn how to configure your C# types so they are properly serialized and deserialized at runtime when using a weakly-typed actor client. These clients use string-based names of methods with request and response payloads that are serialized using the System.Text.Json serializer. Please note that this serialization framework is not specific to Dapr and is separately maintained by the .NET team within the .NET GitHub repository.

When using the weakly-typed Dapr Actor client to invoke methods from your various actors, it’s not necessary to independently serialize or deserialize the method payloads as this will happen transparently on your behalf by the SDK.

The client will use the latest version of System.Text.Json available for the version of .NET you’re building against and serialization is subject to all the inherent capabilities provided in the associated .NET documentation.

The serializer will be configured to use the JsonSerializerOptions.Web default options unless overridden with a custom options configuration which means the following are applied:

  • Deserialization of the property name is performed in a case-insensitive manner
  • Serialization of the property name is performed using camel casing unless the property is overridden with a [JsonPropertyName] attribute
  • Deserialization will read numeric values from number and/or string values

Basic Serialization

In the following example, we present a simple class named Doodad though it could just as well be a record as well.

public class Doodad
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public int Count { get; set; }
}

By default, this will serialize using the names of the members as used in the type and whatever values it was instantiated with:

{"id": "a06ced64-4f42-48ad-84dd-46ae6a7e333d", "name": "DoodadName", "count": 5}

Override Serialized Property Name

The default property names can be overridden by applying the [JsonPropertyName] attribute to desired properties.

Generally, this isn’t going to be necessary for types you’re persisting to the actor state as you’re not intended to read or write them independent of Dapr-associated functionality, but the following is provided just to clearly illustrate that it’s possible.

Override Property Names on Classes

Here’s an example demonstrating the use of JsonPropertyName to change the name for the first property following serialization. Note that the last usage of JsonPropertyName on the Count property matches what it would be expected to serialize to. This is largely just to demonstrate that applying this attribute won’t negatively impact anything - in fact, it might be preferable if you later decide to change the default serialization options but still need to consistently access the properties previously serialized before that change as JsonPropertyName will override those options.

public class Doodad
{
    [JsonPropertyName("identifier")]
    public Guid Id { get; set; }
    public string Name { get; set; }
    [JsonPropertyName("count")]
    public int Count { get; set; }
}

This would serialize to the following:

{"identifier": "a06ced64-4f42-48ad-84dd-46ae6a7e333d", "name": "DoodadName", "count": 5}

Override Property Names on Records

Let’s try doing the same thing with a record from C# 12 or later:

public record Thingy(string Name, [JsonPropertyName("count")] int Count); 

Because the argument passed in a primary constructor (introduced in C# 12) can be applied to either a property or field within a record, using the [JsonPropertyName] attribute may require specifying that you intend the attribute to apply to a property and not a field in some ambiguous cases. Should this be necessary, you’d indicate as much in the primary constructor with:

public record Thingy(string Name, [property: JsonPropertyName("count")] int Count);

If [property: ] is applied to the [JsonPropertyName] attribute where it’s not necessary, it will not negatively impact serialization or deserialization as the operation will proceed normally as though it were a property (as it typically would if not marked as such).

Enumeration types

Enumerations, including flat enumerations are serializable to JSON, but the value persisted may surprise you. Again, it’s not expected that the developer should ever engage with the serialized data independently of Dapr, but the following information may at least help in diagnosing why a seemingly mild version migration isn’t working as expected.

Take the following enum type providing the various seasons in the year:

public enum Season
{
    Spring,
    Summer,
    Fall,
    Winter
}

We’ll go ahead and use a separate demonstration type that references our Season and simultaneously illustrate how this works with records:

public record Engagement(string Name, Season TimeOfYear);

Given the following initialized instance:

var myEngagement = new Engagement("Ski Trip", Season.Winter);

This would serialize to the following JSON:

{"name":  "Ski Trip", "season":  3}

That might be unexpected that our Season.Winter value was represented as a 3, but this is because the serializer is going to automatically use numeric representations of the enum values starting with zero for the first value and incrementing the numeric value for each additional value available. Again, if a migration were taking place and a developer had flipped the order of the enums, this would affect a breaking change in your solution as the serialized numeric values would point to different values when deserialized.

Rather, there is a JsonConverter available with System.Text.Json that will instead opt to use a string-based value instead of the numeric value. The [JsonConverter] attribute needs to be applied to be enum type itself to enable this, but will then be realized in any downstream serialization or deserialization operation that references the enum.

[JsonConverter(typeof(JsonStringEnumConverter<Season>))]
public enum Season
{
    Spring,
    Summer,
    Fall,
    Winter
}

Using the same values from our myEngagement instance above, this would produce the following JSON instead:

{"name":  "Ski Trip", "season":  "Winter"}

As a result, the enum members can be shifted around without fear of introducing errors during deserialization.

Custom Enumeration Values

The System.Text.Json serialization platform doesn’t, out of the box, support the use of [EnumMember] to allow you to change the value of enum that’s used during serialization or deserialization, but there are scenarios where this could be useful. Again, assume that you’re tasking with refactoring the solution to apply some better names to your various enums. You’re using the JsonStringEnumConverter<TType> detailed above so you’re saving the name of the enum to value instead of a numeric value, but if you change the enum name, that will introduce a breaking change as the name will no longer match what’s in state.

Do note that if you opt into using this approach, you should decorate all your enum members with the [EnumMeber] attribute so that the values are consistently applied for each enum value instead of haphazardly. Nothing will validate this at build or runtime, but it is considered a best practice operation.

How can you specify the precise value persisted while still changing the name of the enum member in this scenario? Use a custom JsonConverter with an extension method that can pull the value out of the attached [EnumMember] attributes where provided. Add the following to your solution:

public sealed class EnumMemberJsonConverter<T> : JsonConverter<T> where T : struct, Enum
{
    /// <summary>Reads and converts the JSON to type <typeparamref name="T" />.</summary>
    /// <param name="reader">The reader.</param>
    /// <param name="typeToConvert">The type to convert.</param>
    /// <param name="options">An object that specifies serialization options to use.</param>
    /// <returns>The converted value.</returns>
    public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        // Get the string value from the JSON reader
        var value = reader.GetString();

        // Loop through all the enum values
        foreach (var enumValue in Enum.GetValues<T>())
        {
            // Get the value from the EnumMember attribute, if any
            var enumMemberValue = GetValueFromEnumMember(enumValue);

            // If the values match, return the enum value
            if (value == enumMemberValue)
            {
                return enumValue;
            }
        }

        // If no match found, throw an exception
        throw new JsonException($"Invalid value for {typeToConvert.Name}: {value}");
    }

    /// <summary>Writes a specified value as JSON.</summary>
    /// <param name="writer">The writer to write to.</param>
    /// <param name="value">The value to convert to JSON.</param>
    /// <param name="options">An object that specifies serialization options to use.</param>
    public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
    {
        // Get the value from the EnumMember attribute, if any
        var enumMemberValue = GetValueFromEnumMember(value);

        // Write the value to the JSON writer
        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();
    }
}

Now let’s add a sample enumerator. We’ll set a value that uses the lower-case version of each enum member to demonstrate this. Don’t forget to decorate the enum with the JsonConverter attribute and reference our custom converter in place of the numeral-to-string converter used in the last section.

[JsonConverter(typeof(EnumMemberJsonConverter<Season>))]
public enum Season
{
    [EnumMember(Value="spring")]
    Spring,
    [EnumMember(Value="summer")]
    Summer,
    [EnumMember(Value="fall")]
    Fall,
    [EnumMember(Value="winter")]
    Winter
}

Let’s use our sample record from before. We’ll also add a [JsonPropertyName] attribute just to augment the demonstration:

public record Engagement([property: JsonPropertyName("event")] string Name, Season TimeOfYear);

And finally, let’s initialize a new instance of this:

var myEngagement = new Engagement("Conference", Season.Fall);

This time, serialization will take into account the values from the attached [EnumMember] attribute providing us a mechanism to refactor our application without necessitating a complex versioning scheme for our existing enum values in the state.

{"event":  "Conference",  "season":  "fall"}

Strongly-typed Dapr Actor client

In this section, you will learn how to configure your classes and records so they are properly serialized and deserialized at runtime when using a strongly-typed actor client. These clients are implemented using .NET interfaces and are not compatible with Dapr Actors written using other languages.

This actor client serializes data using an engine called the Data Contract Serializer which converts your C# types to and from XML documents. This serialization framework is not specific to Dapr and is separately maintained by the .NET team within the .NET GitHub repository.

When sending or receiving primitives (like strings or ints), this serialization happens transparently and there’s no requisite preparation needed on your part. However, when working with complex types such as those you create, there are some important rules to take into consideration so this process works smoothly.

Serializable Types

There are several important considerations to keep in mind when using the Data Contract Serializer:

  • By default, all types, read/write properties (after construction) and fields marked as publicly visible are serialized
  • All types must either expose a public parameterless constructor or be decorated with the DataContractAttribute attribute
  • Init-only setters are only supported with the use of the DataContractAttribute attribute
  • Read-only fields, properties without a Get and Set method and internal or properties with private Get and Set methods are ignored during serialization
  • Serialization is supported for types that use other complex types that are not themselves marked with the DataContractAttribute attribute through the use of the KnownTypesAttribute attribute
  • If a type is marked with the DataContractAttribute attribute, all members you wish to serialize and deserialize must be decorated with the DataMemberAttribute attribute as well or they’ll be set to their default values

How does deserialization work?

The approach used for deserialization depends on whether or not the type is decorated with the DataContractAttribute attribute. If this attribute isn’t present, an instance of the type is created using the parameterless constructor. Each of the properties and fields are then mapped into the type using their respective setters and the instance is returned to the caller.

If the type is marked with [DataContract], the serializer instead uses reflection to read the metadata of the type and determine which properties or fields should be included based on whether or not they’re marked with the DataMemberAttribute attribute as it’s performed on an opt-in basis. It then allocates an uninitialized object in memory (avoiding the use of any constructors, parameterless or not) and then sets the value directly on each mapped property or field, even if private or uses init-only setters. Serialization callbacks are invoked as applicable throughout this process and then the object is returned to the caller.

Use of the serialization attributes is highly recommended as they grant more flexibility to override names and namespaces and generally use more of the modern C# functionality. While the default serializer can be relied on for primitive types, it’s not recommended for any of your own types, whether they be classes, structs or records. It’s recommended that if you decorate a type with the DataContractAttribute attribute, you also explicitly decorate each of the members you want to serialize or deserialize with the DataMemberAttribute attribute as well.

.NET Classes

Classes are fully supported in the Data Contract Serializer provided that that other rules detailed on this page and the Data Contract Serializer documentation are also followed.

The most important thing to remember here is that you must either have a public parameterless constructor or you must decorate it with the appropriate attributes. Let’s review some examples to really clarify what will and won’t work.

In the following example, we present a simple class named Doodad. We don’t provide an explicit constructor here, so the compiler will provide an default parameterless constructor. Because we’re using supported primitive types (Guid, string and int32) and all our members have a public getter and setter, no attributes are required and we’ll be able to use this class without issue when sending and receiving it from a Dapr actor method.

public class Doodad
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public int Count { get; set; }
}

By default, this will serialize using the names of the members as used in the type and whatever values it was instantiated with:

<Doodad>
  <Id>a06ced64-4f42-48ad-84dd-46ae6a7e333d</Id>
  <Name>DoodadName</Name>
  <Count>5</Count>
</Doodad>

So let’s tweak it - let’s add our own constructor and only use init-only setters on the members. This will fail to serialize and deserialize not because of the use of the init-only setters, but because there’s no parameterless constructors.

// WILL NOT SERIALIZE PROPERLY!
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; }
}

If we add a public parameterless constructor to the type, we’re good to go and this will work without further annotations.

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; }
}

But what if we don’t want to add this constructor? Perhaps you don’t want your developers to accidentally create an instance of this Doodad using an unintended constructor. That’s where the more flexible attributes are useful. If you decorate your type with a DataContractAttribute attribute, you can drop your parameterless constructor and it will work once again.

[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; }
}

In the above example, we don’t need to also use the DataMemberAttribute attributes because again, we’re using built-in primitives that the serializer supports. But, we do get more flexibility if we use the attributes. From the DataContractAttribute attribute, we can specify our own XML namespace with the Namespace argument and, via the Name argument, change the name of the type as used when serialized into the XML document.

It’s a recommended practice to append the DataContractAttribute attribute to the type and the DataMemberAttribute attributes to all the members you want to serialize anyway - if they’re not necessary and you’re not changing the default values, they’ll just be ignored, but they give you a mechanism to opt into serializing members that wouldn’t otherwise have been included such as those marked as private or that are themselves complex types or collections.

Note that if you do opt into serializing your private members, their values will be serialized into plain text - they can very well be viewed, intercepted and potentially manipulated based on how you’re handing the data once serialized, so it’s an important consideration whether you want to mark these members or not in your use case.

In the following example, we’ll look at using the attributes to change the serialized names of some of the members as well as introduce the IgnoreDataMemberAttribute attribute. As the name indicates, this tells the serializer to skip this property even though it’d be otherwise eligible to serialize. Further, because I’m decorating the type with the DataContractAttribute attribute, it means that I can use init-only setters on the properties.

[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; }
}

When this is serialized, because we’re changing the names of the serialized members, we can expect a new instance of Doodad using the default values this to be serialized as:

<Doodad>
  <id>a06ced64-4f42-48ad-84dd-46ae6a7e333d</id>
  <Count>5</Count>
</Doodad>
Classes in C# 12 - Primary Constructors

C# 12 brought us primary constructors on classes. Use of a primary constructor means the compiler will be prevented from creating the default implicit parameterless constructor. While a primary constructor on a class doesn’t generate any public properties, it does mean that if you pass this primary constructor any arguments or have non-primitive types in your class, you’ll either need to specify your own parameterless constructor or use the serialization attributes.

Here’s an example where we’re using the primary constructor to inject an ILogger to a field and add our own parameterless constructor without the need for any attributes.

public class Doodad(ILogger<Doodad> _logger)
{
    public Doodad() {} //Our parameterless constructor

    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; } 
}

And using our serialization attributes (again, opting for init-only setters since we’re using the serialization attributes):

[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; }
}

.NET Structs

Structs are supported by the Data Contract serializer provided that they are marked with the DataContractAttribute attribute and the members you wish to serialize are marked with the DataMemberAttribute attribute. Further, to support deserialization, the struct will also need to have a parameterless constructor. This works even if you define your own parameterless constructor as enabled in C# 10.

[DataContract]
public struct Doodad
{
    [DataMember]
    public int Count { get; set; }
}

.NET Records

Records were introduced in C# 9 and follow precisely the same rules as classes when it comes to serialization. We recommend that you should decorate all your records with the DataContractAttribute attribute and members you wish to serialize with DataMemberAttribute attributes so you don’t experience any deserialization issues using this or other newer C# functionalities. Because record classes use init-only setters for properties by default and encourage the use of the primary constructor, applying these attributes to your types ensures that the serializer can properly otherwise accommodate your types as-is.

Typically records are presented as a simple one-line statement using the new primary constructor concept:

public record Doodad(Guid Id, string Name, int Count);

This will throw an error encouraging the use of the serialization attributes as soon as you use it in a Dapr actor method invocation because there’s no parameterless constructor available nor is it decorated with the aforementioned attributes.

Here we add an explicit parameterless constructor and it won’t throw an error, but none of the values will be set during deserialization since they’re created with init-only setters. Because this doesn’t use the DataContractAttribute attribute or the DataMemberAttribute attribute on any members, the serializer will be unable to map the target members correctly during deserialization.

public record Doodad(Guid Id, string Name, int Count)
{
    public Doodad() {}
}

This approach does without the additional constructor and instead relies on the serialization attributes. Because we mark the type with the DataContractAttribute attribute and decorate each member with its own DataMemberAttribute attribute, the serialization engine will be able to map from the XML document to our type without issue.

[DataContract]
public record Doodad(
        [property: DataMember] Guid Id,
        [property: DataMember] string Name,
        [property: DataMember] int Count)

Supported Primitive Types

There are several types built into .NET that are considered primitive and eligible for serialization without additional effort on the part of the developer:

There are additional types that aren’t actually primitives but have similar built-in support:

Again, if you want to pass these types around via your actor methods, no additional consideration is necessary as they’ll be serialized and deserialized without issue. Further, types that are themselves marked with the (SerializeableAttribute)[https://learn.microsoft.com/en-us/dotnet/api/system.serializableattribute] attribute will be serialized.

Enumeration Types

Enumerations, including flag enumerations are serializable if appropriately marked. The enum members you wish to be serialized must be marked with the EnumMemberAttribute attribute in order to be serialized. Passing a custom value into the optional Value argument on this attribute will allow you to specify the value used for the member in the serialized document instead of having the serializer derive it from the name of the member.

The enum type does not require that the type be decorated with the DataContractAttribute attribute - only that the members you wish to serialize be decorated with the EnumMemberAttribute attributes.

public enum Colors
{
    [EnumMember]
    Red,
    [EnumMember(Value="g")]
    Green,
    Blue, //Even if used by a type, this value will not be serialized as it's not decorated with the EnumMember attribute
}

Collection Types

With regards to the data contact serializer, all collection types that implement the IEnumerable interface including arays and generic collections are considered collections. Those types that implement IDictionary or the generic IDictionary<TKey, TValue> are considered dictionary collections; all others are list collections.

Not unlike other complex types, collection types must have a parameterless constructor available. Further, they must also have a method called Add so they can be properly serialized and deserialized. The types used by these collection types must themselves be marked with the DataContractAttribute attribute or otherwise be serializable as described throughout this document.

Data Contract Versioning

As the data contract serializer is only used in Dapr with respect to serializing the values in the .NET SDK to and from the Dapr actor instances via the proxy methods, there’s little need to consider versioning of data contracts as the data isn’t being persisted between application versions using the same serializer. For those interested in learning more about data contract versioning visit here.

Known Types

Nesting your own complex types is easily accommodated by marking each of the types with the DataContractAttribute attribute. This informs the serializer as to how deserialization should be performed. But what if you’re working with polymorphic types and one of your members is a base class or interface with derived classes or other implementations? Here, you’ll use the KnownTypeAttribute attribute to give a hint to the serializer about how to proceed.

When you apply the KnownTypeAttribute attribute to a type, you are informing the data contract serializer about what subtypes it might encounter allowing it to properly handle the serialization and deserialization of these types, even when the actual type at runtime is different from the declared type.

[DataContract]
[KnownType(typeof(DerivedClass))]
public class BaseClass
{
    //Members of the base class
}

[DataContract]
public class DerivedClass : BaseClass 
{
    //Additional members of the derived class
}

In this example, the BaseClass is marked with [KnownType(typeof(DerivedClass))] which tells the data contract serializer that DerivedClass is a possible implementation of BaseClass that it may need to serialize or deserialize. Without this attribute, the serialize would not be aware of the DerivedClass when it encounters an instance of BaseClass that is actually of type DerivedClass and this could lead to a serialization exception because the serializer would not know how to handle the derived type. By specifying all possible derived types as known types, you ensure that the serializer can process the type and its members correctly.

For more information and examples about using [KnownType], please refer to the official documentation.

4 - How to: Run and use virtual actors in the .NET SDK

Try out .NET Dapr virtual actors with this example

The Dapr actor package allows you to interact with Dapr virtual actors from a .NET application. In this guide, you learn how to:

  • Create an Actor (MyActor).
  • Invoke its methods on the client application.
MyActor --- MyActor.Interfaces
         |
         +- MyActorService
         |
         +- MyActorClient

The interface project (\MyActor\MyActor.Interfaces)

This project contains the interface definition for the actor. Actor interfaces can be defined in any project with any name. The interface defines the actor contract shared by:

  • The actor implementation
  • The clients calling the actor

Because client projects may depend on it, it’s better to define it in an assembly separate from the actor implementation.

The actor service project (\MyActor\MyActorService)

This project implements the ASP.Net Core web service that hosts the actor. It contains the implementation of the actor, MyActor.cs. An actor implementation is a class that:

  • Derives from the base type Actor
  • Implements the interfaces defined in the MyActor.Interfaces project.

An actor class must also implement a constructor that accepts an ActorService instance and an ActorId, and passes them to the base Actor class.

The actor client project (\MyActor\MyActorClient)

This project contains the implementation of the actor client which calls MyActor’s method defined in Actor Interfaces.

Prerequisites

Step 0: Prepare

Since we’ll be creating 3 projects, choose an empty directory to start from, and open it in your terminal of choice.

Step 1: Create actor interfaces

Actor interface defines the actor contract that is shared by the actor implementation and the clients calling the actor.

Actor interface is defined with the below requirements:

  • Actor interface must inherit Dapr.Actors.IActor interface
  • The return type of Actor method must be Task or Task<object>
  • Actor method can have one argument at a maximum

Create interface project and add dependencies

# 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 ..

Implement IMyActor interface

Define IMyActor interface and MyData data object. Paste the following code into MyActor.cs in the MyActor.Interfaces project.

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}";
        }
    }
}

Step 2: Create actor service

Dapr uses ASP.NET web service to host Actor service. This section will implement IMyActor actor interface and register Actor to Dapr Runtime.

Create actor service project and add dependencies

# 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 ..

Add actor implementation

Implement IMyActor interface and derive from Dapr.Actors.Actor class. Following example shows how to use Actor Reminders as well. For Actors to use Reminders, it must derive from IRemindable. If you don’t intend to use Reminder feature, you can skip implementing IRemindable and reminder specific methods which are shown in the code below.

Paste the following code into MyActor.cs in the MyActorService project:

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;
        }
    }
}

Register actor runtime with ASP.NET Core startup

The Actor runtime is configured through ASP.NET Core Startup.cs.

The runtime uses the ASP.NET Core dependency injection system to register actor types and essential services. This integration is provided through the AddActors(...) method call in ConfigureServices(...). Use the delegate passed to AddActors(...) to register actor types and configure actor runtime settings. You can register additional types for dependency injection inside ConfigureServices(...). These will be available to be injected into the constructors of your Actor types.

Actors are implemented via HTTP calls with the Dapr runtime. This functionality is part of the application’s HTTP processing pipeline and is registered inside UseEndpoints(...) inside Configure(...).

Paste the following code into Startup.cs in the MyActorService project:

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();
        }
    }
}

Step 3: Add a client

Create a simple console app to call the actor service. Dapr SDK provides Actor Proxy client to invoke actor methods defined in Actor Interface.

Create actor client project and add dependencies

# 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 ..

Invoke actor methods with strongly-typed client

You can use ActorProxy.Create<IMyActor>(..) to create a strongly-typed client and invoke methods on the actor.

Paste the following code into Program.cs in the MyActorClient project:

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}");
        }
    }
}

Running the code

The projects that you’ve created can now to test the sample.

  1. Run MyActorService

    Since MyActorService is hosting actors, it needs to be run with the Dapr CLI.

    cd MyActorService
    dapr run --app-id myapp --app-port 5000 --dapr-http-port 3500 -- dotnet run
    

    You will see commandline output from both daprd and MyActorService in this terminal. You should see something like the following, which indicates that the application started successfully.

    ...
    ℹ️  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
    
  2. Run MyActorClient

    MyActorClient is acting as the client, and it can be run normally with dotnet run.

    Open a new terminal an navigate to the MyActorClient directory. Then run the project with:

    dotnet run
    

    You should see commandline output like:

    Startup up...
    Calling SetDataAsync on MyActor:1...
    Got response: Success
    Calling GetDataAsync on MyActor:1...
    Got response: PropertyA: ValueA, PropertyB: ValueB
    

💡 This sample relies on a few assumptions. The default listening port for an ASP.NET Core web project is 5000, which is being passed to dapr run as --app-port 5000. The default HTTP port for the Dapr sidecar is 3500. We’re telling the sidecar for MyActorService to use 3500 so that MyActorClient can rely on the default value.

Now you have successfully created an actor service and client. See the related links section to learn more.