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

ODL library allows you to write a nested resource as the value for a primitive property #3057

Open
gathogojr opened this issue Sep 6, 2024 · 0 comments
Labels

Comments

@gathogojr
Copy link
Contributor

ODL library allows you to write a nested resource as the value for a primitive property

Assemblies affected

  • Microsoft.OData.Core 7.x
  • Microsoft.OData.Core 8.x

Reproduce steps

Consider the following data models, where Customer is a navigation property:

public class Order
{
    public int Id { get; set; }
    public decimal Amount { get; set; }
    public Customer Customer { get; set; }
}

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
}

The following code for writing an OData payload executes okay but produces an invalid output - the value for the Amount primitive property is a nested Customer resource/object:

var modelBuilder = new ODataConventionModelBuilder();
modelBuilder.EntitySet<Order>("Orders");
modelBuilder.EntitySet<Customer>("Customers");

var model = modelBuilder.GetEdmModel();

var orderEntityType = model.SchemaElements.First(d => d.Name == "Order") as IEdmEntityType;
var ordersEntitySet = model.EntityContainer.FindEntitySet("Orders");

var messageWriterSettings = new ODataMessageWriterSettings
{
    EnableMessageStreamDisposal = false,
    Version = ODataVersion.V4,
    ODataUri = new ODataUri { ServiceRoot = new Uri("http://tempuri.org") }
};

var tempStream = new MemoryStream();

IODataResponseMessage asyncResponseMessage = new InMemoryMessage { Stream = tempStream };

await using (var messageWriter = new ODataMessageWriter(asyncResponseMessage, messageWriterSettings, model))
{
    var orderResource = new ODataResource
    {
        TypeName = typeof(Order).FullName,
        Id = new Uri("http://tempuri.org/Orders(1)"),
        Properties = new List<ODataProperty>
        {
            new ODataProperty { Name = "Id", Value = 1 }
        }
    };

    var customerResource = new ODataResource
    {
        TypeName = typeof(Customer).FullName,
        Id = new Uri("http://tempuri.org/Customers(1)"),
        Properties = new List<ODataProperty>
        {
            new ODataProperty { Name = "Id", Value = 1 },
            new ODataProperty { Name = "Name", Value = "Sue" }
        }
    };

    var customerNestedResourceInfo = new ODataNestedResourceInfo
    {
        Name = "Amount", // Not a complex, complex collection or navigation property
        IsCollection = false,
        Url = new Uri("http://tempuri.org/Orders(1)/Amount")
    };

    var resourceWriter = await messageWriter.CreateODataResourceWriterAsync(ordersEntitySet, orderEntityType);

    await resourceWriter.WriteStartAsync(orderResource);
    await resourceWriter.WriteStartAsync(customerNestedResourceInfo);
    await resourceWriter.WriteStartAsync(customerResource);
    await resourceWriter.WriteEndAsync();
    await resourceWriter.WriteEndAsync();
    await resourceWriter.WriteEndAsync();

    tempStream.Position = 0;
    var result = await new StreamReader(tempStream).ReadToEndAsync();

    // result: 
    // {
    //     "@odata.context":"http://tempuri.org/$metadata#Orders/$entity",
    //     "@odata.id":"http://tempuri.org/Orders(1)",
    //     "Id":1,
    //     "Amount":{"@odata.id":"http://tempuri.org/Customers(1)","Id":1,"Name":"Sue"}
    // }
}

Even worse, the library allows you to write the primitive property both with a primitive value as well as a nested resource resulting into a duplicate property, i.e., if you include the Amount property in the Properties collection as follows,

    var orderResource = new ODataResource
    {
        TypeName = typeof(Order).FullName,
        Id = new Uri("http://tempuri.org/Orders(1)"),
        Properties = new List<ODataProperty>
        {
            new ODataProperty { Name = "Id", Value = 1 },
            new ODataProperty { Name = "Amount", Value = 130 }
        }
    };

Below is how the output payload will look like:

{
    "@odata.context":"http://tempuri.org/$metadata#Orders/$entity",
    "@odata.id":"http://tempuri.org/Orders(1)",
    "Id":1,
    "Amount":130,
    "Amount":{"@odata.id":"http://tempuri.org/Customers(1)","Id":1,"Name":"Sue"}
}

Expected result

The library should validate the property assigned to the Name property of an ODataNestedResourceInfo is either a complex or complex collection or navigation property.

Actual result

The library does not validate the property assigned to the Name property of an ODataNestedResourceInfo is either a complex or complex collection or navigation property.

Additional detail

Here's the definition of the InMemoryMessage type:

internal class InMemoryMessage : IODataResponseMessage, IODataResponseMessageAsync, IODataRequestMessage, IODataRequestMessageAsync        
{
    private readonly Dictionary<string, string> headers;

    public InMemoryMessage()
    {
        headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
    }

    public IEnumerable<KeyValuePair<string, string>> Headers
    {
        get { return this.headers; }
    }

    public int StatusCode { get; set; }

    public Uri Url { get; set; }

    public string Method { get; set; }

    public Stream Stream { get; set; }

    public string GetHeader(string headerName)
    {
        return this.headers.TryGetValue(headerName, out string headerValue) ? headerValue : null;
    }

    public void SetHeader(string headerName, string headerValue)
    {
        headers[headerName] = headerValue;
    }

    public Stream GetStream()
    {
        return this.Stream;
    }

    public Task<Stream> GetStreamAsync()
    {
        TaskCompletionSource<Stream> taskCompletionSource = new TaskCompletionSource<Stream>();
        taskCompletionSource.SetResult(this.Stream);
        return taskCompletionSource.Task;
    }
}
@gathogojr gathogojr added the bug label Sep 6, 2024
@marabooy marabooy added the P3 label Sep 10, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants