In this guide you will learn all basics that are necessary to install and use Agents.Net. Additionally while going through the guide you will learn about the basic concepts used in Agents.Net like agents, messages and so on.
Contents
Concept
The basic idea of the framework is this. Each agent does one thing (connects to a database, reads console input, verifies some values, ...). For that it needs specific information (location of the database, the raw console input, ...). Additionally it provides all the information it knows (the active database connection, single console arguments and their values, ...). All information are handled in for of messages. The agent is not concerned where the information comes from or who needs the information provided. Based on that idea alone the system will organize all agents automatically simply based on who needs a specific information which was provided.
Installation
To use the current release simply add it via NuGet:
dotnet add package Agents.Net
To use to latest version from master use the latest NuGet package from github:
- Authenticating to github packages for this repository
- Add package via NuGet
nuget install Agents.Net -prerelease
Scenario
The image above shows the scenario that we are about to implement in this guide. The illustration of the agent community was generated with the Agents.Net.Designer which is currently still in its early alpha state. In this guide we will not use the designer but implement the community ourselves.
The goal of the community is to print "Hello World" to the console. But it will not do it directly. Rather at the start there are two agents (HelloAgent
and WorldAgent
), which provide the two words for the console. The ConsoleMessageJoiner
combines both words to the sentence "Hello World" and finally the ConsoleMessageDisplayAgent
will print the message to the console and terminate the program.
Implementation
In this chapter we will implement the above defined scenario.
Preparation
- Create a new .NET Core Console application named "HelloWorldApp"
- In the directory of the HelloWorldApp.csproj execute the command
dotnet add package Agents.Net
- Open the project in your favorite IDE
Implementing the community
Creating messages
Create the message classes HelloConsoleMessage
, WorldConsoleMessage
and ConsoleMessageCreated
with the following content
public class <MessageName>: Message
{
public <MessageName>(string message, Message predecessorMessage)
: base(predecessorMessage)
{
Message = message;
}
public <MessageName>(string message, IEnumerable<Message> predecessorMessages)
: base(predecessorMessages)
{
Message = message;
}
public string Message { get; }
protected override string DataToString()
{
return $"{nameof(Message)}: {Message}";
}
}
What are messages?
Messages are the communication/data objects that are passed between agents. Agents never interact directly with each other. All messages - except the InitializeMessage
- have one or more predecessor messages. These are the messages that led to the current instance. More on that later in an example. Each message can contain additional information beside their semantical one - meaning the information that is transfered by their type. In this example the messages have the Message
property as an additional information. In the DataToString
method the additional information are stringyfied. This is useful to have better logs - more on that later too.
Creating Agents
1. Create the class HelloAgent
with the following content
[Consumes(typeof(InitializeMessage))]
[Produces(typeof(HelloConsoleMessage))]
public class HelloAgent : Agent
{
public HelloAgent(IMessageBoard messageBoard) : base(messageBoard)
{
}
protected override void ExecuteCore(Message messageData)
{
OnMessage(new HelloConsoleMessage("Hello", messageData));
}
}
What are agents?
Agents are the acting objects in the system. All logic is contained within agents and only agents should execute any logic. They need specific information in form of messages. In this example the agent needs the InitializeMessage
as a trigger to execute its code. Additionally agents produce messages about the logic that was executed, containing the data that was generated. In this case the agent produces a HelloConsoleMessage
with the data "Hello".
2. Create the class WorldAgent
with the following content
[Consumes(typeof(InitializeMessage))]
[Produces(typeof(WorldConsoleMessage))]
public class WorldAgent : Agent
{
public WorldAgent(IMessageBoard messageBoard) : base(messageBoard)
{
}
protected override void ExecuteCore(Message messageData)
{
OnMessage(new WorldConsoleMessage("World", messageData));
}
}
What is implicit parallel execution?
The agent framework allows for implicit parallel execution. In case of this scenario, the HelloAgent
as well as the WorldAgent
will react on the InitializeMessage
that is send, when the message board starts. That means both agents can do their work parallel, although no code explicitly specifies that they can. The message board is designed in a way that these accidental potentials for parallel execution are used - meaning both agents will run parallel.
3. Create the class ConsoleMessageJoiner
with the following content
[Consumes(typeof(WorldConsoleMessage))]
[Consumes(typeof(HelloConsoleMessage))]
[Produces(typeof(ConsoleMessageCreated))]
public class ConsoleMessageJoiner : Agent
{
private readonly MessageCollector<HelloConsoleMessage, WorldConsoleMessage> collector;
public ConsoleMessageJoiner(IMessageBoard messageBoard) : base(messageBoard)
{
collector = new MessageCollector<HelloConsoleMessage, WorldConsoleMessage>(OnMessagesCollected);
}
private void OnMessagesCollected(MessageCollection<HelloConsoleMessage, WorldConsoleMessage> set)
{
OnMessage(new ConsoleMessageCreated($"{set.Message1.Message} {set.Message2.Message}", set));
}
protected override void ExecuteCore(Message messageData)
{
collector.Push(messageData);
}
}
How to safely consume more than one message?
The class above does consume two messages HelloConsoleMessage
and WorldConsoleMessage
. It is impossible to tell when an agent receives a specific message. It is possible to publish 1.000 messages at once. But the order in which they are executed is completely dependent on the whims of the ThreadPool
. Additionally, it is possible that any agent object executes multiple messages parallel. Because of that it is important to be extra careful, when storing something as a state of an agent. Agents.Net comes with two helper classes for this case: MessageCollector
and MessageAggregator
. In this scenario we will only use the MessageCollector
to safely collect the two consumed messages. Only when both messages were received, the passed action will be executed.
In this example we see the definition for the predecessor messages. In the first two agents above there was only one predecessor message - InitializeMessage
. Now with the collector, the message has two predecessor message HelloConsoleMessage
and WorldConsoleMessage
. The predecessors are important, because the message domain is derived from the predecessors. Message domains are not scope of this guide.
4. Create the class ConsoleMessageDisplayAgent
with the following content
[Consumes(typeof(ConsoleMessageCreated))]
public class ConsoleMessageDisplayAgent : Agent
{
private readonly Action terminateAction;
public ConsoleMessageDisplayAgent(IMessageBoard messageBoard, Action terminateAction) : base(messageBoard)
{
this.terminateAction = terminateAction;
}
protected override void ExecuteCore(Message messageData)
{
Console.WriteLine(messageData.Get<ConsoleMessageCreated>().Message);
terminateAction();
}
}
This agent prints the message that was generated by the ConsoleMessageJoiner
to the actual console and afterwards terminates the program by executing the terminateAction
. The terminateAction
will later be passed to the agent.
Gluing it all together
Here we will look at all the classes that are necessary to glue together all classes we created previously. For that we will not use a DI framework, as it is easier to show what we need to setup with this. Additionally we will configure Serilog to log the execution as this is important in order to "debug" Agents.Net.
1. Add Serilog to the project
dotnet add package Serilog
dotnet add package Serilog.Sinks.Async
dotnet add package Serilog.Sinks.File
dotnet add package Serilog.Formatting.Compact
2. Configure Serilog using the following code in the program's main
method
//Setup logging
File.Delete("log.json");
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Verbose()
.WriteTo.Async(l => l.File(new CompactJsonFormatter(), "log.json"))
.CreateLogger();
This will ensure that all Agents.Net message are logged - by setting the minimum level to verbose. Additionally this ensures that the impact on the execution time is minimal.
3. Setup the agent community using the following code in the program's main
method
//Setup community
using ManualResetEvent finishedEvent = new(false);
IMessageBoard messageBoard = new MessageBoard();
messageBoard.Register(new HelloAgent(messageBoard),
new WorldAgent(messageBoard),
new ConsoleMessageJoiner(messageBoard),
new ConsoleMessageDisplayAgent(messageBoard,
() => finishedEvent.Set()));
4. Write start code in the program's main
method
//Run
messageBoard.Start();
Execute the program
Look into the execution log
Here is the execution log that was generated for the execution of the implemented HelloWorldApp:
{"@t":"2020-12-24T17:52:51.8534304Z","@mt":"{@log}","@l":"Verbose","log":{"Agent":"WorldAgent","Type":"Executing","AgentId":"25ca3340-69c0-44b5-a538-0d97299499ec","Message":{"Name":"InitializeMessage","Id":"c163f51a-c19b-42a3-aede-585186324d32","Predecessors":[],"Domain":"3cf6acc6-a852-4c2e-a688-b247a94987eb","Data":"","Child":null,"$type":"MessageLog"},"$type":"AgentLog"}}
{"@t":"2020-12-24T17:52:51.8534661Z","@mt":"{@log}","@l":"Verbose","log":{"Agent":"HelloAgent","Type":"Executing","AgentId":"c531c6be-d4e0-421c-9bbb-6d64a5f5d7b6","Message":{"Name":"InitializeMessage","Id":"c163f51a-c19b-42a3-aede-585186324d32","Predecessors":[],"Domain":"3cf6acc6-a852-4c2e-a688-b247a94987eb","Data":"","Child":null,"$type":"MessageLog"},"$type":"AgentLog"}}
{"@t":"2020-12-24T17:52:51.8586857Z","@mt":"{@log}","@l":"Verbose","log":{"Agent":"WorldAgent","Type":"Publishing","AgentId":"25ca3340-69c0-44b5-a538-0d97299499ec","Message":{"Name":"WorldConsoleMessage","Id":"ca73e3bf-c380-4753-b7a8-987fca8ab56d","Predecessors":["c163f51a-c19b-42a3-aede-585186324d32"],"Domain":"3cf6acc6-a852-4c2e-a688-b247a94987eb","Data":"Message: World","Child":null,"$type":"MessageLog"},"$type":"AgentLog"}}
{"@t":"2020-12-24T17:52:51.8587656Z","@mt":"{@log}","@l":"Verbose","log":{"Agent":"HelloAgent","Type":"Publishing","AgentId":"c531c6be-d4e0-421c-9bbb-6d64a5f5d7b6","Message":{"Name":"HelloConsoleMessage","Id":"78d499a5-c1fc-4ab2-9eec-b882617e1994","Predecessors":["c163f51a-c19b-42a3-aede-585186324d32"],"Domain":"3cf6acc6-a852-4c2e-a688-b247a94987eb","Data":"Message: Hello","Child":null,"$type":"MessageLog"},"$type":"AgentLog"}}
{"@t":"2020-12-24T17:52:51.8589578Z","@mt":"{@log}","@l":"Verbose","log":{"Agent":"ConsoleMessageJoiner","Type":"Executing","AgentId":"961faab4-f394-4f70-83d2-79d5117e160d","Message":{"Name":"WorldConsoleMessage","Id":"ca73e3bf-c380-4753-b7a8-987fca8ab56d","Predecessors":["c163f51a-c19b-42a3-aede-585186324d32"],"Domain":"3cf6acc6-a852-4c2e-a688-b247a94987eb","Data":"Message: World","Child":null,"$type":"MessageLog"},"$type":"AgentLog"}}
{"@t":"2020-12-24T17:52:51.8589626Z","@mt":"{@log}","@l":"Verbose","log":{"Agent":"ConsoleMessageJoiner","Type":"Executing","AgentId":"961faab4-f394-4f70-83d2-79d5117e160d","Message":{"Name":"HelloConsoleMessage","Id":"78d499a5-c1fc-4ab2-9eec-b882617e1994","Predecessors":["c163f51a-c19b-42a3-aede-585186324d32"],"Domain":"3cf6acc6-a852-4c2e-a688-b247a94987eb","Data":"Message: Hello","Child":null,"$type":"MessageLog"},"$type":"AgentLog"}}
{"@t":"2020-12-24T17:52:51.8657758Z","@mt":"{@log}","@l":"Verbose","log":{"Agent":"ConsoleMessageJoiner","Type":"Publishing","AgentId":"961faab4-f394-4f70-83d2-79d5117e160d","Message":{"Name":"ConsoleMessageCreated","Id":"ec45ddf5-2baf-4062-8e22-78f3c498fc05","Predecessors":["78d499a5-c1fc-4ab2-9eec-b882617e1994","ca73e3bf-c380-4753-b7a8-987fca8ab56d"],"Domain":"3cf6acc6-a852-4c2e-a688-b247a94987eb","Data":"Message: Hello World","Child":null,"$type":"MessageLog"},"$type":"AgentLog"}}
{"@t":"2020-12-24T17:52:51.8659039Z","@mt":"{@log}","@l":"Verbose","log":{"Agent":"ConsoleMessageDisplayAgent","Type":"Executing","AgentId":"67adf5b1-d286-4e3f-9069-07264fafac32","Message":{"Name":"ConsoleMessageCreated","Id":"ec45ddf5-2baf-4062-8e22-78f3c498fc05","Predecessors":["78d499a5-c1fc-4ab2-9eec-b882617e1994","ca73e3bf-c380-4753-b7a8-987fca8ab56d"],"Domain":"3cf6acc6-a852-4c2e-a688-b247a94987eb","Data":"Message: Hello World","Child":null,"$type":"MessageLog"},"$type":"AgentLog"}}
This view is optimized for speed as even a short "Hello World" will produce quite a bit of logging. To make is easier to read the Agents.Net.LogViewer was created. The log viewer is still in its early alpha state. Still it is possible to use. Here is what the log would look like in the log viewer:
Further Resources
- read the API Documentation
- take a look at the BDD styled showcase tests
- ask a question in a new discussion
- contact us via email
- try and learn ;-)