Skip to content
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

RequestDelegateFactory should produce clear errors for invalid Delegates #30322

Closed
davidfowl opened this issue Feb 20, 2021 · 4 comments
Closed
Assignees
Labels
area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc area-web-frameworks enhancement This issue represents an ask for new feature or an enhancement to an existing one feature-minimal-actions Controller-like actions for endpoint routing Priority:1 Work that is critical for the release, but we could probably ship without
Milestone

Comments

@davidfowl
Copy link
Member

davidfowl commented Feb 20, 2021

Is your feature request related to a problem? Please describe.

We should make sure its clear what is supported in MapAction instead of it failing with hard to grok errors. For example, #31658 (comment)

Describe the solution you'd like

I think we should have both runtime and compile time validation (an analyzer where possible).

Scenarios for MapActions

1. Forgetting to register a service with the DI container generates an incorrect error as the parameter is viewed as second [FromBody] attribute. Although from the user standpoint the second [FromBody] attribute is not explicit - #35086

app.MapPost("/createUser", ([FromBody] UserDTO userDTO, UserRepository userRepo) => {
    return $"user created  - {userDTO.LastName} - {userRepo.CreateUser(userDTO).LastName}";
 });

The user's intention was to inject the UserRepository as service to use.

Error message:

System.InvalidOperationException: 'Action cannot have more than one FromBody attribute.'

2. Allowing query strings on Post is not semantically correct and might be viewed as a security issue. The user would have expected the [FromQuery] attribute to be ignored/should not bind or should throw an exception at the startup time. Nothing to do.

 app.MapPost("/createUser", ([FromQuery] string userName, [FromBody] User user,) => $"user created- {userName} - {user.LastName} ");

3. FromBody attribute on MapGet does not throw an exception. (User could have copied the method and forget to remove the frombody). Postman allows you to add a body to a get method. However, without a body added to the request, we get 400 which is the correct behavior. If the body is present, binding happens. - Nothing to do - this is by design

builder.Services.AddSingleton<IUserRepository, UserRepository>()

app.MapGet("/findUser/{id}", (int id, [FromBody] UserDTO userDTO, IUserRepository userRepo) => {
    return $"user found - {userDTO.LastName} - {userRepo.FindUserById(id).LastName}";
})

4. In MapGet an implicit injection of any service returns a 400 if the user forgot to register the service in the DI container. However, an explicit use of [FromServices] returns the correct error. This is inconsistent - #35086

  app.MapGet("/findUser/{id}", (int id, UserRepository userRepo) => {
    return $"user found - {id} - {userRepo.FindUserById(id).LastName}";
  });

Error for explicit use of [FromServices]

  app.MapGet("/findUser/{id}", (int id, [FromServices] IUserRepository userRepo) => {
    return $"user found - {id} - {userRepo.FindUserById(id).LastName}";
  });
  System.InvalidOperationException: No service for type 'IUserRepository' has been registered.
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, 
  Type serviceType)
....

5. Improve the error message when routes parameters do match or just ignore casing(lower/upper) - This is a common mistake that users may make - #35087

   app.MapGet("/user/{FirstName}/{lastName}", ([FromRoute()]string? firstName, [FromRoute()]string lastName) => 
    UserService.GetUser(firstName, lastName, "aa"));

Error message (Notice FirstName != firstName):

System.InvalidOperationException: 'firstName is not a route paramter.'

6. What should we do when someone uses attributes that should not be allowed on certain methods ? see example below: -#35088

  app.MapGet("/user", ([Bind("FirstName,LastName")] User user) => $"username {user.FirstName}");

7. Adding an optional/nullability (?) in the route pattern, messes up the Swagger UI and the json file still recognizes as required field. - #35081

   app.MapGet("/user/{id?}", (int? id) => $"userId {id}");

Notice the missing closing bracket
image

@davidfowl davidfowl added feature-minimal-actions Controller-like actions for endpoint routing enhancement This issue represents an ask for new feature or an enhancement to an existing one area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates labels Feb 20, 2021
@javiercn javiercn added this to the Next sprint planning milestone Feb 21, 2021
@ghost
Copy link

ghost commented Feb 21, 2021

Thanks for contacting us.
We're moving this issue to the Next sprint planning milestone for future evaluation / consideration. We will evaluate the request when we are planning the work for the next milestone. To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

@halter73 halter73 added area-servers and removed area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates labels Mar 30, 2021
@halter73 halter73 changed the title MapAction should validate the method before generating code RequestDelegateFactory should produce clear errors for invalid Delegates May 4, 2021
@halter73 halter73 modified the milestones: Next sprint planning, 6.0-rc1 Jul 8, 2021
@halter73 halter73 added this to Done in Minimal APIs 6.0 Jul 8, 2021
@halter73 halter73 moved this from Done to To do in Minimal APIs 6.0 Jul 8, 2021
@rafikiassumani-msft rafikiassumani-msft moved this from To do to Ready in Minimal APIs 6.0 Jul 19, 2021
@rafikiassumani-msft rafikiassumani-msft added the Priority:1 Work that is critical for the release, but we could probably ship without label Jul 20, 2021
@rafikiassumani-msft rafikiassumani-msft self-assigned this Jul 21, 2021
@rafikiassumani-msft rafikiassumani-msft modified the milestones: 6.0-rc1, 6.0-rc2 Jul 26, 2021
@rafikiassumani-msft rafikiassumani-msft moved this from Ready to User experience in Minimal APIs 6.0 Aug 6, 2021
@rafikiassumani-msft rafikiassumani-msft removed this from User experience in Minimal APIs 6.0 Aug 23, 2021
@rafikiassumani-msft rafikiassumani-msft added this to Ready in Minimal APIs 6.0 via automation Aug 23, 2021
@rafikiassumani-msft rafikiassumani-msft moved this from Ready to User experience in Minimal APIs 6.0 Aug 24, 2021
@rafikiassumani-msft rafikiassumani-msft removed this from the 6.0-rc1 milestone Aug 26, 2021
@rafikiassumani-msft rafikiassumani-msft added this to the 6.0-rc2 milestone Aug 26, 2021
@rafikiassumani-msft rafikiassumani-msft removed their assignment Aug 31, 2021
@BrennanConroy
Copy link
Member

