This is an automated email from the ASF dual-hosted git repository.

CurtHagenlocher pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-adbc.git


The following commit(s) were added to refs/heads/main by this push:
     new e6818b37a fix(csharp): support spec-correct driver manifests (#4341)
e6818b37a is described below

commit e6818b37a0361db7ba87b1fd9dc4f5f282393745
Author: Curt Hagenlocher <[email protected]>
AuthorDate: Thu May 21 05:10:34 2026 -0700

    fix(csharp): support spec-correct driver manifests (#4341)
    
    Closes #4329
    
    The driver manager was parsing every TOML file as a connection profile,
    so real driver manifests (with `manifest_version = 1` and a string
    `version`) were rejected with "The 'profile_version' field has an
    invalid value '1.5.2'. It must be an integer." Add a proper
    DriverManifest parser per docs/source/format/driver_manifests.rst, with
    [Driver.shared] as either a single string or a platform-tuple table.
    
    Managed (.NET) driver selection moves from the C#-specific `driver_type`
    field on connection profiles to a scheme-prefixed entrypoint on the
    manifest: `dotnet:Type` for modern .NET, `netfx:Type` for .NET
    Framework. The host rejects a manifest whose scheme doesn't match its
    runtime, so mismatches fail with a clear error instead of an
    assembly-loader mystery. Profile-driven managed loading uses the same
    scheme via an `entrypoint` option in `[Options]`, which the driver
    manager consumes and does not forward to the driver.
    
    Also aligns env_var placeholder support with the spec syntax `{{
    env_var(NAME) }}` per docs/source/format/connection_profiles.rst:
    placeholders may be embedded anywhere in a value, repeated, missing vars
    expand to "" (matching the C/C++ driver manager), and unknown functions
    are rejected.
    
    End-to-end coverage against DuckDB lives in
    
    DriverManifestTests.FindLoadDriver_WithRealDriverManifest_LoadsDuckDbDriver.
    
    Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
---
 .../DriverManager/AdbcDriverManager.cs             | 244 +++++++++------
 .../DriverManager/ConnectionProfile.cs             | 164 +++++++---
 .../DriverManager/DriverManifest.cs                | 331 +++++++++++++++++++++
 .../DriverManager/FilesystemProfileProvider.cs     |   8 +-
 csharp/src/Apache.Arrow.Adbc/readme.md             |  71 +++--
 .../DriverManager/ColocatedManifestTests.cs        |  90 ++++--
 .../DriverManager/DriverManifestTests.cs           | 320 ++++++++++++++++++++
 .../DriverManager/EntrypointSchemeTests.cs         | 123 ++++++++
 .../DriverManager/TomlConnectionProfileTests.cs    | 263 +++++++++++-----
 9 files changed, 1357 insertions(+), 257 deletions(-)

diff --git a/csharp/src/Apache.Arrow.Adbc/DriverManager/AdbcDriverManager.cs 
b/csharp/src/Apache.Arrow.Adbc/DriverManager/AdbcDriverManager.cs
index 88adeddfe..3badc3e45 100644
--- a/csharp/src/Apache.Arrow.Adbc/DriverManager/AdbcDriverManager.cs
+++ b/csharp/src/Apache.Arrow.Adbc/DriverManager/AdbcDriverManager.cs
@@ -64,10 +64,11 @@ namespace Apache.Arrow.Adbc.DriverManager
         /// <para>
         /// <b>Managed (.NET) drivers always require a manifest.</b> This 
method loads the
         /// driver as a native shared library unless a co-located TOML 
manifest is present
-        /// that specifies a managed <c>driver_type</c>. To load a managed 
.NET driver,
-        /// either point <paramref name="driverPath"/> at a directory 
containing a
-        /// co-located <c>.toml</c> manifest, call <see 
cref="LoadFromManifest"/> directly,
-        /// or use <see cref="LoadManagedDriver"/> to load a .NET assembly 
without a manifest.
+        /// whose <c>[Driver].entrypoint</c> begins with a managed-runtime 
scheme prefix
+        /// (e.g. <c>dotnet:My.Driver.Type</c>). To load a managed .NET 
driver, either
+        /// point <paramref name="driverPath"/> at a directory containing such 
a co-located
+        /// manifest or use <see cref="LoadManagedDriver"/> to load a .NET 
assembly
+        /// without a manifest.
         /// </para>
         /// <para>
         /// <b>Security:</b> <paramref name="driverPath"/> must be an 
absolute, fully
@@ -112,12 +113,19 @@ namespace Apache.Arrow.Adbc.DriverManager
         }
 
         /// <summary>
-        /// Loads a native driver assembly from a verified absolute path, with 
no
-        /// further manifest probing. All terminal native-load call sites 
funnel
-        /// through this helper so security policy is applied uniformly.
+        /// Loads a driver assembly from a verified absolute path, with no 
further
+        /// manifest probing. If <paramref name="entrypoint"/> is 
scheme-prefixed
+        /// (<c>dotnet:</c>, <c>netfx:</c>) the managed loader is used; 
otherwise
+        /// the path is loaded as a native shared library. All terminal load 
call
+        /// sites funnel through this helper so security policy and audit 
logging
+        /// are applied uniformly.
         /// </summary>
         private static AdbcDriver LoadNativeDriver(string driverPath, string? 
entrypoint, string loadMethod)
         {
+            if (entrypoint != null && HasManagedEntrypointScheme(entrypoint))
+            {
+                return LoadByEntrypointScheme(driverPath, entrypoint, 
manifestPath: null, loadMethod);
+            }
             string resolvedEntrypoint = entrypoint ?? 
DeriveEntrypoint(driverPath);
             return LoadWithSecurity(
                 driverPath,
@@ -424,25 +432,30 @@ namespace Apache.Arrow.Adbc.DriverManager
         // OpenDatabaseFromProfile – load driver + open database in one step
         // 
-----------------------------------------------------------------------
 
+        /// <summary>The option key the connection profile uses to override 
the driver entrypoint.</summary>
+        internal const string EntrypointOptionKey = "entrypoint";
+
         /// <summary>
         /// Loads the driver specified by <paramref name="profile"/> and opens 
a database,
         /// applying all options from the profile as connection parameters.
         /// </summary>
         /// <remarks>
         /// <para>
-        /// If the profile has a non-null <see 
cref="ConnectionProfile.DriverTypeName"/>, the
-        /// driver is loaded as a managed .NET assembly via <see 
cref="LoadManagedDriver"/> and
-        /// <see cref="ConnectionProfile.DriverName"/> is used as the assembly 
path.
-        /// </para>
-        /// <para>
-        /// Otherwise the driver is loaded as a native shared library via
-        /// <see cref="FindLoadDriver"/>.
+        /// The driver is located by name via <see cref="FindLoadDriver"/>. If 
a driver
+        /// manifest is found, its <c>[Driver].entrypoint</c> determines 
whether the
+        /// driver loads natively or via the managed (.NET) host; a 
scheme-prefixed
+        /// entrypoint such as <c>dotnet:My.Driver.Type</c> selects the 
managed loader.
+        /// The profile's <c>[Options]</c> table may carry an 
<c>entrypoint</c> value
+        /// that overrides anything in the manifest -- useful when 
<c>driver</c> is a
+        /// bare shared-library path with no companion manifest.
         /// </para>
         /// <para>
         /// All options (string, integer, and double) are merged into a single
         /// <c>string → string</c> dictionary.  Integer and double values are 
formatted
         /// using <see cref="CultureInfo.InvariantCulture"/>. The merged 
dictionary is
         /// passed to <see 
cref="AdbcDriver.Open(IReadOnlyDictionary{string,string})"/>.
+        /// The <c>entrypoint</c> option (if any) is consumed by the driver 
manager
+        /// and is not forwarded to the driver.
         /// </para>
         /// <para>
         /// Call <see cref="ConnectionProfile.ResolveEnvVars"/> on the profile 
before
@@ -475,24 +488,24 @@ namespace Apache.Arrow.Adbc.DriverManager
         /// options, the explicit value takes precedence.
         /// </para>
         /// <para>
-        /// If the profile has a non-null <see 
cref="ConnectionProfile.DriverTypeName"/>, the
-        /// driver is loaded as a managed .NET assembly via <see 
cref="LoadManagedDriver"/> and
-        /// <see cref="ConnectionProfile.DriverName"/> is used as the assembly 
path.
+        /// The driver is located by name via <see cref="FindLoadDriver"/>; the
+        /// <c>entrypoint</c> option (if present in either the profile's 
<c>[Options]</c>
+        /// or <paramref name="explicitOptions"/>) is consumed by the driver 
manager
+        /// and overrides any entrypoint specified by a discovered driver 
manifest.
+        /// Scheme-prefixed values such as <c>dotnet:My.Driver.Type</c> route 
the load
+        /// through the managed (.NET) host.
         /// </para>
         /// <para>
-        /// Otherwise the driver is loaded as a native shared library via
-        /// <see cref="FindLoadDriver"/>.
-        /// </para>
-        /// <para>
-        /// All options are merged into a single
-        /// following order (later values override earlier ones for the same 
key):
+        /// All options are merged into a single dictionary in the following 
order (later
+        /// values override earlier ones for the same key):
         /// <list type="number">
         ///   <item>Profile integer options (formatted as strings)</item>
         ///   <item>Profile double options (formatted as strings)</item>
         ///   <item>Profile string options</item>
         ///   <item>Explicit options from <paramref 
name="explicitOptions"/></item>
         /// </list>
-        /// The merged dictionary is passed to <see 
cref="AdbcDriver.Open(IReadOnlyDictionary{string,string})"/>.
+        /// The merged dictionary, minus any <c>entrypoint</c> entry, is 
passed to
+        /// <see cref="AdbcDriver.Open(IReadOnlyDictionary{string,string})"/>.
         /// </para>
         /// </remarks>
         /// <param name="profile">The connection profile specifying the driver 
and options.</param>
@@ -513,25 +526,24 @@ namespace Apache.Arrow.Adbc.DriverManager
         {
             if (profile == null) throw new 
ArgumentNullException(nameof(profile));
 
-            AdbcDriver driver;
-
-            if (!string.IsNullOrEmpty(profile.DriverTypeName))
+            Dictionary<string, string> mergedOptions = new Dictionary<string, 
string>(StringComparer.Ordinal);
+            foreach (KeyValuePair<string, string> kv in 
BuildStringOptions(profile, explicitOptions))
             {
-                // Managed .NET driver path
-                if (string.IsNullOrEmpty(profile.DriverName))
-                    throw new AdbcException(
-                        "The connection profile specifies a driver_type but no 
driver assembly path (driver field).",
-                        AdbcStatusCode.InvalidArgument);
-
-                driver = LoadManagedDriver(profile.DriverName!, 
profile.DriverTypeName!);
+                mergedOptions[kv.Key] = kv.Value;
             }
-            else
+
+            // entrypoint is a driver-manager option, not a driver option: 
pull it out
+            // of the bag before opening the database. When set, it overrides 
any
+            // entrypoint declared by a driver manifest found via 
FindLoadDriver.
+            string? entrypoint = null;
+            if (mergedOptions.TryGetValue(EntrypointOptionKey, out string? 
entrypointValue))
             {
-                // Native shared-library path
-                driver = LoadDriverFromProfile(profile, null, loadOptions, 
additionalSearchPathList);
+                entrypoint = entrypointValue;
+                mergedOptions.Remove(EntrypointOptionKey);
             }
 
-            return driver.Open(BuildStringOptions(profile, explicitOptions));
+            AdbcDriver driver = LoadDriverFromProfile(profile, entrypoint, 
loadOptions, additionalSearchPathList);
+            return driver.Open(mergedOptions);
         }
 
         /// <summary>
@@ -658,15 +670,9 @@ namespace Apache.Arrow.Adbc.DriverManager
         /// </para>
         /// <para>
         /// Co-located manifests allow drivers to ship with metadata about how 
they should
-        /// be loaded (e.g., specifying they're managed .NET drivers via 
<c>driver_type</c>,
-        /// or redirecting to the actual driver location via the <c>driver</c> 
field).
-        /// </para>
-        /// <para>
-        /// <b>Important:</b> Options specified in co-located manifests are 
NOT automatically
-        /// applied to database connections. The manifest is used solely for 
driver loading.
-        /// To use manifest options, explicitly load the profile with
-        /// <see cref="TomlConnectionProfile.FromFile"/> and use
-        /// <see cref="OpenDatabaseFromProfile(ConnectionProfile, 
IReadOnlyDictionary{string, string}?, AdbcLoadFlags, string?)"/>.
+        /// be loaded -- the symbol name to invoke (or, for managed .NET 
drivers, the type
+        /// to instantiate via a <c>dotnet:</c> / <c>netfx:</c> scheme on 
<c>entrypoint</c>),
+        /// and platform-specific shared library paths under 
<c>[Driver.shared]</c>.
         /// </para>
         /// </remarks>
         /// <param name="driverPath">The path to the driver file.</param>
