Entity, Context, and IUnitOfWork

Entity

In the Condense Framework, an Entity is a persisted atomic snapshot of your business information. In code it is represented by a plain C# class definition, decorated with the [Condense.Core.Entity] attribute.

The creation of an entity (or new revision) is an event, which may trigger lamdba methods in your domain.

Identifiers

Entity identifiers are automatically generated and managed by the framework. The identifier is what makes the entity unique - in addition to it’s type.

In some cases it is desirable for the identifier to be set to a known value. This is accomplished by implementing the interface Condense.Core.IUid, which contains a single string IUid { get; } member. The identifier provided by this must not change.

Immutability & Revisions

An entity should be considered a “fact”, and by default it cannot be updated. Entities are typically retrieved using queries that return the most recent, relevant, of type T (eg: .Get<T>()). New entities will supersede older with this query style. Relevance is described later.

Nevertheless, in some cases an entity may be logically mutable, and will be required to change while preserving it’s identifier. In this case it should be specified with [Entity(Mutable=true)]. This will allow multiple revisions of this entity to be stored with the same identifier.

References

This object may be a complex subgraph of other objects, lists, dictionaries, etc. It is required to be serializable.

Plain references to other objects are always stored as part of the entity. This means other entities will be included in the stored data, which is very often not appropriate. To explicitly break this link and store a reference to another entity the Condense.Core.Reference<T> helper is provided. This reference is always to the latest revision of the entity referenced.

[Entity] public class VehicleOrder
{
    public Vehicle Configuration { get; set; } // Will be included as part of the VehicleOrder
    public Reference<Person> Owner { get; set; } // Will be stored as a reference to the Person.
}

The Reference<T> is safe for serialization, and round-trips over WCF. Where the language permits, it implicitly casts to and from T. It will retrieve the referenced T from the ambient unit of work where one exists.

Retention

Business data has a shelf life of relevancy. The Condense framework allows entities to be declared with a retention period. This period can be specified in the range of seconds to years. The framework guarantees that the data will be stored for at least the retention period.

The period is declared with the entity decorator [Entity(RetentionDays=#)] (or RetentionSeconds).

Short term entity retention is useful for “working” data, or query / response summary data. Temporary data can then be used as part of diagnostics in the same manner as more important business information.

This period applies to each individual revision of a mutable entity.

Causality links still remain intact, even when entities in the chain expire and are deleted.

Metadata

Entities are stored with a variety of metadata. This includes information on what caused the creation of the entity, the chain of causality, and timing information. The code-version of the entity at time of storage is also recorded, which allows version coercion rules to be applied when a domain has multiple concurrent active versions.

Causality, Relevance, and Query

In the Condense framework, new or updated entities may trigger and act as inputs for lambda methods in the domain. The method code will execute, creating output entities, which may again trigger more work. This execution and it’s inputs and outputs is recorded as a separate fact, but this causality relationship is recorded in the output entities’ metadata. The entire chain of causality for a given entity (the inputs that caused it to be output, and the inputs the caused them to be output, and so on), is referred to as the entity’s lineage.

Lineage

Conceptually the lineage of an entity is the complete record of entities which directly or indirectly caused it’s existence. This relates business entities together intuitively, a credit report for a customer that arrived due to a credit check request does not necessarily need to be explicitly linked to the customer.

With the lineage recorded for our entities, we now have a tree-like graph of causality which allows us to examine the relationship between two entities that are not explicitly linked in the domain. Furthermore we can determine that two distinct entities are related to each other if they share common lineage. A credit report and an identity report are very closely related if they both share the same customer in their lineage.

Context

From this causality information we can now establish the extremely important concept of context. Condense relies on this concept to allow intuitive querying of data via IUnitOfWork, and this is also leveraged by the runtime to sensibly fill appropriate inputs for lambdas.

The context of a query is the set of entities that are caused by a specific entity, known as the context root. The context root serves as a point of reference for otherwise ambiguous queries.

If an entity is caused by a mutable entity X, then it’s lineage both includes the specific revision of X and a general reference to X. A query can therefore have the context of entities that are caused by a specific revision of a specific entity, or just a specific entity.

This concept of context is used heavily in parameter selection for lambda methods, ensuring that multiple inputs are related by ensuring that a context root of the declared type can be found with appropriate inputs.

Intuitive Query

When discussing a business system, logic is typically discussed with some sort of context. It’s not necessary to continually repeat that a credit check and the identity check refer to the ones for the same customer. Providing this condition continuously would make the documentation difficult to read, and by extension, continually applying some kind of where idcheck.customer == foo in code is also difficult to read.

In the Condense framework, when a context has been established for a unit of work, many of the methods on IUnitOfWork become much more intuitive. An entity is said to be “in context” if it’s lineage contains the context root.

For example, .Get<TEntity>() returns the most relevant entity of that type in context. Strictly speaking, the most recent entity (and revision) of a type which can be coerced (including subtypes) to TEntity, which contains the context root in it’s lineage.

Similarly .All<TEntity>() will return the latest revision of every entity of that type in context.

In the context of a customer, asking to “get the credit report”, will return the most recent, as expected.

Query and Search

For more complex domain querying .Query<TEntity> may be used. This returns a fluent interface for specifying a query, and can be executed using .First() or .All().

Queries will inherit the context of the current unit of work. You can specify a new context using (.InNewContext<TContext>(string id), .InNewContext(object context)) or specify that no context is to be used with .WithoutContext().

You may also specify creation time restriction (in UTC) using .Before(DateTime) and .After(DateTime).

Using .WithDescendant<T>() and .ExcludeWithDescendant<T>() you can return entities that have / have not caused entities of other types. This is designed for scenarios such as LoanApplications which haven’t yet caused a LendingDecision and similar.

The above options are specified to be reasonably performant - the specifics depending on volume of data and storage provider choice.

.Search(...) allows you to specify an arbitrary predicate to filter the result set. This is not guaranteed to be fast and depends on index avaliability ( Indexed field specification on entities is on the backlog). Feature availability depends heavily on the implementation of the LINQ provider backing the storage.

Creation / Access

Typically the lifecycle of the unit of work is managed by the framework. In your lambda methods, an ambient unit of work will be available at Ambient<IUnitOfWork>.Current - this should be used to ensure that work you perform is done as part of any appropriate transactions.

Using Condense Light, you may also create a scoped unit of work using the host’s UnitOfWorkFactory. An independent unit of work is separate to any ambient unit of work which is ongoing, whereas an ambient one will be available using .Current and may be nested.

The unit of work implements IDisposable and must be disposed of. The using(...) { } construct is highly recommended. A unit of work that you create yourself should have either .VoteCommit() or .VoteRollback() called on it before disposal, and one of these must be called if you have performed any write actions. On disposal the unit of work will commit if appropriate.


Couchbase Cloud will support read-only data access with a similar remote interface. In either case it is highly recommended that you use integration interfaces to write to your domain, and suggested that queries are performed using query-command style. eg: OverdueTaskListRequest, OverdueTaskListResponse.