Template Arquitectura Orleans Client¶
Quickstart¶
- En GitHub seleccionar la opción para crear un nuevo repository
- Seleccionar el template
architecture-it/Template-Platform-Orleans
- Seleccionar un nombre y crear al repositorio
- Configurar politicas de seguridad de branch
- Asignar usuarios/teams al repositorio
- Cambiar nombre de los proyectos de
OrleansClient
y*.Grains
)
Arquitectura de Software¶
El template contiene la siguiente arquitectura.
Los proyectos OrleansClient
, Application
, Domain
, Infrastructure
corresponde a la arquitectura que posee actualmente platform con el estandar de Clean Architecture
OrleansServer: Este proyecto se utiliza para configurar y ejecutar un servidor de Orleans durante la fase de desarrollo. No se utiliza en entornos de producción o pruebas. Cualquier modificación que hagamos aquí, como añadir inyecciones de dependencias o cambiar configuraciones, no se reflejará en los servidores de Orleans en entornos de producción o pruebas. Por lo tanto, es crucial llevar un registro de estos cambios y aplicarlos manualmente en los servidores correspondientes si es necesario.
Para realizar la configuración utilizamos la librería Andreani.Arq.Orleans.Server
var builder = WebApplication.CreateBuilder(args);
builder.Host.ConfigureAndreaniWebHost(args)
.AddServerOrleansLocal();
builder.Services
.ConfigureAndreaniWorkerServices()
.AddObservability();
var app = builder.Build();
app.ConfigureAndreaniWorker()
.ConfigureObservability();
app.UseDashboardOrleans();
app.Run();
OrleansClient.Grain.Interface y OrleansClient.Grain
Estos proyectos se adhieren a las mejores prácticas recomendadas por Microsoft. En el proyecto OrleansClient.Grain.Interface, encontrarás las abstracciones o interfaces que son implementadas exclusivamente por los Clientes. Por otro lado, el proyecto OrleansClient.Grain contiene la implementación real de estos granos, es decir, la lógica de negocio que los granos ejecutan en el servidor.
Nota
Es crucial cambiar los nombres de los proyectos para que reflejen de manera clara y única la identidad del proyecto en el que se están utilizando.
Aplication.Test y OrleansClient.Grain.Test Estos proyectos contienen test de los ejemplos del template y estan preparados para realizar Unit test sobre los Grain de Orleans. Ver más información de Unit Test en Orleans
Ejemplos Funcionales¶
Workflow de aplicación
Person CRUD¶
Create Person¶
[HttpPost]
[ProducesResponseType(typeof(CreatePersonResponse), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(List<Notify>), StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Create(CreatePersonCommand body) =>
Result(await Mediator.Send(body));
private readonly IClusterClient _client;
private readonly ILogger<CreatePersonCommandHandler> _logger;
public CreatePersonCommandHandler(IClusterClient client, ILogger<CreatePersonCommandHandler> logger)
{
_client = client;
_logger = logger;
}
public async Task<Response<CreatePersonResponse>> Handle(CreatePersonCommand request, CancellationToken cancellationToken)
{
var info = new PersonInfo()
{
Nombre = request.Nombre,
Apellido = request.Apellido
};
var person = await _client.GetGrain<IPersonGrains>(Guid.NewGuid())
.CreateOrUpdate(info);
_logger.LogDebug("the person was add correctly");
return new Response<CreatePersonResponse>
{
Content = new CreatePersonResponse
{
Message = "Success",
PersonId = person.Id,
},
StatusCode = System.Net.HttpStatusCode.Created
};
}
public class PersonGrains : IGrainBase, IPersonGrains
{
// .... OmitCode
public async Task<PersonDto> CreateOrUpdate(PersonInfo info)
{
_state.State = new PersonStateGrain()
{
Apellido = info.Apellido,
Nombre = info.Nombre,
};
await _state.WriteStateAsync();
await _grainFactory.GetGrain<IBigDirectoryGuidGrain>(nameof(IPersonGrains)).AddAsync(_id);
return MapState();
}
}
- Mapeamos la información nueva del estado y lo persistimos en la base de datos.
- Agregamos el grain al directorio de IPersonGrains Para ver como funcionan los directorios ver
Update Person¶
Se omite el controller
public async Task<Response<PersonDto>> Handle(UpdatePersonCommand request, CancellationToken cancellationToken)
{
var response = new Response<PersonDto>();
Guid _id = Guid.Parse(request.PersonId);
var exist = await _client.GetGrain<IBigDirectoryGuidGrain>(nameof(IPersonGrains))
.Exist(_id);
if (!exist)
{
response.AddNotification("#3123", nameof(request.PersonId), string.Format(ErrorMessage.NOT_FOUND_RECORD, "Person", request.PersonId));
response.StatusCode = System.Net.HttpStatusCode.NotFound;
return response;
}
var info = new PersonInfo()
{
Nombre = request.Nombre,
Apellido = request.Apellido
};
var result = await _client.GetGrain<IPersonGrains>(_id)
.CreateOrUpdate(info);
response.Content = result;
return response;
}
- Validamos que exista el Id ingresado. Recordemos que orleans al realizar una llamada de GetGrain con un Id, en caso de que no exista lo crea automaticamente, en este caso no queremos crear uno nuevo, solo actualizar uno existente. Para eso utilizamos el directorio de grain para IPersonGrains.
- Hidratamos el grain y actualizamos el estado
Get Persons¶
public async Task<Response<List<PersonDto>>> Handle(ListPerson request, CancellationToken cancellationToken)
{
var directory = _client.GetGrain<IBigDirectoryGuidGrain>(nameof(IPersonGrains));
var grains = await directory.GetAll<IPersonGrains, PersonDto>(Guid.Empty.ToString());
return new Response<List<PersonDto>>
{
Content = grains.Content,
StatusCode = System.Net.HttpStatusCode.OK
};
}
User CRUD¶
Register¶
[HttpPost("register")]
[ProducesResponseType(typeof(UserRegisterDto), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(List<Notify>), StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Register(UserRegisterDto body) =>
Result(await Mediator.Send(new UserRegisterRequest(body)));
public async Task<Response<UserRegisterDto>> Handle(UserRegisterRequest request, CancellationToken cancellationToken)
{
var result = new Response<UserRegisterDto>();
try
{
if (await _client.GetGrain<IBigDirectoryStringGrain>(nameof(IUserGrains))
.Exist(request.userInfo.Email))
{
result.AddNotification("#3123", nameof(request.userInfo), "the email has already been used");
result.StatusCode = System.Net.HttpStatusCode.Conflict;
return result;
}
await _client.GetGrain<IUserGrains>(request.userInfo.Email)
.Register(request.userInfo.Password, new(request.userInfo.Name, request.userInfo.Lastname));
result.Content = request.userInfo;
result.StatusCode = System.Net.HttpStatusCode.Created;
}
catch (InvalidLoginException ex)
{
result.AddNotification(new Notify
{
Code = "1",
Message = ex.Message,
Property = nameof(request.userInfo)
});
}
return result;
}
- Validamos que el email ya no haya sido utilizado.
- Cremos el Grain con el email del usuario.
- Almacenamos la información del usuario.
public async Task Register(string Password, PersonInfo info)
{
if (!string.IsNullOrEmpty(_userState.State.Password))
throw new InvalidLoginException("El usuario ya existe");
var person = await _grainFactory.GetGrain<IPersonGrains>(Guid.NewGuid())
.CreateOrUpdate(info);
await _grainFactory.GetGrain<IBigDirectoryStringGrain>(nameof(IUserGrains))
.AddAsync(_username);
_userState.State = new UserStateGrain
{
PersonId = person.Id,
LastLogin = null,
Password = Password,
};
await _userState.WriteStateAsync();
}
- Creamos un grain de IPersonGrains
- Registramos el Grain en el direcorio de IUserGrains
- Persistimos la información
Get Users¶
[HttpGet]
[ProducesResponseType(typeof(List<UserDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(List<Notify>), StatusCodes.Status403Forbidden)]
public async Task<IActionResult> GetAll(string? offset = null, int limit = 10) =>
Result(await Mediator.Send(new GetListUserRequest(offset, limit)));
public async Task<Response<UsersResponse>> Handle(GetListUserRequest request, CancellationToken cancellationToken)
{
var grains = await _client.GetGrain<IBigDirectoryStringGrain>(nameof(IUserGrains))
.GetAll<IUserGrains, UserDto>(request.Offset, limit: request.Limit);
return new Response<UsersResponse>
{
Content = new UsersResponse
{
Data = grains.Content,
limit = grains.Limit,
Next = grains.Next == null || Guid.Parse(grains.Next) == Guid.Empty ? null : grains.Next,
},
StatusCode = System.Net.HttpStatusCode.OK
};
}
Manejo de Timers y Reminders¶
Timer¶
Los temporizadores se utilizan en Orleans para crear comportamientos periódicos que no requieren múltiples activaciones (instancias de grano). Un temporizador en Orleans es similar a la clase estándar .NET System.Threading.Timer.
A continuación, se describen los pasos para implementar un temporizador en Platform-Orleans-Template
, junto con ejemplos de código:
- Crear la interfaz
ITimerGrain
con los métodosTask Start()
,Task Reset()
, yTask Stop()
. Aquí tienes un ejemplo:
public interface ITimerGrain : IGrainWithIntegerKey
{
Task Start();
Task Reset();
Task Stop();
}
- Crear la clase que implementa la interfaz
ITimerGrain
. Aquí tienes un ejemplo de implementación:
public class TimerGrain : ITimerGrain, IGrainBase
{
private readonly ITimerRegistry _timerRegistry;
private IDisposable? _timer;
private int _tick;
private readonly ILogger<TimerGrain> _logger;
public IGrainContext GrainContext { get; }
public TimerGrain(ITimerRegistry timerRegistry,ILogger<TimerGrain> logger, IGrainContext context)
{
_timerRegistry = timerRegistry;
_tick = 0;
_logger = logger;
GrainContext = context;
}
public Task Start()
{
_timer = _timerRegistry.RegisterTimer(GrainContext, x =>
{
_tick++;
_logger.LogInformation("Hello timmer {estado}", _tick);
return Task.CompletedTask;
}, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2));
return Task.CompletedTask;
}
public Task Stop()
{
if( _tick!=0 )
{
_timer?.Dispose();
_tick=0;
}
return Task.CompletedTask;
}
public async Task Reset()
{
await Stop();
await Start();
}
}
- Utilizar
TimerGrain
desde controller o handler, Aquí tienes un ejemplo de cómo utilizarlo desde un controlador:
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]")]
public class TimerController : ApiControllerBase
{
private readonly IClusterClient _client;
public TimerController(IClusterClient client)
{
_client = client;
}
[HttpGet("start")]
public IEnumerable<string> Get()
{
var timer = _client.GetGrain<ITimerGrain>(0);
timer.Start();
return new string[] { "value1", "value2" };
}
[HttpGet("stop")]
public IEnumerable<string> Stop()
{
var timer = _client.GetGrain<ITimerGrain>(0);
timer.Stop();
return new string[] { "value1", "value2" };
}
[HttpGet("reset")]
public IEnumerable<string> Reset()
{
var timer = _client.GetGrain<ITimerGrain>(0);
timer.Reset();
return new string[] { "value1", "value2" };
}
}
Reminder¶
Los recordatorios en Orleans son similares a los temporizadores, pero con algunas diferencias clave:
-
Los recordatorios son persistentes y continúan activándose en casi todas las situaciones, a menos que se cancelen explícitamente.
-
Los recordatorios están asociados a un grano en lugar de una activación específica.
-
Si un grano no tiene ninguna activación asociada cuando se activa un recordatorio, se crea una activación para ese grano.
-
La entrega de un recordatorio se realiza mediante mensajes y sigue la misma semántica de intercalación que otros métodos de grano.
-
Los recordatorios no deben utilizarse para intervalos de tiempo muy cortos; su período debería medirse en minutos, horas o días.
A continuación mostramos los pasos a seguir para implementar un recordatorio en Platform-Orleans-Template
y ejemplos en código:
- Crear la interfaz
IReminderGrain
con los métodosTask Start()
yTask Stop()
. Aquí tienes un ejemplo:
public interface IReminderGrain : IGrainWithIntegerKey
{
Task Start();
Task Stop();
}
- Crear la clase que implementa la interfaz
IReminderGrain
y también implementa la interfazIRemindable
. Aquí tienes un ejemplo de implementación:
public class ReminderGrain : IReminderGrain, IGrainBase, IRemindable
{
private IGrainReminder _reminder=null!;
private readonly ILogger<ReminderGrain> _logger;
public IGrainContext GrainContext { get; }
public ReminderGrain( ILogger<ReminderGrain> logger, IGrainContext context)
{
_logger = logger;
GrainContext = context;
}
public async Task Start()
{
_reminder ??=await this.RegisterOrUpdateReminder(
"ReminderTest",
TimeSpan.Zero,
TimeSpan.FromMinutes(1));
}
public async Task Stop()
{
if(_reminder != null)
{
await this.UnregisterReminder(_reminder);
}
else
{
_reminder = await this.GetReminder("ReminderTest");
await this.UnregisterReminder(_reminder);
}
}
public Task ReceiveReminder(string reminderName, TickStatus status)
{
if (reminderName.Equals("ReminderTest"))
{
_logger.LogInformation("Reminder test is ok");
}
return Task.CompletedTask;
}
}
- Utilizar
ReminderGrain
desde controller o handler. Aquí tienes un ejemplo de cómo utilizarlo desde un controlador:
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]")]
public class ReminderController : ApiControllerBase
{
private readonly IClusterClient _client;
public ReminderController(IClusterClient client)
{
_client = client;
}
[HttpGet("start")]
public IEnumerable<string> Get()
{
var reminder = _client.GetGrain<IReminderGrain>(0);
reminder.Start();
return new string[] { "value1", "value2" };
}
[HttpGet("stop")]
public IEnumerable<string> Stop()
{
var reminder = _client.GetGrain<IReminderGrain>(0);
reminder.Stop();
return new string[] { "value1", "value2" };
}
}
- Agregar la configuración en el archivo
appsettings
. Aquí tienes un ejemplo de cómo hacerlo en formato YAML o JSON:
Orleans:
ReminerServices:
Invariant: System.Data.SqlClient
ConnectionString: Server=localhost;Database=Orleans;User Id=sa;Password=Password1234;
{
"Orleans": {
"ReminerServices": {
"Invariant": "System.Data.SqlClient",
"ConnectionString": "Server=localhost;Database=Orleans;User Id=sa;Password=Password1234;"
}
}
}
Notas
Si estás utilizando el template Platform-Orleans-Template, no es necesario agregar la configuración en appsettings
ya que viene configurada por defecto y se puede cambiar si necesitas utilizar una conexión diferente a la configuración predeterminada.
Para obtener más información sobre los Timers
y Reminders
puede dirigirse a la documentación oficial de Orleans.
Manejo de Streams¶
Orleans streaming es una característica poderosa del framework Orleans que permite a los desarrolladores escribir aplicaciones reactivas que operan sobre secuencias de eventos de forma estructurada. Orleans streaming proporciona un conjunto de abstracciones y APIs que simplifican y robustecen la manipulación de streams. Un flujo en Orleans es una entidad lógica que siempre existe y nunca puede fallar, identificado por su StreamId. Los flujos permiten desacoplar la generación de datos de su procesamiento, en el tiempo y el espacio. Estas secuencias funcionan uniformemente en todos los granos y clientes de Orleans y son compatibles con una amplia gama de tecnologías de colas, como Event Hubs, ServiceBus, Azure Queues y Apache Kafka. Además, la transmisión de Orleans admite enlaces de transmisión dinámicos, gestión transparente del ciclo de vida de consumo de transmisión y proveedores de transmisión extensibles.
Configuración¶
Para configurar el servicio de streaming en el template Platform-Orleans-Template, sigue estos pasos:
- Configuración del servicio de streaming en el archivo
Program.cs
del servidor:
var builder = WebApplication.CreateBuilder(args);
builder.Host.ConfigureAndreaniWebHost(args)
.AddServerOrleansLocal(silo =>
{
silo.AddMemoryStreams("StreamProvider").AddMemoryGrainStorage("PubSubStore");
});
builder.Services.ConfigureAndreaniWorkerServices()
.AddGrainsDependency(builder.Configuration)
.AddObservability();
var app = builder.Build();
app.ConfigureAndreaniWorker().ConfigureObservability();
app.UseDashboardOrleans();
app.Run();
- Configuración del servicio de streaming en el archivo
Program.cs
del cliente:
var builder = WebApplication.CreateBuilder(args);
builder.Host.ConfigureAndreaniWebHost(args).
ConfigureOrleansClientLocal(client=>
{
client.AddMemoryStreams("StreamProvider");
});
builder.Services.ConfigureAndreaniServices()
.AddApplication()
.AddInfrastructure(builder.Configuration)
.AddCors(p => p.AddDefaultPolicy(x => x.AllowAnyOrigin().AllowAnyHeader()))
.AddObservability();
var app = builder.Build();
app.UseCors();
app.ConfigureAndreani().ConfigureObservability();
await Task.Delay(8000);
app.Run();
Notas
En los ejemplos anteriores se utiliza el proveedor de streaming MemoryStreamProvider
, ya que utilizar un proveedor de streaming externo requiere de una configuración adicional.
Publicar un evento¶
Para publicar un evento en un stream, sigue estos pasos:
- Crear la interfaz
IPublisherGrain
con el métodoTask Publish(string message)
:
public interface IPublisherGrain : IGrainWithStringKey
{
Task Publish(string message);
}
- Crear la clase que implementa la interfaz
IPublisherGrain
:
public class PublisherGrain: IGrainBase, IPublisherGrain
{
public IGrainContext GrainContext { get; }
private readonly IAsyncStream<string> _stream;
public PublisherGrain(IGrainContext grainContext)
{
GrainContext = grainContext;
// Creates a GUID for a chat room bean and a chat room stream
var guid = Guid.NewGuid() ;
// Gets one of the providers which we defined in our config
var streamProvider = this.GetStreamProvider("StreamProvider");
// Gets the reference to a stream
var streamId = StreamId.Create("RANDOMDATA", guid);
_stream = streamProvider.GetStream<string>(streamId);
}
public async Task Publish(string message)
{
await _stream.OnNextAsync(message);
}
}
- Utiliza
PublisherGrain
desde controller o handler. Aquí tienes un ejemplo de cómo utilizarlo desde un controlador:
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]")]
public class StreamController : ApiControllerBase
{
private readonly IClusterClient _client;
public StreamController(IClusterClient client)
{
_client = client;
}
[HttpGet("publish")]
public async Task<IEnumerable<string>> Get()
{
var publisher = _client.GetGrain<IPublisherGrain>("0");
await publisher.Publish("Hello World!!!!!");
return new string[] { "value1", "value2" };
}
}
Suscribirse a un evento¶
Para suscribirse a un evento en un stream, debes implementar IGrainBase
y agregar el atributo ImplicitStreamSubscription
al grano, especificando el topico al que deseas suscribirte. Aquí tienes un ejemplo:
[ImplicitStreamSubscription("RANDOMDATA")]
public class ConsumerGrain : IGrainBase, IGrainWithStringKey
{
public IGrainContext GrainContext { get; }
private readonly IAsyncStream<string> _stream;
private readonly ILogger<ConsumerGrain> _logger;
public ConsumerGrain(IGrainContext grainContext, ILogger<ConsumerGrain> logger)
{
GrainContext = grainContext;
_logger = logger;
// Creates a GUID based on our GUID as a grain
var guid = this.GetPrimaryKey();
// Gets one of the providers which we defined in config
var streamProvider = this.GetStreamProvider("StreamProvider");
// Gets the reference to a stream
var streamId = StreamId.Create("RANDOMDATA", guid);
_stream = streamProvider.GetStream<string>(streamId);
}
public async Task OnActivateAsync(CancellationToken _)
{
await _stream.SubscribeAsync(OnNextAsync);
}
public Task OnNextAsync(string item, StreamSequenceToken? token = null)
{
_logger.LogInformation($"Received: {item}");
return Task.CompletedTask;
}
}
Para mas información sobre orleans streaming puede dirigirse a la documentación oficial de Orleans.
Integración Continua¶
El template cuenta con la configuración por defecto para el proceso de CI estandar de la compañía, es necesario realizar las configuración de politicas de seguridad en las branch.
- No permitir push a las protegidas. Ej: main y cicd.
- Analísis de SonarQube bloqueante en Pull Request.
Despliegue Continuo¶
Nuget Grains¶
Para publicar los Grains como paquetes NuGet en GitHub Packages, el template incluye un pipeline que se activa automáticamente al detectar cambios en la carpeta Grain
. Sin embargo, es esencial gestionar de forma consciente la versión del paquete que se desea publicar. La configuración para estos paquetes se halla en el archivo packages.props
, que está situado en el directorio raíz (root
).
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project=".\Build\Common.props" />
<PropertyGroup>
<Authors>Andreani Architecture it</Authors>
<Company>Andreani</Company>
<Version>1.0.0</Version>
<IsPackable>true</IsPackable>
<!--<PackageProjectUrl>{repository url}</PackageProjectUrl>
<RepositoryUrl>{repository url}</RepositoryUrl>-->
</PropertyGroup>
</Project>
Authors
debe ser reeplazada por el equipo dueño de esta aplicación
La etiqueta Version
determina la versión que se asignará al paquete. Es crucial llenar los campos PackageProjectUrl
y RepositoryUrl
con la URL del repositorio de tu aplicación.
Dockerfile¶
El cliente se va a desplegar con estrategía Dockerfile, este template posee una imagenes exclusivas para NET 7 y Orleans
FROM ghcr.io/architecture-it/net-sdk:7.0 AS publish
WORKDIR /app
COPY . .
WORKDIR "/app/src/Api"
RUN dotnet publish "{appname}.csproj" -c Release -o /app/publish
FROM ghcr.io/architecture-it/net:7.0
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "{appname}.dll"]
Warning
Reemplazar appname con el nombre final del proyecto que reemplaza a OrleansClient