@@ -694,6 +700,12 @@ namespace Apache.Arrow.Adbc.DriverManager
             }
         }
 
+        /// <summary>The entrypoint scheme prefix for managed .NET (Core / 5+) 
drivers.</summary>
+        internal const string DotnetEntrypointScheme = "dotnet:";
+
+        /// <summary>The entrypoint scheme prefix for managed .NET Framework 
4.x drivers.</summary>
+        internal const string NetFxEntrypointScheme = "netfx:";
+
         private static AdbcDriver LoadFromManifest(string manifestPath, 
string? entrypoint)
         {
             if (!File.Exists(manifestPath))
@@ -703,68 +715,114 @@ namespace Apache.Arrow.Adbc.DriverManager
                     AdbcStatusCode.NotFound);
             }
 
-            ConnectionProfile manifest = 
FilesystemProfileProvider.LoadFromFile(manifestPath);
+            DriverManifest manifest = 
DriverManifest.LoadFromFile(manifestPath);
 
-            if (string.IsNullOrEmpty(manifest.DriverName))
-            {
-                throw new AdbcException(
-                    $"Driver manifest does not specify a 'driver' field.",
-                    AdbcStatusCode.InvalidArgument);
-            }
+            // Caller-supplied entrypoint wins over the manifest's. Falls back 
to
+            // a derived native symbol name only when neither is provided.
+            string resolvedEntrypoint = entrypoint
+                ?? manifest.Entrypoint
+                ?? DeriveEntrypoint(manifest.LibraryPath);
 
             string? manifestDir = 
Path.GetDirectoryName(Path.GetFullPath(manifestPath));
+            string resolvedPath = ResolveManifestPath(manifest.LibraryPath, 
manifestDir);
 
-            // Check if this is a managed driver
-            if (!string.IsNullOrEmpty(manifest.DriverTypeName))
+            return LoadByEntrypointScheme(resolvedPath, resolvedEntrypoint, 
manifestPath, nameof(LoadFromManifest));
+        }
+
+        /// <summary>Returns <c>true</c> if <paramref name="entrypoint"/> uses 
a managed-runtime scheme prefix.</summary>
+        private static bool HasManagedEntrypointScheme(string entrypoint) =>
+            entrypoint.StartsWith(DotnetEntrypointScheme, 
StringComparison.Ordinal) ||
+            entrypoint.StartsWith(NetFxEntrypointScheme, 
StringComparison.Ordinal);
+
+        /// <summary>
+        /// Resolves a path read out of a manifest: absolute paths are 
validated
+        /// against the security policy as-is; relative paths are anchored to 
the
+        /// manifest's directory and validated to ensure they don't escape it.
+        /// </summary>
+        private static string ResolveManifestPath(string libraryPath, string? 
manifestDir)
+        {
+            if (IsAbsolutePath(libraryPath))
             {
-                // Managed .NET driver - resolve path
-                string driverPath = manifest.DriverName!;
-                if (IsAbsolutePath(driverPath))
-                {
-                    // Absolute path - validate it doesn't contain path 
traversal
-                    DriverManagerSecurity.ValidatePathSecurity(driverPath, 
"manifest driver path");
-                }
-                else
-                {
-                    // Relative path - validate it doesn't escape the manifest 
directory
-                    if (!string.IsNullOrEmpty(manifestDir))
-                    {
-                        driverPath = 
DriverManagerSecurity.ValidateAndResolveManifestPath(manifestDir, driverPath);
-                    }
-                }
+                DriverManagerSecurity.ValidatePathSecurity(libraryPath, 
"manifest driver path");
+                return libraryPath;
+            }
+            if (!string.IsNullOrEmpty(manifestDir))
+            {
+                return 
DriverManagerSecurity.ValidateAndResolveManifestPath(manifestDir!, libraryPath);
+            }
+            return libraryPath;
+        }
+
+        /// <summary>
+        /// Dispatches a driver load by the scheme prefix on the entrypoint 
value.
+        /// Plain symbol names load as native drivers; <c>dotnet:</c> / 
<c>netfx:</c>
+        /// route to the managed loader and are rejected when the host process 
is
+        /// running on the wrong runtime.
+        /// </summary>
+        private static AdbcDriver LoadByEntrypointScheme(
+            string driverPath,
+            string entrypoint,
+            string? manifestPath,
+            string loadMethod)
+        {
+            if (entrypoint.StartsWith(DotnetEntrypointScheme, 
StringComparison.Ordinal))
+            {
+                string typeName = 
entrypoint.Substring(DotnetEntrypointScheme.Length);
+                EnsureRuntime(isNetFramework: false, scheme: 
DotnetEntrypointScheme);
                 return LoadWithSecurity(
                     driverPath,
-                    manifest.DriverTypeName!,
+                    typeName,
                     manifestPath,
-                    nameof(LoadFromManifest),
-                    () => LoadManagedDriverCore(driverPath, 
manifest.DriverTypeName!));
+                    loadMethod,
+                    () => LoadManagedDriverCore(driverPath, typeName));
             }
 
-            // Native driver - resolve entrypoint and path
-            string resolvedEntrypoint = entrypoint ?? 
DeriveEntrypoint(manifest.DriverName!);
-
-            // Resolve driver path
-            string resolvedDriverPath = manifest.DriverName!;
-            if (IsAbsolutePath(resolvedDriverPath))
-            {
-                // Absolute path - validate it doesn't contain path traversal
-                DriverManagerSecurity.ValidatePathSecurity(resolvedDriverPath, 
"manifest driver path");
-            }
-            else
+            if (entrypoint.StartsWith(NetFxEntrypointScheme, 
StringComparison.Ordinal))
             {
-                // Relative path - validate it doesn't escape the manifest 
directory
-                if (!string.IsNullOrEmpty(manifestDir))
-                {
-                    resolvedDriverPath = 
DriverManagerSecurity.ValidateAndResolveManifestPath(manifestDir, 
resolvedDriverPath);
-                }
+                string typeName = 
entrypoint.Substring(NetFxEntrypointScheme.Length);
+                EnsureRuntime(isNetFramework: true, scheme: 
NetFxEntrypointScheme);
+                return LoadWithSecurity(
+                    driverPath,
+                    typeName,
+                    manifestPath,
+                    loadMethod,
+                    () => LoadManagedDriverCore(driverPath, typeName));
             }
 
             return LoadWithSecurity(
-                resolvedDriverPath,
+                driverPath,
                 typeName: null,
                 manifestPath: manifestPath,
-                loadMethod: nameof(LoadFromManifest),
-                () => CAdbcDriverImporter.Load(resolvedDriverPath, 
resolvedEntrypoint));
+                loadMethod: loadMethod,
+                () => CAdbcDriverImporter.Load(driverPath, entrypoint));
+        }
+
+        /// <summary>
+        /// Throws <see cref="AdbcException"/> when the running .NET runtime 
does
+        /// not match the host implied by the entrypoint scheme. The check uses
+        /// <see cref="RuntimeInformation.FrameworkDescription"/> (a runtime
+        /// property) rather than a compile-time symbol: this library can be
+        /// targeted at <c>netstandard2.0</c> and consumed from either runtime.
+        /// </summary>
+        private static void EnsureRuntime(bool isNetFramework, string scheme)
+        {
+            bool hostIsNetFramework = RuntimeInformation.FrameworkDescription
+                .StartsWith(".NET Framework", 
StringComparison.OrdinalIgnoreCase);
+
+            if (isNetFramework && !hostIsNetFramework)
+            {
+                throw new AdbcException(
+                    $"Driver entrypoint scheme '{scheme}' requires .NET 
Framework, but the host process is " +
+                    RuntimeInformation.FrameworkDescription + ".",
+                    AdbcStatusCode.NotImplemented);
+            }
+            if (!isNetFramework && hostIsNetFramework)
+            {
+                throw new AdbcException(
+                    $"Driver entrypoint scheme '{scheme}' requires .NET 5 or 
later, but the host process is " +
+                    RuntimeInformation.FrameworkDescription + ".",
+                    AdbcStatusCode.NotImplemented);
+            }
         }
 
         private static AdbcDriver? TryLoadFromDirectory(string dir, string 
driverName, string? entrypoint)
diff --git a/csharp/src/Apache.Arrow.Adbc/DriverManager/ConnectionProfile.cs 
b/csharp/src/Apache.Arrow.Adbc/DriverManager/ConnectionProfile.cs
index 371ef67c3..f84a3e1b0 100644
--- a/csharp/src/Apache.Arrow.Adbc/DriverManager/ConnectionProfile.cs
+++ b/csharp/src/Apache.Arrow.Adbc/DriverManager/ConnectionProfile.cs
@@ -17,6 +17,8 @@
 
 using System;
 using System.Collections.Generic;
