Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions csharp/src/Microsoft.ML.OnnxRuntime/NativeMethods.shared.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 29 additions & 7 deletions csharp/src/Microsoft.ML.OnnxRuntime/OrtEnv.shared.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace Microsoft.ML.OnnxRuntime
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public enum OrtCompiledModelCompatibility
{
Expand Down Expand Up @@ -77,14 +77,14 @@ public struct EnvironmentCreationOptions
/// <summary>
/// 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.
/// </summary>
Expand All @@ -93,6 +93,28 @@ public sealed class OrtEnv : SafeHandle
#region Static members
private static readonly int ORT_PROJECTION_CSHARP = 2;

/// <summary>
/// Set this to <c>true</c> before accessing any OnnxRuntime type to prevent OnnxRuntime
/// from registering its own <c>DllImportResolver</c> via
/// <c>NativeLibrary.SetDllImportResolver</c>.
/// 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).
/// </summary>
/// <example>
/// <code>
/// // 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();
/// </code>
/// </example>
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.
Expand Down Expand Up @@ -274,7 +296,7 @@ private static void SetLanguageProjection(OrtEnv env)
/// <summary>
/// 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
/// </summary>
/// <returns>Returns a singleton instance of OrtEnv that represents native OrtEnv object</returns>
Expand Down Expand Up @@ -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.
/// </summary>
Expand Down
96 changes: 96 additions & 0 deletions csharp/test/Microsoft.ML.OnnxRuntime.Tests.Common/OrtEnvTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/// <summary>
/// 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.
/// </summary>
[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();
}
}

/// <summary>
/// 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.
/// </summary>
[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
}
Loading