ORM

DWKit uses its own ORM - take a look at its architecture. Here, we will take a look at its program API on the server.

Metadata

In the DWKit ORM, it is crucial to have an object of the EntityModeltype to make database requests for data change and selection. There are three ways to get this object:

EntityModel model = MetadataToModelConverter.GetEntityModelByModelAsync("ModelName", 1).Result;

All attributes with no exceptions are moved from the DataModel to the EntityModel. The second parameter of the function denotes how deeply a model will be assembled by references. For example, if this parameter equals:

- 0 - only the attributes with the specified names, located in the `DataModel`, will appear in the `EntityModel`
- 1 - if there are attributes with the `Type=Reference` type in the `DataModel`, attributes of the entities, which the `DataModel` attributes refer to, are included into the `EntityModel`
- 2 and more - the larger the value, the greater the depth of the models included by reference.
  • by the name of the Form:
EntityModel model = MetadataToModelConverter.GetEntityModelByFormlAsync("FormName").Result;

Those attributes that have the Load checkbox ticked, are moved to the EntityModel from the DataModelspecified in the form just like Main Entity in the Mapping data model section of the admin interface. Depth by refernces (joins) is determined by the same attribute. Besides, the so-called Collections are also included in the EntityModel. In general, Collections represent the EntityModel plus the filter that selects records based on the parent record. The EntityModel for a collection is obtained from the information in the Mapping data model section of the Collections subsection. Attributes for the EntityModel collections are selected based on the Load attribute. An example of a collection - the main EntityModel - invoice and the collection - invoice breakdown that is tied to it. For example, in the previous section, the object has the only collection with the gridHistory name:

{
...
    amount: 111,
    comment: null,
    Id: '90fb3531-808f-43d9-be92-d3c5e8b70ebd',
    __id: '90fb3531-808f-43d9-be92-d3c5e8b70ebd',
    gridHistory: [ <-- Collection
        {
        ...
            command: 'Approve',
            to: 'Accounting review',
            Id: 'cd8ebe54-2a33-4b3a-b318-0bbc1d13af87',
            from: 'BigBoss signing',
            __id: 'cd8ebe54-2a33-4b3a-b318-0bbc1d13af87'
        ...
        },
       ...
    ]
...
}
  • Metadata can also be obtained from the DbObject<T>inheritor. We cover this topic further in this section.

The EntityModel class contains the following information:

  • model.Name - name of a model.
  • model.SchemaName - name of the scheme containing the main table of a model.
  • model.TableName - name of the main table of a model.
  • model.PlainAttributes - attributes located in the main table of a model. They are selected without joins, and their changes are tracked.
  • model.JoinedAttributes - requested via a Join by references, updates are neither tracked nor applied.
  • model.Collections - a list of collections connected to the model, which are requested and changed together with the model. Changes take place in a transaction.
  • model.PrimaryKeyAttribute - a primary key attribute.
  • model.VersionAttribute - the attribute for optimistic locking. If it is set, the ORM will automatically check inalterability of this field from the time of data request till application update.
  • model.LogicalDeleteAttribute - the attribute of logical deleting, when a record is not deleted physically, but is assigned the Deleted attribute.

The attribute is represented by the AttributeModel class and contains the following information:

  • attrbute.Name - name of an attribute
  • attrbute.ColumnName - name of a column
  • attrbute.PropertyName - name of a property in the resulting (data) class
  • attrbute.Type - an attribute type, a compound object that can transform types
  • attrbute.IsReference - determines whether an attribute is a reference to another model (table)
  • attrbute.ReferencedDataModel - a data model (name + scheme + table name), which you refer to
  • attrbute.Children - child attributes

The collection is described by the CollectionModel class and contains the following information:

  • collection.Name - a name that will be the name of an attribute in the resulting object, where the collection data will be moved to.
  • collection.Model - EntityModel that you can use to request data collection.
  • Filter collection.GetCollectionFilter(List<DynamicEntity> parentEntities) - builds a request filter, using a collection of parent entities
  • List<DynamicEntity> collection.GetCollection(DynamicEntity parentEntity, List<DynamicEntity> collectionEntities) - selects the records related to the transmitted parentEntity

Getting data via the ORM, the DynamicEntity dynamic entity

If you have the EntityModel, you can use it to request data from the database. Requesting all data without a filter and sorting the database will look like this:

var data = model.GetAsync(Filter.Empty).Result;
var data = DynamicRepository.GetAsync(model,Filter.Empty).Result;

When requesting the data, you can specify Filter, Order and Paging.

Filter

An objects filter. You can create complex nested filters, using the Filter class. First, specify what operator combines the conditions in the root expression of the filter:

var filter = Filter.And;
var filter = Filter.Or;

Then specify a certain number of conditions - predicates and, probably, nested filters:

var filter = Filter.And.Equal(10,"Amount").LikeRightLeft("Document","Name");

