DtoKit Tutorial
На русском DtoKit Class tree Interface Tree Narrowing interfaces AddDtoKit(...) DtoBuilder Build<T>() and BuildOfType(Type type) ValueRequestEventArgs Build<T>(object helper) and BuildOfType(Type type, object helper) DtoJsonConverterFactory Serialization Deserialization

DtoKit

The DtoKit library is designed to prevent the dependence of the model and presentation of the application, both server and client parts, from the specific implementation of object classes of the subject area. At the same time, its use will avoid data transfer between application levels. using Data Transfer Objects (DTO) or Plain Old CLR Object (POCO). The above applies to the construction of domain objects, their serialization in JSON and deserialization from JSON for interaction between client and server.

Class tree

Class tree from the subject area is a connected acyclic graph (actually, a tree) whose root is the class under consideration, nodes - aggregated classes from the subject area, leaves - properties open for writing.

Let's consider an example from the subject area related to the timetable of ships.

For brevity, we give only the main properties of entities, although they may contain methods, but in this context it does not matter.

For the ShipCall class, we have the following tree (node ​​names are omitted for clarity):

As you can see, the class tree can be infinite.

Interface tree

Now we can develop read-only interfaces that cover all the properties in our domain classes and within the domain, that is, the database keys will not appear anywhere. Let's force the classes to implement them explicitly, if necessary, this will not add any logic. We will edit the classes, but this will not affect anything, since the new properties will only be "aliases" of the class properties.

AlsoLet's not immediately take into account that we will still need to implicitly use the key properties at the loading and transfer levels, at least for client requests to specific objects on the server (we will see later that it is not only for this), so we will mark such properties with a special [Key] attribute.

Here's what happened:

Obviously, according to the modified definition for the class tree in an intuitively understandable way, an interface tree is built.

For example, the IShipCall interface tree looks like this (node ​​names omitted for readability):

As you can see, the interface tree can also be infinite.

Narrowing interfaces

Suppose we want to display a list of ship calls to the client in the form of a table. We don’t need objects as a whole, but we also don’t want to make a separate class for rows of the table, since its "physical" meaning would be unclear.

Suppose we need to display in the table the name of the line, the name of the ship, the name of the port of arrival / departure, the voyage number, the time of arrival / departure, additional information. Then we make a new tree interfaces that contains only this data:

For example, the IShipCallForList interface tree looks like this (node ​​names omitted for readability):

Also in the ShipCall class, we have the AdditionalInfo property, which takes a long time to load due to a separate request to the DB. Therefore, we want to first display a table to the user, and then gradually update these cells.

Let's create another interface:

The IShipCallAdditionInfo interface tree looks like this (node ​​names omitted for readability):

AddDtoKit(...)

This method must be called during the configuration of the dependency injection mechanism.

Through the IServiceCollection services parameter, you need to pass the collection of services provided by the host, and the Action configure parameter must contain a method that registers all interfaces, which are introduced for domain classes, in the collection of services provided by the DtoKit library.

In our case, it will look like this:

Note that in this context the lifetime can only be ServiceLifetime.Transient, others will throw an exception.

DtoBuilder

The DtoBuilder class is designed to load objects from storage, such as a database. Features:

The DtoBuilder instance needs to be obtained through the dependency injection mechanism, it is already registered there when calling AddDtoKit(...):

Then use one of two ways (T is the requested interface):

In both cases, the method will return the constructed object. If you want to load an already existing object, you need to assign it to the Target property. Otherwise, the object will be obtained through dependency injection.

Build<T>() and BuildOfType(Type type)

When using this method, you need to subscribe to the ValueRequest event:

When calling the Build<T>() method, the tree of the requested interface is traversed in width and in each node and leaf the event handler is called, passing an argument like ValueRequestEventArgs.

ValueRequestEventArgs

Members:

public Type RootType { get; } requested interface type
public string Path { get; } path from the class tree root - property names through /
public bool IsNullable { get; } indicates that the property can be set to null. Based on availability? in the property type; DtoBuilder prevents assigning null if the value is false
public bool IsLeaf { get; } indicates that the current node in the interface tree is a leaf. This means that its type was not registered when calling AddDtoKit(...)
public Type NominalType { get; } the property type of the current node in the interface tree
public object? value { get; set; } returns the current value of the property and is used to assign a new value. In the case of a node, the object has already been provided
public bool IsCommited { get; set; } when assigned a value of true, the nodes and leaves of the subtree rooted at that node are skipped.

In the case of a node, no action can be taken because the object has already been assigned. In the case of a sheet, you must explicitly assign a value to the Value property, otherwise an exception will be thrown.

Build<T>(object helper) and BuildOfType(Type type, object helper)

helper - an object of an arbitrary class containing public methods marked with special attributes and having a special metric.

