DCoding.Data.DVault 0.8.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package DCoding.Data.DVault --version 0.8.0
                    
NuGet\Install-Package DCoding.Data.DVault -Version 0.8.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="DCoding.Data.DVault" Version="0.8.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="DCoding.Data.DVault" Version="0.8.0" />
                    
Directory.Packages.props
<PackageReference Include="DCoding.Data.DVault" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add DCoding.Data.DVault --version 0.8.0
                    
#r "nuget: DCoding.Data.DVault, 0.8.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package DCoding.Data.DVault@0.8.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=DCoding.Data.DVault&version=0.8.0
                    
Install as a Cake Addin
#tool nuget:?package=DCoding.Data.DVault&version=0.8.0
                    
Install as a Cake Tool

DVault

DVault is the repository for the DCoding.Data.DVault .NET library.

Installation

Install the provider-neutral DVault package from NuGet and add the provider package that matches the database used by the application. The coordinated DVault package family is version-aligned:

dotnet add package DCoding.Data.DVault --version 0.8.0
dotnet add package DCoding.Data.DVault.Sqlite --version 0.8.0
dotnet add package DCoding.Data.DVault.Postgres --version 0.8.0
dotnet add package DCoding.Data.DVault.MySql --version 0.8.0
dotnet add package DCoding.Data.DVault.Oracle --version 0.8.0
dotnet add package DCoding.Data.DVault.SqlServer --version 0.8.0

Applications still need their normal Entity Framework Core database provider package, such as Microsoft.EntityFrameworkCore.Sqlite for SQLite, Npgsql.EntityFrameworkCore.PostgreSQL for PostgreSQL, Microsoft.EntityFrameworkCore.SqlServer for SQL Server, Oracle.EntityFrameworkCore for Oracle, or Pomelo.EntityFrameworkCore.MySql / MySql.EntityFrameworkCore for MySQL.

Runnable SQLite and PostgreSQL quickstart projects are available under examples/; see examples/README.md for exact build and run commands.

Quickstart

Use this flow in a .NET 10 project that references DCoding.Data.DVault and has an Entity Framework Core provider configured. DVault supports three additive declaration paths:

  • Code-First declarations for app-local EF models that fit the fluent hub, hub-parent satellite, multi-active driving-key, and ordered hub-link surface.
  • Metadata-first declarations through a shared DataVaultMetadataModel or DataVaultMetadataRegistry when one public metadata object should drive schema projection, explicit saves, typed latest/as-of reads, diagnostics, examples, or provider setup.
  • Model-first governance for reviewed dvault.model.v1 JSON artifacts that should be imported, projected into EF metadata, exported canonically, and compared against generated metadata for drift evidence.

Choose one authoritative path for a model boundary and keep the others as compatible alternatives for different ownership needs. See Model-First Governance Workflow for the current dvault.model.v1 JSON artifact contract and DVault EF Design-Time Workflow for the v0.8.0 lifecycle guardrails around reviewed artifacts, EF metadata, migrations, and live schema drift.

Register DVault services

using DCoding.Data.DVault;
using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();
services.AddDVault();

using var serviceProvider = services.BuildServiceProvider(validateScopes: true);

Provider packages can register provider-specific startup extensions alongside the provider-neutral services:

services.AddDVaultSqlite();
services.AddDVaultPostgres();
services.AddDVaultSqlServer();
services.AddDVaultOracle();
services.AddDVaultMySql();

Declare the EF model with Code-First metadata

Declare hubs, satellites, and links in OnModelCreating with ApplyDataVaultMetadata(vault => ...). Business keys, driving keys, payload fields, and link participants use direct scalar member selectors. Composite keys use repeated calls in their canonical order.

using DCoding.Data.DVault;
using Microsoft.EntityFrameworkCore;

public sealed class SalesVaultContext(DbContextOptions<SalesVaultContext> options) : DbContext(options) {
  protected override void OnModelCreating(ModelBuilder modelBuilder) {
    modelBuilder.ApplyDataVaultMetadata(vault => {
      vault.Hub<Customer>(hub => {
        hub.BusinessKey(customer => customer.CustomerId);
        hub.Satellite("Profile", satellite => {
          satellite.Payload(customer => customer.CustomerName);
          satellite.Payload(customer => customer.CustomerStatus);
        });
        hub.Satellite("ContactByType", satellite => {
          satellite.DrivingKey(customer => customer.ContactType);
          satellite.Payload(customer => customer.EmailAddress);
        });
      });

      vault.Hub<Order>(hub => hub.BusinessKey(order => order.OrderId));

      vault.Link("CustomerOrder", link => {
        link.Participant<Customer>();
        link.Participant<Order>();
      });
    });
  }
}

