Skip to content

RDF Entity Access Layer

Purpose and Overview

The Entity Access Layer provides domain-centric CRUD operations over RDF graphs, abstracting away triple-store queries and RDF syntax to expose a clean entity-based API. It bridges the gap between raw RDF triples and rich domain entities, ensuring that RDF data is properly materialized with type safety and business invariants.

Core Goal: Provide full CRUD (Create, Read, Update, Delete) operations over RDF graphs, materializing rich Domain entities with invariants and behavior. The repository layer never returns DTOs—it works exclusively with domain entities.

Key Responsibilities

  1. Domain Entity Materialization: Transform RDF triples into rich domain entities with full type safety
  2. Graph Access Abstraction: Hide RDF-specific query complexity behind standard repository methods
  3. Identity Management: Handle RDF resource ID conventions (fragment IDs, URNs, full URIs) and normalize them to domain keys
  4. Lifecycle Coordination: Coordinate between graph context, readers, writers, and mappers to provide seamless CRUD operations
  5. Bidirectional Mapping: Support both reading (RDF → Entity) and writing (Entity → RDF) operations

Architecture

Component Overview

graph TB
    subgraph "Entity Access Layer"
        RepoBase["RdfRepositoryBase<TEntity>"]
        BasicRepo["RdfBasicRepositoryBase<TEntity>"]
        GraphRepo["RdfGraphRepository<TContext, TRdfDef, TEntity>"]
        GraphRepoKeyed["RdfGraphRepository<TContext, TRdfDef, TEntity, TKey>"]
    end

    subgraph "Infrastructure Layer"
        Context["IRdfGraphContext"]
        Graph["IGraph (VDS.RDF)"]
    end

    subgraph "Core Dependencies"
        Reader["IRdfInstanceReader<TRdfDef>"]
        Writer["IRdfInstanceWriter<TRdfDef>"]
        MapperFactory["IRdfEntityMapperFactory"]
        Mapper["IRdfEntityMapper<TRdfDef, TEntity>"]
    end

    subgraph "Domain Layer"
        Entity["Domain Entity"]
    end

    GraphRepo -->|extends| RepoBase
    GraphRepoKeyed -->|extends| GraphRepo
    RepoBase -->|extends| BasicRepo
    GraphRepo -->|uses| Context
    GraphRepo -->|uses| Reader
    GraphRepo -->|uses| Writer
    GraphRepo -->|uses| MapperFactory

    Context -->|provides| Graph
    Reader -->|extracts from| Graph
    Writer -->|modifies| Graph
    MapperFactory -->|resolves| Mapper
    Mapper -->|produces/consumes| Entity

Interface Hierarchy

graph TB
    subgraph "Repository Interfaces"
        IRdfRepository["IRdfRepository"]
        IRdfReadOnlyBasic["IRdfReadOnlyBasicRepository<TEntity>"]
        IRdfBasic["IRdfBasicRepository<TEntity>"]
        IRdfReadOnly["IRdfReadOnlyRepository<TEntity>"]
        IRdfRepo["IRdfRepository<TEntity>"]
        IRdfGraph["IRdfGraphRepository<TEntity>"]

        IRdfReadOnlyBasicKeyed["IRdfReadOnlyBasicRepository<TEntity, TKey>"]
        IRdfBasicKeyed["IRdfBasicRepository<TEntity, TKey>"]
        IRdfReadOnlyKeyed["IRdfReadOnlyRepository<TEntity, TKey>"]
        IRdfRepoKeyed["IRdfRepository<TEntity, TKey>"]
        IRdfGraphKeyed["IRdfGraphRepository<TEntity, TKey>"]
    end

    IRdfRepository --> IRdfReadOnlyBasic
    IRdfReadOnlyBasic --> IRdfBasic
    IRdfReadOnlyBasic --> IRdfReadOnly
    IRdfBasic --> IRdfRepo
    IRdfReadOnly --> IRdfRepo
    IRdfRepo --> IRdfGraph

    IRdfReadOnlyBasic --> IRdfReadOnlyBasicKeyed
    IRdfBasic --> IRdfBasicKeyed
    IRdfReadOnlyBasicKeyed --> IRdfBasicKeyed
    IRdfReadOnlyBasicKeyed --> IRdfReadOnlyKeyed
    IRdfRepo --> IRdfRepoKeyed
    IRdfReadOnlyKeyed --> IRdfRepoKeyed
    IRdfBasicKeyed --> IRdfRepoKeyed
    IRdfGraph --> IRdfGraphKeyed
    IRdfRepoKeyed --> IRdfGraphKeyed

