Skip to content

Commit

Permalink
Various updates including:
Browse files Browse the repository at this point in the history
- Rebase to tip of ODL master branch.
- Add e2e test ExpandedCustomerEntryOmitNullValuesTest with various cases exercising null values.
- Updates restore null value processing related to base-type properties navigation properties by odata.navigationlink annotations.
- Added output for comparison of omittedNullValues of true & false in same test cases.
- Added test cases for unknonwn(dynamic) properties to verify that no content are restored.
- Added test cases for expanded navigation link to verify that omitted properties are restored for non-null entity, and that no content are restored for null entity.
  • Loading branch information
biaol-odata committed Mar 30, 2018
1 parent bed0bd3 commit bc4a274
Show file tree
Hide file tree
Showing 10 changed files with 621 additions and 157 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,8 @@ private void WriteProperty(
{
WriterValidationUtils.ValidatePropertyNotNull(property);

ODataValue value = property.ODataValue;

string propertyName = property.Name;

if (this.JsonLightOutputContext.MessageWriterSettings.Validations != ValidationKinds.None)
Expand All @@ -197,6 +199,19 @@ private void WriteProperty(
this.currentPropertyInfo = this.JsonLightOutputContext.PropertyCacheHandler.GetProperty(propertyName, owningType);
}

// Optimization for null values:
// If no validation is required, we don't need property serialization info and could try to skip writing null property right away
// If this property is top-level, we cannot optimize here due to backward-compatibility requirement for OData-6.x.
// For very wide and sparse outputs it allows to avoid a lot of dictionary lookups
bool isNullValue = (value == null || value is ODataNullValue);
if (isNullValue && omitNullValues)
{
if (!this.currentPropertyInfo.IsTopLevel && !this.MessageWriterSettings.ThrowIfTypeConflictsWithMetadata)
{
return;
}
}

WriterValidationUtils.ValidatePropertyDefined(this.currentPropertyInfo, this.MessageWriterSettings.ThrowOnUndeclaredPropertyForNonOpenType);

duplicatePropertyNameChecker.ValidatePropertyUniqueness(property);
Expand All @@ -208,8 +223,6 @@ private void WriteProperty(

WriteInstanceAnnotation(property, isTopLevel, currentPropertyInfo.MetadataType.IsUndeclaredProperty);

ODataValue value = property.ODataValue;

// handle ODataUntypedValue
ODataUntypedValue untypedValue = value as ODataUntypedValue;
if (untypedValue != null)
Expand All @@ -233,7 +246,7 @@ private void WriteProperty(
return;
}

if (value is ODataNullValue || value == null)
if (isNullValue)
{
this.WriteNullProperty(property, omitNullValues);
return;
Expand Down
28 changes: 23 additions & 5 deletions src/Microsoft.OData.Core/JsonLight/ODataJsonLightReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2284,26 +2284,29 @@ private void RestoreOmittedNullValues()
ODataResourceBase resource = resourceState.Resource;

IEnumerable<IEdmProperty> selectedProperties;

if (resourceState.SelectedProperties == SelectedPropertiesNode.EntireSubtree)
{
selectedProperties = edmStructuredType.DeclaredProperties;
selectedProperties = GetSelfAndBaseTypeProperties(edmStructuredType);
}
else
{ // Partial subtree. Combine navigation properties and selected properties at the node with distinct.
{
// Partial subtree. Combine navigation properties and selected properties at the node with distinct.
selectedProperties =
resourceState.SelectedProperties.GetSelectedNavigationProperties(edmStructuredType)
as IEnumerable<IEdmProperty>;
selectedProperties = selectedProperties.Concat(
resourceState.SelectedProperties.GetSelectedProperties(edmStructuredType).Values)
resourceState.SelectedProperties.GetSelectedProperties(edmStructuredType))
.Distinct();
}

foreach (IEdmProperty currentProperty in selectedProperties)
{
Debug.Assert(currentProperty.Type != null, "currentProperty.Type != null");
if (!currentProperty.Type.IsNullable)
if (!currentProperty.Type.IsNullable || currentProperty.PropertyKind == EdmPropertyKind.Navigation)
{
// Skip declared properties that are not null-able types.
// Skip declared properties that are not null-able types, and declared navigation properties
// which are specified using odata.navigationlink annotations whose absence stand for null values.
continue;
}

Expand Down Expand Up @@ -2344,6 +2347,21 @@ private void RestoreOmittedNullValues()
}
}

/// <summary>
/// Get all properties defined by the EDM structural type and its base types.
/// </summary>
/// <param name="edmStructuredType">The EDM structural type.</param>
/// <returns>All the properties of this type.</returns>
private static IEnumerable<IEdmProperty> GetSelfAndBaseTypeProperties(IEdmStructuredType edmStructuredType)
{
if (edmStructuredType == null)
{
return Enumerable.Empty<IEdmProperty>();
}

return edmStructuredType.DeclaredProperties.Concat(GetSelfAndBaseTypeProperties(edmStructuredType.BaseType));
}

/// <summary>
/// Add info resolved from context url to current scope.
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions src/Microsoft.OData.Core/ODataMessageWriterSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,7 @@ private void CopyFrom(ODataMessageWriterSettings other)
this.Version = other.Version;
this.OmitNullValues = other.OmitNullValues;
this.LibraryCompatibility = other.LibraryCompatibility;

this.validations = other.validations;
this.ThrowIfTypeConflictsWithMetadata = other.ThrowIfTypeConflictsWithMetadata;
this.ThrowOnDuplicatePropertyNames = other.ThrowOnDuplicatePropertyNames;
Expand Down
15 changes: 6 additions & 9 deletions src/Microsoft.OData.Core/SelectedPropertiesNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ internal sealed class SelectedPropertiesNode
/// <summary>An empty set of navigation properties to return when nothing is selected.</summary>
private static readonly IEnumerable<IEdmNavigationProperty> EmptyNavigationProperties = Enumerable.Empty<IEdmNavigationProperty>();

/// <summary>An empty dictionary of EDM properties to return when nothing is selected.</summary>
private static readonly Dictionary<string, IEdmProperty> EmptyEdmProperties = new Dictionary<string, IEdmProperty>(StringComparer.Ordinal);
/// <summary>An empty enumeration of EDM properties to return when nothing is selected.</summary>
private static readonly IEnumerable<IEdmProperty> EmptyEdmProperties = Enumerable.Empty<IEdmProperty>();

/// <summary>The type of the current node.</summary>
private readonly SelectionType selectionType;
Expand Down Expand Up @@ -344,7 +344,7 @@ internal IEnumerable<IEdmNavigationProperty> GetSelectedNavigationProperties(IEd
/// </summary>
/// <param name="structuredType">The current structured type.</param>
/// <returns>The selected properties at this node level.</returns>
internal IDictionary<string, IEdmProperty> GetSelectedProperties(IEdmStructuredType structuredType)
internal IEnumerable<IEdmProperty> GetSelectedProperties(IEdmStructuredType structuredType)
{
if (this.selectionType == SelectionType.Empty)
{
Expand All @@ -359,16 +359,13 @@ internal IDictionary<string, IEdmProperty> GetSelectedProperties(IEdmStructuredT

if (this.selectionType == SelectionType.EntireSubtree || this.hasWildcard)
{
return structuredType.DeclaredProperties.ToDictionary(sp => sp.Name, StringComparer.Ordinal);
return structuredType.DeclaredProperties;
}

Debug.Assert(this.selectedProperties != null, "selectedProperties != null");

IDictionary<string, IEdmProperty> selectedEdmProperties = this.selectedProperties
.Select(structuredType.FindProperty)
.ToDictionary(p => p.Name, StringComparer.Ordinal);

return selectedEdmProperties;
// Get declared properties selected, and filter out unrecognized properties.
return this.selectedProperties.Select(structuredType.FindProperty).OfType<IEdmProperty>();
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
// </copyright>
//---------------------------------------------------------------------

using System.Runtime.CompilerServices;

namespace Microsoft.Test.OData.Tests.Client.WriteJsonPayloadTests
{
using System;
Expand Down Expand Up @@ -145,6 +147,36 @@ public void ExpandedCustomerEntryTest()
}
}

/// <summary>
/// Write and read an expanded customer entry containing primitive, complex, collection of primitive/complex properties with null values
/// using omit-value=nulls Preference header.
/// </summary>
[TestMethod]
public void ExpandedCustomerEntryOmitNullValuesTest()
{
// Repeat with full and minimal metadata.
foreach (string mimeType in this.mimeTypes.GetRange(0, 2))
{
var settings = new ODataMessageWriterSettings();
settings.ODataUri = new ODataUri() {ServiceRoot = this.ServiceUri};
string rspRecvd = null;

var responseMessageWithModel = new StreamResponseMessage(new MemoryStream());
responseMessageWithModel.SetHeader("Content-Type", mimeType);
responseMessageWithModel.PreferenceAppliedHeader().OmitValues = ODataConstants.OmitValuesNulls;

using (var messageWriter = new ODataMessageWriter(responseMessageWithModel, settings,
WritePayloadHelper.Model))
{
var odataWriter = messageWriter.CreateODataResourceWriter(WritePayloadHelper.CustomerSet,
WritePayloadHelper.CustomerType);
rspRecvd = this.WriteAndVerifyExpandedCustomerEntryWithSomeNullValues(
responseMessageWithModel, odataWriter, /* hasModel */false);
Assert.IsNotNull(rspRecvd);
}
}
}

/// <summary>
/// Write an entry containing stream, named stream
/// </summary>
Expand Down Expand Up @@ -519,10 +551,9 @@ private string WriteAndVerifyOrderFeed(StreamResponseMessage responseMessage, OD
return WritePayloadHelper.ReadStreamContent(stream);
}

private string WriteAndVerifyExpandedCustomerEntry(StreamResponseMessage responseMessage,
ODataWriter odataWriter, bool hasModel, string mimeType)
private void WriteCustomerEntry( ODataWriter odataWriter, bool hasModel, bool withSomeNullValues)
{
ODataResourceWrapper customerEntry = WritePayloadHelper.CreateCustomerEntry(hasModel);
ODataResourceWrapper customerEntry = WritePayloadHelper.CreateCustomerEntry(hasModel, withSomeNullValues);

var loginFeed = new ODataResourceSet() { Id = new Uri(this.ServiceUri + "Customer(-9)/Logins") };
if (!hasModel)
Expand All @@ -533,30 +564,41 @@ private string WriteAndVerifyExpandedCustomerEntry(StreamResponseMessage respons
var loginEntry = WritePayloadHelper.CreateLoginEntry(hasModel);


customerEntry.NestedResourceInfoWrappers = customerEntry.NestedResourceInfoWrappers.Concat(WritePayloadHelper.CreateCustomerNavigationLinks());
customerEntry.NestedResourceInfoWrappers = customerEntry.NestedResourceInfoWrappers.Concat(new[]{ new ODataNestedResourceInfoWrapper()
{
NestedResourceInfo = new ODataNestedResourceInfo()
{
Name = "Logins",
IsCollection = true,
Url = new Uri(this.ServiceUri + "Customer(-9)/Logins")
},
NestedResourceOrResourceSet = new ODataResourceSetWrapper()
{
ResourceSet = loginFeed,
Resources = new List<ODataResourceWrapper>()
{
new ODataResourceWrapper()
// Setting null values for some navigation links, those null links won't be serialized
// as odata.navigationlink annotations.
customerEntry.NestedResourceInfoWrappers = customerEntry.NestedResourceInfoWrappers.Concat(
WritePayloadHelper.CreateCustomerNavigationLinks(withSomeNullValues));
customerEntry.NestedResourceInfoWrappers = customerEntry.NestedResourceInfoWrappers.Concat(
new[]{ new ODataNestedResourceInfoWrapper()
{
Resource = loginEntry,
NestedResourceInfoWrappers = WritePayloadHelper.CreateLoginNavigationLinksWrapper().ToList()
NestedResourceInfo = new ODataNestedResourceInfo()
{
Name = "Logins",
IsCollection = true,
Url = new Uri(this.ServiceUri + "Customer(-9)/Logins")
},
NestedResourceOrResourceSet = new ODataResourceSetWrapper()
{
ResourceSet = loginFeed,
Resources = new List<ODataResourceWrapper>()
{
new ODataResourceWrapper()
{
Resource = loginEntry,
NestedResourceInfoWrappers = WritePayloadHelper.CreateLoginNavigationLinksWrapper(withSomeNullValues).ToList()
}
}
}
}
}
}
}});
});

ODataWriterHelper.WriteResource(odataWriter, customerEntry);
}

private string WriteAndVerifyExpandedCustomerEntry(StreamResponseMessage responseMessage,
ODataWriter odataWriter, bool hasModel, string mimeType)
{
WriteCustomerEntry(odataWriter, hasModel, false);

// Some very basic verification for the payload.
bool verifyFeedCalled = false;
Expand Down Expand Up @@ -601,6 +643,57 @@ private string WriteAndVerifyExpandedCustomerEntry(StreamResponseMessage respons
return WritePayloadHelper.ReadStreamContent(stream);
}

private string WriteAndVerifyExpandedCustomerEntryWithSomeNullValues(StreamResponseMessage responseMessage,
ODataWriter odataWriter, bool hasModel)
{
WriteCustomerEntry(odataWriter, hasModel, /* withSomeNullValues */true);

// Some very basic verification for the payload.
bool verifyFeedCalled = false;
int verifyEntryCalled = 0;
int verifyNullValuesCount = 0;
bool verifyNavigationCalled = false;
Action<ODataResourceSet> verifyFeed = (feed) =>
{
verifyFeedCalled = true;
};

Action<ODataResource> verifyEntry = (entry) =>
{
if (entry.TypeName.Contains("Customer"))
{
Assert.AreEqual(4, entry.Properties.Count());
verifyEntryCalled++;
}

if (entry.TypeName.Contains("Login"))
{
Assert.AreEqual(2, entry.Properties.Count());
verifyEntryCalled++;
}

// Counting restored null property value during response de-serialization.
verifyNullValuesCount += entry.Properties.Count(p => p.Value == null);
};

Action<ODataNestedResourceInfo> verifyNavigation = (navigation) =>
{
Assert.IsNotNull(navigation.Name);
verifyNavigationCalled = true;
};

Stream stream = responseMessage.GetStream();
stream.Seek(0, SeekOrigin.Begin);
WritePayloadHelper.ReadAndVerifyFeedEntryMessage(false, responseMessage, WritePayloadHelper.CustomerSet, WritePayloadHelper.CustomerType,
verifyFeed, verifyEntry, verifyNavigation);
Assert.IsTrue(verifyFeedCalled);
Assert.IsTrue(verifyEntryCalled == 2);
Assert.IsTrue(verifyNullValuesCount == 4);
Assert.IsTrue(verifyNavigationCalled);

return WritePayloadHelper.ReadStreamContent(stream);
}

private string WriteAndVerifyCarEntry(StreamResponseMessage responseMessage, ODataWriter odataWriter,
bool hasModel, string mimeType)
{
Expand Down
Loading

0 comments on commit bc4a274

Please sign in to comment.