Files
Masterarbeit/QuellCode/CentronERP/docs/reference/architecture/dtos-and-entities.md
T
Christoph Schwörer f045b99a25 Codebasis als Dateien ins Arbeitsrepo statt als Gitlink
QuellCode/CentronERP war nur als Gitlink (Submodul-Referenz auf 79c1142)
getrackt, ohne .gitmodules und ohne erreichbares Remote. Der
Untersuchungsgegenstand der Versuchsreihe war damit nicht reproduzierbar
gesichert: Ein Klon haette ein leeres Verzeichnis erhalten, und die Belege
der 3.287 Anforderungen waeren nicht ueberpruefbar gewesen.

Umstellung:
- Historie nach c:\DEV\CentronERP_git_snapshot_79c1142 ausgelagert
  (vollstaendig lesbar, enthaelt 79c1142 und Vorgaenger 89ccfd6)
- Gitlink aus dem Index entfernt
- Dateiinhalt aufgenommen: 24.557 Dateien, rund 333 MB

Die verschachtelte .gitignore der Codebasis gilt weiter, Build-Artefakte
bleiben ausgeschlossen. Details in Versuche/Versuch_01/_Codebasis-Nachweis.md
2026-08-26 07:43:51 +02:00

8.2 KiB

On DTOs and Entities

In our c-entron.NET solution we use three different types of objects that hold actual data:

type layer usage
Entity BL/WebServiceBL interaction with the database via NHibernate
DTO WebService/Logics transfer from entity to viewmodel via webservice
ViewModels ViewModel/UI display data to the user and allow user to edit it

Because ViewModels are pretty straight forward and quite boring we're just going to ignore them here.

We're going to use the Thingy class for examples here again. For a real implementation check out MyDayWorkItem and associated classes.

Entities