[Startup] an optional method that, if present, is called before the object is constructed
[Shutdown] optional method, which, if present, is called after the construction of the object is completed
[Before] an optional method that, if present, is called before requesting the value of each node or leaf
[After] an optional method that, if present, is called after requesting the value of each node or leaf
[Path("/...")] a method required for leaves and optional for nodes. If defined for a node, must return the value of the value argument if the supplied object remains, either null or a reference to another object, at the developer's discretion. If not defined for a sheet, an exception is thrown. One method can have multiple [Path("/...")] attributes. All paths must be different class limits

Parameters for methods marked with [Before], [After], [Path("/...")]:

stringpath path from the root of the class tree
Type type the property type of the current node in the interface tree
object? value the initial value of the property of the current node. For a sheet, the value is default. Keep in mind that if the leaf property has some value by default when creating an object, it cannot be counted on, as it is lost.
bool isLeaf indicates that the current node in the interface tree is a leaf. This means that its type was not registered when calling AddDtoKit(...)
bool isNullable indicates that the property can be set to null. Based on availability? in the property type; DtoBuilder prevents assigning null if the value is false
ref bool isCommitted when assigned a value of true, the nodes and leaves of the subtree rooted at that node are skipped.

Important! The use of infinite interface trees is limited! If a node of the same type is encountered on the path from the node to the root, then this is an error, except when these nodes are ends of the same edge. In this case, there is no error, but only the key properties are loaded.

For example, you can write a program that prints all paths:

/
/ID_LINE
/ID_SHIPCALL
/AdditionalInfo
/arrival
/departure
/voyage
/Port
/Port/ID_PORT
/Port/Name
/PrevCall
/PrevCall/ID_LINE
/PrevCall/ID_SHIPCALL
/Route
/Route/ID_LINE
/Route/ID_ROUTE
/Route/Line
/Route/Line/ID_LINE
/Route/Line/Name
/Route/Vessel
/Route/Vessel/ID_VESSEL
/Route/Vessel/Brutto
/Route/Vessel/CallSign
/Route/Vessel/Height
/Route/Vessel/Length
/Route/Vessel/Name
/Route/Vessel/Netto
/Route/Vessel/Width
/Route/Vessel/Port
/Route/Vessel/Port/ID_PORT
/Route/Vessel/Port/Name

We see that for PrevCall only keys are requested.

DtoJsonConverterFactory

The class DtoJsonConverterFactory is a custom Json converter factory for working with interfaces registered via AddDtoKit(...), as well as their collections using standard tools from the System.Text.Json namespace. *. Features:

The DtoJsonConverterFactory instance needs to be obtained through the dependency injection mechanism, it is already registered there when AddDtoKit(...), create ecinstance of System.Text.Json.JsonSerializerOptions, add a factory to its converters, and then use the normal mechanism by substituting the JsonSerializerOptions argument:

Serialization

Properties used only during serialization:

public KeysProcessing KeysProcessing { get; set; } Specifies how to deal with key properties:
  • KeysProcessing.OnlyKeysForRepeats
  • for re-serializable objects within the current Json only key properties and special field {..., "$keyOnly": true} are output (default)
  • KeysProcessing.Usual
  • serialization in the usual way: all writable properties of the class are output in Json
  • KeysProcessing.OnlyKeys
  • only key properties and special field {..., "$keyOnly": true} are output
The value of this field can no longer be changed after the first serialization has started.
public bool WithMagic when assigning the value true, a special field {"$magic": "applied", ...} is added to each Json object. This is convenient for visual verification that all the necessary objects are serialized using DtoJsonConverterFactory

Deserialization

Properties used only during deserialization:

public bool UseEndOfDataNull { get; set; } when deserializing a top-level Json array, points to possibly partial content. The last element of the next array, equal to null means the end of the collection and sets IsEndOfData = true. The fact that the collection will be transferred in parts is reported to the client separately, for example, through the http header
public bool IsEndOfData { get; } the value true means that UseEndOfDataNull == true and null came, i.e. passing the collection in parts completed. The fact that the collection will be transferred in parts is reported to the client separately, for example, through the http header
public object? target {get; set; } when assigning an object to this property, Json is deserialized into it, instead of creating a new object. It is not recommended to read this property, since during the deserialization process it changes unpredictably from the point of view of the observer

Also, when deserializing a top-level Json array, special dummy types can be used to control the population of the target collection of objects. In this case, the target collection must exist and must be assigned to the Target property before deserialization. The dummy type is specified inas a type parameter when calling JsonSerializer.Deserialize<...>(...). The return value of such a call should be ignored as this is just a stub and actually populates the collection previously assigned to the Target property. Meaning of use the technique is to run on the client with an ObservableCollection associated with the user interface.

RewritableListStub<T> populates the target collection from the beginning, reusing objects already contained in it, removing unnecessary ones or adding missing ones at the end
AppendableListStub<T> adds objects to the end of the target collection. Convenient for filling parts.
UpdateableListStub<T> Updates the objects in the target collection by finding them by key properties.
For example, in the example with ship call additional information, we first load ship calls with empty additional information, and then load only additional information, entering it in the required lines. Since additional information on the legend is calculated slowly, the user does not wait, but receives table quickly, and then sees additional information.