NSubstitute декоратор интерфейса

Версия 1.1 от Alexandr Fokin на 2026/03/18 15:24

 Пример создание тестового декоратора, в котором в одном методе задается кастомное поведение, а другие методы пробрасывают поведение исходной реализации. 
 Reflection | РефлексияDI IOC AOP
 public static void RegistryDecorator(IServiceCollection services)
{
    var sourceRegistration = services.First(
        e => e.ServiceType.FullName == "IService");

    services.Remove(sourceRegistration);
    services.Add(new ServiceDescriptor(
        sourceRegistration.ImplementationType,
        sourceRegistration.ImplementationType,
        ServiceLifetime.Singleton));
    services.Add(new ServiceDescriptor(
        sourceRegistration.ServiceType,
        (s) => BuildDecorator(
            sourceRegistration,
            s.GetRequiredService(sourceRegistration.ImplementationType)),
        ServiceLifetime.Singleton));
}

private static object BuildDecorator(
    ServiceDescriptor descriptor,
    object source)
{
    static IEnumerable<MethodInfo> GetInterfaceMethodsRecursive(Type type)
    {
        foreach (var method in type.GetMethods(BindingFlags.Instance | BindingFlags.Public))
        {
            yield return method;
        }

        if (type.IsInterface)
        {
            foreach (var iface in type.GetInterfaces())
            {
                foreach (var method in GetInterfaceMethodsRecursive(iface))
                {
                    yield return method;
                }
            }
        }
    }

    static object[] CreateFakeParameters(MethodInfo method)
    {
        var parameters = method.GetParameters();
        var result = new List<object>(parameters.Length);

        foreach (var elem in parameters)
        {
            var type = !elem.ParameterType.IsByRef
                ? elem.ParameterType
                : elem.ParameterType.GetElementType();
            var value = !type.IsValueType
                ? null
                : Activator.CreateInstance(type);
            result.Add(value);
        }

        return result.ToArray();
    }

    var decorator = Substitute.For(
        new[] { descriptor.ServiceType },
        Array.Empty<object>());

    var methodsForDecoration = GetInterfaceMethodsRecursive(descriptor.ServiceType)
        .ToHashSet();

    // Подменяем поведение нужного метода.
    // string Action1(string text)
    var customMethod = methodsForDecoration.Single(e => e.Name == "Action1");
    customMethod
        .Invoke(decorator, CreateFakeParameters(customMethod))
        .ReturnsForAnyArgs(
            e =>
            {
                return $"Test {e[0]}";
            });
    methodsForDecoration.Remove(customMethod);

    // Пробрасываем остальные методы интерфейса без изменения.
    foreach (var elem in methodsForDecoration)
    {
        elem
            .Invoke(decorator, CreateFakeParameters(elem))
            .ReturnsForAnyArgs(
                e =>
                {
                    var parameters = e.Args();
                    var result = elem.Invoke(source, parameters);

                    // для передачи out параметров.
                    for (var i = 0; i < parameters.Length; i++)
                    {
                        e[i] = parameters[i];
                    }

                    return result;
                });
    }

    // invoke test
    // var p = CreateFakeParameters(customMethod);
    // p[0] = "name";
    // var r = customMethod.Invoke(decorator, p);

    return decorator;
}