Skip to content

Commit 35523fd

Browse files
Refactor to support any number of parameters with validation
- Replace hardcoded switch statements with dynamic delegate generation using expression trees - Add parameter count and type validation at setup time (not invocation time) - Support methods with up to 16 parameters (C# Func<> limit) - Add comprehensive error handling tests - Validate type compatibility between handler and method parameters - Throw ArgumentException with clear messages for mismatches - Fixes all review comments about silent failures and missing validation Co-authored-by: JeanMarcMbouma <16613177+JeanMarcMbouma@users.noreply.github.com>
1 parent c50e2a5 commit 35523fd

File tree

2 files changed

+286
-60
lines changed

2 files changed

+286
-60
lines changed

src/MockLite.Core/Mock.cs

Lines changed: 143 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -100,17 +100,7 @@ public Mock<T> Setup<TResult>(Expression<Func<T, TResult>> expression, Func<TRes
100100
public Mock<T> Setup<TResult, T1>(Expression<Func<T, TResult>> expression, Func<T1, TResult> handler)
101101
{
102102
var (method, args) = ExtractMethod(expression);
103-
var parameters = method.GetParameters();
104-
105-
// Create a delegate that matches the method signature and extracts only the first parameter
106-
Delegate behavior = parameters.Length switch
107-
{
108-
1 => new Func<T1, TResult>(handler),
109-
2 => new Func<T1, object?, TResult>((p1, p2) => handler(p1)),
110-
3 => new Func<T1, object?, object?, TResult>((p1, p2, p3) => handler(p1)),
111-
_ => new Func<TResult>(() => handler(default(T1)!))
112-
};
113-
103+
var behavior = CreatePartialHandlerDelegate(method, handler, new[] { typeof(T1) }, typeof(TResult));
114104
_proxy.Setup(method, args, behavior);
115105
return this;
116106
}
@@ -136,16 +126,7 @@ public Mock<T> Setup<TResult, T1>(Expression<Func<T, TResult>> expression, Func<
136126
public Mock<T> Setup<TResult, T1, T2>(Expression<Func<T, TResult>> expression, Func<T1, T2, TResult> handler)
137127
{
138128
var (method, args) = ExtractMethod(expression);
139-
var parameters = method.GetParameters();
140-
141-
// Create a delegate that matches the method signature and extracts only the first two parameters
142-
Delegate behavior = parameters.Length switch
143-
{
144-
2 => new Func<T1, T2, TResult>(handler),
145-
3 => new Func<T1, T2, object?, TResult>((p1, p2, p3) => handler(p1, p2)),
146-
_ => new Func<TResult>(() => handler(default(T1)!, default(T2)!))
147-
};
148-
129+
var behavior = CreatePartialHandlerDelegate(method, handler, new[] { typeof(T1), typeof(T2) }, typeof(TResult));
149130
_proxy.Setup(method, args, behavior);
150131
return this;
151132
}
@@ -172,15 +153,7 @@ public Mock<T> Setup<TResult, T1, T2>(Expression<Func<T, TResult>> expression, F
172153
public Mock<T> Setup<TResult, T1, T2, T3>(Expression<Func<T, TResult>> expression, Func<T1, T2, T3, TResult> handler)
173154
{
174155
var (method, args) = ExtractMethod(expression);
175-
var parameters = method.GetParameters();
176-
177-
// Create a delegate that matches the method signature and extracts only the first three parameters
178-
Delegate behavior = parameters.Length switch
179-
{
180-
3 => new Func<T1, T2, T3, TResult>(handler),
181-
_ => new Func<TResult>(() => handler(default(T1)!, default(T2)!, default(T3)!))
182-
};
183-
156+
var behavior = CreatePartialHandlerDelegate(method, handler, new[] { typeof(T1), typeof(T2), typeof(T3) }, typeof(TResult));
184157
_proxy.Setup(method, args, behavior);
185158
return this;
186159
}
@@ -460,6 +433,134 @@ private static PropertyInfo ExtractProperty(LambdaExpression expr)
460433
throw new ArgumentException("Expression must be a property access");
461434
}
462435

436+
/// <summary>
437+
/// Creates a delegate that wraps a partial handler, extracting only the needed parameters from the method signature.
438+
/// </summary>
439+
/// <param name="method">The target method being mocked.</param>
440+
/// <param name="handler">The handler delegate that receives a subset of parameters.</param>
441+
/// <param name="handlerParamTypes">The types of parameters expected by the handler.</param>
442+
/// <param name="returnType">The return type of the method.</param>
443+
/// <returns>A delegate matching the full method signature that forwards to the partial handler.</returns>
444+
private static Delegate CreatePartialHandlerDelegate(MethodInfo method, Delegate handler, Type[] handlerParamTypes, Type returnType)
445+
{
446+
var methodParams = method.GetParameters();
447+
448+
// Validate that the method has at least as many parameters as the handler expects
449+
if (methodParams.Length < handlerParamTypes.Length)
450+
{
451+
throw new ArgumentException(
452+
$"The method '{method.Name}' has {methodParams.Length} parameter(s), but the handler expects {handlerParamTypes.Length} parameter(s). " +
453+
$"The handler can only receive up to {methodParams.Length} parameter(s).",
454+
nameof(handler));
455+
}
456+
457+
// Validate type compatibility for each handler parameter
458+
for (int i = 0; i < handlerParamTypes.Length; i++)
459+
{
460+
var methodParamType = methodParams[i].ParameterType;
461+
var handlerParamType = handlerParamTypes[i];
462+
463+
if (!handlerParamType.IsAssignableFrom(methodParamType) && methodParamType != handlerParamType)
464+
{
465+
throw new ArgumentException(
466+
$"Parameter type mismatch at position {i}: method '{method.Name}' has parameter type '{methodParamType.Name}' " +
467+
$"but handler expects '{handlerParamType.Name}'.",
468+
nameof(handler));
469+
}
470+
}
471+
472+
// If the method has the exact same number of parameters as the handler, just return the handler
473+
if (methodParams.Length == handlerParamTypes.Length)
474+
{
475+
return handler;
476+
}
477+
478+
// Build parameter expressions for the full method signature
479+
var methodParamExpressions = methodParams
480+
.Select(p => Expression.Parameter(p.ParameterType, p.Name))
481+
.ToArray();
482+
483+
// Create expression to invoke the handler with only the first N parameters
484+
var handlerConstant = Expression.Constant(handler);
485+
var handlerParams = methodParamExpressions.Take(handlerParamTypes.Length).ToArray();
486+
var invokeExpression = Expression.Invoke(handlerConstant, handlerParams);
487+
488+
// Build the correct Func<> delegate type
489+
var delegateTypeArgs = methodParams.Select(p => p.ParameterType).Append(returnType).ToArray();
490+
Type delegateType;
491+
492+
if (delegateTypeArgs.Length == 1)
493+
{
494+
// Func<TResult> - only return type
495+
delegateType = typeof(Func<>).MakeGenericType(delegateTypeArgs);
496+
}
497+
else
498+
{
499+
// Func<T1, ..., TN, TResult>
500+
var funcType = delegateTypeArgs.Length switch
501+
{
502+
2 => typeof(Func<,>),
503+
3 => typeof(Func<,,>),
504+
4 => typeof(Func<,,,>),
505+
5 => typeof(Func<,,,,>),
506+
6 => typeof(Func<,,,,,>),
507+
7 => typeof(Func<,,,,,,>),
508+
8 => typeof(Func<,,,,,,,>),
509+
9 => typeof(Func<,,,,,,,,>),
510+
10 => typeof(Func<,,,,,,,,,>),
511+
11 => typeof(Func<,,,,,,,,,,>),
512+
12 => typeof(Func<,,,,,,,,,,,>),
513+
13 => typeof(Func<,,,,,,,,,,,,>),
514+
14 => typeof(Func<,,,,,,,,,,,,,>),
515+
15 => typeof(Func<,,,,,,,,,,,,,,>),
516+
16 => typeof(Func<,,,,,,,,,,,,,,,>),
517+
17 => typeof(Func<,,,,,,,,,,,,,,,,>),
518+
_ => throw new NotSupportedException($"Methods with more than 16 parameters are not supported.")
519+
};
520+
delegateType = funcType.MakeGenericType(delegateTypeArgs);
521+
}
522+
523+
// Create the lambda with the correct delegate type
524+
var lambdaExpression = Expression.Lambda(delegateType, invokeExpression, methodParamExpressions);
525+
526+
return lambdaExpression.Compile();
527+
}
528+
529+
/// <summary>
530+
/// Creates an action delegate that wraps a partial handler for OnCall, extracting only the needed parameters.
531+
/// </summary>
532+
/// <param name="method">The target method being mocked.</param>
533+
/// <param name="handler">The handler action that receives a subset of parameters.</param>
534+
/// <param name="handlerParamTypes">The types of parameters expected by the handler.</param>
535+
private static void ValidateAndSetupOnCall(MethodInfo method, Type[] handlerParamTypes, string parameterName)
536+
{
537+
var methodParams = method.GetParameters();
538+
539+
// Validate that the method has at least as many parameters as the handler expects
540+
if (methodParams.Length < handlerParamTypes.Length)
541+
{
542+
throw new ArgumentException(
543+
$"The method '{method.Name}' has {methodParams.Length} parameter(s), but the handler expects {handlerParamTypes.Length} parameter(s). " +
544+
$"The handler can only receive up to {methodParams.Length} parameter(s).",
545+
parameterName);
546+
}
547+
548+
// Validate type compatibility for each handler parameter
549+
for (int i = 0; i < handlerParamTypes.Length; i++)
550+
{
551+
var methodParamType = methodParams[i].ParameterType;
552+
var handlerParamType = handlerParamTypes[i];
553+
554+
if (!handlerParamType.IsAssignableFrom(methodParamType) && methodParamType != handlerParamType)
555+
{
556+
throw new ArgumentException(
557+
$"Parameter type mismatch at position {i}: method '{method.Name}' has parameter type '{methodParamType.Name}' " +
558+
$"but handler expects '{handlerParamType.Name}'.",
559+
parameterName);
560+
}
561+
}
562+
}
563+
463564
// --- Callback Methods ---
464565

465566
/// <summary>
@@ -613,11 +714,8 @@ public Mock<T> OnCall(Expression<Func<T, object?>> expression, Action handler)
613714
public Mock<T> OnCall<T1>(Expression<Func<T, object?>> expression, Action<T1> handler)
614715
{
615716
var (method, _) = ExtractMethod(expression);
616-
_proxy.OnInvocation(method, args =>
617-
{
618-
if (args.Length >= 1)
619-
handler((T1)args[0]!);
620-
});
717+
ValidateAndSetupOnCall(method, new[] { typeof(T1) }, nameof(handler));
718+
_proxy.OnInvocation(method, args => handler((T1)args[0]!));
621719
return this;
622720
}
623721

@@ -641,11 +739,8 @@ public Mock<T> OnCall<T1>(Expression<Func<T, object?>> expression, Action<T1> ha
641739
public Mock<T> OnCall<T1, T2>(Expression<Func<T, object?>> expression, Action<T1, T2> handler)
642740
{
643741
var (method, _) = ExtractMethod(expression);
644-
_proxy.OnInvocation(method, args =>
645-
{
646-
if (args.Length >= 2)
647-
handler((T1)args[0]!, (T2)args[1]!);
648-
});
742+
ValidateAndSetupOnCall(method, new[] { typeof(T1), typeof(T2) }, nameof(handler));
743+
_proxy.OnInvocation(method, args => handler((T1)args[0]!, (T2)args[1]!));
649744
return this;
650745
}
651746

@@ -671,11 +766,8 @@ public Mock<T> OnCall<T1, T2>(Expression<Func<T, object?>> expression, Action<T1
671766
public Mock<T> OnCall<T1, T2, T3>(Expression<Func<T, object?>> expression, Action<T1, T2, T3> handler)
672767
{
673768
var (method, _) = ExtractMethod(expression);
674-
_proxy.OnInvocation(method, args =>
675-
{
676-
if (args.Length >= 3)
677-
handler((T1)args[0]!, (T2)args[1]!, (T3)args[2]!);
678-
});
769+
ValidateAndSetupOnCall(method, new[] { typeof(T1), typeof(T2), typeof(T3) }, nameof(handler));
770+
_proxy.OnInvocation(method, args => handler((T1)args[0]!, (T2)args[1]!, (T3)args[2]!));
679771
return this;
680772
}
681773

@@ -720,11 +812,8 @@ public Mock<T> OnCall(Expression<Action<T>> expression, Action handler)
720812
public Mock<T> OnCall<T1>(Expression<Action<T>> expression, Action<T1> handler)
721813
{
722814
var method = ExtractVoidMethod(expression);
723-
_proxy.OnInvocation(method, args =>
724-
{
725-
if (args.Length >= 1)
726-
handler((T1)args[0]!);
727-
});
815+
ValidateAndSetupOnCall(method, new[] { typeof(T1) }, nameof(handler));
816+
_proxy.OnInvocation(method, args => handler((T1)args[0]!));
728817
return this;
729818
}
730819

@@ -748,11 +837,8 @@ public Mock<T> OnCall<T1>(Expression<Action<T>> expression, Action<T1> handler)
748837
public Mock<T> OnCall<T1, T2>(Expression<Action<T>> expression, Action<T1, T2> handler)
749838
{
750839
var method = ExtractVoidMethod(expression);
751-
_proxy.OnInvocation(method, args =>
752-
{
753-
if (args.Length >= 2)
754-
handler((T1)args[0]!, (T2)args[1]!);
755-
});
840+
ValidateAndSetupOnCall(method, new[] { typeof(T1), typeof(T2) }, nameof(handler));
841+
_proxy.OnInvocation(method, args => handler((T1)args[0]!, (T2)args[1]!));
756842
return this;
757843
}
758844

@@ -778,11 +864,8 @@ public Mock<T> OnCall<T1, T2>(Expression<Action<T>> expression, Action<T1, T2> h
778864
public Mock<T> OnCall<T1, T2, T3>(Expression<Action<T>> expression, Action<T1, T2, T3> handler)
779865
{
780866
var method = ExtractVoidMethod(expression);
781-
_proxy.OnInvocation(method, args =>
782-
{
783-
if (args.Length >= 3)
784-
handler((T1)args[0]!, (T2)args[1]!, (T3)args[2]!);
785-
});
867+
ValidateAndSetupOnCall(method, new[] { typeof(T1), typeof(T2), typeof(T3) }, nameof(handler));
868+
_proxy.OnInvocation(method, args => handler((T1)args[0]!, (T2)args[1]!, (T3)args[2]!));
786869
return this;
787870
}
788871

0 commit comments

Comments
 (0)