public sealed class Customer {
  public string CustomerId { get; set; } = string.Empty;
  public string CustomerName { get; set; } = string.Empty;
  public string CustomerStatus { get; set; } = string.Empty;
  public string ContactType { get; set; } = string.Empty;
  public string EmailAddress { get; set; } = string.Empty;
}

public sealed class Order {
  public string OrderId { get; set; } = string.Empty;
}

Code-First metadata is additive. It does not ask callers to put DVault hash-key, load-timestamp, or record-source technical fields on domain entities, and it does not create a public Code-First-to-registry bridge. Callers that want the same public metadata object reused by schema, save, and read paths should use the registry-backed metadata path shown below.

Save explicitly

Persistence remains an explicit service boundary. DataVaultSaveRequest carries the load timestamp and record source, and callers choose when to write vault rows through IDataVaultSaveService. DVault does not intercept SaveChanges or hide persistence behind ordinary EF entity tracking.

The raw request example below uses explicit metadata objects. Applications that want to avoid repeating those metadata declarations in loaders can opt the DbContext into a DataVaultMetadataRegistry with UseDataVaultMetadata() and then use registry-backed requests or the typed mapper helpers such as SaveHubAsync(...), SaveLinkAsync(...), and SaveOrdinaryHubSatelliteAsync(...).

using DCoding.Data.DVault;
using DCoding.Data.DVault.Modeling;
using Microsoft.Extensions.DependencyInjection;

public static class SalesVaultWriter {
  public static async Task SaveCustomerOrderAsync(
      SalesVaultContext context,
      IServiceProvider serviceProvider,
      CancellationToken cancellationToken = default) {
    var customer = new DataVaultHubMetadata("Customer", ["CustomerId"]);
    var order = new DataVaultHubMetadata("Order", ["OrderId"]);
    var profile = new DataVaultSatelliteMetadata(
        "Profile",
        customer.ToReference(),
        ["CustomerName", "CustomerStatus"]);
    var customerOrder = new DataVaultLinkMetadata(
        "CustomerOrder",
        [customer.ToReference(), order.ToReference()]);

    var loadTimestamp = new DateTimeOffset(2026, 5, 11, 10, 15, 0, TimeSpan.Zero);
    var saveService = serviceProvider.GetRequiredService<IDataVaultSaveService>();

    var hubResult = await saveService.SaveAsync(
        context,
        new DataVaultSaveRequest(
            loadTimestamp,
            "crm-import",
            [
                new(customer, [new("CustomerId", "C-100")]),
                new(order, [new("OrderId", "O-200")]),
            ],
            []),
        cancellationToken);

    var customerHashKey = hubResult.SavedRecords.Single(record =>
        record.Kind == DataVaultTableKind.Hub && record.MetadataName == "Customer").HashKey;
    var orderHashKey = hubResult.SavedRecords.Single(record =>
        record.Kind == DataVaultTableKind.Hub && record.MetadataName == "Order").HashKey;

    await saveService.SaveAsync(
        context,
        new DataVaultSaveRequest(
            loadTimestamp,
            "crm-import",
            [],
            [
                new(customerOrder, [new("Customer", customerHashKey), new("Order", orderHashKey)]),
            ],
            [
                new(
                    profile,
                    customerHashKey,
                    [new("CustomerName", "Alice Adams"), new("CustomerStatus", "Active")],
                    "customer-profile-active"),
            ]),
        cancellationToken);
  }
}

For loaders that already have multiple source batches prepared, DataVaultBulkSaveRequest processes ordered save requests through the same service and keeps satellite HashDiff state in memory across the batch.

Read typed latest and as-of satellite projections

IDataVaultReadService provides provider-neutral latest and as-of satellite reads. The common path maps selected rows through a caller-owned projector delegate so application code can return typed read models without binding DTOs through reflection. Passing an asOf timestamp to DataVaultLatestSatelliteReadRequest selects the latest row visible at that point in time.

using DCoding.Data.DVault;
using DCoding.Data.DVault.Modeling;
using Microsoft.Extensions.DependencyInjection;

var readService = serviceProvider.GetRequiredService<IDataVaultReadService>();
var customer = new DataVaultHubMetadata("Customer", ["CustomerId"]);
var profile = new DataVaultSatelliteMetadata(
    "Profile",
    customer.ToReference(),
    ["CustomerName", "CustomerStatus"]);

var latestProfiles = await readService.ReadLatestSatelliteAsync(
    context,
    new DataVaultLatestSatelliteReadRequest(profile, [customerHashKey]),
    row => new CustomerProfileRead(
        row.RequiredString("ParentHashKey"),
        row.RequiredString("CustomerName"),
        row.RequiredString("CustomerStatus"),
        row.RequiredDateTimeOffset("LoadTimestamp")),
    cancellationToken);

