For this example, we will scrap the pointless GreetingRequest, and mockup a scheduler for our guests to demonstrate
the very important concepts of context and lineage. Context is how the Condense framework selects specific business
entities from scant details - such as only their type eg:
The full file for this example is here: https://gist.github.com/markdchurchill/925e6e076a959e8bb48df035bca3e181#file-simple3-cs
This example turned from a step-by-step into a description of core concepts and probably needs to be cleaned up!
When a business entity is emitted by a lambda, then the framework records that it has been caused by the inputs to that execution. The lineage of an entity is comprised of the entities that caused it’s existance, including their lineage, recursively. As business entities are very commonly related by causality, lineage provides a consistant way to find related business entities - commonly we are looking for those related to a specific root entity, this is referred to as the context.
A UnitOfWork may have a context root. This is a reference to a specific business entity which serves as a point of reference for any otherwise ambiguous requests.
For example, a request to
.Get<T>() will return the newest entity of type
T “in context” - it’s lineage contains
the context root; or in other words, the newest entity of type
T which was caused by the context root.
.All<T>() will return the latest version of every entity of type
T in context.
In very plain english: “Get the most relevant T”.
This context is established semi-automatically. If we skip ahead to simple3.cs, the signature for the lambda method which produces a Schedule from the PreferredSessions and Restrictions data:
“This code should be executed when we have a Preferred Session and Restrictions entity for (/under/related to) a Guest”
This method is declared to execute in the context of a Guest. The ambient IUnitOfWork will have a context root set to a specific Guest entity. How is this selected? Firstly, execution will be triggered by one of the input entities arriving (the trigger). The framework will then attempt to create an execution plan which satisfies the lambda definition.
In this case, it will take the trigger and attempt to find:
An entity of type Guest in the lineage of the trigger to act as the context root, and
Suitable entities for the other parameters that can be found in this context.
If an execution plan can be found, the method will then be executed. By default, the attempt to execute will happen once whenever a new triggering
parameter arrives. This will happen multiple times for a method with multiple parameters - but the method will not execute unless an execution plan can be filled - in this case it would require all parameters to be non-null (existing). Note in this case that the method returns
IEnumerable, indicating that there may be multiple results.
By default Condense considers a business entity as an immutable fact. As entities are commonly accessed in context by their type, the default behaviour of storing multiple UserAddressDeclaration facts, and retrieving the latest using a simple
.Get<UserAddressDeclaration>() works well. In other cases, particularily in the case of entities which are referenced by others (thus accessible by a known UID, eg
.Get<T>(string uid), etc), logically the entity is mutable, and Condense provides this option.
Declaring an entity with
[Entity(Mutable = true)] will allow this entity to be changed and then saved in a unit of work, where this would normally cause an exception. Condense will store a new revision of this entity. This entity will now be returned via
.Get<T>(string uid). Older revisions will be accessible using
.Get<T>(string uid, string revision) and references to a specific revision.
Referencing entities is an obvious need for any non-trivial system. Condense manages it’s own metadata around an entity, including it’s ReferenceKey, which includes identifier and version information. For entities that will only be accessed via context, the identifier is also generated automatically. However there are several reasons why an assigned key may be required (especially interoperability). To enable this, an entity may implement
IUid, which has the single property
Uid. Implement this as a regular auto-property, or read-only derived data, and the runtime will use this for the identifier.
Business entities are stored as an atomic document; the entire type is serialized out. This means that entities having lists and dictionaries avoid complicated mapping, however a standard in-code reference to another entity will result in that entity being included rather than referenced. This is generally not what a developer wants, although sometimes it can be useful.
Condense.Core.Reference<T> type. This type should be used for properties where a true reference is needed. Automatically casting itself to and from T, but serializing out as a reference, it plays nicely with entity storage and over-the-wire protocols such as the runtimes automatic service implementation. Under the hood, it will use the ambient IUnitOfWork to retrieve the referenced instance when needed. Currently it has the restriction
where T : IUid, however this restriction may be removed in later versions.
In this example we are now exposing two endpoints.
The Greeting entity has been declared with a retention. The runtime will store the Entity for at least the length of the retention. After this point it will may not be accessible, and may not take up storage space. Retention can be declared with the entity definition, or an entity may implement
IRetentionPolicy. ( An interface very subject to change). The range of this value can be from seconds, anywhere up to years. We recommend you have a retention period defined for every entity, unless you can think of a specific reason to the contrary.
The next example is Simple Example 4, however it hasn’t been written yet!