Tour of UBORA codebase

The following gives a tour of some of the components that were coded to implement the feature of “indicating a clinical need” which embodies the functionality of creating and editing a clinical need page.


Commands, command handlers and domain events

The resources (i.e. clinical needs) under this section are event-sourced which means for every change to the resource there is a corresponding event. To make changes to the domain state in UBORA then command objects are to be invoked through the central command & query processor. A command is a data transfer object (DTO) that encapsulates the intent and the arguments of the invoked operation. A so-called handler is found for each command which validates in a user-friendly manner (with text messages that are displayable in the user interface) whether the invoked operation is valid in the current domain state of the platform. So in other words, the command invocations can be rejected. When accepted, the handler persists the necessary events to the database, which will in turn change the projection of the resource — the clinical need.

Example

Here is the command class and the handler class for the feature of “indicating a clinical need”. The command has properties/fields that describe the clinical need to be indicated. The Handler implements the interface ICommandHandler<IndicateClinicalNeedCommand> which signals that it’s the handler for this type of command and provides a blueprint for the method of public ICommandResult Handle(IndicateClinicalNeedCommand cmd)

The handler persists two domain events:

  1. event with the values of the newly indicated “clinical need”;
  2. event for the opening of a new discussion which is a re-usable commenting/forum feature;

Notes

  • The handler class is nested inside the command class and just called “Handler” for better discoverability and locality of the related classes. It could very well be moved to another file and called something else altogether. In its current form it is found/displayed as “IndicateClinicalNeedCommand.Handler” in the codebase.
  • The Command inherits from UserCommand which is a base class for commands that are invokable by end-users. It has an Actor property which indicates who the end-user was authenticated as.
  • ITagsAndKeywords is a simple interface to normalize the way “clinical tags” are named in the code.
  • IDocumentSession dependency is constructor injected into the Handler which is the abstraction to communicate with the database.
  • QuillDelta is a value object which encapsulates the text editor’s format that is used throughout UBORA platform.


Code

1. public class IndicateClinicalNeedCommand : UserCommand, ITagsAndKeywords  
2. {  
3.     public Guid ClinicalNeedId { get; set; }  
4.   
5.     public string Title { get; set; }  
6.     public QuillDelta Description { get; set; }  
7.   
8.     public string ClinicalNeedTag { get; set; }  
9.     public string AreaOfUsageTag { get; set; }  
10.     public string PotentialTechnologyTag { get; set; }  
11.     public string Keywords { get; set; }  
12.   
13.     public class Handler : ICommandHandler<IndicateClinicalNeedCommand>  
14.     {  
15.         private readonly IDocumentSession _documentSession;  
16.   
17.         public Handler(IDocumentSession documentSession)  
18.         {  
19.             _documentSession = documentSession;  
20.         }  
21.   
22.         public ICommandResult Handle(IndicateClinicalNeedCommand cmd)  
23.         {  
24.             var clinicalNeedIndicatedEvent = new ClinicalNeedIndicatedEvent(  
25.                 initiatedBy: cmd.Actor,  
26.                 clinicalNeedId: cmd.ClinicalNeedId,  
27.                 title: cmd.Title,  
28.                 description: cmd.Description,  
29.                 clinicalNeedTag: cmd.ClinicalNeedTag,  
30.                 areaOfUsageTag: cmd.AreaOfUsageTag,  
31.                 potentialTechnologyTag: cmd.PotentialTechnologyTag,  
32.                 keywords: cmd.Keywords);  
33.   
34.             var discussionOpenedEvent = new DiscussionOpenedEvent(  
35.                 initiatedBy: cmd.Actor,  
36.                 discussionId: cmd.ClinicalNeedId,  
37.                 attachedToEntity: new AttachedToEntity(EntityName.ClinicalNeed, cmd.ClinicalNeedId),  
38.                 additionalDiscussionData: ImmutableDictionary<string, object>.Empty);  
39.   
40.             _documentSession.Events.Append(cmd.ClinicalNeedId, clinicalNeedIndicatedEvent, discussionOpenedEvent);  
41.             _documentSession.SaveChanges();  
42.   
43.             return CommandResult.Success;  
44.         }  
45.     }  
46. }  

 

 

Projections

The domain events are chronologically applied to so-called projections that build up a snapshot of the latest domain state. Some projections are aggregates or entities which are the last boundary in the application to make sure that the state to be persisted is valid. Aggregates validate within their own invariants/fields, while handlers can validate over many aggregates.

Example

The DiscussionOpenedEvent is applied to Discussion projection/aggregate/entity and ClinicalNeedIndicatedEvent is applied to ClinicalNeed projection.

Notes

  • The state of these projections can only be changed through events.
  • These projections are immutable otherwise and can be used for querying.

Code

1. public class Discussion : Entity<Discussion>  
2. {  
3.     public Guid Id { get; private set; }  
4.     public virtual ImmutableList<Comment> Comments { get; private set; } = ImmutableList.Create<Comment>();  
5.     public int CommentCount => Comments.Count;  
6.     public DateTimeOffset? LastCommentActivityAt { get; private set; }  
7.     public AttachedToEntity AttachedToEntity { get; private set; }  
8.     public virtual ImmutableDictionary<string, object> AdditionalDiscussionData { get; private set; }  
9.   
10.     private void Apply(DiscussionOpenedEvent e)  
11.     {  
12.         Id = e.DiscussionId;  
13.         AttachedToEntity = e.AttachedToEntity;  
14.         AdditionalDiscussionData = e.AdditionalDiscussionData;  
15.     }  
16.   
17.     ...  
18.   
19.   
20. public class ClinicalNeed : Entity<ClinicalNeed>, ITagsAndKeywords  
21. {  
22.     public Guid Id { get; private set; }  
23.     public Guid DiscussionId { get; private set; }  
24.     public string Title { get; private set; }  
25.     public QuillDelta Description { get; private set; }  
26.     public string ClinicalNeedTag { get; private set; }  
27.     public string AreaOfUsageTag { get; private set; }  
28.     public string PotentialTechnologyTag { get; private set; }  
29.     public string Keywords { get; private set; }  
30.     public DateTimeOffset IndicatedAt { get; private set; }  
31.     public Guid IndicatorUserId { get; private set; }  
32.   
33.     private void Apply(ClinicalNeedIndicatedEvent @event)  
34.     {  
35.         if (IndicatedAt != default(DateTimeOffset))  
36.             throw new InvalidOperationException();  
37.   
38.         Id = @event.ClinicalNeedId;  
39.         DiscussionId = @event.ClinicalNeedId;  
40.         Title = @event.Title;  
41.         Description = @event.Description;  
42.         ClinicalNeedTag = @event.ClinicalNeedTag;  
43.         AreaOfUsageTag = @event.AreaOfUsageTag;  
44.         PotentialTechnologyTag = @event.PotentialTechnologyTag;  
45.         Keywords = @event.Keywords;  
46.         IndicatedAt = @event.Timestamp;  
47.         IndicatorUserId = @event.InitiatedBy.UserId;  
48.     }  
49.   
50.     ...