Skip to content

MapAction MVP #29878

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

Merged
merged 17 commits into from
Feb 18, 2021
Merged

MapAction MVP #29878

merged 17 commits into from
Feb 18, 2021

Conversation

halter73
Copy link
Member

@halter73 halter73 commented Feb 3, 2021

This is an early prototype for MapAction. This is a good time to provide feedback on names and project structure.

Before this can be merged we need to support more parameter binding attributes and add tests. Input and output formatters, model binding validation, Swagger/OpenApi support, MapAction filters and more will all come later.

Addresses #29684

[HttpPost("/EchoTodo")]
JsonResult EchoTodo([FromBody] Todo todo) => new(todo);

endpoints.MapAction((Func<Todo, JsonResult>)EchoTodo);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cast is a bit weird here - do any of the language enhancements we've discussed help out with this kind of methodgroup-to-delegate conversion?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Brain is a bit fried today but not sure which enhancement we discussed that would fix this.

If this were written as a lambda I agree the discussions we've had would help. It would have a natural type, we'd synthesize a delegate, use that and it would become the type passed to MapAction(Delegate). This is a method group though, not a lambda. Don't recall us talking about enhancing those.

It's not outside the realm of possibilities, I just haven't thought about it a bunch yet.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This came up as well. The idea is that overloads aren't supported we didn't understand why this would be problematic.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Brain is feeling a bit better today: medium rare, no longer well done.

Yeah we did talk about this. It's the same idea: giving method groups a natural type. That lets them have a tie breaker in cases where a Delegate overload exists.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jaredpar We should figure out what preview release of .NET 6 we could expect a version of this. We'll likely have something in preview2 and I don't expect we'd have anything that early in the compiler timeframe but we should target a release because this is pretty unusable in C# without it 😄

/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
public static IEndpointConventionBuilder MapAction(
this IEndpointRouteBuilder endpoints,
Delegate action)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Linker unfriendly 😄 .

cc @eerhardt

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's linker unfriendly about the API? You can have APIs that take delegates, and the linker won't trim them away.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do a ton of reflection on the delegate that's passed in.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Expression stuff is probably going to get you into more trouble. I'm in the middle of annotating Linq.Expressions now, and it isn't pretty how much stuff is not trim compatible.

Copy link
Member Author

@halter73 halter73 Feb 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're also considering Roslyn source generators as an alternative which I assume is more linker-friendly. @davidfowl has already started experimenting with this at https://github.com/davidfowl/uController/tree/aa8bcb4b30764e42bd72d326cb2718a4d4eaf4a9/src/uController.SourceGenerator.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, source generators are usually better, if possible

// paramterExpression = BindParamenter(formProperty, parameter, parameter.FromForm);
//}
//else if (parameter.FromBody)
if (parameter.CustomAttributes.Any(a => typeof(IFromBodyAttribute).IsAssignableFrom(a.AttributeType)))
Copy link
Contributor

@pranavkm pranavkm Feb 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We spent a bit of time trying to declutter ApiControllers by inferring default sources for parameters. The rules are fairly simple:

  • Complex type -> FromBody
  • Name appears as a route value -> FromRoute
  • Everything else -> FromQuery

It would be crummy to lose that as part of this programming model.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{
var bodyValue = await httpContext.Request.ReadFromJsonAsync(bodyType!);

await invoker(target, httpContext, bodyValue);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have to think about validation looks like here. The way this works in MVC is that deserialization errors and validation errors end up in model state. For ApiControllers a filter turns the model state in a 400 response (which includes the a serialized form of model state).

  • We could re-use ModelState here. It's not necessarily the best data structure to represent a graph of values, but MVC's validation system already understands it so that's one fewer thing to deal with.
  • How do we surface the error to the user? We could bind a ModelState and let you deal with it. If we wanted to support MVC's auto validation as a feature (it's pretty neat and removes boiler plate), we could add it as a filter that's configured by default.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't add any implicit behavior until we figure out a way to remove said behavior. We currently don't have an application model so we need something reasonable that describes this behavior.