+using System.Text;
+using System.Text.RegularExpressions;
 
 namespace Apache.Arrow.Adbc.DriverManager
 {
@@ -33,12 +35,20 @@ namespace Apache.Arrow.Adbc.DriverManager
     /// </para>
     /// <para>
     /// Options come in three typed flavors: string, 64-bit integer, and 
double.
-    /// String option values of the form <c>env_var(ENV_VAR_NAME)</c> are 
expanded
-    /// from the named environment variable by <see cref="ResolveEnvVars"/>.
+    /// String option values may contain <c>{{ env_var(NAME) }}</c> 
placeholders that
+    /// <see cref="ResolveEnvVars"/> expands using process environment 
variables.
     /// </para>
     /// </remarks>
     public sealed class ConnectionProfile
     {
+        // Per docs/source/format/connection_profiles.rst, dynamic 
substitutions
+        // are written as `{{ <function-call> }}` and may appear anywhere 
inside
+        // a string value. The character set inside the placeholder excludes
+        // braces so adjacent placeholders don't accidentally merge.
+        private static readonly Regex PlaceholderRegex = new Regex(
+            @"\{\{\s*([^{}]*?)\s*\}\}",
+            RegexOptions.Compiled | RegexOptions.CultureInvariant);
+
         private const string EnvVarPrefix = "env_var(";
 
         private readonly Dictionary<string, string> _stringOptions;
@@ -49,25 +59,24 @@ namespace Apache.Arrow.Adbc.DriverManager
         /// Initializes a new <see cref="ConnectionProfile"/>.
         /// </summary>
         /// <param name="driverName">
-        /// The driver name. For native drivers this is the path to a shared 
library or
-        /// a bare driver name; for managed drivers this is the path to the 
.NET assembly.
-        /// </param>
-        /// <param name="driverTypeName">
-        /// The fully-qualified .NET type name of the <see cref="AdbcDriver"/> 
subclass
-        /// to instantiate for managed (pure .NET) drivers, or <c>null</c> for 
native drivers.
+        /// The driver reference: a bare driver name (resolved against the 
manifest
+        /// search path), an absolute or relative path to a shared library, or 
an
+        /// absolute or relative path to a driver manifest <c>.toml</c> file. 
For
+        /// managed (.NET) drivers, the manifest at this location selects the
+        /// runtime via <c>[Driver].entrypoint</c>; alternatively, a profile 
that
+        /// points directly at a managed assembly can supply the type name 
through
+        /// an <c>entrypoint</c> option.
         /// </param>
         /// <param name="stringOptions">String options, or <c>null</c> for 
none.</param>
         /// <param name="intOptions">Integer options, or <c>null</c> for 
none.</param>
         /// <param name="doubleOptions">Double options, or <c>null</c> for 
none.</param>
         public ConnectionProfile(
             string? driverName = null,
-            string? driverTypeName = null,
             IReadOnlyDictionary<string, string>? stringOptions = null,
             IReadOnlyDictionary<string, long>? intOptions = null,
             IReadOnlyDictionary<string, double>? doubleOptions = null)
         {
             DriverName = driverName;
-            DriverTypeName = driverTypeName;
             _stringOptions = new Dictionary<string, 
string>(StringComparer.Ordinal);
             if (stringOptions != null)
             {
@@ -86,25 +95,16 @@ namespace Apache.Arrow.Adbc.DriverManager
         }
 
         /// <summary>
-        /// Gets the name of the driver specified by this profile, or 
<c>null</c> if
-        /// the profile does not specify a driver.
+        /// Gets the driver reference specified by this profile, or 
<c>null</c> if
+        /// the profile does not specify one. May be a bare driver name, a 
shared
+        /// library path, or a driver manifest path.
         /// </summary>
         public string? DriverName { get; }
 
         /// <summary>
-        /// Gets the fully-qualified .NET type name of the <see 
cref="AdbcDriver"/>
-        /// subclass to instantiate for managed (pure .NET) drivers, or 
<c>null</c>
-        /// for native drivers.
-        /// </summary>
-        /// <example>
-        /// <c>Apache.Arrow.Adbc.Drivers.BigQuery.BigQueryDriver</c>
-        /// </example>
-        public string? DriverTypeName { get; }
-
-        /// <summary>
-        /// Gets the string options specified by this profile. Values of the 
form
-        /// <c>env_var(ENV_VAR_NAME)</c> will be expanded from the named 
environment
-        /// variable when <see cref="ResolveEnvVars"/> is called.
+        /// Gets the string options specified by this profile. Values may 
contain
+        /// <c>{{ env_var(NAME) }}</c> placeholders that <see 
cref="ResolveEnvVars"/>
+        /// expands using process environment variables.
         /// </summary>
         public IReadOnlyDictionary<string, string> StringOptions => 
_stringOptions;
 
@@ -119,38 +119,106 @@ namespace Apache.Arrow.Adbc.DriverManager
         public IReadOnlyDictionary<string, double> DoubleOptions => 
_doubleOptions;
 
         /// <summary>
-        /// Returns a new profile with any <c>env_var(NAME)</c> values in
-        /// <see cref="StringOptions"/> replaced by the value of the 
corresponding
-        /// environment variable.
+        /// Returns a new profile with any <c>{{ env_var(NAME) }}</c> 
placeholders
+        /// in <see cref="StringOptions"/> expanded using process environment
+        /// variables.
         /// </summary>
+        /// <remarks>
+        /// <para>
+        /// Placeholder syntax matches the ADBC spec (see
+        /// <c>docs/source/format/connection_profiles.rst</c>):
+        /// </para>
+        /// <list type="bullet">
+        ///   <item>
+        ///     <description>
+        ///       Placeholders use <c>{{ }}</c> as the escape delimiters and 
may
+        ///       appear anywhere inside a value. Whitespace inside the braces 
is
+        ///       optional. Multiple placeholders may appear in one value
+        ///       (e.g. <c>"jdbc://{{ env_var(HOST) }}:{{ env_var(PORT) 
}}/db"</c>).
+        ///     </description>
+        ///   </item>
+        ///   <item>
+        ///     <description>
+        ///       A missing environment variable expands to an empty string and
+        ///       processing continues; this matches the C/C++ driver manager.
+        ///     </description>
+        ///   </item>
+        ///   <item>
+        ///     <description>
+        ///       The only supported function inside a placeholder is
+        ///       <c>env_var(NAME)</c>. Any other content -- including a 
literal
+        ///       <c>{{</c> in a value -- is rejected with
+        ///       <see cref="AdbcStatusCode.InvalidArgument"/>.
+        ///     </description>
+        ///   </item>
+        /// </list>
+        /// </remarks>
         /// <exception cref="AdbcException">
-        /// Thrown when a referenced environment variable is not set.
+        /// Thrown when a placeholder uses an unrecognized function or is 
malformed
+        /// (e.g. missing the closing parenthesis or environment variable 
name).
         /// </exception>
         public ConnectionProfile ResolveEnvVars()
         {
             Dictionary<string, string> resolved = new Dictionary<string, 
string>(StringComparer.Ordinal);
             foreach (KeyValuePair<string, string> kv in _stringOptions)
             {
-                string value = kv.Value;
-                if (value.StartsWith(EnvVarPrefix, StringComparison.Ordinal) &&
-                    value.EndsWith(")", StringComparison.Ordinal))
-                {
-                    string varName = value.Substring(EnvVarPrefix.Length, 
value.Length - EnvVarPrefix.Length - 1);
-                    string? envValue = 
Environment.GetEnvironmentVariable(varName);
-                    if (envValue == null)
-                    {
-                        throw new AdbcException(
-                            $"Environment variable '{varName}' required by 
profile option '{kv.Key}' is not set.",
-                            AdbcStatusCode.InvalidState);
-                    }
-                    resolved[kv.Key] = envValue;
-                }
-                else
-                {
-                    resolved[kv.Key] = value;
-                }
+                resolved[kv.Key] = ExpandPlaceholders(kv.Key, kv.Value);
+            }
+            return new ConnectionProfile(DriverName, resolved, _intOptions, 
_doubleOptions);
+        }
+
+        /// <summary>
+        /// Substitutes every <c>{{ ... }}</c> placeholder in <paramref 
name="value"/>
+        /// with its expansion. The only recognized function is 
<c>env_var(NAME)</c>;
+        /// anything else is an error.
+        /// </summary>
+        private static string ExpandPlaceholders(string key, string value)
+        {
+            if (string.IsNullOrEmpty(value) || value.IndexOf("{{", 
StringComparison.Ordinal) < 0)
+            {
+                return value;
+            }
+
+            StringBuilder sb = new StringBuilder(value.Length);
+            int lastIndex = 0;
+            foreach (Match match in PlaceholderRegex.Matches(value))
+            {
+                sb.Append(value, lastIndex, match.Index - lastIndex);
+                sb.Append(ExpandFunction(key, match.Groups[1].Value));
+                lastIndex = match.Index + match.Length;
+            }
+            sb.Append(value, lastIndex, value.Length - lastIndex);
+            return sb.ToString();
+        }
+
+        private static string ExpandFunction(string key, string content)
+        {
+            if (!content.StartsWith(EnvVarPrefix, StringComparison.Ordinal))
+            {
+                throw new AdbcException(
+                    $"Profile option '{key}' uses an unsupported substitution 
'{content}'. " +
+                    "Only env_var(NAME) is recognized.",
+                    AdbcStatusCode.InvalidArgument);
             }
-            return new ConnectionProfile(DriverName, DriverTypeName, resolved, 
_intOptions, _doubleOptions);
+            if (content.Length == 0 || content[content.Length - 1] != ')')
+            {
+                throw new AdbcException(
+                    $"Profile option '{key}' has a malformed env_var() 
placeholder: missing closing parenthesis.",
+                    AdbcStatusCode.InvalidArgument);
+            }
+
+            string varName = content.Substring(EnvVarPrefix.Length, 
content.Length - EnvVarPrefix.Length - 1);
+            if (varName.Length == 0)
+            {
+                throw new AdbcException(
+                    $"Profile option '{key}' has a malformed env_var() 
placeholder: missing environment variable name.",
+                    AdbcStatusCode.InvalidArgument);
+            }
+
+            // Missing environment variables expand to empty per the spec, 
matching
+            // the C/C++ driver manager. Callers that want to require an env 
var
+            // should validate after ResolveEnvVars returns.
+            return Environment.GetEnvironmentVariable(varName) ?? string.Empty;
         }
     }
 }
