arrow-left

All pages
gitbookPowered by GitBook
1 of 4

Loading...

Loading...

Loading...

Loading...

Setup

Prerequisite software: Docker

hashtag
Creating a projects

Create a new console application project in a new folder using dotnet command.

Also, create a Web API project using the same folder for communicating with our Service. Then add both projects to the common solution.

Then we add the Cronus dependency.

This is the minimum set of packages for our Cronus host to work.

hashtag
Run docker images

  • Setup Cassandra (Container memory is limited to 2GB): docker run --restart=always -d --name cassandra -p 9042:9042 -p 9160:9160 -p 7199:7199 -p 7001:7001 -p 7000:7000 cassandra

  • Setup RabbitMq (Container memory is limited to 512MB): docker run --restart=always -d --hostname node1 -e RABBITMQ_NODENAME=docker-UNIQUENAME-rabbitmq --name rabbitmq -p 15672:15672 -p 5672:5672 elders/rabbitmq:3.8.3

hashtag
Setup configuration file

Add appsettings.json with the following configuration into the project folder.

//This should be int the Service and in the Api.

You can also see how the Cronus application can be configured in more detail in

This is the code that your Program.cs in TaskManager.Service should contain.

This is the code that you should add in the Program.cs in TaskManager.Api.

hashtag
F5

Explore Projections

Projections are queryable models used for the reading part of our application. We can design projections in such a way that we can manage what data we want to store and by what will be searched. Events are the basis for projections data.

For using projections we should update the configuration file for both API and Service.

And add some dependencies.

hashtag
Create a projection for querying tasks

You can choose whitch implementation to use. You can get hte tasks(commented in the controller) with same name, or all tasks.

Every time the event will occur it will be handled and persist in its state.

hashtag
Read the state

Inject IProjectionReader that will be responsible for getting the projection state by Id on which projection was subscribed before: Subscribe<TaskCreated>(x => x.UserId).

hashtag
Connect Dashboard

(The dashboard is not requerd)

If we hit this controller immediately after the first start, it could lead to a probable read error. We need to give it some time to initialize our new projection store and build new versions of the projections. For an empty event store, it could take less than a few seconds but in order not to wait for this and verify that all working properly, we will check it manually.

is a UI management tool for the Cronus framework. It hosts inside our Application so add this missing code to our background service.

Start our Cronus Service and API.

In the dashboard select the Connections tab and click New Connection. Set the predefined port for the Cronus endpoint: and specify your connection name. Click Check and then Add Connection. After you add a connection select it from the drop-down menu and navigate to the Projections tab. You would be able to see all projections in the system.

Now we would be able to request a controller with userId. GetAsync method of IProjectionReader will restore all events related to projection and apply them to the state.

> dotnet new console --name TaskManager.Service
 dotnet new webapi --name TaskManager.Api
cd TaskManager.Api
dotnet add package Cronus

cd ../TaskManager.Service
dotnet add package Cronus
dotnet add package Cronus.Transport.RabbitMQ
dotnet add package Cronus.Persistence.Cassandra
dotnet add package Cronus.Serialization.NewtonsoftJson
dotnet add package Microsoft.Extensions.Hosting
appsettings.json
  "Persistence": { /* ... */ },
  "Projections": {
      "Cassandra": {
        "ConnectionString": "Contact Points=127.0.0.1;Port=9042;Default Keyspace=taskmanager_projections"
      }
  }