This expression represents the following filter Amount = 10 AND Name LIKE ('%Document%'). With the nested filter:

var filter = Filter.And.Equal(10,"Amount").NestOr().LikeRightLeft("Document","Name").LikeRightLeft("Deed","Name").Parent().GreaterOrEqual(DateTime.Today,"Date");

You are going to get the following filter Amount = 10 AND (Name LIKE ('%Document%') OR Name LIKE ('%Deed%')) AND Date >= '2018-01-16'. Filters can be merged, using the Merge function:

var filter = Filter.And.Equal(10,"Amount");
var filter1 = Filter.And.LikeRightLeft("Document","Name");
filter = filter.Merge(filter1);

You are going to get the same expression as in the first case Amount = 10 AND Name LIKE ('%Document%'). To represent the NULL value, use Null.Value, for example:

var filter = Filter.And.Equal(Null.Value,"Amount");

Correspondence of the filter functions to SQL expressions

Function Expression
f.LikeRight(v,n) n LIKE ('v%')
f.LikeLeft(v,n) n LIKE ('%v')
f.LikeRightLeft(v,n) n LIKE ('%v%')
f.NotLikeRight(v,n) n NOT LIKE ('v%')
f.NotLikeLeft(v,n) n NOT LIKE ('%v')
f.NotLikeRightLeft(v,n) n NOT LIKE ('%v%')
f.Equal(v,n) n = v
f.NotEqual(v,n) n <> v
f.Greater(v,n) n > v
f.Less(v,n) n < v
f.GreaterOrEqual(v,n) n >= v
f.LessOrEqual(v,n) n <= v
f.In(List<> {v1,v2,...},n) n IN (v1,v2,...)
f.NotIn(List<> {v1,v2,...},n) n NOT IN (v1,v2,...)
f.Custom(subquery,parameters) user's parameterized subrequest

Order

Sets the sorting order in the database.

var order1 = Order.StartAsc("Name").Desc("Amount");
var order2 = Order.StartDesc("Amount").Asc("Name");

Both expressions will give us a Name ASC, Amount DESC sorting.

Pagination

Sets a data page for the selection:

var skip = 100;
var take = 10;
var paging = Paging.Create(skip,take);

Skips 100 records and returns 10 records.

The DynamicEntity dynamic entity

You can use Filter, Order and Paging in a request. For example:

var data = model.GetAsync(filter,order,paging).Result;

At the same time, data corresponds to List<DynamicEntity>. DynamicEntity is a transformed ORM record with the specified properties. You can get and change property values via indexers:

var data = model.GetAsync(filter,order,paging).Result;
DynamicEntity document = data.First();
var name = document["Name"];
var amount = document["Amount"];
document["Amount"] = amount + 10;
var id = document.GetId();

Or you can cast DynamicEntity in dynamic and manipulate the data in dynamically:

var data = model.GetAsync(filter,order,paging).Result;
dynamic document = data.First() as dynamic;
var name = document.Name;
document.Amount = document.Amount + 10;

Usually, the usage type of DynamicEntity depends on a particular task.

Changing data (insert,update,delete)

As in the case of data requesting, you need the EntityModel model to change data. Two options are available. First, you can use the extension methods of the EntityModel itself or the DynamicRepository. For example, you receive a JSON with the changed form data from the client, and you need to perform insert or update operations for the main record and insert, update or delete operations for the collections. Let's assume that you receive the data as a request.Data line:

var model = MetadataToModelConverter.GetEntityModelByFormlAsync("FormName").Result;
var data = new DynamicEntityDeserializer(model).DeserializeSingle(request.Data, resetIdsFromClient: true, initMissingFields: false);
model.UpdateSingleAsync(data).Wait();

This means that you first receive the EntityModel for the form, then deserialize the JSON in the DynamicEntity and call the UpdateSingleAsync method that updates the data in the database itself. Besides, in case there are any collectons in the data, they will be updated together with the main record. You should use another method to delete the main record. You receive the IDs of the records to be deleted in request.Data, and even in case you receive the records as a whole, you can get IDs from them.