diff --git a/csharp/src/Apache.Arrow.Adbc/DriverManager/DriverManifest.cs 
b/csharp/src/Apache.Arrow.Adbc/DriverManager/DriverManifest.cs
new file mode 100644
index 000000000..9ad35036b
--- /dev/null
+++ b/csharp/src/Apache.Arrow.Adbc/DriverManager/DriverManifest.cs
@@ -0,0 +1,331 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace Apache.Arrow.Adbc.DriverManager
+{
+    /// <summary>
+    /// A parsed ADBC driver manifest. A driver manifest is a TOML file that
+    /// describes <i>where</i> a driver shared library lives and how to load 
it.
+    /// It is distinct from a <see cref="ConnectionProfile"/>, which describes
+    /// <i>how to open a database</i> with a driver and carries option 
key/value
+    /// pairs.
+    /// </summary>
+    /// <remarks>
+    /// <para>
+    /// The manifest format is defined in
+    /// <c>docs/source/format/driver_manifests.rst</c>:
+    /// </para>
+    /// <code>
+    /// manifest_version = 1
+    ///
+    /// name = "Driver Display Name"
+    /// version = "1.2.3"            # the driver's own version, a string
+    /// publisher = "..."
+    /// license = "Apache-2.0"
+    /// source = "..."
+    ///
+    /// [Driver]
+    /// entrypoint = "AdbcDriverInit"   # optional; defaults are derived from 
the file name
+    ///
+    /// # Either a single path:
+    /// [Driver]
+    /// shared = "/path/to/libadbc_driver.so"
+    ///
+    /// # Or platform-tuple-keyed paths:
+    /// [Driver.shared]
+    /// linux_amd64   = "/path/to/libadbc_driver.so"
+    /// macos_arm64   = "/path/to/libadbc_driver.dylib"
+    /// windows_amd64 = "C:\\path\\to\\adbc_driver.dll"
+    /// </code>
+    /// <para>
+    /// The <see cref="Entrypoint"/> value may be a plain native symbol name
+    /// (e.g. <c>AdbcDriverDuckdbInit</c>) or a scheme-prefixed value such as
+    /// <c>dotnet:Apache.Arrow.Adbc.Drivers.BigQuery.BigQueryDriver</c> /
+    /// <c>netfx:My.Driver.Class</c>. The scheme tells the driver manager which
+    /// managed-runtime host to start; values without a scheme are loaded as
+    /// native C entrypoints.
+    /// </para>
+    /// </remarks>
+    internal sealed class DriverManifest
+    {
+        private const string ManifestVersionField = "manifest_version";
+
+        private DriverManifest(
+            long manifestVersion,
+            string? name,
+            string? version,
+            string? publisher,
+            string? license,
+            string? source,
+            string? entrypoint,
+            string libraryPath)
+        {
+            ManifestVersion = manifestVersion;
+            Name = name;
+            Version = version;
+            Publisher = publisher;
+            License = license;
+            Source = source;
+            Entrypoint = entrypoint;
+            LibraryPath = libraryPath;
+        }
+
+        /// <summary>The manifest format version. Always 1 today.</summary>
+        public long ManifestVersion { get; }
+
+        /// <summary>Display name of the driver.</summary>
+        public string? Name { get; }
+
+        /// <summary>The driver's own version (a free-form string per the 
spec).</summary>
+        public string? Version { get; }
+
+        /// <summary>Publisher of the driver.</summary>
+        public string? Publisher { get; }
+
+        /// <summary>License identifier of the driver.</summary>
+        public string? License { get; }
+
+        /// <summary>Where this driver came from (e.g. a package 
name).</summary>
+        public string? Source { get; }
+
+        /// <summary>
+        /// The entrypoint value from <c>[Driver].entrypoint</c>. May be a 
plain
+        /// symbol name, a <c>dotnet:</c>-prefixed type name, or any other
+        /// scheme-prefixed value. May be <c>null</c> if the manifest does not
+        /// specify one (in which case callers derive a default).
+        /// </summary>
+        public string? Entrypoint { get; }
+
+        /// <summary>
+        /// The driver library path for the current platform, resolved from
+        /// <c>[Driver.shared]</c>. May be an absolute path or a path relative
+        /// to the manifest's directory; callers are responsible for 
resolution.
+        /// </summary>
+        public string LibraryPath { get; }
+
+        /// <summary>
+        /// Returns <c>true</c> if the given parsed TOML content looks like a
+        /// driver manifest rather than a connection profile.
+        /// </summary>
+        /// <remarks>
+        /// The discriminator is the presence of <c>manifest_version</c> at the
+        /// root level, or any <c>[Driver]</c> / <c>[Driver.shared]</c> 
section.
+        /// </remarks>
+        internal static bool LooksLikeManifest(Dictionary<string, 
Dictionary<string, object>> sections)
+        {
+            if (sections == null) return false;
+
+            if (sections.TryGetValue("", out Dictionary<string, object>? root) 
&&
+                root.ContainsKey(ManifestVersionField))
+            {
+                return true;
+            }
+
+            return sections.ContainsKey("Driver") || 
sections.ContainsKey("Driver.shared");
+        }
+
+        /// <summary>
+        /// Parses a driver manifest from TOML content.
+        /// </summary>
+        /// <param name="tomlContent">The raw TOML text to parse.</param>
+        /// <returns>The parsed <see cref="DriverManifest"/>.</returns>
+        /// <exception cref="ArgumentNullException">If <paramref 
name="tomlContent"/> is null.</exception>
+        /// <exception cref="AdbcException">
+        /// Thrown when the TOML is malformed, the manifest version is 
unsupported,
+        /// or no library path can be resolved for the current platform.
+        /// </exception>
+        public static DriverManifest LoadFromContent(string tomlContent)
+        {
+            if (tomlContent == null) throw new 
ArgumentNullException(nameof(tomlContent));
+
+            Dictionary<string, Dictionary<string, object>> sections;
+            try
+            {
+                sections = TomlParser.Parse(tomlContent);
+            }
+            catch (FormatException ex)
+            {
+                throw new AdbcException(
+                    "Invalid TOML driver manifest: " + ex.Message,
+                    AdbcStatusCode.InvalidArgument,
+                    ex);
+            }
+
+            Dictionary<string, object> root = sections.TryGetValue("", out 
Dictionary<string, object>? r)
+                ? r
+                : new Dictionary<string, object>();
+
+            long manifestVersion = ReadManifestVersion(root);
+            if (manifestVersion != 1)
+            {
+                throw new AdbcException(
+                    $"Driver manifest version '{manifestVersion}' is not 
supported by this driver manager.",
+                    AdbcStatusCode.NotImplemented);
+            }
+
+            string? name = ReadOptionalString(root, "name");
+            string? version = ReadOptionalString(root, "version");
+            string? publisher = ReadOptionalString(root, "publisher");
+            string? license = ReadOptionalString(root, "license");
+            string? source = ReadOptionalString(root, "source");
+
+            string? entrypoint = null;
+            if (sections.TryGetValue("Driver", out Dictionary<string, object>? 
driverSection))
+            {
+                entrypoint = ReadOptionalString(driverSection, "entrypoint");
+            }
+
+            string libraryPath = ResolveLibraryPath(sections, driverSection);
+
+            return new DriverManifest(
+                manifestVersion,
+                name,
+                version,
+                publisher,
+                license,
+                source,
+                entrypoint,
+                libraryPath);
+        }
+
+        /// <summary>
+        /// Parses a driver manifest from a file path.
+        /// </summary>
+        public static DriverManifest LoadFromFile(string filePath)
+        {
+            if (filePath == null) throw new 
ArgumentNullException(nameof(filePath));
+            string content = File.ReadAllText(filePath, Encoding.UTF8);
+            return LoadFromContent(content);
+        }
+
+        private static long ReadManifestVersion(Dictionary<string, object> 
root)
+        {
+            // Per the spec, manifest_version defaults to 1 when absent.
+            if (!root.TryGetValue(ManifestVersionField, out object? 
versionObj))
+            {
+                return 1;
+            }
+
+            if (versionObj is long lv)
+            {
+                return lv;
+            }
+
+            throw new AdbcException(
+                $"The 'manifest_version' field has an invalid value 
'{versionObj}'. It must be an integer.",
+                AdbcStatusCode.InvalidArgument);
+        }
+
+        private static string? ReadOptionalString(Dictionary<string, object> 
section, string key)
+        {
+            if (section.TryGetValue(key, out object? obj) && obj is string s)
+            {
+                return s;
+            }
+            return null;
+        }
+
+        /// <summary>
+        /// Resolves the library path from the manifest. Supports the two spec 
forms:
+        /// <c>[Driver].shared = "..."</c> (single string) and
+        /// <c>[Driver.shared]</c> table keyed by platform tuple.
+        /// </summary>
+        private static string ResolveLibraryPath(
+            Dictionary<string, Dictionary<string, object>> sections,
+            Dictionary<string, object>? driverSection)
+        {
+            // Form 1: [Driver].shared = "/path/to/lib"
+            if (driverSection != null &&
+                driverSection.TryGetValue("shared", out object? sharedObj) &&
+                sharedObj is string sharedPath)
+            {
+                if (string.IsNullOrEmpty(sharedPath))
+                {
+                    throw new AdbcException(
+                        "Driver manifest has an empty 'Driver.shared' path.",
+                        AdbcStatusCode.InvalidArgument);
+                }
+                return sharedPath;
+            }
+
+            // Form 2: [Driver.shared] table keyed by platform tuple
+            if (sections.TryGetValue("Driver.shared", out Dictionary<string, 
object>? platformTable))
+            {
+                string current = GetCurrentPlatformTuple();
+                if (platformTable.TryGetValue(current, out object? platObj) && 
platObj is string platPath)
+                {
+                    if (string.IsNullOrEmpty(platPath))
+                    {
+                        throw new AdbcException(
+                            $"Driver manifest has an empty path for current 
platform '{current}'.",
+                            AdbcStatusCode.InvalidArgument);
+                    }
+                    return platPath;
+                }
+
+                List<string> tuples = new List<string>(platformTable.Count);
+                foreach (KeyValuePair<string, object> kv in platformTable) 
tuples.Add(kv.Key);
+                tuples.Sort(StringComparer.Ordinal);
+                throw new AdbcException(
+                    $"Driver manifest has no entry for current platform 
'{current}'. " +
+                    $"Available platforms: {string.Join(", ", tuples)}.",
+                    AdbcStatusCode.NotFound);
+            }
+
+            throw new AdbcException(
+                "Driver manifest does not specify a library path. " +
+                "Provide a 'Driver.shared' value, either as a single string or 
a " +
+                "platform-tuple-keyed table.",
+                AdbcStatusCode.InvalidArgument);
+        }
+
+        /// <summary>
+        /// Returns the platform tuple identifying the current OS and 
architecture,
+        /// matching the format used by the ADBC driver-manifest spec
+        /// (e.g. <c>windows_amd64</c>, <c>linux_arm64</c>, 
<c>macos_amd64</c>).
+        /// </summary>
+        internal static string GetCurrentPlatformTuple()
+        {
+            string os;
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) os = 
"windows";
+            else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) os = 
"macos";
+            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) os = 
"linux";
+#if NET6_0_OR_GREATER
+            else if (RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD)) os = 
"freebsd";
+#endif
+            else os = "unknown";
+
+            string arch;
+            switch (RuntimeInformation.ProcessArchitecture)
+            {
+                case Architecture.X64: arch = "amd64"; break;
+                case Architecture.Arm64: arch = "arm64"; break;
+                case Architecture.X86: arch = "x86"; break;
+                case Architecture.Arm: arch = "arm"; break;
+                default: arch = 
RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant(); break;
+            }
+
+            return os + "_" + arch;
+        }
+    }
+}
diff --git 
a/csharp/src/Apache.Arrow.Adbc/DriverManager/FilesystemProfileProvider.cs 
b/csharp/src/Apache.Arrow.Adbc/DriverManager/FilesystemProfileProvider.cs
index 47e51c74e..870bbb8ae 100644
--- a/csharp/src/Apache.Arrow.Adbc/DriverManager/FilesystemProfileProvider.cs
+++ b/csharp/src/Apache.Arrow.Adbc/DriverManager/FilesystemProfileProvider.cs
@@ -131,12 +131,6 @@ namespace Apache.Arrow.Adbc.DriverManager
                 driverName = driverStr;
             }
 
-            string? driverTypeName = null;
-            if (root.TryGetValue("driver_type", out object? driverTypeObj) && 
driverTypeObj is string driverTypeStr)
-            {
-                driverTypeName = driverTypeStr;
-            }
-
             Dictionary<string, string> stringOpts = new Dictionary<string, 
string>(StringComparer.Ordinal);
             Dictionary<string, long> intOpts = new Dictionary<string, 
long>(StringComparer.Ordinal);
             Dictionary<string, double> doubleOpts = new Dictionary<string, 
double>(StringComparer.Ordinal);
@@ -173,7 +167,7 @@ namespace Apache.Arrow.Adbc.DriverManager
                 }
             }
 
-            return new ConnectionProfile(driverName, driverTypeName, 
stringOpts, intOpts, doubleOpts);
+            return new ConnectionProfile(driverName, stringOpts, intOpts, 
doubleOpts);
         }
 
         /// <summary>
diff --git a/csharp/src/Apache.Arrow.Adbc/readme.md 
b/csharp/src/Apache.Arrow.Adbc/readme.md
index 75f956ccb..3b4b14a2f 100644
--- a/csharp/src/Apache.Arrow.Adbc/readme.md
+++ b/csharp/src/Apache.Arrow.Adbc/readme.md
@@ -27,42 +27,79 @@ The `Apache.Arrow.Adbc.DriverManager` namespace provides a 
.NET implementation o
 ### Features
 
 - **Driver discovery**: search for ADBC drivers by name across configurable 
directories (environment variable, user-level, system-level).
-- **TOML manifest loading**: locate drivers via `.toml` manifest files that 
specify the shared library path.
+- **TOML driver manifests**: locate drivers via `.toml` manifest files that 
specify the shared library path per platform.
 - **Connection profiles**: load reusable connection configurations (driver + 
options) from `.toml` profile files.
+- **Managed (.NET) drivers**: load .NET drivers via a scheme-prefixed 
`entrypoint` (`dotnet:` for .NET 5+, `netfx:` for .NET Framework 4.x).
 - **Custom profile providers**: plug in your own `IConnectionProfileProvider` 
implementation.
 
-### TOML Manifest / Profile Format
+### Driver Manifest Format
 
-#### Connection Profile Example (Snowflake)
+A *driver manifest* is a TOML file describing where a driver lives and how to 
load it. The format is shared across all ADBC driver-manager implementations 
and documented in `docs/source/format/driver_manifests.rst`.
 