Core Components

IRdfRepository

Purpose: Base marker interface for all RDF repositories.

Properties:

public interface IRdfRepository
{
    string GraphName { get; }
    string? EntityName { get; set; }
}

RdfGraphRepository<TGraphContext, TRdfDefinition, TEntity>

Purpose: Domain entity repository implementation for non-keyed entities backed by RDF graphs with full CRUD support.

Type Parameters:

  • TGraphContext : IRdfGraphContext - The graph context type
  • TRdfDefinition : IRdfDefinition - The RDF definition type for intermediate representation
  • TEntity : IEntity - Domain entity type

Dependencies:

  • TGraphContext - Graph lifecycle and access (see Graph Context Documentation)
  • IRdfInstanceReader<TRdfDefinition> - RDF definition extraction from graphs
  • IRdfInstanceWriter<TRdfDefinition> - RDF definition writing to graphs
  • IRdfEntityMapperFactory - Entity mapper resolution
  • string graphName - Target graph identifier

RdfGraphRepository<TGraphContext, TRdfDefinition, TEntity, TKey>

Purpose: Domain entity repository implementation for keyed entities with efficient key-based operations.

Type Parameters:

  • TGraphContext : IRdfGraphContext - The graph context type
  • TRdfDefinition : IRdfDefinition - The RDF definition type
  • TEntity : IEntity<TKey> - Domain entity type with a key
  • TKey - Entity primary key type (typically Guid or string)

Key Operations:

Category Method Signature Description
Read Find(TKey) TEntity? Find(TKey id) Find entity by key (returns null if not found)
Read Get(TKey) TEntity Get(TKey id) Get entity by key (throws if not found)
Read FindById(string) TEntity? FindById(string mrid) Find entity by MRID
Read Find(predicate) TEntity? Find(Expression<Func<TEntity, bool>>) Find first entity matching predicate
Read Get(predicate) TEntity Get(Expression<Func<TEntity, bool>>) Get entity matching predicate (throws if not found)
Read GetList() List<TEntity> GetList() Get all entities in graph
Read GetList(predicate) List<TEntity> GetList(Expression<Func<TEntity, bool>>) Get entities matching predicate
Read GetPagedList(...) List<TEntity> GetPagedList(int skipCount, int maxResultCount, string sorting) Get paged and sorted entities
Read GetCount() long GetCount() Count entities in graph
Write Insert(entity) TEntity Insert(TEntity entity) Insert new entity (throws if exists)
Write InsertMany(entities) void InsertMany(IEnumerable<TEntity> entities) Batch insert entities
Write Update(entity) TEntity Update(TEntity entity) Update existing entity (throws if not exists)
Write UpdateMany(entities) void UpdateMany(IEnumerable<TEntity> entities) Batch update entities
Write Upsert(entity) TEntity Upsert(TEntity entity) Insert or update entity
Write UpsertMany(entities) void UpsertMany(IEnumerable<TEntity> entities) Batch insert or update
Delete Delete(TKey) void Delete(TKey id) Delete entity by key
Delete Delete(entity) void Delete(TEntity entity) Delete entity instance
Delete Delete(predicate) void Delete(Expression<Func<TEntity, bool>>) Delete entities matching predicate
Delete DeleteById(string) void DeleteById(string mrid) Delete entity by MRID
Delete DeleteMany(ids) void DeleteMany(IEnumerable<TKey> ids) Batch delete by keys
Delete DeleteMany(entities) void DeleteMany(IEnumerable<TEntity> entities) Batch delete entities
Graph GetGraphContext(...) IGraph GetGraphContext(string? graphName = null) Get underlying RDF graph
Graph GetGraphContextAsync(...) Task<IGraph> GetGraphContextAsync(string? graphName = null, ...) Get underlying RDF graph async

CRUD Operations

Read Operations

Read operations use IRdfInstanceReader<TRdfDefinition> to extract RDF definitions and IRdfEntityMapper<TRdfDefinition, TEntity>.Map() to convert them to domain entities.

Entity Resolution Flow (Read)

