Documentation
Provider assembly boundaries
Compatibility and package migration plan
On this page
DubUrl publishes deliberately acyclic package layers:
- The
DubUrlNuGet package contains provider-neutral contracts and automatic discovery. It discovers provider assemblies in application runtime dependencies as well as installed provider archives. DubUrl.Provider.*packages own database-family and connection-technology implementations underProviders/. They depend on Core without concrete ADO.NET driver dependencies.- Provider archives combine family implementations with a driver, using an implementation DLL and a provider-specific
.deps.json. Selections sharing implementations, driver and runtime compatibility use one archive and retain database-specific installation IDs as aliases. Packaging collects dependencies throughProviderBinding.Build.csproj. DubUrl.Applicationembeds the generated catalog and exposes it throughBundledProviderCatalog.ConnectorServiceuses this catalog by default; hosts may supply a replacement. It describes available providers without referencing or loading their implementation DLLs. The all-provider aggregate is removed. TheDubUrl.CoreNuGet package is no longer produced; provider packages depend onDubUrl. The physicalDubUrl.Coreproject andDubUrl.Core.dllassembly keep their names, as do existing public namespaces.
For selective NuGet use, install DubUrl, DubUrl.Provider.PostgreSql, and Npgsql, then write:
var url = new ConnectionUrl("pg://localhost/demo");
No archive, registry parameter, or explicit factory registration is required. Core discovers assemblies named DubUrl.Provider.* from application runtime dependencies and the default load context. Explicit factory registrations are honored; installed archives take precedence for overlapping aliases.
Project folders match their assembly names; the provider-neutral Core project publishes as DubUrl:
| Project | Responsibility |
|---|---|
DubUrl.Core/DubUrl.Core.csproj | Publishes DubUrl, containing DubUrl.Core.dll; provider-neutral contracts and infrastructure |
DubUrl.Application/BundledProviderCatalog.cs | Default catalog shared by every application host |
connectors/ConnectorCatalog.targets | Runs the shared catalog exporter, combining packaging selectors with implementation attributes |
Providers/Build/ProviderBinding.Build.csproj | Temporary, non-packable dependency collector; only real implementation DLLs and rewritten .deps.json files are packaged |
Core owns its source files through normal SDK inclusion. The former aggregate project is removed.
Normal builds generate and embed catalog definitions without optional download fields; they do not require connector archives, previously generated artifacts, PowerShell, or network access beyond package restore. Pack builds add the download URL and SHA-256 after creating each archive. The Application catalog lists all catalog schemes, including optional OLE DB connectors, without registering or loading those drivers. The matrix contains packaging selectors and distribution metadata; it does not repeat mapper/dialect/parametrizer types, URL schemes, file-based markers, or factory type names. Provider attributes are read once per discovery pass and retained as registrations, including constructors and dialect instances. The driver assembly supplies its factory through Core’s shared factory-discovery code. URL compilation does not rescan provider attributes. Application factory lookup honors DbProviderFactories, then resolves the driver from runtime dependencies; installed factories remain isolated. ODBC options and lazy optional BulkCopy adapters use the same registration logic in both paths. Installed definitions name a real provider implementation as the dependency anchor, plus implementation assembly paths and the driver selector; Core discovers attributes directly, without a generated class or entry-point type. Rebuild old connector archives and catalogs to use this definition shape.
Provider-neutral contracts belong in DubUrl.Core; database-specific declarations, mappers, rewriters, dialects, SQL mappings, and driver discovery sources belong in their owning provider projects. Their sources and resources are physically colocated there, rather than linked from the Core source tree. Adding a database must not require a change to DubUrl.Core. See provider modules for the authoring example and explicit registration path.
Public-type migration
Provider implementation namespaces now begin with DubUrl.Provider.<Family> and follow their folders. For example, import DubUrl.Provider.PostgreSql.Sql for PgsqlDialect and DubUrl.Provider.PostgreSql.Mapping for PostgresqlMapper. Core contracts retain their existing namespaces. The Core NuGet package is now DubUrl; remove any DubUrl.Core package reference. There is no compatibility shim or all-provider aggregate. Existing DubUrl users must add their chosen provider packages explicitly. DubUrl.Core.dll, its project path, and public namespaces remain unchanged. The former DubUrl.dll and BuiltInProviderModule/BuiltInProviderCatalog APIs are removed; use Core discovery and DubUrl.Application.BundledProviderCatalog instead. Connector IDs select database/driver combinations; there are no per-driver binding DLLs or Npgsql binding NuGet packages.
This is a public namespace migration: applications must update imports and recompile, and existing connector binaries and reflection-based type names must be rebuilt or updated. Assembly forwarding cannot preserve a renamed namespace. No legacy namespace wrappers are introduced.
An additional DubUrl.Abstractions package is intentionally deferred. It becomes worthwhile only when at least two independently versioned hosts need the same contracts without catalog/parsing implementation, or when DubUrl.Core gains a dependency that provider modules should not inherit. Until either condition is met, another assembly would add versioning surface without creating an actual dependency boundary.
Shared schema generation and provider SQL
Schema support uses the same provider packages as querying. Connectors select and distribute a provider; connectors using the same SQL dialect reuse its schema behavior.
| Location | Responsibility |
|---|---|
DubUrl.Schema | Models, builders, constraints, dependency planning, expression traversal, templates, native-type validation and fallback, script assembly |
DubUrl.Core/Schema (assembly DubUrl.Core) | ISchemaSqlRenderer contract and reusable StandardSchemaSqlRenderer implementation |
Providers/<Family>/DubUrl.Provider.*/Schema/Generation | DDL capabilities, native-type sets, and engine-specific SQL renderers |
Providers/<Family>/DubUrl.Provider.*/Discovery | Catalog queries and engine-specific schema discovery implementations |
connectors/ | Provider/driver composition and distribution; no copied schema templates |
IDialect.SchemaSqlRenderer defaults to StandardSchemaSqlRenderer.Instance. BaseDialect exposes a virtual property, and each supported built-in provider explicitly supplies its own renderer. Core has no engine-specific branches: it owns contracts, identifier-quoting helpers, standard generated-column clause formatting, generic unsupported-operation errors, and optional delegation to an explicitly supplied renderer. Provider packages own identity, generated-column and regex syntax, object-name qualification, index naming, and foreign-key placement, including SQLite’s inline foreign keys and DuckDB’s sequence-backed identities.
DialectCapabilities describes support through ObjectNameParts flags and support booleans. Copying capabilities never selects SQL behavior. Comments, views, native types, and logical type/function mappings retain their provider-owned declarations.
DubUrl.Schema renders expressions and literals using the dialect’s existing formatters, then delegates SQL variation points to ISchemaSqlRenderer. The contract accepts primitive values and already-rendered SQL, so neither Core nor provider projects need to reference schema models or templates.
To add engine behavior, subclass the standard renderer in the provider project and override the required operations:
using DubUrl.Querying.Dialects;
namespace DubUrl.Provider.Example.Schema.Generation;
internal sealed class ProviderSchemaSqlRenderer : StandardSchemaSqlRenderer
{
public static new ProviderSchemaSqlRenderer Instance { get; } = new();
public override string RenderRegex(IDialect dialect, string expression, string pattern)
=> $"provider_regexp({expression}, {pattern})";
}
Expose it from the provider’s BaseDialect subclass:
public override ISchemaSqlRenderer SchemaSqlRenderer
=> ProviderSchemaSqlRenderer.Instance;
The example’s provider_regexp stands for the provider’s actual SQL function. expression and pattern are already rendered SQL: do not quote them again. Unoverridden operations retain provider-neutral formatting, qualification checks, and unsupported-operation errors. To extend an existing provider renderer, pass that renderer explicitly to the StandardSchemaSqlRenderer(ISchemaSqlRenderer fallback) constructor. An implementation can also implement ISchemaSqlRenderer directly when none of the standard syntax applies. Instances should be stateless and safe to share between renders.
Adding a database-specific override requires no shared template changes, engine selector enums, or additional connector/schema packages. Keep reusable helpers in Core and engine decisions in providers.
Custom provider migration: replace ObjectNameQualification, IdentityColumnSyntax, GeneratedColumnSyntax and RegularExpressionSyntax selections with ObjectNameParts and the SupportsIdentityColumns, SupportsStoredGeneratedColumns, SupportsVirtualGeneratedColumns and SupportsRegularExpressionChecks properties. These describe capabilities only. Supply IDialect.SchemaSqlRenderer explicitly to implement the corresponding SQL; capability-only custom dialects now receive Core’s generic unsupported-operation errors for identity, generation and regex.
Bulk copy registration and optional typed adapters
Bulk copy belongs to a provider/connector registration. Native DuckDB and SqlClient registrations opt in; ODBC registrations remain unsupported even when they share the exact same SQL dialect. SchemaSqlRenderer stays on IDialect; there is no BulkCopy hook on the dialect.
| Location | Responsibility |
|---|---|
DubUrl.Core/BulkCopy (assembly DubUrl.Core) | Contracts, deferred adapter activation, scoped optional dependency resolution |
DubUrl.BulkCopy | Public extension and factory facade; references Core only |
Providers/<Family>/DubUrl.Provider.* | Driver-independent database behavior and optional adapter declarations |
Providers/DuckDb/DubUrl.Provider.DuckDb.Native.BulkCopy | Typed DuckDB appender engine |
Providers/SqlServer/DubUrl.Provider.SqlServer.SqlClient.BulkCopy | Typed Microsoft.Data.SqlClient bulk engine |
| Native connector modules | Driver deployment, conditional adapter dependency, provider registration |
The typed adapters reference their driver packages at compile time. Core, the facade, and provider families do not. Native DuckDB and SqlClient connector artifacts include only their corresponding adapter and driver dependency closure; ODBC artifacts do not acquire these native adapters.
Register and activate an adapter
ProviderRegistration.BulkCopyProvider defaults to null and is copied into ResolvedConnection. ConnectionUrl.BulkCopyProvider exposes the selection to the shared factory. Explicit registrations can opt out by leaving the property null, including when reusing a native mapper.
Provider families can declare optional support without linking an adapter assembly:
[BulkCopyProvider("DubUrl.Provider.DuckDb.Native.BulkCopy",
"DubUrl.Provider.DuckDb.Native.BulkCopy.DuckDbBulkCopyProvider")]
The non-inherited mapper attribute creates a LazyBulkCopyProvider for attribute-based and generated connector registration. Built-in registrations use the same deferred provider explicitly. Custom modules can set BulkCopyProvider = new LazyBulkCopyProvider(assemblyName, typeName) when constructing their registration, or supply a preconstructed IBulkCopyProvider when linking an adapter intentionally.
Registration and engine creation do not load the optional adapter. On Write, the deferred engine resolves its configured assembly/type in the actual driver’s AssemblyLoadContext, activates the IBulkCopyProvider, and delegates the operation. Reflection is confined to this activation boundary. DuckDB row/value calls, null handling, date/time conversion, and SqlClient bulk operations use typed APIs without method discovery or MethodInfo.Invoke in the data-transfer loop.
Deploy the adapter built against the connector’s driver version with that connector. A host using only Core and the facade can load the optional DLLs at runtime without referencing them at compile time. Older mapper-factory-only registries do not implicitly acquire bulk support: use explicit registrations. The public IBulkCopyEngine namespace remains DubUrl.BulkCopy, with its former facade assembly identity forwarded to Core. The one-argument ConnectionUrl constructor discovers installed connectors directly from the DubUrl package. Application provider dependencies supply fallback registrations; hosts can also supply an explicit registry to bypass discovery.
Optional DLL deployment
Use the normal call when the connector’s dependency loader can already find its DLLs:
using DubUrl.BulkCopy;
connection.BulkCopy("Customer", reader);
For separately deployed adapter/helper assemblies, supply search directories:
connection.BulkCopy("Customer", reader, new BulkCopyDependencyResolver(new[]
{
Path.Combine(AppContext.BaseDirectory, "optional-bulk-dependencies")
}));
The resolver uses the driver’s normal load context first, then probes the driver directory and supplied directories. Handlers exist only during the operation and are removed after success or failure. Overlapping scopes cooperate. Missing assemblies produce a diagnostic naming the DLL and explaining how to supply it. Native runtime assets retain the connector/driver’s existing native load-context handling; the resolver neither downloads packages nor changes machine-wide configuration.
Custom typed engines should wrap native operations in dependencies.Execute(driverAssembly, action) to retain scoped helper resolution and missing-dependency diagnostics. Use ResolveType only for an optional adapter/helper activation boundary. The shared facade disposes engines and leaves source readers owned by callers. Typed DuckDB appenders and SqlClient bulk objects are disposed on success and failure. SqlClient retains batch size 1000, streaming enabled, and timeout 0. This does not add an ODBC or PostgreSQL bulk engine.