Method caller
Target method
public static class AlbumCommands
{
static readonly QueryCommand UpdateAlbum = new("UPDATE albums SET Title = @title WHERE AlbumId = @albumId");
public static Task<int> SaveAlbum(DbConnection cnn, int albumId, string title, CancellationToken cancellationToken)
=> UpdateAlbum.ExecuteAsync(cnn, new { albumId, title }, ct: cancellationToken);
}
Adapt the delegate
public record SaveAlbumArgs(int AlbumId, string Title);
MethodInfo method = typeof(AlbumCommands).GetMethod(nameof(AlbumCommands.SaveAlbum))
?? throw new InvalidOperationException("SaveAlbum was not found.");
Func<SaveAlbumArgs, DbConnection, CancellationToken, Task<int>> save = MethodCaller.Create<Func<SaveAlbumArgs, DbConnection, CancellationToken, Task<int>>>(
method,
CallerParameter<DbConnection>.ByType(),
CallerParameter<CancellationToken>.ByType());
int affected = await save(new SaveAlbumArgs(12, "Blue"), cnn, cancellationToken);
The first delegate argument supplies mapped method parameters through the parameter source system. The later arguments supply parameters selected by the caller bindings.
Match by type
CallerParameter<CancellationToken>.ByType();
A type binding matches an otherwise unbound target parameter with the exact caller argument type.
Match by name
CallerParameter<int>.Named("userId");
A named binding targets the method parameter with that name.
Mix direct and mapped inputs
Func<SaveAlbumArgs, int, CancellationToken, Task<int>> save = MethodCaller.Create<Func<SaveAlbumArgs, int, CancellationToken, Task<int>>>(
method,
CallerParameter<int>.Named("userId"),
CallerParameter<CancellationToken>.ByType());
Caller supplied bindings take precedence over a value that could also come from the mapped source.
Return shape
Func<SaveAlbumArgs, int> syncCall;
Func<SaveAlbumArgs, Task<int>> taskCall;
Func<SaveAlbumArgs, ValueTask<int>> valueTaskCall;
The delegate return type follows the wrapped method return shape.
Binding details
A type binding uses the exact target parameter type. No match leaves that caller argument unused. More than one available exact match is ambiguous.
CallerParameter<CancellationToken>.ByType();
A named binding takes precedence over type matching. A missing named target also leaves that caller argument unused.
CallerParameter<int>.Named("userId");
A caller supplied binding takes precedence over a value that could also come from the mapped source.
Func<SaveAlbumArgs, int, CancellationToken, Task<int>> save = MethodCaller.Create<Func<SaveAlbumArgs, int, CancellationToken, Task<int>>>(
method,
CallerParameter<int>.Named("userId"),
CallerParameter<CancellationToken>.ByType());
The first delegate argument has a fixed compile time source type. Runtime derived types do not switch the parameter source mapping.
Unusable mapped value
Func<SaveAlbumArgs, Task<int>> save = MethodCaller.Create<Func<SaveAlbumArgs, Task<int>>>(method);
If a target parameter is assigned to the mapped source but the current source value is not usable, invocation fails. It is not silently replaced by another source.