Building cloud native applications with Microsoft Orleans

This post is part of the .NET Advent Calendar 2023.

Many thanks to Dustin Moris Gorski for organizing all of this. Checkout his other many excellent posts.

At CodicePlastico we love the actor model. Some of us are great fans of Elixir and its stack, others love Akka.NET and the way it provides to develop applications adopting actor model in the .net ecosystem. 

But why should I choose the actor model for my next application? Because it could be the right choice in some scenarios. If you are involved in developing IoT systems, vehicles, object tracking, betting or banking applications, then the actor model is a great choice. In general, if you need to track the state of a domain entity and access it frequently, the actor model could be the right tool! The actor model could be the right choice even if we need to develop something with entities that would “speak” to each other -e.g. by sending messages to notify changes and letting them update their status, if needed.

Interesting, right?! Many programming languages and frameworks are based on the actor model: Scala, Elixir, Erlang, Pony and Akka are only a few of them, maybe the most famous. Considering .NET world, Akka.net, Dapr, Proto.Actor, Coyote are some libraries or frameworks that allow us to adopt the actor model and, in the case of Dapr and Proto.Actor, to build a language-agnostic system, where actors implemented in different languages interact with each other. While I was researching all these libraries and frameworks, for sure, I missed a great, stable framework Microsoft has been developing since 2009: Microsoft Orleans. It’s been used for Halo 4, Skype and some Azure services, only to name a few.

So, if you are curious to explore something about actor model and this exciting topic in the .net ecosystem, this could be the right post for you!

What is an Actor Model?

Let’s start from the definition that Carl Hewitt, the creator of the Actor model, gave in 1973:

The actor model in computer science is a mathematical model of concurrent computation that treats an Actor as the universal primitive of concurrent computation. 
In response to a message 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 indirectly through messaging (removing the need for lock-based synchronization).

From this definition we understand that an Actor can:

  • receive messages,
  • process the messages it receives,
  • apply its business logic to the data contained into the message and take decision if
    • modify and persist its state
    • send messages to other actors

This model is great to develop cloud native, concurrent, distributed and scalable systems easily.

Let’s introduce Microsoft Orleans

If you want to fix in your mind the components of a Microsoft Orleans Cloud Native application, you have to imagine a large cultivated field of cereals. What are the main objects that you find in this relaxing picture?

First of all, the most important part of this picture is the cereals, because without cereals the cultivated field is useless. And what are cereals made of? Obviously… grains! Bingo! Grains are the first item you have to fix in your mind! How many grains do you find in a cultivated field? So many, and their number can grow during the season, when the field is… “running”.

The second focal point is the container of those grains: where do we deploy our grains? When we want to use those grains we have to store them in a silo. And what happens when the number of grains grows exponentially and a single silo is not enough? We need more silos and… we need something to manage those silos. So we need a cluster, something that keeps track of the status of each Silo and decides how grains are stored and retrieved.

So, let’s recap and apply this metaphor to the software. 

Grains

A grain is an actor or, better, is a virtual actor. Actors in Orleans are virtual because they are managed by the runtime. Developers shouldn’t worry about creating or destroying them, or knowing about the hierarchy of those system elements: they tell the runtime they need an instance of an actor and… voilà, the instance is provided. If the system needs: 

  • a new grain, then the runtime creates it and returns its reference to the caller; 
  • an existing grain, the runtime returns the reference to the instanced actor, if the grain is already in memory. However, if a previously active actor was unloaded from memory and persisted to storage due to inactivity, then the runtime will have to reactivate it first by loading its persisted state from the database and then return its reference;

An actor activation occurs just when another actor sends a message to it. If the actor is not receiving messages for a configurable amount of time, the runtime deactivates it.

The following image shows the actor’s life cycle

To identify grains, developers use an identifier. Orleans supports these types of identifiers:

  • Long 
  • GUID 
  • String 
  • GUID + String
  • Long + String

Finally, an actor-based application could have different kinds of actors. Since an actor is a class that implements the IGrainWith…Key (where the … is where you define the kind of the identifier) and inherits from Grain, the methods of the grain define its behavior.

To recap, the following is the equation of a grain 

Silos

Silos are the runtime that manages grains. A silo could be a console application or an api, hosted in a Windows Service or in IIS, deployed on premise, in a Docker container or in the cloud (i.e., using Azure App Service). Typically there’s a silo per machine.

Silos need 2 TCP ports to be opened:

  • 30000 for silo-to-silo communications
  • 11111 for client-to-silo communication

