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.,
LengthvsVoltage) - 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:
- You need a completely flat DTO for a legacy system that doesn't support nested objects
- You're generating database column mappings (but even then, value objects can be mapped)
- Performance profiling shows nested object creation is a bottleneck (unlikely)
Considered Options¶
- Separate DTO Classes with Dto Suffix
- Flattened Properties (No Separate Classes)
- 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 -
LengthDtois distinct fromVoltageDtoat 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
- because it maintains strong type safety -
-
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:
- Code Review - Verify that all CIM Datatype DTOs follow the
{TypeName}Dtonaming convention - Architecture Tests - Use ArchUnit or similar to enforce that classes in
*.Contractsor*.Dtonamespaces end withDtosuffix - Generated Code Inspection - Validate that the
CimModelGeneratorproduces 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
LengthDtois distinct fromVoltageDtoat 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 |
Related Decisions¶
- Code generator templates must be updated to apply
Dtosuffix 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