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¶
- Domain Entity Materialization: Transform RDF triples into rich domain entities with full type safety
- Graph Access Abstraction: Hide RDF-specific query complexity behind standard repository methods
- Identity Management: Handle RDF resource ID conventions (fragment IDs, URNs, full URIs) and normalize them to domain keys
- Lifecycle Coordination: Coordinate between graph context, readers, writers, and mappers to provide seamless CRUD operations
- 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 typeTRdfDefinition : IRdfDefinition- The RDF definition type for intermediate representationTEntity : IEntity- Domain entity type
Dependencies:
TGraphContext- Graph lifecycle and access (see Graph Context Documentation)IRdfInstanceReader<TRdfDefinition>- RDF definition extraction from graphsIRdfInstanceWriter<TRdfDefinition>- RDF definition writing to graphsIRdfEntityMapperFactory- Entity mapper resolutionstring 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 typeTRdfDefinition : IRdfDefinition- The RDF definition typeTEntity : IEntity<TKey>- Domain entity type with a keyTKey- Entity primary key type (typicallyGuidorstring)
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¶
- Keep repositories domain-focused: Never return DTOs from repositories
- Graph name consistency: Use meaningful, stable graph names
- Single responsibility: Each repository handles one aggregate root type
- Use keyed repositories: Prefer keyed variants for efficient key-based lookups
Entity Mapping¶
- Implement Unmap for write operations: If your repository needs Insert/Update/Upsert, implement
CanUnmapandUnmapin your mapper - Register mappers properly: Ensure mappers are registered with dependency injection
- Enforce invariants: Validate required properties in mapper
- Handle missing data gracefully: Return
nullfrom mapper when data is invalid
Identity Management¶
- Normalize IDs consistently: Keys are automatically normalized via
NormalizeKey - Follow RDF conventions:
rdf:IDfor fragments:#_uuidrdf:aboutfor full URIs:urn:uuid:xxxxorbase#_uuid- Avoid composite keys: Composite keys (containing
|,,,;,:) are not supported
Error Handling¶
- Repository level:
Find: Returnnullwhen entity doesn't existGet: Throw exception when entity doesn't existInsert: Throw when entity already existsUpdate: Throw when entity doesn't existUpsert: Never throws for existence (insert or update)- Mapper level: Throw
NotSupportedExceptionif write operation called withoutCanUnmapsupport
Performance Considerations¶
- Use keyed lookups:
Find(TKey)andGet(TKey)are O(1) operations; predicate-based finds are O(n) - Batch operations: Use
InsertMany,UpsertMany,DeleteManywhen processing multiple entities - Predicate operations:
Delete(predicate)andGetList(predicate)scan all entities - use key-based methods when possible - Pagination:
GetPagedListloads 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¶
- Repositories work exclusively with domain entities - Never expose DTOs, RDF types, or triple-store details through repository interfaces
- Mappers support bidirectional mapping - Implement
Unmapfor write operations,Mapfor read operations - Context handles infrastructure - Repositories delegate all graph operations to
IRdfGraphContext - Writer handles modifications - All insert/update/delete operations go through
IRdfInstanceWriter - Reader handles extraction - All find/list operations go through
IRdfInstanceReader - Each layer has clear boundaries - Infrastructure → Reading/Writing → Mapping → Repository → Application
- 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.
Related Resources¶
- Graph Context - RDF graph lifecycle and data loading
- Mappers - Bidirectional entity mapping patterns