-For unmanaged drivers loaded from native shared libraries:
+#### Native Driver Manifest Example (Snowflake)
 
 ```toml
-profile_version = 1
-driver = "libadbc_driver_snowflake"
+manifest_version = 1
+
+name = "Snowflake"
+version = "1.5.2"
+publisher = "snowflake.com"
+
+[Driver]
 entrypoint = "AdbcDriverSnowflakeInit"
 
+[Driver.shared]
+windows_amd64 = "C:\\path\\to\\adbc_driver_snowflake.dll"
+linux_amd64   = "/usr/local/lib/libadbc_driver_snowflake.so"
+macos_arm64   = "/opt/homebrew/lib/libadbc_driver_snowflake.dylib"
+```
+
+#### Managed Driver Manifest Example (BigQuery)
+
+Managed .NET drivers use a scheme-prefixed `entrypoint`:
+
+- `dotnet:` for modern .NET (.NET 5 and later, including .NET 8 / .NET 10)
+- `netfx:` for .NET Framework 4.x
+
+The host process rejects a manifest whose scheme doesn't match its runtime, so 
a `dotnet:` manifest on a .NET Framework process (or vice versa) fails with a 
clear error rather than mysteriously failing inside the assembly loader.
+
+```toml
+manifest_version = 1
+
+name = "BigQuery"
+version = "1.2.0"
+
+[Driver]
+entrypoint = "dotnet:Apache.Arrow.Adbc.Drivers.BigQuery.BigQueryDriver"
+shared = "Apache.Arrow.Adbc.Drivers.BigQuery.dll"
+```
+
+`shared` is relative to the manifest's directory. Managed .NET assemblies are 
platform-neutral, so the single-string form of `shared` is usually appropriate; 
the platform-tuple table is also accepted.
+
+### Connection Profile Format
+
+A *connection profile* points at a driver and supplies options to apply when 
opening a database. Profiles can name a driver by manifest name (resolved 
against the standard search paths), by direct path to a shared library, or by 
direct path to a manifest.
+
+```toml
+profile_version = 1
+driver = "snowflake"
+
 [Options]
 adbc.snowflake.sql.account = "myaccount"
 adbc.snowflake.sql.warehouse = "mywarehouse"
-adbc.snowflake.sql.auth_type = "auth_snowflake"
-username = "myuser"
-password = "env_var(SNOWFLAKE_PASSWORD)"
+password = "{{ env_var(SNOWFLAKE_PASSWORD) }}"
 ```
 
-#### Managed Driver Profile Example (BigQuery)
-
-For managed .NET drivers:
+If the profile points directly at a shared library that uses a non-default 
entrypoint (or at a managed assembly that needs a `dotnet:` / `netfx:` 
selector), supply it through the `entrypoint` option. The driver manager 
consumes that option and does not forward it to the driver:
 
 ```toml
 profile_version = 1
 driver = "C:\\path\\to\\Apache.Arrow.Adbc.Drivers.BigQuery.dll"
-driver_type = "Apache.Arrow.Adbc.Drivers.BigQuery.BigQueryDriver"
 
 [Options]
+entrypoint = "dotnet:Apache.Arrow.Adbc.Drivers.BigQuery.BigQueryDriver"
 adbc.bigquery.project_id = "my-project"
-adbc.bigquery.auth_type = "service"
-adbc.bigquery.json_credential = "env_var(BIGQUERY_JSON_CREDENTIAL)"
+adbc.bigquery.json_credential = "{{ env_var(BIGQUERY_JSON_CREDENTIAL) }}"
 ```
 
 #### Format Notes
@@ -70,9 +107,7 @@ adbc.bigquery.json_credential = 
"env_var(BIGQUERY_JSON_CREDENTIAL)"
 - Use `profile_version = 1` for the version field (legacy `version` is also 
supported for backward compatibility)
 - Use `[Options]` for the options section (legacy `[options]` is also 
supported for backward compatibility)
 - Boolean option values are converted to the string equivalents `"true"` or 
`"false"`.
-- Values of the form `env_var(ENV_VAR_NAME)` are expanded from the named 
environment variable at connection time.
-- For unmanaged drivers, use `driver` for the library path and `entrypoint` 
for the initialization function.
-- For managed drivers, use `driver` for the assembly path and `driver_type` 
for the fully-qualified type name.
+- String values may contain `{{ env_var(NAME) }}` placeholders, which are 
expanded from process environment variables when `ResolveEnvVars()` is called. 
The `{{` and `}}` delimiters serve as escapes: any text outside placeholders is 
treated literally. Placeholders may appear anywhere inside a value and may be 
repeated. A missing environment variable expands to an empty string. Only 
`env_var(NAME)` is recognized; other content inside a placeholder is an error.
 
 ### Managed Driver Loading (.NET Core / .NET 8)
 
diff --git 
a/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/ColocatedManifestTests.cs 
b/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/ColocatedManifestTests.cs
index 4c72c40cb..19c4ef1a5 100644
--- 
a/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/ColocatedManifestTests.cs
+++ 
b/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/ColocatedManifestTests.cs
@@ -75,6 +75,17 @@ namespace Apache.Arrow.Adbc.Tests.DriverManager
             return (dllPath, tomlPath);
         }
 
+        /// <summary>
+        /// Scheme prefix this test process must use when selecting managed 
drivers
+        /// (matches the runtime hosting the test).
+        /// </summary>
+        private static string ManagedScheme =>
+#if NETFRAMEWORK
+            "netfx:";
+#else
+            "dotnet:";
+#endif
+
         /// <summary>
         /// Creates test files where the manifest uses a relative path to a 
real assembly.
         /// </summary>
@@ -98,12 +109,12 @@ namespace Apache.Arrow.Adbc.Tests.DriverManager
             File.WriteAllText(placeholderDllPath, "placeholder");
             _tempFiles.Add(placeholderDllPath);
 
-            // Create manifest that uses relative path to the real assembly
-            string toml = "version = 1\n"
-                + "driver = \"" + realAssemblyName + "\"\n"
-                + "driver_type = \"" + typeName + "\"\n"
-                + "\n[options]\n"
-                + "from_manifest = \"true\"\n";
+            // Driver manifest: scheme-prefixed entrypoint selects the managed 
runtime,
+            // [Driver].shared = "..." carries the relative assembly path.
+            string toml = "manifest_version = 1\n"
+                + "\n[Driver]\n"
+                + "entrypoint = \"" + ManagedScheme + typeName + "\"\n"
+                + "shared = \"" + realAssemblyName + "\"\n";
 
             string tomlPath = Path.Combine(tempDir, baseName + ".toml");
             File.WriteAllText(tomlPath, toml);
@@ -133,8 +144,8 @@ namespace Apache.Arrow.Adbc.Tests.DriverManager
                 CreateTestFilesWithRelativeDriver("test_driver", typeName);
 
             // LoadDriver should auto-detect the co-located manifest and use 
it to determine:
-            // - The actual driver location (from the 'driver' field - 
relative path)
-            // - Whether it's a managed driver (from 'driver_type')
+            // - The actual driver location (from [Driver].shared, a relative 
path here)
+            // - The managed runtime to host the driver (from the scheme 
prefix on entrypoint)
             AdbcDriver driver = AdbcDriverManager.LoadDriver(dllPath);
             Assert.NotNull(driver);
             // Check type name instead of IsType to avoid assembly identity 
issues
@@ -195,20 +206,57 @@ namespace Apache.Arrow.Adbc.Tests.DriverManager
         }
 
         [Fact]
-        public void LoadDriver_ExplicitEntrypointStillWorks()
+        public void LoadDriver_ExplicitEntrypointOverridesManifest()
         {
+            // Build a manifest whose [Driver].entrypoint is something the 
caller will
+            // override. The caller passes a scheme-prefixed entrypoint 
pointing at
+            // the real managed driver type; that override must win over the 
manifest
+            // value and select the managed loader.
             string typeName = typeof(FakeAdbcDriver).FullName!;
 
             (string dllPath, string tomlPath, string realAssemblyPath) =
-                CreateTestFilesWithRelativeDriver("entrypoint_test", typeName);
+                CreateTestFilesWithManifestEntrypoint("entrypoint_test", 
"AdbcDriverInit");
 
-            // Even with a manifest, explicit entrypoint parameter should work
-            // (though for managed drivers, entrypoint doesn't apply - it's 
ignored)
-            AdbcDriver driver = AdbcDriverManager.LoadDriver(dllPath, 
"CustomEntrypoint");
+            AdbcDriver driver = AdbcDriverManager.LoadDriver(dllPath, 
ManagedScheme + typeName);
             Assert.NotNull(driver);
             Assert.Equal(typeName, driver.GetType().FullName);
         }
 