Clusters

As a silo manages grains, a cluster manages silos. A cluster needs a database to persist and share silos-related data: SQL Server, MySQL, Postgres and Oracle are supported. The cluster stores its data into cluster membership tables and communicates using a cluster membership protocol, as shown in the following image

Hosting

We can deploy an Orleans actor system in two ways:

  • in a co-hosted application, the silo and its clients are hosted in the same application, i.e. a minimal API. The client activates grains and the runtime manages them.
    This is perfect for simple applications and a great opportunity to start with a small footprint, keeping complexity low.
  • in an external clients application, clients and runtimes are hosted in different applications. Those applications share a library containing the grains contracts.

The amazing thing is that the developers’ experience is the same. Through the client, developers activate a grain that could be on the same machine or somewhere in the cloud, but for us it is only a reference of an instanced grain managed by the silo. In simple words, Microsoft Orleans manages for us all this complexity.

Creating an application from scratch with Orleans

Requirements

Now that we know what Microsoft Orleans is and how it works, we’ll start to create our actor model-based application for a new restaurant born a few days ago: PizzaPlastica.

PizzaPlastica commits us to a new, revolutionary ordering system platform. The requirements are the following:

  • the application must be as fast as possible during rush hours (they heard about something called cache to improve responsiveness…)
  • the restaurant has got an unique identifier and each table is numbered
  • customers can add and remove items
  • customers can close the order
  • a table order item has got these data: name and cost of the dish and quantity 
  • the table order should be closed automatically after 3 hours, removing all table order items

The CEO of PizzaPlastica will start with a single restaurant with 100 tables near here, but she wants to grow during the first year: she wants to open 10000 restaurants and manage at least 1000000 tables at the same time.

So, we need an application that scales, probably we could deploy the application on premise initially, but after a few months we should migrate the application to the cloud. We want to keep the application as easy as possible, with minimal dependencies on the infrastructure (our dream is to manage only the application and the database), but we need a cache because our tables are stateful entities. The migration to the cloud should be painless, theoretically moving only the data to the cloud database and deploying the application somewhere. And we need a scheduled task to close the table ordination.

With these requirements and these bullet points (cached and stateful entities, cloud-native application, at least a scheduled job) Microsoft Orleans could be the right choice 💪

Co-hosted application setup

To setup the application, open your preferred folder and execute the following commands in the terminal

md PizzaPlastica.OrderingSystem
cd PizzaPlastica.OrderingSystem
dotnet new sln
md src
cd src
dotnet new webapi -n PizzaPlastica.OrderingSystem
dotnet new classlib -n PizzaPlastica.OrderingSystem.Abstractions
cd ..
dotnet sln add src\PizzaPlastica.OrderingSystem\PizzaPlastica.OrderingSystem.csproj
dotnet sln add src\PizzaPlastica.OrderingSystem.Abstractions\PizzaPlastica.OrderingSystem.Abstractions.csproj

These two projects have different purposes:

  • PizzaPlastica.OrderingSystem.Abstractions contains the grain interfaces and shared objects
  • PizzaPlastica.OrderingSystem is a minimal api that hosts the silo and the grains (this is why we call this application co-hosted)

Developing your first grain

First of all, we have to create the grain interface. As said before, a grain must implement a IGrainWith…Key interface. These interfaces are provided by the Microsoft.Orleans.Core.Abstractions NuGet package that we are going to add to the PizzaPlastica.OrderingSystem.Abstractions project using the command

dotnet add package Microsoft.Orleans.Core.Abstractions

Then add the ITableOrderGrain interface

using Orleans.Concurrency;
using System.Collections.ObjectModel;

namespace PizzaPlastica.OrderingSystem.Abstractions;
public interface ITableOrderGrain : IGrainWithGuidCompoundKey
{
    Task OpenTableOrder();
    Task CloseTableOrder();
    Task<Guid> AddOrderItem(string name, double cost, int quantity);
    Task RemoveOrderItem(Guid orderItemId);
    [ReadOnly]
    Task<TableOrderItem> GetOrderItemDetails(Guid orderItemId);
    [ReadOnly]
    Task<ReadOnlyCollection<TableOrderItem>> GetOrderItems();
}

For this kind of grain we choose a compound key, composed by a guid and a string: the guid stands for the restaurant unique identifier, and the string represents the unique number of the table of this specific restaurant.