var asOfProfiles = await readService.ReadLatestSatelliteAsync(
    context,
    new DataVaultLatestSatelliteReadRequest(profile, [customerHashKey], asOfTimestamp),
    row => new CustomerProfileRead(
        row.RequiredString("ParentHashKey"),
        row.RequiredString("CustomerName"),
        row.RequiredString("CustomerStatus"),
        row.RequiredDateTimeOffset("LoadTimestamp")),
    cancellationToken);

public sealed record CustomerProfileRead(
    string ParentHashKey,
    string CustomerName,
    string CustomerStatus,
    DateTimeOffset LoadTimestamp);

The lower-level ReadLatestSatelliteRowsAsync(...) API remains available as the advanced escape hatch. It returns DataVaultSatelliteReadRecord values containing the parent hash key, driving-key values, hash diff, load timestamp, record source, and payload values for callers that need row-level dictionaries or custom projections.

Read PIT and bridge projections

PIT-backed as-of reads and bridge reads are provider-neutral read-service helpers over already materialized tables. They do not refresh PIT rows, maintain bridge rows, infer graph closure, or add provider-specific PIT/bridge optimization. Use them when the current model already projects the PIT or bridge table and application loading has populated the rows.

PIT-backed reads target one DataVaultPitMetadata declaration, explicit parent hash keys, and an asOf timestamp. ReadPitRowsAsync(...) returns raw DataVaultPitReadRecord rows; ReadPitAsync(...) maps selected rows through a caller-owned projection delegate with exact-name access to ParentHashKey, LoadTimestamp, and declared satellite segments.

var pit = new DataVaultPitMetadata(customer.ToReference(), ["Profile", "Status"]);
var snapshots = await readService.ReadPitAsync(
    context,
    new DataVaultPitAsOfReadRequest(pit, [customerHashKey], asOfTimestamp),
    row => {
      var profile = row.RequiredSatellite("Profile");
      var status = row.OptionalSatellite("Status");

      return new CustomerSnapshotRead(
          row.RequiredString("ParentHashKey"),
          row.RequiredDateTimeOffset("LoadTimestamp"),
          profile.RequiredString("CustomerName"),
          profile.RequiredString("CustomerStatus"),
          status?.NullableString("StatusCode"));
    },
    cancellationToken);

Bridge reads target one DataVaultBridgeMetadata declaration and filter by endpoint hash keys. Many-to-many bridges support DataVaultBridgeTraversalEndpoint.From and DataVaultBridgeTraversalEndpoint.To. Hierarchy bridges support DataVaultBridgeTraversalEndpoint.Ancestor and DataVaultBridgeTraversalEndpoint.Descendant, require a bounded maximumDepth, and expose TraversalDepth on hierarchy rows.

var customerOrder = new DataVaultLinkMetadata(
    "CustomerOrder",
    [customer.ToReference(), order.ToReference()]);
var customerOrderBridge = DataVaultBridgeMetadata.ManyToMany(
    "CustomerOrder",
    customer.ToReference(),
    customerOrder.ToReference(),
    order.ToReference());

var orderHashKeys = await readService.ReadBridgeAsync(
    context,
    new DataVaultBridgeReadRequest(
        customerOrderBridge,
        DataVaultBridgeTraversalEndpoint.From,
        [customerHashKey]),
    row => row.RequiredString("OrderHashKey"),
    cancellationToken);

var regionHierarchy = DataVaultBridgeMetadata.Hierarchy(
    "SalesRegionHierarchy",
    DataVaultMetadataReference.Hub("SalesRegion"),
    DataVaultMetadataReference.Link("SalesRegionParentChild"),
    DataVaultMetadataReference.Hub("SalesRegion"),
    ancestorParticipantOrdinal: 0,
    descendantParticipantOrdinal: 1);

var descendantRegions = await readService.ReadBridgeAsync(
    context,
    new DataVaultBridgeReadRequest(
        regionHierarchy,
        DataVaultBridgeTraversalEndpoint.Ancestor,
        ["region-a"],
        maximumDepth: 2),
    row => new SalesRegionPathRead(
        row.RequiredString("DescendantSalesRegionHashKey"),
        row.RequiredInt32("TraversalDepth")),
    cancellationToken);

ReadBridgeRowsAsync(...) returns DataVaultBridgeReadRecord values with endpoint hash keys in generated column order. Each endpoint value carries the public endpoint role, endpoint name, generated column name, and hash key. Typed bridge projectors use exact generated column names such as OrderHashKey, AncestorSalesRegionHashKey, DescendantSalesRegionHashKey, and TraversalDepth.