+        /// <summary>
+        /// Like <see cref="CreateTestFilesWithRelativeDriver"/> but lets the 
caller
+        /// control the manifest's <c>[Driver].entrypoint</c> value -- useful 
for
+        /// tests that verify caller-supplied entrypoint overrides win.
+        /// </summary>
+        private (string placeholderDllPath, string tomlPath, string 
realAssemblyPath) CreateTestFilesWithManifestEntrypoint(
+            string baseName,
+            string manifestEntrypoint)
+        {
+            string tempDir = Path.Combine(Path.GetTempPath(), 
Guid.NewGuid().ToString("N"));
+            Directory.CreateDirectory(tempDir);
+            _tempDirs.Add(tempDir);
+
+            string realAssemblyPath = typeof(FakeAdbcDriver).Assembly.Location;
+            string realAssemblyName = Path.GetFileName(realAssemblyPath);
+            string copiedAssemblyPath = Path.Combine(tempDir, 
realAssemblyName);
+            File.Copy(realAssemblyPath, copiedAssemblyPath, overwrite: true);
+            _tempFiles.Add(copiedAssemblyPath);
+
+            string placeholderDllPath = Path.Combine(tempDir, baseName + 
".dll");
+            File.WriteAllText(placeholderDllPath, "placeholder");
+            _tempFiles.Add(placeholderDllPath);
+
+            string toml = "manifest_version = 1\n"
+                + "\n[Driver]\n"
+                + "entrypoint = \"" + manifestEntrypoint + "\"\n"
+                + "shared = \"" + realAssemblyName + "\"\n";
+
+            string tomlPath = Path.Combine(tempDir, baseName + ".toml");
+            File.WriteAllText(tomlPath, toml);
+            _tempFiles.Add(tomlPath);
+
+            return (placeholderDllPath, tomlPath, copiedAssemblyPath);
+        }
+
         [Fact]
         public void LoadDriver_RelativePathInManifest_ResolvedCorrectly()
         {
@@ -226,9 +274,10 @@ namespace Apache.Arrow.Adbc.Tests.DriverManager
             _tempFiles.Add(localAssemblyPath);
 
             // Manifest uses relative path to the driver
-            string toml = "version = 1\n"
-                + "driver = \"" + assemblyFileName + "\"\n"
-                + "driver_type = \"" + typeName + "\"\n";
+            string toml = "manifest_version = 1\n"
+                + "\n[Driver]\n"
+                + "entrypoint = \"" + ManagedScheme + typeName + "\"\n"
+                + "shared = \"" + assemblyFileName + "\"\n";
 
             string dllPath = Path.Combine(tempDir, "wrapper.dll");
             string tomlPath = Path.Combine(tempDir, "wrapper.toml");
@@ -269,9 +318,10 @@ namespace Apache.Arrow.Adbc.Tests.DriverManager
             _tempFiles.Add(copiedAssemblyPath);
 
             // Create manifest with relative path
-            string toml = "version = 1\n"
-                + "driver = \"" + assemblyFileName + "\"\n"
-                + "driver_type = \"" + typeName + "\"\n";
+            string toml = "manifest_version = 1\n"
+                + "\n[Driver]\n"
+                + "entrypoint = \"" + ManagedScheme + typeName + "\"\n"
+                + "shared = \"" + assemblyFileName + "\"\n";
 
             string soPath = Path.Combine(tempDir, "test.driver.so");
             string soToml = Path.Combine(tempDir, "test.driver.toml");
@@ -306,7 +356,7 @@ namespace Apache.Arrow.Adbc.Tests.DriverManager
             // Note: LoadManagedDriver does not currently detect co-located 
manifests.
             // It loads directly from the specified assembly path.
             // To use manifest redirection, use LoadDriver with a co-located 
manifest
-            // that specifies driver_type.
+            // whose [Driver].entrypoint carries a dotnet:/netfx: scheme 
prefix.
             string typeName = typeof(FakeAdbcDriver).FullName!;
             string assemblyPath = typeof(FakeAdbcDriver).Assembly.Location;
 
diff --git 
a/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/DriverManifestTests.cs 
b/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/DriverManifestTests.cs
new file mode 100644
index 000000000..db1c51938
--- /dev/null
+++ b/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/DriverManifestTests.cs
@@ -0,0 +1,320 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.InteropServices;
+using Apache.Arrow.Adbc.DriverManager;
+using Xunit;
+
+namespace Apache.Arrow.Adbc.Tests.DriverManager
+{
+    /// <summary>
+    /// Tests for <see cref="DriverManifest"/>. These cover the format 
documented
+    /// in <c>docs/source/format/driver_manifests.rst</c>: the
+    /// <c>manifest_version</c>, the optional metadata fields, and the two
+    /// shapes of <c>[Driver.shared]</c> (single string vs. platform table).
+    /// Also includes a real-driver round-trip against DuckDB that originally
+    /// reproduced https://github.com/apache/arrow-adbc/issues/4329.
+    /// </summary>
+    [Collection(DriverManagerSecurityCollection.Name)]
+    public class DriverManifestTests : IDisposable
+    {
+        private readonly List<string> _tempDirs = new List<string>();
+
+        public void Dispose()
+        {
+            foreach (string d in _tempDirs)
+            {
+                try { if (Directory.Exists(d)) Directory.Delete(d, true); } 
catch { }
+            }
+        }
+
+        // 
-----------------------------------------------------------------------
+        // [Driver.shared] in single-string form
+        // 
-----------------------------------------------------------------------
+
+        [Fact]
+        public void LoadFromContent_DriverSharedAsString_ReturnsThatPath()
+        {
+            const string toml = @"
+manifest_version = 1
+name = ""Example""
+
+[Driver]
+shared = ""/usr/local/lib/libadbc_driver_example.so""
+";
+            DriverManifest manifest = DriverManifest.LoadFromContent(toml);
+            Assert.Equal("/usr/local/lib/libadbc_driver_example.so", 
manifest.LibraryPath);
+            Assert.Equal("Example", manifest.Name);
+            Assert.Equal(1, manifest.ManifestVersion);
+            Assert.Null(manifest.Entrypoint);
+        }
+
+        // 
-----------------------------------------------------------------------
+        // [Driver.shared] as a platform-tuple table
+        // 
-----------------------------------------------------------------------
+
+        [Fact]
+        public void 
LoadFromContent_DriverSharedAsPlatformTable_PicksCurrentPlatform()
+        {
+            string current = DriverManifest.GetCurrentPlatformTuple();
+            string toml =
+                "manifest_version = 1\n" +
+                "\n[Driver.shared]\n" +
+                current + " = \"/path/for/this/platform\"\n" +
+                "irrelevant_platform = \"/should/not/match\"\n";
+
+            DriverManifest manifest = DriverManifest.LoadFromContent(toml);
+            Assert.Equal("/path/for/this/platform", manifest.LibraryPath);
+        }
+
+        [Fact]
+        public void LoadFromContent_NoMatchingPlatform_ThrowsNotFound()
+        {
+            // Build a table that intentionally excludes the current platform.
+            const string toml = @"
+manifest_version = 1
+
+[Driver.shared]
+made_up_os_made_up_arch = ""/nowhere""
+";
+            AdbcException ex = Assert.Throws<AdbcException>(
+                () => DriverManifest.LoadFromContent(toml));
+            Assert.Equal(AdbcStatusCode.NotFound, ex.Status);
+            // Error message should mention the current platform tuple
+            Assert.Contains(DriverManifest.GetCurrentPlatformTuple(), 
ex.Message);
+        }
+
+        // 
-----------------------------------------------------------------------
+        // Metadata fields
+        // 
-----------------------------------------------------------------------
+
+        [Fact]
+        public void LoadFromContent_ReadsAllMetadataFields()
+        {
+            // Mixes single- and double-quoted strings to exercise both forms.
+            const string toml = @"
+manifest_version = 1
+
+name = ""Display Name""
+version = '1.5.2'
+publisher = 'ExampleCo'
+license = 'Apache-2.0'
+source = 'pkg-manager'
+
+[Driver]
+entrypoint = ""AdbcDriverExampleInit""
+shared = ""/path/lib.so""
+";
+            DriverManifest manifest = DriverManifest.LoadFromContent(toml);
+            Assert.Equal("Display Name", manifest.Name);
+            Assert.Equal("1.5.2", manifest.Version);
+            Assert.Equal("ExampleCo", manifest.Publisher);
+            Assert.Equal("Apache-2.0", manifest.License);
+            Assert.Equal("pkg-manager", manifest.Source);
+            Assert.Equal("AdbcDriverExampleInit", manifest.Entrypoint);
+        }
+
+        // 
-----------------------------------------------------------------------
+        // manifest_version defaulting + validation
+        // 
-----------------------------------------------------------------------
+
+        [Fact]
+        public void LoadFromContent_NoManifestVersion_DefaultsTo1()
+        {
+            const string toml = @"
+[Driver]
+shared = ""/path/lib.so""
+";
+            DriverManifest manifest = DriverManifest.LoadFromContent(toml);
+            Assert.Equal(1, manifest.ManifestVersion);
+        }
+
+        [Fact]
+        public void LoadFromContent_ManifestVersion2_ThrowsNotImplemented()
+        {
+            const string toml = @"
+manifest_version = 2
+
+[Driver]
+shared = ""/path/lib.so""
+";
+            AdbcException ex = Assert.Throws<AdbcException>(
+                () => DriverManifest.LoadFromContent(toml));
+            Assert.Equal(AdbcStatusCode.NotImplemented, ex.Status);
+        }
+
+        [Fact]
+        public void 
LoadFromContent_ManifestVersionAsString_ThrowsInvalidArgument()
+        {
+            // Despite the docs example showing 'version = "1.5.2"' (which is 
the
+            // driver's own version), manifest_version itself must be an 
integer.
+            const string toml = @"
+manifest_version = ""1""
+
+[Driver]
+shared = ""/path/lib.so""
+";
+            AdbcException ex = Assert.Throws<AdbcException>(
+                () => DriverManifest.LoadFromContent(toml));
+            Assert.Equal(AdbcStatusCode.InvalidArgument, ex.Status);
+        }
+
+        // 
-----------------------------------------------------------------------
+        // Missing Driver.shared
+        // 
-----------------------------------------------------------------------
+
+        [Fact]
+        public void LoadFromContent_NoLibraryPath_ThrowsInvalidArgument()
+        {
+            const string toml = @"
+manifest_version = 1
+name = ""Example""
+
+[Driver]
+entrypoint = ""AdbcDriverInit""
+";
+            AdbcException ex = Assert.Throws<AdbcException>(
+                () => DriverManifest.LoadFromContent(toml));
+            Assert.Equal(AdbcStatusCode.InvalidArgument, ex.Status);
+        }
+
+        [Fact]
+        public void 
LoadFromContent_EmptyDriverSharedString_ThrowsInvalidArgument()
+        {
+            const string toml = @"
+manifest_version = 1
+
+[Driver]
+shared = """"
+";
+            AdbcException ex = Assert.Throws<AdbcException>(
+                () => DriverManifest.LoadFromContent(toml));
+            Assert.Equal(AdbcStatusCode.InvalidArgument, ex.Status);
+        }
+
+        // 
-----------------------------------------------------------------------
+        // Malformed TOML wraps to AdbcException
+        // 
-----------------------------------------------------------------------
+
+        [Fact]
+        public void LoadFromContent_MalformedToml_ThrowsAdbcException()
+        {
+            // Unterminated string -> FormatException out of TomlParser, 
wrapped.
+            const string toml = "manifest_version = 1\n[Driver]\nshared = 
\"unterminated\n";
+            AdbcException ex = Assert.Throws<AdbcException>(
+                () => DriverManifest.LoadFromContent(toml));
+            Assert.Equal(AdbcStatusCode.InvalidArgument, ex.Status);
+        }
+
+        // 
-----------------------------------------------------------------------
+        // Round-trip with the literal-string form used in the docs example
+        // 
-----------------------------------------------------------------------
+
+        [Fact]
+        public void LoadFromContent_DocsExampleStyle_Parses()
+        {
+            // Mirrors docs/source/cpp/recipe_driver/driver_example.toml.in
+            // (which uses single-quoted strings throughout).
+            string current = DriverManifest.GetCurrentPlatformTuple();
+            string toml =
+                "manifest_version = 1\n" +
+                "\n" +
+                "name = 'Driver Example'\n" +
+                "publisher = 'arrow-adbc-docs'\n" +
+                "license = 'Apache-2.0'\n" +
+                "version = '1.0.0'\n" +
+                "source = 'recipe'\n" +
+                "\n" +
+                "[ADBC]\n" +
+                "version = 'v1.1.0'\n" +
+                "\n" +
+                "[Driver]\n" +
+                "[Driver.shared]\n" +
+                current + " = '/opt/adbc/libadbc_driver_example.so'\n";
+
+            DriverManifest manifest = DriverManifest.LoadFromContent(toml);
+            Assert.Equal("Driver Example", manifest.Name);
+            Assert.Equal("1.0.0", manifest.Version);
+            Assert.Equal("/opt/adbc/libadbc_driver_example.so", 
manifest.LibraryPath);
+        }
+
+        // 
-----------------------------------------------------------------------
+        // End-to-end against a real driver (DuckDB). Originally written as the
+        // regression test for 
https://github.com/apache/arrow-adbc/issues/4329:
+        // FindLoadDriver was routing real driver manifests through the
+        // connection-profile loader, which rejected the spec-correct
+        // `version = "1.5.2"` field as a non-integer.
+        // 
-----------------------------------------------------------------------
+
+        [Fact]
+        public void FindLoadDriver_WithRealDriverManifest_LoadsDuckDbDriver()
+        {
+            // Locate the DuckDB native library copied next to the test 
assembly
+            // by the CopyDuckDb MSBuild target (see 
Apache.Arrow.Adbc.Testing.csproj).
+            string root = Directory.GetCurrentDirectory();
+            string duckdbFile;
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+                duckdbFile = Path.Combine(root, "duckdb.dll");
+            else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+                duckdbFile = Path.Combine(root, "libduckdb.dylib");
+            else
+                duckdbFile = Path.Combine(root, "libduckdb.so");
+
+            Assert.True(File.Exists(duckdbFile), $"DuckDB library missing at 
{duckdbFile}");
+
+            string current = DriverManifest.GetCurrentPlatformTuple();
+            // TOML basic strings interpret \ as an escape, so backslashes in
+            // Windows paths must be doubled.
+            string tomlEscapedPath = duckdbFile.Replace("\\", "\\\\");
+
+            string manifest =
+                "manifest_version = 1\n" +
+                "\n" +
+                "name = \"DuckDB\"\n" +
+                "version = \"1.5.2\"        # driver version - a string per 
the spec\n" +
+                "publisher = \"duckdb.org\"\n" +
+                "license = \"MIT\"\n" +
+                "\n" +
+                "[ADBC]\n" +
+                "version = \"1.1.0\"\n" +
+                "\n" +
+                "[Driver]\n" +
+                "entrypoint = \"duckdb_adbc_init\"\n" +
+                "\n" +
+                "[Driver.shared]\n" +
+                current + " = \"" + tomlEscapedPath + "\"\n";
+
+            string tempDir = Path.Combine(Path.GetTempPath(), 
Guid.NewGuid().ToString("N"));
+            Directory.CreateDirectory(tempDir);
+            _tempDirs.Add(tempDir);
+
+            string manifestPath = Path.Combine(tempDir, "duckdb.toml");
+            File.WriteAllText(manifestPath, manifest);
+
+            using AdbcDriver driver = AdbcDriverManager.FindLoadDriver(
+                "duckdb",
+                entrypoint: "duckdb_adbc_init",
+                loadOptions: AdbcLoadFlags.Default,
+                additionalSearchPathList: tempDir);
+
+            Assert.NotNull(driver);
+        }
+    }
+}
diff --git 
a/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/EntrypointSchemeTests.cs 
b/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/EntrypointSchemeTests.cs
new file mode 100644
index 000000000..aafd99e6b
--- /dev/null
+++ b/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/EntrypointSchemeTests.cs
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using Apache.Arrow.Adbc.DriverManager;
+using Xunit;
+
+namespace Apache.Arrow.Adbc.Tests.DriverManager
+{
+    /// <summary>
+    /// Tests for the entrypoint-scheme dispatch in <see 
cref="AdbcDriverManager"/>:
+    /// <c>dotnet:</c> and <c>netfx:</c> prefixes select the managed loader and
+    /// fail closed when the host process runs on the wrong .NET runtime.
+    /// </summary>
+    [Collection(DriverManagerSecurityCollection.Name)]
+    public class EntrypointSchemeTests : IDisposable
+    {
+        private readonly List<string> _tempDirs = new List<string>();
+
+        public void Dispose()
+        {
+            foreach (string d in _tempDirs)
+            {
+                try { if (Directory.Exists(d)) Directory.Delete(d, true); } 
catch { }
+            }
+        }
+
+        /// <summary>The "other" managed scheme for this test process (the one 
the host can't load).</summary>
+        private static string ForeignScheme =>
+#if NETFRAMEWORK
+            "dotnet:";
+#else
+            "netfx:";
+#endif
+
+        /// <summary>The matching managed scheme for this test 
process.</summary>
+        private static string NativeScheme =>
+#if NETFRAMEWORK
+            "netfx:";
+#else
+            "dotnet:";
+#endif
+
+        private string CreateManifest(string entrypoint, string 
assemblyFileName)
+        {
+            string tempDir = Path.Combine(Path.GetTempPath(), 
Guid.NewGuid().ToString("N"));
+            Directory.CreateDirectory(tempDir);
+            _tempDirs.Add(tempDir);
+
+            string realAssemblyPath = typeof(FakeAdbcDriver).Assembly.Location;
+            File.Copy(realAssemblyPath, Path.Combine(tempDir, 
assemblyFileName), overwrite: true);
+
+            string toml = "manifest_version = 1\n"
+                + "\n[Driver]\n"
+                + "entrypoint = \"" + entrypoint + "\"\n"
+                + "shared = \"" + assemblyFileName + "\"\n";
+
+            string tomlPath = Path.Combine(tempDir, "scheme_test.toml");
+            File.WriteAllText(tomlPath, toml);
+            return tomlPath;
+        }
+
+        [Fact]
+        public void Manifest_ForeignScheme_FailsClosedWithClearError()
+        {
+            // A dotnet: manifest under .NET Framework (or netfx: under modern 
.NET)
+            // should fail before any reflection happens, with a message that 
names
+            // the requested scheme.
+            string assemblyFileName = 
Path.GetFileName(typeof(FakeAdbcDriver).Assembly.Location);
+            string typeName = typeof(FakeAdbcDriver).FullName!;
+            string entrypoint = ForeignScheme + typeName;
+            string tomlPath = CreateManifest(entrypoint, assemblyFileName);
+
+            AdbcException ex = Assert.Throws<AdbcException>(
+                () => AdbcDriverManager.FindLoadDriver(tomlPath));
+            Assert.Equal(AdbcStatusCode.NotImplemented, ex.Status);
+            Assert.Contains(ForeignScheme.TrimEnd(':'), ex.Message);
+        }
+
+        [Fact]
+        public void Manifest_NativeScheme_LoadsManagedDriver()
+        {
+            // Sanity check: the matching scheme on the same code path does 
load.
+            string assemblyFileName = 
Path.GetFileName(typeof(FakeAdbcDriver).Assembly.Location);
+            string typeName = typeof(FakeAdbcDriver).FullName!;
+            string entrypoint = NativeScheme + typeName;
+            string tomlPath = CreateManifest(entrypoint, assemblyFileName);
+
+            AdbcDriver driver = AdbcDriverManager.FindLoadDriver(tomlPath);
+            Assert.NotNull(driver);
+            Assert.Equal(typeName, driver.GetType().FullName);
+        }
+
+        [Fact]
+        public void LoadDriver_ExplicitForeignSchemeEntrypoint_FailsClosed()
+        {
+            // The dispatch fires even without a manifest: an explicit 
caller-supplied
+            // entrypoint with a foreign scheme prefix is rejected on this 
runtime.
+            string realAssemblyPath = typeof(FakeAdbcDriver).Assembly.Location;
+            string typeName = typeof(FakeAdbcDriver).FullName!;
+
+            AdbcException ex = Assert.Throws<AdbcException>(
+                () => AdbcDriverManager.LoadDriver(realAssemblyPath, 
ForeignScheme + typeName));
+            Assert.Equal(AdbcStatusCode.NotImplemented, ex.Status);
+        }
+    }
+}
diff --git 
a/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/TomlConnectionProfileTests.cs
 
b/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/TomlConnectionProfileTests.cs
index a6a0f35dc..c811b47ec 100644
--- 
a/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/TomlConnectionProfileTests.cs
+++ 
b/csharp/test/Apache.Arrow.Adbc.Tests/DriverManager/TomlConnectionProfileTests.cs
@@ -292,7 +292,7 @@ version = 1
 driver = ""d""
 
 [options]
-password = ""env_var(ADBC_TEST_PASSWORD_TOML)""
+password = ""{{ env_var(ADBC_TEST_PASSWORD_TOML) }}""
 plain = ""notanenvvar""
 ";
                 ConnectionProfile profile = 
FilesystemProfileProvider.LoadFromContent(toml).ResolveEnvVars();
@@ -307,7 +307,7 @@ plain = ""notanenvvar""
         }
 
         [Fact]
-        public void ResolveEnvVars_NoEnvVarValues_ReturnsSameProfile()
+        public void ResolveEnvVars_NoPlaceholders_ReturnsSameValues()
         {
             const string toml = @"
 version = 1
@@ -320,6 +320,101 @@ key = ""value""
             Assert.Equal("value", profile.StringOptions["key"]);
         }
 
+        [Fact]
+        public void 
ResolveEnvVars_PlaceholderEmbeddedInString_ExpandedInPlace()
+        {
+            // Per spec: placeholders may appear anywhere inside a value.
+            const string varName = "ADBC_TEST_EMBEDDED_HOST";
+            Environment.SetEnvironmentVariable(varName, "prod.example.com");
+            try
+            {
+                const string toml = @"
+version = 1
+driver = ""d""
+
+[options]
+uri = ""postgres://user@{{ env_var(ADBC_TEST_EMBEDDED_HOST) }}:5432/db""
+";
+                ConnectionProfile profile = 
FilesystemProfileProvider.LoadFromContent(toml).ResolveEnvVars();
+                Assert.Equal("postgres://[email protected]:5432/db", 
profile.StringOptions["uri"]);
+            }
+            finally
+            {
+                Environment.SetEnvironmentVariable(varName, null);
+            }
+        }
+
+        [Fact]
+        public void ResolveEnvVars_MultiplePlaceholdersInOneValue_AllExpanded()
+        {
+            const string hostVar = "ADBC_TEST_MULTI_HOST";
+            const string portVar = "ADBC_TEST_MULTI_PORT";
+            Environment.SetEnvironmentVariable(hostVar, "db.local");
+            Environment.SetEnvironmentVariable(portVar, "5433");
+            try
+            {
+                const string toml = @"
+version = 1
+driver = ""d""
+
+[options]
+uri = ""postgres://{{ env_var(ADBC_TEST_MULTI_HOST) }}:{{ 
env_var(ADBC_TEST_MULTI_PORT) }}/db""
+";
+                ConnectionProfile profile = 
FilesystemProfileProvider.LoadFromContent(toml).ResolveEnvVars();
+                Assert.Equal("postgres://db.local:5433/db", 
profile.StringOptions["uri"]);
+            }
+            finally
+            {
+                Environment.SetEnvironmentVariable(hostVar, null);
+                Environment.SetEnvironmentVariable(portVar, null);
+            }
+        }
+
+        [Fact]
+        public void 
ResolveEnvVars_WhitespaceVariationsInsidePlaceholder_AllAccepted()
+        {
+            const string varName = "ADBC_TEST_WS_VAR";
+            Environment.SetEnvironmentVariable(varName, "X");
+            try
+            {
+                // No whitespace, lots of whitespace, asymmetric whitespace -- 
all valid.
+                const string toml = @"
+version = 1
+driver = ""d""
+
+[options]
+tight = ""{{env_var(ADBC_TEST_WS_VAR)}}""
+loose = ""{{    env_var(ADBC_TEST_WS_VAR)    }}""
+asymmetric = ""{{ env_var(ADBC_TEST_WS_VAR)}}""
+";
+                ConnectionProfile profile = 
FilesystemProfileProvider.LoadFromContent(toml).ResolveEnvVars();
+                Assert.Equal("X", profile.StringOptions["tight"]);
+                Assert.Equal("X", profile.StringOptions["loose"]);
+                Assert.Equal("X", profile.StringOptions["asymmetric"]);
+            }
+            finally
+            {
+                Environment.SetEnvironmentVariable(varName, null);
+            }
+        }
+
+        [Fact]
+        public void 
ResolveEnvVars_BareEnvVarSyntax_NotInterpretedAsPlaceholder()
+        {
+            // The old (pre-spec) C# implementation treated a whole-value 
'env_var(NAME)'
+            // as a placeholder. The spec requires '{{ }}' delimiters, so the 
bare form
+            // is now a literal string.
+            const string toml = @"
+version = 1
+driver = ""d""
+
+[options]
+literal = ""env_var(NOT_A_PLACEHOLDER)""
+";
+            ConnectionProfile profile = 
FilesystemProfileProvider.LoadFromContent(toml).ResolveEnvVars();
+            Assert.Equal("env_var(NOT_A_PLACEHOLDER)", 
profile.StringOptions["literal"]);
+        }
+
         // 
-----------------------------------------------------------------------
         // Positive: AdbcDriverManager DeriveEntrypoint
         // 
-----------------------------------------------------------------------
@@ -438,12 +533,15 @@ driver = ""mydriver""
         }
 
         // 
-----------------------------------------------------------------------
-        // Negative: env_var expansion – variable not set
+        // env_var expansion – variable not set
         // 
-----------------------------------------------------------------------
 
         [Fact]
-        public void ResolveEnvVars_MissingEnvVar_ThrowsAdbcException()
+        public void ResolveEnvVars_MissingEnvVar_ExpandsToEmptyString()
         {
+            // Per spec (and matching the C/C++ driver manager): a missing env 
var
+            // expands to "" and processing of the rest of the value continues.
+            // Example from the spec: "foo{{ env_var(MISSING) }}bar" -> 
"foobar".
             const string varName = "ADBC_TEST_DEFINITELY_NOT_SET_XYZ";
             Environment.SetEnvironmentVariable(varName, null);
 
@@ -452,12 +550,61 @@ version = 1
 driver = ""d""
 
 [options]
-password = ""env_var(ADBC_TEST_DEFINITELY_NOT_SET_XYZ)""
+password = ""{{ env_var(ADBC_TEST_DEFINITELY_NOT_SET_XYZ) }}""
+greeting = ""foo{{ env_var(ADBC_TEST_DEFINITELY_NOT_SET_XYZ) }}bar""
+";
+            ConnectionProfile profile = 
FilesystemProfileProvider.LoadFromContent(toml).ResolveEnvVars();
+            Assert.Equal("", profile.StringOptions["password"]);
+            Assert.Equal("foobar", profile.StringOptions["greeting"]);
+        }
+
+        // 
-----------------------------------------------------------------------
+        // Negative: malformed / unsupported placeholders
+        // 
-----------------------------------------------------------------------
+
+        [Fact]
+        public void ResolveEnvVars_UnsupportedFunction_ThrowsInvalidArgument()
+        {
+            const string toml = @"
+version = 1
+driver = ""d""
+
+[options]
+weird = ""{{ unknown_func(FOO) }}""
+";
+            ConnectionProfile profile = 
FilesystemProfileProvider.LoadFromContent(toml);
+            AdbcException ex = Assert.Throws<AdbcException>(() => 
profile.ResolveEnvVars());
+            Assert.Equal(AdbcStatusCode.InvalidArgument, ex.Status);
+        }
+
+        [Fact]
+        public void ResolveEnvVars_MissingClosingParen_ThrowsInvalidArgument()
+        {
+            const string toml = @"
+version = 1
+driver = ""d""
+
+[options]
+oops = ""{{ env_var(FOO }}""
 ";
             ConnectionProfile profile = 
FilesystemProfileProvider.LoadFromContent(toml);
             AdbcException ex = Assert.Throws<AdbcException>(() => 
profile.ResolveEnvVars());
-            Assert.Equal(AdbcStatusCode.InvalidState, ex.Status);
-            Assert.Contains(varName, ex.Message);
+            Assert.Equal(AdbcStatusCode.InvalidArgument, ex.Status);
+        }
+
+        [Fact]
+        public void ResolveEnvVars_EmptyVarName_ThrowsInvalidArgument()
+        {
+            const string toml = @"
+version = 1
+driver = ""d""
+
+[options]
+oops = ""{{ env_var() }}""
+";
+            ConnectionProfile profile = 
FilesystemProfileProvider.LoadFromContent(toml);
+            AdbcException ex = Assert.Throws<AdbcException>(() => 
profile.ResolveEnvVars());
+            Assert.Equal(AdbcStatusCode.InvalidArgument, ex.Status);
         }
 
         // 
-----------------------------------------------------------------------
@@ -606,34 +753,6 @@ driver = ""abs_driver""
             Assert.Equal("abs_driver", profile!.DriverName);
         }
 
-        // 
-----------------------------------------------------------------------
-        // Positive: driver_type field in TOML profile
-        // 
-----------------------------------------------------------------------
-
-        [Fact]
-        public void ParseProfile_WithDriverType_ParsedCorrectly()
-        {
-            const string toml = @"
-version = 1
-driver = ""Apache.Arrow.Adbc.Tests.dll""
-driver_type = ""Apache.Arrow.Adbc.Tests.DriverManager.FakeAdbcDriver""
-";
-            ConnectionProfile profile = 
FilesystemProfileProvider.LoadFromContent(toml);
-            Assert.Equal("Apache.Arrow.Adbc.Tests.dll", profile.DriverName);
-            
Assert.Equal("Apache.Arrow.Adbc.Tests.DriverManager.FakeAdbcDriver", 
profile.DriverTypeName);
-        }
-
-        [Fact]
-        public void ParseProfile_WithoutDriverType_DriverTypeNameIsNull()
-        {
-            const string toml = @"
-version = 1
-driver = ""mydriver""
-";
-            ConnectionProfile profile = 
FilesystemProfileProvider.LoadFromContent(toml);
-            Assert.Null(profile.DriverTypeName);
-        }
-
         // 
-----------------------------------------------------------------------
         // Positive: LoadManagedDriver loads a managed .NET driver by 
reflection
         // 
-----------------------------------------------------------------------
@@ -692,35 +811,50 @@ driver = ""d""
 
         // 
-----------------------------------------------------------------------
         // Positive: OpenDatabaseFromProfile end-to-end with managed driver
+        //
+        // Managed drivers are selected by a scheme-prefixed 'entrypoint' 
option:
+        // dotnet:Type for modern .NET, netfx:Type for .NET Framework. The 
driver
+        // manager consumes the entrypoint option before opening the database.
         // 
-----------------------------------------------------------------------
 
+        /// <summary>
+        /// Scheme prefix this test process must use when selecting managed 
drivers
+        /// -- a dotnet: entrypoint on .NET Framework (or vice versa) is a 
runtime
+        /// mismatch that the driver manager intentionally rejects.
+        /// </summary>
+        private static string ManagedScheme =>
+#if NETFRAMEWORK
+            "netfx:";
+#else
+            "dotnet:";
+#endif
+
         [Fact]
         public void OpenDatabaseFromProfile_ManagedDriver_OpensDatabase()
         {
             string assemblyPath = typeof(FakeAdbcDriver).Assembly.Location;
             string typeName = typeof(FakeAdbcDriver).FullName!;
 
-            // Build TOML content; escape any backslashes in the Windows 
assembly path.
             string escapedPath = assemblyPath.Replace("\\", "\\\\");
-            string toml = "version = 1\n"
+            string toml = "profile_version = 1\n"
                 + "driver = \"" + escapedPath + "\"\n"
-                + "driver_type = \"" + typeName + "\"\n"
-                + "\n[options]\n"
+                + "\n[Options]\n"
+                + "entrypoint = \"" + ManagedScheme + typeName + "\"\n"
                 + "project_id = \"my-project\"\n"
                 + "region = \"us-east1\"\n";
 
             ConnectionProfile profile = 
FilesystemProfileProvider.LoadFromContent(toml);
             AdbcDatabase db = 
AdbcDriverManager.OpenDatabaseFromProfile(profile);
 
-            // Use type name comparison to avoid assembly identity issues when 
loaded via Assembly.LoadFrom
             
Assert.Equal("Apache.Arrow.Adbc.Tests.DriverManager.FakeAdbcDatabase", 
db.GetType().FullName);
 
-            // Access parameters via reflection since the type identity differs
             System.Reflection.PropertyInfo? paramsProp = 
db.GetType().GetProperty("Parameters");
             Assert.NotNull(paramsProp);
             IReadOnlyDictionary<string, string> parameters = 
(IReadOnlyDictionary<string, string>)paramsProp!.GetValue(db)!;
             Assert.Equal("my-project", parameters["project_id"]);
             Assert.Equal("us-east1", parameters["region"]);
+            // entrypoint is consumed by the driver manager, not forwarded to 
the driver
+            Assert.False(parameters.ContainsKey("entrypoint"));
         }
 
         // 
-----------------------------------------------------------------------
@@ -776,14 +910,13 @@ driver = ""d""
         }
 
         [Fact]
-        public void 
OpenDatabaseFromProfile_ManagedDriverMissingAssemblyPath_ThrowsAdbcException()
+        public void OpenDatabaseFromProfile_NoDriver_ThrowsAdbcException()
         {
-            // driver_type is set but the driver (assembly path) field is 
omitted.
+            // A profile with no 'driver' field cannot be opened on its own.
             const string toml = @"
-version = 1
-driver_type = ""Apache.Arrow.Adbc.Tests.DriverManager.FakeAdbcDriver""
+profile_version = 1
 
-[options]
+[Options]
 key = ""value""
 ";
             ConnectionProfile profile = 
FilesystemProfileProvider.LoadFromContent(toml);
@@ -876,11 +1009,11 @@ key = ""value""
         }
 
         // 
-----------------------------------------------------------------------
-        // ResolveEnvVars preserves DriverTypeName
+        // ResolveEnvVars preserves the driver reference and other non-env 
values
         // 
-----------------------------------------------------------------------
 
         [Fact]
-        public void ResolveEnvVars_DriverTypeNameIsPreserved()
+        public void ResolveEnvVars_DriverNameIsPreserved()
         {
             const string varName = "ADBC_TEST_RESOLVE_ENVVAR_HOST";
             Environment.SetEnvironmentVariable(varName, "myhost");
@@ -889,13 +1022,12 @@ key = ""value""
                 const string toml = @"
 version = 1
 driver = ""MyDriver.dll""
-driver_type = ""My.Namespace.MyDriver""
 
 [options]
-host = ""env_var(ADBC_TEST_RESOLVE_ENVVAR_HOST)""
+host = ""{{ env_var(ADBC_TEST_RESOLVE_ENVVAR_HOST) }}""
 ";
                 ConnectionProfile resolved = 
FilesystemProfileProvider.LoadFromContent(toml).ResolveEnvVars();
-                Assert.Equal("My.Namespace.MyDriver", resolved.DriverTypeName);
+                Assert.Equal("MyDriver.dll", resolved.DriverName);
                 Assert.Equal("myhost", resolved.StringOptions["host"]);
             }
             finally
@@ -915,20 +1047,18 @@ host = ""env_var(ADBC_TEST_RESOLVE_ENVVAR_HOST)""
             string typeName = typeof(FakeAdbcDriver).FullName!;
 
             string escapedPath = assemblyPath.Replace("\\", "\\\\");
-            string toml = "version = 1\n"
+            string toml = "profile_version = 1\n"
                 + "driver = \"" + escapedPath + "\"\n"
-                + "driver_type = \"" + typeName + "\"\n"
-                + "\n[options]\n"
+                + "\n[Options]\n"
+                + "entrypoint = \"" + ManagedScheme + typeName + "\"\n"
                 + "known_key = \"hello\"\n"
                 + "unknown_widget = \"ignored_by_driver\"\n";
 
             ConnectionProfile profile = 
FilesystemProfileProvider.LoadFromContent(toml);
             AdbcDatabase db = 
AdbcDriverManager.OpenDatabaseFromProfile(profile);
 
-            // Use type name comparison to avoid assembly identity issues
             
Assert.Equal("Apache.Arrow.Adbc.Tests.DriverManager.FakeAdbcDatabase", 
db.GetType().FullName);
 
-            // Access parameters via reflection since the type identity differs
             System.Reflection.PropertyInfo? paramsProp = 
db.GetType().GetProperty("Parameters");
             Assert.NotNull(paramsProp);
             IReadOnlyDictionary<string, string> parameters = 
(IReadOnlyDictionary<string, string>)paramsProp!.GetValue(db)!;
@@ -1051,10 +1181,10 @@ bool_key = false
             string typeName = typeof(FakeAdbcDriver).FullName!;
 
             string escapedPath = assemblyPath.Replace("\\", "\\\\");
-            string toml = "version = 1\n"
+            string toml = "profile_version = 1\n"
                 + "driver = \"" + escapedPath + "\"\n"
-                + "driver_type = \"" + typeName + "\"\n"
-                + "\n[options]\n"
+                + "\n[Options]\n"
+                + "entrypoint = \"" + ManagedScheme + typeName + "\"\n"
                 + "profile_option = \"from_profile\"\n"
                 + "shared_option = \"profile_value\"\n";
 
@@ -1068,21 +1198,14 @@ bool_key = false
 
             AdbcDatabase db = 
AdbcDriverManager.OpenDatabaseFromProfile(profile, explicitOptions);
 
-            // Use type name comparison to avoid assembly identity issues
             
Assert.Equal("Apache.Arrow.Adbc.Tests.DriverManager.FakeAdbcDatabase", 
db.GetType().FullName);
 
-            // Access parameters via reflection since the type identity differs
             System.Reflection.PropertyInfo? paramsProp = 
db.GetType().GetProperty("Parameters");
             Assert.NotNull(paramsProp);
             IReadOnlyDictionary<string, string> parameters = 
(IReadOnlyDictionary<string, string>)paramsProp!.GetValue(db)!;
 
-            // Profile-only option should be present
             Assert.Equal("from_profile", parameters["profile_option"]);
-
-            // Explicit-only option should be present
             Assert.Equal("from_explicit", parameters["explicit_option"]);
-
-            // Shared option: explicit should override profile
             Assert.Equal("explicit_value", parameters["shared_option"]);
         }
 
@@ -1093,19 +1216,17 @@ bool_key = false
             string typeName = typeof(FakeAdbcDriver).FullName!;
 
             string escapedPath = assemblyPath.Replace("\\", "\\\\");
-            string toml = "version = 1\n"
+            string toml = "profile_version = 1\n"
                 + "driver = \"" + escapedPath + "\"\n"
-                + "driver_type = \"" + typeName + "\"\n"
-                + "\n[options]\n"
+                + "\n[Options]\n"
+                + "entrypoint = \"" + ManagedScheme + typeName + "\"\n"
                 + "key = \"value\"\n";
 
             ConnectionProfile profile = 
FilesystemProfileProvider.LoadFromContent(toml);
             AdbcDatabase db = 
AdbcDriverManager.OpenDatabaseFromProfile(profile, null);
 
-            // Use type name comparison to avoid assembly identity issues
             
Assert.Equal("Apache.Arrow.Adbc.Tests.DriverManager.FakeAdbcDatabase", 
db.GetType().FullName);
 
-            // Access parameters via reflection since the type identity differs
             System.Reflection.PropertyInfo? paramsProp = 
db.GetType().GetProperty("Parameters");
             Assert.NotNull(paramsProp);
             IReadOnlyDictionary<string, string> parameters = 
(IReadOnlyDictionary<string, string>)paramsProp!.GetValue(db)!;

Reply via email to