Developing APIs using Actor model in ASP.NET Core
The following article describes how to developing APIs using Actor model in ASP.NET Core. It will show some benefits of build APIs using actor model pattern and some good reasons about adopting Orleans.NET as actor model framework. This article has been written in collaboration with @francomelandri.
3-tier architecture
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.
Actor model frameworks
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 project overview
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.
Grain (a.k.a Actor)
The following code shows an actor interface declaration in Orleans:
Actors are the basic building blocks of Orleans applications and are the only units of isolation and distribution. Orleans framework calls actors Grain. Grains actually are virtual actors, they have 4 key properties: perpetual existence, auto instantiation, location transparency, auto scale-out;
- 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 threading
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.
Grain and Silos workflow
In Orleans programming model, implementing interfaces is the only way to declare a grain, Let’s take a look to the following example:
On the other side, client will call the ValueGrain
through the use of the IValueGrain
interface:
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 Task
;
Client configuration
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:
Let’s focus on some key points:
line 27
:UseLocalhostClustering
defines some key information about Orleans clustering. It is possible to override some default info: theClusterId
is a unique ID for the Orleans cluster. All clients and silo that uses this ID will be able to directly talk to each other. TheServiceId
is the unique ID for your application, that will be used by some provider, for example, the persistence provider;line 21
: TheIClusterClient
interface 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 theclient.Connect().Wait()
construct;
Silos concept
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:
Let’s focus on some key points:
UseLocalhostClustering
configures 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 best practices
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.
Scenarios
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.
Cluster management
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
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.
Grain persistence using relational data source
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.AdoNet
to 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
\OrleansAdoNetContent
where 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:
The 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).
Read and modify state
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:
The StorageProvider
stores some key informations about the state of grains: GrainIdHash
, TypeHash
, Payload
(in JSON format).
Cart API using Orleans
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.API
project 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;GrainInterfaces
contains interfaces definitions and theCartState
which represents the state of our Cart;Grain
contains the concrete implementation of our interfaces;Silo
is the project which refers our grains projects and use it in order to build our Silo using Orleans;
Grains definition
Grain
and GrainInterfaces
projects contains all grains and corresponding interfaces. First of all, this is the ICartGrain
interface definition:
It implements the IGrainWithGuidKey
interface, so the grain will be stored using a Guid identifier. It also defines 3 methods: GetCart
, GetProducts
, 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:
The class uses the [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.
Silo configuration
The Silo project purpose is to configure Silo in order to host ICartGrains
. It contains only one C# file, Program.cs
:
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.
Cart.API project
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 Silo
, Grains
, GrainInterfaces
project and, finally, Storage is our database.
Let’s start by taking an overview on the Startup.cs
file of this project:
The file implements all the configurations required to run Orleans client and it creates 5 different clients. It also registers the ICartGrain
interfaces. The second core part of Cart.API project is the CartController
, which exposes create and update routes:
The 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 Product
state.
Result and monitoring
Finally, here is all the routes exposed by our Cart.API
:
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.
Final thoughts
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