Register metadata once and opt in a DbContext

The metadata-first DataVaultMetadataModel path remains supported and compatible for v0.5 users, shared metadata, examples, and advanced scenarios. Applications that want one authoritative metadata source can register a model or prebuilt registry during service setup and opt selected contexts into registry-backed projection through DbContextOptionsBuilder.

using DCoding.Data.DVault;
using DCoding.Data.DVault.Modeling;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

var customer = new DataVaultHubMetadata("Customer", ["Customer Id"]);
var customerProfile = new DataVaultSatelliteMetadata(
    "CustomerProfile",
    customer.ToReference(),
    ["Profile Name", "Customer Status"]);
var salesVaultMetadata = new DataVaultMetadataModel(
    [customer],
    [],
    [customerProfile]);

var services = new ServiceCollection();
services.AddDVault(options => options.UseMetadataModel(salesVaultMetadata));
services.AddDbContext<SalesVaultContext>(options => {
  options.UseSqlite(connectionString);
  options.UseDataVaultMetadata();
});

public sealed class SalesVaultContext(DbContextOptions<SalesVaultContext> options) : DbContext(options) {
}

UseDataVaultMetadata() consumes the app-level registry registered by AddDVault(...). A context can override that default by passing an explicit DataVaultMetadataModel or DataVaultMetadataRegistry to UseDataVaultMetadata(...). If the same EF model receives two different DVault metadata sources, model building fails with a DVault source-conflict diagnostic instead of merging or duplicating projection. The SQLite and PostgreSQL quickstarts intentionally use this registry-backed path; see examples/README.md for exact commands and provider setup.

Model-first governed artifacts

Use model-first governance when a source-controlled dvault.model.v1 JSON artifact should be the reviewed authority for a Data Vault model. This path is separate from app-local Code-First declarations and from registry-backed metadata-first setup: JSON artifacts are imported with DataVaultModelArtifactImporter.ImportJson, projected through UseDataVaultMetadata(DataVaultModelImportResult), exported from fluent Code-First declaration callbacks or already-materialized metadata with DataVaultModelArtifactExporter.ExportJson, and compared against generated EF metadata with DataVaultModelDriftReporter.Compare.

using DCoding.Data.DVault;

var json = await File.ReadAllTextAsync("models/sales-vault.json", cancellationToken);
var importResult = DataVaultModelArtifactImporter.ImportJson(json, "models/sales-vault.json");

if (!importResult.IsValid) {
  throw new InvalidOperationException(
      string.Join(Environment.NewLine, importResult.Diagnostics.Select(diagnostic => diagnostic.Message)));
}

services.AddDVault(options => options.UseMetadataModel(importResult));
services.AddDbContext<SalesVaultContext>(options => {
  options.UseSqlite(connectionString);
  options.UseDataVaultMetadata(importResult);
});

if (importResult.MetadataRegistry is not null) {
  var canonicalJson = DataVaultModelArtifactExporter.ExportJson(importResult.MetadataRegistry);
}

var codeFirstJson = DataVaultModelArtifactExporter.ExportJson(vault => {
  vault.Hub<Customer>(hub => hub.BusinessKey(customer => customer.CustomerId));
});

using var context = new SalesVaultContext(options);
var driftReport = DataVaultModelDriftReporter.Compare(importResult, context);

Treat the JSON artifact and any drift report as review evidence. dvault.model.v1 artifacts use exact schemaVersion handling, canonical JSON declaration ordering, the default naming.policy, strict unknown-field rejection, loadTimestampStorage tokens provider-default, iso-8601-utc-text, and utc-ticks, and stable hubs, links, satellites, pits, and bridges declaration categories. See Model-First Governance Workflow for the full workflow, versioning rules, YAML boundary, and current limitations.

Diagnostics and explain output

IDataVaultDiagnosticsService can analyze metadata models, registries, Code-First declarations, and configured DbContexts. Validation and explain output can run without a save request. Provider-specific save-strategy dispatch diagnostics are request-bound, so strategy status remains not evaluated until a single save request or ordered bulk save request is supplied.

Multi-active satellite opt-in

Ordinary satellites remain the default. A satellite becomes multi-active only when it declares one or more driving keys, and those names define the canonical identity tuple for each active satellite row. Driving-key values stay separate from payload values, and hashDiff continues to represent payload state for change detection rather than driving-key identity.

