Skip to content

Commit c44912a

Browse files
committed
Improve data access infrastructure, migrations, and documentation
1 parent ece527a commit c44912a

4 files changed

Lines changed: 95 additions & 126 deletions

File tree

‎README.md‎

Lines changed: 72 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,98 @@
11
# NuExt.System.Data
22

3-
`NuExt.System.Data` provides a lightweight and extensible foundation for data access in .NET applications. It combines practical ADO.NET extensions, reusable DAL and database context abstractions, explicit transaction management, and provider-independent database migrations with validated history tracking.
3+
`NuExt.System.Data` is a **lightweight, provider-independent data access foundation for .NET applications and application frameworks**, combining practical ADO.NET extensions, explicit database context ownership, reusable DAL abstractions, and deterministic database migration orchestration.
44

55
[![NuGet](https://img.shields.io/nuget/v/NuExt.System.Data.svg)](https://www.nuget.org/packages/NuExt.System.Data)
66
[![Ask DeepWiki](https://img.shields.io/badge/Ask-DeepWiki-blue)](https://deepwiki.com/nu-ext/NuExt.System.Data)
77
[![Build](https://github.com/nu-ext/NuExt.System.Data/actions/workflows/ci.yml/badge.svg)](https://github.com/nu-ext/NuExt.System.Data/actions/workflows/ci.yml)
88
[![License](https://img.shields.io/github/license/nu-ext/NuExt.System.Data?label=license)](https://github.com/nu-ext/NuExt.System.Data/blob/main/LICENSE)
99
[![Downloads](https://img.shields.io/nuget/dt/NuExt.System.Data.svg)](https://www.nuget.org/packages/NuExt.System.Data)
1010

11-
## Features
11+
## Why NuExt.System.Data?
1212

13-
- **ADO.NET Extensions**: Provides helpers for reading, converting, and manipulating data through `DataReader`, `DataRow`, and `DataTable`.
14-
- **DAL Infrastructure**: Includes lightweight base classes and database context abstractions for implementing data access layers.
15-
- **Database Migrations**: Provides provider-independent orchestration with ordered execution, sequence-aware history validation, optional version metadata, cancellation support, and provider-specific execution contexts.
13+
- **Provider-independent** — common DAL and migration contracts remain separate from provider-specific connections, transactions, and history storage
14+
- **Ownership-aware** — DAL operations explicitly distinguish owned contexts from caller-provided contexts
15+
- **Composable** — independent operations and larger transactional units of work use the same DAL implementation
16+
- **Migration-safe** — configured migrations and applied history are validated as one ordered sequence before pending changes run
17+
- **ADO.NET-focused** — typed reader, row, and table helpers without an ORM or tracking model
18+
- **Consistent across runtimes** — a stable API surface across modern .NET, .NET Standard, and .NET Framework
19+
- **Dependency-light** — no required ORM, hosting model, dependency injection container, or database provider
1620

17-
## DAL Context Ownership
21+
## Designed for
22+
23+
NuExt.System.Data is intended for application and framework authors who need:
24+
25+
- a small reusable foundation above ADO.NET rather than a full ORM;
26+
- explicit ownership and transaction boundaries in data access code;
27+
- the same DAL methods for standalone operations and caller-controlled units of work;
28+
- deterministic migration orchestration with provider-specific execution;
29+
- strict validation of migration ordering and applied history;
30+
- compatibility across modern .NET and legacy .NET Framework targets.
31+
32+
## Core infrastructure
33+
34+
### ADO.NET extensions
35+
36+
- `DataReaderExtensions` adds name-based typed accessors and nullable getters for older targets.
37+
- `DataRowExtensions` provides typed and nullable conversions by index, name, `DataColumn`, or `DataRowVersion`.
38+
- `DataTableExtensions` provides focused table operations without object tracking.
39+
- Compatibility implementations resolve column names to ordinals and preserve provider behavior.
40+
41+
### DAL and database contexts
42+
43+
- `IDbContext` defines the lifetime contract for provider-specific database contexts.
44+
- `IDbContextExecutor<TDbContext>` creates and executes owned context scopes while also supporting operations over existing contexts.
45+
- `DalBase<TDbContext>` centralizes DAL execution and exposes the same synchronous and asynchronous operation model for owned and borrowed contexts.
46+
- `DbContextExtensions` adds focused helpers around context execution without hiding ownership boundaries.
47+
48+
### Explicit context ownership
1849

1950
The DAL infrastructure supports two execution models:
2051

21-
- **Owned context**: When no database context is provided, the provider-specific executor creates and owns the context. It manages the connection, transaction, commit or rollback, and resource cleanup.
22-
- **Borrowed context**: When an existing database context is provided, the DAL participates in the caller's unit of work without committing, rolling back, or disposing the context.
52+
- **Owned context** — when no context is supplied, the provider-specific executor creates and owns the context, including connection lifetime, transaction completion, and cleanup.
53+
- **Borrowed context** — when an existing context is supplied, the DAL participates in the caller's unit of work without committing, rolling back, or disposing that context.
2354

24-
This allows individual DAL operations to run independently while also supporting coordinated operations across multiple DAL components within the same transaction.
55+
This design allows a DAL operation to run independently or compose with other operations inside one caller-controlled transaction without duplicating data access logic.
2556

26-
## Database Migration Model
57+
### Deterministic database migrations
2758

28-
The migration framework is designed for application-integrated, forward-only database evolution. Migrations are registered explicitly in the order in which they must be applied, making the target schema deterministic and avoiding reflection-based discovery.
59+
Migration orchestration is provider-independent; transaction handling and history persistence are provider-specific.
2960

30-
Each applied migration is recorded with its one-based position in the configured migration sequence, stable identifier, application time, and optional version information. Before applying pending migrations, the framework validates that the stored history is an exact prefix of the configured sequence, including both sequence positions and migration identifiers. This detects removed, renamed, reordered, unknown, or incorrectly positioned migrations before any schema changes are made.
61+
- `DbMigration<TContext>` represents an ordered schema or data change with a stable identifier.
62+
- `DbMigrationContext` carries cancellation and provides a base for provider-specific state.
63+
- `DbMigrator<TContext>` validates migration configuration and history before applying pending migrations.
64+
- `IDbMigrationExecutor<TContext>` defines the provider-specific execution and transaction boundary.
65+
- `IDbMigrationHistory<TContext>` defines migration history storage.
66+
- `AppliedDbMigration` records the sequence, identifier, UTC application time, and optional version.
3167

32-
Transaction ownership is explicit and provider-controlled. A provider can execute the complete migration batch within its own transaction or participate in an existing database context, allowing migrations and application operations to share the same transaction.
68+
Before executing pending migrations, the migrator verifies that:
3369

34-
Migration history storage is provider-defined. Providers may maintain a single history or isolate multiple independently validated migration sequences while reusing the same orchestration model.
70+
- configured migration identifiers are non-empty and unique;
71+
- applied history does not contain more entries than the current configuration;
72+
- applied sequence numbers are contiguous and one-based;
73+
- every applied identifier matches the configured migration at the same position.
3574

36-
## Commonly Used Types
75+
This treats migration history as a strict prefix of the configured migration sequence and prevents silently continuing from reordered, removed, duplicated, or otherwise inconsistent migrations.
3776

38-
- **`DalBase<TDbContext>`**: Base class for DAL implementations that supports borrowed contexts and provider-managed owned context scopes.
39-
- **`DataReaderExtensions`**: Provides extension methods for data reader operations.
40-
- **`DataRowExtensions`**: Provides extension methods for data row operations.
41-
- **`DataTableExtensions`**: Provides extension methods for data table operations.
42-
- **`IDbContext`**: Defines transaction state and lifecycle operations for database contexts.
43-
- **`IDbContextExecutor<TDbContext>`**: Defines provider-specific ownership and execution of database context scopes.
44-
- **`AppliedDbMigration`**: Represents an applied migration together with its sequence position, identifier, application time, and optional version information.
45-
- **`DbMigration<TContext>`**: Base class for defining ordered database schema and data migrations.
46-
- **`DbMigrationContext`**: Base class for provider-specific migration execution contexts.
47-
- **`DbMigrator<TContext>`**: Validates migration configuration and applied history, and executes pending migrations.
48-
- **`IDbMigrationExecutor<TContext>`**: Defines provider-specific migration execution and transaction handling.
49-
- **`IDbMigrationHistory<TContext>`**: Defines provider-specific storage operations for applied migration history.
77+
### Cancellation and transaction boundaries
78+
79+
- Cancellation is checked before execution and before each pending migration.
80+
- The migration context passes the cancellation token to provider code and migrations.
81+
- A migration is recorded only after its `Up` operation completes successfully.
82+
- `IDbMigrationExecutor<TContext>` controls transactions and keeps migration history consistent.
83+
84+
## Compatibility
85+
86+
- **.NET Standard 2.0+, .NET 8/9/10, and .NET Framework 4.6.2+**
87+
- Works with provider-specific implementations built on ADO.NET
88+
- Includes selected ADO.NET API backports for older targets
89+
- Does not require Entity Framework or another ORM
5090

5191
## Installation
5292

5393
Via [NuGet](https://www.nuget.org/):
5494

55-
```sh
95+
```shell
5696
dotnet add package NuExt.System.Data
5797
```
5898

@@ -62,25 +102,21 @@ Or via Visual Studio:
62102
2. Search for `NuExt.System.Data`.
63103
3. Click "Install".
64104

65-
## Compatibility
66-
67-
**.NET Standard 2.0+, .NET 8/9/10 and .NET Framework 4.6.2+**
68-
69-
## Usage Examples
105+
## Provider implementation
70106

71-
For a complete provider implementation and migration example, see:
107+
For a complete provider implementation, transaction strategy, migration history store, and usage example, see:
72108

73109
- [NuExt.System.Data.SQLite](https://github.com/nu-ext/NuExt.System.Data.SQLite)
74110

75111
## Ecosystem
76112

113+
- [NuExt.System](https://github.com/nu-ext/NuExt.System)
114+
- [NuExt.System.Data.SQLite](https://github.com/nu-ext/NuExt.System.Data.SQLite)
77115
- [NuExt.Minimal.Mvvm](https://github.com/nu-ext/NuExt.Minimal.Mvvm)
78116
- [NuExt.Minimal.Mvvm.SourceGenerator](https://github.com/nu-ext/NuExt.Minimal.Mvvm.SourceGenerator)
79117
- [NuExt.Minimal.Behaviors.Wpf](https://github.com/nu-ext/NuExt.Minimal.Behaviors.Wpf)
80118
- [NuExt.Minimal.Mvvm.Wpf](https://github.com/nu-ext/NuExt.Minimal.Mvvm.Wpf)
81119
- [NuExt.Minimal.Mvvm.MahApps.Metro](https://github.com/nu-ext/NuExt.Minimal.Mvvm.MahApps.Metro)
82-
- [NuExt.System](https://github.com/nu-ext/NuExt.System)
83-
- [NuExt.System.Data.SQLite](https://github.com/nu-ext/NuExt.System.Data.SQLite)
84120

85121
## Contributing
86122

‎src/NuExt.System.Data.csproj‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
<TargetFrameworks>netstandard2.1;netstandard2.0;net10.0;net9.0;net8.0;net471;net462</TargetFrameworks>
55
<Nullable>enable</Nullable>
66
<ImplicitUsings>enable</ImplicitUsings>
7-
<Description>High-level data access utilities for .NET, providing extensions for DataReader, DataRow, and DataTable, lightweight DAL and database context abstractions, and a provider-independent migration framework with ordered execution and validated history tracking.</Description>
7+
<Description>Lightweight, provider-independent data access foundation for .NET, providing ADO.NET extensions for DataReader, DataRow, and DataTable, reusable DAL and database context abstractions, and deterministic database migrations with ordered execution and validated history tracking.</Description>
88
<RootNamespace />
99
<GenerateDocumentationFile>True</GenerateDocumentationFile>
1010
<NoWarn>$(NoWarn);1591;NETSDK1233</NoWarn>
1111
<VersionPrefix>0.8.4</VersionPrefix>
1212
</PropertyGroup>
1313

1414
<ItemGroup Condition="'$(NuExt_UseLocalProjects)' != 'true'">
15-
<PackageReference Include="NuExt.System" Version="0.7.8" />
15+
<PackageReference Include="NuExt.System" Version="0.8.0" />
1616
</ItemGroup>
1717

1818
<ItemGroup Condition="'$(NuExt_UseLocalProjects)' == 'true'">

‎src/System/Data/DataRowExtensions.Nullable.cs‎

Lines changed: 4 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,6 @@ partial class DataRowExtensions
1717
{
1818
return b;
1919
}
20-
Debug.Assert(obj is long);
21-
/*int result;
22-
if (int.TryParse(Convert.ToString(obj), out result))
23-
{
24-
return result != 0;
25-
}*/
2620
return Convert.ToBoolean(obj);
2721
}
2822

@@ -39,12 +33,6 @@ partial class DataRowExtensions
3933
{
4034
return b;
4135
}
42-
Debug.Assert(obj is long);
43-
/*int result;
44-
if (int.TryParse(Convert.ToString(obj), out result))
45-
{
46-
return result != 0;
47-
}*/
4836
return Convert.ToBoolean(obj);
4937
}
5038

@@ -61,12 +49,6 @@ partial class DataRowExtensions
6149
{
6250
return b;
6351
}
64-
Debug.Assert(obj is long);
65-
/*int result;
66-
if (int.TryParse(Convert.ToString(obj), out result))
67-
{
68-
return result != 0;
69-
}*/
7052
return Convert.ToBoolean(obj);
7153
}
7254

@@ -83,12 +65,6 @@ partial class DataRowExtensions
8365
{
8466
return b;
8567
}
86-
Debug.Assert(obj is long);
87-
/*int result;
88-
if (int.TryParse(Convert.ToString(obj), out result))
89-
{
90-
return result != 0;
91-
}*/
9268
return Convert.ToBoolean(obj);
9369
}
9470

@@ -405,11 +381,7 @@ partial class DataRowExtensions
405381
return null;
406382
}
407383
object obj = dataRow[columnIndex];
408-
if (obj is Guid g)
409-
{
410-
return g;
411-
}
412-
return Guid.Parse(Convert.ToString(obj)!);//TODO
384+
return ConvertToGuid(obj);
413385
}
414386

415387
public static Guid? GetNullableGuid(this DataRow dataRow, string columnName)
@@ -421,11 +393,7 @@ partial class DataRowExtensions
421393
return null;
422394
}
423395
object obj = dataRow[columnName];
424-
if (obj is Guid g)
425-
{
426-
return g;
427-
}
428-
return Guid.Parse(Convert.ToString(obj)!);//TODO
396+
return ConvertToGuid(obj);
429397
}
430398

431399
public static Guid? GetNullableGuid(this DataRow dataRow, DataColumn column)
@@ -437,11 +405,7 @@ partial class DataRowExtensions
437405
return null;
438406
}
439407
object obj = dataRow[column];
440-
if (obj is Guid g)
441-
{
442-
return g;
443-
}
444-
return Guid.Parse(Convert.ToString(obj)!);//TODO
408+
return ConvertToGuid(obj);
445409
}
446410

447411
public static Guid? GetNullableGuid(this DataRow dataRow, DataColumn column, DataRowVersion version)
@@ -453,11 +417,7 @@ partial class DataRowExtensions
453417
return null;
454418
}
455419
object obj = dataRow[column, version];
456-
if (obj is Guid g)
457-
{
458-
return g;
459-
}
460-
return Guid.Parse(Convert.ToString(obj)!);//TODO
420+
return ConvertToGuid(obj);
461421
}
462422

463423
public static short? GetNullableInt16(this DataRow dataRow, int columnIndex)

‎src/System/Data/DataRowExtensions.cs‎

Lines changed: 17 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,7 @@ public static bool GetBoolean(this DataRow dataRow, int columnIndex)
1414
{
1515
return b;
1616
}
17-
Debug.Assert(obj is long || obj is int);
18-
/*int result;
19-
if (int.TryParse(Convert.ToString(obj), out result))
20-
{
21-
return result != 0;
22-
}*/
17+
2318
return Convert.ToBoolean(obj);
2419
}
2520

@@ -32,12 +27,7 @@ public static bool GetBoolean(this DataRow dataRow, string columnName)
3227
{
3328
return b;
3429
}
35-
Debug.Assert(obj is long || obj is int);
36-
/*int result;
37-
if (int.TryParse(Convert.ToString(obj), out result))
38-
{
39-
return result != 0;
40-
}*/
30+
4131
return Convert.ToBoolean(obj);
4232
}
4333

@@ -50,12 +40,7 @@ public static bool GetBoolean(this DataRow dataRow, DataColumn column)
5040
{
5141
return b;
5242
}
53-
Debug.Assert(obj is long || obj is int);
54-
/*int result;
55-
if (int.TryParse(Convert.ToString(obj), out result))
56-
{
57-
return result != 0;
58-
}*/
43+
5944
return Convert.ToBoolean(obj);
6045
}
6146

@@ -68,12 +53,7 @@ public static bool GetBoolean(this DataRow dataRow, DataColumn column, DataRowVe
6853
{
6954
return b;
7055
}
71-
Debug.Assert(obj is long || obj is int);
72-
/*int result;
73-
if (int.TryParse(Convert.ToString(obj), out result))
74-
{
75-
return result != 0;
76-
}*/
56+
7757
return Convert.ToBoolean(obj);
7858
}
7959

@@ -290,49 +270,42 @@ public static Guid GetGuid(this DataRow dataRow, int columnIndex)
290270
ArgumentNullException.ThrowIfNull(dataRow);
291271

292272
object obj = dataRow[columnIndex];
293-
if (obj is Guid g)
294-
{
295-
return g;
296-
}
297-
return Guid.Parse(Convert.ToString(obj)!);//TODO
273+
return ConvertToGuid(obj);
298274
}
299275

300276
public static Guid GetGuid(this DataRow dataRow, string columnName)
301277
{
302278
ArgumentNullException.ThrowIfNull(dataRow);
303279

304280
object obj = dataRow[columnName];
305-
if (obj is Guid g)
306-
{
307-
return g;
308-
}
309-
return Guid.Parse(Convert.ToString(obj)!);//TODO
281+
return ConvertToGuid(obj);
310282
}
311283

312284
public static Guid GetGuid(this DataRow dataRow, DataColumn column)
313285
{
314286
ArgumentNullException.ThrowIfNull(dataRow);
315287

316288
object obj = dataRow[column];
317-
if (obj is Guid g)
318-
{
319-
return g;
320-
}
321-
return Guid.Parse(Convert.ToString(obj)!);//TODO
289+
return ConvertToGuid(obj);
322290
}
323291

324292
public static Guid GetGuid(this DataRow dataRow, DataColumn column, DataRowVersion version)
325293
{
326294
ArgumentNullException.ThrowIfNull(dataRow);
327295

328296
object obj = dataRow[column, version];
329-
if (obj is Guid g)
330-
{
331-
return g;
332-
}
333-
return Guid.Parse(Convert.ToString(obj)!);//TODO
297+
return ConvertToGuid(obj);
334298
}
335299

300+
private static Guid ConvertToGuid(object value)
301+
{
302+
return value switch
303+
{
304+
Guid guid => guid,
305+
string text => Guid.Parse(text),
306+
_ => throw new InvalidCastException($"Cannot convert a value of type '{value.GetType()}' to '{typeof(Guid)}'.")
307+
};
308+
}
336309
public static short GetInt16(this DataRow dataRow, int columnIndex)
337310
{
338311
ArgumentNullException.ThrowIfNull(dataRow);

0 commit comments

Comments
 (0)