dotnet add package Cronus.Projection.Cassandra
Configuration.
Ensure that service has been started properly.
Cronus Dashboardarrow-up-right
http://localhost:7477arrow-up-right
A live green badge means that the projection is synchronized with ES and ready to use.
[DataContract(Name = "c94513d1-e5ee-4aae-8c0f-6e85b63a4e03")]
public class TaskProjection : ProjectionDefinition<TaskProjectionData, TaskId>,
    IEventHandler<TaskCreated>
{
    public TaskProjection()
    {
        //Id.NID - here we are subscribing by tenant
        //in our case the tenant is: "tenant"
        //so we well get all events
        Subscribe<TaskCreated>(x => new TaskId(x.Id.NID));
    }

    public Task HandleAsync(TaskCreated @event)
    {
        Data task = new Data();

        task.Id = @event.Id;
        task.UserId = @event.UserId;
        task.Name = @event.Name;
        task.Timestamp = @event.Timestamp;

        State.Tasks.Add(task);

        return Task.CompletedTask;
    }
    public IEnumerable<Data> GetTaskByName(string name)
    {
        return State.Tasks.Where(x => x.Name.Equals(name));
    }
}
appsettings.json
{
  "Cronus": {
    "BoundedContext": "taskmanager",
    "Tenants": [ "tenant" ],
    "Transport": {
        "RabbitMQ": {
            "Server": "127.0.0.1",
            "VHost": "taskmanager"
        },
        "PublicRabbitMQ": [
            {
                "Server": "127.0.0.1",
                "VHost": "unicom-public",
                "FederatedExchange": {
                    "UpstreamUri": "guest:guest@localhost:5672",
                    "VHost": "unicom-public",
                    "UseSsl": false,
                    "MaxHops": 1
                }
            }
        ]
    },
    "Persistence": {
        "Cassandra": {
            "ConnectionString": "Contact Points=127.0.0.1;Port=9042;Default Keyspace=taskmanager_es"
        }
    },
    "Projections": {
        "Cassandra": {
            "ConnectionString": "Contact Points=127.0.0.1;Port=9042;Default Keyspace=taskmanager_projections"
        }
    },
    "Cluster": {
        "Consul": {
            "Address": "127.0.0.1"
        }
    },
    "AtomicAction": {
        "Redis": {
            "ConnectionString": "127.0.0.1:6379"
        }
    }
}
}
Program.cs
using Cronus11Service;
using Elders.Cronus;

IHost host = Host.CreateDefaultBuilder(args)
        .ConfigureServices((hostContext, services) =>
        {
            services.AddHostedService<Worker>();
            services.AddCronus(hostContext.Configuration);

        })
        .UseDefaultServiceProvider((context, options) =>
        {
            options.ValidateScopes = context.HostingEnvironment.IsDevelopment();
            options.ValidateScopes = false;
            options.ValidateOnBuild = false;
        })
        .Build();

host.Run();
Program.cs
builder.Services.AddCronus(builder.Configuration);

builder.Host.UseDefaultServiceProvider((context, options) =>
{
    options.ValidateScopes = context.HostingEnvironment.IsDevelopment();
    options.ValidateScopes = false;
    options.ValidateOnBuild = false;
}
);

....

app.UseCronusAspNetCore();
[DataContract(Name = "c135893e-b9e3-453a-b0e0-53545094ec5d")]
public class TaskProjectionData
{
    public TaskProjectionData()
    {
        Tasks = new List<Data>();
    }

    [DataMember(Order = 1)]
    public List<Data> Tasks { get; set; }

    [DataContract(Name = "317b3cbb-593a-4ffc-8284-d5f5c599d8ae")]
    public class Data
    {
        [DataMember(Order = 1)]
        public TaskId Id { get; set; }

        [DataMember(Order = 2)]
        public UserId UserId { get; set; }

        [DataMember(Order = 3)]
        public string Name { get; set; }

        [DataMember(Order = 4)]
        public DateTimeOffset CreatedAt { get; set; }