modelBuilder.ApplyDataVaultMetadata(vault => {
  vault.Hub<Customer>(hub => {
    hub.BusinessKey(customer => customer.CustomerId);
    hub.Satellite("ContactByType", satellite => {
      satellite.DrivingKey(customer => customer.ContactType);
      satellite.DrivingKey(customer => customer.RegionCode);
      satellite.Payload(customer => customer.EmailAddress);
    });
  });
});

The save operation supplies driving-key values by logical name. Caller enumeration order does not matter; the values below are matched by name and persisted in the declared canonical order ContactType, then RegionCode:

var contact = new DataVaultSatelliteMetadata(
    "ContactByType",
    customer.ToReference(),
    ["EmailAddress"],
    ["ContactType", "RegionCode"]);

await saveService.SaveAsync(
    context,
    new DataVaultSaveRequest(
        loadTimestamp,
        "crm-import",
        [],
        [],
        [
            new(
                contact,
                customerHashKey,
                [new("RegionCode", "DE"), new("ContactType", "billing")],
                [new("EmailAddress", "billing-de@example.test")],
                "contact-payload-hash"),
        ]),
    cancellationToken);

The provider-neutral projection stores driving-key columns immediately after the parent hash-key column and before HashDiff, LoadTimestamp, RecordSource, and payload columns. The satellite primary key and parent/latest-state index expand to include the driving-key tuple, so different driving-key tuples for the same parent can coexist. Current multi-active support is intentionally narrow and opt-in; future work includes PIT over multi-active satellites, bridge interactions involving multi-active state, link-based PIT support, and provider-specific optimized multi-active save behavior. Provider-specific save strategies may decline multi-active batches so the provider-neutral writer handles the documented baseline.

Provider Packages

DCoding.Data.DVault contains the provider-neutral API, metadata model, naming conventions, stable hashing, read helpers, diagnostics, and EF fallback writer. Provider packages extend that base registration without changing the explicit save or read APIs.

DCoding.Data.DVault.Sqlite registers the optimized SQLite set-based save strategy. DCoding.Data.DVault.Postgres registers an optimized Npgsql/PostgreSQL strategy for clean contexts that use set-based INSERT ... ON CONFLICT DO NOTHING hub and link writes plus latest-state satellite checks. DCoding.Data.DVault.SqlServer registers an optimized SQL Server strategy for clean contexts with set-based unique-row inserts and latest-state satellite checks. DCoding.Data.DVault.Oracle registers an Oracle-gated insert-only strategy for clean Oracle.EntityFrameworkCore hub/link batches and declines unsupported shapes, including dirty tracked contexts and request batches that contain satellite operations, so the provider-neutral fallback writer handles them. DCoding.Data.DVault.MySql supports Pomelo.EntityFrameworkCore.MySql and MySql.EntityFrameworkCore, and registers an optimized MySQL strategy for clean supported MySQL contexts.

Provider-specific save-strategy registration and provider capability-profile selection are separate surfaces. The core package includes built-in capability profiles for the known SQLite, PostgreSQL, SQL Server, Oracle, Pomelo MySQL, and official MySQL EF provider names. Direct ApplyDataVaultMetadata(...) calls can pass an explicit DataVaultProviderCapabilityProfile when an application wants deterministic provider-specific schema projection at model-building time. Registry-backed UseDataVaultMetadata(...) remains the easiest path when one metadata source should drive schema, save, and read usage.

Migration from v0.5

v0.5 metadata-first DataVaultMetadataModel usage remains valid in v0.6.0. Existing applications can keep constructing metadata models, registering them with AddDVault(options => options.UseMetadataModel(...)), and opting DbContexts into UseDataVaultMetadata().

New application code can prefer Code-First declarations when the Data Vault model fits the implemented hub-parent satellite and ordered hub-link surface. Keep metadata-first declarations for shared metadata registries, example-local quickstarts, link-parent satellite shapes, bridge/PIT metadata baselines, or naming requirements outside the bounded Code-First API.

Query generated tables

using Microsoft.EntityFrameworkCore;

public static class SalesVaultReader {
  public static async Task<IReadOnlyList<Dictionary<string, object>>> ReadCustomerOrdersAsync(
      SalesVaultContext context,
      CancellationToken cancellationToken = default) {
    return await context
        .Set<Dictionary<string, object>>("LinkCustomerOrder")
        .AsNoTracking()
        .ToListAsync(cancellationToken);
  }
}

The shared-type table names and columns in this quickstart follow DVault's default naming conventions, for example HubCustomer, HubOrder, LinkCustomerOrder, CustomerHashKey, OrderHashKey, LoadTimestamp, and RecordSource. Direct EF queries remain available for table-specific projections, custom joins, diagnostics, and cases outside the typed read helper baseline.

