Desarrollo de un grano

Antes de escribir código para implementar una clase de grano, cree un proyecto de biblioteca de clases que tenga como destino .NET Standard o .NET Core (opción preferida), o bien .NET Framework 4.6.1 o una versión superior (si no puede usar .NET Standard o .NET Core debido a dependencias). Las interfaces de grano y las clases de grano se pueden definir en el mismo proyecto de biblioteca de clases, o en dos proyectos diferentes para separar mejor las interfaces de la implementación. En cualquier caso, los proyectos deben hacer referencia a los paquetes NuGet Microsoft.Orleans.Core.Abstractions y Microsoft.Orleans.CodeGenerator.MSBuild.

Para obtener instrucciones más detalladas, consulte la sección Configuración del proyecto del Tutorial 1: Conceptos básicos de Orleans.

Interfaces y clases de grano

Los granos interactúan entre sí y se les llama desde fuera mediante la invocación de métodos declarados como parte de las interfaces de grano respectivas. Una clase de grano implementa una o varias interfaces de grano declaradas anteriormente. Todos los métodos de una interfaz de grano deben devolver un elemento Task (para los métodos void), Task<TResult> o ValueTask<TResult> (para los métodos que devuelven valores de tipo T).

A continuación se muestra un extracto del ejemplo de servicio de presencia de Orleans versión 1.5:

public interface IPlayerGrain : IGrainWithGuidKey
{
    Task<IGameGrain> GetCurrentGame();
    Task JoinGame(IGameGrain game);
    Task LeaveGame(IGameGrain game);
}

public class PlayerGrain : Grain, IPlayerGrain
{
    private IGameGrain _currentGame;

    // Game the player is currently in. May be null.
    public Task<IGameGrain> GetCurrentGame()
    {
       return Task.FromResult(_currentGame);
    }

    // Game grain calls this method to notify that the player has joined the game.
    public Task JoinGame(IGameGrain game)
    {
       _currentGame = game;

       Console.WriteLine(
           $"Player {GetPrimaryKey()} joined game {game.GetPrimaryKey()}");

       return Task.CompletedTask;
    }

   // Game grain calls this method to notify that the player has left the game.
   public Task LeaveGame(IGameGrain game)
   {
       _currentGame = null;

       Console.WriteLine(
           $"Player {GetPrimaryKey()} left game {game.GetPrimaryKey()}");

       return Task.CompletedTask;
   }
}

Valores devueltos de métodos de grano

Un método de grano que devuelve un valor de tipo T se define en una interfaz de grano de modo que devuelva un elemento Task<T>. En el caso de los métodos de grano que no están marcados con la palabra clave async, cuando el valor devuelto está disponible, suele devolverse mediante la instrucción siguiente:

public Task<SomeType> GrainMethod1()
{
    return Task.FromResult(GetSomeType());
}

Un método de grano que no devuelve ningún valor (es decir, un método "void") se define en una interfaz de grano de modo que devuelva un Task. El elemento Task devuelto indica la ejecución asincrónica y la finalización del método. Para los métodos de grano que no están marcados con la palabra clave async, cuando un método "void" completa su ejecución, debe devolver el valor especial de Task.CompletedTask:

public Task GrainMethod2()
{
    return Task.CompletedTask;
}

Un método de grano marcado como async devuelve el valor directamente:

public async Task<SomeType> GrainMethod3()
{
    return await GetSomeTypeAsync();
}

Un método de grano void marcado como async que no devuelve ningún valor simplemente se devuelve al final de su ejecución:

public async Task GrainMethod4()
{
    return;
}

Si un método de grano recibe el valor devuelto de otra llamada de método asincrónico (tanto si es a un grano como si no) y no necesita realizar el control de errores de esa llamada, simplemente puede devolver el elemento Task que recibe de esa llamada asincrónica:

public Task<SomeType> GrainMethod5()
{
    Task<SomeType> task = CallToAnotherGrain();

    return task;
}

Del mismo modo, un método de grano void puede devolver un elemento Task devuelto por otra llamada, en lugar de esperarlo.

