loading...
Cover image for Developing APIs using Actor model in ASP.NET Core

Developing APIs using Actor model in ASP.NET Core

samueleresca profile image Samuele Resca ・9 min read

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.  

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;
  1. Perpetual existence means that actors (aka Grains) always exists. Developers does not have the explicit possibility to create or destroy an actor;
  2. Automatic instantiation: the runtime automatically create in-memory instances, which are called activations;
  3. 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;
  4. 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 27UseLocalhostClustering defines some key information about Orleans clustering. It is possible to override some default info: the ClusterId 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. The ServiceId is the unique ID for your application, that will be used by some provider, for example, the persistence provider;
  • line 21: The IClusterClient 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 the client.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 the CartState 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 

Posted on by:

samueleresca profile

Samuele Resca

@samueleresca

Samuele Resca is an Microsoft MVP Visual Studio and Development Technologies, Software Engineer, specializing mainly on ASP.NET MVC and in general about everything that revolves around the web. Samuele was born in 1994, and works as a software developer @ YOOX NET-A-PORTER Group He loves the MVC frameworks, ASP.NET MVC, Javascript, Node.js and Typescript.

Discussion

markdown guide
 

Great article. I've been looking for an excuse to use Orleans.

I would like to highlight one point about Orleans: it seems most suitable for distributed Stateful processing.

The stateless middle tier mentioned by the article (as well as FaaS offering like AWS Lambda) use a data shipping paradigm. In other words, the code and the data are separate and in order to fulfill a request the necessary data has to be shipped (probably via database call) into where the code is executing. Then probably shipped back out to the database to save it.

This works very well for a lot of cases. But sometimes data shipping is problematic. When there is a very large amount of data needed to process a request and/or a very high volume of requests, then the cost of IO to load/save data on every request can increase latency to unacceptable levels.

The solution then is to co-locate the data with the code. When a request comes in, the data is already there in memory, ready to use for processing. Requests change the data in memory. The data is loaded only when the actor is loaded and saved when the actor is unloaded (or periodically).

The main 3 examples of stateful actors I can recall seeing in presentations are all for gaming. 1) An MMO which loads a very large character state when the player logs in, updates it frequently during their play session, and saves it periodically + when they log off (presentation from Yan Cui). 2) A gambling system, presumably used to track in-progress sports events (presentation from Anton Moldovan). 3) And of course a couple of Halo games (from MSR promo videos about Orleans). Presumably other kinds of real-time use cases could be served well by stateful processing.

You can use stateful actors in places where stateless would work. However, random usage patterns probably won't see much perf benefit over stateless. Because actors will be loaded frequently, causing idle actors to be unloaded frequently, and it becomes nearly the same IO profile as data shipping. And stateless is potentially easier to operate using FaaS providers than setting up an Orleans cluster.

This comment was just to make readers aware of the various tradeoffs and some example uses of stateful processing. HTH.

 

Hi,

Thank you for the feedback :). IMHO, another concrete example, which is not part of the gaming world is this one: .I found it very interesting and Josef explains it very well. Let me know what you think. Sem

 

Thanks for this video! I hadn't seen it before.

Restaurants are another great use case. Each customer visit is a real-time scenario which: begins, stays active for a while and gets updated, and then ends. This matches really well with the Orleans virtual actor lifecycle. So the scenario seems really well suited. Pretty much anything that is "session"-like would fit really well.

The best-fit use case for actors where I work is probably training sessions. I'll have to keep this in mind when we hit scaling issues. Whereas administrative functions (adding trainees, courses, registrations) are pretty much random access and fit better with "data shipping".

 

I always enjoy seeing things on Orleans. When it was initially pitched as the technology upon which to build the company's new, project-agnostic backend (and subsequently used for Robinson, Hunt, and our own cancelled project), it took a while to really wrap our heads around it. I wanted to push F# as well, but decided that was bit too drastic of a lifestyle change to foist on the team.

Another similar framework I've had my eye on is proto.actor/. Not used it, however.

 

I've never heard about the actor model and the Orleans project. It sounds fascinating. Thank you for the gentle introduction into both subjects. I must give a closer look at them in my free time.