Este é mais um artigo de uma série sobre o Microsoft Orleans. Caso não tenha visto o primeiro, recomendo que o leia: Conhecendo o Microsoft Orleans.
Como dito no artigo passado, o Orleans está na segunda versão e uma das novidades dela é o suporte ao .NET Standard 2.0, desta forma, como o título indica, a aplicação demonstrada aqui utilizará este framework.
Curso C# (C Sharp) - Introdução ao ASP.NET Core
Conhecer o cursoEstrutura da solução
Antes de colocarmos a mão na massa, é importante entender como funciona a solução do Orleans. Esta biblioteca recomenda que um projeto que a implemente seja estruturado com no mínimo quatro projetos:
-
OrleansHost: Este projeto irá criar um executável que iniciará os silos, os hosts do Orleans que conterá as instâncias dos grãos. É possível iniciar vários silos, que irão trabalhar em conjunto e compartilhar a carga.
-
GrainsInterfaces: Este projeto contém as interfaces que definem todos os grãos contido nos silos.
-
Grains: Este projeto contém a implementação de todos os grãos definidos em GrainsInterfaces. Por isso, ele é implementado em conjunto com o OrleansHost.
-
Inteface: Este projeto pode ser qualquer projeto de interface, mas geralmente trata-se de API. Ele irá se conectar ao OrleansHost, por TCP, para ter acesso aos grãos.
Durante o desenvolvimento, o projeto interface e o OrleansHost podem ser executados em conjunto na mesma máquina. Mas em produção, geralmente eles são executados separadamente. O OrleansHost é implementado em um cluster e a interface em um servidor web.
Neste artigo, a solução será feita em uma máquina Mac OSX, utilizando a versão 2.1.4, em conjunto com o Visual Studio Code, pois amo este ambiente. Mas o Orleans fornece um plugin de templates para o Visual Studio, que facilita a criação de projetos com esta biblioteca. Desta forma, caso esteja utilizando esta IDE recomendo que instale este plugin.
Criando o projeto GrainsInterfaces
Como o projeto Grains necessita do GrainsInterfaces e o OrleansHost fará uso do Grains, o primeiro projeto que precisa ser criado é o GrainsInterfaces.
Caso esteja utilizando o Visual Studio com o plugin do Orleans instalado, crie um projeto com base no template “Orleans Grain Interface Collection. No meu ambiente, incialmente irei criar uma pasta chamada OrleansDotNet, e dentro dela uma solução:
dotnet new sln -n OrleansDotNet
Em seguida, um projeto Class Library:
dotnet new classlib -n GraosInterfaces
Neste projeto adicione a biblioteca Microsoft.Orleans.OrleansCodeGenerator.Build
:
dotnet add package Microsoft.Orleans.OrleansCodeGenerator.Build
E adicione nele a interface abaixo:
using System;
using Orleans;
using System.Threading.Tasks;
namespace OrleansDotNet.GraosInterfaces
{
public interface IGraoContador: IGrainWithStringKey
{
Task Incremento(int incremento);
Task<int> GetContador();
}
}
As interfaces grãos devem definir a implementação de uma das interfaces “GrainWith*
” do Orleans e definir métodos que retornem uma Task
, para métodos void
, ou Task
, caso o método retorne algum valor.
A interface implementada acima, IGrainWithStringKey
, é utilizada para indicar que o código de identificação do grão será definido com uma string. Infelizmente a documentação do Orleans ainda não documenta bem todas as interfaces disponíveis, mas em um artigo futuro abordo todas.
Criando o projeto Grains
Com a interface definida podemos implementá-la um um projeto Grains. Caso esteja utilizando o Visual Studio, crie um projeto com o template Orleans Grain Class Collection. No meu ambiente, irei criar um projeto Class Library chamado Graos:
dotnet new classlib -n Graos
E este projeto também precisa da referência abaixo:
dotnet add package Microsoft.Orleans.OrleansCodeGenerator.Build
Em seguida, referencie o projeto de interface:
dotnet add Graos.csproj reference ../GraosInterfaces/GraosInterfaces.csproj
Agora podemos implementar o nosso grão:
using System;
using Orleans;
using System.Threading.Tasks;
using OrleansDotNet.GraosInterfaces;
namespace OrleansDotNet.Graos
{
public class GraoContador: Grain, IGraoContador
{
private int _contador;
public Task Incremento(int incremento)
{
_contador += incremento;
return Task.CompletedTask;
}
public Task<int> GetContador()
{
return Task.FromResult(_contador);
}
}
}
Esta classe não tem segredo, o importante é implementar a nossa interface grão e herdar a classe Grain
. Dentro dela é definido um contador simples.
Criando o projeto OrleansHost
Com o grão definido, podemos definir o nosso silo (host). Caso esteja utilizando o Visual Studio, crie um projeto com base no template “Orleans Dev/Test Host”.
No meu ambiente irei criar uma aplicação console chamada Silo:
dotnet new console -n Silo
Nele adicione adicione uma referência para a biblioteca Microsoft.Orleans.Server
:
dotnet add package Microsoft.Orleans.Server
Como iremos registrar o log do silo do console, adicione a referencia abaixo:
dotnet add package Microsoft.Extensions.Logging.Console
Por fim, adicione a referencia do projeto Graos:
dotnet add Silo.csproj reference ../Graos/Graos.csproj
E na classe Program
adicione o código abaixo:
using System;
using Orleans;
using Orleans.Runtime.Configuration;
using Orleans.Hosting;
using Orleans.Configuration;
using System.Threading.Tasks;
using OrleansDotNet.Graos;
using Microsoft.Extensions.Logging;
using System.Runtime.Loader;
using System.Threading;
using System.Net;
namespace OrleansDotNet.Silo
{
class Program
{
private static ISiloHost silo;
private static readonly ManualResetEvent siloStopped = new ManualResetEvent(false);
static void Main(string[] args)
{
silo = new SiloHostBuilder()
.UseLocalhostClustering()
.Configure<ClusterOptions>(options =>
{
options.ClusterId = "OrleansDotNet-cluster";
options.ServiceId = "OrleansDotNet";
})
.Configure<EndpointOptions>(options => options.AdvertisedIPAddress = IPAddress.Loopback)
.ConfigureApplicationParts(parts => parts.AddApplicationPart(typeof(GraoContador).Assembly).WithReferences())
.ConfigureLogging(logging => logging.AddConsole())
.Build();
Task.Run(StartSilo);
AssemblyLoadContext.Default.Unloading += context =>
{
Task.Run(StopSilo);
siloStopped.WaitOne();
};
siloStopped.WaitOne();
}
private static async Task StartSilo()
{
await silo.StartAsync();
Console.WriteLine("Silo iniciado");
}
private static async Task StopSilo()
{
await silo.StopAsync();
Console.WriteLine("Silo parado");
siloStopped.Set();
}
}
}
Nesta classe, estamos definindo que o Silo irá utilizar as configurações de um cluster local (UseLocalhostClustering()
), também é definido as identificações do cluster, bem com o seu IP (Configure()
). Por fim, se define o grão que será adicionado ao silo e onde o seu log deve ser exibido.
Com o silo definido, ele é iniciado. Ele só irá parar em caso de algum erro ou quando o usuário forçar a sua parada.
Criando o projeto Interface
Com o Silo definido podemos criar a nossa aplicação interface. Neste projeto vou criar uma aplicação WebAPI chamada InterfaceAPI:
dotnet new webapi -n InterfaceApi
Aproveite e já vincule os projetos a solução:
dotnet sln OrleansDotNet.sln add **/*.csproj
Na api é necessário adicionar a referência Microsoft.Orleans.Client
:
dotnet add package Microsoft.Orleans.Client
E referenciar o projeto GraosInterfaces:
dotnet add InterfaceApi.csproj reference ../GraosInterfaces/GraosInterfaces.csproj
Para trabalhar com “dependency injection”, vamos configurar o cliente do Orleans na classe Startup
conforme o código abaixo:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Orleans;
using Orleans.Runtime.Configuration;
using Orleans.Hosting;
using System.Net;
using Orleans.Configuration;
using OrleansDotNet.GraosInterfaces;
namespace InterfaceApi
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddSingleton<IClusterClient>(provider =>
{
var client = new ClientBuilder()
.UseLocalhostClustering()
.Configure<ClusterOptions>(options =>
{
options.ClusterId = "OrleansDotNet-cluster";
options.ServiceId = "OrleansDotNet";
})
.ConfigureApplicationParts(parts => parts.AddApplicationPart(typeof(IGraoContador).Assembly).WithReferences())
.ConfigureLogging(logging => logging.AddConsole())
.Build();
StartClientWithRetries(client).Wait();
return client;
});
}
private static async Task StartClientWithRetries(IClusterClient client)
{
for (var i=0; i<5; i++)
{
try
{
await client.Connect();
return;
}
catch(Exception)
{ }
await Task.Delay(TimeSpan.FromSeconds(5));
}
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, Microsoft.AspNetCore.Hosting.IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMvc();
app.UseStaticFiles();
}
}
}
Note que a configuração do cliente é parecida com o do host:
var client = new ClientBuilder()
.UseLocalhostClustering()
.Configure<ClusterOptions>(options =>
{
options.ClusterId = "OrleansDotNet-cluster";
options.ServiceId = "OrleansDotNet";
})
.ConfigureApplicationParts(parts => parts.AddApplicationPart(typeof(IGraoContador).Assembly).WithReferences())
.ConfigureLogging(logging => logging.AddConsole())
.Build();
As diferenças é que agora estamos utilizando a classe ClientBuilder
e se adiciona a interface IGraoContador
(em detrimento a GraoContador
definida no host.
Nesta classe também foi definido o método StartClientWithRetries
que tenta se conectar ao host 5 vezes antes de desistir. Quando a conexão for obtida, o objeto client
é retornado.
Este cliente será obtido no controller, conforme o código abaixo:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Orleans;
using OrleansDotNet.GraosInterfaces;
namespace InterfaceApi.Controllers
{
[Route("api/[controller]")]
public class ContadorController : Controller
{
private IClusterClient client;
public ContadorController(IClusterClient client){
this.client = client;
}
[HttpGet]
public async Task<int> Get()
{
var contador = client.GetGrain<IGraoContador>("TW-1");
return await contador.GetContador();
}
[HttpPost]
public async Task Post()
{
var contador = client.GetGrain<IGraoContador>("TW-1");
await contador.Incremento(1);
}
}
}
Para obter o grão, utilizamos o método GetGrain
. A este método deve ser passado a chave do grão:
var contador = client.GetGrain<IGraoContador>("TW-1");
Como definimos que a chave dele será uma string, acima estou passando uma string arbitrária. A partir da instância obtida os métodos do grão são chamados, como o GetContador()
:
return await contador.GetContador();
Com isso, nós estamos obtendo informações do nosso grão contido no silo.
Para finalizar, criei dentro da pasta wwwroot
um arquivo HTML, onde os métodos da api são chamados:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<title>Exemplo de Orleans, no ASP.NET Core *.*</title>
</head>
<body>
<div class="container">
<div class="card">
<div class="card-body">
Contador: <spam id="countValor">0</spam>
<button id="btnIncrement" class="btn-primary">Incrementar</button>
</div>
</div>
</div>
<!-- JavaScript -->
<script src="https://code.jquery.com/jquery-3.1.0.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
<script type="text/javascript">
$(document).ready(function () {
$.get("api/contador/", function (value) {
console.log('GET contador=' + value);
$('#countValor').html(value);
});
$('#btnIncrement').click(function () {
$.post("api/contador/", function () {
$.get("api/contador/", function (value) {
console.log('GET contador=' + value);
$('#countValor').html(value);
});
});
return false;
});
});
</script>
</body>
</html>
Pronto, a nossa aplicação está finalizada. Para testá-la, primeiro é necessário iniciar o Silo:
Em seguida a aplicação web:
No navegador teremos o resultado abaixo:
Ao clicar em “Incrementar”, veremos o valor do contador ser incrementado:
Conclusão
Este é um exemplo simples, porém funcional, que nos dá uma noção do poder do Orleans. Em artigos futuros mostrarei mais detalhes dele.
Caso queria executar a aplicação demostrada aqui no curso, você pode vê-la aqui no GitHub.