Live schema drift checks

DVault has a bounded live-schema drift path for comparing expected Data Vault metadata with the physical schema in a reachable database. The live snapshot surface is intentionally limited to DVault-owned tables, ordered columns, named primary-key constraints, and secondary indexes. It does not compare foreign keys, arbitrary non-DVault database objects, destructive migration plans, or repair operations.

SQLite is the supported v1 live-schema reader. It is available through DataVaultLiveSchemaReader.ReadAsync(context) and returns a classified DataVaultLiveSchemaReadResult rather than silently skipping unsupported or unavailable environments. Compare the result with expected metadata using DataVaultLiveSchemaDriftReporter.Compare:

using DCoding.Data.DVault;

using var context = new SalesVaultContext(options);
var liveSchema = await DataVaultLiveSchemaReader.ReadAsync(context);
var report = DataVaultLiveSchemaDriftReporter.Compare(metadataModel, liveSchema);

if (report.HasBlockingDifferences) {
  throw new InvalidOperationException(report.ToDisplayString());
}

Providers without a built-in live-schema reader return DataVaultLiveSchemaReadStatus.UnsupportedProvider. A supported provider whose database cannot be reached returns DataVaultLiveSchemaReadStatus.Unavailable. Both outcomes become stable blocking drift differences when passed to DataVaultLiveSchemaDriftReporter.Compare.

PostgreSQL, SQL Server, Oracle, and MySQL live-schema readers are not first-class supported readers in this slice. Existing external provider integration evidence remains opt-in behind the documented connection-string environment variables: DVAULT_TEST_POSTGRES_CONNECTION_STRING, DVAULT_TEST_SQLSERVER_CONNECTION_STRING, DVAULT_TEST_ORACLE_CONNECTION_STRING, and DVAULT_TEST_MYSQL_CONNECTION_STRING. Default local test execution does not require those external databases.

v0.8.0 Release Notes

The v0.8.0 release adds EF Core lifecycle guardrails around design-time metadata, migration preflight checks, model drift reports, and live schema drift evidence. See docs/releases/v0.8.0.md for the release-note record, package scope, compatibility notes, design-time workflow boundaries, and package verification posture.

Notable user-facing changes:

  • DataVaultModelDriftReporter can compare expected metadata with generated EF metadata and reviewed dvault.model.v1 artifacts before schema changes are applied.
  • DataVaultLiveSchemaReader and DataVaultLiveSchemaDriftReporter provide the bounded SQLite live-schema comparison lane.
  • Migration guardrail diagnostics DVM2001-DVM2006 classify lifecycle risks such as missing primary keys, missing load timestamps, missing record sources, satellite HashDiff drift, missing parent hash keys, and missing secondary indexes.
  • The design-time workflow documentation defines the consumer-owned registration pattern for IDesignTimeDbContextFactory, metadata registries, drift checks, and migration preflight.
  • Plan documentation now uses topic-first filenames while preserving ticket traceability inside the plan files.

Current v0.8.0 Limitations

Lifecycle guardrails remain explicit library APIs. DVault does not ship a first-party dotnet ef command shim, does not intercept EF migration commands, does not automatically execute migrations, and does not apply schema repairs. Startup-project and target-project splits for design-time discovery remain outside the v0.8.0 boundary. Live-schema drift reading is SQLite-first; PostgreSQL, SQL Server, Oracle, and MySQL still rely on external opt-in integration evidence rather than first-class live-schema readers.

Model-first APIs continue to operate on JSON artifacts, fluent Code-First declaration callbacks, and already-materialized metadata through DataVaultModelArtifactImporter.ImportJson, DataVaultModelArtifactExporter.ExportJson, UseDataVaultMetadata(DataVaultModelImportResult), and DataVaultModelDriftReporter.Compare. PIT-backed reads and bridge reads do not maintain PIT rows, maintain bridge rows, infer graph closure, or provide provider-specific PIT/bridge read optimization.

Layout

  • DVault.slnx: Canonical root solution file for build and test automation.
  • src/DCoding.Data/: Non-packable build anchor for the DCoding.Data source-root namespace family.
  • src/DCoding.Data.DVault/: Main library project. The NuGet package id and root namespace are DCoding.Data.DVault.
  • src/DCoding.Data.DVault.*: Provider extension packages for SQLite, PostgreSQL, SQL Server, Oracle, and MySQL.
  • tests/DCoding.Data.DVault.Tests/: Unit, integration, and shared test projects for DVault.
  • examples/: Runnable SQLite and PostgreSQL quickstart projects.
  • benchmarks/: Local performance benchmark projects.
  • docs/: Documentation and design notes.

