OData / AspNetCoreOData

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

Support record as request payload type #1001

Open aetos382 opened 1 year ago

aetos382 commented 1 year ago

Assemblies affected ASP.NET Core OData 8.2

Describe the bug Does not support record as request payload type.

Reproduce steps Clone this repo and run the app. https://github.com/aetos382/RecordSupport

Data Model

public record Product(
    string Id,
    string Name);
public record Product(
    string Id,
    string Name);
public record Customer(
    string Id,
    string Name);

EDM (CSDL) Model

<edmx:Edmx xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx" Version="4.0">
  <edmx:DataServices>
    <Schema xmlns="http://docs.oasis-open.org/odata/ns/edm" Namespace="RecordSupport.Models">
      <EntityType Name="Order">
        <Key>
          <PropertyRef Name="Id"/>
        </Key>
        <Property Name="Id" Type="Edm.String" Nullable="false"/>
        <Property Name="Name" Type="Edm.String" Nullable="false"/>
      </EntityType>
      <EntityType Name="Customer">
        <Key>
          <PropertyRef Name="Id"/>
        </Key>
        <Property Name="Id" Type="Edm.String" Nullable="false"/>
        <Property Name="Name" Type="Edm.String" Nullable="false"/>
      </EntityType>
    </Schema>
    <Schema xmlns="http://docs.oasis-open.org/odata/ns/edm" Namespace="Default">
      <EntityContainer Name="Container">
        <EntitySet Name="Orders" EntityType="RecordSupport.Models.Order"/>
        <EntitySet Name="Customers" EntityType="RecordSupport.Models.Customer"/>
      </EntityContainer>
    </Schema>
  </edmx:DataServices>
</edmx:Edmx>

Request/Response

POST http://localhost:xxx/api/Products
Content-Type: application/json

{
  "Id": "P1",
  "Name": "P1"
}
POST http://localhost:xxx/api/Orders
Content-Type: application/json

{
  "Id": "O1",
  "Name": "O1"
}
POST http://localhost:xxx/api/Customers
Content-Type: application/json

{
  "Id": "C1",
  "Name": "C1"
}

Expected behavior All request payloads are successfully deserialized and passed as arguments to the controller's action method.

Actual behavior The request for Products succeeds. This controller does not use any OData functionality. For Orders, the action method is called, but the argument is always passed null. For Customers, no action methods are called.

Additional context Requests to Orders and Customers seem to raise the following exceptions.

Cannot dynamically create an instance of type 'RecordSupport.Models.Order'. Reason: No parameterless constructor defined.

   at System.RuntimeType.ActivatorCache..ctor(RuntimeType rt)
   at System.RuntimeType.CreateInstanceDefaultCtor(Boolean publicOnly, Boolean wrapExceptions)
   at Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataResourceDeserializer.CreateResourceInstance(IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext)
   at Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataResourceDeserializer.ReadResource(ODataResourceWrapper resourceWrapper, IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext)
   at Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataResourceDeserializer.ReadInline(Object item, IEdmTypeReference edmType, ODataDeserializerContext readContext)
   at Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataResourceDeserializer.<ReadAsync>d__2.MoveNext()
   at Microsoft.AspNetCore.OData.Formatter.ODataInputFormatter.<ReadFromStreamAsync>d__10.MoveNext()
   at Microsoft.AspNetCore.OData.Formatter.ODataInputFormatter.LoggerError(HttpContext context, Exception ex)
   at Microsoft.AspNetCore.OData.Formatter.ODataInputFormatter.<ReadFromStreamAsync>d__10.MoveNext()
   at Microsoft.AspNetCore.OData.Formatter.ODataInputFormatter.<ReadRequestBodyAsync>d__8.MoveNext()
julealgon commented 1 year ago

It seems like records is just a special case, but this problem is much wider.

Does it work if you provide this Order implementation?

public class Product
{
    public Product(string id, string name)
    {
        this.Id = id;
        this.Name = name;
    }

    public string Id { get; }

    public string Name { get; }
}

If it produces the same error (which I assume it will), I'd suggest renaming your issue to talk about "types without default constructor" instead of limiting it to "records".

This would basically be a duplicate of:

aetos382 commented 1 year ago

Surely the essential reason is that the payload type has no parameterless constructor. But records are specifically supported in ASP.NET Core WebAPI. I think there should be no difference in that limitation between using regular WebAPI and using OData. So what is available in the standard WebAPI should be available in OData as well.

If ASP.NET OData has plans to go beyond the scope of standard WebAPI support and fully support bindings to types that have no parameterless constructors, that would be great. However, if it will be a long time before that release, I would like to see support for records as a special case for now.