        [DataMember(Order = 5)]
        public DateTimeOffset Timestamp { get; set; }
    }
}
[ApiController]
[Route("[controller]/[action]")]
public class TaskController : ControllerBase
{
private readonly IPublisher<CreateTask> _publisher;
private readonly IProjectionReader _projectionReader;

public TaskController(IPublisher<CreateTask> publisher, IProjectionReader reader)
{
    _publisher = publisher;
    _projectionReader = reader;
}

//.... create task code ..//

[HttpGet]
public async Task<IActionResult> GetTasksByName(string name)
{

    ReadResult<TaskProjection> readResult = await _projectionReader.GetAsync<TaskProjection>(new TaskId("tenant"));

    if (readResult.IsSuccess == false)
        return NotFound();

    var TasksByName = readResult.Data.GetTaskByName(name);


    return Ok(TasksByName);

    ////Get all tasks
    //return Ok(readResult.Data.State.Tasks.Select(x => new TaskData
    //{
    //    CreatedAt = x.CreatedAt,
    //    Id = x.Id,
    //    Name = x.Name,
    //    Timestamp = x.Timestamp,
    //    UserId = x.UserId
    //}));
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    logger.LogInformation("Starting service...");
    cronusHost.Start();
    
    // Dashboard configuration
    cronusDashboard = CronusApi.GetHost();
    cronusApi.Provider = cronusDashboard.Services;
    await cronusDashboard.StartAsync().ConfigureAwait(false);
    
    logger.LogInformation("Service started!");
}

Quick Start

To help you get started quickly on the Cronus we will build an application that will satisfy all future business requirements.

hashtag
Business requirements

  • We need a new task management system.

  • We need data to be consistent.

  • We need to be able to reassign tasks inside the user group.

  • We need an accurate progress report for every user.

  • Groups progress report needs to be secured such that only group members can access it.

  • We need a notification to the group members when a user finishes his task.

  • We need a screen to view the historical changes in user activity.

  • When users close their accounts we need to ask them why (optional survey).

  • We need to generate a monthly report that indicates why lost users closed their accounts.

Persist First Event

hashtag
Create Ids, commands and events

First, we need to add a UserId and TaskId to have the Identifications of these two entities

[DataContract(Name = "d5e50e1f-5886-4608-9361-9fe0eb440a6b")]
[DataContract(Name = "00f5463f-633a-49f4-9fbe-f98e0911c2f5")]

Then we need to create a Cronus command for task creation and an Event that will indicate that the event has occurred.

hashtag
Create an Aggregate and Application Service

Add that inherits with the generic .

Apply method will pass the event to the state of an aggregate and change its state.

Finally, we can create an for command handling.

We register a handler by inheriting from ICommandHandler<>. When the command arrives we read the state of the aggregate, and if it is not found we create a new one and call SaveAsync to save its state to the database.

hashtag
Create Controller and send a request

Now we need a controller to publish our commands and create tasks.

Here we create TaskId and UserId and inject_IPublisher<CreateTask>_to publish the command. After this, the command will be sent to RabbitMq and then handled in Application Service.

Now let's start our Service and API. We should be able to make post requests to our Controller throw the Swagger and create our first Task in the system. It must be persisted in the .

hashtag
Inspection of the Event Store

Download or any other UI tool for Cassandra.

Let's take an Id from the response and encode it to Base64. Than try: select * from taskmanagerevents where id = 'dXJuOnRlbmFudDp0YXNrOmU1MjA1NTA3LWYyNmUtNGExMy05OTU4LTNjMzVlYzAwY2I1Yw=='

public class TaskId : AggregateRootId
{
TaskId() { }
public TaskId(string id) : base("tenant", "task", id) { }
}
public class UserId : AggregateRootId
{
UserId() { }
public UserId(string id) : base("tenant", "user", id) { }
}
Aggregate
AggregateRoot
state
Application Service
Event Store
DevCenter arrow-up-right
I highly recommend debugging on the first run to better understand the flow of program execution.
Use DevCenter tool for Cassandra visualization.
[DataContract(Name = "857d960c-4b91-49cc-98fd-fa543906c52d")]
public class CreateTask : ICommand
{
    public CreateTask() { }

