Skip to content

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 IRdfProperty interface
  • 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 null for 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 CanUnmap property

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&lt;TKey&gt;"]
        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 resource
  • Properties - Collection of IRdfProperty instances

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<,> or IRdfEntityMapper<,,>
  • Builds type registry mapping interface to implementation
  • Caches registry using Lazy<T> for subsequent lookups
  • Resolves instances via IServiceProvider with 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&lt;T&gt;"]
        Reader["IRdfInstanceReader&lt;T&gt;"]
    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 TRdfDefinition for 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 ITransientDependency via ABP
  • Fail Fast: Return null on 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
}