📜 ⬆️ ⬇️

How to cook DTO?

Over the past month and a half I have been able to work on the backend of three projects. In each, it was necessary to prepare classes for interacting with a remote service through the exchange of XML documents; in particular, it was required to prepare DTO-classes for each type of message. For each of the services there was a description from the developers, with examples of XML documents, so the work was relatively simple: take an example, run through the xsd utility, get a class, correct types, add missing fields / properties, test. Operations are routine, after a dozen classes, thinking was not particularly needed, so thoughts began to accumulate in my head about how to speed up the development process or improve the output. It turned out both that, and another.

Tldr
For cooking, we take a DTO semi-finished product from xsd, add wrappers for primitive types, add sugar (implicit operators), put in a microwave for 30 minutes , we get a convenient data transfer object.

First, punch the sample XML document into the xsd utility. DTO-object after exiting xsd.exe looks like this:

//   :  xsd [System.SerializableAttribute] [XmlTypeAttribute(AnonymousType = true)] public class DtoProduct { private string productidfield; private string productnamefield; private string productpricefield; /*...*/ [XmlElement] public string productid { get{return this.productidfield;} set{this.productidfield = value;} } [XmlElement] public string productname { get{return this.productnamefield;} set{this.productnamefield = value;} } [XmlElement] public string productprice { get{return this.productpricefield;} set{this.productpricefield = value;} } } 

In addition to the problems with the style (it is treated by Resharper and \ or Ctrl + H), we have a problem with the types: decimal is better for price, and long for Id. For the correct indication of the types of users of our DTO will thank us, or at least will not want us to burn in hell. Let's make changes, at the same time having brought names to the corporate standard.

 [System.SerializableAttribute] [XmlTypeAttribute(AnonymousType = true)] public class DtoProduct { private decimal _productPrice; //... [XmlElement("productprice")] public decimal ProductPrice { get{return _productPrice;} set{_productPrice = value;} } } 

Already better, but there are problems with (de) serialization: a remote service may have its own data transfer format. This is especially true for dates and decimal values. Plus, sometimes there are specific types (guid, code, mail).
')
Another problem: default values. If the value of the string property is null, the property will not be serialized (read, if the property has not been assigned values, the property will not be serialized). Double, int, bool are value types (Value Types), and they cannot be null; as a result, int properties are serialized by default (read, if the int property is not assigned a value, 0 is serialized). Most likely, it will not bring harm, but this is not described in the code behavior, which I would like to avoid.

So, we come to the need to create rules for (de) serialization of base types. As an example, consider Money (decimal), which is serialized as “d.dd” (the dot separator, two characters after the separator). Let's create a class XmlMoneyWrapper, inherit it from IXmlSerializable.

 public class XmlMoneyWrapper : IXmlSerializable { public decimal Value { get; set; } //     public override string ToString() { return Value.ToString("0.00", CultureInfo.InvariantCulture); } #region IXmlSerializable Members public XmlSchema GetSchema() { return null; } public void ReadXml(XmlReader reader) { string value = reader.ReadString(); // TODO change to TryParse? try { Value = Decimal.Parse(value, new NumberFormatInfo { NumberDecimalSeparator = "." }); } catch (Exception exc) { String err = String.Format("Can't deserialize string {0} to decimal. Expected number decimal separator is dot \".\"", value); throw new SerializationException(err, exc); } reader.Read(); } public void WriteXml(XmlWriter writer) { writer.WriteString(ToString()); } #endregion } 

And change our DTO:

 [System.SerializableAttribute] [XmlTypeAttribute(AnonymousType = true)] public class DtoProduct { private XmlMoneyWrapper _productPrice; //... [XmlElement("productprice")] public XmlMoneyWrapper ProductPrice //    ,   { get { return _productPrice; } set { _productPrice = value; } } } 

We made a Nullable property, which is initiated as null; we saved the DTO user from having to think about the serialization format. However, working with DTO has become more complicated. Now the if (product.ProductPrice> 10.00) check will have to be replaced with if (product.ProductPrice.Value> 10.00).

Conclusion: you need to add a couple of tablespoons of sugar to overload operators of implicit type conversions.

 public static implicit operator XmlMoneyWrapper(decimal arg) // decimal to XmlMoneyWrapper { XmlMoneyWrapper res = new XmlMoneyWrapper { Value = arg }; return res; } 

 public static implicit operator decimal (XmlMoneyWrapper arg) // XmlMoneyWrapper to decimal { return arg.Value; } 

Now the user can again use the code of the form if (product.ProductPrice> 10.00). At the same time, in the class comment (and commit) it is worth making a warning about implicit casts. In addition, colleagues using our DTO may not remember implicit operators, so it is worth adding an example of use. After all, our goal is not to show off a newly studied feature?

Unfortunately, some types are not compatible with implicit coercion. For example, a string of limited length: in the interface overload

 public static implicit operator XmlLimitedStringWrapper(string arg) 

There is no space for the argument of the maximum string length. For the same reason, it will not be possible to create a reusable class with a custom default value. In such situations, it remains only to work with fields and properties. For example, a string class with a limited maximum length can be used like this:

 [System.SerializableAttribute] [XmlTypeAttribute(AnonymousType = true)] public class DtoProduct { private readonly XmlLimitedStringWrapper _productName = new XmlLimitedStringWrapper(16); // Create string with maxLength = 16. // ... // Max symbols: 16. [XmlElement("productname")] public string ProductName { get{return _productName = _productName.Value;} set{_productName.Value = value;} } } 

As a result of these manipulations, the raw DTO semi-finished product turns into a fairly convenient product. All formatting logic is hidden from the user, the user can use the usual basic types in his code. Creating DTO classes (by feeling) takes a little less time than before. Due to Nullable wrappers traffic may be slightly reduced. The development of all XmlPrimitiveTypeWrapper types takes about one day (with unit testing). In the following projects, you can take ready-made wrappers, the benefit they do not change much.

The disadvantages are standard: the convenience required the complexity of the architecture; it may take time for colleagues to create a DTO with new classes.

Source: https://habr.com/ru/post/270473/


All Articles