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
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
#if !NET
using System.Linq;
Expand Down Expand Up @@ -374,16 +373,15 @@ public static AIFunction Create(MethodInfo method, object? target, string? name
}

/// <summary>
/// Creates an <see cref="AIFunction"/> instance for a method, specified via an <see cref="MethodInfo"/> for
/// and instance method, along with a <see cref="Type"/> representing the type of the target object to
/// instantiate each time the method is invoked.
/// Creates an <see cref="AIFunction"/> instance for a method, specified via a <see cref="MethodInfo"/> for
/// an instance method and a <see cref="Func{AIFunctionArguments,Object}"/> for constructing an instance of
/// the receiver object each time the <see cref="AIFunction"/> is invoked.
/// </summary>
/// <param name="method">The instance method to be represented via the created <see cref="AIFunction"/>.</param>
/// <param name="targetType">
/// The <see cref="Type"/> to construct an instance of on which to invoke <paramref name="method"/> when
/// the resulting <see cref="AIFunction"/> is invoked. <see cref="Activator.CreateInstance(Type)"/> is used,
/// utilizing the type's public parameterless constructor. If an instance can't be constructed, an exception is
/// thrown during the function's invocation.
/// <param name="createInstanceFunc">
/// Callback used on each function invocation to create an instance of the type on which the instance method <paramref name="method"/>
/// will be invoked. If the returned instance is <see cref="IAsyncDisposable"/> or <see cref="IDisposable"/>, it will be disposed of
/// after <paramref name="method"/> completes its invocation.
/// </param>
/// <param name="options">Metadata to use to override defaults inferred from <paramref name="method"/>.</param>
/// <returns>The created <see cref="AIFunction"/> for invoking <paramref name="method"/>.</returns>
Expand Down Expand Up @@ -457,22 +455,16 @@ public static AIFunction Create(MethodInfo method, object? target, string? name
/// </para>
/// </remarks>
/// <exception cref="ArgumentNullException"><paramref name="method"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="targetType"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="createInstanceFunc"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="method"/> represents a static method.</exception>
/// <exception cref="ArgumentException"><paramref name="method"/> represents an open generic method.</exception>
/// <exception cref="ArgumentException"><paramref name="method"/> contains a parameter without a parameter name.</exception>
/// <exception cref="ArgumentException"><paramref name="targetType"/> is not assignable to <paramref name="method"/>'s declaring type.</exception>
/// <exception cref="JsonException">A parameter to <paramref name="method"/> or its return type is not serializable.</exception>
public static AIFunction Create(
MethodInfo method,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type targetType,
AIFunctionFactoryOptions? options = null)
{
_ = Throw.IfNull(method);
_ = Throw.IfNull(targetType);

return ReflectionAIFunction.Build(method, targetType, options ?? _defaultOptions);
}
Func<AIFunctionArguments, object> createInstanceFunc,
AIFunctionFactoryOptions? options = null) =>
ReflectionAIFunction.Build(method, createInstanceFunc, options ?? _defaultOptions);