    public CreateTask(TaskId id, UserId userId, string name, DateTimeOffset timestamp
    {
        if (id is null) throw new ArgumentNullException(nameof(id));
        if (userId is null) throw new ArgumentNullException(nameof(userId));
        if (name is null) throw new ArgumentNullException(nameof(name));
        if (timestamp == default) throw new ArgumentNullException(nameof(timestamp));

        Id = id;
        UserId = userId;
        Name = name;
        Timestamp = timestamp;
    }

    [DataMember(Order = 1)]
    public TaskId Id { get; private set; }

    [DataMember(Order = 2)]
    public UserId UserId { get; private set; }

    [DataMember(Order = 3)]
    public string Name { get; private set; }

    [DataMember(Order = 4)]
    public DateTimeOffset Timestamp { get; private set; }

    public override string ToString()
    {
        return $"Create a task with id '{Id}' and name '{Name}' for user [{UserId}].";
    }
}
[DataContract(Name = "728fc4e7-628b-4962-bd68-97c98aa05694")]
public class TaskCreated : IEvent
{
    TaskCreated() { }

    public TaskCreated(TaskId id, UserId userId, string name, DateTimeOffset timestamp)
    {
        Id = id;
        UserId = userId;
        Name = name;
        CreatedAt = DateTimeOffset.UtcNow;
        Timestamp = timestamp;
    }

    [DataMember(Order = 1)]
    public TaskId Id { get; private set; }

    [DataMember(Order = 2)]
    public UserId UserId { get; private set; }

    [DataMember(Order = 3)]
    public string Name { get; private set; }

    [DataMember(Order = 4)]
    public DateTimeOffset CreatedAt { get; private set; }

    [DataMember(Order = 5)]
    public DateTimeOffset Timestamp { get; private set; }

    public override string ToString()
    {
        return $"Task with id '{Id}' and name '{Name}' for user [{UserId}] at {CreatedAt} has been created.";
    }
}
public class TaskAggregate : AggregateRoot<TaskState>
{
    public TaskAggregate() { }

    public void CreateTask(TaskId id, UserId userId, string name, DateTimeOffset deadline)
    {
        IEvent @event = new TaskCreated(id, userId, name, deadline);
        Apply(@event);
    }
}
public class TaskState : AggregateRootState<TaskAggregate, TaskId>
{
    public override TaskId Id { get; set; }

    public UserId UserId { get; set; }

    public string Name { get; set; }

    public DateTimeOffset CreatedAt { get; set; }

    public DateTimeOffset Deadline { get; set; }

    public void When(TaskCreated @event)
    {
        Id = @event.Id;
        UserId = @event.UserId;
        Name = @event.Name;
        CreatedAt = @event.CreatedAt;
        Deadline = @event.Timestamp;
    }
}
[DataContract(Name = "ef669879-5d35-4cb7-baea-39a7c46c9e13")]
public class TaskService : ApplicationService<TaskAggregate>,
ICommandHandler<CreateTask>
{
    public TaskService(IAggregateRepository repository) : base(repository) { }

    public async Task HandleAsync(CreateTask command)
    {
        ReadResult<TaskAggregate> taskResult = await repository.LoadAsync<TaskAggregate>(command.Id).ConfigureAwait(false);
        if (taskResult.NotFound)
        {
            var task = new TaskAggregate();
            task.CreateTask(command.Id, command.UserId, command.Name, DateTimeOffset.UtcNow);
            await repository.SaveAsync(task).ConfigureAwait(false);
        }
    }
}
[ApiController]
[Route("[controller]/[action]")]
public class TaskController : ControllerBase
{
    private readonly IPublisher<ICommand> _publisher;

    public TaskController(IPublisher<ICommand> publisher)
    {
        _publisher = publisher;
    }

    [HttpPost]
    public IActionResult CreateTask(CreateTaskRequest request)
    {
        string id = Guid.NewGuid().ToString();
        string Userid = Guid.NewGuid().ToString();
        TaskId taskId = new TaskId(id);
        UserId userId = new UserId(Userid);
        var expireDate = DateTimeOffset.UtcNow;
        expireDate.AddDays(request.DaysActive);

        CreateTask command = new CreateTask(taskId, userId, request.Name, expireDate);

        if (_publisher.Publish(command) == false)
        {
            return Problem($"Unable to publish command. {command.Id}: {command.Name}");
        };
        return Ok(id);
    }
}
public class CreateTaskRequest
{
    [Required]
    public string Name { get; set; }

    [Required]
    public int DaysActive { get; set; }
}
)