public override TEntity? FindById(string mrid)
{
    var graph = GetGraphContext();
    if (graph == null || string.IsNullOrWhiteSpace(mrid))
        return default;

    // Extract RDF definition using Reader
    var definition = Reader.Find(graph, mrid);
    if (definition == null || !Mapper.CanMap(definition.ClassName))
        return default;

    // Map to domain entity using forward mapping
    return Mapper.Map(definition);
}

Find by Key (Keyed Repository)

public virtual TEntity? Find(TKey id)
{
    var graph = GetGraphContext();
    if (graph == null)
        return default;

    // Normalize the key to a string representation
    var idString = NormalizeKey(id);
    if (string.IsNullOrWhiteSpace(idString))
        return default;

    // Extract the instance definition from the graph
    var definition = Reader.Find(graph, idString);
    if (definition == null || !Mapper.CanMap(definition.ClassName))
        return default;

    return Mapper.Map(definition);
}

Find with Predicate

public override TEntity? Find(Expression<Func<TEntity, bool>> predicate)
{
    var graph = GetGraphContext();
    if (graph == null)
        return default;

    var definitions = Reader.GetList(graph);
    var compiledPredicate = predicate.Compile();

    foreach (var definition in definitions)
    {
        if (!Mapper.CanMap(definition.ClassName))
            continue;

        var entity = Mapper.Map(definition);
        if (entity != null && compiledPredicate(entity))
            return entity;
    }

    return default;
}

Write Operations

Write operations require mappers that support reverse mapping (CanUnmap = true and Unmap() method). They use IRdfInstanceWriter<TRdfDefinition> to modify the graph.

Insert (Create New Entity)

public override TEntity Insert(TEntity entity)
{
    if (entity == null)
        throw new ArgumentNullException(nameof(entity));

    var graph = GetGraphContext();
    if (graph == null)
        throw new InvalidOperationException("Graph context is not available.");

    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 when converting {typeof(TEntity).Name} to RDF definition.");

    if (Writer.Exists(graph, definition.Id))
        throw new InvalidOperationException($"Instance with id '{definition.Id}' already exists. Use Update instead.");

    var triplesInserted = Writer.Insert(graph, definition);

    return entity;
}

Update (Modify Existing Entity)

public override TEntity Update(TEntity entity)
{
    if (entity == null)
        throw new ArgumentNullException(nameof(entity));

    var graph = GetGraphContext();
    if (graph == null)
        throw new InvalidOperationException("Graph context is not available.");

    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 when converting {typeof(TEntity).Name} to RDF definition.");

    var instanceId = definition.Id;
    if (string.IsNullOrWhiteSpace(instanceId))
        throw new InvalidOperationException($"Instance id is invalid or represents a composite key.");

    if (!Writer.Exists(graph, instanceId))
        throw new InvalidOperationException($"Instance with id '{instanceId}' does not exist. Use Upsert instead.");

    var netChange = Writer.Update(graph, definition);

    return entity;
}

Upsert (Insert or Update)

public override TEntity Upsert(TEntity entity)
{
    if (entity == null)
        throw new ArgumentNullException(nameof(entity));

    var graph = GetGraphContext();
    if (graph == null)
        throw new InvalidOperationException("Graph context is not available.");

    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 when converting {typeof(TEntity).Name} to RDF definition.");

    var instanceId = definition.Id;
    if (string.IsNullOrWhiteSpace(instanceId))
        throw new InvalidOperationException($"Instance id is invalid or represents a composite key.");

    var exists = Writer.Exists(graph, instanceId);

    if (exists)
    {
        Writer.Update(graph, definition);
    }
    else
    {
        Writer.Insert(graph, definition);
    }

    return entity;
}

Delete Operations

Delete operations use IRdfInstanceWriter<TRdfDefinition> to remove triples from the graph.

Delete by Key (Keyed Repository)

public virtual void Delete(TKey id)
{
    var graph = GetGraphContext();
    if (graph == null)
        return;

    var idString = NormalizeKey(id);
    if (string.IsNullOrWhiteSpace(idString))
        return;

    var triplesDeleted = Writer.Delete(graph, idString);
}

Delete by MRID

public override void DeleteById(string mrid)
{
    var graph = GetGraphContext();
    if (graph == null || string.IsNullOrWhiteSpace(mrid))
        return;

    var triplesDeleted = Writer.Delete(graph, mrid);
}

