pardeike/Harmony · error · ArgumentException

Interface methods must be called virtually

Error message

Interface methods must be called virtually

What it means

AccessTools.MethodDelegate<DelegateType> throws this ArgumentException when the method being wrapped is declared on an interface but the caller explicitly passed virtualCall: false. Interface methods have no concrete body, so a direct (non-virtual) call to them is meaningless and Harmony refuses to create such a delegate.

Solutions

  1. Pass virtualCall: true so the call is dispatched virtually to the implementing object.
  2. Resolve the concrete implementation method from the implementing class instead of the interface.
  3. If you truly need a non-virtual call, target a non-interface declaring type.

Example fix

// before
var d = AccessTools.MethodDelegate<Func<int>>(typeof(IMover).GetMethod("Step"), mover, false);
// after
var d = AccessTools.MethodDelegate<Func<int>>(typeof(IMover).GetMethod("Step"), mover); // virtualCall defaults to true
Defensive patterns

Strategy: validation

Validate before calling

if (method.DeclaringType?.IsInterface == true && !virtualCall)
    throw new InvalidOperationException("Interface methods require virtualCall: true");

Type guard

bool IsNonVirtualInterfaceCall(MethodInfo m, bool virtualCall) => m.DeclaringType?.IsInterface == true && !virtualCall;

Try / catch

try { var d = AccessTools.MethodDelegate<T>(method, instance, nonVirtual); }
catch (ArgumentException) when (method.DeclaringType?.IsInterface == true) { d = AccessTools.MethodDelegate<T>(method, instance); }

Prevention

When it happens

Trigger: AccessTools.MethodDelegate<T>(interfaceMethod, instance, nonVirtualCall: false) - i.e. virtualCall parameter false - where method.DeclaringType.IsInterface is true.

Common situations: Wrapping a method obtained from typeof(IMyInterface).GetMethod(...) while disabling virtual dispatch, often to call a base/default implementation directly.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of pardeike/Harmony@e7872dc170 (2026-09-15). Data as JSON: /api/errors/1a2e7e65b4a6a492. Report an issue: GitHub.

Appendix: source

Thrown at Harmony/Tools/AccessTools.cs:1805

		/// </remarks>
		///
		public static DelegateType MethodDelegate<DelegateType>(MethodInfo method, object instance = null, bool virtualCall = true, Type[] delegateArgs = null) where DelegateType : Delegate
		{
			if (method is null)
				throw new ArgumentNullException(nameof(method));

			var delegateType = typeof(DelegateType);

			// Static method delegate
			if (method.IsStatic)
			{
				return (DelegateType)Delegate.CreateDelegate(delegateType, method);
			}

			var declaringType = method.DeclaringType;
			if (declaringType != null && declaringType.IsInterface && !virtualCall)
			{
				throw new ArgumentException("Interface methods must be called virtually");
			}

			// Open instance method delegate ...
			if (instance is null)
			{
				var delegateParameters = delegateType.GetMethod("Invoke").GetParameters();
				if (delegateParameters.Length == 0)
				{
					// Following should throw an ArgumentException with the proper message string.
					_ = Delegate.CreateDelegate(typeof(DelegateType), method);
					// But in case it doesn't...
					throw new ArgumentException("Invalid delegate type");
				}
				var delegateInstanceType = delegateParameters[0].ParameterType;
				// Exceptional case: delegate struct instance type cannot be created from an interface method.
				// This case is handled in the "non-virtual call" case, using the struct method and the matching delegate instance type.
				if (declaringType != null && declaringType.IsInterface && delegateInstanceType.IsValueType)
				{

View on GitHub (pinned to e7872dc170)