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

#907: Enable read the untyped collection #905

Merged
merged 4 commits into from
May 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,7 @@ private object ReadNestedResourceInline(ODataResourceWrapper resourceWrapper, IE
{
ODataResourceWrapper resourceWrapper = CreateResourceWrapper(elementType, refLinkWrapper, readContext);
resourceSetWrapper.Resources.Add(resourceWrapper);
resourceSetWrapper.Items.Add(resourceWrapper); // in the next major release, we should only use 'Items'.
}

return resourceSetWrapper;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//-----------------------------------------------------------------------------
// <copyright file="ODataPrimitiveWrapper.cs" company=".NET Foundation">
// Copyright (c) .NET Foundation and Contributors. All rights reserved.
// See License.txt in the project root for license information.
// </copyright>
//------------------------------------------------------------------------------

using Microsoft.OData;

namespace Microsoft.AspNetCore.OData.Formatter.Wrapper
{
/// <summary>
/// Encapsulates <see cref="ODataPrimitiveValue"/> in an Untyped (declared or undeclared) collection.
/// </summary>
public class ODataPrimitiveWrapper : ODataItemWrapper
{
/// <summary>
/// Initializes a new instance of <see cref="ODataPrimitiveWrapper"/>.
/// </summary>
/// <param name="value">The wrapped primitive value.</param>
public ODataPrimitiveWrapper(ODataPrimitiveValue value)
{
Value = value ?? throw Error.ArgumentNull(nameof(value));
}

/// <summary>
/// Gets the wrapped <see cref="ODataPrimitiveValue"/>.
/// </summary>
public ODataPrimitiveValue Value { get; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
// </copyright>
//------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Threading.Tasks;
Expand Down Expand Up @@ -191,6 +190,16 @@ private static void ReadODataItem(ODataReader reader, Stack<ODataItemWrapper> it
linkResourceSetWrapper.DeltaItems.Add(linkBaseWrapper);
break;

case ODataReaderState.Primitive:
Contract.Assert(itemsStack.Count > 0, "The primitive should be a non-null primitive value within an untyped collection.");
// Be noted:
// 1) if a 'null' value or a resource/object in the untyped collection goes to ODataResource flow
// 2) if a collection value in the untyped collection goes to ODataResourceSet flow
// 3) Since it's untyped, there's no logic for 'Enum' value, it means it's treated as primitive value.
habbes marked this conversation as resolved.
Show resolved Hide resolved
ODataResourceSetWrapper resourceSetParentWrapper = (ODataResourceSetWrapper)itemsStack.Peek();
habbes marked this conversation as resolved.
Show resolved Hide resolved
resourceSetParentWrapper.Items.Add(new ODataPrimitiveWrapper((ODataPrimitiveValue)reader.Item));
break;

default:
Contract.Assert(false, "We should never get here, it means the ODataReader reported a wrong state.");
break;
Expand Down Expand Up @@ -229,6 +238,7 @@ private static void ReadResource(ODataReader reader, Stack<ODataItemWrapper> ite
if (parentResourceSet != null)
{
parentResourceSet.Resources.Add(resourceWrapper);
parentResourceSet.Items.Add(resourceWrapper);// in the next major release, we should only use 'Items'.
}
else if (parentDeleteResourceSet != null)
{
Expand Down Expand Up @@ -301,11 +311,18 @@ private static void ReadResourceSet(ODataReader reader, Stack<ODataItemWrapper>
ODataResourceSetWrapper resourceSetWrapper = new ODataResourceSetWrapper(resourceSet);
if (itemsStack.Count > 0)
{
ODataNestedResourceInfoWrapper parentNestedResourceInfo = (ODataNestedResourceInfoWrapper)itemsStack.Peek();
Contract.Assert(parentNestedResourceInfo != null, "this has to be an inner resource set. inner resource sets always have a nested resource info.");
Copy link
Contributor

Choose a reason for hiding this comment

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

Can itemStack contain null entries? Or does this assertion (and others like it in this class) make the assumption that the cast will return null if the conversion fails? If the item at the top of the stack is not the expected type, an InvalidCastException will be thrown. If we expect null, we should consider using itemStack.Peek() as ODataNestedResourceInfoWrapper.
But I don't mind throwing the exception if that was by design.

Copy link
Contributor

Choose a reason for hiding this comment

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

I know this doesn't apply to the new code change, but I've seen it in other places in this class and thought it's worth pointing out.

Copy link
Member Author

Choose a reason for hiding this comment

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

ODL reading logic guarantees so far.
In my plan, I will retrieve the reading as a service and customer can customize the reading process. It's a future feature.

Copy link
Contributor

Choose a reason for hiding this comment

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

The guarantee makes sense. My comment was more on the fact that Contact.Assert verifies that the parentNestedResourceInfo is not null. However, if the cast would have failed, parentNestedResourceInfo would not have been set to null, an exception would have been thrown, and therefore the Contact.Assert would not have been reached.

Copy link
Member Author

Choose a reason for hiding this comment

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

itemsStack.Peek() could return 'null', I think.
ODataNestedResourceInfoWrapper parentNestedResourceInfo = (ODataNestedResourceInfoWrapper)null;

is valid. We can sync it more, but it's no scope of this PR. :)

Contract.Assert(parentNestedResourceInfo.NestedResourceInfo.IsCollection == true, "Only collection nested properties can contain resource set as their child.");
Contract.Assert(parentNestedResourceInfo.NestedItems.Count == 0, "Each nested property can contain only one resource set as its direct child.");
parentNestedResourceInfo.NestedItems.Add(resourceSetWrapper);
ODataItemWrapper peekedWrapper = itemsStack.Peek();
if (peekedWrapper is ODataNestedResourceInfoWrapper parentNestedResourceInfo)
{
Contract.Assert(parentNestedResourceInfo.NestedResourceInfo.IsCollection == true, "Only collection nested properties can contain resource set as their child.");
Contract.Assert(parentNestedResourceInfo.NestedItems.Count == 0, "Each nested property can contain only one resource set as its direct child.");
parentNestedResourceInfo.NestedItems.Add(resourceSetWrapper);
}
else
{
ODataResourceSetWrapper parentResourceSet = (ODataResourceSetWrapper)peekedWrapper;
parentResourceSet.Items.Add(resourceSetWrapper);
}
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public sealed class ODataResourceSetWrapper : ODataResourceSetBaseWrapper
public ODataResourceSetWrapper(ODataResourceSet resourceSet)
{
Resources = new List<ODataResourceWrapper>();
Items = new List<ODataItemWrapper>();
ResourceSet = resourceSet;
}

Expand All @@ -35,5 +36,14 @@ public ODataResourceSetWrapper(ODataResourceSet resourceSet)
/// Resource set only contains resources.
/// </summary>
public IList<ODataResourceWrapper> Resources { get; }

/// <summary>
/// Gets the nested items of this ResourceSet.
/// Since we have 'Resources' to contain ODataResource items in the collection (we have to keep it avoid breaking changes).
/// This list is used to hold other items also, for example a primitive or a collection, etc.
/// I assume the order of items does matter, so 'Items' contains all 'Resources' item to keep the same order.
/// In the next major release, we should combine 'Resources' and 'Items' together.
/// </summary>
public IList<ODataItemWrapper> Items { get; }
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there supposed to be code somewhere that reads the Items property to actually populate the CLR type?

Copy link
Member Author

Choose a reason for hiding this comment

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

What do you mean? @corranrogue9

To add a Kind property in the base type and implement to return different kind in different wrapper?

Copy link
Contributor

Choose a reason for hiding this comment

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

I also have a similar question:

  1. Why do we need to make it public? Do we expect customers to use this?
  2. If we make it public, do we expect customers be both read and update the collection?

Copy link
Member Author

Choose a reason for hiding this comment

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

Since 'Resources' is open/public, we can't stop customers adding new resources into that.
Same reason, we can't stop customers to modify the 'Items'.

Copy link
Contributor

Choose a reason for hiding this comment

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

Items is being introduced in this PR, so it doesn't exist in existing code. So my question was whether we need to expose it to customers and why.

Copy link
Member Author

Choose a reason for hiding this comment

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

Customer may override the 'CreateResourceSet' method in ODataResourceSetDeserializer to access the 'Items'.

Copy link
Contributor

Choose a reason for hiding this comment

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

I see.

}
}
25 changes: 25 additions & 0 deletions src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5884,6 +5884,22 @@
Gets the nested items that are part of this nested resource info.
</summary>
</member>
<member name="T:Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataPrimitiveWrapper">
<summary>
Encapsulates <see cref="T:Microsoft.OData.ODataPrimitiveValue"/> in an Untyped (declared or undeclared) collection.
</summary>
</member>
<member name="M:Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataPrimitiveWrapper.#ctor(Microsoft.OData.ODataPrimitiveValue)">
<summary>
Initializes a new instance of <see cref="T:Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataPrimitiveWrapper"/>.
</summary>
<param name="value">The wrapped primitive value.</param>
</member>
<member name="P:Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataPrimitiveWrapper.Value">
<summary>
Gets the wrapped <see cref="T:Microsoft.OData.ODataPrimitiveValue"/>.
</summary>
</member>
<member name="T:Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataReaderExtensions">
<summary>
Extension methods for <see cref="T:Microsoft.OData.ODataReader"/>.
Expand Down Expand Up @@ -5969,6 +5985,15 @@
Resource set only contains resources.
</summary>
</member>
<member name="P:Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceSetWrapper.Items">
<summary>
Gets the nested items of this ResourceSet.
Since we have 'Resources' to contain ODataResource items in the collection (we have to keep it avoid breaking changes).
This list is used to hold other items also, for example a primitive or a collection, etc.
I assume the order of items does matter, so 'Items' contains all 'Resources' item to keep the same order.
In the next major release, we should combine 'Resources' and 'Items' together.
</summary>
</member>
<member name="T:Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceWrapper">
<summary>
Encapsulates an <see cref="T:Microsoft.OData.ODataResource"/>.
Expand Down
4 changes: 4 additions & 0 deletions src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -626,10 +626,14 @@ Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataNestedResourceInfoWrapper
Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataNestedResourceInfoWrapper.NestedItems.get -> System.Collections.Generic.IList<Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataItemWrapper>
Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataNestedResourceInfoWrapper.NestedResourceInfo.get -> Microsoft.OData.ODataNestedResourceInfo
Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataNestedResourceInfoWrapper.ODataNestedResourceInfoWrapper(Microsoft.OData.ODataNestedResourceInfo nestedInfo) -> void
Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataPrimitiveWrapper
Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataPrimitiveWrapper.ODataPrimitiveWrapper(Microsoft.OData.ODataPrimitiveValue value) -> void
Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataPrimitiveWrapper.Value.get -> Microsoft.OData.ODataPrimitiveValue
Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataReaderExtensions
Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceSetBaseWrapper
Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceSetBaseWrapper.ODataResourceSetBaseWrapper() -> void
Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceSetWrapper
Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceSetWrapper.Items.get -> System.Collections.Generic.IList<Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataItemWrapper>
Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceSetWrapper.ODataResourceSetWrapper(Microsoft.OData.ODataResourceSet resourceSet) -> void
Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceSetWrapper.Resources.get -> System.Collections.Generic.IList<Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceWrapper>
Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceSetWrapper.ResourceSet.get -> Microsoft.OData.ODataResourceSet
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//-----------------------------------------------------------------------------
// <copyright file="ODataPrimitiveWrapperTests.cs" company=".NET Foundation">
// Copyright (c) .NET Foundation and Contributors. All rights reserved.
// See License.txt in the project root for license information.
// </copyright>
//------------------------------------------------------------------------------

using Microsoft.AspNetCore.OData.Formatter.Wrapper;
using Microsoft.AspNetCore.OData.Tests.Commons;
using Microsoft.OData;
using Xunit;

namespace Microsoft.AspNetCore.OData.Tests.Formatter.Wrapper
{
public class ODataPrimitiveWrapperTests
{
[Fact]
public void Ctor_ThrowsArgumentNull_PrimitiveValue()
{
// Arrange & Act & Assert
ExceptionAssert.ThrowsArgumentNull(() => new ODataPrimitiveWrapper(null), "value");
}

[Fact]
public void Ctor_Sets_CorrectProperties()
{
// Arrange & Act & Assert
ODataPrimitiveValue oDataPrimitiveValue = new ODataPrimitiveValue(42);

// Act
ODataPrimitiveWrapper wrapper = new ODataPrimitiveWrapper(oDataPrimitiveValue);

// Assert
Assert.Same(oDataPrimitiveValue, wrapper.Value);
}
}
}
Loading