Delete by Entity

public override void Delete(TEntity entity)
{
    if (entity == null)
        return;

    var graph = GetGraphContext();
    if (graph == null)
        return;

    if (!Mapper.CanUnmap)
        return;

    var definition = Mapper.Unmap(entity);
    if (definition == null || string.IsNullOrWhiteSpace(definition.Id))
        return;

    Writer.Delete(graph, definition.Id);
}

Delete by Predicate

public override void Delete(Expression<Func<TEntity, bool>> predicate)
{
    if (predicate == null)
        return;

    var graph = GetGraphContext();
    if (graph == null)
        return;

    var definitions = Reader.GetList(graph);
    var compiledPredicate = predicate.Compile();
    var instanceIdsToDelete = new List<string>();

    foreach (var definition in definitions)
    {
        if (!Mapper.CanMap(definition.ClassName))
            continue;

        var entity = Mapper.Map(definition);
        if (entity != null && compiledPredicate(entity))
            instanceIdsToDelete.Add(definition.Id);
    }

    if (instanceIdsToDelete.Count > 0)
        Writer.DeleteMany(graph, instanceIdsToDelete);
}

DeleteMany (Batch Delete - Keyed Repository)

public virtual void DeleteMany(IEnumerable<TKey> ids)
{
    var graph = GetGraphContext();
    if (graph == null || ids == null)
        return;

    var instanceIds = new List<string>();
    foreach (var id in ids)
    {
        var idString = NormalizeKey(id);
        if (!string.IsNullOrWhiteSpace(idString))
            instanceIds.Add(idString);
    }

    if (instanceIds.Count > 0)
        Writer.DeleteMany(graph, instanceIds);
}

Operation Requirements

Mapper Capabilities

Operation Requires CanMap Requires Map Requires CanUnmap Requires Unmap Notes
Find / Get ✅ Yes ✅ Yes ❌ No ❌ No Read-only operation
FindById ✅ Yes ✅ Yes ❌ No ❌ No Read-only operation
GetList ✅ Yes ✅ Yes ❌ No ❌ No Read-only operation
GetCount ✅ Yes ❌ No ❌ No ❌ No Only checks CanMap
Insert ❌ No ❌ No ✅ Yes ✅ Yes Entity → RDF conversion
Update ❌ No ❌ No ✅ Yes ✅ Yes Entity → RDF conversion
Upsert ❌ No ❌ No ✅ Yes ✅ Yes Entity → RDF conversion
Delete (by key/MRID) ❌ No ❌ No ❌ No ❌ No Uses ID only
Delete (by entity) ❌ No ❌ No ✅ Yes ✅ Yes Needs ID from Unmap
Delete (by predicate) ✅ Yes ✅ Yes ❌ No ❌ No Needs to evaluate predicate

Error Handling

Scenario Operation Behavior
Entity not found Get(id) / Get(predicate) Throws EntityNotFoundException or InvalidOperationException
Entity not found Find(id) / Find(predicate) Returns null
Entity already exists Insert Throws InvalidOperationException
Entity doesn't exist Update Throws InvalidOperationException
Mapper doesn't support Unmap Insert / Update / Upsert Throws NotSupportedException
Mapper returns null from Unmap Insert / Update / Upsert Throws InvalidOperationException
Graph context unavailable Any write operation Throws InvalidOperationException
Invalid/composite key Get(id) Throws NotSupportedException
Invalid/composite key Find(id) Returns null
Entity null Insert / Update / Upsert Throws ArgumentNullException

Sequence Diagrams

Read Operation Flow

sequenceDiagram
    participant Service as Application Service
    participant Repository as RdfGraphRepository
    participant Context as RdfGraphContext
    participant Reader as IRdfInstanceReader
    participant Mapper as IRdfEntityMapper
    participant Graph as IGraph

    Service->>Repository: Find(id)
    Repository->>Repository: NormalizeKey(id)
    Repository->>Context: GetGraph(graphName)
    Context-->>Repository: IGraph

    Repository->>Reader: Find(graph, idString)
    Reader->>Graph: Query triples
    Graph-->>Reader: RDF triples
    Reader-->>Repository: TRdfDefinition

    Repository->>Mapper: CanMap(definition.ClassName)
    Mapper-->>Repository: true
    Repository->>Mapper: Map(definition)
    Mapper-->>Repository: TEntity

    Repository-->>Service: TEntity