private sealed class ReflectionAIFunction : AIFunction
{
Expand Down Expand Up @@ -503,10 +495,11 @@ public static ReflectionAIFunction Build(MethodInfo method, object? target, AIFu

public static ReflectionAIFunction Build(
MethodInfo method,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type targetType,
Func<AIFunctionArguments, object> createInstanceFunc,
AIFunctionFactoryOptions options)
{
_ = Throw.IfNull(method);
_ = Throw.IfNull(createInstanceFunc);

if (method.ContainsGenericParameters)
{
Expand All @@ -518,13 +511,7 @@ public static ReflectionAIFunction Build(
Throw.ArgumentException(nameof(method), "The method must be an instance method.");
}

if (method.DeclaringType is { } declaringType &&
!declaringType.IsAssignableFrom(targetType))
{
Throw.ArgumentException(nameof(targetType), "The target type must be assignable to the method's declaring type.");
}

return new(ReflectionAIFunctionDescriptor.GetOrCreate(method, options), targetType, options);
return new(ReflectionAIFunctionDescriptor.GetOrCreate(method, options), createInstanceFunc, options);
}

private ReflectionAIFunction(ReflectionAIFunctionDescriptor functionDescriptor, object? target, AIFunctionFactoryOptions options)
Expand All @@ -536,20 +523,17 @@ private ReflectionAIFunction(ReflectionAIFunctionDescriptor functionDescriptor,

private ReflectionAIFunction(
ReflectionAIFunctionDescriptor functionDescriptor,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type targetType,
Func<AIFunctionArguments, object> createInstanceFunc,
AIFunctionFactoryOptions options)
{
FunctionDescriptor = functionDescriptor;
TargetType = targetType;
CreateInstance = options.CreateInstance;
CreateInstanceFunc = createInstanceFunc;
AdditionalProperties = options.AdditionalProperties ?? EmptyReadOnlyDictionary<string, object?>.Instance;
}

public ReflectionAIFunctionDescriptor FunctionDescriptor { get; }
public object? Target { get; }
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
public Type? TargetType { get; }
public Func<Type, AIFunctionArguments, object>? CreateInstance { get; }
public Func<AIFunctionArguments, object>? CreateInstanceFunc { get; }

public override IReadOnlyDictionary<string, object?> AdditionalProperties { get; }
public override string Name => FunctionDescriptor.Name;
Expand All @@ -566,14 +550,12 @@ private ReflectionAIFunction(
object? target = Target;
try
{
if (TargetType is { } targetType)
if (CreateInstanceFunc is { } func)
{
Debug.Assert(target is null, "Expected target to be null when we have a non-null target type");
Debug.Assert(!FunctionDescriptor.Method.IsStatic, "Expected an instance method");

target = CreateInstance is not null ?
CreateInstance(targetType, arguments) :
Activator.CreateInstance(targetType);
target = func(arguments);
if (target is null)
{
Throw.InvalidOperationException("Unable to create an instance of the target type.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,24 +106,6 @@ public AIFunctionFactoryOptions()
/// </remarks>
public Func<object?, Type?, CancellationToken, ValueTask<object?>>? MarshalResult { get; set; }

/// <summary>
/// Gets or sets a delegate used with <see cref="AIFunctionFactory.Create(MethodInfo, Type, AIFunctionFactoryOptions?)"/> to create the receiver instance.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="AIFunctionFactory.Create(MethodInfo, Type, AIFunctionFactoryOptions?)"/> creates <see cref="AIFunction"/> instances that invoke an
/// instance method on the specified <see cref="Type"/>. This delegate is used to create the instance of the type that will be used to invoke the method.
/// By default if <see cref="CreateInstance"/> is <see langword="null"/>, <see cref="Activator.CreateInstance(Type)"/> is used. If
/// <see cref="CreateInstance"/> is non-<see langword="null"/>, the delegate is invoked with the <see cref="Type"/> to be instantiated and the
/// <see cref="AIFunctionArguments"/> provided to the <see cref="AIFunction.InvokeAsync"/> method.
/// </para>
/// <para>
/// Each created instance will be used for a single invocation. If the object is <see cref="IAsyncDisposable"/> or <see cref="IDisposable"/>, it will
/// be disposed of after the invocation completes.
/// </para>
/// </remarks>
public Func<Type, AIFunctionArguments, object>? CreateInstance { get; set; }

/// <summary>Provides configuration options produced by the <see cref="ConfigureParameterBinding"/> delegate.</summary>
public readonly record struct ParameterBindingOptions
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ public void InvalidArguments_Throw()
Assert.Throws<ArgumentNullException>("method", () => AIFunctionFactory.Create(method: null!, target: new object()));
Assert.Throws<ArgumentNullException>("method", () => AIFunctionFactory.Create(method: null!, target: new object(), name: "myAiFunk"));
Assert.Throws<ArgumentNullException>("target", () => AIFunctionFactory.Create(typeof(AIFunctionFactoryTest).GetMethod(nameof(InvalidArguments_Throw))!, (object?)null));
Assert.Throws<ArgumentNullException>("targetType", () => AIFunctionFactory.Create(typeof(AIFunctionFactoryTest).GetMethod(nameof(InvalidArguments_Throw))!, (Type)null!));
Assert.Throws<ArgumentNullException>("createInstanceFunc", () =>
AIFunctionFactory.Create(typeof(AIFunctionFactoryTest).GetMethod(nameof(InvalidArguments_Throw))!, (Func<AIFunctionArguments, object>)null!));
Assert.Throws<ArgumentException>("method", () => AIFunctionFactory.Create(typeof(List<>).GetMethod("Add")!, new List<int>()));
}

Expand Down Expand Up @@ -312,16 +313,12 @@ public async Task Create_NoInstance_UsesActivatorUtilitiesWhenServicesAvailable(

AIFunction func = AIFunctionFactory.Create(
typeof(MyFunctionTypeWithOneArg).GetMethod(nameof(MyFunctionTypeWithOneArg.InstanceMethod))!,
typeof(MyFunctionTypeWithOneArg),
new()
static arguments =>
{
CreateInstance = (type, arguments) =>
{
Assert.NotNull(arguments.Services);
return ActivatorUtilities.CreateInstance(arguments.Services, type);
},
MarshalResult = (result, type, cancellationToken) => new ValueTask<object?>(result),
});
Assert.NotNull(arguments.Services);
return ActivatorUtilities.CreateInstance(arguments.Services, typeof(MyFunctionTypeWithOneArg));
},
new() { MarshalResult = (result, type, cancellationToken) => new ValueTask<object?>(result) });

Assert.NotNull(func);
var result = (Tuple<MyFunctionTypeWithOneArg, MyArgumentType>?)await func.InvokeAsync(new() { Services = sp });
Expand All @@ -330,55 +327,41 @@ public async Task Create_NoInstance_UsesActivatorUtilitiesWhenServicesAvailable(
}

[Fact]
public async Task Create_NoInstance_UsesActivatorWhenServicesUnavailable()
public async Task Create_CreateInstanceReturnsNull_ThrowsDuringInvocation()
{
AIFunction func = AIFunctionFactory.Create(
typeof(MyFunctionTypeWithNoArgs).GetMethod(nameof(MyFunctionTypeWithNoArgs.InstanceMethod))!,
typeof(MyFunctionTypeWithNoArgs),
new()
{
MarshalResult = (result, type, cancellationToken) => new ValueTask<object?>(result),
});
typeof(MyFunctionTypeWithOneArg).GetMethod(nameof(MyFunctionTypeWithOneArg.InstanceMethod))!,
static _ => null!);

Assert.NotNull(func);
Assert.Equal("42", await func.InvokeAsync());
await Assert.ThrowsAsync<InvalidOperationException>(async () => await func.InvokeAsync());
}

[Fact]
public async Task Create_NoInstance_ThrowsWhenCantConstructInstance()
public async Task Create_WrongConstructedType_ThrowsDuringInvocation()
{
var sp = new ServiceCollection().BuildServiceProvider();

AIFunction func = AIFunctionFactory.Create(
typeof(MyFunctionTypeWithOneArg).GetMethod(nameof(MyFunctionTypeWithOneArg.InstanceMethod))!,
typeof(MyFunctionTypeWithOneArg));
static _ => new MyFunctionTypeWithNoArgs());

Assert.NotNull(func);
await Assert.ThrowsAsync<MissingMethodException>(async () => await func.InvokeAsync(new() { Services = sp }));
await Assert.ThrowsAsync<TargetException>(async () => await func.InvokeAsync());
}

[Fact]
public void Create_NoInstance_ThrowsForStaticMethod()
{
Assert.Throws<ArgumentException>("method", () => AIFunctionFactory.Create(
typeof(MyFunctionTypeWithNoArgs).GetMethod(nameof(MyFunctionTypeWithNoArgs.StaticMethod))!,
typeof(MyFunctionTypeWithNoArgs)));
}

[Fact]
public void Create_NoInstance_ThrowsForMismatchedMethod()
{
Assert.Throws<ArgumentException>("targetType", () => AIFunctionFactory.Create(
typeof(MyFunctionTypeWithNoArgs).GetMethod(nameof(MyFunctionTypeWithNoArgs.InstanceMethod))!,
typeof(MyFunctionTypeWithOneArg)));
static _ => new MyFunctionTypeWithNoArgs()));
}

[Fact]
public async Task Create_NoInstance_DisposableInstanceCreatedDisposedEachInvocation()
{
AIFunction func = AIFunctionFactory.Create(
typeof(DisposableService).GetMethod(nameof(DisposableService.GetThis))!,
typeof(DisposableService),
static _ => new DisposableService(),
new()
{
MarshalResult = (result, type, cancellationToken) => new ValueTask<object?>(result),
Expand All @@ -397,7 +380,7 @@ public async Task Create_NoInstance_AsyncDisposableInstanceCreatedDisposedEachIn
{
AIFunction func = AIFunctionFactory.Create(
typeof(AsyncDisposableService).GetMethod(nameof(AsyncDisposableService.GetThis))!,
typeof(AsyncDisposableService),
static _ => new AsyncDisposableService(),
new()
{
MarshalResult = (result, type, cancellationToken) => new ValueTask<object?>(result),
Expand All @@ -416,7 +399,7 @@ public async Task Create_NoInstance_DisposableAndAsyncDisposableInstanceCreatedD
{
AIFunction func = AIFunctionFactory.Create(
typeof(DisposableAndAsyncDisposableService).GetMethod(nameof(DisposableAndAsyncDisposableService.GetThis))!,
typeof(DisposableAndAsyncDisposableService),
static _ => new DisposableAndAsyncDisposableService(),
new()
{
MarshalResult = (result, type, cancellationToken) => new ValueTask<object?>(result),
Expand Down Expand Up @@ -821,11 +804,7 @@ public ValueTask DisposeAsync()

private sealed class MyFunctionTypeWithNoArgs
{
private string _value = "42";

public static void StaticMethod() => throw new NotSupportedException();

public string InstanceMethod() => _value;
}

private sealed class MyFunctionTypeWithOneArg(MyArgumentType arg)
Expand Down
Loading