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.
[Route("{name}/new")]
[HttpPost]
public async Task<ActionResult<FindPersonsGreetings>> Post(string name, NewGreeting newGreeting)
{
await _commandProcessor.SendAsync(new AddGreeting(name, newGreeting.Greeting));
var personsGreetings = await _queryProcessor.ExecuteAsync(new FindGreetingsForPerson(name));
if (personsGreetings == null) return new NotFoundResult();
return Ok(personsGreetings);
}
Handling Examples
Handler code listens for and responds to requests or queries. The handler for the above request and query are:
[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()))
};
}
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.
[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);
}
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
[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);
}