var model = MetadataToModelConverter.GetEntityModelByFormlAsync("FormName").Result;
var ids = JsonConvert.DeserializeObject<List<object>>(request.Data).Select(c=>model.PrimaryKeyAttribute.Type.ParseToCLRType(c);
model.DeleteAsync(ids).Wait();

Second, you can request the data from the database and get them as a special ObservableEntityContainer object that not only contains the data, but also tracks changes in the data, and sends them to the database.

var container =  DynamicRepository.GetObservableEntities(model,filter).Result;
IEnumeralble<DynamicEntity> data = container.Entities;
var document = data.First() as dynamic;
document.Amount = document.Amount + 10;
container.ApplyAsync().Wait();

The container tracks changes of the DynamicEntitity located in it. You can also add new DynamicEntitity and delete the existing ones. All the changes will be applied and saved in the database. Adding a new record:

var newEntity = model.NewAsync().Result;
newEntity.Name = "New";
container.Merge(new List<DynamicEntity>{newEntity});
container.ApplyAsync().Wait();

Deleting records:

IEnumeralble<DynamicEntity> data = container.Entities;
var document = data.First() as dynamic;
container.Remove(new List<DynamicEntity>{document});
container.ApplyAsync().Wait();

You can also change records via the Merge function.

Typed ORM objects

DynamicEntity is an untyped and informal object. Using it, you can write shorter generalized algorithms of business logic. However, in case there are multiple unique algorithms for a certain object, using DynamicEntity, you will have to write a lot of repetitious noisy code. In the DWKit ORM, you can inherit from a special DbObject<T> object and obtain all the ORM functions as a typed object. First you should declare an object:

public class EntityA : DbObject<EntityA>
{
    [DbObjectModel(IsKey = true)]
    public Guid Id
    {
        get => _entity.Id;
        set => _entity.Id = value;
    }
    [DbObjectModel]
    public byte Type
    {
        get => _entity.Type;
        set => _entity.Type = value;
    }
    [DbObjectModel]
    public string Name
    {
        get => _entity.Name;
        set => _entity.Name = value;
        }
}

Here you can see a declared entity stored in the EntityA table with the Id, Name and Type columns. These class and table in the database are all you need to request, change, insert and delete data. As you can notice, the EntityA class is a wrapper over the DynamicEntity, that's why you have to constantly read and update the properties in the inner _entity object. However, DbObject<T> also contains the EntityModel model and ObservableEntityContainer. All properties uploaded from the database should be assigned the DbObjectModel attribute and should transmit the changes to the inner entity object. Here's an example of working with data:

var a1 = new EntityA() {Id = Guid.NewGuid(), Name = "A1", Type = 1};
var a2 = new EntityA() {Id = Guid.NewGuid(), Name = "A2", Type = 2};
EntityA.ApplyAsync(a1, a2).Wait();
a1.Type = 3;
EntityA.ApplyAsync(a1);
EntityA.DeleteAsync(a1, a2).Wait(); 

An example of requesting data:

var data = EntityA.SelectAsync(filter,order).Result;

Refer to the previous section to find out how to build filter and order. In case you want to create another typed entity referring to the EntityA and include an attribute from the EntityA in it, you should also do it via attributes.

public class EntityB : DbObject<EntityB>
{
    [DbObjectModel(TableType = typeof(EntityA), ParentPropertyName = nameof(AId), ColumnName = "Name")]
    public string AName
    {
        get => _entity.AName;
        set => _entity.AName = value;
    }
    [DbObjectModel]
    public byte Type
    {
        get => _entity.Type;
        set => _entity.Type = value;
    }
    [DbObjectModel(IsKey = true)]
    public Guid Id
    {
        get => _entity.Id;
        set => _entity.Id = value;
    }
    [DbObjectModel]
    public string Name
    {
        get => _entity.Name;
        set => _entity.Name = value;
    }
    [DbObjectModel(ColumnName = "EntityAId")]
    public Guid AId
    {
        get => _entity.AId;
        set => _entity.AId = value;
    }
}

In this case, the DWKit ORM will build a join between the objects itself.

  • [DbObjectModel(TableType = typeof(EntityA), ParentPropertyName = nameof(AId), ColumnName = "Name")] - sets a join by the EntityA.Id - EntityB.Aid columns.
  • [DbObjectModel(ColumnName = "EntityAId")] - in case the ColumnName is specified, it is not necessary to name a column the same name as a property object.

The DWKit ORM and transactions

In the DWKit ORM, you can open transactions implicitly in the using block, and all the nested operations will be performed in this transaction. It is somewhat similar to TransactionScope, however, it does not use the distributed transactions mechanism.

using (var shared = new SharedTransaction())
{
    shared.BeginTranactionAsync().Wait();
    EntityA.ApplyAsync(a1, a2).Wait();
    EntityB.ApplyAsync(b1, b2).Wait();
    shared.Commit();
}

In case there is a thrown exception in the using block, the transaction will rollback itself. You should call Commit explicitly. However, it is OK if while calling the methods one transaction will be opened inside another one.

using (var shared1 = new SharedTransaction())
{
    shared1.BeginTranactionAsync().Wait();
    ....
    using (var shared2 = new SharedTransaction())
    {
        shared2.BeginTranactionAsync().Wait();
    }
    ....
}

In case shared1 was opened and started, shared2 won't open and start, and all expressions inside the second using will use shared1. Thus, you can use SharedTransaction regardless of whether the transaction has opened earlier or not.

However, you should be carful because a transaction cannot be created again, and database management systems cannot increase (or decrease) the level of transaction isolation while a transaction is being carried out. The level of isolation inside a nested transaction will remain the same as inside an external one.

You can change the level of isolation in the parameters of the BeginTranactionAsync method.

shared.BeginTranactionAsync(IsolationLevel.ReadCommitted).Wait();