Skip to content

Use Separate DTO Classes for CIM Datatypes with Dto Suffix Convention

Context and Problem Statement

The CIM (Common Information Model) standard defines composite datatypes such as Length, Voltage, ActivePower, and Resistance. These datatypes consist of three components: a numeric value, a unit symbol (e.g., meters, volts), and a unit multiplier (e.g., kilo, mega). When generating DTOs for service-to-service communication, we need to decide how to represent these CIM Datatypes and establish a clear naming convention that distinguishes DTOs from domain value objects.

Decision Drivers

  • IEC 61970 Compliance - Maintain semantic alignment with the CIM standard model structure
  • Type Safety - Ensure compile-time distinction between different physical quantities (e.g., Length vs Voltage)
  • Reusability - Maximize code reuse across the codebase
  • DDD Alignment - Support clean separation between domain layer and application contracts
  • Serialization Clarity - Produce clean, predictable JSON structures for API consumers
  • Developer Experience - Minimize confusion when working with both domain and DTO types

When Flattening Might Be Acceptable

Only consider flattening if:

  1. You need a completely flat DTO for a legacy system that doesn't support nested objects
  2. You're generating database column mappings (but even then, value objects can be mapped)
  3. Performance profiling shows nested object creation is a bottleneck (unlikely)

Considered Options

  1. Separate DTO Classes with Dto Suffix
  2. Flattened Properties (No Separate Classes)
  3. Namespace-Only Separation (No Suffix)

Decision Outcome

Chosen option: "Separate DTO Classes with Dto Suffix", because it provides the best balance of type safety, CIM compliance, and clear distinction between domain value objects and transfer objects. Domain value objects remain suffix-free (Length, Voltage), while DTOs use the Dto suffix (LengthDto, VoltageDto).

Consequences

  • Good:

    • because it maintains strong type safety - LengthDto is distinct from VoltageDto at compile time
    • because it enables reusable validation logic encapsulated within each DTO class
    • because it produces clean nested JSON serialization matching CIM semantics
    • because the naming convention eliminates ambiguity between domain and DTO types
  • Bad:

    • because it introduces additional classes that must be maintained alongside domain value objects
    • because it requires explicit mapping between domain value objects and DTOs

Confirmation

Compliance with this ADR can be confirmed through:

  1. Code Review - Verify that all CIM Datatype DTOs follow the {TypeName}Dto naming convention
  2. Architecture Tests - Use ArchUnit or similar to enforce that classes in *.Contracts or *.Dto namespaces end with Dto suffix
  3. Generated Code Inspection - Validate that the CimModelGenerator produces separate DTO classes with the correct suffix

Pros and Cons of the Options

Option 1: Separate DTO Classes with Dto Suffix

Domain value objects have no suffix, DTOs always have Dto suffix.

// Domain Value Object (no suffix) 
namespace GridLab.Gmss.Cim.Domain;

public class Length : ValueObject 
{ 
    public float Value { get; private set; }
    public UnitSymbol Unit { get; private set; }
    public UnitMultiplier Multiplier { get; private set; }
}

// DTO (with Dto suffix) 
namespace GridLab.Gmss.Cim.Application.Contracts; 

public class LengthDto 
{ 
    public float? Value { get; set; } 
    public UnitSymbol? Unit { get; set; } = UnitSymbol.m; 
    public UnitMultiplier? Multiplier { get; set; } = UnitMultiplier.none;
}

// Usage in entity DTO 
public class ClampDto : ConductingEquipmentDto
{ 
    public LengthDto? LengthFromTerminal1 { get; set; }
}
  • Good:

    • because type name alone indicates whether it's a domain or DTO type
    • because no namespace conflicts - can use both in same file without aliases
    • because aligns with common .NET conventions (e.g., CustomerDto, OrderDto)
    • because LengthDto is distinct from VoltageDto at compile time
    • because validation and default values can be encapsulated per datatype
  • Neutral:

    • because requires mapping layer between domain and DTO
  • Bad:

    • because increases total number of classes in the codebase

Option 2: Flattened Properties (No Separate Classes)

Inline the value, unit, and multiplier as separate properties with naming prefixes.

// Flattened approach 
public class ClampDto : ConductingEquipmentDto 
{ 
    public float? LengthFromTerminal1Value { get; set; } 
    public UnitSymbol? LengthFromTerminal1Unit { get; set; } 
    public UnitMultiplier? 
    LengthFromTerminal1Multiplier { get; set; } 
}
  • Good:

    • because fewer classes to maintain
    • because simpler flat structure for basic serialization
  • Bad:

    • because loses type safety - no compile-time distinction between Length and Voltage
    • because property triplets repeated everywhere, violating DRY
    • because validation logic scattered across consuming code
    • because deviates from CIM standard model semantics
    • because verbose and error-prone property naming

Option 3: Namespace-Only Separation (No Suffix)

Both domain and DTO use same name Length, distinguished only by namespace.

// Domain
namespace GridLab.Gmss.Cim.Domain;

public class Length : ValueObject 
{ 
}

// DTO 
namespace GridLab.Gmss.Cim.Application.Contracts; 

public class Length
{ 
}

More Information

Naming Convention Summary

Layer Example Class
Domain Value Object Length
DTO LengthDto
Domain Entity ACLineSegment
Entity DTO ACLineSegmentDto
  • Code generator templates must be updated to apply Dto suffix for contract generation
  • AutoMapper or manual mapping profiles needed for domain ↔ DTO conversion
  • Shared enums (UnitSymbol, UnitMultiplier) remain suffix-free and are shared between layers