Ability to apply type hints to delegates (unknown number of arguments, f.i. ASP.Net minimal API) #8404
Replies: 3 comments 6 replies
-
I'm not following this part: at the point your input types are specified |
Beta Was this translation helpful? Give feedback.
-
Thanks. This particular part got the point across: Model(u => new { u.Username, u.FullName, u.SubscribedToNewsLetter })
.CreateWith((INewsLetterApi newsLetterApi, /*anonymous type*/ TModel model) => { Essentially there is no middle ground with types in lambda signatures: once you specify one you have to specify all. That is troublesome when one of the types is going to be an anonymous type which is unspeakable. In this case though why do you need the |
Beta Was this translation helpful? Give feedback.
-
I've noticed C# isn't really eager to resolve the types if you throw in nested lambdas. However, with some help, you can hint it and work around most issues. Instead of having The solution using above workarounds: internal class Factory
{
public static Configuration<TResource, TModel> CreateConfiguration<TResource, TModel>(Func<TResource, TModel> projection)
{
throw new NotImplementedException();
}
public static Configuration<TResource, TModel> CreateConfiguration<TResource, TModel, TServices>(
Func<TServices, Expression<Func<TResource, TModel>>> projectionFactory)
{
throw new NotImplementedException();
}
public static Configuration<TResource, TModel> CreateConfiguration<TResource, TModel, TServices>(
Func<TServices, Task<Expression<Func<TResource, TModel>>>> asyncProjectionFactory)
{
throw new NotImplementedException();
}
}
internal class Configuration<TResource, TModel>
{
public void Create(Func<TModel, TResource> handler)
{
throw new NotImplementedException();
}
public void Create<TServices>(Func<TModel, TServices, TResource> handler)
{
throw new NotImplementedException();
}
public void Create<TServices>(Func<TModel, TServices, Task<TResource>> asyncHandler)
{
throw new NotImplementedException();
}
} How to use these: internal record Input(int Id, string Name, bool Whatever);
internal static class Identity
{
/// <summary>
/// Hint the compiler that the argument is an <see cref="Expression"/>.
/// </summary>
/// <typeparam name="T">The expression type.</typeparam>
/// <param name="expression">The expression to return.</param>
/// <returns><paramref name="expression"/>.</returns>
public static T Expression<T>(T expression) where T : Expression => expression;
} Creating a configuration with a model projection: // No services
var config = Factory.CreateConfiguration((Input i) => new { i.Id, i.Name });
// Single service
var config = Factory.CreateConfiguration((DbContext db) =>
Identity.Expression((Input i) => new { i.Id, i.Name, Batch = db.Set<object>().First() }));
// Multiple services, sync
var config = Factory.CreateConfiguration(((DbContext db, HttpContext httpContext) services) =>
Identity.Expression((Input i) => new { i.Id, i.Name, Batch = services.db.Set<object>().First() }));
// Multiple services, async
var config = Factory.CreateConfiguration(async ((DbContext db, HttpContext httpContext) services) =>
{
await Task.Delay(1000);
// Compiler can't resolve the lambda to target expression without a bit of help
return Identity.Expression((Input i) => new { i.Id, i.Name, Batch = services.db.Set<object>().First() });
}); Setting the create handler: // No services
config.Create(model => new Input(model.Id, model.Name, false);
// Multiple services, sync
config.Create<(DbContext db, HttpContext httpContext)>((model, services) => new Input(model.Id, model.Name, true));
// Multiple services, async
config.Create<(DbContext db, HttpContext httpContext)>(async (model, services) =>
{
await services.db.Set<object>.AnyAsync();
return new Input(model.Id, model.Name, true);
}); This might actually be slightly improved when #258 is implemented and the tuple elements can be deconstructed directly in the delegates. This still doesn't affect the benefits of |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
I'm currently working a lot with methods that in the most basic usage expect simple things like projections (
Expression<Func<TIn, TOut>>
) or transformations (Func<TIn, TOut>
). However, as with ASP.Net Core minimal API, I want consumers to be able to use services as well. This is achievable by adding an additional method with an additional generic argument, however one would need to extend this in case a second, third, fourth, ... service are to be injected. Delegates are the obvious way to escape this, but delegates and anonymous types are not best buddies. See the following example of this situation in a CRUD framework:So far so good. This works exactly as desired and C# can infer the anonymous model type
TModel
that's the output of the projection. However, now if I want to connect the newsletter subscription to an external API, I'm going to have to inject that into the handler somehow. I could do that using a methodvoid CreateWith<TService>(Func<TModel, TService, TEntity> factory)
. The next issue will be that in some cases I'd want to be able to have async handling, so I will also need to add overloads forTask<T>
andValueTask<T>
returns, possibly even more if there are other awaitable types that make sense to support, while also retaining the directT
support for the really simple callbacks.The obvious way out is to support the following signature:
CreateWith(Delegate factory)
, which also happens to be what ASP.Net Core supports for declaring minimal API handlers. Borrowing from that very same minimal API, we can take such a factory delegate, inspect it's input types, inspect it's output type and bring this all back toFunc<HttpContext, TModel, Task<TEntity>>)
using a tiny bit of expression wiring. However, this overlooks the part of specifying the factory delegate, because now the input types have to be declared inline and one of my input types is the anonymousTModel
, which I can't specify. One might argue that partial generic argument specification might help here, but I don't yet see how to apply that here and also I guess that's still not on the radar for release soon.So let's continue down the broken path and see what can be fixed. While a
Delegate
fixes the issue of the unknown / unspecified arguments, it opens the issue of making the known inputs unknown. We can make a combination though, by havingCreateWith(Func<TModel, Delegate> wrappedFactory)
. With a bit of expression wiring we can still reduce this down toFunc<HttpContext, TModel, Task<TEntity>>
, which will invokewrappedFactory
and then invoke the returned delegate with the required services.Again, so far so good. However, I've now lost the return type. While this is fine in the case of my create statement, I'm running into situations where I want to return yet another derived expression. Typing those expressions during compile time is impossible because I can't specify the return type for the
Delegate
argument. However, if I could somehow hint the compiler that myDelegate
needs to have a return ofExpression<Func<TEntity, TModel>>
(or whatever combination ofTEntity
andTModel
that would require specifying the type forTModel
), this is solvable as well.Because delegates logically can't implement interfaces, there's no approach there. I'd imagine the least invasive way might actually be an attribute that declares the return type, but honestly I'm baffled I can't find anyone having the same issue and same solution, which leads me to think there must be an oversight in what I'm trying to achieve. Besides the return, I could also imagine one would want to declare required input arguments in a delegate. It would actually be possible to make this quite strict, declaring that the delegate has arguments
X, Y, Z
, in that exact order, but not constraining any arguments that would follow.Can someone please provide a bit of feedback on the above? I'm guessing it would be possible to create this as a library and roslyn generator, although I'm not sure if it's possible to inject the types from a generator. Any comments are greatly appreciated!
Beta Was this translation helpful? Give feedback.
All reactions