On the most basic level entities are just a row in a database table managed by our ORM NHibernate. If we have a Thingy there is a corresponding row in the Thingies table, where the primary key equals the I3D. Entities can never leave the BL-Layer as they cannot be used in the webservice and need a connection to NHibernate and the DB itself. CRUD-Operations are done via Session.GetGenericDAO\<T>().SaveOrUpdate/Get/Delete and can only be executed with a valid entity.`

As an entity is simply for holding data it should never include any logic, no overrides and not even a ctor. To avoid confusion all properties present in an entity should also be mapped to a column in the database.

An entitiy needs 2 classes:

The actual entity that holds all data and is used for all operations. It must inherit the BaseEntity abstract base class that adds the unique identifier (I3D) for us. The correct project for entities is Centron.Entities and then the Entities directory.

All properties must be declared virtual and have to have both a setter and a getter. Both things are required for the interaction with NHibernate.

public class Thingy : BaseEntity
{
    public virtual string SomeProperty { get; set; }
    public virtual int SomeOtherProperty { get; set; }
}

Aside from the actual entity we also need a mapping class from Fluent NHibernate. This class maps the properties to the database column and must inherit ClassMap<T>. Even tough NHibernate can figure a lot of the properties out itself, you should always set the table, the id and ALL properties. Mapped properties need to be described with .Not, .Nullable(), .Lenght() etc. as closely as possible. Check out 'Fluent NHibernate in a Nutshell' for more infos. The correct project for entities is Centron.DAO and then the Mappings directory.

public class ThingyMaps : ClassMap<Thingy>
{
    public ThingyMaps
    {
        this.Table("Thingies");

        this.Id(f => f.I3D);
        
        this.Map(f => f.SomeProperty).Nullable();
        this.Map(f => f.SomeOtherProperty).Not.Nullable();
    }
}

DTOs

In the field of programming a data transfer object (DTO) is an object that carries data between processes. (https://en.wikipedia.org/wiki/Data_transfer_object)

For us DTOs do exactly that. They transfer the data from the entity on the bl-layer to the viewmodel in the UI over WebService or direct database connection. Our webservice exclusivly uses DTOs, so all other applications and the c-entron.NET ILogics use them. The project for DTOs is Centron.WebServices.Core and then the wrongly named Entities directory.

Like entities DTOs should never hold any kind of logic, no overrides and no ctors. It can include properties not present in the entity but this should be only be done in very specific circumstances.

Constructing DTOs is very easy: just create a class <yourclass>DTO (ThingyDTO for us), inherit BaseDTO and copy all properties from the entity. Then add the [DataContract] attribute to your class and the [DataMember] attribute to each property.

There are certain things that need to be avoided.

Properties of type DateTime can be dangerous. The default value of DateTime in .NET is 01.01.0001 which cannot be parsed to json and will throw an exception. Especially if the DateTime property gets filled by the database, is used in multiple locations or by other applications. Using a nullable DateTime? is the easiest way around that.

List properties must use List<T>. Using IList<T> or other types of list (IEnumerable<T>, ICollection<T>) can lead to problems with serializing it into json. As this is not widely done in our DTOs please consider updating existing DTOs.

KnownTypes

If your DTO inherits from a different class (NOT BaseDTO) you must add the type to GetKnownTypes() in KnownTypes.cs. Additionally each webservice method that uses that DTO also needs the [ServiceKnownType(nameof(KnownTypes.GetKnownTypes), typeof(KnownTypes))] attribute in ICentronRestService. Otherwise the webservice will throw an exception while parsing your DTO to json.

Converting DTOs to Entities and vice versa

DTO -> Entity

Converting from a DTO to an entity can be somewhat annoying and complicated.
NEVER use the ObjectMapper for this. (Yes, I know, there are a lot of places that do)
What we need to do is as follows:

  1. check if the I3D of the ThingyDTO is 0.
    > yes: new Thingy();
    > no: load the entity from the database (via the appopriate BL method)
  2. take over all properties from the DTO to the entity by hand.

This is done to ensure that all entities that are already saved to the DB (I3D != 0) are unique and properly managed by NHibernate.

public Thingy ConvertThingyDTOToEntity(ThingyDTO dto)
{
    Guard.NotNull(dto, nameof(dto));

    Thingy entity;
    if(dto.I3D != 0)
        entity = new ThingyBL(this.Session).LoadThingy(dto.I3D);
    else
        entity = new Thingy();

    entity.I3D = dto.I3D;
    entity.SomeProperty = dto.SomeProperty;
    [..]
    return entity;
}

Entity -> DTO

Converting an entity to a DTO is very simple as we can just use the ObjectMapper.Map<TEntity, TDTO>().
To use the Objectmapper you should create a configuration file. These files go under Centron.Bl -> Webservice -> ObjectMapperConfiguration.
If there is a fitting class already just add this.CreateMap<Thingy, ThingyDTO>();, else we need to create a new file.

public class ThingyConfiguration : Profile
{
    protected ovveride void Configure()
    {
        this.CreateMap<Thingy, ThingyDTO>();
    }
}

Generating mapping and entities

The following SQL statements generate your mappings and entities for you.

There could be errors inside, you need to manually verify them.

SELECT 'Map(m => m.' + c.name + ').Column("' + c.name + '")' + 
       CASE WHEN ty.name = 'text' THEN '.Length(int.MaxValue)' ELSE '' END +
       CASE WHEN ty.name = 'varchar' THEN '.Length(' + CONVERT(varchar(100), c.max_length) + ')' ELSE '' END +
       CASE WHEN ty.name = 'nvarchar' THEN '.Length(' + CONVERT(varchar(100),c.max_length / 2) + ')' ELSE '' END +
       CASE WHEN c.is_nullable = 1 THEN '.Nullable()' ELSE '' END + ';', c.max_length
FROM sys.all_columns c
INNER JOIN sys.tables t ON t.object_id = c.object_id
INNER JOIN sys.types ty ON ty.system_type_id = c.system_type_id
WHERE t.name = 'TableName' AND ty.name <> 'sysname' AND c.Name <> 'I3D'

SELECT 'public virutal ' +
       CASE WHEN ty.name IN ('text', 'nvarchar', 'varchar') THEN 'string' ELSE '' END +
       CASE WHEN ty.name = 'bit' THEN 'bool' ELSE '' END +
       CASE WHEN ty.name = 'datetime' THEN 'DateTime' ELSE '' END +
	   CASE WHEN c.is_nullable = 1 AND ty.name = 'datetime' THEN 'DateTime' ELSE '' END +
       CASE WHEN ty.name = 'float' THEN 'double' ELSE '' END +
       CASE WHEN ty.name = 'int' THEN 'int' ELSE '' END +
       CASE WHEN ty.name = 'uniqueidentifier' THEN 'Guid' ELSE '' END +
       CASE WHEN ty.name = 'char' THEN 'char' ELSE '' END +
       CASE WHEN c.is_nullable = 1 AND ty.name NOT IN ('text', 'nvarchar', 'varchar', 'datetime') THEN '?' ELSE '' END +
       ' ' + c.name + ' { get; set; }'
FROM sys.all_columns c
INNER JOIN sys.tables t ON t.object_id = c.object_id
INNER JOIN sys.types ty ON ty.system_type_id = c.system_type_id
WHERE t.name = 'TableName' AND ty.name <> 'sysname' AND c.Name <> 'I3D'

In the mapping Length(0) should be .Length(int.MaxValue)

For the entities ? should be DateTime?