Processing of related objects (Unit of Work)
XData has supported processing data object mapped to number of tricky related tables. Transactional support is required to apply changes over various but related data objects.
For example we have a business object having one ore more related objects collections. Every one of thous can have related collections of other objects and so on. And some business logic works with this related conglomerate as single complicated entity required submitted into DB using one transaction.
XData is support using transaction, but some non terminal data objects can be inserted inside the same transaction and writing code that can support this case is a non trivial thing.
Common but elegant way to make it simple and clear is a realizing "Unit of Work" pattern. Most of ORM has own realization of this pattern, but XData using this mechanic on right level of business logic - to operate with related conglomerate of business objects. Using of Unit of Work pattern with XData is pure declarative: no custom business logic classes is needed to use it.
Lets describe this by example:
Initialization of new UoW container for new root object is described using static Add method of the Work class:
var rep = dataScope.GetRepository<Model.Patient>(Instance);
var instance = rep.New();
/*
Using static class Work we init UoW container with new root object and describe
collections of related objects. Because of new root object we use Work.Empty
method to initialize that collections without fetching any data.
*/
using (var work = Work.Add(instance,
Work.Empty<Model.Patient, Model.PatientDiagnosis>()
.Empty<Model.Patient, Model.PatientOper>()))
{
if (new PatientWindow { DataContext = instance, Owner = window }
.ShowDialog() == true)
work.Submit(); // submitting UoW container changes
}
Initialization of UoW container for existing object (update):
/*
Using static class Work we init UoW container over existed root object and describe
collections of related objects. Because of root object is exists collections will
be filled from DB using Work.Fill method.
*/
using (var work = Work.Add(patient,
Work.Fill<Model.Patient, Model.PatientDiagnosis>()
.Fill<Model.Patient, Model.PatientOper>()))
{
if (new PatientWindow { DataContext = patient, Owner = window }
.ShowDialog() != true) return;
work.Submit(); // submitting UoW container changes
}
Initialization of single related object inside of UoW container (one-to-one relationship) is described by Add method, that analog to Empty and Fill methods we use before.
UoW container can be initialized with a not only single root object, but using collection of root objects with overloaded Add method that accept an collection (ICollection) of root objects.
To select a single object form UoW container use Get method. This method can accept predicate as parameter to specify unique condition of object selection. After acquiring a single object we can change it (or delete it as explained in followed example):
_patient = work.Get();
_patient.SetDeleted(True);
To select multiple objects from UoW container can be made using Select method:
var newOperations = work.Child<Model.PatientOper>()
.Select(x => x.Date > startDate);
To acquire detail UoW container we can use Child method. This method can accept predicate to select root object when root container has initialized by collection of objects. New related object can be added to detail UoW container using New method we discussed above:
work.Child<Model.PatientOper>()
.New(x => x.SomeField = 42, x => x.OtherField = "some");
Important
Unit of Work realization is not internal part of XData. It can be attached to Your project when this functionality is really needed using separate NuGet package XData WorkSet (UnitOfWork) package.
In addition, the UoW container supports exporting hierarchically constructed objects using the static Export method. At the same time, it is possible to export any selected part of the contents of the UoW container, starting either from the root node or from any container node.
To export a node, it must contain a property of type Guid for storing the object key in the UoW container and another property of type Guid for storing the data layer of the UoW container. These properties should not be included in the mapping.
The GetKey (or it's overload version) and SelectKeys methods are used to get the value of the keys of UoW container objects.
To export an object with child objects, it is necessary to define a property to which it will be used to store an array of child objects (or a child instance for one-to-one relation). Such properties should also not be included in the mapping.
In addition, the bool property can be defined in the child objects to pass the delete flag. Such properties should also not be included in the mapping.
To determine the structure of the exported part of the UoW container, a LINQ-style descriptor is used, the use of which is clear from the example below. Using the descriptor, it is possible not only to determine which child objects should be exported together with the root element, but also what service properties of objects at each level, what information will be filled.
var expInvoice = Work.Export(work.DbLayer, key.Value,
() => Work.Root<Invoice>(z => z.Layer, /* Guid Data scope layer property */
z => z.Key) /* Guid Work set key property */
.With(z => z.InvoiceSpec, /* InvoiceSpec[] child entities array property */
z => z.Key, /* child entity: Guid Work set key property */
z => z.Deleted)); /* child entity: bool delete flag property */
After exporting, this object (and its child objects transferred with it) can be modified somewhere outside the system (for example, on a JS-based Web client) and then applied using the Apply method. In addition to the object itself, the Apply method accepts a descriptor built on the principle similar to that described above.
Work.Apply(inv, () => Work.Root<Invoice>(z => z.Layer, z => z.Key)
.With(z => z.InvoiceSpec, z => z.Key, z => z.Deleted));
Warning
In order to apply changes to the UoW container, the data layer (data scope) on which it is built must not be destroyed like the UoW container itself. XData has internal caching mechanisms that allow them to store and retrieve their instances by layer ID using the GetDataScope and GetWorkSet methods.
Important
Data layers are cached independently of UoW containers and you need to destroy the data layer after removing the UoW container separately!