Mappers¶
Purpose and Overview¶
The Mapping Layer transforms intermediate RDF data structures (implementing IRdfDefinition) into strongly-typed domain entities and vice versa, with proper type safety, validation, and invariants. Mappers bridge the gap between generic RDF triple stores and rich domain models.
Core Goal: Provide bidirectional mapping between weakly-typed RDF property collections and domain entities that enforce:
- Required property validation
- Type safety (enums, value objects, complex types)
- Business invariants
- Identity resolution and normalization
Note: This is the base RDF library. Domain-specific mappers (e.g., CIM entity mappers) are implemented in application-specific projects that reference this library.
Key Responsibilities¶
1. RDF Class Recognition¶
Mappers identify which RDF classes they can handle via the CanMap method:
- Supports multiple naming conventions (
"Equipment","cim:Equipment") - Enables polymorphic mapping through the mapper factory
- Provides flexible namespace handling
2. Identity Extraction and Validation¶
Mappers extract and validate entity primary keys using IRdfIdentifierParser:
- Parse RDF resource IDs (
#_uuid-123,urn:uuid:...) - Convert to typed keys (e.g.,
Guid) for keyed entities - Use string-based MRID for non-keyed entities
- Reject entities with invalid or missing identifiers
3. Property Resolution¶
Mappers extract properties from RDF definitions:
- Navigate properties via
IRdfPropertyinterface - Handle different property types (literal, resource, compound)
- Support multiple naming conventions for flexibility
4. Type Conversion and Validation¶
Mappers transform RDF literal strings into typed values using IRdfValueParser:
- Parse enumerations with case-insensitive matching
- Parse numeric values (float, int) with invariant culture
- Parse boolean values with multiple format support
- Validate required vs. optional properties
5. Entity Construction¶
Mappers instantiate domain entities with validated data:
- Call entity constructors with required parameters
- Enforce domain invariants during creation
- Return
nullfor invalid/incomplete data - Preserve entity integrity
6. Reverse Mapping (Entity → RDF)¶
Mappers optionally support converting entities back to RDF definitions:
- Serialize entity properties to RDF definitions
- Generate proper type names and property URIs
- Support Insert, Update, and Upsert operations in repositories
- Indicated by the
CanUnmapproperty
Architecture¶
Component Structure¶
graph TB
subgraph "Mapper Layer"
Factory["RdfEntityMapperFactory"]
BaseInterface["IRdfEntityMapper<TRdfDefinition, TEntity>"]
KeyedInterface["IRdfEntityMapper<TRdfDefinition, TEntity, TKey>"]
KeyedImpl["KeyedEntityMapper"]
NonKeyedImpl["NonKeyedEntityMapper"]
end
Factory -->|"Resolves & Injects"| KeyedImpl
Factory -->|"Resolves & Injects"| NonKeyedImpl
BaseInterface -->|"Extends"| KeyedInterface
KeyedInterface -->|"Implements"| KeyedImpl
BaseInterface -->|"Implements"| NonKeyedImpl
subgraph "Dependencies"
IdentityParser["IRdfIdentifierParser"]
ValueParser["IRdfValueParser"]
NodeResolver["IRdfNodeResolver"]
end
KeyedImpl -->|"Uses"| IdentityParser
KeyedImpl -->|"Uses"| ValueParser
NonKeyedImpl -->|"Uses"| IdentityParser
NonKeyedImpl -->|"Uses"| ValueParser
subgraph "Core Types"
Definition["IRdfDefinition"]
Property["IRdfProperty"]
end
Definition <-->|"Map / Unmap"| KeyedImpl
Definition <-->|"Map / Unmap"| NonKeyedImpl
subgraph "Domain"
KeyedEntity["Entity<TKey>"]
NonKeyedEntity["Entity"]
end
KeyedEntity <-->|"Map / Unmap"| KeyedImpl
NonKeyedEntity <-->|"Map / Unmap"| NonKeyedImpl Core Interfaces¶
IRdfEntityMapper<TRdfDefinition, TEntity>¶
The base interface for all entity mappers with default implementations for optional reverse mapping:
public interface IRdfEntityMapper<TRdfDefinition, TEntity>
where TRdfDefinition : class, IRdfDefinition
where TEntity : class, IEntity
{
/// <summary>
/// Returns true if this mapper can handle the given RDF class (type) of this resource.
/// Matches both unqualified (e.g., "Equipment") and qualified (e.g., "cim:Equipment") names.
/// </summary>
bool CanMap(string className);
/// <summary>
/// Creates a domain entity from the given RDF definition.
/// Returns null if the definition does not have required data.
/// </summary>
TEntity? Map(TRdfDefinition definition);
/// <summary>
/// Gets a value indicating whether this mapper supports reverse mapping (entity to RDF definition).
/// </summary>
bool CanUnmap => false;
/// <summary>
/// Creates an RDF definition from the given domain entity.
/// Returns null if reverse mapping is not supported or the entity cannot be converted.
/// </summary>
TRdfDefinition? Unmap(TEntity entity) => null;
}
IRdfEntityMapper<TRdfDefinition, TEntity, TKey>¶
Extension for keyed entities - inherits all members from the base interface:
public interface IRdfEntityMapper<TRdfDefinition, TEntity, TKey> : IRdfEntityMapper<TRdfDefinition, TEntity>
where TRdfDefinition : class, IRdfDefinition
where TEntity : class, IEntity<TKey>
{
}
Mapping Directions:
| Direction | Method | Purpose |
|---|---|---|
| RDF → Entity | Map(TRdfDefinition) | Read operations (Find, GetList, etc.) |
| Entity → RDF | Unmap(TEntity) | Write operations (Insert, Update, Upsert) |
IRdfEntityMapperFactory¶
Resolves entity-specific mappers with automatic discovery and dependency injection:
public interface IRdfEntityMapperFactory
{
/// <summary>
/// Gets the RDF entity mapper for the specified entity type.
/// </summary>
IRdfEntityMapper<TRdfDefinition, TEntity> GetMapper<TRdfDefinition, TEntity>()
where TRdfDefinition : class, IRdfDefinition
where TEntity : class, IEntity;
/// <summary>
/// Gets the RDF entity mapper for the specified entity type and key type.
/// </summary>
IRdfEntityMapper<TRdfDefinition, TEntity, TKey> GetMapper<TRdfDefinition, TEntity, TKey>()
where TRdfDefinition : class, IRdfDefinition
where TEntity : class, IEntity<TKey>;
}
RDF Core Types¶
Mappers work with these core types when building or parsing RDF definitions:
| Type | Purpose | Description |
|---|---|---|
IRdfDefinition | Resource representation | Contains Id, Type, and Properties collection |
IRdfProperty | Property contract | Base interface for all property types |
The IRdfDefinition interface provides:
Id- The resource identifier (rdf:ID or rdf:about)ClassName- The RDF class (type) of the resourceProperties- Collection ofIRdfPropertyinstances
Mapper Implementation Pattern¶
public class MyEntityMapper : IRdfEntityMapper<MyRdfDefinition, MyEntity, Guid>
{
private readonly IRdfIdentifierParser _identifierParser;
private readonly IRdfValueParser _valueParser;
public MyEntityMapper(IRdfIdentifierParser identifierParser, IRdfValueParser valueParser)
{
_identifierParser = identifierParser;
_valueParser = valueParser;
}
#region Forward Mapping (RDF → Entity)
public bool CanMap(string className)
{
if (string.IsNullOrWhiteSpace(className))
return false;
var name = className.Trim();
return name.Equals("MyEntity", StringComparison.Ordinal)
|| name.EndsWith(":MyEntity", StringComparison.Ordinal);
}
public MyEntity? Map(MyRdfDefinition definition)
{
if (definition == null)
return null;
// 1. Extract and validate identity
if (!_identifierParser.TryParseGuid(definition.Id, out var entityId))
return null;
// 2. Extract properties from definition
var name = GetPropertyValue(definition, "name") ?? $"MyEntity:{entityId:D}";
// 3. Parse typed values
var valueStr = GetPropertyValue(definition, "value");
if (!_valueParser.TryParseFloat(valueStr, out var value))
return null;
// 4. Construct domain entity with validated data
return new MyEntity(entityId, name, value);
}
private string? GetPropertyValue(MyRdfDefinition definition, string propertyName)
{
// Implementation depends on your RdfDefinition structure
return definition.Properties
.FirstOrDefault(p => p.Name.EndsWith(propertyName))
?.ToString();
}
#endregion
#region Reverse Mapping (Entity → RDF)
public bool CanUnmap => true;
public MyRdfDefinition? Unmap(MyEntity entity)
{
if (entity == null)
return null;
// Create definition with ID and class name
var definition = new MyRdfDefinition(
id: entity.Id.ToString("D"),
className: "ns:MyEntity"
);
// Add properties
definition.Properties.Add(new MyLiteralProperty("ns:name", entity.Name));
definition.Properties.Add(new MyLiteralProperty("ns:value", entity.Value.ToString()));
return definition;
}
#endregion
}
Mapping Flow¶
Forward Mapping (Read Operations)¶
sequenceDiagram
participant Repo as RdfGraphRepository
participant Factory as RdfEntityMapperFactory
participant Mapper as EntityMapper
participant Parser as IRdfIdentifierParser
participant ValueParser as IRdfValueParser
participant Domain as Domain Entity
Repo->>Factory: GetMapper<TRdfDefinition, TEntity>()
Factory-->>Repo: Return mapper
Repo->>Mapper: CanMap(definition.ClassName)
Mapper-->>Repo: true
Repo->>Mapper: Map(definition)
Mapper->>Parser: TryParseGuid() or TryParseId()
Parser-->>Mapper: Guid or string ID
Mapper->>ValueParser: TryParseFloat(), TryParseEnum(), etc.
ValueParser-->>Mapper: Parsed values
Mapper->>Domain: new Entity(id, name, value, ...)
Domain-->>Mapper: Entity instance
Mapper-->>Repo: Entity Reverse Mapping (Write Operations)¶
sequenceDiagram
participant Repo as RdfGraphRepository
participant Mapper as EntityMapper
participant Writer as IRdfInstanceWriter
participant Graph as IGraph
Repo->>Mapper: CanUnmap
Mapper-->>Repo: true
Repo->>Mapper: Unmap(entity)
Mapper->>Mapper: Create IRdfDefinition(id, type)
Mapper->>Mapper: Add IRdfProperty instances
Mapper-->>Repo: IRdfDefinition
Repo->>Writer: Insert(graph, definition)
Writer->>Graph: Assert triples
Writer-->>Repo: triplesInserted RdfEntityMapperFactory Implementation¶
The factory uses automatic discovery via assembly scanning with ABP framework integration:
public class RdfEntityMapperFactory : IRdfEntityMapperFactory, ITransientDependency
{
private readonly IServiceProvider _serviceProvider;
private readonly Lazy<Dictionary<Type, Type>> _mapperTypes;
public RdfEntityMapperFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
_mapperTypes = new Lazy<Dictionary<Type, Type>>(DiscoverMapperTypes);
}
private Dictionary<Type, Type> DiscoverMapperTypes()
{
var result = new Dictionary<Type, Type>();
var assemblyFinder = _serviceProvider.GetService<IAssemblyFinder>();
if (assemblyFinder == null)
return result;
foreach (var assembly in assemblyFinder.Assemblies)
{
Type[] types;
try { types = assembly.GetTypes(); }
catch { continue; }
foreach (var type in types)
{
if (type.IsAbstract || type.IsInterface)
continue;
var interfaces = type.GetInterfaces()
.Where(i => i.IsGenericType)
.Where(i =>
{
var def = i.GetGenericTypeDefinition();
return def == typeof(IRdfEntityMapper<,>)
|| def == typeof(IRdfEntityMapper<,,>);
});
foreach (var iface in interfaces)
{
result[iface] = type;
}
}
}
return result;
}
public IRdfEntityMapper<TRdfDefinition, TEntity> GetMapper<TRdfDefinition, TEntity>()
where TRdfDefinition : class, IRdfDefinition
where TEntity : class, IEntity
{
var targetInterface = typeof(IRdfEntityMapper<TRdfDefinition, TEntity>);
if (_mapperTypes.Value.TryGetValue(targetInterface, out var mapperType))
{
return (IRdfEntityMapper<TRdfDefinition, TEntity>)_serviceProvider.GetRequiredService(mapperType);
}
var mapper = _serviceProvider.GetService<IRdfEntityMapper<TRdfDefinition, TEntity>>();
if (mapper != null)
return mapper;
throw new InvalidOperationException(
$"No IRdfEntityMapper<{typeof(TRdfDefinition).Name}, {typeof(TEntity).Name}> implementation was found or registered.");
}
public IRdfEntityMapper<TRdfDefinition, TEntity, TKey> GetMapper<TRdfDefinition, TEntity, TKey>()
where TRdfDefinition : class, IRdfDefinition
where TEntity : class, IEntity<TKey>
{
var targetInterface = typeof(IRdfEntityMapper<TRdfDefinition, TEntity, TKey>);
if (_mapperTypes.Value.TryGetValue(targetInterface, out var mapperType))
{
return (IRdfEntityMapper<TRdfDefinition, TEntity, TKey>)_serviceProvider.GetRequiredService(mapperType);
}
var mapper = _serviceProvider.GetService<IRdfEntityMapper<TRdfDefinition, TEntity, TKey>>();
if (mapper != null)
return mapper;
// Fallback: Try non-keyed mapper if it also implements keyed interface
var nonKeyedMapper = _serviceProvider.GetService<IRdfEntityMapper<TRdfDefinition, TEntity>>();
if (nonKeyedMapper is IRdfEntityMapper<TRdfDefinition, TEntity, TKey> keyedMapper)
return keyedMapper;
throw new InvalidOperationException(
$"No IRdfEntityMapper<{typeof(TRdfDefinition).Name}, {typeof(TEntity).Name}, {typeof(TKey).Name}> implementation was found or registered.");
}
}
Discovery Mechanism:
- Scans all loaded assemblies via ABP's
IAssemblyFinder - Identifies types implementing
IRdfEntityMapper<,>orIRdfEntityMapper<,,> - Builds type registry mapping interface to implementation
- Caches registry using
Lazy<T>for subsequent lookups - Resolves instances via
IServiceProviderwith dependency injection
Bidirectional Mapping Support¶
When to Implement Unmap¶
Implement reverse mapping (CanUnmap = true and Unmap method) when your repository needs to support:
| Operation | Requires Unmap | Description |
|---|---|---|
Insert | ✅ Yes | Create new entity in graph |
Update | ✅ Yes | Modify existing entity |
Upsert | ✅ Yes | Insert or update |
Find | ❌ No | Read-only operation |
GetList | ❌ No | Read-only operation |
Delete | ❌ No | Uses ID only, not full entity |
Read-Only Mapper (Default)¶
For read-only scenarios, mappers don't need to implement Unmap - default implementations return false and null:
public class ReadOnlyEntityMapper : IRdfEntityMapper<MyRdfDefinition, ReadOnlyEntity, Guid>
{
public bool CanMap(string className) => className.EndsWith(":ReadOnlyEntity");
public ReadOnlyEntity? Map(MyRdfDefinition definition)
{
// Forward mapping implementation
}
// CanUnmap defaults to false (from interface default)
// Unmap defaults to returning null (from interface default)
}
Full CRUD Mapper¶
For full CRUD support, implement both directions by overriding the defaults:
public class CrudEntityMapper : IRdfEntityMapper<MyRdfDefinition, CrudEntity, Guid>
{
// Forward mapping
public bool CanMap(string className) => className.EndsWith(":CrudEntity");
public CrudEntity? Map(MyRdfDefinition definition) { /* ... */ }
// Reverse mapping - override defaults
public bool CanUnmap => true;
public MyRdfDefinition? Unmap(CrudEntity entity)
{
if (entity == null) return null;
var definition = new MyRdfDefinition(
id: entity.Id.ToString("D"),
className: "ns:CrudEntity");
// Add properties...
return definition;
}
}
Repository Integration¶
Write Operations Flow¶
// Repository checks mapper capability before write operations
public virtual TEntity Upsert(TEntity entity)
{
if (!Mapper.CanUnmap)
throw new NotSupportedException(
$"Mapper for {typeof(TEntity).Name} does not support reverse mapping.");
var definition = Mapper.Unmap(entity);
if (definition == null)
throw new InvalidOperationException("Mapper returned null.");
// Perform upsert using Writer
if (Writer.Exists(graph, definition.Id))
Writer.Update(graph, definition);
else
Writer.Insert(graph, definition);
return entity;
}
Error Handling¶
| Scenario | Behavior |
|---|---|
CanUnmap = false | NotSupportedException thrown by repository |
Unmap returns null | InvalidOperationException thrown by repository |
| Entity validation fails | Return null from Unmap |
| Domain constructor throws | Return null from Map |
Architectural Relationships¶
Key Integration Points¶
1. Factory → Mapper Resolution¶
Purpose: Automatic mapper discovery and instantiation
| Step | Action |
|---|---|
| 1 | Factory initialized with IServiceProvider |
| 2 | On first request, scans assemblies via IAssemblyFinder |
| 3 | Builds registry: IRdfEntityMapper<TRdfDef, TEntity> → ConcreteMapper |
| 4 | Caches registry for subsequent lookups |
| 5 | Resolves mapper via DI with injected dependencies |
2. Mapper → Repository Operations¶
| Repository Method | Mapper Method Used |
|---|---|
Find(id) | Map(definition) |
FindById(mrid) | Map(definition) |
GetList() | Map(definition) (multiple) |
Insert(entity) | Unmap(entity) |
Update(entity) | Unmap(entity) |
Upsert(entity) | Unmap(entity) |
Delete(entity) | Unmap(entity) for ID extraction |
Dependency Flow¶
graph LR
subgraph "Application Layer"
Service[Application Service]
end
subgraph "Repository Layer"
Repo[RdfGraphRepository]
Factory[RdfEntityMapperFactory]
Writer["IRdfInstanceWriter<T>"]
Reader["IRdfInstanceReader<T>"]
end
subgraph "Mapper Layer"
Mapper["EntityMapper"]
IdentityParser[IRdfIdentifierParser]
ValueParser[IRdfValueParser]
end
subgraph "Core Layer"
Definition[IRdfDefinition]
Properties[IRdfProperty]
end
subgraph "Data Layer"
Graph[IGraph]
end
subgraph "Domain Layer"
Entity["Domain Entity"]
end
Service -->|"Find/Upsert"| Repo
Repo -->|"GetMapper()"| Factory
Factory -->|"Resolve via DI"| Mapper
Repo -->|"Map (read)"| Mapper
Mapper -->|"Parse IDs"| IdentityParser
Mapper -->|"Parse values"| ValueParser
Definition -->|"Input"| Mapper
Properties -->|"Contained in"| Definition
Mapper -->|"Output"| Entity
Repo -->|"Unmap (write)"| Mapper
Entity -->|"Input"| Mapper
Mapper -->|"Output"| Definition
Repo -->|"Read"| Reader
Reader -->|"Query"| Graph
Repo -->|"Write"| Writer
Writer -->|"Assert/Retract"| Graph Design Principles¶
- Single Responsibility: Each mapper handles one entity type
- Interface Segregation: Keyed and non-keyed mappers use appropriate interfaces
- Generic RDF Definition: Mappers are parameterized by
TRdfDefinitionfor flexibility - Default Implementations: C# 8+ default interface methods reduce boilerplate
- Bidirectional by Design: Support both read (Map) and write (Unmap) operations
- Optional Write Support: Default implementation allows read-only mappers
- Dependency Injection: Parsers injected via constructor
- Transient Lifetime: Factory registered as
ITransientDependencyvia ABP - Fail Fast: Return
nullon validation failures, let repository handle errors - Type Safety: Convert to strong types before entity construction
- Convention over Configuration: Automatic discovery eliminates manual registration
- Flexible Class Matching: Support multiple naming conventions for RDF classes
Usage Examples¶
Mapper Registration (Automatic)¶
Mappers are automatically discovered via assembly scanning - no manual registration needed:
// RdfEntityMapperFactory discovers mappers automatically
// because they implement IRdfEntityMapper<TRdfDefinition, TEntity>
// or IRdfEntityMapper<TRdfDefinition, TEntity, TKey>
Using Mappers via Repository (Read)¶
// Get repository with graph context
var repository = serviceProvider.GetRequiredService<IRdfGraphRepository<MyEntity, Guid>>();
// Find by ID - uses Map() internally
var entity = repository.Find(Guid.Parse("c24a1f3e-..."));
// Get list - uses Map() for each definition
var entities = repository.GetList();
Using Mappers via Repository (Write)¶
// Create new entity
var newEntity = new MyEntity(Guid.NewGuid(), "Name", 42.0f);
// Insert uses Unmap() internally
repository.Insert(newEntity);
// Update uses Unmap() internally
entity.Name = "Updated Name";
repository.Update(entity);
// Upsert uses Unmap() internally
repository.Upsert(newEntity);
Direct Mapper Usage (Testing)¶
var mapper = serviceProvider.GetRequiredService<IRdfEntityMapper<MyRdfDefinition, MyEntity, Guid>>();
// Forward mapping (RDF → Entity)
var entity = mapper.Map(rdfDefinition);
// Reverse mapping (Entity → RDF)
if (mapper.CanUnmap)
{
var definition = mapper.Unmap(entity);
// definition can be used with IRdfInstanceWriter
}