This might be done, all bullets either have a linked issue/PR that is closed or are marked "nothing to do".

Minimal APIs 6.0 automation moved this from User experience to Done Sep 16, 2021
@BrennanConroy
Copy link
Member

Lets verify first before closing

@BrennanConroy BrennanConroy reopened this Sep 16, 2021
Minimal APIs 6.0 automation moved this from Done to Ready Sep 16, 2021
@rafikiassumani-msft rafikiassumani-msft moved this from Ready to In progress in Minimal APIs 6.0 Sep 17, 2021
@BrennanConroy
Copy link
Member

All 7 scenarios validated against RTM build and before/after for each shown below.

Scenario 1:

// Forgot to register UserRepository as service
app.MapPost("/createUser", ([FromBody] UserDTO userDTO, UserRepository userRepo) => {
    return $"user created  - {userDTO.LastName} - {userRepo.CreateUser(userDTO).LastName}";
 });

Before:
System.InvalidOperationException: 'Action cannot have more than one FromBody attribute.'

After:

Unhandled exception. System.InvalidOperationException: Failure to infer one or more parameters.
Below is the list of parameters that we found:

Parameter           | Source
---------------------------------------------------------------------------------
userDTO             | Body (Attribute)
userRepo            | UNKNOWN


Did you mean to register the "UNKNOWN" parameters as a Service?

Scenario 2:

app.MapPost("/createUser", ([FromQuery] string userName, [FromBody] User user,) => $"user created- {userName} - {user.LastName} ");
Query on POST might be viewed as a security issue - we decided to do nothing here

Scenario 3:

builder.Services.AddSingleton<IUserRepository, UserRepository>();

app.MapGet("/findUser/{id}", (int id, [FromBody] UserDTO userDTO, IUserRepository userRepo) => {
    return $"user found - {userDTO.LastName} - {userRepo.FindUserById(id).LastName}";
});

[FromBody] on GET isn't common and generally a mistake on the users part. We decided to do nothing here.

Scenario 4

Part 1:

// No UserRepository registered
app.MapGet("/findUser/{id}", (int id, UserRepository userRepo) => {
    return $"user found - {id} - {userRepo.FindUserById(id).LastName}";
});

Part 2:

app.MapGet("/findUser/{id}", (int id, [FromServices] IUserRepository userRepo) => {
    return $"user found - {id} - {userRepo.FindUserById(id).LastName}";
});

Before:
Part 1: Returns 400
Pat 2: 500 and exception logged

An unhandled exception has occurred while executing the request.
      System.InvalidOperationException: No service for type 'IUserRepository' has been registered.

After:
Part 1: On startup:

Unhandled exception. System.InvalidOperationException: Body was inferred but the method does not allow inferred body parameters.
Below is the list of parameters that we found:

Parameter           | Source
---------------------------------------------------------------------------------
id                  | Route (Inferred)
userRepo            | Body (Inferred)


Did you mean to register the "Body (Inferred)" parameter(s) as a Service or apply the [FromService] or [FromBody] attribute?

Part 2: On Request
500 and exception logged

An unhandled exception has occurred while executing the request.
      System.InvalidOperationException: No service for type 'IUserRepository' has been registered.

Scenario 5

Improve the error message when routes parameters do not match or just ignore casing(lower/upper)

app.MapGet("/user/{FirstName}/{lastName}", ([FromRoute]string? firstName, [FromRoute]string lastName) => 
    $"{firstName} {lastName});

Before:
System.InvalidOperationException: 'firstName is not a route paramter.'

After:
FirstName matches firstName

Scenario 6

What should we do when someone uses attributes that should not be allowed on certain methods?

app.MapGet("/user", ([Bind("FirstName,LastName")] User user) => $"username {user.FirstName}");

Before:
Nothing

After:
Analyzer warning:
Warning ASP0003 BindAttribute should not be specified for a MapGet Delegate parameter

Scenario 7

Adding an optional/nullability (?) in the route pattern, messes up the Swagger UI and the json file still recognizes as required field.
app.MapGet("/user/{id?}", (int? id) => $"userId {id}");

Before:
image

After:
image

Minimal APIs 6.0 automation moved this from In progress to Done Sep 22, 2021
@ghost ghost locked as resolved and limited conversation to collaborators Nov 3, 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 subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc area-web-frameworks enhancement This issue represents an ask for new feature or an enhancement to an existing one feature-minimal-actions Controller-like actions for endpoint routing Priority:1 Work that is critical for the release, but we could probably ship without
Projects
No open projects
Development

No branches or pull requests

7 participants