Show me the code!

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:

[RequestLogging(0, HandlerTiming.Before)]
[UsePolicyAsync(step:1, policy: Policies.Retry.EXPONENTIAL_RETRYPOLICYASYNC)]
public override async Task<AddPerson> HandleAsync(AddPerson addPerson, CancellationToken cancellationToken = default(CancellationToken))
{
	await _uow.Database.InsertAsync<Person>(new Person(addPerson.Name));
	
	return await base.HandleAsync(addPerson, cancellationToken);
}
[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);
}

Last updated