Write Operation Flow (Upsert)

sequenceDiagram
    participant Service as Application Service
    participant Repository as RdfGraphRepository
    participant Mapper as IRdfEntityMapper
    participant Writer as IRdfInstanceWriter
    participant Graph as IGraph

    Service->>Repository: Upsert(entity)

    Repository->>Mapper: CanUnmap
    Mapper-->>Repository: true

    Repository->>Mapper: Unmap(entity)
    Mapper-->>Repository: TRdfDefinition

    Repository->>Writer: Exists(graph, id)
    Writer->>Graph: Check triples
    Graph-->>Writer: exists/not exists
    Writer-->>Repository: boolean

    alt Entity exists
        Repository->>Writer: Update(graph, definition)
        Writer->>Graph: Retract old triples
        Writer->>Graph: Assert new triples
    else Entity doesn't exist
        Repository->>Writer: Insert(graph, definition)
        Writer->>Graph: Assert triples
    end

    Writer-->>Repository: triples count
    Repository-->>Service: TEntity

Delete Operation Flow

sequenceDiagram
    participant Service as Application Service
    participant Repository as RdfGraphRepository
    participant Writer as IRdfInstanceWriter
    participant Graph as IGraph

    Service->>Repository: Delete(id)

    Repository->>Repository: NormalizeKey(id)
    Repository->>Writer: Delete(graph, idString)
    Writer->>Graph: Get triples with subject
    Graph-->>Writer: Matching triples
    Writer->>Graph: Retract triples
    Writer-->>Repository: triplesDeleted

    Repository-->>Service: void

Core Dependencies

Infrastructure

IRdfGraphContext

Purpose: Manages RDF graph lifecycle and data loading.

Key Capabilities:

  • Multi-graph container management (CreateGraph, GetGraph, RemoveGraph)
  • Load from multiple sources (LoadFromFileAsync, LoadFromUriAsync, LoadFromStreamAsync, LoadFromStringAsync)
  • Graph existence checks (GraphExists, IsLoaded)
  • Thread-safe concurrent access

See: Graph Context Documentation for detailed information.

Reading

IRdfInstanceReader<TRdfDefinition>

Purpose: Extracts structured RDF data into intermediate definition objects.

Key Operations:

public interface IRdfInstanceReader<TRdfDefinition>
    where TRdfDefinition : class, IRdfDefinition
{
    /// <summary>
    /// Searches the graph for a definition matching the given instance identifier.
    /// </summary>
    TRdfDefinition? Find(IGraph graph, string instanceId);

    /// <summary>
    /// Retrieves all RDF definitions from the graph.
    /// </summary>
    IEnumerable<TRdfDefinition> GetList(IGraph graph);
}

Writing

IRdfInstanceWriter<TRdfDefinition>

Purpose: Modifies RDF graphs by inserting, updating, and deleting triples.

Key Operations:

public interface IRdfInstanceWriter<TRdfDefinition>
    where TRdfDefinition : class, IRdfDefinition
{
    /// <summary>
    /// Inserts an RDF definition into the graph.
    /// </summary>
    int Insert(IGraph graph, TRdfDefinition definition);

    /// <summary>
    /// Inserts multiple RDF definitions into the graph.
    /// </summary>
    int InsertMany(IGraph graph, IEnumerable<TRdfDefinition> definitions);

    /// <summary>
    /// Deletes an instance from the graph by its identifier.
    /// </summary>
    int Delete(IGraph graph, string instanceId, bool deleteIncomingReferences = true);

    /// <summary>
    /// Deletes multiple instances from the graph.
    /// </summary>
    int DeleteMany(IGraph graph, IEnumerable<string> instanceIds);

    /// <summary>
    /// Updates an existing instance (delete + insert).
    /// </summary>
    int Update(IGraph graph, TRdfDefinition definition);

    /// <summary>
    /// Checks if an instance exists in the graph.
    /// </summary>
    bool Exists(IGraph graph, string instanceId);
}

Mapping

IRdfEntityMapperFactory

Purpose: Factory for creating RDF entity mappers.

Contract:

