Edit

Tutorial: Hello world

This overview ties into the Hello World sample application.

The main concepts of Orleans involve a silo, a client, and one or more grains. Creating an Orleans app involves configuring the silo, configuring the client, and writing the grains.

Configure the silo

Configure silos programmatically via ISiloBuilder and several supplemental option classes. You can find a list of all options at List of options classes.

public static async Task SiloMain(string[] args)
{
    await Host.CreateDefaultBuilder(args)
        .UseOrleans(siloBuilder =>
        {
            siloBuilder.UseLocalhostClustering()
                .Configure<ClusterOptions>(options =>
                {
                    options.ClusterId = "dev";
                    options.ServiceId = "HelloWorldApp";
                })
                .Configure<EndpointOptions>(
                    options => options.AdvertisedIPAddress = IPAddress.Loopback)
                .ConfigureLogging(logging => logging.AddConsole());
        })
        .RunConsoleAsync();
}

The preceding code:

  • Creates a default host builder.
  • Calls UseOrleans to configure the silo.
  • Uses localhost clustering for local development.
  • Configures the cluster and service IDs.
  • Configures the endpoint to listen on loopback.
  • Adds console logging.

Configure silos programmatically via ISiloHostBuilder and several supplemental option classes. You can find a list of all options at List of options classes.

static async Task<ISiloHost> StartSilo(string[] args)
{
    var siloHostBuilder = new SiloHostBuilder()
        .UseLocalhostClustering()
        .Configure<ClusterOptions>(options =>
        {
            options.ClusterId = "dev";
            options.ServiceId = "HelloWorldApp";
        })
        .Configure<EndpointOptions>(
            options => options.AdvertisedIPAddress = IPAddress.Loopback)
        .ConfigureApplicationParts(
            parts => parts.AddApplicationPart(typeof(HelloGrain).Assembly).WithReferences())
        .ConfigureLogging(logging => logging.AddConsole());

    var host = siloHostBuilder.Build();
    await host.StartAsync();

    return host;
}
Option Used for
.UseLocalhostClustering() Configures the client to connect to a silo on the localhost.
ClusterOptions ClusterId is the name for the Orleans cluster; it must be the same for the silo and client so they can communicate. ServiceId is the ID used for the application and must not change across deployments.
EndpointOptions Tells the silo where to listen. For this example, use loopback.
Option Used for
ConfigureApplicationParts Adds the grain class and interface assembly as application parts to your Orleans application. This is not needed in Orleans 7.0+ as source generators handle this automatically.

After loading the configurations, build the host and then start it asynchronously.

Configure the client

Similar to the silo, configure the client via IClientBuilder and a similar collection of option classes.

public static async Task ClientMain(string[] args)
{
    using IHost host = Host.CreateDefaultBuilder(args)
        .UseOrleansClient(clientBuilder =>
        {
            clientBuilder.UseLocalhostClustering()
                .Configure<ClusterOptions>(options =>
                {
                    options.ClusterId = "dev";
                    options.ServiceId = "HelloWorldApp";
                });
        })
        .ConfigureLogging(logging => logging.AddConsole())
        .Build();

    await host.StartAsync();

    var client = host.Services.GetRequiredService<IClusterClient>();
    Console.WriteLine("Client successfully connected to silo host");

    await DoClientWork(client);

    await host.StopAsync();
}

The preceding code:

  • Creates a default host builder.
  • Calls UseOrleansClient to configure the client.
  • Uses localhost clustering to connect to the local silo.
  • Configures the cluster and service IDs to match the silo.
  • Starts the host and retrieves the IClusterClient from the service provider.

Similar to the silo, configure the client via IClientBuilder and a similar collection of option classes.

static async Task<IClusterClient> StartClientWithRetries()
{
    _attempt = 0;
    var client = new ClientBuilder()
        .UseLocalhostClustering()
        .Configure<ClusterOptions>(options =>
        {
            options.ClusterId = "dev";
            options.ServiceId = "HelloWorldApp";
        })
        .ConfigureLogging(logging => logging.AddConsole())
        .Build();

    await client.Connect(RetryFilter);
    Console.WriteLine("Client successfully connected to silo host");
    return client;
}

private static async Task<bool> RetryFilter(Exception exception)
{
    if (exception.GetType() != typeof(SiloUnavailableException))
    {
        Console.WriteLine($"Cluster client failed to connect to cluster with unexpected error. Exception: {exception}");
        return false;
    }
    _attempt++;
    Console.WriteLine($"Cluster client attempt {_attempt} of 5 failed to connect to cluster. Exception: {exception}");
    if (_attempt > 5)
    {
        return false;
    }
    await Task.Delay(TimeSpan.FromSeconds(4));
    return true;
}
Option Used for
.UseLocalhostClustering() Same as for the silo
ClusterOptions Same as for the silo

Find a more in-depth guide to configuring your client in the Client configuration section of the Configuration Guide.

Write a grain

Grains are the key primitives of the Orleans programming model. They are the building blocks of an Orleans application, serving as atomic units of isolation, distribution, and persistence. Grains are objects representing application entities. Just like in classic Object-Oriented Programming, a grain encapsulates an entity's state and encodes its behavior in code logic. Grains can hold references to each other and interact by invoking methods exposed via interfaces.

Read more about them in the Grains section of the Orleans documentation.

This is the main body of code for the Hello World grain:

public class HelloGrain : Orleans.Grain, IHello
{
    private readonly ILogger<HelloGrain> _logger;

    public HelloGrain(ILogger<HelloGrain> logger) => _logger = logger;

    Task<string> IHello.SayHello(string greeting)
    {
        _logger.LogInformation("SayHello message received: greeting = '{Greeting}'", greeting);
        return Task.FromResult($"You said: '{greeting}', I say: Hello!");
    }
}

A grain class implements one or more grain interfaces. For more information, see the Grains section.

public interface IHello : Orleans.IGrainWithIntegerKey
{
    Task<string> SayHello(string greeting);
}

How the parts work together

This programming model builds on the core concept of distributed Object-Oriented Programming. Start the ISiloHost first. Then, start the OrleansClient program. The Main method of OrleansClient calls the method that starts the client, StartClientWithRetries(). Pass the client to the DoClientWork() method.

static async Task DoClientWork(IClusterClient client)
{
    var friend = client.GetGrain<IHello>(0);
    var response = await friend.SayHello("Good morning, my friend!");
    Console.WriteLine($"\n\n{response}\n\n");
}

At this point, OrleansClient creates a reference to the IHello grain and calls its SayHello() method via the IHello interface. This call activates the grain in the silo. OrleansClient sends a greeting to the activated grain. The grain returns the greeting as a response to OrleansClient, which then displays it on the console.

Run the sample app

To run the sample app, refer to the Readme.