public Task GrainMethod6()
{
    Task task = CallToAsyncAPI();
    return task;
}

Se puede usar ValueTask<T> en lugar de Task<T>.

Referencia de grano

Una referencia de grano es un objeto proxy que implementa la misma interfaz de grano que la clase de grano correspondiente. Encapsula la identidad lógica (tipo y clave única) del grano de destino. Las referencias de grano se usan para realizar llamadas al grano de destino. Cada referencia de grano se establece con un solo grano (una sola instancia de la clase de grano), pero puede crear varias referencias independientes al mismo grano.

Dado que una referencia de grano representa la identidad lógica del grano de destino, es independiente de la ubicación física del grano y sigue siendo válida incluso después de un reinicio completo del sistema. Los desarrolladores pueden usar las referencias de grano igual que cualquier otro objeto de .NET. Pueden pasarse a un método, usarse como un valor devuelto de método e incluso guardarse en el almacenamiento persistente.

Para obtener una referencia de grano, hay que pasar la identidad de un grano al método IGrainFactory.GetGrain<TGrainInterface>(Type, Guid), donde T es la interfaz de grano y key es la clave única del grano dentro del tipo.

A continuación se muestran ejemplos de cómo obtener una referencia de grano de la interfaz IPlayerGrain definida anteriormente.

Desde dentro de una clase de grano:

IPlayerGrain player = GrainFactory.GetGrain<IPlayerGrain>(playerId);

Desde código de cliente de Orleans:

IPlayerGrain player = client.GetGrain<IPlayerGrain>(playerId);

Invocación del método de grano

El modelo de programación de Orleans se basa en la programación asincrónica. Aquí se muestra cómo se realiza una invocación de método de grano con la referencia de grano del ejemplo anterior:

// Invoking a grain method asynchronously
Task joinGameTask = player.JoinGame(this);

// The await keyword effectively makes the remainder of the
// method execute asynchronously at a later point
// (upon completion of the Task being awaited) without blocking the thread.
await joinGameTask;

// The next line will execute later, after joinGameTask has completed.
players.Add(playerId);

Es posible unir dos o más Tasks; la operación de combinación crea otro elemento Task que se resuelve cuando se han completado todos los elementos Task que lo constituyen. Se trata de un patrón útil cuando un grano necesita iniciar varios cálculos y esperar a que todos se completen antes de continuar. Por ejemplo, un grano de front-end que genera una página web integrada por muchas partes podría realizar varias llamadas de back-end (una para cada parte) y recibir un elemento Task para cada resultado. Después, el grano esperaría la combinación de todos los elementos Tasks; al resolverse el elemento Task combinado, se completarían los elementos Task individuales y se recibirían todos los datos necesarios para dar formato a la página web.

Ejemplo:

List<Task> tasks = new List<Task>();
Message notification = CreateNewMessage(text);

foreach (ISubscriber subscriber in subscribers)
{
    tasks.Add(subscriber.Notify(notification));
}

// WhenAll joins a collection of tasks, and returns a joined
// Task that will be resolved when all of the individual notification Tasks are resolved.
Task joinedTask = Task.WhenAll(tasks);

await joinedTask;

// Execution of the rest of the method will continue
// asynchronously after joinedTask is resolve.

Métodos virtuales

Opcionalmente, una clase de grano puede invalidar los métodos virtuales OnActivateAsync y OnDeactivateAsync; estos los invoca el runtime de Orleans tras la activación y desactivación de cada grano de la clase. Esto ofrece al código de grano la oportunidad de realizar operaciones adicionales de inicialización y limpieza. Si OnActivateAsync lanza una excepción, se produce un error en el proceso de activación. Si bien siempre se llama a OnActivateAsync, si se invalida, como parte del proceso de activación del grano, no se garantiza que se llame a OnDeactivateAsync en todas las situaciones, por ejemplo, en caso de error del servidor u otro evento anómalo. Por eso, las aplicaciones no deben basarse en OnDeactivateAsync para realizar operaciones críticas, como la persistencia de cambios de estado. Solo deben usarlo para las operaciones en las que sea la mejor opción.