Skip to content

Data Transfer Objects

Max Stepanskiy edited this page Mar 31, 2021 · 57 revisions

Nemo is a an object-relational mapper for DTO's and POCO's. There is no base class or any special attribute that one needs to decorate their POCO's and DTO's with. However, all objects defined as interfaces must inherit IDataEntity marker interface.

Declaring a data transfer object

As class
public class Customer
{
     [PrimaryKey, MapColumn("CustomerID")]
     public string Id { get; set; }
     public string CompanyName { get; set; }
     public IList<IOrder> Orders { get; set; }
}   

A class can be any POCO even the one used with other ORM frameworks. One can re-purpose EF or NHibernate POCO's (although NHibernate ISet is not supported) since mapping is convention based.

As interface
public interface ICustomer : IDataEntity
{
     [PrimaryKey, MapColumn("CustomerID")]
     string Id { get; set; }
     string CompanyName { get; set; }
     IList<IOrder> Orders { get; set; }
}

Generally using DTO's as interfaces with Nemo is more flexible:

  • Provides ability to model DTO's using multiple inheritance
  • Facilitates version tolerant mapping
  • Provides support for immutable DTO's
  • Can be applied to POCO's used with other ORM frameworks

Interfaces normally do not work well with serialization: one has to know beforehand what concrete implementations of an interface can be encountered. That's is not an issue when using Nemo: Nemo serialization works well with both interfaces and classes.

Supported types

A DTO contains either simple or complex properties.

Simple properties can be natively mapped from an underlying data object. A simple property is a property of the following type:

  • Primitive (Boolean, Byte, SByte, Int16, UInt16, Int32, UInt32, Int64, UInt64, Char, Double, and Single).
  • Decimal
  • String
  • Guid
  • DateTime
  • DateTimeOffset
  • TimeSpan
  • Enum
  • Nullable<T> of above-mentioned types
  • List<T> of above-mentioned types (requires a ListConverter)
  • Byte[]
  • XmlDocument (requires an XmlTypeConverter)
  • XmlReader (requires an XmlReaderTypeConverter)

A complex property is a property of the following type:

  • Any DTO (any class or an interface marked with IDataEntity)
  • Any List<T> of DTO's
  • Any IList<T> of DTO's
    • It is a more flexible way to define collections within DTO's

    • It facilitates a more robust implementation of immutability when calling AsReadOnly<T>() extension method

    • Developer can apply an aspect to a property of type IList<T>

      • Sorted aspect - makes sure a collection is sorted; when Sorted aspect is applied a collection is manifested as SortedList<T> (an implementation of IList<T>)
        public interface ICustomer : IDataEntity
        {
            string Id { get; set; }
            ...
            [Sorted(ComparerType = typeof(IOrderComparer)]
            IList<IOrder> Orders { get; set; }
        }
      • Distinct aspect - makes sure a collection is a set of distinct values; when Distinct aspect is applied a collection is manifested as HashList<T> (an implementation of IList<T>)
        public interface ICustomer : IDataEntity
        {
            string Id { get; set; }
            ...
            [Distinct(EqualityComparerType = typeof(IOrderEqualityComparer)]
            IList<IOrder> Orders { get; set; }
        }
      • Distinct and Sorted can be combined; however when both aspects are applied a collection is manifested as a SortedList<T>
      • When aspects are applied to a property which is NOT an IList<T>, they are simply ignored
      • Note: IList<T> with aspects is slower to deserialize

Any property which does not fall into above-mentioned category will be ignored during data mapping. However, a developer may be able to populate those properties manually. Note: serialization supports wider range of data types, thus manually populated properties will be able to go on the wire.

Convention based mapping

As it is mentioned in one of the previous sections, data mapping in Nemo is convention based. More precisely, data mapping is based on property names and types. However, this may not not always be the case: property name may not match with a column name (or a property name of a legacy object). In this case a developer would use MapColumnAttribute (or MapPropertyAttribute when mapping to another object). Sometimes a type would not match either. In this case there is a multitude of type converter attributes available to facilitate proper type mapping.

Mapping to a complex property is a bit more complicated. Take a look at the above-mentioned ICustomer interface. It declares a property Orders of type IList<IOrder>. Normally SQL query would not return a list of orders as a part of a customer row. Instead a query would produce two result sets: one with customer rows and another with order rows. A definition of a relation is missing to put customers and orders together. In order to resolve this a developer would define a "foreign key" on IOrder which links orders and customers. One has to use a combination of PrimaryKeyAttribute and ReferencesAttribute attributes to accomplish that. As one can see in the previous secionts, ICustomer utilizes PrimaryKeyAttribute attribute. And here is how a developer would define a "foreign key" on IOrder:

public interface IOrder : IDataEntity
{
    int OrderId { get; set; }
    [References(typeof(ICustomer)]
    string CustomerId { get; set; }
    ...
}

After setting up a relation, a developer can eager load a customer with all related orders.

Persistence ignorance

All examples use attributes to define object mapping meta-data. However, one can follow persistence ignorance pattern if desired. All is needed is to define mapping class which inherits from EntityMap<T> class. Here is a sample class definition:

public class Customer
{
    public string Id { get; set; }
    public string Name { get; set; }
    public string Notes { get; set; }
    public DateTime RegistrationDate { get; set; }
}

public class CustomerMap : EntityMap<Customer>
{
    public CustomerMap()
    {
        TableName = "Customers";
        Property(c => c.Id).Column("CustomerID").Parameter("CustomerID").PrimaryKey();
        Property(c => c.Name).Column("CustomerName");
        Property(c => c.Notes).Not.Persistent().Not.Serializable();
        Property(c => c.RegistrationDate).Column("CustomerRegistrationDate").WithTransform<UtcDateTimeConverter>();
    }
}

Supported operations

Nemo provides a variety of operations in order to facilitate effective use of POCO's and DTO's. Majority of those operations are static methods of the ObjectFactory class. Using ObjectFactory a developer can

  • Instantiate a DTO
  • Map a DTO from any other (e.g., legacy) object
  • Bind a DTO defined as interface to a any (e.g., legacy) object
  • Retrieve a DTO (or a list of DTO's) from an underlying storage
  • Perform CUD operations

Note: CUD operations are based on stored procedures with enforced naming convention. A developer cannot specify SQL statement as a parameter to a CUD method, but given proper configuration CUD statements can be autogenerated as long as Active Record pattern is used