Architecture
Architecture⌗
ArturRios.Data is a family of small, focused NuGet packages. This page shows how they fit together,
the key types in each, and the design principles they share.
Package dependencies⌗
flowchart TB
Output["ArturRios.Output<br/><i>DataOutput / ProcessOutput envelopes</i>"]
subgraph Relational["Relational stack — EF Core"]
direction TB
Core["ArturRios.Data.Relational.Core<br/><i>interfaces, EfRepository, EfUnitOfWork,<br/>BaseDbContext, IDatabaseProvider seam</i>"]
Sqlite["ArturRios.Data.Sqlite<br/><i>AddSqliteProvider()</i>"]
Postgres["ArturRios.Data.PostgreSql<br/><i>AddPostgreSqlProvider()</i>"]
MySql["ArturRios.Data.MySql<br/><i>(deferred)</i>"]
Dapper["ArturRios.Data.Dapper<br/><i>read-only raw SQL</i>"]
end
subgraph NoSQL["NoSQL stores — standalone"]
direction TB
Mongo["ArturRios.Data.MongoDb<br/><i>document repository + transactions</i>"]
Dynamo["ArturRios.Data.DynamoDb<br/><i>async repository over IDynamoDBContext</i>"]
end
subgraph Export["File export — standalone"]
direction TB
ExportCore["ArturRios.Data.Export<br/><i>exporter factory, column map,<br/>CSV / JSON / TXT / MessagePack</i>"]
ExportExcel["ArturRios.Data.Export.Excel<br/><i>.xlsx add-on</i>"]
end
Sqlite --> Core
Postgres --> Core
MySql --> Core
Dapper --> Core
Core --> Output
Mongo --> Output
Dynamo --> Output
ExportExcel --> ExportCore
ExportCore --> Output
EF["Microsoft.EntityFrameworkCore"]:::ext
EfProviders["Npgsql / Pomelo / Sqlite EF provider"]:::ext
DapperLib["Dapper"]:::ext
MongoLib["MongoDB.Driver"]:::ext
Aws["AWSSDK.DynamoDBv2"]:::ext
MsgPack["MessagePack"]:::ext
ClosedXml["ClosedXML"]:::ext
Core --> EF
Sqlite --> EfProviders
Postgres --> EfProviders
MySql --> EfProviders
Dapper --> DapperLib
Mongo --> MongoLib
Dynamo --> Aws
ExportCore --> MsgPack
ExportExcel --> ClosedXml
classDef ext fill:#8882,stroke-dasharray:3 3;
The families are deliberately separate. The relational providers and the Dapper read path build
on ArturRios.Data.Relational.Core (EF Core). The NoSQL packages do not depend on the relational
core — pulling EF Core into a MongoDB or DynamoDB app would be wasteful — so they depend only on
ArturRios.Output and their native driver. MongoDB and DynamoDB are also separate from each other: their
data models diverge too much (composite keys and a key/scan access model in DynamoDB vs. documents with
LINQ/predicate queries in MongoDB) to share one interface without becoming leaky. The export packages
are independent of all of it — they take any IEnumerable<T>, so they need no store at all.
Excel is split out for the same reason, one level down: ClosedXML is a heavy dependency, so it lives in an add-on that apps opt into. The core keeps no compile-time reference to it — the add-on registers a marker type that the exporter factory resolves at runtime.
The result envelope⌗
Every backend returns the same envelope types from ArturRios.Output. A ProcessOutput carries success
state, error messages, and info messages; DataOutput<T> adds a typed payload.
classDiagram
class ProcessOutput {
+bool Success
+List~string~ Messages
+List~string~ Errors
+WithError(string) ProcessOutput
}
class DataOutput~T~ {
+T Data
+WithData(T) DataOutput~T~
}
ProcessOutput <|-- DataOutput
Relational model⌗
The relational core exposes four repository interfaces (a read-only tier and a full read/write tier,
each in a sync and an async flavour), all constrained to T : Entity. EfRepository<T> implements all
four; EfUnitOfWork implements both unit-of-work interfaces. Consumers derive their entities from
Entity (or VersionedEntity for optimistic concurrency) and their DbContext from BaseDbContext.
classDiagram
class Entity { +long Id }
class VersionedEntity { +Guid ConcurrencyStamp }
Entity <|-- VersionedEntity
class IReadOnlyRepository~T~ {
+Query() IQueryable~T~
+GetAll() DataOutput
+GetById(long) DataOutput
}
class IRepository~T~ {
+Create(T) DataOutput
+CreateRange(items) DataOutput
+Update(T) DataOutput
+UpdateRange(items) DataOutput
+Delete(T) DataOutput
+DeleteRange(ids) DataOutput
}
class IAsyncReadOnlyRepository~T~
class IAsyncRepository~T~
class EfRepository~T~
IReadOnlyRepository <|-- IRepository
IAsyncReadOnlyRepository <|-- IAsyncRepository
IRepository <|.. EfRepository
IAsyncRepository <|.. EfRepository
class IUnitOfWork {
+ExecuteInTransaction(work) ProcessOutput
}
class IAsyncUnitOfWork {
+ExecuteInTransactionAsync(work) Task
}
class EfUnitOfWork
IUnitOfWork <|.. EfUnitOfWork
IAsyncUnitOfWork <|.. EfUnitOfWork
The provider seam⌗
The core never references a specific EF provider. Each provider package implements IDatabaseProvider
and registers it as a singleton, exposing which DatabaseType it handles; AddDataConfigFromSettings<TContext>
(or AddDataConfigFromEnvironment<TContext>) reads the configured DatabaseType and picks the matching
provider out of the registered set to configure the DbContext. This is why you call both AddXProvider()
and AddDataConfigFromSettings<TContext>() / AddDataConfigFromEnvironment<TContext>().
Registration validates this eagerly: if it can prove no registered provider matches the configured
DatabaseType, it throws a DataAccessException naming the missing package rather than failing on the
first query.
classDiagram
class IDatabaseProvider {
+DatabaseType Type
+Configure(builder, connectionString)
}
class SqliteProvider
class PostgreSqlProvider
class MySqlProvider
IDatabaseProvider <|.. SqliteProvider
IDatabaseProvider <|.. PostgreSqlProvider
IDatabaseProvider <|.. MySqlProvider
MongoDB model⌗
MongoDB uses a distinct interface family (IDocumentRepository<T> / IAsyncDocumentRepository<T> plus
read-only tiers) with string/ObjectId identity. MongoDocumentRepository<T> implements them over a
MongoContext that carries the ambient session used by MongoUnitOfWork transactions.
classDiagram
class Document { +string Id }
class VersionedDocument { +long Version }
Document <|-- VersionedDocument
class IDocumentRepository~T~
class IAsyncDocumentRepository~T~
class MongoDocumentRepository~T~
IDocumentRepository <|.. MongoDocumentRepository
IAsyncDocumentRepository <|.. MongoDocumentRepository
class IMongoUnitOfWork
class IAsyncMongoUnitOfWork
class MongoUnitOfWork
IMongoUnitOfWork <|.. MongoUnitOfWork
IAsyncMongoUnitOfWork <|.. MongoUnitOfWork
DynamoDB model⌗
DynamoDB has no shared base class — items are plain POCOs annotated with AWS attributes. The single async repository interface maps to DynamoDB’s real access model (key-based load, partition-key query, scan, batch).
classDiagram
class IAsyncDynamoRepository~T~ {
+SaveAsync(T) DataOutput
+LoadAsync(hashKey) DataOutput
+LoadAsync(hashKey, rangeKey) DataOutput
+QueryAsync(hashKey) DataOutput
+ScanAsync(conditions) DataOutput
+SaveManyAsync(items) DataOutput
+LoadManyAsync(hashKeys) DataOutput
}
class DynamoRepository~T~
IAsyncDynamoRepository <|.. DynamoRepository
Export model⌗
Export has no store and no entity base class — it takes any IEnumerable<T>. IExporter<T> is the one
contract; ExporterBase<T> centralizes the null-guarding, envelope conversion, and stream lifetime, so
a concrete exporter only implements the format-specific write. IExporterFactory maps an
ExportFormat to the right exporter out of the container.
classDiagram
class IExporter~T~ {
+WriteAsync(data, stream) ProcessOutput
+WriteToFileAsync(data, path) ProcessOutput
}
class ExporterBase~T~ {
#WriteCoreAsync(data, stream, ct)
}
IExporter <|.. ExporterBase
class CsvExporter~T~
class JsonExporter~T~
class TxtExporter~T~
class MessagePackExporter~T~
class ExcelExporter~T~
ExporterBase <|-- CsvExporter
ExporterBase <|-- JsonExporter
ExporterBase <|-- TxtExporter
ExporterBase <|-- MessagePackExporter
ExporterBase <|-- ExcelExporter
class IExporterFactory {
+Resolve(format) IExporter~T~
}
class ExporterFactory
IExporterFactory <|.. ExporterFactory
ExporterFactory ..> IExporter : resolves
The columnar formats (CSV, Excel) share one ColumnMap, which compiles and caches a per-type column
plan from the record’s public properties, honouring [ExportColumn] and [ExportIgnore].
Design principles⌗
- Modular packaging. One package per backend; install only what you use. NoSQL packages don’t drag in EF Core.
- Envelopes, not exceptions. Every public repository/unit-of-work method catches infrastructure
exceptions and returns them as
DataOutput/ProcessOutputerrors. Optimistic-concurrency conflicts become a friendly “concurrency conflict” error. The one intentional exception isOperationCanceledException, which propagates so cooperative cancellation stays idiomatic. - Opt-in optimistic concurrency. Derive from
VersionedEntity/VersionedDocument, or add[DynamoDBVersion], to get conditional writes; without it, writes are last-writer-wins. - Transactions where the engine supports them. Relational and MongoDB expose a delegate-based unit of work; the Dapper read path enlists in the relational transaction. (DynamoDB transactions are a planned addition.)
- Consistent naming.
AddDataConfigFromSettings/AddMongoData/AddDynamoData/AddExportfor DI;DataOutput<T>/ProcessOutputeverywhere;Asyncsuffix +CancellationTokenon async members.
See the Relational, MongoDB, DynamoDB, and Export guides for full usage.