If we support something like model state, it would need to be an explicit parameter to the method (which is a much cleaner way of opting-into functionality).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're going to have some way to configure filters for these globally (services.AddRouting(options => options.MapActionFilters. [Insert / Remove ])). That should be sufficient to enable some implicit runtime behavior that's also removable.

But binding the type once we enable validation would be a good start.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should do that after, not in this PR

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah, absolutely. Just pointing out that we have a gap here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're going to have some way to configure filters for these globally

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So you're going to end up with an application model, just without the "see and modify" part of it 😆

@Pilchie
Copy link
Member

Pilchie commented Feb 3, 2021

What area- label should we use for this sort of work?

@halter73
Copy link
Member Author

halter73 commented Feb 3, 2021

I think area-servers is fine since it will live Microsoft.AspNetCore.Http(.Abstractions). It's definitely more MVC-adjacent than most stuff with the area-servers label though. I could go either way.

@davidfowl
Copy link
Member

Lets give it both labels to confuse everyone 😄

@halter73
Copy link
Member Author

halter73 commented Feb 4, 2021

@Pilchie after meeting with @mkArtakMSFT today, I think it makes sense to use the area-mvc label and use the MVC syncs to discuss this.

@halter73 halter73 added area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates and removed area-servers labels Feb 4, 2021

const int defaultOrder = 0;

foreach (var routeAttribute in routeAttributes)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be problematic to say

a) one action -> one route attribute (exactly one method constraint \ template)
b) each route attribute must have a template

The current implementation simply skips over route attributes without a template.

catch (IOException ex)
{
LogRequestBodyIOException(httpContext, ex);
httpContext.Abort();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pranavkm is this how MVC handles it? I thought it set a 400 response status instead?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, why is it aborting the connection?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSON parsing errors result in a 400 response that includes details from the parser. Other IO errors result in 400s.

IMO, I assumed the entirety of this type was WIP so I hadn't looked at it in detail. We should factor it because this isn't a sustainable format to maintain.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very much a work in progress. I imagine we'll want to make logic like this reusable from different kinds of code-gen and move it out of this class at some point.

As for the status code, it now sets a 400 for InvalidDataExceptions. For aborted request, the status code doesn't matter much since the response doesn't get written to a socket and Kestrel will set the status code to 0 before hosting logs it anyway.

Copy link
Member

@Tratcher Tratcher Feb 18, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, we missed the invalid Json scenario here. That should also return a 400. edit Nevermind, is InvalidDataException the exception thrown for invalid Json?

@halter73 halter73 merged commit 3f0bec1 into main Feb 18, 2021
@halter73 halter73 deleted the halter73/29684 branch February 18, 2021 22:58
@halter73 halter73 added the api-ready-for-review API is ready for formal API review - https://github.com/dotnet/apireviews label Mar 1, 2021
@ghost
Copy link

ghost commented Mar 1, 2021

Thank you for submitting this for API review. This will be reviewed by @dotnet/aspnet-api-review at the next meeting of the ASP.NET Core API Review group. Please ensure you take a look at the API review process documentation and ensure that:

  • The PR contains changes to the reference-assembly that describe the API change. Or, you have included a snippet of reference-assembly-style code that illustrates the API change.
  • The PR describes the impact to users, both positive (useful new APIs) and negative (breaking changes).
  • Someone is assigned to "champion" this change in the meeting, and they understand the impact and design of the change.

@halter73 halter73 added feature-minimal-actions Controller-like actions for endpoint routing and removed api-ready-for-review API is ready for formal API review - https://github.com/dotnet/apireviews labels Mar 1, 2021
@amcasey amcasey added the area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc label Jun 2, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates feature-minimal-actions Controller-like actions for endpoint routing
Projects
None yet
Development

Successfully merging this pull request may close these issues.