diff --git a/csharp/src/Microsoft.ML.OnnxRuntime/NativeMethods.shared.cs b/csharp/src/Microsoft.ML.OnnxRuntime/NativeMethods.shared.cs
index a6b267c6802cf..81d4f2589151b 100644
--- a/csharp/src/Microsoft.ML.OnnxRuntime/NativeMethods.shared.cs
+++ b/csharp/src/Microsoft.ML.OnnxRuntime/NativeMethods.shared.cs
@@ -476,9 +476,25 @@ internal static class NativeMethods
static NativeMethods()
{
#if !NETSTANDARD2_0 && !__ANDROID__ && !__IOS__
- // Register a custom DllImportResolver to handle platform-specific library loading.
- // Replaces default resolution specifically on Windows for case-sensitivity.
- NativeLibrary.SetDllImportResolver(typeof(NativeMethods).Assembly, DllImportResolver);
+ if (!OrtEnv.DisableDllImportResolver)
+ {
+ try
+ {
+ // Register a custom DllImportResolver to handle platform-specific library loading.
+ // Replaces default resolution specifically on Windows for case-sensitivity.
+ NativeLibrary.SetDllImportResolver(typeof(NativeMethods).Assembly, DllImportResolver);
+ }
+ catch (InvalidOperationException)
+ {
+ // A resolver is already registered for this assembly (e.g., by the host application).
+ // This is not fatal — the host's resolver will handle library loading.
+ System.Diagnostics.Trace.WriteLine(
+ "[OnnxRuntime] A DllImportResolver is already registered for this assembly. "
+ + "OnnxRuntime's built-in resolver will not be used. "
+ + "To suppress this message, set OrtEnv.DisableDllImportResolver = true "
+ + "before using any OnnxRuntime APIs.");
+ }
+ }
#endif
#if NETSTANDARD2_0
diff --git a/csharp/src/Microsoft.ML.OnnxRuntime/OrtEnv.shared.cs b/csharp/src/Microsoft.ML.OnnxRuntime/OrtEnv.shared.cs
index 6fcff438c5cf3..22f541e2207fa 100644
--- a/csharp/src/Microsoft.ML.OnnxRuntime/OrtEnv.shared.cs
+++ b/csharp/src/Microsoft.ML.OnnxRuntime/OrtEnv.shared.cs
@@ -12,7 +12,7 @@ namespace Microsoft.ML.OnnxRuntime
///
///
/// This enum is used to determine whether a pre-compiled model can be used with specific execution providers
- /// and devices, or if recompilation is needed.
+ /// and devices, or if recompilation is needed.
///
public enum OrtCompiledModelCompatibility
{
@@ -77,14 +77,14 @@ public struct EnvironmentCreationOptions
///
/// The singleton class OrtEnv contains the process-global ONNX Runtime environment.
/// It sets up logging, creates system wide thread-pools (if Thread Pool options are provided)
- /// and other necessary things for OnnxRuntime to function.
- ///
+ /// and other necessary things for OnnxRuntime to function.
+ ///
/// Create or access OrtEnv by calling the Instance() method. Instance() can be called multiple times.
/// It would return the same instance.
- ///
+ ///
/// CreateInstanceWithOptions() provides a way to create environment with options.
/// It must be called once before Instance() is called, otherwise it would not have effect.
- ///
+ ///
/// If the environment is not explicitly created, it will be created as needed, e.g.,
/// when creating a SessionOptions instance.
///
@@ -93,6 +93,28 @@ public sealed class OrtEnv : SafeHandle
#region Static members
private static readonly int ORT_PROJECTION_CSHARP = 2;
+ ///
+ /// Set this to true before accessing any OnnxRuntime type to prevent OnnxRuntime
+ /// from registering its own DllImportResolver via
+ /// NativeLibrary.SetDllImportResolver.
+ /// This is useful when the host application needs to register its own custom resolver
+ /// for the OnnxRuntime assembly. Must be set before any OnnxRuntime API is used
+ /// (i.e., before the internal NativeMethods static constructor runs).
+ ///
+ ///
+ ///
+ /// // Disable OnnxRuntime's built-in resolver before any ORT usage
+ /// OrtEnv.DisableDllImportResolver = true;
+ ///
+ /// // Register your own resolver
+ /// NativeLibrary.SetDllImportResolver(typeof(OrtEnv).Assembly, MyCustomResolver);
+ ///
+ /// // Now use OnnxRuntime normally
+ /// var env = OrtEnv.Instance();
+ ///
+ ///
+ public static bool DisableDllImportResolver { get; set; } = false;
+
private static readonly byte[] _defaultLogId = NativeOnnxValueHelper.StringToZeroTerminatedUtf8(@"CSharpOnnxRuntime");
// This must be static and set before the first creation call, otherwise, has no effect.
@@ -274,7 +296,7 @@ private static void SetLanguageProjection(OrtEnv env)
///
/// Instantiates (if not already done so) a new OrtEnv instance with the default logging level
/// and no other options. Otherwise returns the existing instance.
- ///
+ ///
/// It returns the same instance on every call - `OrtEnv` is singleton
///
/// Returns a singleton instance of OrtEnv that represents native OrtEnv object
@@ -523,7 +545,7 @@ public OrtLoggingLevel EnvLogLevel
/// A registered execution provider library can be used by all sessions created with the OrtEnv instance.
/// Devices the execution provider can utilize are added to the values returned by GetEpDevices() and can
/// be used in SessionOptions.AppendExecutionProvider to select an execution provider for a device.
- ///
+ ///
/// Coming: A selection policy can be specified and ORT will automatically select the best execution providers
/// and devices for the model.
///
diff --git a/csharp/test/Microsoft.ML.OnnxRuntime.Tests.Common/OrtEnvTests.cs b/csharp/test/Microsoft.ML.OnnxRuntime.Tests.Common/OrtEnvTests.cs
index aa1b683acd668..c298a95392317 100644
--- a/csharp/test/Microsoft.ML.OnnxRuntime.Tests.Common/OrtEnvTests.cs
+++ b/csharp/test/Microsoft.ML.OnnxRuntime.Tests.Common/OrtEnvTests.cs
@@ -532,4 +532,100 @@ public void TestDllImportResolverDoesNotThrow()
}
}
}
+
+#if !NETSTANDARD2_0
+ [Collection("Ort Inference Tests")]
+ public class OrtEnvExternalDllImportResolverTest
+ {
+ private System.Reflection.Assembly LoadIsolatedOnnxRuntimeAssembly(out System.Runtime.Loader.AssemblyLoadContext alc)
+ {
+ // Load a fresh copy of the ONNX Runtime assembly into a new AssemblyLoadContext.
+ // This guarantees we get a clean slate for static fields/constructors, avoiding
+ // interference from other xUnit tests that may have already initialized OrtEnv
+ // in the default context.
+ //
+ // Native library resolution (e.g., onnxruntime.dll) falls through to the default
+ // ALC when the isolated context cannot resolve it, so P/Invoke calls still work.
+ alc = new System.Runtime.Loader.AssemblyLoadContext("IsolatedORT_" + Guid.NewGuid(), isCollectible: true);
+ string asmPath = typeof(OrtEnv).Assembly.Location;
+ return alc.LoadFromAssemblyPath(asmPath);
+ }
+
+ ///
+ /// Verifies the scenario where an external caller registers a DllImportResolver FIRST,
+ /// and then OrtEnv is initialized. ORT's try/catch should handle the conflict gracefully.
+ ///
+ [Fact(DisplayName = "TestExternalResolverRegisteredFirst")]
+ public void TestExternalResolverRegisteredFirst()
+ {
+ var asm = LoadIsolatedOnnxRuntimeAssembly(out var alc);
+ try
+ {
+ // 1. External application registers its own resolver FIRST.
+ // Returning IntPtr.Zero means "not handled" — the runtime falls back to
+ // its default resolution logic, so native libraries still load normally.
+ NativeLibrary.SetDllImportResolver(asm, (libraryName, a, searchPath) => IntPtr.Zero);
+
+ // 2. ORT initializes (triggers NativeMethods static constructor).
+ // It will attempt to register its own resolver, which will throw
+ // InvalidOperationException internally, but the try/catch safety net
+ // prevents an unhandled TypeInitializationException.
+ var ortEnvType = asm.GetType("Microsoft.ML.OnnxRuntime.OrtEnv");
+ var instanceMethod = ortEnvType.GetMethod("Instance", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
+ var ortEnvInstance = instanceMethod.Invoke(null, null);
+ Assert.NotNull(ortEnvInstance);
+
+ // Verify ORT is fully functional despite the resolver conflict.
+ var getVersionMethod = ortEnvType.GetMethod("GetVersionString", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
+ var version = (string)getVersionMethod.Invoke(ortEnvInstance, null);
+ Assert.False(string.IsNullOrEmpty(version));
+ }
+ finally
+ {
+ alc.Unload();
+ }
+ }
+
+ ///
+ /// Verifies that setting DisableDllImportResolver = true BEFORE ORT initializes
+ /// successfully prevents ORT from registering its own resolver, leaving the assembly
+ /// free for the external application to register theirs LATER without throwing.
+ ///
+ [Fact(DisplayName = "TestDisableDllImportResolverWorks")]
+ public void TestDisableDllImportResolverWorks()
+ {
+ var asm = LoadIsolatedOnnxRuntimeAssembly(out var alc);
+ try
+ {
+ var ortEnvType = asm.GetType("Microsoft.ML.OnnxRuntime.OrtEnv");
+
+ // 1. Set OrtEnv.DisableDllImportResolver = true FIRST.
+ var disableProp = ortEnvType.GetProperty("DisableDllImportResolver", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
+ Assert.NotNull(disableProp);
+ disableProp.SetValue(null, true);
+
+ // 2. ORT initializes (triggers NativeMethods static constructor).
+ // It should respect the flag and SKIP calling NativeLibrary.SetDllImportResolver.
+ var instanceMethod = ortEnvType.GetMethod("Instance", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
+ var ortEnvInstance = instanceMethod.Invoke(null, null);
+ Assert.NotNull(ortEnvInstance);
+
+ // 3. External application registers its own resolver AFTER ORT initialized.
+ // If the flag works correctly, ORT skipped its own SetDllImportResolver call,
+ // so this registration should succeed without throwing InvalidOperationException.
+ // Returning IntPtr.Zero means "not handled" — falls back to default resolution.
+ var ex = Record.Exception(() =>
+ {
+ NativeLibrary.SetDllImportResolver(asm, (libraryName, a, searchPath) => IntPtr.Zero);
+ });
+
+ Assert.Null(ex); // No InvalidOperationException = ORT correctly skipped registration
+ }
+ finally
+ {
+ alc.Unload();
+ }
+ }
+ }
+#endif
}