As you can see, methods GetOrderItemDetails and GetOrderItems are decorated by the [ReadOnly] attribute. Orleans provides some attributes to enhance multi-threading performance: in particular [ReadOnly] tells the runtime that this method doesn’t change the grain state. 

Go to the PizzaPlastica.OrderingSystem project and define the class TableOrderGrain . Let’s add some logic

using Orleans;
using PizzaPlastica.OrderingSystem.Abstractions;

namespace PizzaPlastica.OrderingSystem.Grains;

public class TableOrderGrain : Grain, ITableOrderGrain
{
    // STATE
    private bool IsOpen { get; set; }
    private List<TableOrderItem> OrderItems { get; set; }


    // BEHAVIOR
    public Task OpenTableOrder()
    {
        if(IsOpen)
            throw new InvalidStateException("Table has already opened.");

        this.IsOpen = true;
        OrderItems = new List<TableOrderItem>();
 
        return Task.CompletedTask;
    }

    public async Task<Guid> AddOrderItem(string name, double cost, int quantity)
    {
        if (!IsOpen)
            throw new InvalidStateException("Table should be opened.");

        var orderItemId = Guid.NewGuid();
        OrderItems.Add(new TableOrderItem 
        {
            Id = orderItemId,
            Name = name,
            Cost = cost,
            Quantity = quantity
        });

        return orderItemId;
    }


    ...
}

As you can see, the grain type represents the behavior of the grain and its properties represent the state

The PizzaPlastica.OrderingSystem is the project where the grains live and it contains the silo of our application, hosted in a minimal web api. To accomplish this purpose it is necessary to add the Microsoft.Orleans.Server NuGet package by executing the command 

dotnet add package Microsoft.Orleans.Server

Testing a grain

Move to the solution folder and create the test project

md test
cd test
dotnet new nunit -n PizzaPlastica.OrderingSystem.Testsdotnet sln add src\PizzaPlastica.OrderingSystem.Tests\PizzaPlastica.OrderingSystem.Tests.csproj

Remove the UnitTest1.cs file and add the reference to Microsoft.Orleans.TestingHost using the command

dotnet add package Microsoft.Orleans.TestingHost

Then add this fixtures class which will bootstrap the testing environment needed by each test class in the project.

using Orleans.TestingHost;

namespace PizzaPlastica.OrderingSystem.Tests.Fixtures;

public class ClusterFixture : IDisposable
{
    public ClusterFixture()
    {
        var builder = new TestClusterBuilder();
        builder.AddSiloBuilderConfigurator<TestSiloConfigurations>();

        Cluster = builder.Build();
        Cluster.Deploy();
    }

    public void Dispose() => Cluster.StopAllSilos();
    public TestCluster Cluster { get; }
}

public class TestSiloConfigurations : ISiloConfigurator
{
    public void Configure(ISiloBuilder siloBuilder)
    {
        // PUT HERE SERVICE DEPENDENCIES...
    }
}

And finally, create your test class TableOrderGrainTests. The following logic tests an entire scenario where we open a table order and add a new item

using PizzaPlastica.OrderingSystem.Abstractions;
using PizzaPlastica.OrderingSystem.Tests.Fixtures;

namespace PizzaPlastica.OrderingSystem.Tests;

public class TableOrderGrainTests
{
    private ClusterFixture fixture;

    [SetUp]
    public void SetUp()
    {
        fixture = new ClusterFixture();
    }

    [Test]
    public async Task Open_a_table_order_and_add_an_item()
    {
        // ARRANGE
        var restaurantId = Guid.NewGuid();
        var tableId = 3;
        var tableOrderGrain = fixture.Cluster.GrainFactory.GetGrain<ITableOrderGrain>(restaurantId, tableId.ToString());

        // ACT 
        await tableOrderGrain.OpenTableOrder();
        var orderItemId = await tableOrderGrain.AddOrderItem("Pizza Hawaii", 6.5, 1);
        var orderItems = await tableOrderGrain.GetOrderItems();

        // ASSERT
        Assert.That(orderItems.Count, Is.EqualTo(1));
        Assert.That(orderItems[0].Id, Is.EqualTo(orderItemId));
        Assert.That(orderItems[0].Name, Is.EqualTo("Pizza Hawaii"));
        Assert.That(orderItems[0].Cost, Is.EqualTo(6.5));
        Assert.That(orderItems[0].Quantity, Is.EqualTo(1));
    }
}

Create your API

Now, to expose the application logic via api, it is necessary to modify the Program.cs class and register Orleans after the web application builder was created