All current .NET projects are included in DVault.slnx. Empty future-use folders contain .gitkeep files so the layout is present in clean checkouts.

Local Validation

dotnet build DVault.slnx --nologo
dotnet test DVault.slnx --nologo
dotnet pack DVault.slnx --configuration Release --nologo
bash tools/verify-packages.sh
bash tools/check-format.sh

The normal test run includes package-specific public API snapshot checks for DCoding.Data.DVault and the five provider packages. See docs/quality/api-surface-snapshots.md for the approved baseline location and the explicit update workflow for intentional API changes.

bash tools/verify-packages.sh inspects the artifacts created under artifacts/packages/ by the solution-level pack command. It expects exactly the six DVault library packages and matching symbol packages, checks README and XML documentation entries, validates declared NuGet metadata, and confirms each provider package depends on the packed DCoding.Data.DVault version. The verifier intentionally fails when stale, unexpected, or non-packable package artifacts remain in artifacts/packages/.

Provider integration tests use stable xUnit trait categories so required local coverage and opt-in external database coverage can be selected explicitly:

  • Category=ProviderIntegration.RequiredLocal: required SQLite-backed integration coverage that does not need external services.
  • Category=ProviderSmoke.Default: provider package registration and configuration-contract smoke coverage that runs in the default local path.
  • Category=ProviderIntegration.ExternalOptIn: live external database integration coverage, currently Postgres, SQL Server, Oracle, and MySQL.

To make the default local provider boundary explicit in a focused run, exclude opt-in external database tests:

dotnet test DVault.slnx --nologo --filter "Category!=ProviderIntegration.ExternalOptIn"

Benchmarks

Run the local SQLite scenario comparison benchmarks from the repository root:

dotnet run --project benchmarks/DCoding.Data.DVault.Benchmarks/DCoding.Data.DVault.Benchmarks.csproj --configuration Release -- --iterations 1 --warmup 0

The benchmark executable compares conventional EF and DVault flows for the shared customer profile history contract, a larger customer profile bulk-history contract, and the reduced order-product fulfillment history contract. It uses SQLite temporary files by default and does not require Postgres, SQL Server, Oracle, MySQL, Docker, DVAULT_TEST_POSTGRES_CONNECTION_STRING, DVAULT_TEST_SQLSERVER_CONNECTION_STRING, DVAULT_TEST_ORACLE_CONNECTION_STRING, or DVAULT_TEST_MYSQL_CONNECTION_STRING. Increase --iterations and --warmup locally when collecting steadier timing numbers.

Optional Local Postgres Integration Tests

Postgres integration tests are opt-in and are skipped by default. Normal dotnet test execution does not require Postgres, Docker, or checked-in machine-specific configuration.

To run the Postgres-backed integration tests, provide a developer-managed PostgreSQL database connection string in DVAULT_TEST_POSTGRES_CONNECTION_STRING:

DVAULT_TEST_POSTGRES_CONNECTION_STRING='Host=localhost;Port=5432;Database=dvault_tests;Username=dvault;Password=local-secret' dotnet test DVault.slnx --nologo -p:DVAULT_TEST_POSTGRES_CONNECTION_STRING=Configured

To select only the live Postgres integration category, use the same configured connection string with the provider category filter:

DVAULT_TEST_POSTGRES_CONNECTION_STRING='Host=localhost;Port=5432;Database=dvault_tests;Username=dvault;Password=local-secret' dotnet test DVault.slnx --nologo --filter "Category=ProviderIntegration.ExternalOptIn&Provider=Postgres" -p:DVAULT_TEST_POSTGRES_CONNECTION_STRING=Configured

DVault does not provision Docker containers or databases for these tests. The configured database must already exist, and the configured user must be allowed to create and drop temporary schemas. Keep credentials in local environment variables or another untracked secret store, not in repository files.

Optional Local SQL Server Integration Tests

SQL Server integration tests are opt-in and are skipped by default. Normal dotnet test execution does not require SQL Server, Docker, or checked-in machine-specific configuration.

To run the SQL Server smoke lane, provide a developer-managed SQL Server database connection string in DVAULT_TEST_SQLSERVER_CONNECTION_STRING and run the representative repo-root command:

DVAULT_TEST_SQLSERVER_CONNECTION_STRING='Server=localhost;Database=dvault_tests;User Id=dvault;Password=local-secret;TrustServerCertificate=True' dotnet test DVault.slnx --filter FullyQualifiedName~SqlServer -p:DVAULT_TEST_SQLSERVER_CONNECTION_STRING=Configured

