Originally posted on https://samueleresca.net
The following article describes how to developing APIs using Actor model in ASP.NET Core. It will show some benefits of building APIs using the actor model pattern and some good reasons for adopting Orleans.NET as actor model framework. This article has been written in collaboration with @francomelandri.
When we think about services, we usually refer to the traditional stateless 3-tier architecture which is composed by a front-end (or multiple clients), stateless middle tier which exposes data and the storage layer. The limited scalability of data layer, which has to be consulted every request, it is usually avoided by using a cache layer between the middle tier and the data layer. However, cache layer is not the best choice in terms of scalability and concurrency. Sometimes it is necessary a cache manager which can handle all concurrency problems.
The actor model in computer science is a mathematical model of concurrent computation that treats "actors" as the universal primitives of concurrent computation. In response to a message that it receives, an actor can: make local decisions, create more actors, send more messages, and determine how to respond to the next message received. Actors may modify their own private state, but can only affect each other through messages (avoiding the need for any locks).
Actors isolation become a point of strength when you are designing a distributed and high concurrency system. Actor model has gained a lot of popularity over years, therefore some frameworks born with the propose to abstract the actor model principle, for example: Akka.NET. Akka.NET exploits the actor model to provide a level of abstraction that makes it easier to write correct concurrent, parallel and distributed systems. The actor model spans the set of Akka libraries, providing you with a consistent way of understanding and using them.
Orleans concept is similar to Akka, since Akka.NET still burden developers with many distributed system complexities, Orleans approach is to provide an higher-level of abstraction over that pattern.
The following code shows an actor interface declaration in Orleans:
- Perpetual existence means that actors (aka Grains) always exists. Developers does not have the explicit possibility to create or destroy an actor;
- Automatic instantiation: the runtime automatically create in-memory instances, which are called activations;
- Location transparency: actors instantiation is similar to virtual memory. Consumer does not know where they are stored, all that part is managed by the framework;
- Auto scale-out: Orleans manages grains also in terms of horizontal-scaling;
Actor communicate by using asyncronous messages. Orleans runtime exposes async messages as instances of the class
System.Threading.Tasks.Task, although the asyncronous process of messages, actor activations are single threaded. While Orleans may execute turns of different activations in parallel, each activations always executes one turn at time.
In Orleans programming model, implementing interfaces is the only way to declare a grain, Let's take a look to the following example:
ValueGrain through the use of the
ValueController calls the grain by referring the interface
IValueGrain, since the
IValueGrain interface implements the
IGrainWithInteger interface, the client will pass an integer in order to activate our grain. Another key point is that the result is always exposed as
Task(of T) or
In order to send some requests to grains, the client should be configured via a
ClientBuilder. The following code shows an example of client configuration:
UseLocalhostClusteringdefines some key information about Orleans clustering. It is possible to override some default info: the
ClusterIdis a unique ID for the Orleans cluster. All clients and silo that uses this ID will be able to directly talk to each other. The
ServiceIdis the unique ID for your application, that will be used by some provider, for example, the persistence provider;
line 21: The
IClusterClientinterface is intialized as singleton, therefore It will present only a single instance of the client for many grains;
line 37: we execute the client connection by using the
Clients aren't enough to setup an actor model application. Grains live in cluster of server or processes, which are called Silos. So, in order to activate grains, we should start a cluster of silos. Silo configuration is, in broad terms, specular to
ClientConfiguration. Hence, Orleans exposes the
SiloHostBuilder through the nuget package
Microsoft.Orleans.Server. Let's take a look to silos initialization:
UseLocalhostClusteringconfigures the silo to use development-only clustering and listen on localhost. It defines same info already present in client;
ConfigureLogging: configure the logging provider. It may called mutliple times;
UseDashboard: configure the monitoring dashboard;
Orleans is a product by Microsoft Research, it was released in 2014. Since Orleans has very different approach than classical N-tier applications, Microsoft research also released a best practices document.
First of all, you should consider Orleans when you have significant number of loosely coupled grains. Secondly, grains must be small, therefore large grains with bulk operations does not give any advantages in terms of performance. Finally, grains must be isolated, since a huge number of accesses between entities causes performances degradations.
In order to understand the Orleans cluster management, you should consider some key characteristic. First of all, the framework automatically manages clusters lifecycle, hence failures are the norm and can happen any time.
Grain persistence technique helps to store grain informations inside a persistence storage. The Grain persistence can use different out-of-box providers, for example: Azure table or ADO.NET. Orleans also allows to extend and implement custom providers, hence you can connect your grains with not supported data sources.
The ADO .NET Grain Storage provider allows you to store grain state in relational databases. Here is a list of supported databases: SQL Server, MySQL/MariaDB, PostgreSQL, Oracle; In order to implement the state persistence into our project we should proceed with following steps:
- Add the following package
Install-Package Microsoft.Orleans.Persistence.AdoNetto your Silo project;
- Add database specific vendor package to your Silo project. For example, SQLServer corresponding package is
System.Data.SqlClient. You can find a complete list of packages here;
- Execute SQL scripts for the supported database vendors, which are copied to project directory
\OrleansAdoNetContentwhere each of supported ADO.NET extensions has its own directory;
Finally, we need to modify the
Program.cs file of Silo in order to setup a connection between the our silo and database:
AddAdoNetGrainSorage creates a named connection using a specific provider, in our case the
System.Data.SqlClient and using a specific connection string. The
UseJsonFormat flag specifies the format of stored data (JSON/XML).
Once our Silo is configured correctly, we can proceed by updating grains code:
ValueGrain has a new decorator:
[StorageProvider(ProviderName="OrleansStorage")]. It indicates that our grain uses a storage provider which is called: OrleansStorage. Furthermore,
ValueGrain extend the
Grain<SavedState> generic class: the
SavedState class is an representation of the state of our grain. The SavedState is stored inside OrleansStorage alias, which, in that case, stores into a SQLServer instance. Let's take a look to some infos stored into Storage table of SQLServer:
StorageProvider stores some key informations about the state of grains:
Payload (in JSON format).
The following chapter describe how to create a Cart API using Orleans and ASP.NET Core. The solution implement an ecommerce Cart web service, due to the demo purpose, we will keep the service as simple as possible. Let's take a look to the solution structure:
Cart.APIproject is the client of our Orleans cluster. It is an ASP.NET Core API project which exposes some routes in order to get carts and add new product to our cart;
GrainInterfacescontains interfaces definitions and the
CartStatewhich represents the state of our Cart;
Graincontains the concrete implementation of our interfaces;
Silois the project which refers our grains projects and use it in order to build our Silo using Orleans;
GrainInterfaces projects contains all grains and corresponding interfaces. First of all, this is the
ICartGrain interface definition:
IGrainWithGuidKey interface, so the grain will be stored using a Guid identifier. It also defines 3 methods:
AddProduct; The following interface exposes all the necessary methods in order to get and add products to our cart; Finally, the following class describes the implementation of
CartGrain virtual actor model:
[StorageProvider(ProviderName="CartStorage")] in order to store some informations about the cart status. If the cart does not exists, it will be created with an empty collection of products. The
AddProduct method simply accept a product and add it to our collection of products, therefore the
GetProduct method retrieves all the products stored in a specific basket.
The Silo project purpose is to configure Silo in order to host
ICartGrains. It contains only one C# file,
Program.cs simply configure the Silo and all the related providers, for example, it uses the
.AddAdoNetGrainStorage in order to configure CartStorage. Finally, it uses
UseDashboard() in order to provide the out-of-box Orleans monitoring dashboard.
The last project defines all the routes in order to perform update and read operations on a specific cart. The project uses the Web API template of ASP.NET Core. I've already speak about ASP.NET Core Web APIs in following articles: Implementing SOLID REST API using ASPNET Core, .NET Core 2.1 highlights: standing on the shoulders of giants, Build web service using F# and ASPNET Core. To be clear, the following schema gives an overview of the architecture: The front-end part is our
Cart.API project. The Actor-based middle tier is contained in
GrainInterfaces project and, finally, Storage is our database. Let's start by taking an overview on the
Startup.cs file of this project:
ICartGrain interfaces. The second core part of Cart.API project is the
CartController, which exposes create and update routes:
CartController uses the
IClusterClient. Due to ASP.NET Core framework, which is based on dependency injection, also the
IClusterClient is initialised using DI. The
CartController simply uses grains to get and update informations related to Cart. Above all, I suggest to put a request and response models instead of state model, in this demo the controller actions refer directly to
Finally, here is all the routes exposed by our
Silo project also exposes the Orleans dashboard at the following url:
http://localhost:8080/. It contains some useful informations about grains and silos and it also provides some insights about the status of the system.
Orleans is a powerful tool to implement actor model pattern and it is very useful in order developing APIs using Actor model in ASP.NET Core. It provides an high level abstraction hence, it simplify the implementation of an high distributed system. There are more concepts about Orleans which are no treated in this article: Timers and Reminders, Dependency Injection, Observers, Stateless Worker Grains; They contribute to empower your high-distributed system. You can find the repository on Github at following link: https://github.com/samueleresca/blog-orleans-deepdive. Cover image OXO tower London