There is an old principle: show don't tell, and this introduction is about showing you what you can do with Brighter and Darker. It's not about how - more detailed documentation elsewhere shows you how to write this code. It's not about why - articles elsewhere discuss some of the reasons behind this approach. It is just, let me see how Brighter works.
Brighter and Darker
Brighter is about Requests
A Request is a message sent over a bus. A request may update state.
A Command is an instruction to execute some behavior. An Event is a notification.
You use the Command Processor to separate the sender from the receiver, and to provide middleware functionality like a retry.
Darker is about Queries
A Query is a message executed via a bus that returns a Result. A query does not update state.
You use the Query Processor to separate the requester from the replier, and to provide middleware functionality like a retry.
Middleware
Both Brighter and Darker allow you to provide middleware that runs between a request or query being made and being handled. The middleware used by a handler is configured by attributes.
Sending and Querying Example
In this example, we show sending a command, and querying for the results of issuing it, from within an ASP.NET WebAPI controller method.
Handler code listens for and responds to requests or queries. The handler for the above request and query are:
Using an External Bus
As well as using an Internal Bus, in Brighter you can use an External Bus - middleware such as RabbitMQ or Kafka - to send a request between processes. Brighter supports both sending a request, and provides a Dispatcher than can listen for requests on middleware and forward it to a handler.
The following code sends a request to another process.
The following code receives a message, sent from another process, via a dispatcher. It uses an Inbox to ensure that it does not process duplicate messages
[QueryLogging(0)]
[RetryableQuery(1, Retry.EXPONENTIAL_RETRYPOLICYASYNC)]
public override async Task<FindPersonsGreetings> ExecuteAsync(FindGreetingsForPerson query, CancellationToken cancellationToken = new CancellationToken())
{
var sql = @"select p.Id, p.Name, g.Id, g.Message
from Person p
inner join Greeting g on g.Recipient_Id = p.Id";
var people = await _uow.Database.QueryAsync<Person, Greeting, Person>(sql, (person, greeting) =>
{
person.Greetings.Add(greeting);
return person;
}, splitOn: "Id");
var peopleGreetings = people.GroupBy(p => p.Id).Select(grp =>
{
var groupedPerson = grp.First();
groupedPerson.Greetings = grp.Select(p => p.Greetings.Single()).ToList();
return groupedPerson;
});
var person = peopleGreetings.Single();
return new FindPersonsGreetings
{
Name = person.Name,
Greetings = person.Greetings.Select(g => new Salutation(g.Greet()))
};
}
[RequestLogging(0, HandlerTiming.Before)]
[UsePolicyAsync(step:1, policy: Policies.Retry.EXPONENTIAL_RETRYPOLICYASYNC)]
public override async Task<AddGreeting> HandleAsync(AddGreeting addGreeting, CancellationToken cancellationToken = default(CancellationToken))
{
var posts = new List<Guid>();
//We use the unit of work to grab connection and transaction, because Outbox needs
//to share them 'behind the scenes'
var tx = await _uow.BeginOrGetTransactionAsync(cancellationToken);
try
{
var searchbyName = Predicates.Field<Person>(p => p.Name, Operator.Eq, addGreeting.Name);
var people = await _uow.Database.GetListAsync<Person>(searchbyName, transaction: tx);
var person = people.Single();
var greeting = new Greeting(addGreeting.Greeting, person);
//write the added child entity to the Db
await _uow.Database.InsertAsync<Greeting>(greeting, tx);
//Now write the message we want to send to the Db in the same transaction.
posts.Add(await _postBox.DepositPostAsync(new GreetingMade(greeting.Greet()), cancellationToken: cancellationToken));
//commit both new greeting and outgoing message
await tx.CommitAsync(cancellationToken);
}
catch (Exception e)
{
_logger.LogError(e, "Exception thrown handling Add Greeting request");
//it went wrong, rollback the entity change and the downstream message
await tx.RollbackAsync(cancellationToken);
return await base.HandleAsync(addGreeting, cancellationToken);
}
//Send this message via a transport. We need the ids to send just the messages here, not all outstanding ones.
//Alternatively, you can let the Sweeper do this, but at the cost of increased latency
await _postBox.ClearOutboxAsync(posts, cancellationToken:cancellationToken);
return await base.HandleAsync(addGreeting, cancellationToken);
}
[UseInboxAsync(step:0, contextKey: typeof(GreetingMadeHandlerAsync), onceOnly: true )]
[RequestLoggingAsync(step: 1, timing: HandlerTiming.Before)]
[UsePolicyAsync(step:2, policy: Policies.Retry.EXPONENTIAL_RETRYPOLICYASYNC)]
public override async Task<GreetingMade> HandleAsync(GreetingMade @event, CancellationToken cancellationToken = default(CancellationToken))
{
var posts = new List<Guid>();
var tx = await _uow.BeginOrGetTransactionAsync(cancellationToken);
try
{
var salutation = new Salutation(@event.Greeting);
await _uow.Database.InsertAsync<Salutation>(salutation, tx);
posts.Add(await _postBox.DepositPostAsync(new SalutationReceived(DateTimeOffset.Now), cancellationToken: cancellationToken));
await tx.CommitAsync(cancellationToken);
}
catch (Exception e)
{
_logger.LogError(e, "Could not save salutation");
//if it went wrong rollback entity write and Outbox write
await tx.RollbackAsync(cancellationToken);
return await base.HandleAsync(@event, cancellationToken);
}
await _postBox.ClearOutboxAsync(posts, cancellationToken: cancellationToken);
return await base.HandleAsync(@event, cancellationToken);
}