-
Notifications
You must be signed in to change notification settings - Fork 10.3k
Support BindAsync Surrogates in Minimal APIs #45525
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
cc: @halter73, @davidfowl |
This is basically a dupe of #35489. We're currently exploring writing a source generator for minimal APIs (see https://github.com/davidfowl/uController), so anything we design we'd want to make sure keeps working for that as well. |
@davidfowl Doh! GitHub failed me. I knew I remembered this conversation. I spent 15-20 mins trying to find it, but I couldn't find the thread. Would this proposal still apply? Is there a working API - yet? The short, short version of this proposal is being able to provide a way to say |
Based on the example source generator example, I see this fitting in at around line 463 with: private bool HasBindAsync(Type type, out MethodInfo mi, out int parameterCount)
{
// TODO: Validate return type
mi = GetStaticMethodFromHierarchy(type, "BindAsync", new[] { _wellKnownTypes.HttpContextType }, m => true);
mi ??= GetStaticMethodFromHierarchy(type, "BindAsync", new[] { _wellKnownTypes.HttpContextType, _wellKnownTypes.ParamterInfoType }, m => true);
+ if (mi == null && bindFromHttpContextService.TryGetSurrogate(type, out var surrogateType))
+ {
+ return HasBindAsync(surrogateType, out mi, out parameterCount);
+ }
parameterCount = mi?.GetParameters().Length ?? 0;
return mi != null;
} |
I don't think it's that trivial. How do we resolve the The codegen runtime code that looks like: // Fallback case
// DI
// Surrogate
// Body
if (serviceProviderIsService.IsService(parameter.ParameterType))
{
// It's a service
}
else if (serviceProviderIsService.IsService(typeof(ParameterBinder<>).MakeGenericType(parameter.ParameterType)))
{
// There's a parameter binder here
}
else
{
// Assume frombody
} I'd much prefer an attribute that points to a type so that this feature would be more pay for play and statically analyzable. |
True. I wasn't thinking about zero access at design-time. You almost had me stumped. I definitely had to rethink the problem, but I think the same goal can be achieved by just ditching all the runtime aspects. What if a surrogate could be provided to the source code generator in a statically determined way without any DI or attributes? Let the sorcery ensue 🧙🏽♂️ ! Source code generation is all about design-time. So we need to hook into a design-time process that the source code generator can pick up. What if we add... <ItemGroup>
<AdditionalFiles Include="Asp.Versioning.ApiVersion"
SurrogateType="Asp.Versioning.Http.ApiVersionBinder"
MinimalApi="true" />
</ItemGroup> Then any library author can register a surrogate at design-time and have it picked up through the standard NuGet build extensions. Developers still have the ability to remove or update the mapping. This is a bit of contrived way of using
This information can be retrieved and used with something like: private static Dictionary<Type, Type> MapBindAsyncSurrogateTypes(SourceGeneratorContext context)
{
const string Prefix = "build_metadata.additionalfiles.";
const string MinimalApi = Prefix + nameof(MinimalApi);
const string SurrogateType = Prefix + nameof(SurrogateType);
var map = default(Dictionary<Type, Type);
foreach (var file in context.AdditionalFiles)
{
var options = context.AnalyzerConfigOptions.GetOptions(file);
if (!options.TryGetValue(MinimalApi, out var value) ||
!bool.TryParse(value, out minApi) ||
!minApi)
{
continue;
}
if (Type.GetType(file.Path, throwOnError: false) is not { } targetType)
{
continue;
}
if (options.TryGetValue(SurrogateType, out value) &&
Type.GetType(value, throwOnError: false) is { } surrogateType)
{
map ??= new();
map[targetType] = surrogateType;
}
return map ?? new(capacity: 0);
} The previous example will likely initialize this somewhere earlier (just once) and pass it in. Something like: private bool HasBindAsync(
Type type,
+ Dictionary<Type, Type> surrogateTypeMap,
out MethodInfo mi,
out int parameterCount)
{
// TODO: Validate return type
mi = GetStaticMethodFromHierarchy(type, "BindAsync", new[] { _wellKnownTypes.HttpContextType }, m => true);
mi ??= GetStaticMethodFromHierarchy(type, "BindAsync", new[] { _wellKnownTypes.HttpContextType, _wellKnownTypes.ParamterInfoType }, m => true);
+ if (mi == null && surrogateTypeMap.TryGetValue(type, out var surrogateType))
+ {
+ mi = GetStaticMethodFromHierarchy(surrogateType, "BindAsync", new[] { _wellKnownTypes.HttpContextType }, m => true);
+ mi ??= GetStaticMethodFromHierarchy(surrogateType, "BindAsync", new[] { _wellKnownTypes.HttpContextType, _wellKnownTypes.ParamterInfoType }, m => true);
+ }
parameterCount = mi?.GetParameters().Length ?? 0;
return mi != null;
} Thoughts? |
Or an attribute 😄. That would work at both runtime and statically. I know it makes the minimal API noisier and you don't want to couple this type to the type you don't own. I can think of a couple of ways to do this association:
The most direct associations are the first 2, put an attribute on the type or put it on the parameter. The big downside of the attribute is that the owner of the library can't make the type available for binding automagically as part of their call to add their services (in this case AddApiVersioning). I would like the parameter defined to somehow hint to the system that it needs this behavior, or that this parameter is indeed special. When the type has no way to know it's being bound. Maybe this just adds a little overhead to the pure fallback case so using the DI container is the right thing to do here. That code I wrote initially would look like this in the generated code for the source generator: if (serviceProviderIsService.IsService(typeof(ApiVersion))
{
// It's a service
}
else if (serviceProviderIsService.IsService(typeof(IParameterBinder<ApiVersion>)))
{
// There's a parameter binder here
}
else
{
// Assume frombody
} PS: This is the language feature that is trying to formalize these surrogate types (dotnet/csharplang#5496). I'm working with the team closely to see if we can solve scenarios like this generally with this language feature. It still has the "problem" of having to create a new type though. |
I'm gonna close this as a dupe |
Let's continue the discussion on that original issue with the DI based idea. |
Summary
Support parameter binding using a surrogate type.
Motivation and goals
The existing parameter binding mechanisms are insufficient for all types of model binding. This is particularly painful for extension authors that previously relied on using
IModelBinderProvider
andIModelBinder
.As a developer, I might want:
This will not work out-of-the-box. There is no way to resolve
ApiVersion
.All of the current, supported strategies are insufficient for binding:
ApiVersion
type cannot have aBindAsync
methodTryParse
:IApiVersionParser
so developers can change the parsing implementationIModelBinder
There are 3 possible workarounds today:
HttpContext
as a parmaeter and retrieve the value viaHttpContext.GetRequestedApiVersion()
a. This isn't very minimal
a.
services.AddTransient(sp => sp.GetRequiredService<IHttpContextAccessor>().HttpContext?.GetRequestedApiVersion()!
b. This is currently supported by explicitly opting into
services.EnableApiVersionBinding()
Proposal
The proposal would be to add a service that can accept a surrogate type that can perform the binding. This would allow library authors to provide a similar experience to
IModelBinder
without a developer having to do anything special. Surrogates would be registered through DI, which would enable anyone to add, remove, or replace surrogate types. Just as there is a 1-to-1 pairing of type toBindAsync
orIBindableFromHttpContext<T>
, so too, there can be exactly one corresponding surrogate type. A surrogate type will be considered as the last option before choosingIServiceProvider.GetService
.The proposed API would be (final names TDB):
The default implementation of the service would be:
A surrogate type falls under the same rules for
BindAsync
custom binding albeit on an alternate type.An example implementation would be:
The surrogate can be registered in DI with:
This is a generic approach can used to delegate any implementation of
BindAsync
to an alternate type. Other frameworks, such as OData, could support Minimal APIs using this same mechanism.Risks / unknowns
None that are immediately evident. The only thing changing in runtime behavior is which type to look for
BindAsync
on.The text was updated successfully, but these errors were encountered: