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 }