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:
[RequestLoggingAsync(0,HandlerTiming.Before)][UsePolicyAsync(step:1, policy:Policies.Retry.EXPONENTIAL_RETRYPOLICYASYNC)]publicoverrideasyncTask<AddPerson> HandleAsync(AddPerson addPerson,CancellationToken cancellationToken =default){awaitusingvar connection =await_relationalDbConnectionProvider.GetConnectionAsync(cancellationToken);awaitconnection.ExecuteAsync("insert into Person (Name) values (@Name)",new {Name =addPerson.Name});returnawait base.HandleAsync(addPerson, cancellationToken);}
[QueryLogging(0)][RetryableQuery(1,Retry.EXPONENTIAL_RETRYPOLICYASYNC)]public override async Task<FindPersonsGreetings> ExecuteAsync(FindGreetingsForPerson query, CancellationToken cancellationToken = new CancellationToken())
{ //Retrieving parent and child is a bit tricky with Dapper. From raw SQL We wget back a set that has a row-per-child. We need to turn that
//into one entity per parent, with a collection of children. To do that we bring everything back into memory, group by parent id and collate all
//the children for that group.var sql =@"select p.Id, p.Name, g.Id, g.Message from Person p inner join Greeting g on g.Recipient_Id = p.Id";awaitusingvar connection =await_relationalDbConnectionProvider.GetConnectionAsync(cancellationToken);var people =awaitconnection.QueryAsync<Person,Greeting,Person>(sql, (person, greeting) => {person.Greetings.Add(greeting);return person; }, splitOn:"Id");if (!people.Any()) {returnnewFindPersonsGreetings(){Name =query.Name, Greetings =Array.Empty<Salutation>()}; }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();returnnewFindPersonsGreetings { Name =person.Name, Greetings =person.Greetings.Select(g =>newSalutation(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.
[RequestLoggingAsync(0,HandlerTiming.Before)][UsePolicyAsync(step:1, policy:Policies.Retry.EXPONENTIAL_RETRYPOLICYASYNC)]public override async Task<AddGreeting> HandleAsync(AddGreeting addGreeting, CancellationToken cancellationToken = default)
{var posts =newList<Guid>(); //We use the unit of work to grab connection and transaction, because Outbox needs //to share them 'behind the scenes'var conn =await_transactionProvider.GetConnectionAsync(cancellationToken);var tx =await_transactionProvider.GetTransactionAsync(cancellationToken);try {var people =awaitconn.QueryAsync<Person>("select * from Person where name = @name",new {name =addGreeting.Name}, tx );var person =people.SingleOrDefault();if (person !=null) {var greeting =newGreeting(addGreeting.Greeting, person); //write the added child entity to the Dbawaitconn.ExecuteAsync("insert into Greeting (Message, Recipient_Id) values (@Message, @RecipientId)",new { greeting.Message, RecipientId =greeting.RecipientId }, tx); //Now write the message we want to send to the Db in the same transaction.posts.Add(await_postBox.DepositPostAsync(newGreetingMade(greeting.Greet()), _transactionProvider, cancellationToken: cancellationToken)); //commit both new greeting and outgoing messageawait_transactionProvider.CommitAsync(cancellationToken); } }catch (Exception e) {_logger.LogError(e,"Exception thrown handling Add Greeting request"); //it went wrong, rollback the entity change and the downstream messageawait_transactionProvider.RollbackAsync(cancellationToken);returnawait base.HandleAsync(addGreeting, cancellationToken); }finally {_transactionProvider.Close(); } //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 latencyawait_postBox.ClearOutboxAsync(posts, cancellationToken:cancellationToken);returnawait 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
[UseInbox(step:0, contextKey:typeof(GreetingMadeHandler), onceOnly:true )] [RequestLogging(step:1, timing:HandlerTiming.Before)][UsePolicy(step:2, policy:Policies.Retry.EXPONENTIAL_RETRYPOLICY)]publicoverrideGreetingMadeHandle(GreetingMade @event){var posts =newList<Guid>();var tx =_transactionConnectionProvider.GetTransaction();var conn =tx.Connection; try {var salutation =newSalutation(@event.Greeting);conn.Execute("insert into Salutation (greeting) values (@greeting)",new {greeting =salutation.Greeting}, tx); posts.Add(_postBox.DepositPost(newSalutationReceived(DateTimeOffset.Now), _transactionConnectionProvider));_transactionConnectionProvider.Commit(); }catch (Exception e) {_logger.LogError(e,"Could not save salutation"); //if it went wrong rollback entity write and Outbox write_transactionConnectionProvider.Rollback();return base.Handle(@event); }_postBox.ClearOutbox(posts.ToArray());return base.Handle(@event);}