// *** ORLEANS REGISTRATION E CONFIGURATION ***
builder.Host.UseOrleans(siloBuilder =>
{
    if (builder.Environment.IsDevelopment())
    {
        siloBuilder.UseLocalhostClustering();
    }
});

and expose the logic through different endpoints

app.MapPost("restaurants/{restaurantId}/tables/{tableId}", 
    async (
        IClusterClient client, 
        Guid restaurantId, 
        int tableId) =>
{
    try
    {
        var grainRef = client.GetGrain<ITableOrderGrain>(restaurantId, tableId.ToString());
        await grainRef.OpenTableOrder();
        return Results.Ok();
    }
    catch(InvalidStateException exc)
    {
        return Results.BadRequest(exc.Message);
    }
})
.WithName("OpenTable")
.WithOpenApi();

Coding the interaction between the api and grains is very easy. The code block above shows that only two lines of code are necessary

  • obtain a grain reference
  • call the desired grain behavior, represented by a method

The Orleans runtime hides and manages the complexity!

Now the grain is running but when it is deactivated by the runtime or due to an application restart, the application loses its state. It’s time to add persistence.

The grain persistence

Staying is a local development environment, the persistence is only in-memory but this step prepares grains to be production ready. Adding persistence for development purposes is easy: we have to modify the Orleans registration by adding in-memory persistence

// *** ORLEANS REGISTRATION E CONFIGURATION ***
builder.Host.UseOrleans(siloBuilder =>
{
    if (builder.Environment.IsDevelopment())
    {
        siloBuilder.UseLocalhostClustering();
        siloBuilder.AddMemoryGrainStorage("tableorderstorage");
    }
});

Now that the environment is configured, we need to provide the TableOrderGrain class with the structure to persist data. So it is necessary to create a new class that represents the grain state

[GenerateSerializer]
public class TableOrderState
{
    [Id(0)]
    public bool IsOpen { get; set; }
    [Id(1)]
    public IList<TableOrderItem> OrderItems { get; set; }
}

The grain should be modified to accomplish the persistence requirements

public class TableOrderGrain : Grain, ITableOrderGrain
{
    private IPersistentState<TableOrderState> TableOrder { get; }

    public TableOrderGrain(
        [PersistentState(stateName: "table-order", 
                         storageName: "tableorderstorage")]        
            IPersistentState<TableOrderState> state)
    {
       TableOrder = state;
    }

    public Task OpenTableOrder()
    {
        if (TableOrder.State.IsOpen)
            throw new InvalidStateException("Table has already opened.");

        TableOrder.State.IsOpen = true;
        TableOrder.State.OrderItems = new List<TableOrderItem>();

        return TableOrder.WriteStateAsync();
    }
   
   ...
}

Finally it is necessary to align the test environment dependencies into ClusterFixture 

public class TestSiloConfigurations : ISiloConfigurator
{
    public void Configure(ISiloBuilder siloBuilder)
    {
        // PUT HERE SERVICE DEPENDENCIES...
        siloBuilder.AddMemoryGrainStorage("tableorderstorage");
    }
}

Use reminders for scheduled tasks

Reminders are useful to schedule tasks in the future when the period is greater than or equal to 1 minute (for shorter periods timers should be the right choice). Reminders could be recurring tasks and they could be stopped. As we did for persistence, we start adding reminders for local development purposes and we will be ready for production configuration. First of all, it is necessary to add the dependency executing the command

dotnet add package Microsoft.Orleans.Reminders

Then the service should be registered in Program.cs class

// *** ORLEANS REGISTRATION E CONFIGURATION ***
builder.Host.UseOrleans(siloBuilder =>
{
    if (builder.Environment.IsDevelopment())
    {
        siloBuilder.UseLocalhostClustering();
        siloBuilder.AddMemoryGrainStorage("tableorderstorage");
        siloBuilder.UseInMemoryReminderService();
    }
});

Remember to add this dependency to ClusterFixture class in the test project.

Finally we have to modify the grain class to add this logic

public class TableOrderGrain : Grain, ITableOrderGrain, IRemindable
{
    private IPersistentState<TableOrderState> TableOrder { get; }
    private IGrainReminder _reminder = null;

    ...