public interface IRdfEntityMapperFactory
{
    IRdfEntityMapper<TRdfDefinition, TEntity> GetMapper<TRdfDefinition, TEntity>()
        where TRdfDefinition : class, IRdfDefinition
        where TEntity : class, IEntity;

    IRdfEntityMapper<TRdfDefinition, TEntity, TKey> GetMapper<TRdfDefinition, TEntity, TKey>()
        where TRdfDefinition : class, IRdfDefinition
        where TEntity : class, IEntity<TKey>;
}

IRdfEntityMapper<TRdfDefinition, TEntity>

Purpose: Bidirectional transformation between RDF definitions and domain entities.

Contract:

public interface IRdfEntityMapper<TRdfDefinition, TEntity>
    where TRdfDefinition : class, IRdfDefinition
    where TEntity : class, IEntity
{
    // Forward mapping (RDF → Entity)
    bool CanMap(string className);
    TEntity? Map(TRdfDefinition definition);

    // Reverse mapping (Entity → RDF)
    bool CanUnmap => false;  // Default: false
    TRdfDefinition? Unmap(TEntity entity) => null;  // Default: null
}

public interface IRdfEntityMapper<TRdfDefinition, TEntity, TKey> : IRdfEntityMapper<TRdfDefinition, TEntity>
    where TRdfDefinition : class, IRdfDefinition
    where TEntity : class, IEntity<TKey>
{
}

See: Mappers Documentation for implementation patterns and examples.

Key Normalization

The keyed repository normalizes entity keys to string representations suitable for RDF resource identification:

protected virtual string? NormalizeKey(object? key)
{
    if (key is null)
        return null;

    // Reject composite keys by simple heuristics
    static bool LooksComposite(string s) =>
        s.Contains('|') || s.Contains(',') || s.Contains(';') || s.Contains(':');

    switch (key)
    {
        case string s:
            s = s.Trim();
            if (s.Length == 0 || LooksComposite(s)) return null;
            return s;

        case Guid g:
            // Canonical "D" format: 00000000-0000-0000-0000-000000000000
            return g.ToString("D", CultureInfo.InvariantCulture);

        default:
            var str = key.ToString()?.Trim();
            if (string.IsNullOrEmpty(str) || LooksComposite(str)) return null;
            return str;
    }
}

Usage Examples

Read Operations

// Assuming repository is injected or created
var repository = serviceProvider.GetRequiredService<IRdfGraphRepository<MyEntity, Guid>>();

// Find by key (returns null if not found)
var entity = repository.Find(Guid.Parse("550e8400-e29b-41d4-a716-446655440000"));

// Get by key (throws if not found)
var entity = repository.Get(Guid.Parse("550e8400-e29b-41d4-a716-446655440000"));

// Find by MRID
var entity = repository.FindById("550e8400-e29b-41d4-a716-446655440000");

// Find by predicate
var entity = repository.Find(e => e.Name == "Example");

// Get by predicate (throws if not found)
var entity = repository.Get(e => e.Name == "Example");

// Get all entities
var allEntities = repository.GetList();

// Get filtered list
var filteredEntities = repository.GetList(e => e.Value > 100);

// Get paged and sorted list
var pagedEntities = repository.GetPagedList(
    skipCount: 0,
    maxResultCount: 10,
    sorting: "Name DESC"
);

// Get count
var count = repository.GetCount();

Write Operations

// Create new entity
var newEntity = new MyEntity(Guid.NewGuid(), "Example", 42.0f);

// Insert (throws if already exists)
repository.Insert(newEntity);

// Batch insert
repository.InsertMany(new[] { entity1, entity2, entity3 });

// Update (throws if doesn't exist)
existingEntity.Name = "Updated Name";
repository.Update(existingEntity);

// Batch update
repository.UpdateMany(new[] { entity1, entity2 });

// Upsert (insert or update)
repository.Upsert(entity);

// Batch upsert
repository.UpsertMany(new[] { entity1, entity2, entity3 });

Delete Operations

// Delete by key (keyed repository)
repository.Delete(Guid.Parse("550e8400-e29b-41d4-a716-446655440000"));

// Delete by MRID
repository.DeleteById("550e8400-e29b-41d4-a716-446655440000");

// Delete by entity
repository.Delete(existingEntity);

// Delete by predicate
repository.Delete(e => e.Value < 10);

// Batch delete by keys
repository.DeleteMany(new[] { id1, id2, id3 });

