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…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"]
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 / Microting (Pomelo fork) / 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 <|-- DataOutputRelational 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 <|.. EfUnitOfWorkThe 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 <|.. MySqlProviderMongoDB 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 <|.. MongoUnitOfWorkDynamoDB 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 <|.. DynamoRepositoryExport 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 : resolvesThe 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.
Asynchronous conventions
Every await inside these packages calls .ConfigureAwait(false). A library that captures the caller’s
synchronisation context deadlocks anyone who blocks on the returned task, and forces a context hop on every
resumption for no benefit — a library has no UI thread to get back to.
Where a value is bound by await using, the value is bound first and the scope declared separately:
var tx = await context.Database.BeginTransactionAsync(ct).ConfigureAwait(false);
await using var txScope = tx.ConfigureAwait(false);
Writing await using var tx = (…).ConfigureAwait(false) would make tx a ConfiguredAsyncDisposable,
which is not the transaction the surrounding code needs. Splitting it keeps the variable’s own type and
still configures the disposal await.
Testing
The test suite is xUnit, and every test is named with the Given / When / Then pattern. Every test class
carries a Category trait, so the two kinds can be run — and reported — separately:
dotnet test src/ArturRios.Data.sln --filter "Category=Unit"
dotnet test src/ArturRios.Data.sln --filter "Category=Functional"
Unit tests exercise the code in isolation against test doubles: contracts, options, dependency-injection registration, the column map and the exporters over in-memory streams. Functional tests run against real stores — SQLite for the relational and Dapper paths, an ephemeral MongoDB replica set for the document repository and its transactions, and DynamoDB Local for the item repository. CI runs the two as separate jobs, and both must pass before a pull request can be merged.