    public async Task OpenTableOrder()
    {
        if (TableOrder.State.IsOpen)
            throw new InvalidStateException("Table has already opened.");

        TableOrder.State.IsOpen = true;
        TableOrder.State.OrderItems = new List<TableOrderItem>();

        _reminder = await this.RegisterOrUpdateReminder("TableOrderExpired",
          dueTime: TimeSpan.Zero,
          period: TimeSpan.FromHours(3));

        
        await TableOrder.WriteStateAsync();
    }

    public async Task CloseTableOrder()
    {
        if (!TableOrder.State.IsOpen)
            throw new InvalidStateException("Table has already closed.");

        TableOrder.State.IsOpen = false;
        TableOrder.State.OrderItems = new List<TableOrderItem>();

        if (_reminder is not null)
        {
            await this.UnregisterReminder(_reminder);
            _reminder = null;
        }

        await TableOrder.WriteStateAsync();
    }

    public Task ReceiveReminder(string reminderName, TickStatus status)
    {
        return reminderName switch
        {
            "TableOrderExpired" => CloseTableOrder(),
            _ => Task.CompletedTask
        };
    }

    ...
}

When an order is opened on a table, we register a reminder named TableOrderExpired that will fire after 3 hours. The runtime invokes the ReceiveReminder method that calls CloseTableOrder() where the reminder is unregistered.

Going to production

Going to production it’s easy but it requires some configuration.

First of all we need a database for the runtime: Microsoft Orleans provides connectors to different databases (SQL Server, Postgres, MySql, Oracle). We choose SQL Server.

When your sql instance is ready, create tables by running the scripts Microsoft has made available in the documentation.

In particular, we setup our database running these scripts:

Don’t forget to execute this last command, missing from the scripts above