// Batch delete entities
repository.DeleteMany(new[] { entity1, entity2 });

Direct Graph Access

// Get underlying RDF graph for advanced operations
IGraph graph = repository.GetGraphContext();

// Or async
IGraph graph = await repository.GetGraphContextAsync();

// Access a different graph
IGraph otherGraph = repository.GetGraphContext("other-graph-name");

Guidelines and Best Practices

Repository Design

  1. Keep repositories domain-focused: Never return DTOs from repositories
  2. Graph name consistency: Use meaningful, stable graph names
  3. Single responsibility: Each repository handles one aggregate root type
  4. Use keyed repositories: Prefer keyed variants for efficient key-based lookups

Entity Mapping

  1. Implement Unmap for write operations: If your repository needs Insert/Update/Upsert, implement CanUnmap and Unmap in your mapper
  2. Register mappers properly: Ensure mappers are registered with dependency injection
  3. Enforce invariants: Validate required properties in mapper
  4. Handle missing data gracefully: Return null from mapper when data is invalid

Identity Management

  1. Normalize IDs consistently: Keys are automatically normalized via NormalizeKey
  2. Follow RDF conventions:
  3. rdf:ID for fragments: #_uuid
  4. rdf:about for full URIs: urn:uuid:xxxx or base#_uuid
  5. Avoid composite keys: Composite keys (containing |, ,, ;, :) are not supported

Error Handling

  1. Repository level:
  2. Find: Return null when entity doesn't exist
  3. Get: Throw exception when entity doesn't exist
  4. Insert: Throw when entity already exists
  5. Update: Throw when entity doesn't exist
  6. Upsert: Never throws for existence (insert or update)
  7. Mapper level: Throw NotSupportedException if write operation called without CanUnmap support

Performance Considerations

  1. Use keyed lookups: Find(TKey) and Get(TKey) are O(1) operations; predicate-based finds are O(n)
  2. Batch operations: Use InsertMany, UpsertMany, DeleteMany when processing multiple entities
  3. Predicate operations: Delete(predicate) and GetList(predicate) scan all entities - use key-based methods when possible
  4. Pagination: GetPagedList loads all entities into memory before paging - consider alternatives for very large datasets

Summary

Component Responsibilities

Component Layer Responsibility Input Output
RdfGraphRepository<TContext, TRdfDef, TEntity> Repository Non-keyed domain CRUD operations TEntity / MRID Domain entities
RdfGraphRepository<TContext, TRdfDef, TEntity, TKey> Repository Keyed domain CRUD operations TKey / TEntity Domain entities
RdfRepositoryBase<TEntity> Repository Common CRUD patterns with predicates TEntity / predicates Domain entities
RdfBasicRepositoryBase<TEntity> Repository Base CRUD operations TEntity Domain entities
IRdfGraphContext Infrastructure Multi-graph lifecycle management RDF/XML IGraph
IRdfInstanceReader<TRdfDef> Reading Extract RDF resources IGraph + ID TRdfDefinition
IRdfInstanceWriter<TRdfDef> Writing Modify RDF graphs TRdfDefinition Triple counts
IRdfEntityMapperFactory Mapping Mapper resolution Entity type IRdfEntityMapper
IRdfEntityMapper<TRdfDef, TEntity> Mapping Bidirectional conversion TRdfDefinition ↔ Entity Domain entity / TRdfDefinition

Golden Rules

  1. Repositories work exclusively with domain entities - Never expose DTOs, RDF types, or triple-store details through repository interfaces
  2. Mappers support bidirectional mapping - Implement Unmap for write operations, Map for read operations
  3. Context handles infrastructure - Repositories delegate all graph operations to IRdfGraphContext
  4. Writer handles modifications - All insert/update/delete operations go through IRdfInstanceWriter
  5. Reader handles extraction - All find/list operations go through IRdfInstanceReader
  6. Each layer has clear boundaries - Infrastructure → Reading/Writing → Mapping → Repository → Application
  7. Keyed repositories are preferred - Use keyed variants for efficient O(1) lookups when entity keys are known

This structure keeps RDF concerns isolated, preserves domain rules, and provides a clean entity-based API to application services with full CRUD support.


  • Graph Context - RDF graph lifecycle and data loading
  • Mappers - Bidirectional entity mapping patterns