The configured SQL Server principal must be able to create and drop temporary dvault_test_* schemas and tables in the target database. The tests create isolated schemas, validate one hub save, one link save, and one satellite save through the optimized SQL Server provider strategy, then drop the generated schema. Missing DVAULT_TEST_SQLSERVER_CONNECTION_STRING produces a deterministic skip message instead of loading the conditional SQL Server provider package.

DVault does not provision Docker containers or databases for these tests. The configured database must already exist, and the configured user must be allowed to create and drop temporary schemas. Keep credentials in local environment variables or another untracked secret store, not in repository files.

Optional Local Oracle Integration Tests

Oracle integration tests are opt-in and are skipped by default. Normal dotnet test execution does not require Oracle, Docker, or checked-in machine-specific configuration.

To run the Oracle-backed smoke test, provide a developer-managed Oracle database connection string in DVAULT_TEST_ORACLE_CONNECTION_STRING:

DVAULT_TEST_ORACLE_CONNECTION_STRING='User Id=dvault;Password=local-secret;Data Source=localhost:1521/FREEPDB1' dotnet test DVault.slnx --nologo -p:DVAULT_TEST_ORACLE_CONNECTION_STRING=Configured

To select only the live Oracle integration category, use the same configured connection string with the provider category filter:

DVAULT_TEST_ORACLE_CONNECTION_STRING='User Id=dvault;Password=local-secret;Data Source=localhost:1521/FREEPDB1' dotnet test DVault.slnx --nologo --filter "Category=ProviderIntegration.ExternalOptIn&Provider=Oracle" -p:DVAULT_TEST_ORACLE_CONNECTION_STRING=Configured

DVault does not provision Docker containers, Oracle databases, or Oracle users for these tests. The configured database and user must already exist, and the configured user must be allowed to create and drop temporary tables. Keep credentials in local environment variables or another untracked secret store, not in repository files.

Optional Local MySQL Integration Tests

MySQL integration tests are opt-in and are skipped by default. Normal dotnet test execution does not require MySQL, Docker, or checked-in machine-specific configuration.

To run the MySQL-backed integration test, provide a developer-managed MySQL database connection string in DVAULT_TEST_MYSQL_CONNECTION_STRING:

DVAULT_TEST_MYSQL_CONNECTION_STRING='Server=localhost;Port=3306;Database=dvault_tests;User=dvault;Password=local-secret;AllowPublicKeyRetrieval=True;SslMode=Disabled' dotnet test DVault.slnx --nologo -p:DVAULT_TEST_MYSQL_CONNECTION_STRING=Configured

To select only the live MySQL integration category, use the same configured connection string with the provider category filter:

DVAULT_TEST_MYSQL_CONNECTION_STRING='Server=localhost;Port=3306;Database=dvault_tests;User=dvault;Password=local-secret;AllowPublicKeyRetrieval=True;SslMode=Disabled' dotnet test DVault.slnx --nologo --filter "Category=ProviderIntegration.ExternalOptIn&Provider=MySQL" -p:DVAULT_TEST_MYSQL_CONNECTION_STRING=Configured

The integration project conditionally restores MySql.EntityFrameworkCore only when the MySQL opt-in property is non-empty. When running the live MySQL path, keep the environment variable set for test execution and pass the non-secret MSBuild marker property shown above so the conditional provider package is available during restore and build. DVault does not provision Docker containers or databases for these tests. The configured database must already exist, and the configured user must be allowed to create and drop the smoke-test table. Keep credentials in local environment variables or another untracked secret store, not in repository files.

License

DVault uses the Apache License 2.0. See LICENSE.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (5)

Showing the top 5 NuGet packages that depend on DCoding.Data.DVault:

Package Downloads
DCoding.Data.DVault.Oracle

Oracle provider extensions and optimized write strategies for DCoding.Data.DVault.

DCoding.Data.DVault.MySql

MySQL provider extensions and optimized write strategies for DCoding.Data.DVault.

DCoding.Data.DVault.Postgres

PostgreSQL provider extensions and optimized write strategies for DCoding.Data.DVault.

DCoding.Data.DVault.Sqlite

SQLite provider extensions and optimized write strategies for DCoding.Data.DVault.

DCoding.Data.DVault.SqlServer

SQL Server provider extensions and optimized write strategies for DCoding.Data.DVault.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.15.0 0 5/19/2026
0.14.0 59 5/18/2026
0.13.0 64 5/18/2026
0.12.0 64 5/17/2026
0.11.0 76 5/15/2026
0.10.0 82 5/15/2026
0.9.0 94 5/14/2026
0.8.0 99 5/13/2026
0.7.0 101 5/13/2026
0.6.0 129 5/11/2026
0.5.0 134 5/9/2026
0.4.1 95 5/3/2026