INSERT INTO OrleansQuery(QueryKey, QueryText)
VALUES
(
    'CleanupDefunctSiloEntriesKey','
    DELETE FROM OrleansMembershipTable
    WHERE DeploymentId = @DeploymentId
        AND @DeploymentId IS NOT NULL
        AND IAmAliveTime < @IAmAliveTime
        AND Status != 3;
');

Now the database instance is ready

Finally, open the PizzaPlastica.OrderingSystem project and add the following dependencies, based on the database you choose

dotnet add package Microsoft.Orleans.Clustering.AdoNet
dotnet add package Microsoft.Orleans.Persistence.AdoNet
dotnet add package Microsoft.Orleans.Reminders.AdoNet
dotnet add package System.Data.SqlClient

and add configuration for the production environment

builder.Host.UseOrleans(siloBuilder =>
{
    if (builder.Environment.IsDevelopment())
    {
        siloBuilder.UseLocalhostClustering();
        siloBuilder.AddMemoryGrainStorage("tableorderstorage");
        siloBuilder.UseInMemoryReminderService();
    }
    else
    {
        siloBuilder.Services.AddSerializer(serializerBuilder =>
        {
            serializerBuilder.AddNewtonsoftJsonSerializer(
                isSupported: type => type.Namespace.StartsWith("PizzaPlastica.OrderingSystem.Abstractions"));
        });

        // CREAZIONE DEL CLUSTER PER AMBIENTI DI STAGING / PRODUZIONE
        siloBuilder.Configure<ClusterOptions>(options =>
        {
            options.ClusterId = "PizzaPlasticaCluster";
            options.ServiceId = "BestOrderingSystemEver";
        })
        .UseAdoNetClustering(options =>
        {
            options.ConnectionString = builder.Configuration.GetConnectionString("SqlOrleans");
            options.Invariant = "System.Data.SqlClient";
        })
        .ConfigureEndpoints(siloPort: 11111, gatewayPort: 30000);

        // REGISTRAZIONE REMINDERS PER AMBIENTI DI STAGING / PRODUZIONE
        siloBuilder.UseAdoNetReminderService(reminderOptions => {
            reminderOptions.ConnectionString = builder.Configuration.GetConnectionString("SqlOrleans");
            reminderOptions.Invariant = "System.Data.SqlClient";
        });

        // REGISTRAZIONE STORAGE PER AMBIENTI DI STAGING / PRODUZIONE
        siloBuilder.AddAdoNetGrainStorage("tableorderstorage", storageOptions =>
        {
            storageOptions.ConnectionString = builder.Configuration.GetConnectionString("SqlOrleans");
            storageOptions.Invariant = "System.Data.SqlClient";
        });
    }
});

If you start the application now, after the invoking OpenTableOrder method, your tables start to collect data for…

…cluster management

…reminder persistence

…and grain states

As you can see, the field PayloadBinary of the OrleansStorage table contains the state of the grain where persistence is identified by the string table-order . If you want to deserialize the field content, run this query 

SELECT CAST([PayloadBinary] AS VARCHAR(MAX)) AS Payload
FROM [OrderingSystem].[dbo].[OrleansStorage]

and you obtain this result

{
    "$id": "1",
    "$type": "PizzaPlastica.OrderingSystem.Grains.TableOrderState, PizzaPlastica.OrderingSystem",
    "IsOpen": true,
    "OrderItems": {
        "$type": "System.Collections.Generic.List`1[[PizzaPlastica.OrderingSystem.Abstractions.TableOrderItem, PizzaPlastica.OrderingSystem.Abstractions]], System.Private.CoreLib",
        "$values": [{
                "$id": "2",
                "$type": "PizzaPlastica.OrderingSystem.Abstractions.TableOrderItem, PizzaPlastica.OrderingSystem.Abstractions",
                "Id": "2a62dd13-163c-4d3b-bccb-33c9d8879f46",
                "Name": "Pizza",
                "Cost": 7.8,
                "Quantity": 2
            }, {
                "$id": "3",
                "$type": "PizzaPlastica.OrderingSystem.Abstractions.TableOrderItem, PizzaPlastica.OrderingSystem.Abstractions",
                "Id": "bab4e43e-58ae-4903-b72b-5590088a3514",
                "Name": "Pizza",
                "Cost": 7.8,
                "Quantity": 2
            }
        ]
    }
}

And finally… scale with the external clients mode

If you want to scale you cannot leave both the silo and the api code in the same project. In a mission critical application, silos could manage many thousands of grains and probability you should scale your application. Maybe api should not scale as silos, so it could be better to separate silo and api projects.

To accomplish this purpose, create a new minimal project named PizzaPlastica.OrderingSystem.Api. Add the Microsoft.Orleans.Client package via Nuget and move all endpoints here. Finally the api must know where the cluster membership table is, so copy the same connection string to the api project and add it to the Program.cs file, where the cluster registration is configured.

builder.Host.UseOrleansClient((hostContext, clientBuilder) =>
{
    if (builder.Environment.IsDevelopment())
    {
        // DEV CLUSTER
        clientBuilder.UseLocalhostClustering();
    }
    else
    {
        // STAGING/PRODUCTION CLUSTER
        clientBuilder.Configure<ClusterOptions>(options =>
        {
            options.ClusterId = "PizzaPlasticaCluster";
            options.ServiceId = "BestOrderingSystemEver";
        })
        .UseAdoNetClustering(options =>
        {
            options.ConnectionString = builder.Configuration.GetConnectionString("SqlOrleans");
            options.Invariant = "System.Data.SqlClient";
        });
    }
});

Run both the silo and api project specifying production environment

dotnet run --environment Production

and if everything was done correctly, you should see the application is still in the exact same state you left it when it ran in the co-hosted environment.

Repository

You can find all the source code here:

https://github.com/CodicePlastico/net-advent-calendar-2023-ms-orleans

You’ll find both solutions, co-hosted and external clients

Microsoft Orleans ❤️ .NET Aspire

Recently Microsoft released a new “stack for building resilient, observable, and configurable cloud-native applications with .NET”, as they said in the post where this new tool was launched. If you are asking yourself how to merge these topics, Reuben Bond has got the answer:

Conclusions

I hope this long post could help you to discover and learn something more about this amazing framework. Orleans has got many other features: streams, filters, dashboards… so enjoy!  

I cannot be clearer than the community to explain what Orleans is so I want to close this article sharing with you this takeaway from the GitHub page of the project:

Orleans is a cross-platform framework for building robust, scalable distributed applications
Orleans builds on the developer productivity of .NET and brings it to the world of distributed applications, such as cloud services. Orleans scales from a single on-premises server to globally distributed, highly-available applications in the cloud.
Orleans takes familiar concepts like objects, interfaces, async/await, and try/catch and extends them to multi-server environments. As such, it helps developers experienced with single-server applications transition to building resilient, scalable cloud services and other distributed applications. For this reason, Orleans has often been referred to as “Distributed .NET“.

Finally, but not least, I want to express my gratitude to my colleague Moreno Gentili, who shared the effort of creating this article with me. His willingness for a valuable and timely review was crucial. Thank you also for the late-night discussions and for the skill in providing feedback. This highlights the value of being part of a team and learning from each other.

Merry Xmas folks and Happy New Year!

About Actor Model:

About Microsoft Orleans: