OData / AspNetCoreOData

ASP.NET Core OData: A server library built upon ODataLib and ASP.NET Core
Other
446 stars 157 forks source link

[Request] Add support for arbitrary models in custom actions #848

Open julealgon opened 1 year ago

julealgon commented 1 year ago

Custom functions today are very cumbersome to use: they require the use of parameters that are bound to a custom dictionary-like structure, ODataActionParameters. The consumer then needs to read the parameters from this object to use them.

This goes against standard MVC model binding which allows (and recommends) binding your parameters directly into a strongly typed model object that represent them.

I think adding native support in OData to bind the body of the action to an actual custom model (like MVC does) would make the framework vastly easier to use.

From the docs: https://learn.microsoft.com/en-us/odata/webapi-8/fundamentals/actions-functions#bound-action-1

Instead of:

    [HttpPost("odata/Books({key})/Rate")]
    public IActionResult Rate([FromODataUri] string key, ODataActionParameters parameters)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest();
        }

        int rating = (int)parameters["rating"];

        if (rating < 0)
        {
            return BadRequest();
        }

        return Ok(new BookRating() { BookID = key, Rating = rating });
    }

We'd do:

    public class RatingViewModel
    {
        public int Rating { get; set; }
    }

    [HttpPost("odata/Books({key})/Rate")]
    public IActionResult Rate([FromODataUri] string key, RatingViewModel ratingViewModel)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest();
        }

        if (ratingViewModel.Rating < 0)
        {
            return BadRequest();
        }

        return Ok(new BookRating() { BookID = key, Rating = ratingViewModel.Rating });
    }

This would naturally also open the door for standard validation using DataAnnotations or FluentValidation, making it much more seamless when coming from a normal MVC route.

When specifying the EDM for the function, I'm not entirely sure what would need to be done there, but maybe something that would generate the required EDM automatically based on the complex type could be achieved?

    builder.EntityType<Book>()
        .Action("rate")
        .Parameter<RatingViewModel>("ratingViewModel");

The Parameter method could identify that the type is not a primitive type, and apply the rules as needed (either transparently transform it into multiple Parameter<primitiveType> calls for each property, or just support this natively at the EDM level as well.

xuzhg commented 1 year ago

@julealgon Actually, I had a rough design for a similar situation. Correct me at https://github.com/OData/AspNetCoreOData/blob/main/src/Microsoft.AspNetCore.OData/Formatter/FromODataBodyAttribute.cs

It's not finished yet until I have more clear customer usages. Any thoughts?

julealgon commented 1 year ago

@julealgon Actually, I had a rough design for a similar situation. Correct me at https://github.com/OData/AspNetCoreOData/blob/main/src/Microsoft.AspNetCore.OData/Formatter/FromODataBodyAttribute.cs

It's not finished yet until I have more clear customer usages. Any thoughts?

That's certainly a start right there!

Personally, I think it would be awesome if we found a way to do it without requiring a custom attribute or at least a way to default to that attribute for action complex type parameters similar to how standard MVC routes default to [FromBody] for POST requests when using [ApiController].

I don't precisely know how the convention was created with [ApiController] but would suggest looking into it to see if we could define a similar convention for OData actions.

julealgon commented 5 months ago

Forgot to link this with the other issue were we discussed about it and I mentioned I'd open it:

@habex-ch FYI