diff --git a/src/Microsoft.OData.Core/Evaluation/ODataResourceMetadataBuilder.cs b/src/Microsoft.OData.Core/Evaluation/ODataResourceMetadataBuilder.cs
index a8bfbfb9e8..caab1247e4 100644
--- a/src/Microsoft.OData.Core/Evaluation/ODataResourceMetadataBuilder.cs
+++ b/src/Microsoft.OData.Core/Evaluation/ODataResourceMetadataBuilder.cs
@@ -4,6 +4,8 @@
//
//---------------------------------------------------------------------
+using System.Diagnostics;
+
#if ODATA_CLIENT
namespace Microsoft.OData.Client
#else
@@ -23,6 +25,9 @@ namespace Microsoft.OData.Evaluation
///
internal abstract class ODataResourceMetadataBuilder
{
+ /// The expanded entities that have been processed.
+ protected readonly HashSet ProcessedExpandedEntityNames = new HashSet(StringComparer.Ordinal);
+
#if !ODATA_CLIENT
///
/// Gets an instance of the metadata builder which never returns anything other than nulls.
@@ -36,6 +41,7 @@ internal static ODataResourceMetadataBuilder Null
}
#endif
+
///
/// Gets instance of the metadata builder which belongs to the parent odata resource
///
@@ -156,6 +162,27 @@ internal virtual void MarkNestedResourceInfoProcessed(string navigationPropertyN
{
}
+ ///
+ /// Return whether the given expanded entity of specified name has been processed.
+ ///
+ /// The name of the expanded entity.
+ /// True if the expanded entity has been processed; False otherwise.
+ internal virtual bool IsExpandedEntityProcessed(string name)
+ {
+ return this.ProcessedExpandedEntityNames.Contains(name);
+ }
+
+ ///
+ /// Marks the given expanded entity as processed.
+ ///
+ /// The name of the expanded entity we've already processed.
+ internal virtual void MarkExpandedEntityProcessed(string expandedEntityName)
+ {
+ Debug.Assert(!string.IsNullOrEmpty(expandedEntityName), "!string.IsNullOrEmpty(expandedEntityName)");
+ Debug.Assert(this.ProcessedExpandedEntityNames != null, "this.ProcessedExpandedEntityNames != null");
+ this.ProcessedExpandedEntityNames.Add(expandedEntityName);
+ }
+
///
/// Returns the next unprocessed nested resource info or null if there's no more navigation links to process.
///
diff --git a/src/Microsoft.OData.Core/JsonLight/ODataJsonLightPropertySerializer.cs b/src/Microsoft.OData.Core/JsonLight/ODataJsonLightPropertySerializer.cs
index bf15f45380..40dad0c29e 100644
--- a/src/Microsoft.OData.Core/JsonLight/ODataJsonLightPropertySerializer.cs
+++ b/src/Microsoft.OData.Core/JsonLight/ODataJsonLightPropertySerializer.cs
@@ -84,6 +84,7 @@ internal void WriteTopLevelProperty(ODataProperty property)
null /*owningType*/,
true /* isTopLevel */,
false /* allowStreamProperty */,
+ false /*omitNullValues */,
this.CreateDuplicatePropertyNameChecker());
this.JsonLightValueSerializer.AssertRecursionDepthIsZero();
@@ -100,11 +101,15 @@ internal void WriteTopLevelProperty(ODataProperty property)
/// Whether the properties are being written for complex value. Also used for detecting whether stream properties
/// are allowed as named stream properties should only be defined on ODataResource instances
///
+ /// Whether to omit null property values.
+ /// Value should be 'false' if writing delta payload, otherwise derived from request preference header.
+ ///
/// The DuplicatePropertyNameChecker to use.
internal void WriteProperties(
IEdmStructuredType owningType,
IEnumerable properties,
bool isComplexValue,
+ bool omitNullValues,
IDuplicatePropertyNameChecker duplicatePropertyNameChecker)
{
if (properties == null)
@@ -119,6 +124,8 @@ internal void WriteProperties(
owningType,
false /* isTopLevel */,
!isComplexValue,
+ // Annotated properties won't be omitted even if it is null.
+ omitNullValues && property.InstanceAnnotations.Count == 0,
duplicatePropertyNameChecker);
}
}
@@ -141,7 +148,7 @@ private bool IsOpenProperty(ODataProperty property)
else
{
// TODO: (issue #888) this logic results in type annotations not being written for dynamic properties on types that are not
- // marked as open. Type annotations should always be written for dynamic properties whose type cannot be hueristically
+ // marked as open. Type annotations should always be written for dynamic properties whose type cannot be heuristically
// determined. Need to change this.currentPropertyInfo.MetadataType.IsOpenProperty to this.currentPropertyInfo.MetadataType.IsDynamic,
// and fix related tests and other logic (this change alone results in writing type even if it's already implied by context).
isOpenProperty = (!this.WritingResponse && this.currentPropertyInfo.MetadataType.OwningType == null) // Treat property as dynamic property when writing request and owning type is null
@@ -164,6 +171,9 @@ private bool IsOpenProperty(ODataProperty property)
/// true when writing a top-level property; false for nested properties.
/// Should pass in true if we are writing a property of an ODataResource instance, false otherwise.
/// Named stream properties should only be defined on ODataResource instances.
+ /// Whether to omit null property values for writing.
+ /// Value should be 'false' if writing top level properties or delta payload, otherwise derived from request preference header.
+ ///
/// The DuplicatePropertyNameChecker to use.
[SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Splitting the code would make the logic harder to understand; class coupling is only slightly above threshold.")]
private void WriteProperty(
@@ -171,6 +181,7 @@ private void WriteProperty(
IEdmStructuredType owningType,
bool isTopLevel,
bool allowStreamProperty,
+ bool omitNullValues,
IDuplicatePropertyNameChecker duplicatePropertyNameChecker)
{
WriterValidationUtils.ValidatePropertyNotNull(property);
@@ -204,6 +215,15 @@ private void WriteProperty(
ODataValue value = property.ODataValue;
+ bool isNullValue = (value == null || value is ODataNullValue);
+ if (isNullValue && omitNullValues)
+ {
+ if (!this.currentPropertyInfo.IsTopLevel)
+ {
+ return;
+ }
+ }
+
// handle ODataUntypedValue
ODataUntypedValue untypedValue = value as ODataUntypedValue;
if (untypedValue != null)
@@ -227,7 +247,7 @@ private void WriteProperty(
return;
}
- if (value is ODataNullValue || value == null)
+ if (isNullValue)
{
this.WriteNullProperty(property);
return;
diff --git a/src/Microsoft.OData.Core/JsonLight/ODataJsonLightReader.cs b/src/Microsoft.OData.Core/JsonLight/ODataJsonLightReader.cs
index db2a72ba80..c95c3590ec 100644
--- a/src/Microsoft.OData.Core/JsonLight/ODataJsonLightReader.cs
+++ b/src/Microsoft.OData.Core/JsonLight/ODataJsonLightReader.cs
@@ -1658,6 +1658,12 @@ private void ReadExpandedNestedResourceInfoEnd(bool isCollection)
IODataJsonLightReaderResourceState parentResourceState = (IODataJsonLightReaderResourceState)this.ParentScope;
parentResourceState.NavigationPropertiesRead.Add(this.CurrentNestedResourceInfo.Name);
+ if (parentResourceState.SelectedProperties.SelectedExpandedEntities.Contains(this.CurrentNestedResourceInfo.Name))
+ {
+ // Record that we read the expanded entity
+ parentResourceState.MetadataBuilder.MarkExpandedEntityProcessed(this.CurrentNestedResourceInfo.Name);
+ }
+
// replace the 'NestedResourceInfoStart' scope with the 'NestedResourceInfoEnd' scope
this.ReplaceScope(ODataReaderState.NestedResourceInfoEnd);
}
@@ -2232,6 +2238,9 @@ private void EndEntry()
if (this.State == ODataReaderState.ResourceStart)
{
+ // For non-delta response, need to restore omitted null values as required.
+ RestoreOmittedNullValues();
+
this.EndEntry(
new JsonLightResourceScope(
ODataReaderState.ResourceEnd,
@@ -2256,6 +2265,82 @@ private void EndEntry()
}
}
+ ///
+ /// For response de-serialization, restore the null values for properties that are omitted
+ /// by the omit-values=nulls preference header.
+ ///
+ private void RestoreOmittedNullValues()
+ {
+ // Restore omitted null value only when processing response and when
+ // Preference header omit-values=nulls is specified.
+ if (ShouldRestoreNullValues())
+ {
+ IODataJsonLightReaderResourceState resourceState = this.CurrentResourceState;
+ IEdmStructuredType edmStructuredType = resourceState.ResourceType;
+ SelectedPropertiesNode selectedProperties = resourceState.SelectedProperties;
+
+ if (selectedProperties == SelectedPropertiesNode.Empty)
+ {
+ return;
+ }
+ else if (resourceState.Resource != null)
+ {
+ Debug.Assert(edmStructuredType != null, "edmStructuredType != null");
+ ODataResourceBase resource = resourceState.Resource;
+
+
+ IList nullPropertyNames =
+ selectedProperties.GetSelectedNullValueProperties(edmStructuredType, resourceState);
+
+ // Restore null Expanded entities, which are specified as "isExpandedNavigationProperty" the child nodes.
+ foreach (string name in selectedProperties.SelectedExpandedEntities)
+ {
+ if (!resource.MetadataBuilder.IsExpandedEntityProcessed(name))
+ {
+ nullPropertyNames.Add(name);
+ }
+ }
+
+ // Mark as processed, will throw if duplicate is detected.
+ foreach (string name in nullPropertyNames)
+ {
+ this.CurrentResourceState.PropertyAndAnnotationCollector.MarkPropertyAsProcessed(name);
+ }
+
+ RestoreNullODataProperties(nullPropertyNames);
+ }
+ }
+ }
+
+ ///
+ /// Restore the null property values in the resource.
+ ///
+ /// The list of names of the properties to be restored with null values.
+ private void RestoreNullODataProperties(IList nullPropertyNames)
+ {
+ IList properties = nullPropertyNames.Select(name => new ODataProperty()
+ {
+ Name = name,
+ Value = new ODataNullValue()
+ }).ToList();
+
+ ODataResourceBase resource = this.CurrentResourceState.Resource;
+
+ ReadOnlyEnumerable readonlyEnumerable =
+ resource.Properties as ReadOnlyEnumerable;
+ if (readonlyEnumerable != null)
+ {
+ // For non-entity resource, add the property underneath the read-only enumerable.
+ resource.Properties.ConcatToReadOnlyEnumerable("Properties", properties);
+ }
+ else
+ {
+ // For entity resource, concatenate the property to original enumerable.
+ resource.Properties =
+ resource.Properties.Concat(properties);
+ }
+ }
+
///
/// Add info resolved from context url to current scope.
///
diff --git a/src/Microsoft.OData.Core/JsonLight/ODataJsonLightResourceDeserializer.cs b/src/Microsoft.OData.Core/JsonLight/ODataJsonLightResourceDeserializer.cs
index 34b8372881..49080d223a 100644
--- a/src/Microsoft.OData.Core/JsonLight/ODataJsonLightResourceDeserializer.cs
+++ b/src/Microsoft.OData.Core/JsonLight/ODataJsonLightResourceDeserializer.cs
@@ -998,6 +998,10 @@ internal ODataJsonLightReaderNestedResourceInfo ReadPropertyWithoutValue(IODataJ
ODataStreamReferenceValue streamPropertyValue = this.ReadStreamPropertyValue(resourceState, propertyName);
AddResourceProperty(resourceState, edmProperty.Name, streamPropertyValue);
}
+ else if (this.JsonLightInputContext.ShouldRestoreNullValues())
+ {
+ AddResourceProperty(resourceState, propertyName, new ODataNullValue());
+ }
else
{
throw new ODataException(ODataErrorStrings.ODataJsonLightResourceDeserializer_PropertyWithoutValueWithWrongType(propertyName, propertyTypeReference.FullName()));
@@ -1320,7 +1324,18 @@ private ODataJsonLightReaderNestedResourceInfo InnerReadUndeclaredProperty(IODat
// Property without a value can't be ignored if we don't know what it is.
if (!propertyWithValue)
{
- throw new ODataException(ODataErrorStrings.ODataJsonLightResourceDeserializer_OpenPropertyWithoutValue(propertyName));
+ if (this.JsonLightInputContext.ShouldRestoreNullValues())
+ {
+ // For case of property without values that are possibly omitted, restore omitted null value
+ // and return nested resource as null.
+ AddResourceProperty(resourceState, propertyName, new ODataNullValue());
+ return null;
+ }
+ else
+ {
+ throw new ODataException(
+ ODataErrorStrings.ODataJsonLightResourceDeserializer_OpenPropertyWithoutValue(propertyName));
+ }
}
object propertyValue = null;
diff --git a/src/Microsoft.OData.Core/JsonLight/ODataJsonLightWriter.cs b/src/Microsoft.OData.Core/JsonLight/ODataJsonLightWriter.cs
index d05623db7f..a5d64bfb8b 100644
--- a/src/Microsoft.OData.Core/JsonLight/ODataJsonLightWriter.cs
+++ b/src/Microsoft.OData.Core/JsonLight/ODataJsonLightWriter.cs
@@ -270,15 +270,32 @@ protected override void StartResource(ODataResource resource)
}
this.jsonLightResourceSerializer.InstanceAnnotationWriter.WriteInstanceAnnotations(parentNavLink.GetInstanceAnnotations(), parentNavLink.Name);
- }
- // Write the property name of an expanded navigation property to start the value.
- this.jsonWriter.WriteName(parentNavLink.Name);
+ // Annotated resource won't be omitted even if it is null.
+ if (!this.ShouldOmitNullValues() || parentNavLink.GetInstanceAnnotations().Count > 0)
+ {
+ // Write the property name of an expanded navigation property to start the value of null per preference setting.
+ this.jsonWriter.WriteName(parentNavLink.Name);
+
+ // Optimization: write null and return directly.
+ this.jsonWriter.WriteValue((string)null);
+ return;
+ }
+ }
+ else
+ {
+ // Write the property name of an expanded navigation property to start the value.
+ this.jsonWriter.WriteName(parentNavLink.Name);
+ }
}
if (resource == null)
{
- this.jsonWriter.WriteValue((string)null);
+ if (!this.ShouldOmitNullValues())
+ {
+ this.jsonWriter.WriteValue((string)null);
+ }
+
return;
}
@@ -331,6 +348,7 @@ protected override void StartResource(ODataResource resource)
this.ResourceType,
resource.Properties,
false /* isComplexValue */,
+ this.ShouldOmitNullValues(),
this.DuplicatePropertyNameChecker);
this.jsonLightResourceSerializer.JsonLightValueSerializer.AssertRecursionDepthIsZero();
@@ -1228,6 +1246,7 @@ private void WriteDeltaResourceProperties(IEnumerable properties)
this.ResourceType,
properties,
false /* isComplexValue */,
+ false /* omitNullValues */,
this.DuplicatePropertyNameChecker);
this.jsonLightResourceSerializer.JsonLightValueSerializer.AssertRecursionDepthIsZero();
}
diff --git a/src/Microsoft.OData.Core/Microsoft.OData.Core.txt b/src/Microsoft.OData.Core/Microsoft.OData.Core.txt
index b4b10e8f0e..2407f768ab 100644
--- a/src/Microsoft.OData.Core/Microsoft.OData.Core.txt
+++ b/src/Microsoft.OData.Core/Microsoft.OData.Core.txt
@@ -241,7 +241,6 @@ HttpHeaderValueLexer_TokenExpectedButFoundQuotedString=An error occurred when pa
HttpHeaderValueLexer_FailedToReadTokenOrQuotedString=An error occurred when parsing the HTTP header '{0}'. The header value '{1}' is incorrect at position '{2}' because a token or a quoted-string is expected at this position but were not found.
HttpHeaderValueLexer_InvalidSeparatorAfterQuotedString=An error occurred when parsing the HTTP header '{0}'. The header value '{1}' is incorrect at position '{2}' because '{3}' is not a valid separator after a quoted-string.
HttpHeaderValueLexer_EndOfFileAfterSeparator=An error occurred when parsing the HTTP header '{0}'. The header value '{1}' is incorrect at position '{2}' because the header value should not end with the separator '{3}'.
-
MediaType_EncodingNotSupported=The character set '{0}' is not supported.
MediaTypeUtils_DidNotFindMatchingMediaType=A supported MIME type could not be found that matches the acceptable MIME types for the request. The supported type(s) '{0}' do not match any of the acceptable MIME types '{1}'.
diff --git a/src/Microsoft.OData.Core/ODataConstants.cs b/src/Microsoft.OData.Core/ODataConstants.cs
index e0d8069b5d..b29469d53b 100644
--- a/src/Microsoft.OData.Core/ODataConstants.cs
+++ b/src/Microsoft.OData.Core/ODataConstants.cs
@@ -59,6 +59,19 @@ internal static class ODataInternalConstants
///
public const string ContentIdHeader = "Content-ID";
+ #region Preference Headers
+ ///
+ /// Valid value for OmitValuesPreferenceToken
indicating nulls are omitted.
+ ///
+ public const string OmitValuesNulls = "nulls";
+
+ ///
+ /// Valid value for OmitValuesPreferenceToken
indicating defaults are omitted.
+ /// Internal access only since "defaults" is not supported yet.
+ ///
+ internal const string OmitValuesDefaults = "defaults";
+ #endregion Preference Headers
+
///
/// Name of the Content-Length HTTP header.
///
@@ -205,6 +218,7 @@ internal static class ODataInternalConstants
/// The $deletedLink token indicates delta deleted link.
internal const string ContextUriDeletedLink = UriSegmentSeparator + DeletedLink;
+
#endregion Context URL
}
}
diff --git a/src/Microsoft.OData.Core/ODataContextUrlInfo.cs b/src/Microsoft.OData.Core/ODataContextUrlInfo.cs
index f4ecf5f596..5d9665aeda 100644
--- a/src/Microsoft.OData.Core/ODataContextUrlInfo.cs
+++ b/src/Microsoft.OData.Core/ODataContextUrlInfo.cs
@@ -401,7 +401,6 @@ private static string CreateSelectExpandContextUriSegment(SelectExpandClause sel
/// The generated expand string.
private static string ProcessSubExpand(string expandNode, string subExpand)
{
-
return expandNode + ODataConstants.ContextUriProjectionStart + subExpand + ODataConstants.ContextUriProjectionEnd;
}
@@ -415,7 +414,6 @@ private static string CombineSelectAndExpandResult(IList selectList, ILi
if (selectList.Any())
{
-
currentExpandClause += String.Join(ODataConstants.ContextUriProjectionPropertySeparator, selectList.ToArray());
}
diff --git a/src/Microsoft.OData.Core/ODataInputContext.cs b/src/Microsoft.OData.Core/ODataInputContext.cs
index e4c3abd4db..321983d81f 100644
--- a/src/Microsoft.OData.Core/ODataInputContext.cs
+++ b/src/Microsoft.OData.Core/ODataInputContext.cs
@@ -646,6 +646,15 @@ internal Uri ResolveUri(Uri baseUri, Uri payloadUri)
return null;
}
+ ///
+ /// Returns whether properties of null values should be restored when materializing the payload.
+ ///
+ /// Return true to restore null-value properties; false otherwise.
+ internal bool ShouldRestoreNullValues()
+ {
+ return this.ReadingResponse && this.MessageReaderSettings.NullValuesOmitted;
+ }
+
///
/// Perform the actual cleanup work.
///
diff --git a/src/Microsoft.OData.Core/ODataMessageReader.cs b/src/Microsoft.OData.Core/ODataMessageReader.cs
index ede858649b..598b1ae311 100644
--- a/src/Microsoft.OData.Core/ODataMessageReader.cs
+++ b/src/Microsoft.OData.Core/ODataMessageReader.cs
@@ -169,6 +169,16 @@ public ODataMessageReader(IODataResponseMessage responseMessage, ODataMessageRea
this.model = model ?? GetModel(this.container);
this.edmTypeResolver = new EdmTypeReaderResolver(this.model, this.settings.ClientCustomTypeResolver);
+ // Whether null values were omitted in the response, false by default.
+ this.settings.NullValuesOmitted = false;
+ if (string.Equals(responseMessage.PreferenceAppliedHeader().OmitValues, ODataConstants.OmitValuesNulls,
+ StringComparison.OrdinalIgnoreCase))
+ {
+ // If the Preference-Applied header's omit-values parameter is present in the response, its value should
+ // be applied to the reader's setting.
+ this.settings.NullValuesOmitted = true;
+ }
+
// If the Preference-Applied header on the response message contains an annotation filter, we set the filter
// to the reader settings if it's not already set, so that we would only read annotations that satisfy the filter.
string annotationFilter = responseMessage.PreferenceAppliedHeader().AnnotationFilter;
diff --git a/src/Microsoft.OData.Core/ODataMessageReaderSettings.cs b/src/Microsoft.OData.Core/ODataMessageReaderSettings.cs
index b168c674ad..b2398e3794 100644
--- a/src/Microsoft.OData.Core/ODataMessageReaderSettings.cs
+++ b/src/Microsoft.OData.Core/ODataMessageReaderSettings.cs
@@ -202,6 +202,11 @@ public ODataMessageQuotas MessageQuotas
///
internal bool ThrowOnUndeclaredPropertyForNonOpenType { get; private set; }
+ ///
+ /// True if null values are omitted, per the omit-values parameter in Preference header of the message.
+ ///
+ internal bool NullValuesOmitted { get; set; }
+
///
/// Creates a shallow copy of this .
///
@@ -259,6 +264,7 @@ private void CopyFrom(ODataMessageReaderSettings other)
this.MaxProtocolVersion = other.MaxProtocolVersion;
this.ReadUntypedAsString = other.ReadUntypedAsString;
this.ShouldIncludeAnnotation = other.ShouldIncludeAnnotation;
+ this.NullValuesOmitted = other.NullValuesOmitted;
this.validations = other.validations;
this.ThrowOnDuplicatePropertyNames = other.ThrowOnDuplicatePropertyNames;
this.ThrowIfTypeConflictsWithMetadata = other.ThrowIfTypeConflictsWithMetadata;
diff --git a/src/Microsoft.OData.Core/ODataMessageWriter.cs b/src/Microsoft.OData.Core/ODataMessageWriter.cs
index d0a5788699..be02bc0ccb 100644
--- a/src/Microsoft.OData.Core/ODataMessageWriter.cs
+++ b/src/Microsoft.OData.Core/ODataMessageWriter.cs
@@ -151,6 +151,14 @@ public ODataMessageWriter(IODataResponseMessage responseMessage, ODataMessageWri
WriterValidationUtils.ValidateMessageWriterSettings(this.settings, this.writingResponse);
this.message = new ODataResponseMessage(responseMessage, /*writing*/ true, this.settings.EnableMessageStreamDisposal, /*maxMessageSize*/ -1);
+ // Set the writer settings per Preference-Applied header's parameter omit-values.
+ // For response writing, source of specifying omit-null-values preference is the Preference-Applied header.
+ if (string.Equals(responseMessage.PreferenceAppliedHeader().OmitValues, ODataConstants.OmitValuesNulls,
+ StringComparison.OrdinalIgnoreCase))
+ {
+ this.settings.OmitNullValues = true;
+ }
+
// If the Preference-Applied header on the response message contains an annotation filter, we set the filter
// to the writer settings so that we would only write annotations that satisfy the filter.
string annotationFilter = responseMessage.PreferenceAppliedHeader().AnnotationFilter;
diff --git a/src/Microsoft.OData.Core/ODataMessageWriterSettings.cs b/src/Microsoft.OData.Core/ODataMessageWriterSettings.cs
index d287c43e22..a7ffb5724c 100644
--- a/src/Microsoft.OData.Core/ODataMessageWriterSettings.cs
+++ b/src/Microsoft.OData.Core/ODataMessageWriterSettings.cs
@@ -183,6 +183,14 @@ public ODataUri ODataUri
///
internal bool ThrowOnDuplicatePropertyNames { get; private set; }
+ ///
+ /// Whether to omit null values when writing response.
+ ///
+ ///
+ /// Default value is false, that means serialize null values.
+ ///
+ internal bool OmitNullValues { get; set; }
+
///
/// Returns whether ThrowOnUndeclaredPropertyForNonOpenType validation setting is enabled.
///
@@ -404,6 +412,7 @@ private void CopyFrom(ODataMessageWriterSettings other)
this.shouldIncludeAnnotation = other.shouldIncludeAnnotation;
this.useFormat = other.useFormat;
this.Version = other.Version;
+ this.OmitNullValues = other.OmitNullValues;
this.LibraryCompatibility = other.LibraryCompatibility;
this.validations = other.validations;
diff --git a/src/Microsoft.OData.Core/ODataOutputContext.cs b/src/Microsoft.OData.Core/ODataOutputContext.cs
index 801972a4f2..e8e7af9587 100644
--- a/src/Microsoft.OData.Core/ODataOutputContext.cs
+++ b/src/Microsoft.OData.Core/ODataOutputContext.cs
@@ -437,6 +437,15 @@ public virtual Task WriteErrorAsync(ODataError odataError, bool includeDebugInfo
}
#endif
+ ///
+ /// Returns whether properties of null values should be omitted when serializing the payload.
+ ///
+ /// Return true to omit null-value properties; false otherwise.
+ internal bool ShouldOmitNullValues()
+ {
+ return this.WritingResponse && this.MessageWriterSettings.OmitNullValues;
+ }
+
///
/// Writes an into the message payload.
///
diff --git a/src/Microsoft.OData.Core/ODataPreferenceHeader.cs b/src/Microsoft.OData.Core/ODataPreferenceHeader.cs
index 82a905504b..7642edb408 100644
--- a/src/Microsoft.OData.Core/ODataPreferenceHeader.cs
+++ b/src/Microsoft.OData.Core/ODataPreferenceHeader.cs
@@ -6,8 +6,10 @@
namespace Microsoft.OData
{
+ using System;
using System.Collections.Generic;
using System.Diagnostics;
+ using System.Diagnostics.CodeAnalysis;
using System.Globalization;
///
@@ -61,6 +63,11 @@ public class ODataPreferenceHeader
///
private const string ODataTrackChangesPreferenceToken = "odata.track-changes";
+ ///
+ /// The omit-values preference token.
+ ///
+ private const string OmitValuesPreferenceToken = "omit-values";
+
///
/// The Prefer header name.
///
@@ -243,6 +250,34 @@ public string AnnotationFilter
}
}
+ ///
+ /// Property to get and set the "omit-values" preference to the "Prefer" header on the underlying IODataRequestMessage or
+ /// the "Preference-Applied" header on the underlying IODataResponseMessage.
+ /// For the preference or applied preference of omitting null values, use string "nulls".
+ ///
+ public string OmitValues
+ {
+ get
+ {
+ HttpHeaderValueElement omitValues = this.Get(OmitValuesPreferenceToken);
+ return omitValues != null ? omitValues.Value : null;
+ }
+
+ [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "Need lower case string here.")]
+ set
+ {
+ ExceptionUtils.CheckArgumentStringNotEmpty(value, "OmitValues");
+
+ string normalizedValue = value.Trim().ToLowerInvariant();
+ if (string.Equals(normalizedValue, ODataConstants.OmitValuesNulls, StringComparison.Ordinal))
+ {
+ this.Set(new HttpHeaderValueElement(OmitValuesPreferenceToken, ODataConstants.OmitValuesNulls, EmptyParameters));
+ }
+
+ // No-ops for other preference values, including "defaults" which is in the OData protocol but not implemented yet.
+ }
+ }
+
///
/// Property to get and set the "respond-async" preference to the "Prefer" header on the underlying IODataRequestMessage or
/// the "Preference-Applied" header on the underlying IODataResponseMessage.
diff --git a/src/Microsoft.OData.Core/ODataReaderCore.cs b/src/Microsoft.OData.Core/ODataReaderCore.cs
index 1afac8a4ac..4953894389 100644
--- a/src/Microsoft.OData.Core/ODataReaderCore.cs
+++ b/src/Microsoft.OData.Core/ODataReaderCore.cs
@@ -664,6 +664,16 @@ protected void DecreaseResourceDepth()
this.currentResourceDepth--;
}
+
+ ///
+ /// Returns whether properties of null values should be restored when materializing the payload.
+ ///
+ /// Return true to restore null-value properties; false otherwise.
+ protected bool ShouldRestoreNullValues()
+ {
+ return this.inputContext.ShouldRestoreNullValues();
+ }
+
///
/// Reads the next from the message payload.
///
diff --git a/src/Microsoft.OData.Core/ODataWriterCore.cs b/src/Microsoft.OData.Core/ODataWriterCore.cs
index a5523c3df7..8ed12769ae 100644
--- a/src/Microsoft.OData.Core/ODataWriterCore.cs
+++ b/src/Microsoft.OData.Core/ODataWriterCore.cs
@@ -1144,6 +1144,16 @@ protected void ValidateNoDeltaLinkForExpandedResourceSet(ODataResourceSet resour
}
}
+ ///
+ /// Returns whether properties of null values should be omitted when serializing the payload.
+ ///
+ /// Return true to omit null-value properties; false otherwise.
+ protected bool ShouldOmitNullValues()
+ {
+ // Per protocol, omitNullValues doesn't affect delta payload.
+ return !this.writingDelta && this.outputContext.ShouldOmitNullValues();
+ }
+
///
/// Verifies that calling WriteStart resourceSet is valid.
///
diff --git a/src/Microsoft.OData.Core/ReadOnlyEnumerableExtensions.cs b/src/Microsoft.OData.Core/ReadOnlyEnumerableExtensions.cs
index ec2888ada7..5a983b662f 100644
--- a/src/Microsoft.OData.Core/ReadOnlyEnumerableExtensions.cs
+++ b/src/Microsoft.OData.Core/ReadOnlyEnumerableExtensions.cs
@@ -83,5 +83,20 @@ internal static ReadOnlyEnumerable ConcatToReadOnlyEnumerable(this IEnumer
readOnlyEnumerable.AddToSourceList(item);
return readOnlyEnumerable;
}
+
+ ///
+ /// Returns a ReadOnlyEnumerableOfT that is the result of plus .
+ ///
+ /// The element type of the enumerable.
+ /// The source enumerable to concatenate.
+ /// The name of the collection to report in case there's an error.
+ /// Items to be concatenated to the source enumerable.
+ /// Returns a ReadOnlyEnumerableOfT that is the result of plus .
+ internal static ReadOnlyEnumerable ConcatToReadOnlyEnumerable(this IEnumerable source, string collectionName, IList items)
+ {
+ ReadOnlyEnumerable readOnlyEnumerable = source.GetOrCreateReadOnlyEnumerable(collectionName);
+ readOnlyEnumerable.AddToSourceList(items);
+ return readOnlyEnumerable;
+ }
}
}
diff --git a/src/Microsoft.OData.Core/ReadOnlyEnumerableOfT.cs b/src/Microsoft.OData.Core/ReadOnlyEnumerableOfT.cs
index 9cbe2fa077..f40ed36db7 100644
--- a/src/Microsoft.OData.Core/ReadOnlyEnumerableOfT.cs
+++ b/src/Microsoft.OData.Core/ReadOnlyEnumerableOfT.cs
@@ -4,6 +4,8 @@
//
//---------------------------------------------------------------------
+using System.Linq;
+
namespace Microsoft.OData
{
#region Namespaces
@@ -79,5 +81,20 @@ internal void AddToSourceList(T itemToAdd)
this.sourceList.Add(itemToAdd);
}
+
+ ///
+ /// This internal method adds to the wrapped source list. From the public's perspective, this enumerable is still readonly.
+ ///
+ /// Items to be added to the source list.
+ internal void AddToSourceList(IList itemsToAdd)
+ {
+ Debug.Assert(this.sourceList != null, "this.sourceList != null");
+ Debug.Assert(itemsToAdd != null, nameof(itemsToAdd) + " != null");
+
+ foreach (var item in itemsToAdd)
+ {
+ AddToSourceList(item);
+ }
+ }
}
}
diff --git a/src/Microsoft.OData.Core/SelectedPropertiesNode.cs b/src/Microsoft.OData.Core/SelectedPropertiesNode.cs
index 7411aa0a70..1e1b2e5b76 100644
--- a/src/Microsoft.OData.Core/SelectedPropertiesNode.cs
+++ b/src/Microsoft.OData.Core/SelectedPropertiesNode.cs
@@ -4,6 +4,10 @@
//
//---------------------------------------------------------------------
+using System.Collections;
+using System.Runtime.InteropServices.ComTypes;
+using Microsoft.OData.JsonLight;
+
namespace Microsoft.OData
{
#region Namespaces
@@ -38,6 +42,9 @@ internal sealed class SelectedPropertiesNode
/// An empty set of navigation properties to return when nothing is selected.
private static readonly IEnumerable EmptyNavigationProperties = Enumerable.Empty();
+ /// An empty enumeration of EDM properties to return when nothing is selected.
+ private static readonly IEnumerable EmptyEdmProperties = Enumerable.Empty();
+
/// The type of the current node.
private SelectionType selectionType = SelectionType.PartialSubtree;
@@ -204,6 +211,19 @@ internal enum SelectionType
PartialSubtree = 2,
}
+ ///
+ /// Gets names of the expanded entity nodes at current level.
+ ///
+ internal IEnumerable SelectedExpandedEntities
+ {
+ get
+ {
+ return this.children == null
+ ? Enumerable.Empty()
+ : this.children.Where(node => node.Value.isExpandedNavigationProperty).Select(c => c.Key);
+ }
+ }
+
///
/// Creates a node from the given raw $select query option value, structural type information and service model.
///
@@ -441,11 +461,110 @@ internal IEnumerable GetSelectedNavigationProperties(IEd
return selectedNavigationProperties.Distinct();
}
+ ///
+ /// Gets all selected structured properties at the current node.
+ ///
+ /// The current structured type.
+ /// The selected structured properties at this node level.
+ /// Result does not include the dynamic properties, which can be retrieved by GetSelectedDynamicProperties
+ internal IEnumerable GetSelectedProperties(IEdmStructuredType structuredType)
+ {
+ if (this.selectionType == SelectionType.Empty)
+ {
+ return EmptyEdmProperties;
+ }
+
+ // We cannot determine the selected properties without the user model. This means we won't be computing the missing properties.
+ if (structuredType == null)
+ {
+ return EmptyEdmProperties;
+ }
+
+ if (this.selectionType == SelectionType.EntireSubtree || this.hasWildcard)
+ {
+ return structuredType.Properties();
+ }
+
+ Debug.Assert(this.selectedProperties != null, "selectedProperties != null");
+
+ // Get properties selected, and filter out unrecognized properties.
+ return this.selectedProperties.Select(structuredType.FindProperty).OfType();
+ }
+
+ ///
+ /// Gets the list of names for selected nullable declared or dynamic properties missing from payload.
+ ///
+ /// The current structured type.
+ /// The current resource state.
+ /// The list of property names missing from payload.
+ internal IList GetSelectedNullValueProperties(IEdmStructuredType structuredType,
+ IODataJsonLightReaderResourceState resourceState)
+ {
+ IList result = new List();
+
+ // Nothing is selected.
+ // Or we cannot determine the selected properties without the user model. This means we won't be computing the missing properties.
+ if (this.selectionType == SelectionType.Empty || structuredType == null)
+ {
+ return result;
+ }
+
+ if (this.selectionType == SelectionType.EntireSubtree || this.hasWildcard)
+ {
+ // Combine the list of declared properties with selected properties w/o wildcard,
+ IEnumerable properties = structuredType.Properties()
+ .Where(p => p.Type.IsNullable &&
+ p.PropertyKind != EdmPropertyKind.Navigation)
+ .Select(p => p.Name);
+
+ if (this.selectedProperties != null)
+ {
+ properties = properties.Concat(
+ this.selectedProperties.Where(name => !name.Equals(StarSegment)));
+ }
+
+ // Get distinct items that haven't been read.
+ result = properties.Where(name =>
+ !resourceState.Resource.Properties.Any(pRead => pRead.Name.Equals(name, StringComparison.Ordinal)) &&
+ !resourceState.NavigationPropertiesRead.Contains(name))
+ .Distinct().ToList();
+ }
+ else
+ {
+ foreach (string selected in this.selectedProperties)
+ {
+ // Skip properties that have been read (primitive or structural).
+ bool alreadyRead = resourceState.Resource.Properties.Any(p => p.Name.Equals(selected, StringComparison.Ordinal))
+ || resourceState.NavigationPropertiesRead.Contains(selected);
+
+ if (alreadyRead)
+ {
+ continue;
+ }
+
+ IEdmProperty property = structuredType.FindProperty(selected);
+ if (property != null
+ && (!property.Type.IsNullable
+ || property.PropertyKind == EdmPropertyKind.Navigation))
+ {
+ // When resolved to declared property successfully,
+ // Skip declared properties that are not null-able types.
+ // Skip navigation properties (no navigation links need to be restored as null.)
+ continue;
+ }
+
+ result.Add(selected);
+ }
+ }
+
+ return result;
+ }
+
///
/// Gets the selected stream properties for the current node.
///
/// The current entity type.
- /// The selected stream properties.
+ /// The selected stream properties at this node level.
internal IDictionary GetSelectedStreamProperties(IEdmEntityType entityType)
{
if (this.selectionType == SelectionType.Empty)
diff --git a/src/Microsoft.OData.Core/ShippingAssemblyAttributes.cs b/src/Microsoft.OData.Core/ShippingAssemblyAttributes.cs
index 8dd911d958..271a4cf883 100644
--- a/src/Microsoft.OData.Core/ShippingAssemblyAttributes.cs
+++ b/src/Microsoft.OData.Core/ShippingAssemblyAttributes.cs
@@ -19,6 +19,7 @@
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Microsoft.Test.Taupo.OData" + AssemblyRef.TestPublicKey)]
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Microsoft.OData.Core.Tests" + AssemblyRef.TestPublicKey)]
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Microsoft.OData.Client.Tests" + AssemblyRef.TestPublicKey)]
+[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Microsoft.Test.OData.Tests.Client" + AssemblyRef.TestPublicKey)]
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Microsoft.Test.OData.DependencyInjection.NetCore" + AssemblyRef.TestPublicKey)]
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Microsoft.OData.Core.Tests.NetCore" + AssemblyRef.TestPublicKey)]
#pragma warning restore 436
diff --git a/src/Microsoft.OData.Core/Uri/NodeToStringBuilder.cs b/src/Microsoft.OData.Core/Uri/NodeToStringBuilder.cs
index 042e8ab6fd..b42cab6c6f 100644
--- a/src/Microsoft.OData.Core/Uri/NodeToStringBuilder.cs
+++ b/src/Microsoft.OData.Core/Uri/NodeToStringBuilder.cs
@@ -10,10 +10,10 @@ namespace Microsoft.OData
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
+ using System.Text;
using System.Text.RegularExpressions;
using Microsoft.OData.Edm;
using Microsoft.OData.UriParser;
- using System.Text;
///
/// Build QueryNode to String Representation
diff --git a/src/Microsoft.OData.Core/UriParser/Binders/SelectBinder.cs b/src/Microsoft.OData.Core/UriParser/Binders/SelectBinder.cs
index 41e909a4af..44f5e3ce7a 100644
--- a/src/Microsoft.OData.Core/UriParser/Binders/SelectBinder.cs
+++ b/src/Microsoft.OData.Core/UriParser/Binders/SelectBinder.cs
@@ -45,7 +45,6 @@ public SelectBinder(IEdmModel model, IEdmStructuredType edmType, int maxDepth, S
/// A new SelectExpandClause decorated with the information from the selectToken
public SelectExpandClause Bind(SelectToken tokenIn)
{
-
if (tokenIn == null || !tokenIn.Properties.Any())
{
// if there are no properties selected for this level, then by default we select
diff --git a/src/Microsoft.OData.Core/UriParser/Binders/SelectExpandClauseFinisher.cs b/src/Microsoft.OData.Core/UriParser/Binders/SelectExpandClauseFinisher.cs
index c80755e2c1..54047087ba 100644
--- a/src/Microsoft.OData.Core/UriParser/Binders/SelectExpandClauseFinisher.cs
+++ b/src/Microsoft.OData.Core/UriParser/Binders/SelectExpandClauseFinisher.cs
@@ -30,7 +30,6 @@ public static void AddExplicitNavPropLinksWhereNecessary(SelectExpandClause clau
IEnumerable selectedPaths = selectItems.OfType().Select(item => item.SelectedPath);
foreach (ExpandedNavigationSelectItem navigationSelect in selectItems.Where(I => I.GetType() == typeof(ExpandedNavigationSelectItem)))
{
-
AddExplicitNavPropLinksWhereNecessary(navigationSelect.SelectAndExpand);
}
diff --git a/test/EndToEndTests/Tests/Client/Build.Desktop/Microsoft.Test.OData.Tests.Client.csproj b/test/EndToEndTests/Tests/Client/Build.Desktop/Microsoft.Test.OData.Tests.Client.csproj
index e578da7c44..c3607a897f 100644
--- a/test/EndToEndTests/Tests/Client/Build.Desktop/Microsoft.Test.OData.Tests.Client.csproj
+++ b/test/EndToEndTests/Tests/Client/Build.Desktop/Microsoft.Test.OData.Tests.Client.csproj
@@ -205,6 +205,7 @@
+
diff --git a/test/EndToEndTests/Tests/Client/Build.Desktop/WriteJsonPayloadTests/ReadOmitNullsResponseTests.cs b/test/EndToEndTests/Tests/Client/Build.Desktop/WriteJsonPayloadTests/ReadOmitNullsResponseTests.cs
new file mode 100644
index 0000000000..2f61d7751c
--- /dev/null
+++ b/test/EndToEndTests/Tests/Client/Build.Desktop/WriteJsonPayloadTests/ReadOmitNullsResponseTests.cs
@@ -0,0 +1,530 @@
+//---------------------------------------------------------------------
+//
+// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
+//
+//---------------------------------------------------------------------
+
+namespace Microsoft.Test.OData.Tests.Client.WriteJsonPayloadTests
+{
+ using System;
+ using System.IO;
+ using System.Linq;
+ using System.Text.RegularExpressions;
+ using Microsoft.OData;
+ using Microsoft.Test.OData.Services.TestServices;
+ using Microsoft.Test.OData.Tests.Client.Common;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+ using Microsoft.OData.Edm;
+ using Microsoft.OData.UriParser;
+
+ ///
+ /// Generate response by writing various payload with/without omit-value=nulls preference,
+ /// and read the response on client side.
+ /// Based on sample test provided by Mike Pizzo as omit-values=nulls requirement test cases
+ /// related to dynamic properties and annotation-only properties.
+ ///
+ [TestClass]
+ public class ReadOmitNullsResponseTest : EndToEndTestBase
+ {
+ public ReadOmitNullsResponseTest()
+ : base(ServiceDescriptors.AstoriaDefaultService)
+ {
+ }
+
+ public override void CustomTestInitialize()
+ {
+ WritePayloadHelper.CustomTestInitialize(this.ServiceUri);
+ }
+
+
+
+ [TestMethod]
+ public void ReadWriteNullExpandedNavigationTest_omitNulls_projectedEntity()
+ {
+ // Expanded entity with projection.
+ ReadWriteNullExpandedNavigationTest_omitNulls( /*nestedSelect*/true);
+ }
+
+ [TestMethod]
+ public void ReadWriteNullExpandedNavigationTest_omitNulls()
+ {
+ // Expanded entity without projection.
+ ReadWriteNullExpandedNavigationTest_omitNulls( /*nestedSelect*/false);
+ }
+
+ [TestMethod]
+ public void ReadWriteNullExpandedNavigationTest_notOmitNulls()
+ {
+ // set up model
+ EdmModel model = null;
+ EdmEntityType employeeType = null;
+ EdmEntitySet employeeSet = null;
+
+ SetupModel(out model, out employeeType, out employeeSet);
+
+ Uri serviceUri = new Uri("http://test/");
+
+ string expectedNulls = @"{
+ ""@odata.context"":""http://test/$metadata#employees(Name,Title,Dynamic,Address,Manager(),Friend())/$entity"",
+ ""Name"":""Fred"",
+ ""Title@test.annotation"":""annotationValue"",""Title"":null,
+ ""Dynamic"":null,
+ ""DynamicAnnotated@test.annotation"":""annotationValue"",""DynamicAnnotated"":null,
+ ""Address@test.annotation"":""InsufficientPrivileges"",""Address"":null,
+ ""Manager"":null,
+ ""Friend"": {""Name"":""FriendOfFred""}
+ }";
+ expectedNulls = Regex.Replace(expectedNulls, @"\s*", string.Empty, RegexOptions.Multiline);
+ Assert.IsTrue(expectedNulls.Contains(serviceUri.ToString()));
+
+ var writeSettings = new ODataMessageWriterSettings() { BaseUri = serviceUri };
+ writeSettings.ODataUri = new ODataUriParser(model, serviceUri, new Uri("employees?$select=Name,Title,Dynamic,Address&$expand=Manager,Friend", UriKind.Relative)).ParseUri();
+
+ // validate writing a null navigation property value
+ var stream = new MemoryStream();
+ var responseMessage = new StreamResponseMessage(stream);
+ responseMessage.SetHeader("Preference-Applied", "odata.include-annotations=\"*\"");
+ var messageWriter = new ODataMessageWriter(responseMessage, writeSettings, model);
+ var writer = messageWriter.CreateODataResourceWriter(employeeSet, employeeType);
+
+ WriteResponse(writer);
+
+ stream.Flush();
+
+ // compare written stream to expected stream
+ stream.Seek(0, SeekOrigin.Begin);
+ var streamReader = new StreamReader(stream);
+ Assert.AreEqual(expectedNulls, streamReader.ReadToEnd(), "Did not generate expected string when not omitting nulls");
+
+ // validate reading back the stream
+ var readSettings = new ODataMessageReaderSettings() { BaseUri = serviceUri, ReadUntypedAsString = false };
+ stream.Seek(0, SeekOrigin.Begin);
+ var messageReader = new ODataMessageReader(responseMessage, readSettings, model);
+ var reader = messageReader.CreateODataResourceReader(employeeSet, employeeType);
+ string reading = null;
+ bool managerReturned = false;
+ bool addressReturned = false;
+ int employeeEntityCount = 0;
+ ODataResource employeeResource = null;
+ ODataResource managerResource = null;
+ ODataResource friendResource = null;
+ ODataResource addressResource = null;
+ while (reader.Read())
+ {
+ switch (reader.State)
+ {
+ case ODataReaderState.ResourceEnd:
+ switch (reading)
+ {
+ case "Manager":
+ // Primitive null value for nested resource "Manager" is reported here.
+ managerResource = reader.Item as ODataResource;
+ managerReturned = true;
+ break;
+ case "Address":
+ // Primitive null value for nested complex value "Address" is reported here.
+ addressResource = reader.Item as ODataResource;
+ addressReturned = true;
+ break;
+ case "Employee":
+ ++employeeEntityCount;
+
+ // Nested resource "Friend" reading is completed before the reading of top level resource "Employee".
+ if (employeeEntityCount == 1)
+ {
+ friendResource = reader.Item as ODataResource;
+ }
+ else if (employeeEntityCount == 2)
+ {
+ employeeResource = reader.Item as ODataResource;
+ }
+ else
+ {
+ Assert.Fail($"Employee resource entry count is {employeeEntityCount}, should not be more than 2.");
+ }
+ break;
+ }
+ break;
+ case ODataReaderState.NestedResourceInfoStart:
+ reading = ((ODataNestedResourceInfo)reader.Item).Name;
+ break;
+ case ODataReaderState.NestedResourceInfoEnd:
+ reading = "Employee";
+ break;
+ }
+ }
+
+ Assert.IsTrue(employeeEntityCount == 2, $"employeeEntityCount: Expected: 2, Actual: {employeeEntityCount}");
+
+ Assert.IsTrue(addressReturned, "Address is not returned but it should be.");
+ Assert.IsNull(addressResource, "Address resource is not null but it should be.");
+
+ Assert.IsTrue(managerReturned, "Manager is not returned but it should be, because payload has its value of null.");
+ Assert.IsNull(managerResource, "Manager resource is not null but it should be, because payload has its value of null.");
+
+ Assert.IsNotNull(friendResource, "Friend resource should not be null.");
+
+ VerifyAdditionalProperties(employeeResource);
+ }
+
+ [TestMethod]
+ public void ReadWriteNullNavigationTest_omitNulls()
+ {
+ // set up model
+ EdmModel model = null;
+ EdmEntityType employeeType = null;
+ EdmEntitySet employeeSet = null;
+
+ SetupModel(out model, out employeeType, out employeeSet);
+
+ Uri serviceUri = new Uri("http://test/");
+
+ // Requirement: Omit null value for nested resource if omit-values=nulls is specified.
+ string expectedNoNulls = @"{
+ ""@odata.context"":""http://test/$metadata#employees(Name,Title,Dynamic,Address,Manager,Friend())/$entity"",
+ ""Name"":""Fred"",
+ ""Title@test.annotation"":""annotationValue"",""Title"":null,
+ ""DynamicAnnotated@test.annotation"":""annotationValue"",""DynamicAnnotated"":null,
+ ""Address@test.annotation"":""InsufficientPrivileges"",""Address"":null,
+ ""Friend"": {""Name"":""FriendOfFred""}
+ }";
+
+ expectedNoNulls = Regex.Replace(expectedNoNulls, @"\s*", string.Empty, RegexOptions.Multiline);
+ Assert.IsTrue(expectedNoNulls.Contains(serviceUri.ToString()));
+
+ var writeSettings = new ODataMessageWriterSettings() { BaseUri = serviceUri };
+ writeSettings.ODataUri = new ODataUriParser(model, serviceUri, new Uri("employees?$select=Name,Title,Dynamic,Address,Manager&$expand=Friend", UriKind.Relative)).ParseUri();
+
+ // validate writing a null navigation property value
+ var stream = new MemoryStream();
+ var responseMessage = new StreamResponseMessage(stream);
+ responseMessage.SetHeader("Preference-Applied", "omit-values=nulls,odata.include-annotations=\"*\"");
+
+ var messageWriter = new ODataMessageWriter(responseMessage, writeSettings, model);
+ var writer = messageWriter.CreateODataResourceWriter(employeeSet, employeeType);
+
+ WriteResponse(writer);
+
+ stream.Flush();
+
+ // compare written stream to expected stream
+ stream.Seek(0, SeekOrigin.Begin);
+ var streamReader = new StreamReader(stream);
+ Assert.AreEqual(expectedNoNulls, streamReader.ReadToEnd(), "Did not generate expected string when omitting nulls");
+
+ // validate reading back the stream
+ var readSettings = new ODataMessageReaderSettings() { BaseUri = serviceUri, ReadUntypedAsString = false };
+ stream.Seek(0, SeekOrigin.Begin);
+ var messageReader = new ODataMessageReader(responseMessage, readSettings, model);
+ var reader = messageReader.CreateODataResourceReader(employeeSet, employeeType);
+ string reading = null;
+ bool addressReturned = false;
+ bool managerReturned = false;
+ int employeeEntityCount = 0;
+ ODataResource employeeResource = null;
+ ODataResource friendResource = null;
+ while (reader.Read())
+ {
+ switch (reader.State)
+ {
+ case ODataReaderState.ResourceEnd:
+ switch (reading)
+ {
+ case "Address":
+ addressReturned = true;
+ break;
+ case "Manager":
+ managerReturned = true;
+ break;
+ case "Employee":
+ ++employeeEntityCount;
+
+ // Nested resource "Friend" is completed first, and the top level resource "Employee" is completed later.
+ if (employeeEntityCount == 1)
+ {
+ friendResource = reader.Item as ODataResource;
+ }
+ else if (employeeEntityCount == 2)
+ {
+ employeeResource = reader.Item as ODataResource;
+ }
+ else
+ {
+ Assert.Fail($"Employee resource entry count is {employeeEntityCount}, should not be more than 2.");
+ }
+ break;
+ }
+ break;
+ case ODataReaderState.NestedResourceInfoStart:
+ reading = ((ODataNestedResourceInfo)reader.Item).Name;
+ break;
+ case ODataReaderState.NestedResourceInfoEnd:
+ reading = "Employee";
+ break;
+ }
+ }
+
+ Assert.IsTrue(employeeEntityCount == 2, $"employeeEntityCount: Expected: 2, Actual: {employeeEntityCount}");
+ Assert.IsTrue(addressReturned, "Address is returned but it should not be.");
+ Assert.IsFalse(managerReturned, "Manager is returned but it should not be, because it is navigation link and its value is omitted in payload.");
+ Assert.IsNotNull(friendResource, "Friend resource is null but it should not be.");
+ VerifyAdditionalProperties(employeeResource);
+ Assert.IsTrue(employeeResource.Properties != null);
+ Assert.AreEqual(4, employeeResource.Properties.Count());
+ Assert.IsFalse(employeeResource.Properties.Any(p => p.Name.Equals("Address")));
+ Assert.IsFalse(employeeResource.Properties.Any(p => p.Name.Equals("Manager")));
+ }
+
+ private void ReadWriteNullExpandedNavigationTest_omitNulls(bool nestedSelect)
+ {
+ // set up model
+ EdmModel model = null;
+ EdmEntityType employeeType = null;
+ EdmEntitySet employeeSet = null;
+
+ SetupModel(out model, out employeeType, out employeeSet);
+
+ Uri serviceUri = new Uri("http://test/");
+
+ // Requirement: Omit null value for nested resource if omit-values=nulls is specified.
+ string expectedNoNulls = nestedSelect
+ ? @"{
+ ""@odata.context"":""http://test/$metadata#employees(Name,Title,Dynamic,Address,Manager(),Friend(Name,Title))/$entity"",
+ ""Name"":""Fred"",
+ ""Title@test.annotation"":""annotationValue"",""Title"":null,
+ ""DynamicAnnotated@test.annotation"":""annotationValue"",""DynamicAnnotated"":null,
+ ""Address@test.annotation"":""InsufficientPrivileges"",""Address"":null,
+ ""Friend"": {""Name"":""FriendOfFred""}
+ }"
+ : @"{
+ ""@odata.context"":""http://test/$metadata#employees(Name,Title,Dynamic,Address,Manager(),Friend())/$entity"",
+ ""Name"":""Fred"",
+ ""Title@test.annotation"":""annotationValue"",""Title"":null,
+ ""DynamicAnnotated@test.annotation"":""annotationValue"",""DynamicAnnotated"":null,
+ ""Address@test.annotation"":""InsufficientPrivileges"",""Address"":null,
+ ""Friend"": {""Name"":""FriendOfFred""}
+ }";
+
+ expectedNoNulls = Regex.Replace(expectedNoNulls, @"\s*", string.Empty, RegexOptions.Multiline);
+ Assert.IsTrue(expectedNoNulls.Contains(serviceUri.ToString()));
+
+ var writeSettings = new ODataMessageWriterSettings() { BaseUri = serviceUri };
+
+ Uri uri = nestedSelect
+ ? new Uri("employees?$select=Name,Title,Dynamic,Address&$expand=Manager,Friend($select=Name,Title)", UriKind.Relative)
+ : new Uri("employees?$select=Name,Title,Dynamic,Address&$expand=Manager,Friend", UriKind.Relative);
+ writeSettings.ODataUri = new ODataUriParser(model, serviceUri, uri).ParseUri();
+
+ // validate writing a null navigation property value
+ var stream = new MemoryStream();
+ var responseMessage = new StreamResponseMessage(stream);
+ responseMessage.SetHeader("Preference-Applied", "omit-values=nulls,odata.include-annotations=\"*\"");
+
+ var messageWriter = new ODataMessageWriter(responseMessage, writeSettings, model);
+ var writer = messageWriter.CreateODataResourceWriter(employeeSet, employeeType);
+
+ WriteResponse(writer);
+
+ stream.Flush();
+
+ // compare written stream to expected stream
+ stream.Seek(0, SeekOrigin.Begin);
+ var streamReader = new StreamReader(stream);
+ Assert.AreEqual(expectedNoNulls, streamReader.ReadToEnd(), "Did not generate expected string when omitting nulls");
+
+ // validate reading back the stream
+ var readSettings = new ODataMessageReaderSettings() { BaseUri = serviceUri, ReadUntypedAsString = false };
+ stream.Seek(0, SeekOrigin.Begin);
+ var messageReader = new ODataMessageReader(responseMessage, readSettings, model);
+ var reader = messageReader.CreateODataResourceReader(employeeSet, employeeType);
+ string reading = null;
+ bool addressReturned = false;
+ bool managerReturned = false;
+ int employeeEntityCount = 0;
+ ODataResource employeeResource = null;
+ ODataResource friendResource = null;
+ while (reader.Read())
+ {
+ switch (reader.State)
+ {
+ case ODataReaderState.ResourceEnd:
+ switch (reading)
+ {
+ case "Address":
+ addressReturned = true;
+ break;
+ case "Manager":
+ managerReturned = true;
+ break;
+ case "Employee":
+ case "Friend":
+ ++employeeEntityCount;
+
+ // Nested resource "Friend" is completed first, and the top level resource "Employee" is completed later.
+ if (employeeEntityCount == 1)
+ {
+ friendResource = reader.Item as ODataResource;
+ }
+ else if (employeeEntityCount == 2)
+ {
+ employeeResource = reader.Item as ODataResource;
+ }
+ else
+ {
+ Assert.Fail($"Employee resource entry count is {employeeEntityCount}, should not be more than 2.");
+ }
+ break;
+ }
+ break;
+ case ODataReaderState.NestedResourceInfoStart:
+ reading = ((ODataNestedResourceInfo)reader.Item).Name;
+ break;
+ case ODataReaderState.NestedResourceInfoEnd:
+ reading = "Employee";
+ break;
+ }
+ }
+
+ Assert.IsTrue(employeeEntityCount == 2, $"employeeEntityCount: Expected: 2, Actual: {employeeEntityCount}");
+ Assert.IsTrue(addressReturned, "Address is annotated, should be returned but isn't.");
+ Assert.IsFalse(managerReturned, "Manager is returned but it should not be, because value is omitted in payload.");
+
+ Assert.IsNotNull(friendResource, "Friend resource is null but it should not be.");
+ Assert.IsTrue(friendResource.Properties.Count() == (nestedSelect ? 2 : 3));
+ Assert.IsTrue(friendResource.Properties.Any(p => (p.Name.Equals("Name", StringComparison.Ordinal) && p.Value.Equals("FriendOfFred"))));
+ Assert.IsTrue(friendResource.Properties.Any(p => (p.Name.Equals("Title", StringComparison.Ordinal) && p.Value == null)));
+ Assert.IsTrue(nestedSelect
+ ? !friendResource.Properties.Any(p => p.Name.Equals("Address", StringComparison.Ordinal))
+ : friendResource.Properties.Any(p => (p.Name.Equals("Address", StringComparison.Ordinal) && p.Value == null)));
+
+
+ Assert.IsNotNull(employeeResource);
+ VerifyAdditionalProperties(employeeResource);
+ Assert.IsTrue(employeeResource.Properties != null && employeeResource.Properties.Any(p => p.Name.Equals("Manager") && (p.Value == null)),
+ "Expanded null entity should be restored if omit-values=nulls.");
+ }
+
+ private void SetupModel(out EdmModel model, out EdmEntityType employeeType, out EdmEntitySet employeeSet)
+ {
+ // set up model
+ model = new EdmModel();
+ var addressType = model.AddComplexType("test", "Address");
+ addressType.AddStructuralProperty("city", EdmPrimitiveTypeKind.String, true);
+ employeeType = model.AddEntityType("test", "Employee", null, false, true);
+ var key = employeeType.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String, false);
+ employeeType.AddKeys(key);
+ employeeType.AddStructuralProperty("Title", EdmPrimitiveTypeKind.String, true);
+ employeeType.AddStructuralProperty("Address", new EdmComplexTypeReference(addressType, true));
+
+ employeeType.AddUnidirectionalNavigation(
+ new EdmNavigationPropertyInfo
+ {
+ Name = "Manager",
+ TargetMultiplicity = EdmMultiplicity.ZeroOrOne,
+ Target = employeeType
+ }
+ );
+
+ employeeType.AddUnidirectionalNavigation(
+ new EdmNavigationPropertyInfo
+ {
+ Name = "Friend",
+ TargetMultiplicity = EdmMultiplicity.ZeroOrOne,
+ Target = employeeType
+ }
+ );
+
+ var container = model.AddEntityContainer("test", "service");
+
+ employeeSet = container.AddEntitySet("employees", employeeType);
+ }
+
+ private void WriteResponse(ODataWriter writer)
+ {
+ writer.WriteStart(new ODataResource
+ {
+ Properties = new ODataProperty[]
+ {
+ new ODataProperty {Name = "Name", Value = "Fred" },
+ new ODataProperty {Name = "Title", Value = new ODataNullValue(),
+ InstanceAnnotations = new ODataInstanceAnnotation[] {
+ new ODataInstanceAnnotation("test.annotation", new ODataPrimitiveValue( "annotationValue" ))
+ }
+ },
+ new ODataProperty {Name = "Dynamic", Value = new ODataNullValue() },
+ new ODataProperty {Name = "DynamicAnnotated", Value = new ODataNullValue(),
+ InstanceAnnotations = new ODataInstanceAnnotation[] {
+ new ODataInstanceAnnotation("test.annotation", new ODataPrimitiveValue( "annotationValue" ))
+ }
+ }
+ }
+ });
+
+ // write address
+ ODataNestedResourceInfo addrNestedResourceInfo = new ODataNestedResourceInfo
+ {
+ Name = "Address",
+ IsCollection = false
+ };
+ addrNestedResourceInfo.SetInstanceAnnotations(new ODataInstanceAnnotation[]
+ { new ODataInstanceAnnotation("test.annotation", new ODataPrimitiveValue("InsufficientPrivileges")) }
+ );
+
+ writer.WriteStart(addrNestedResourceInfo);
+ writer.WriteStart((ODataResource)null);
+ writer.WriteEnd(); //address
+ writer.WriteEnd(); //address nestedInfo
+
+ // write manager
+ writer.WriteStart(new ODataNestedResourceInfo
+ {
+ Name = "Manager",
+ IsCollection = false
+ });
+ writer.WriteStart((ODataResource)null);
+ writer.WriteEnd();
+ writer.WriteEnd(); // manager nested info
+
+ // write friend
+ writer.WriteStart(new ODataNestedResourceInfo
+ {
+ Name = "Friend",
+ IsCollection = false
+ });
+ writer.WriteStart(new ODataResource
+ {
+ Properties = new ODataProperty[]
+ {
+ new ODataProperty { Name = "Name", Value = "FriendOfFred" }
+ }
+ });
+ writer.WriteEnd();
+ writer.WriteEnd(); // friend nested info
+
+ writer.WriteEnd(); // resource
+ }
+
+ private void VerifyAdditionalProperties(ODataResource employeeResource)
+ {
+ Assert.IsNotNull(employeeResource, "Employee should not be null");
+ Assert.IsNotNull(employeeResource.Properties.FirstOrDefault(p => p.Name.Equals("Name", StringComparison.Ordinal)));
+ var titleProperty = employeeResource.Properties.FirstOrDefault(p => p.Name.Equals("Title", StringComparison.Ordinal));
+ Assert.IsNotNull(titleProperty, "Title property should not be null");
+ Assert.IsNull(titleProperty.Value, "Title property value should be null");
+ var titleAnnotation = titleProperty.InstanceAnnotations.FirstOrDefault(a => a.Name.Equals("test.annotation", StringComparison.Ordinal));
+ Assert.IsNotNull(titleAnnotation, "Title property missing the test.annotation");
+ Assert.AreEqual(((ODataPrimitiveValue)titleAnnotation.Value).Value.ToString(), "annotationValue");
+ var dynamicProperty = employeeResource.Properties.FirstOrDefault(p => p.Name.Equals("Dynamic", StringComparison.Ordinal));
+ Assert.IsNotNull(dynamicProperty, "Dynamic property should not be null");
+ Assert.IsNull(dynamicProperty.Value, "Dynamic property value should be null");
+ var dynamicAnnotatedProperty = employeeResource.Properties.FirstOrDefault(p => p.Name.Equals("DynamicAnnotated", StringComparison.Ordinal));
+ Assert.IsNotNull(dynamicAnnotatedProperty, "DynamicAnnotated property should not be null");
+ Assert.IsNull(dynamicAnnotatedProperty.Value, "DynamicAnnotated property value should be null");
+ var dynamicAnnotation = dynamicAnnotatedProperty.InstanceAnnotations.FirstOrDefault(a => a.Name.Equals("test.annotation", StringComparison.Ordinal));
+ Assert.IsNotNull(dynamicAnnotation, "DynamicAnnotated property missing the test.annotation");
+ Assert.AreEqual(((ODataPrimitiveValue)dynamicAnnotation.Value).Value.ToString(), "annotationValue");
+ }
+ }
+}
+
+
diff --git a/test/EndToEndTests/Tests/Client/Build.Desktop/WriteJsonPayloadTests/WriteJsonWithoutModelTests.cs b/test/EndToEndTests/Tests/Client/Build.Desktop/WriteJsonPayloadTests/WriteJsonWithoutModelTests.cs
index 72cc1e0fbc..2eccad2d04 100644
--- a/test/EndToEndTests/Tests/Client/Build.Desktop/WriteJsonPayloadTests/WriteJsonWithoutModelTests.cs
+++ b/test/EndToEndTests/Tests/Client/Build.Desktop/WriteJsonPayloadTests/WriteJsonWithoutModelTests.cs
@@ -145,6 +145,36 @@ public void ExpandedCustomerEntryTest()
}
}
+ ///
+ /// Write and read an expanded customer entry containing primitive, complex, collection of primitive/complex properties with null values
+ /// using omit-value=nulls Preference header.
+ ///
+ [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);
+ }
+ }
+ }
+
///
/// Write an entry containing stream, named stream
///
@@ -519,10 +549,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)
@@ -533,30 +562,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()
- {
- 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()
+ {
+ 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;
@@ -601,6 +641,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 verifyFeed = (feed) =>
+ {
+ verifyFeedCalled = true;
+ };
+
+ Action 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 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)
{
diff --git a/test/EndToEndTests/Tests/Client/Build.Desktop/WriteJsonPayloadTests/WritePayloadHelper.cs b/test/EndToEndTests/Tests/Client/Build.Desktop/WriteJsonPayloadTests/WritePayloadHelper.cs
index b6aeb4bce0..e832e50405 100644
--- a/test/EndToEndTests/Tests/Client/Build.Desktop/WriteJsonPayloadTests/WritePayloadHelper.cs
+++ b/test/EndToEndTests/Tests/Client/Build.Desktop/WriteJsonPayloadTests/WritePayloadHelper.cs
@@ -4,6 +4,8 @@
//
//---------------------------------------------------------------------
+using System.Diagnostics;
+
namespace Microsoft.Test.OData.Tests.Client.WriteJsonPayloadTests
{
using System;
@@ -594,9 +596,9 @@ public static ODataResourceWrapper CreateOrderEntry2(bool hasModel)
#region ExpandedCustomerEntryTestHelper
- public static ODataResourceWrapper CreateCustomerEntry(bool hasModel)
+ public static ODataResourceWrapper CreateCustomerEntry(bool hasModel, bool withSomeNullValues = false)
{
- var customerEntryWrapper = CreateCustomerResourceWrapperNoMetadata(hasModel);
+ var customerEntryWrapper = CreateCustomerResourceWrapperNoMetadata(hasModel, withSomeNullValues);
var customerEntry = customerEntryWrapper.Resource;
customerEntry.Id = new Uri(ServiceUri + "Customer(-9)");
@@ -1079,7 +1081,7 @@ public static ODataResourceWrapper CreateAuditInforWrapper()
};
}
- public static IEnumerable CreateCustomerNavigationLinks()
+ public static IEnumerable CreateCustomerNavigationLinks(bool withSomeNullValues)
{
return new List()
{
@@ -1098,7 +1100,9 @@ public static IEnumerable CreateCustomerNavigati
{
Name = "Husband",
IsCollection = false,
- Url = new Uri(ServiceUri + "Customer(-9)/Husband")
+ Url = withSomeNullValues
+ ? null
+ : new Uri(ServiceUri + "Customer(-9)/Husband")
}
},
new ODataNestedResourceInfoWrapper()
@@ -1107,7 +1111,9 @@ public static IEnumerable CreateCustomerNavigati
{
Name = "Wife",
IsCollection = false,
- Url = new Uri(ServiceUri + "Customer(-9)/Wife")
+ Url = withSomeNullValues
+ ? null
+ : new Uri(ServiceUri + "Customer(-9)/Wife")
}
},
new ODataNestedResourceInfoWrapper()
@@ -1131,7 +1137,7 @@ public static ODataResource CreateLoginEntry(bool hasModel)
return loginEntry;
}
- public static IEnumerable CreateLoginNavigationLinksWrapper()
+ public static IEnumerable CreateLoginNavigationLinksWrapper(bool withSomeNullValues)
{
return new List()
{
@@ -1150,7 +1156,9 @@ public static IEnumerable CreateLoginNavigationL
{
Name = "LastLogin",
IsCollection = false,
- Url = new Uri(ServiceUri + "Login('2')/LastLogin")
+ Url = withSomeNullValues
+ ? null
+ : new Uri(ServiceUri + "Login('2')/LastLogin")
}
},
new ODataNestedResourceInfoWrapper()
@@ -1159,7 +1167,9 @@ public static IEnumerable CreateLoginNavigationL
{
Name = "SentMessages",
IsCollection = true,
- Url = new Uri(ServiceUri + "Login('2')/SentMessages")
+ Url = withSomeNullValues
+ ? null
+ : new Uri(ServiceUri + "Login('2')/SentMessages")
}
},
new ODataNestedResourceInfoWrapper()
@@ -1386,7 +1396,7 @@ public static ODataNestedResourceInfo AddOrderEntryCustomNavigation(ODataResourc
return orderEntry2Navigation;
}
- public static ODataResourceWrapper CreateCustomerResourceWrapperNoMetadata(bool hasModel)
+ public static ODataResourceWrapper CreateCustomerResourceWrapperNoMetadata(bool hasModel, bool withSomeNullValues = false)
{
var customerEntry = new ODataResource()
{
@@ -1394,7 +1404,8 @@ public static ODataResourceWrapper CreateCustomerResourceWrapperNoMetadata(bool
};
var customerEntryP1 = new ODataProperty { Name = "CustomerId", Value = -9 };
- var customerEntryP2 = new ODataProperty { Name = "Name", Value = "CustomerName" };
+ string aCustomerName = withSomeNullValues ? null : "CustomerName";
+ var customerEntryP2 = new ODataProperty {Name = "Name", Value = aCustomerName };
var primaryContactInfo_nestedInfoWrapper = new ODataNestedResourceInfoWrapper()
{
diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/IntegrationTests/Reader/JsonLight/PropertyAndValueJsonLightReaderIntegrationTests.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/IntegrationTests/Reader/JsonLight/PropertyAndValueJsonLightReaderIntegrationTests.cs
index 3a8714590e..1d2079472c 100644
--- a/test/FunctionalTests/Microsoft.OData.Core.Tests/IntegrationTests/Reader/JsonLight/PropertyAndValueJsonLightReaderIntegrationTests.cs
+++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/IntegrationTests/Reader/JsonLight/PropertyAndValueJsonLightReaderIntegrationTests.cs
@@ -8,10 +8,12 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Reflection;
using System.Text;
using FluentAssertions;
using Microsoft.OData.Tests.JsonLight;
using Microsoft.OData.Edm;
+using Microsoft.OData.Tests.Evaluation;
using Microsoft.Test.OData.DependencyInjection;
using Xunit;
using ODataErrorStrings = Microsoft.OData.Strings;
@@ -20,6 +22,8 @@ namespace Microsoft.OData.Tests.IntegrationTests.Reader.JsonLight
{
public class PropertyAndValueJsonLightReaderIntegrationTests
{
+ private static string omitValuesNulls = "omit-values=" + ODataConstants.OmitValuesNulls;
+
[Fact]
public void ReadingPayloadInt64SingleDoubleDecimalWithoutSuffix()
{
@@ -67,7 +71,7 @@ public void ReadingPayloadInt64SingleDoubleDecimalWithoutSuffix()
IEdmModel mainModel = TestUtils.WrapReferencedModelsToMainModel("EntityNs", "MyContainer", model);
List entries = new List();
- ODataNestedResourceInfo navigationLink;
+ ODataNestedResourceInfo nestedResourceInfo;
this.ReadEntryPayload(mainModel, payload, entitySet, entityType,
reader =>
{
@@ -77,7 +81,7 @@ public void ReadingPayloadInt64SingleDoubleDecimalWithoutSuffix()
entries.Add(reader.Item as ODataResource);
break;
case ODataReaderState.NestedResourceInfoStart:
- navigationLink = (ODataNestedResourceInfo)reader.Item;
+ nestedResourceInfo = (ODataNestedResourceInfo)reader.Item;
break;
default:
break;
@@ -106,7 +110,7 @@ public void ReadingPayloadInt64SingleDoubleDecimalWithoutSuffix()
}
[Fact]
- public void ReadNullableCollectionValue()
+ public void ReadNullableCollectionValueExpanded()
{
EdmModel model = new EdmModel();
EdmEntityType entityType = new EdmEntityType("NS", "MyTestEntity");
@@ -128,25 +132,30 @@ public void ReadNullableCollectionValue()
"}";
IEdmModel mainModel = TestUtils.WrapReferencedModelsToMainModel("EntityNs", "MyContainer", model);
- ODataResource entry = null;
- this.ReadEntryPayload(mainModel, payload, entitySet, entityType, reader => { entry = entry ?? reader.Item as ODataResource; });
- Assert.NotNull(entry);
- var intCollection = entry.Properties.FirstOrDefault(
- s => string.Equals(
- s.Name,
- "NullableIntNumbers",
- StringComparison.OrdinalIgnoreCase)).Value.As();
- var list = new List();
- foreach (var val in intCollection.Items)
+ foreach (bool isNullValuesOmitted in new bool[] {false, true})
{
- list.Add(val as int?);
- }
+ ODataResource entry = null;
+ this.ReadEntryPayload(mainModel, payload, entitySet, entityType,
+ reader => { entry = entry ?? reader.Item as ODataResource; }, isNullValuesOmitted);
+ Assert.NotNull(entry);
+
+ var intCollection = entry.Properties.FirstOrDefault(
+ s => string.Equals(
+ s.Name,
+ "NullableIntNumbers",
+ StringComparison.OrdinalIgnoreCase)).Value.As();
+ var list = new List();
+ foreach (var val in intCollection.Items)
+ {
+ list.Add(val as int?);
+ }
- Assert.Equal(0, list[0]);
- Assert.Null(list[1]);
- Assert.Equal(1, (int)list[2]);
- Assert.Equal(2, (int)list[3]);
+ Assert.Equal(0, list[0]);
+ Assert.Null(list[1]);
+ Assert.Equal(1, (int) list[2]);
+ Assert.Equal(2, (int) list[3]);
+ }
}
[Fact]
@@ -176,53 +185,60 @@ public void ReadOpenCollectionPropertyValue()
"}";
IEdmModel mainModel = TestUtils.WrapReferencedModelsToMainModel("EntityNs", "MyContainer", model);
- ODataResource entry = null;
- List complexCollection = new List();
- ODataNestedResourceInfo nestedOpenCollection = null;
- bool startComplexCollection = false;
- this.ReadEntryPayload(mainModel, payload, entitySet, entityType,
- reader =>
- {
- switch (reader.State)
- {
- case ODataReaderState.NestedResourceInfoStart:
- startComplexCollection = true;
- break;
- case ODataReaderState.ResourceEnd:
- if(startComplexCollection)
- {
- complexCollection.Add(reader.Item as ODataResource);
- }
- entry = reader.Item as ODataResource;
- break;
- case ODataReaderState.NestedResourceInfoEnd:
- startComplexCollection = false;
- nestedOpenCollection = reader.Item as ODataNestedResourceInfo;
- break;
- }
- });
- Assert.NotNull(entry);
- var intCollection = entry.Properties.FirstOrDefault(
- s => string.Equals(
- s.Name,
- "OpenPrimitiveCollection",
- StringComparison.OrdinalIgnoreCase)).Value.As();
- var list = new List();
- foreach (var val in intCollection.Items)
+ foreach (bool isNullValuesOmitted in new bool[] {false, true})
{
- list.Add(val as int?);
- }
+ ODataResource entry = null;
+ List complexCollection = new List();
+ ODataNestedResourceInfo nestedOpenCollection = null;
+ bool startComplexCollection = false;
+ this.ReadEntryPayload(mainModel, payload, entitySet, entityType,
+ reader =>
+ {
+ switch (reader.State)
+ {
+ case ODataReaderState.NestedResourceInfoStart:
+ startComplexCollection = true;
+ break;
+ case ODataReaderState.ResourceEnd:
+ if (startComplexCollection)
+ {
+ complexCollection.Add(reader.Item as ODataResource);
+ }
+ entry = reader.Item as ODataResource;
+ break;
+ case ODataReaderState.NestedResourceInfoEnd:
+ startComplexCollection = false;
+ nestedOpenCollection = reader.Item as ODataNestedResourceInfo;
+ break;
+ }
+ },
+ nullValuesOmitted: isNullValuesOmitted);
+ Assert.NotNull(entry);
+
+ var intCollection = entry.Properties.FirstOrDefault(
+ s => string.Equals(
+ s.Name,
+ "OpenPrimitiveCollection",
+ StringComparison.OrdinalIgnoreCase)).Value.As();
+ var list = new List();
+ foreach (var val in intCollection.Items)
+ {
+ list.Add(val as int?);
+ }
- Assert.Equal(0, list[0]);
- Assert.Equal(1, (int)list[1]);
- Assert.Equal(2, (int)list[2]);
+ Assert.Equal(0, list[0]);
+ Assert.Equal(1, (int) list[1]);
+ Assert.Equal(2, (int) list[2]);
- Assert.Equal("OpenComplexTypeCollection", nestedOpenCollection.Name);
+ Assert.Equal("OpenComplexTypeCollection", nestedOpenCollection.Name);
- foreach (var val in complexCollection)
- {
- val.Properties.FirstOrDefault(s => string.Equals(s.Name, "CLongId", StringComparison.OrdinalIgnoreCase)).Value.ShouldBeEquivalentTo(1L, "value should be in correct type.");
+ foreach (var val in complexCollection)
+ {
+ val.Properties.FirstOrDefault(
+ s => string.Equals(s.Name, "CLongId", StringComparison.OrdinalIgnoreCase))
+ .Value.ShouldBeEquivalentTo(1L, "value should be in correct type.");
+ }
}
}
@@ -325,7 +341,7 @@ public void ReadingTypeDefinitionPayloadJsonLight()
"}";
List entries = new List();
- ODataNestedResourceInfo navigationLink = null;
+ ODataNestedResourceInfo nestedResourceInfo = null;
this.ReadEntryPayload(model, payload, entitySet, entityType,
reader =>
{
@@ -335,7 +351,7 @@ public void ReadingTypeDefinitionPayloadJsonLight()
entries.Add(reader.Item as ODataResource);
break;
case ODataReaderState.NestedResourceInfoStart:
- navigationLink = (ODataNestedResourceInfo)reader.Item;
+ nestedResourceInfo = (ODataNestedResourceInfo)reader.Item;
break;
default:
break;
@@ -348,11 +364,89 @@ public void ReadingTypeDefinitionPayloadJsonLight()
propertyList[1].Name.Should().Be("Weight");
propertyList[1].Value.Should().Be(60.5);
- navigationLink.Name.Should().Be("Address");
+ nestedResourceInfo.Name.Should().Be("Address");
var address = entries[1];
address.Properties.FirstOrDefault(s => string.Equals(s.Name, "CountryRegion", StringComparison.OrdinalIgnoreCase)).Value.Should().Be("China");
}
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void ReadingTypeDefinitionPayloadJsonLightWithOmittedNullValues(bool bNullValuesOmitted)
+ {
+ EdmEntityType entityType;
+ EdmEntitySet entitySet;
+ EdmModel model = BuildEdmModelForOmittedNullValuesTestCases(out entityType, out entitySet);
+
+ const string payload = @"
+ {
+ ""@odata.context"":""http://www.example.com/$metadata#EntityNs.MyContainer.People/$entity"",
+ ""@odata.id"":""http://mytest"",
+ ""Id"":0,
+ ""Weight"":60.5,
+ ""Address"":{""CountryRegion"":""US"", ""City"":""Redmond""},
+ ""Education"":null
+ }";
+
+ List entries = new List();
+ List nestedResourceInfos = new List();
+ string reading = "init";
+ this.ReadEntryPayload(model, payload, entitySet, entityType,
+ reader =>
+ {
+ switch (reader.State)
+ {
+ case ODataReaderState.ResourceStart:
+ if (reading.Equals("Education", StringComparison.Ordinal))
+ {
+ reader.Item.Should().BeNull();
+ }
+ else
+ {
+ entries.Add(reader.Item as ODataResource);
+ }
+ break;
+ case ODataReaderState.NestedResourceInfoStart:
+ nestedResourceInfos.Add((ODataNestedResourceInfo)reader.Item);
+ reading = (reader.Item as ODataNestedResourceInfo).Name;
+ break;
+ default:
+ break;
+ }
+ },
+ nullValuesOmitted: bNullValuesOmitted);
+
+ Assert.Equal(2, entries.Count);
+
+ ODataResource person = entries[0];
+ person.Properties.FirstOrDefault(s => string.Equals(s.Name, "Id", StringComparison.Ordinal))
+ .Value.Should().Be(0);
+ person.Properties.FirstOrDefault(s => string.Equals(s.Name, "Weight", StringComparison.Ordinal))
+ .Value.Should().Be(60.5);
+
+ ODataResource address = entries[1];
+ address.Properties.FirstOrDefault(s => string.Equals(s.Name, "CountryRegion", StringComparison.Ordinal))
+ .Value.Should().Be("US");
+ address.Properties.FirstOrDefault(s => string.Equals(s.Name, "City", StringComparison.Ordinal))
+ .Value.Should().Be("Redmond");
+
+ if (bNullValuesOmitted)
+ {
+ // Omitted value should be restored to null.
+ address.Properties.FirstOrDefault(s => string.Equals(s.Name, "ZipCode", StringComparison.Ordinal))
+ .Value.Should().BeNull();
+ }
+ else
+ {
+ address.Properties.Any(s => string.Equals(s.Name, "ZipCode", StringComparison.Ordinal)).Should().BeFalse();
+ }
+
+ nestedResourceInfos.Count().Should().Be(3);
+ nestedResourceInfos.Any(info => info.Name.Equals("Address")).Should().BeTrue();
+ nestedResourceInfos.Any(info => info.Name.Equals("Education")).Should().BeTrue();
+ nestedResourceInfos.Any(info => info.Name.Equals("Company")).Should().BeTrue();
+ }
+
[Fact]
public void ReadingTypeDefinitionPayloadWithTypeAnnotationJsonLight()
{
@@ -397,7 +491,7 @@ public void ReadingTypeDefinitionPayloadWithTypeAnnotationJsonLight()
"}";
List entries = new List();
- ODataNestedResourceInfo navigationLink = null;
+ ODataNestedResourceInfo nestedResourceInfo = null;
this.ReadEntryPayload(model, payload, entitySet, entityType,
reader =>
{
@@ -407,7 +501,7 @@ public void ReadingTypeDefinitionPayloadWithTypeAnnotationJsonLight()
entries.Add(reader.Item as ODataResource);
break;
case ODataReaderState.NestedResourceInfoStart:
- navigationLink = (ODataNestedResourceInfo)reader.Item;
+ nestedResourceInfo = (ODataNestedResourceInfo)reader.Item;
break;
default:
break;
@@ -422,7 +516,7 @@ public void ReadingTypeDefinitionPayloadWithTypeAnnotationJsonLight()
propertyList[2].Name.Should().Be("Height");
propertyList[2].Value.Should().Be(180);
- navigationLink.Name.Should().Be("Address");
+ nestedResourceInfo.Name.Should().Be("Address");
var address = entries[1];
address.Properties.FirstOrDefault(s => string.Equals(s.Name, "CountryRegion", StringComparison.OrdinalIgnoreCase)).Value.Should().Be("China");
}
@@ -474,7 +568,7 @@ public void ReadingTypeDefinitionPayloadWithMultipleTypeDefinitionJsonLight()
"}";
List entries = new List();
- ODataNestedResourceInfo navigationLink = null;
+ ODataNestedResourceInfo nestedResourceInfo = null;
this.ReadEntryPayload(model, payload, entitySet, entityType,
reader =>
{
@@ -484,7 +578,7 @@ public void ReadingTypeDefinitionPayloadWithMultipleTypeDefinitionJsonLight()
entries.Add(reader.Item as ODataResource);
break;
case ODataReaderState.NestedResourceInfoStart:
- navigationLink = (ODataNestedResourceInfo)reader.Item;
+ nestedResourceInfo = (ODataNestedResourceInfo)reader.Item;
break;
default:
break;
@@ -497,7 +591,7 @@ public void ReadingTypeDefinitionPayloadWithMultipleTypeDefinitionJsonLight()
propertyList[1].Name.Should().Be("Weight");
propertyList[1].Value.Should().Be(60.5);
- navigationLink.Name.Should().Be("Address");
+ nestedResourceInfo.Name.Should().Be("Address");
var address = entries[1];
address.Properties.FirstOrDefault(s => string.Equals(s.Name, "CountryRegion", StringComparison.OrdinalIgnoreCase)).Value.Should().Be("China");
}
@@ -546,7 +640,7 @@ public void ReadingTypeDefinitionPayloadWithEdmTypeAnnotationJsonLight()
"}";
List entries = new List();
- ODataNestedResourceInfo navigationLink = null;
+ ODataNestedResourceInfo nestedResourceInfo = null;
this.ReadEntryPayload(model, payload, entitySet, entityType,
reader =>
{
@@ -556,7 +650,7 @@ public void ReadingTypeDefinitionPayloadWithEdmTypeAnnotationJsonLight()
entries.Add(reader.Item as ODataResource);
break;
case ODataReaderState.NestedResourceInfoStart:
- navigationLink = (ODataNestedResourceInfo)reader.Item;
+ nestedResourceInfo = (ODataNestedResourceInfo)reader.Item;
break;
default:
break;
@@ -569,7 +663,7 @@ public void ReadingTypeDefinitionPayloadWithEdmTypeAnnotationJsonLight()
propertyList[1].Name.Should().Be("Weight");
propertyList[1].Value.Should().Be(60.5);
- navigationLink.Name.Should().Be("Address");
+ nestedResourceInfo.Name.Should().Be("Address");
var address = entries[1];
address.Properties.FirstOrDefault(s => string.Equals(s.Name, "CountryRegion", StringComparison.OrdinalIgnoreCase)).Value.Should().Be("China");
}
@@ -773,6 +867,687 @@ public void ReadingNullValueForDeclaredCollectionPropertyInComplexTypeShouldFail
read.ShouldThrow().WithMessage("A null value was found for the property named 'CountriesOrRegions', which has the expected type 'Collection(Edm.String)[Nullable=False]'. The expected type 'Collection(Edm.String)[Nullable=False]' does not allow null values.");
}
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void ReadingPropertyInTopLevelOrInComplexTypeShouldRestoreOmittedNullValues(
+ bool bNullValuesOmitted)
+ {
+ EdmEntityType entityType;
+ EdmEntitySet entitySet;
+ EdmModel model = BuildEdmModelForOmittedNullValuesTestCases(out entityType, out entitySet);
+
+ const string payload =@"{
+ ""@odata.context"":""http://www.example.com/$metadata#EntityNs.MyContainer.People/$entity"",
+ ""@odata.id"":""http://mytest"",
+ ""Id"":0,
+ ""Education"":{""Id"":1}
+ }";
+
+ List entries = new List();
+ this.ReadEntryPayload(model, payload, entitySet, entityType,
+ reader =>
+ {
+ switch (reader.State)
+ {
+ case ODataReaderState.ResourceStart:
+ entries.Add(reader.Item as ODataResource);
+ break;
+ default:
+ break;
+ }
+ },
+ nullValuesOmitted: bNullValuesOmitted);
+
+ Assert.Equal(2, entries.Count);
+
+ ODataResource edu = entries.FirstOrDefault(s => string.Equals(s.TypeName, "NS.Edu", StringComparison.Ordinal));
+ edu.Should().NotBeNull();
+ edu.Properties.FirstOrDefault(s => string.Equals(s.Name, "Id", StringComparison.Ordinal)).Value.Should().Be(1);
+
+ if (bNullValuesOmitted)
+ {
+ // null-able collection should be restored.
+ edu.Properties.FirstOrDefault(s => string.Equals(s.Name, "Campuses", StringComparison.Ordinal)).Value.Should().BeNull();
+ // null-able property value should be restored.
+ edu.Properties.FirstOrDefault(s => string.Equals(s.Name, "SchoolName", StringComparison.Ordinal)).Value.Should().BeNull();
+ edu.Properties.FirstOrDefault(s => string.Equals(s.Name, "OrgId", StringComparison.Ordinal)).Value.Should().BeNull();
+ edu.Properties.FirstOrDefault(s => string.Equals(s.Name, "OrgCategory", StringComparison.Ordinal)).Value.Should().BeNull();
+
+ }
+ else
+ {
+ edu.Properties.Any(s => string.Equals(s.Name, "Campuses", StringComparison.Ordinal)).Should().BeFalse();
+ edu.Properties.Any(s => string.Equals(s.Name, "SchoolName", StringComparison.Ordinal)).Should().BeFalse();
+ edu.Properties.Any(s => string.Equals(s.Name, "OrgId", StringComparison.Ordinal)).Should().BeFalse();
+ edu.Properties.Any(s => string.Equals(s.Name, "OrgCategory", StringComparison.Ordinal)).Should().BeFalse();
+ }
+
+ ODataResource person = entries.FirstOrDefault(s => string.Equals(s.TypeName, "NS.Person", StringComparison.Ordinal));
+ person.Should().NotBeNull();
+ person.Properties.FirstOrDefault(s => string.Equals(s.Name, "Id", StringComparison.Ordinal)).Value.Should().Be(0);
+
+ if (bNullValuesOmitted)
+ {
+ // omitted null-able property should be restored as null.
+ person.Properties.FirstOrDefault(s => string.Equals(s.Name, "Height", StringComparison.Ordinal)).Value.Should().BeNull();
+ }
+ else
+ {
+ person.Properties.Any(s => string.Equals(s.Name, "Height", StringComparison.Ordinal)).Should().BeFalse();
+ }
+
+ // missing non-null-able property should not be restored.
+ person.Properties.Any(s => string.Equals(s.Name, "Weight", StringComparison.Ordinal)).Should().BeFalse();
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void ReadingPropertyInTopLevelOrInComplexTypeShouldRestoreOmittedNullValuesWithSelectedPropertiesEntireSubTree(
+ bool bNullValuesOmitted)
+ {
+ EdmEntityType entityType;
+ EdmEntitySet entitySet;
+ EdmModel model = BuildEdmModelForOmittedNullValuesTestCases(out entityType, out entitySet);
+
+ // null-able property Height is not selected, thus should not be restored.
+ // null-able property Address is selected, thus should be restored.
+ // Property Education is null-able.
+ const string payloadWithQueryOption = @"{
+ ""@odata.context"":""http://www.example.com/$metadata#EntityNs.MyContainer.People(Id,UnknownPropX,Education,Address)/$entity"",
+ ""@odata.id"":""http://mytest"",
+ ""Id"":0,
+ ""Education"":{""Id"":1},
+ ""Address"":null
+ }";
+ const string payloadWithWildcardInQueryOption = @"{
+ ""@odata.context"":""http://www.example.com/$metadata#EntityNs.MyContainer.People(Id,UnknownPropX,Education/*,Address)/$entity"",
+ ""@odata.id"":""http://mytest"",
+ ""Id"":0,
+ ""Education"":{""Id"":1},
+ ""Address"":null
+ }";
+
+ foreach (string payload in new string[] {payloadWithQueryOption, payloadWithWildcardInQueryOption})
+ {
+ List entries = new List();
+ List nestedResourceInfos = new List();
+ string reading = "init";
+ this.ReadEntryPayload(model, payload, entitySet, entityType,
+ reader =>
+ {
+ switch (reader.State)
+ {
+ case ODataReaderState.ResourceStart:
+ if (reading.Equals("Address", StringComparison.Ordinal))
+ {
+ reader.Item.Should().BeNull();
+ }
+ else
+ {
+ entries.Add(reader.Item as ODataResource);
+ }
+ break;
+ case ODataReaderState.NestedResourceInfoStart:
+ nestedResourceInfos.Add(reader.Item as ODataNestedResourceInfo);
+ reading = (reader.Item as ODataNestedResourceInfo).Name;
+ break;
+ default:
+ break;
+ }
+ },
+ nullValuesOmitted: bNullValuesOmitted);
+
+ entries.Count.Should().Be(2);
+ nestedResourceInfos.Count.Should().Be(2);
+ nestedResourceInfos.FirstOrDefault(s => string.Equals(s.Name, "Education", StringComparison.Ordinal))
+ .Should().NotBeNull();
+ nestedResourceInfos.FirstOrDefault(s => string.Equals(s.Name, "Address", StringComparison.Ordinal))
+ .Should().NotBeNull();
+
+ // Education
+ ODataResource edu =
+ entries.FirstOrDefault(s => string.Equals(s.TypeName, "NS.Edu", StringComparison.Ordinal));
+ edu.Should().NotBeNull();
+ edu.Properties.FirstOrDefault(s => string.Equals(s.Name, "Id", StringComparison.Ordinal))
+ .Value.Should().Be(1);
+ if (bNullValuesOmitted)
+ {
+ // null-able collection should be restored.
+ edu.Properties.FirstOrDefault(s => string.Equals(s.Name, "Campuses", StringComparison.Ordinal))
+ .Value.Should().BeNull();
+ // null-able property value should be restored.
+ edu.Properties.FirstOrDefault(s => string.Equals(s.Name, "SchoolName", StringComparison.Ordinal))
+ .Value.Should().BeNull();
+ }
+ else
+ {
+ edu.Properties.Any(s => string.Equals(s.Name, "Campuses", StringComparison.Ordinal)).Should().BeFalse();
+ edu.Properties.Any(s => string.Equals(s.Name, "SchoolName", StringComparison.Ordinal)).Should().BeFalse();
+ }
+
+ // Person
+ ODataResource person =
+ entries.FirstOrDefault(s => string.Equals(s.TypeName, "NS.Person", StringComparison.Ordinal));
+ person.Should().NotBeNull();
+
+ // Verify that unknown property doesn't cause anomaly. And it is restored only when omit-values=nulls is specified.
+ if (bNullValuesOmitted)
+ {
+ person.Properties.Single(s => string.Equals(s.Name, "UnknownPropX", StringComparison.Ordinal)).Value
+ .Should().BeNull();
+ }
+ else
+ {
+ person.Properties.Any(s => string.Equals(s.Name, "UnknownPropX", StringComparison.Ordinal)).Should().BeFalse();
+ }
+
+ person.Properties.FirstOrDefault(s => string.Equals(s.Name, "Id", StringComparison.Ordinal))
+ .Value.Should().Be(0);
+
+ // null-able but not selected properties and not-null-able properties should not be restored.
+ person.Properties.Any(s => string.Equals(s.Name, "Height", StringComparison.Ordinal)).Should().BeFalse();
+ person.Properties.Any(s => string.Equals(s.Name, "Weight", StringComparison.Ordinal)).Should().BeFalse();
+ }
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void ReadingPropertyInTopLevelOrInComplexTypeShouldRestoreOmittedNullValuesWithSelectedPropertiesPartialSubTree(
+ bool bNullValuesOmitted)
+ {
+ EdmEntityType entityType;
+ EdmEntitySet entitySet;
+ EdmModel model = BuildEdmModelForOmittedNullValuesTestCases(out entityType, out entitySet);
+
+ // null-able property Height is not selected, thus should not be restored.
+ // null-able property Address is selected, thus should be restored.
+ // Property Education is null-able.
+ const string payloadWithSelectedPropertiesPartialSubTreeInQueryOption = @"{
+ ""@odata.context"":""http://www.example.com/$metadata#EntityNs.MyContainer.People(Id,%20Education/Id,%20Education/SchoolName,%20Education/UnknownPropX,%20Address)/$entity"",
+ ""@odata.id"":""http://mytest"",
+ ""Id"":0,
+ ""Education"":{""Id"":1}
+ }";
+
+ List entries = new List();
+ List nestedResourceInfos = new List();
+ this.ReadEntryPayload(model, payloadWithSelectedPropertiesPartialSubTreeInQueryOption, entitySet, entityType,
+ reader =>
+ {
+ switch (reader.State)
+ {
+ case ODataReaderState.ResourceStart:
+ entries.Add(reader.Item as ODataResource);
+ break;
+ case ODataReaderState.NestedResourceInfoStart:
+ nestedResourceInfos.Add(reader.Item as ODataNestedResourceInfo);
+ break;
+ default:
+ break;
+ }
+ },
+ nullValuesOmitted: bNullValuesOmitted);
+
+ entries.Count.Should().Be(2);
+ nestedResourceInfos.Count.Should().Be(1);
+ nestedResourceInfos.FirstOrDefault(s => string.Equals(s.Name, "Education", StringComparison.Ordinal))
+ .Should().NotBeNull();
+
+ // Education
+ ODataResource edu =
+ entries.FirstOrDefault(s => string.Equals(s.TypeName, "NS.Edu", StringComparison.Ordinal));
+ edu.Should().NotBeNull();
+
+ // Verify that unknown property doesn't cause anomaly. And it is restored only when omit-values=nulls is specified.
+ if (bNullValuesOmitted)
+ {
+ edu.Properties.Single(s => string.Equals(s.Name, "UnknownPropX", StringComparison.Ordinal)).Value
+ .Should().BeNull();
+ }
+ else
+ {
+ edu.Properties.Any(s => string.Equals(s.Name, "UnknownPropX", StringComparison.Ordinal))
+ .Should().BeFalse();
+ }
+
+ edu.Properties.FirstOrDefault(s => string.Equals(s.Name, "Id", StringComparison.Ordinal))
+ .Value.Should().Be(1);
+ if (bNullValuesOmitted)
+ {
+ // null-able property value should be restored.
+ edu.Properties.FirstOrDefault(s => string.Equals(s.Name, "SchoolName", StringComparison.Ordinal))
+ .Value.Should().BeNull();
+ }
+ else
+ {
+ edu.Properties.Any(s => string.Equals(s.Name, "SchoolName", StringComparison.Ordinal)).Should().BeFalse();
+ }
+
+ // not selected property should not be restored.
+ edu.Properties.Any(s => string.Equals(s.Name, "Campuses", StringComparison.Ordinal)).Should().BeFalse();
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void ReadingUnknownPropertyOfNonOpenTypeShouldWork(bool bNullValuesOmitted)
+ {
+ EdmEntityType entityType;
+ EdmEntitySet entitySet;
+ EdmModel model = BuildEdmModelForOmittedNullValuesTestCases(out entityType, out entitySet);
+
+ // null-able property Height is not selected, thus should not be restored.
+ // null-able property Address is selected, thus should be restored.
+ // Property Education is null-able.
+ const string payloadWithSelectedPropertiesPartialSubTreeInQueryOption = @"{
+ ""@odata.context"":""http://www.example.com/$metadata#EntityNs.MyContainer.People(Id,%20Education/Id,%20Education/SchoolName,%20Education/UnknownPropX,%20Address)/$entity"",
+ ""@odata.id"":""http://mytest"",
+ ""Id"":0,
+ ""Education"":{""Id"":1, ""UnknownPropX"": ""pX""}
+ }";
+
+ List entries = new List();
+ List nestedResourceInfos = new List();
+ this.ReadEntryPayload(model, payloadWithSelectedPropertiesPartialSubTreeInQueryOption, entitySet, entityType,
+ reader =>
+ {
+ switch (reader.State)
+ {
+ case ODataReaderState.ResourceStart:
+ entries.Add(reader.Item as ODataResource);
+ break;
+ case ODataReaderState.NestedResourceInfoStart:
+ nestedResourceInfos.Add(reader.Item as ODataNestedResourceInfo);
+ break;
+ default:
+ break;
+ }
+ },
+ nullValuesOmitted: bNullValuesOmitted);
+
+ // Education
+ ODataResource edu =
+ entries.FirstOrDefault(s => string.Equals(s.TypeName, "NS.Edu", StringComparison.Ordinal));
+ edu.Should().NotBeNull();
+
+ // Verify that unknown property on non-open type doesn't cause anomaly.
+ edu.Properties.Single(s => string.Equals(s.Name, "UnknownPropX", StringComparison.Ordinal)).Value
+ .Should().Equals("pX");
+ }
+
+ [Fact]
+ public void ReadingNormalDynamicCollectionPropertyInOpenStructuralTypeShouldWork()
+ {
+ EdmEntityType entityType;
+ EdmEntitySet entitySet;
+ EdmModel model = GetDynamicModel(out entityType, out entitySet);
+
+ const string payload =
+ "{" +
+ "\"@odata.context\":\"http://www.example.com/$metadata#EntityNs.MyContainer.People(Id,%20MyDynamic,%20Address/MyDynamic1)/$entity\"," +
+ "\"@odata.id\":\"http://mytest\"," +
+ "\"Id\":0," +
+ "\"Test@odata.type\":\"#Collection(Edm.String)\"," +
+ "\"Test\":null," +
+ "\"MyDynamic@odata.type\":\"#Collection(Edm.String)\"," +
+ "\"MyDynamic\":[\"mystr\"]," +
+ "\"Address\":{\"CountryRegion\":\"China\",\"Test1@odata.type\":\"#Collection(Edm.Int32)\",\"Test1\":null, \"MyDynamic1@odata.type\":\"#Collection(Edm.Int32)\",\"MyDynamic1\":[1]}" +
+ "}";
+
+ List entries = new List();
+ List nestedResourceInfos = new List();
+
+ this.ReadEntryPayload(model, payload, entitySet, entityType,
+ reader =>
+ {
+ switch (reader.State)
+ {
+ case ODataReaderState.ResourceStart:
+ entries.Add(reader.Item as ODataResource);
+ break;
+ case ODataReaderState.NestedResourceInfoStart:
+ nestedResourceInfos.Add(reader.Item as ODataNestedResourceInfo);
+ break;
+ default:
+ break;
+ }
+ }
+ );
+
+ entries.Count.Should().Be(2);
+ nestedResourceInfos.Count.Should().Be(1);
+
+ ODataResource person = entries.FirstOrDefault(s => string.Equals(s.TypeName, "NS.Person", StringComparison.Ordinal));
+ person.Should().NotBeNull();
+ person.Properties.FirstOrDefault(p => string.Equals(p.Name, "MyDynamic", StringComparison.Ordinal)).Value.Should().NotBeNull();
+
+ ODataResource address = entries.FirstOrDefault(s => string.Equals(s.TypeName, "NS.Address", StringComparison.Ordinal));
+ address.Should().NotBeNull();
+ address.Properties.FirstOrDefault(p => string.Equals(p.Name, "MyDynamic1", StringComparison.Ordinal))
+ .Value.Should().NotBeNull();
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void ReadingNullableDynamicCollectionPropertyInOpenStructuralTypeShouldWork(bool bNullValuesOmitted)
+ {
+ EdmEntityType entityType;
+ EdmEntitySet entitySet;
+ EdmModel model = GetDynamicModel(out entityType, out entitySet);
+
+ const string payload =
+ "{" +
+ "\"@odata.context\":\"http://www.example.com/$metadata#EntityNs.MyContainer.People(Id,%20MyDynamic,%20Address/MyDynamic1)/$entity\"," +
+ "\"@odata.id\":\"http://mytest\"," +
+ "\"Id\":0," +
+ "\"Test@odata.type\":\"#Collection(Edm.String)\"," +
+ "\"Test\":null," +
+ "\"Address\":{\"CountryRegion\":\"China\",\"Test1@odata.type\":\"#Collection(Edm.Int32)\",\"Test1\":null}" +
+ "}";
+
+ List entries = new List();
+ List nestedResourceInfos = new List();
+
+ this.ReadEntryPayload(model, payload, entitySet, entityType,
+ reader =>
+ {
+ switch (reader.State)
+ {
+ case ODataReaderState.ResourceStart:
+ entries.Add(reader.Item as ODataResource);
+ break;
+ case ODataReaderState.NestedResourceInfoStart:
+ nestedResourceInfos.Add(reader.Item as ODataNestedResourceInfo);
+ break;
+ default:
+ break;
+ }
+ },
+ nullValuesOmitted: bNullValuesOmitted);
+
+ entries.Count.Should().Be(2);
+ nestedResourceInfos.Count.Should().Be(1);
+
+ ODataResource person = entries.FirstOrDefault(s => string.Equals(s.TypeName, "NS.Person", StringComparison.Ordinal));
+ person.Should().NotBeNull();
+ if (bNullValuesOmitted)
+ {
+ person.Properties.Any(p => string.Equals(p.Name, "MyDynamic", StringComparison.Ordinal)).Should().BeTrue();
+ }
+ else
+ {
+ person.Properties.Any(p => string.Equals(p.Name, "MyDynamic", StringComparison.Ordinal)).Should().BeFalse();
+ }
+
+ ODataResource address =entries.FirstOrDefault(s => string.Equals(s.TypeName, "NS.Address", StringComparison.Ordinal));
+ address.Should().NotBeNull();
+ if (bNullValuesOmitted)
+ {
+ address.Properties.Any(p => string.Equals(p.Name, "MyDynamic1", StringComparison.Ordinal)).Should().BeTrue();
+ }
+ else
+ {
+ address.Properties.Any(p => string.Equals(p.Name, "MyDynamic1", StringComparison.Ordinal)).Should().BeFalse();
+ }
+ }
+
+ private EdmModel GetDynamicModel(out EdmEntityType entityType, out EdmEntitySet entitySet)
+ {
+ EdmModel model = new EdmModel();
+
+ EdmComplexType complexType = new EdmComplexType("NS", "Address", null, false, true/*isOpen*/);
+ complexType.AddStructuralProperty("CountryRegion", EdmPrimitiveTypeKind.String);
+ model.AddElement(complexType);
+
+ entityType = new EdmEntityType("NS", "Person", null, false, true/*isOpen*/);
+ entityType.AddKeys(entityType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32));
+ entityType.AddStructuralProperty("Address", new EdmComplexTypeReference(complexType, false));
+ model.AddElement(entityType);
+
+ EdmEntityContainer container = new EdmEntityContainer("EntityNs", "MyContainer");
+ entitySet = container.AddEntitySet("People", entityType);
+ model.AddElement(container);
+
+ return model;
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void ReadingPropertyInTopLevelOrInComplexTypeShouldRestoreOmittedNullValuesWithSelectedPropertiesPartialSubTreeAndBaseTypeProperty(
+ bool bNullValuesOmitted)
+ {
+ EdmEntityType entityType;
+ EdmEntitySet entitySet;
+ EdmModel model = BuildEdmModelForOmittedNullValuesTestCases(out entityType, out entitySet);
+
+ // null-able property Address is selected, thus should be restored.
+ // Property Education is null-able.
+ const string payloadWithWildcardInQueryOption = @"{
+ ""@odata.context"":""http://www.example.com/$metadata#EntityNs.MyContainer.People(Id,%20Education/Id,%20Education/OrgId,%20Address)/$entity"",
+ ""@odata.id"":""http://mytest"",
+ ""Id"":0,
+ ""Education"":{""Id"":1}
+ }";
+
+ List entries = new List();
+ List nestedResourceInfos = new List();
+ this.ReadEntryPayload(model, payloadWithWildcardInQueryOption, entitySet, entityType,
+ reader =>
+ {
+ switch (reader.State)
+ {
+ case ODataReaderState.ResourceStart:
+ entries.Add(reader.Item as ODataResource);
+ break;
+ case ODataReaderState.NestedResourceInfoStart:
+ nestedResourceInfos.Add(reader.Item as ODataNestedResourceInfo);
+ break;
+ default:
+ break;
+ }
+ },
+ nullValuesOmitted: bNullValuesOmitted);
+
+ entries.Count.Should().Be(2);
+ nestedResourceInfos.Count.Should().Be(1);
+ nestedResourceInfos.FirstOrDefault(s => string.Equals(s.Name, "Education", StringComparison.Ordinal))
+ .Should().NotBeNull();
+
+ // Education
+ ODataResource edu =
+ entries.FirstOrDefault(s => string.Equals(s.TypeName, "NS.Edu", StringComparison.Ordinal));
+ edu.Should().NotBeNull();
+ edu.Properties.FirstOrDefault(s => string.Equals(s.Name, "Id", StringComparison.Ordinal))
+ .Value.Should().Be(1);
+
+ // selected null-able property from base type should be restored to null if omitted.
+ if (bNullValuesOmitted)
+ {
+ edu.Properties.FirstOrDefault(s => string.Equals(s.Name, "OrgId", StringComparison.Ordinal))
+ .Value.Should().BeNull();
+ }
+ else
+ {
+ edu.Properties.Any(s => string.Equals(s.Name, "OrgId", StringComparison.Ordinal)).Should().BeFalse();
+ }
+
+ // not selected property should not be restored.
+ edu.Properties.Any(s => string.Equals(s.Name, "OrgCategory", StringComparison.Ordinal)).Should().BeFalse();
+ edu.Properties.Any(s => string.Equals(s.Name, "SchoolName", StringComparison.Ordinal)).Should().BeFalse();
+ edu.Properties.Any(s => string.Equals(s.Name, "Campuses", StringComparison.Ordinal)).Should().BeFalse();
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void ReadingWithSelectExpandClauseShouldRestoreOmittedNullValuesWithSelectedPropertiesPartialSubTree(
+ bool bNullValuesOmitted)
+ {
+ EdmEntityType entityType;
+ EdmEntitySet entitySet;
+ EdmModel model = BuildEdmModelForOmittedNullValuesTestCases(out entityType, out entitySet);
+
+ // null-able property Height is not selected, thus should not be restored.
+ // Property Education is null-able.
+ const string payloadWithSelectExpandClauseInQueryOption = @"{
+ ""@odata.context"":""http://www.example.com/$metadata#EntityNs.MyContainer.People(Id,Education/Id,Education/SchoolName,Company/Id,Company/Name)/$entity"",
+ ""@odata.id"":""http://www.example.com/$metadata#EntityNs.MyContainer.People(0)"",
+ ""Id"":0,
+ ""Education"":{""Id"":1},
+ ""Company"":{
+ ""@odata.id"":""http://www.example.com/$metadata#EntityNs.MyContainer.Companies(11)"",
+ ""Id"":11}
+ }";
+
+ List entries = new List();
+ List nestedResourceInfos = new List();
+ this.ReadEntryPayload(model, payloadWithSelectExpandClauseInQueryOption, entitySet, entityType,
+ reader =>
+ {
+ switch (reader.State)
+ {
+ case ODataReaderState.ResourceStart:
+ entries.Add(reader.Item as ODataResource);
+ break;
+ case ODataReaderState.NestedResourceInfoStart:
+ nestedResourceInfos.Add(reader.Item as ODataNestedResourceInfo);
+ break;
+ default:
+ break;
+ }
+ },
+ nullValuesOmitted: bNullValuesOmitted);
+
+ nestedResourceInfos.Count.Should().Be(2);
+ ODataNestedResourceInfo eduNestedResouce =
+ nestedResourceInfos.FirstOrDefault(s => string.Equals(s.Name, "Education", StringComparison.Ordinal));
+
+ // Edu is complex type, its Url should be null.
+ eduNestedResouce.Should().NotBeNull();
+ eduNestedResouce.Url.Should().BeNull();
+ // Navigation link for Company entity should be non-null.
+ ODataNestedResourceInfo companyLink =
+ nestedResourceInfos.FirstOrDefault(s => string.Equals(s.Name, "Company", StringComparison.Ordinal));
+ companyLink.Should().NotBeNull();
+ companyLink.Url.Should().NotBeNull();
+
+ entries.Count.Should().Be(3);
+ // Education
+ ODataResource edu =
+ entries.FirstOrDefault(s => string.Equals(s.TypeName, "NS.Edu", StringComparison.Ordinal));
+ edu.Should().NotBeNull();
+
+ edu.Properties.FirstOrDefault(s => string.Equals(s.Name, "Id", StringComparison.Ordinal))
+ .Value.Should().Be(1);
+ if (bNullValuesOmitted)
+ {
+ // null-able property value should be restored.
+ edu.Properties.FirstOrDefault(s => string.Equals(s.Name, "SchoolName", StringComparison.Ordinal))
+ .Value.Should().BeNull();
+ }
+ else
+ {
+ edu.Properties.Any(s => string.Equals(s.Name, "SchoolName", StringComparison.Ordinal)).Should().BeFalse();
+ }
+ // not selected property should not be restored.
+ edu.Properties.Any(s => string.Equals(s.Name, "Campuses", StringComparison.Ordinal)).Should().BeFalse();
+
+ // Company
+ ODataResource company =
+ entries.FirstOrDefault(s => string.Equals(s.TypeName, "NS.Cmpny", StringComparison.Ordinal));
+ company.Should().NotBeNull();
+ company.Properties.FirstOrDefault(s => string.Equals(s.Name, "Id", StringComparison.Ordinal))
+ .Value.Should().Be(11);
+ if (bNullValuesOmitted)
+ {
+ company.Properties.FirstOrDefault(s => string.Equals(s.Name, "Name", StringComparison.Ordinal))
+ .Value.Should().BeNull();
+ }
+ else
+ {
+ company.Properties.Any(s => string.Equals(s.Name, "Name", StringComparison.Ordinal)).Should().BeFalse();
+ }
+
+ // Person
+ ODataResource person =
+ entries.FirstOrDefault(s => string.Equals(s.TypeName, "NS.Person", StringComparison.Ordinal));
+ person.Should().NotBeNull();
+ person.Properties.FirstOrDefault(s => string.Equals(s.Name, "Id", StringComparison.Ordinal))
+ .Value.Should().Be(0);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void ReadingWithSelectExpandClauseShouldHaveNoContentForNullNavigationProperty(
+ bool bNullValuesOmitted)
+ {
+ EdmEntityType entityType;
+ EdmEntitySet entitySet;
+ EdmModel model = BuildEdmModelForOmittedNullValuesTestCases(out entityType, out entitySet);
+
+ // Context URL with subtree of navigation entity selected.
+ const string payloadWithSelectExpandClauseInQueryOption1 = @"{
+ ""@odata.context"":""http://www.example.com/$metadata#EntityNs.MyContainer.People(Id,Education/Id,Education/SchoolName,Company/Id,Company/Name)/$entity"",
+ ""@odata.id"":""http://www.example.com/$metadata#EntityNs.MyContainer.People(0)"",
+ ""Id"":0,
+ ""Education"":{""Id"":1}
+ }";
+
+ // Context URL with entire navigation entity selected.
+ const string payloadWithSelectExpandClauseInQueryOption2 = @"{
+ ""@odata.context"":""http://www.example.com/$metadata#EntityNs.MyContainer.People(Id,Education/Id,Education/SchoolName,Company)/$entity"",
+ ""@odata.id"":""http://www.example.com/$metadata#EntityNs.MyContainer.People(0)"",
+ ""Id"":0,
+ ""Education"":{""Id"":1}
+ }";
+
+ foreach (string payload in new string[]
+ {
+ payloadWithSelectExpandClauseInQueryOption1,
+ payloadWithSelectExpandClauseInQueryOption2
+ })
+ {
+ List entries = new List();
+ List nestedResourceInfos = new List();
+ this.ReadEntryPayload(model, payload, entitySet, entityType,
+ reader =>
+ {
+ switch (reader.State)
+ {
+ case ODataReaderState.ResourceStart:
+ entries.Add(reader.Item as ODataResource);
+ break;
+ case ODataReaderState.NestedResourceInfoStart:
+ nestedResourceInfos.Add(reader.Item as ODataNestedResourceInfo);
+ break;
+ default:
+ break;
+ }
+ },
+ nullValuesOmitted: bNullValuesOmitted);
+
+ entries.Count.Should().Be(2);
+ // Expanded Company entity via null navigation link is not restored.
+ // see example: http://services.odata.org/V4/TripPinService/People('ronaldmundy')?$expand=Photo
+ entries.Any(s => string.Equals(s.TypeName, "NS.Cmpny", StringComparison.Ordinal)).Should().BeFalse();
+
+ nestedResourceInfos.Count.Should().Be(2);
+ // There should be a navigation link reported as missing from payload.
+ ODataNestedResourceInfo companyLink =
+ nestedResourceInfos.FirstOrDefault(s => string.Equals(s.Name, "Company", StringComparison.Ordinal));
+ companyLink.Should().NotBeNull();
+ // Reported missing/un-processed navigation link has URL constructed from metadata and
+ // @odata.id annotation of navigation source.
+ companyLink.Url.Should().NotBeNull();
+ }
+ }
+
[Fact]
public void ReadingNullValueForDynamicCollectionPropertyInOpenStructuralTypeShouldWork()
{
@@ -840,13 +1615,23 @@ public void ReadDateTimeOffsetWithCustomFormat()
birthday.Value.Should().Be(new DateTimeOffset(2012, 4, 12, 18, 43, 10, TimeSpan.Zero));
}
- private void ReadEntryPayload(IEdmModel userModel, string payload, EdmEntitySet entitySet, IEdmEntityType entityType, Action action, bool isIeee754Compatible = true, IServiceProvider container = null)
+ private void ReadEntryPayload(IEdmModel userModel, string payload, EdmEntitySet entitySet, IEdmEntityType entityType,
+ Action action,
+ bool isIeee754Compatible = true,
+ IServiceProvider container = null,
+ bool nullValuesOmitted = false)
{
var message = new InMemoryMessage() { Stream = new MemoryStream(Encoding.UTF8.GetBytes(payload)), Container = container};
string contentType = isIeee754Compatible
? "application/json;odata.metadata=minimal;IEEE754Compatible=true"
: "application/json;odata.metadata=minimal;IEEE754Compatible=false";
message.SetHeader("Content-Type", contentType);
+
+ if (nullValuesOmitted)
+ {
+ message.SetHeader("Preference-Applied", omitValuesNulls);
+ }
+
var readerSettings = new ODataMessageReaderSettings { EnableMessageStreamDisposal = true };
readerSettings.Validations &= ~ValidationKinds.ThrowOnUndeclaredPropertyForNonOpenType;
using (var msgReader = new ODataMessageReader((IODataResponseMessage)message, readerSettings, userModel))
@@ -858,5 +1643,88 @@ private void ReadEntryPayload(IEdmModel userModel, string payload, EdmEntitySet
}
}
}
+
+ private EdmModel BuildEdmModelForOmittedNullValuesTestCases(out EdmEntityType entityType, out EdmEntitySet entitySet)
+ {
+ EdmModel model = new EdmModel();
+
+ entityType = new EdmEntityType("NS", "Person");
+ entityType.AddKeys(entityType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32));
+
+ EdmTypeDefinition weightType = new EdmTypeDefinition("NS", "Wgt", EdmPrimitiveTypeKind.Double);
+ EdmTypeDefinitionReference weightTypeRef = new EdmTypeDefinitionReference(weightType, false);
+ entityType.AddStructuralProperty("Weight", weightTypeRef);
+
+ EdmTypeDefinition heightType = new EdmTypeDefinition("NS", "Ht", EdmPrimitiveTypeKind.Double);
+ EdmTypeDefinitionReference heightTypeRef = new EdmTypeDefinitionReference(heightType, true);
+ entityType.AddStructuralProperty("Height", heightTypeRef);
+
+ EdmComplexType addressType = new EdmComplexType("NS", "OpnAddr");
+ EdmComplexTypeReference addressTypeRef = new EdmComplexTypeReference(addressType, true);
+
+ EdmTypeDefinition countryRegionType = new EdmTypeDefinition("NS", "CR", EdmPrimitiveTypeKind.String);
+ EdmTypeDefinitionReference countryRegionTypeRef = new EdmTypeDefinitionReference(countryRegionType, false);
+ addressType.AddStructuralProperty("CountryRegion", countryRegionTypeRef);
+
+ // Address/City
+ EdmTypeDefinition cityType = new EdmTypeDefinition("NS", "Cty", EdmPrimitiveTypeKind.String);
+ EdmTypeDefinitionReference cityTypeRef = new EdmTypeDefinitionReference(cityType, false);
+ addressType.AddStructuralProperty("City", cityTypeRef);
+
+ EdmTypeDefinition zipCodeType = new EdmTypeDefinition("NS", "ZC", EdmPrimitiveTypeKind.String);
+ EdmTypeDefinitionReference zipCodeTypeRef = new EdmTypeDefinitionReference(zipCodeType, true);
+ addressType.AddStructuralProperty("ZipCode", zipCodeTypeRef);
+
+ entityType.AddStructuralProperty("Address", addressTypeRef);
+
+ // Education
+ EdmComplexType organizationType = new EdmComplexType("NS", "Org");
+ organizationType.AddStructuralProperty("OrgId", EdmPrimitiveTypeKind.Int32);
+ organizationType.AddStructuralProperty("OrgCategory", EdmPrimitiveTypeKind.String);
+
+ EdmComplexType educationType = new EdmComplexType("NS", "Edu", organizationType);
+ EdmComplexTypeReference educationTypeRef = new EdmComplexTypeReference(educationType, true);
+
+ // top level null-able complex type properties
+ educationType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32); // same property name but inside different type.
+ educationType.AddStructuralProperty("Campuses",
+ new EdmCollectionTypeReference(new EdmCollectionType(EdmCoreModel.Instance.GetString(true))));
+ educationType.AddStructuralProperty("SchoolName", EdmPrimitiveTypeKind.String);
+
+ entityType.AddStructuralProperty("Education", educationTypeRef);
+
+ // Company entity type.
+ EdmEntityType companyEntityType = new EdmEntityType("NS", "Cmpny");
+ entityType.AddKeys(companyEntityType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32));
+ companyEntityType.AddStructuralProperty("Name",
+ new EdmTypeDefinitionReference(
+ new EdmTypeDefinition("NS", "CmpyName", EdmPrimitiveTypeKind.String),
+ true));
+ companyEntityType.AddStructuralProperty("Address", addressTypeRef);
+
+ EdmNavigationPropertyInfo companyNav = new EdmNavigationPropertyInfo()
+ {
+ Name = "Company",
+ ContainsTarget = true,
+ Target = companyEntityType,
+ TargetMultiplicity = EdmMultiplicity.ZeroOrOne
+ };
+
+ // Add navigation link from Person to Company.
+ entityType.AddUnidirectionalNavigation(companyNav);
+
+ model.AddElement(weightType);
+ model.AddElement(heightType);
+ model.AddElement(addressType);
+ model.AddElement(entityType);
+ model.AddElement(companyEntityType);
+
+ EdmEntityContainer container = new EdmEntityContainer("EntityNs", "MyContainer");
+ entitySet = container.AddEntitySet("People", entityType);
+ container.AddEntitySet("Companies", companyEntityType);
+ model.AddElement(container);
+
+ return model;
+ }
}
}
diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/JsonLight/ODataJsonLightDeltaWriterTests.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/JsonLight/ODataJsonLightDeltaWriterTests.cs
index 89839a45db..3235db68d7 100644
--- a/test/FunctionalTests/Microsoft.OData.Core.Tests/JsonLight/ODataJsonLightDeltaWriterTests.cs
+++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/JsonLight/ODataJsonLightDeltaWriterTests.cs
@@ -60,6 +60,22 @@ public class ODataJsonLightDeltaWriterTests
}
};
+ private readonly ODataResource customerUpdatedWithNullValues = new ODataResource
+ {
+ Id = new Uri("Customers('BOTTM')", UriKind.Relative),
+ Properties = new List
+ {
+ new ODataProperty { Name = "ContactName", Value = null },
+ },
+ TypeName = "MyNS.Customer",
+ SerializationInfo = new ODataResourceSerializationInfo
+ {
+ NavigationSourceEntityTypeName = "MyNS.Customer",
+ NavigationSourceKind = EdmNavigationSourceKind.EntitySet,
+ NavigationSourceName = "Customers"
+ }
+ };
+
private readonly ODataDeltaDeletedLink linkToOrder10643 = new ODataDeltaDeletedLink(new Uri("Customers('ALFKI')", UriKind.Relative), new Uri("Orders('10643')", UriKind.Relative), "Orders");
private readonly ODataDeltaLink linkToOrder10645 = new ODataDeltaLink(new Uri("Customers('BOTTM')", UriKind.Relative), new Uri("Orders('10645')", UriKind.Relative), "Orders");
@@ -1356,7 +1372,7 @@ public void WriteNestedDeletedEntryInDeletedEntry()
new ODataDeletedResource()
{
Reason = DeltaDeletedEntryReason.Deleted,
- Properties = new ODataProperty[]
+ Properties = new ODataProperty[]
{
new ODataProperty() { Name = "Name", Value = "Scissors" },
new ODataProperty() { Name = "Id", Value = 1 }
@@ -2031,7 +2047,7 @@ public void WriteTopLevelDeletedEntityFromDifferentSet()
this.TestPayload().Should().Be("{\"@context\":\"http://host/service/$metadata#Customers/$delta\",\"@count\":5,\"@deltaLink\":\"Customers?$expand=Orders&$deltatoken=8015\",\"value\":[{\"@id\":\"Customers('BOTTM')\",\"ContactName\":\"Susan Halvenstern\"},{\"@context\":\"http://host/service/$metadata#Orders/$deletedEntity\",\"@removed\":{\"reason\":\"deleted\"},\"@id\":\"Orders(10643)\"}]}");
}
-
+
[Fact]
public void WriteTopLevelDeletedEntityFromDifferentSetWithoutInfo()
{
@@ -2049,6 +2065,155 @@ public void WriteTopLevelDeletedEntityFromDifferentSetWithoutInfo()
this.TestPayload().Should().Be("{\"@context\":\"http://host/service/$metadata#Customers/$delta\",\"@count\":5,\"@deltaLink\":\"Customers?$expand=Orders&$deltatoken=8015\",\"value\":[{\"@id\":\"Customers('BOTTM')\",\"ContactName\":\"Susan Halvenstern\"},{\"@context\":\"http://host/service/$metadata#Orders/$deletedEntity\",\"@removed\":{\"reason\":\"deleted\"},\"@id\":\"Orders(10643)\"}]}");
}
+ ///
+ /// When using regular OData writer with required writingDelta = true
to write
+ /// containing , property value of null
+ /// should always be written in response (regardless of omitNullValues preference setting).
+ ///
+ [Fact]
+ public void WriteTopLevelEntityWithNullPropertyValues()
+ {
+ this.TestInit(this.GetModel());
+
+
+ foreach (bool omitNullValues in new bool[] {true, false})
+ {
+ using (this.stream = new MemoryStream())
+ {
+ // Create resource set writer with specified omitNullValues setting
+ ODataJsonLightWriter writer = new ODataJsonLightWriter(
+ CreateJsonLightOutputContext(this.stream, this.GetModel(), omitNullValues, false, null, ODataVersion.V401),
+ this.GetCustomers(),
+ this.GetCustomerType(),
+ true,
+ false,
+ /*writingDelta*/ true);
+ writer.WriteStart(feedWithoutInfo);
+ writer.WriteStart(customerUpdatedWithNullValues);
+ writer.WriteEnd(); // customer
+ writer.WriteEnd(); // delta resource set
+ writer.Flush();
+
+ this.TestPayload().Should().Be(
+ "{\"@context\":\"http://host/service/$metadata#Customers/$delta\",\"@count\":5,\"@deltaLink\":\"Customers?$expand=Orders&$deltatoken=8015\",\"value\":[{\"@id\":\"Customers('BOTTM')\",\"ContactName\":null}]}");
+ }
+ }
+ }
+
+ ///
+ /// When using with required writingDelta = true
to write
+ /// containing , property value of null
+ /// should always be written in response (regardless of omitNullValues preference setting).
+ ///
+ [Fact]
+ public void WriteEntityWithNullPropertyValueInDeltaFeed()
+ {
+ this.TestInit(this.GetModel());
+
+ ODataDeltaResourceSet feed = new ODataDeltaResourceSet();
+
+ ODataResource orderEntry = new ODataResource()
+ {
+ SerializationInfo = new ODataResourceSerializationInfo
+ {
+ NavigationSourceEntityTypeName = "MyNS.Order",
+ NavigationSourceKind = EdmNavigationSourceKind.EntitySet,
+ NavigationSourceName = "Orders"
+ },
+ Properties = new ODataProperty[]
+ {
+ new ODataProperty() { Name = "Id", Value = 1 }
+ },
+ };
+
+ ODataNestedResourceInfo shippingAddressInfo = new ODataNestedResourceInfo
+ {
+ Name = "ShippingAddress",
+ IsCollection = false
+ };
+
+ ODataResource shippingAddress = new ODataResource
+ {
+ Properties = new List
+ {
+ new ODataProperty { Name = "City", Value = "Shanghai" },
+ new ODataProperty { Name = "PostalCode", Value = null }
+ }
+ };
+
+ var result = new ODataQueryOptionParser(this.GetModel(), this.GetCustomerType(), this.GetCustomers(), new Dictionary { { "$expand", "Orders($select=ShippingAddress)" }}).ParseSelectAndExpand();
+
+ ODataUri odataUri = new ODataUri()
+ {
+ ServiceRoot = new Uri("http://host/service"),
+ SelectAndExpand = result
+ };
+
+ foreach (bool omitNullValues in new bool[] {true, false})
+ {
+ using (this.stream = new MemoryStream())
+ {
+ var outputContext = CreateJsonLightOutputContext(this.stream, this.GetModel(), omitNullValues,
+ false, odataUri, ODataVersion.V401);
+ ODataJsonLightDeltaWriter writer = new ODataJsonLightDeltaWriter(outputContext, this.GetProducts(),
+ this.GetProductType());
+ writer.WriteStart(feed);
+ writer.WriteStart(orderEntry);
+ writer.WriteStart(shippingAddressInfo);
+ writer.WriteStart(shippingAddress);
+ writer.WriteEnd(); // shippingAddress
+ writer.WriteEnd(); // shippingAddressInfo
+ writer.WriteEnd(); // Order
+ writer.WriteEnd(); // Feed
+ writer.Flush();
+
+ this.TestPayload().Should().Be(
+ "{\"@context\":\"http://host/service/$metadata#Products(Orders(ShippingAddress))/$delta\",\"value\":[{\"@context\":\"http://host/service/$metadata#Orders/$entity\",\"Id\":1,\"ShippingAddress\":{\"City\":\"Shanghai\",\"PostalCode\":null}}]}");
+ }
+ }
+ }
+
+ ///
+ /// When using regular OData writer with required writingDelta = true
to write ,
+ /// property value of null should always be written in response (regardless of omitNullValues preference setting).
+ ///
+ [Fact]
+ public void WriteDeletedResourceWithPropertyValueNull()
+ {
+ this.TestInit(this.GetModel());
+ ODataDeletedResource deletedEntity = new ODataDeletedResource()
+ {
+ Reason = DeltaDeletedEntryReason.Changed,
+ TypeName = "MyNS.PhysicalProduct",
+ Properties = new[]
+ {
+ new ODataProperty {Name = "Id", Value = new ODataPrimitiveValue(1)},
+ new ODataProperty {Name = "Name", Value = new ODataPrimitiveValue("car")},
+ new ODataProperty {Name = "Material", Value = null}
+ }
+ };
+
+ foreach (bool omitNullValues in new bool[] {true, false})
+ {
+ using (this.stream = new MemoryStream())
+ {
+ ODataDeltaResourceSet feed = new ODataDeltaResourceSet();
+ ODataJsonLightWriter writer = new ODataJsonLightWriter(
+ CreateJsonLightOutputContext(this.stream, this.GetModel(), omitNullValues, false, null,
+ ODataVersion.V401),
+ this.GetProducts(), this.GetProductType(), true, false, /*writingDelta*/true);
+ writer.WriteStart(feed);
+ writer.WriteStart(deletedEntity);
+ writer.WriteEnd();
+ writer.WriteEnd();
+ writer.Flush();
+
+ this.TestPayload().Should().Be(
+ "{\"@context\":\"http://host/service/$metadata#Products/$delta\",\"value\":[{\"@removed\":{\"reason\":\"changed\"},\"@type\":\"#MyNS.PhysicalProduct\",\"Id\":1,\"Name\":\"car\",\"Material\":null}]}");
+ }
+ }
+ }
+
[Fact]
public void WriteEntityFromDifferentSetToEntitySetShouldFail()
{
@@ -2423,9 +2588,20 @@ private string TestPayload()
return (new StreamReader(stream)).ReadToEnd();
}
- private static ODataJsonLightOutputContext CreateJsonLightOutputContext(MemoryStream stream, IEdmModel userModel, bool fullMetadata = false, ODataUri uri = null, ODataVersion version = ODataVersion.V4)
+ private static ODataJsonLightOutputContext CreateJsonLightOutputContext(MemoryStream stream, IEdmModel userModel,
+ bool fullMetadata = false, ODataUri uri = null, ODataVersion version = ODataVersion.V4)
{
- var settings = new ODataMessageWriterSettings { Version = version, ShouldIncludeAnnotation = ODataUtils.CreateAnnotationFilter("*") };
+ return CreateJsonLightOutputContext(stream, userModel, /*omitNullValues*/ false, fullMetadata, uri, version);
+ }
+
+ private static ODataJsonLightOutputContext CreateJsonLightOutputContext(MemoryStream stream, IEdmModel userModel, bool omitNullValues, bool fullMetadata = false, ODataUri uri = null, ODataVersion version = ODataVersion.V4)
+ {
+ var settings = new ODataMessageWriterSettings
+ {
+ Version = version,
+ ShouldIncludeAnnotation = ODataUtils.CreateAnnotationFilter("*"),
+ OmitNullValues = omitNullValues
+ };
settings.SetServiceDocumentUri(new Uri("http://host/service"));
if (uri != null)
{
diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/JsonLight/ODataJsonLightDeserializerTests.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/JsonLight/ODataJsonLightDeserializerTests.cs
index b99f2cf566..f8947eed50 100644
--- a/test/FunctionalTests/Microsoft.OData.Core.Tests/JsonLight/ODataJsonLightDeserializerTests.cs
+++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/JsonLight/ODataJsonLightDeserializerTests.cs
@@ -13,6 +13,7 @@
using Microsoft.OData.Json;
using Microsoft.OData.JsonLight;
using Microsoft.OData.Edm;
+using Microsoft.Test.OData.Utils.ODataLibTest;
using Xunit;
using ErrorStrings = Microsoft.OData.Strings;
@@ -867,6 +868,58 @@ public void ParsingInstanceAnnotationsInTopLevelPropertyShouldSkipBaseOnSettings
property.InstanceAnnotations.Count.Should().Be(0);
}
+ [Fact]
+ public void ParsingPrimitiveTypePropertyInsideComplextPropertyShouldSucceed()
+ {
+ EdmModel model = this.CreateEdmModelWithEntity();
+
+ // Add complex type to model.
+ EdmComplexType complexType = new EdmComplexType("TestNamespace", "Addr");
+ complexType.AddStructuralProperty("CountryRegion", EdmPrimitiveTypeKind.String);
+ IEdmComplexTypeReference complexTypeReference = new EdmComplexTypeReference(complexType, true);
+
+ EdmEntityType entityType = model.GetEntityType("TestNamespace.Customer");
+ entityType.AddStructuralProperty("Address", complexTypeReference);
+
+ IEdmTypeReference primitiveTypeRef =
+ ((IEdmComplexTypeReference) (entityType.GetProperty("Address").Type)).FindProperty(
+ "CountryRegion").Type;
+
+ string payload = @"{
+ ""@odata.context"":""http://odata.org/test/$metadata#Customers(1)/Address/CountryRegion"",
+ ""value"":""US""
+ }";
+
+ ODataJsonLightPropertyAndValueDeserializer deserializer = new ODataJsonLightPropertyAndValueDeserializer(this.CreateJsonLightInputContext(payload, model));
+ ODataProperty property = deserializer.ReadTopLevelProperty(primitiveTypeRef);
+ property.Value.Should().Be("US");
+ }
+
+ [Fact]
+ public void ParsingTopLevelComplextPropertyShouldFail()
+ {
+ EdmModel model = this.CreateEdmModelWithEntity();
+
+ // Add complex type to model.
+ EdmComplexType complexType = new EdmComplexType("TestNamespace", "Addr");
+ complexType.AddStructuralProperty("CountryRegion", EdmPrimitiveTypeKind.String);
+ IEdmComplexTypeReference complexTypeReference = new EdmComplexTypeReference(complexType, true);
+
+ EdmEntityType entityType = model.GetEntityType("TestNamespace.Customer");
+ entityType.AddStructuralProperty("Address", complexTypeReference);
+
+ string payload = @"{
+ ""@odata.context"":""http://odata.org/test/$metadata#Customers(1)/Address"",
+ ""value"":{""CountryRegion"":""US""}
+ }";
+
+ ODataJsonLightPropertyAndValueDeserializer deserializer = new ODataJsonLightPropertyAndValueDeserializer(this.CreateJsonLightInputContext(payload, model));
+
+ // Currently using de-serializer to read top-level complex property directly is not supported. Need to use ODataReader.Read API instead.
+ Action action = () => deserializer.ReadTopLevelProperty(complexTypeReference);
+ action.ShouldThrow().WithMessage(
+ "An internal error 'ODataJsonLightPropertyAndValueDeserializer_ReadPropertyValue' occurred.");
+ }
#endregion
#region Complex properties instance annotation
@@ -1078,7 +1131,7 @@ public void ParsingExpectedComplexPropertyActualNotShouldThrow()
var complexTypeRef = new EdmComplexTypeReference(complexType, false);
var odataReader = this.CreateJsonLightInputContext("\"CountryRegion\":\"China\"", model, false)
.CreateResourceReader(null, complexType);
- Action action = () =>
+ Action action = () =>
{
while (odataReader.Read())
{
diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/JsonLight/ODataJsonLightPropertySerializerTests.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/JsonLight/ODataJsonLightPropertySerializerTests.cs
index a073b587ea..3f04d502d3 100644
--- a/test/FunctionalTests/Microsoft.OData.Core.Tests/JsonLight/ODataJsonLightPropertySerializerTests.cs
+++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/JsonLight/ODataJsonLightPropertySerializerTests.cs
@@ -313,6 +313,7 @@ private string SerializeProperty(IEdmStructuredType owningType, ODataProperty od
owningType,
new[] { odataProperty },
/*isComplexValue*/ false,
+ /*omitNullValues*/ false,
new NullDuplicatePropertyNameChecker());
jsonLightOutputContext.JsonWriter.EndObjectScope();
diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/Writer/JsonLight/ODataJsonLightPropertyTypeSerializerTests.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/Writer/JsonLight/ODataJsonLightPropertyTypeSerializerTests.cs
index cd66510db4..d840304e18 100644
--- a/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/Writer/JsonLight/ODataJsonLightPropertyTypeSerializerTests.cs
+++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/Writer/JsonLight/ODataJsonLightPropertyTypeSerializerTests.cs
@@ -445,6 +445,7 @@ private string SerializeProperty(ODataProperty odataProperty, ODataVersion versi
this.entityType,
new[] { odataProperty },
/*isComplexValue*/ false,
+ /*omitNullValues*/ false,
new NullDuplicatePropertyNameChecker());
jsonLightOutputContext.JsonWriter.EndObjectScope();
diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/SelectedPropertiesNodeTests.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/SelectedPropertiesNodeTests.cs
index f8e89cba4b..fc903e1264 100644
--- a/test/FunctionalTests/Microsoft.OData.Core.Tests/SelectedPropertiesNodeTests.cs
+++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/SelectedPropertiesNodeTests.cs
@@ -113,6 +113,15 @@ public void WildcardShouldSelectAllPropertiesParsingTest()
SelectedPropertiesNode.Create("*").Should().HaveStreams(this.cityType, "Photo").And.HaveNavigations(this.cityType, "Districts").And.HaveChild(this.cityType, "Districts", c => c.Should().BeSameAsEmpty());
}
+ [Fact]
+ public void WildcardShouldSelectAllProperties()
+ {
+ SelectedPropertiesNode.Create("*").Should()
+ .HaveProperties(this.cityType, "Id", "Name", "Size", "Photo", "Districts")
+ .And.HaveNavigations(this.cityType, "Districts")
+ .And.HaveChild(this.cityType, "Districts", c => c.Should().BeSameAsEmpty());
+ }
+
[Fact]
public void SingleStreamPropertyWithNormalProperty()
{
@@ -141,6 +150,12 @@ public void SpecifyingTheSameNavigationTwiceShouldNotCauseDuplicates()
public void SpecifyingAWildCardShouldNotCauseDuplicates()
{
SelectedPropertiesNode.Create("Districts,*,Photo").Should().HaveStreams(this.cityType, "Photo").And.HaveNavigations(this.cityType, "Districts").And.HaveChild(this.cityType, "Districts", c => c.Should().HaveEntireSubtree());
+
+ SelectedPropertiesNode.Create("Districts,*,Photo").Should()
+ .HaveStreams(this.cityType, "Photo")
+ .And.HaveProperties(this.cityType, "Id", "Name", "Size", "Photo", "Districts")
+ .And.HaveNavigations(this.cityType, "Districts")
+ .And.HaveChild(this.cityType, "Districts", c => c.Should().HaveEntireSubtree());
}
[Fact]
@@ -166,6 +181,7 @@ public void DeepAndWideSelection1()
// 2) 'Districts/*' should not override 'Districts'
SelectedPropertiesNode.Create("*,Districts,Districts/*").Should()
.HaveStreams(this.cityType, "Photo")
+ .And.HaveProperties(this.cityType, "Id", "Name", "Size", "Photo", "Districts")
.And.HaveNavigations(this.cityType, "Districts")
.And.HaveChild(this.cityType, "Districts", c => c.Should().HaveEntireSubtree());
}
@@ -180,6 +196,7 @@ public void DeepAndWideSelection2()
"Districts",
c => c.Should()
.HaveStreams(this.districtType, "Thumbnail")
+ .And.HaveProperties(this.districtType, "Id", "Zip", "Thumbnail", "City")
.And.HaveChild(this.districtType, "City", c2 => c2.Should().HaveEntireSubtree()));
}
@@ -193,6 +210,7 @@ public void WildcardAfterNavigationShouldNotSelectTheEntireTree()
"Districts",
c => c.Should()
.HaveStreams(this.districtType, "Thumbnail")
+ .And.HaveProperties(this.districtType, "Id", "Zip", "Thumbnail", "City")
.And.HaveNavigations(this.districtType, "City"));
}
@@ -332,7 +350,10 @@ public void CombiningPropertyWithWildcardShouldBeWildcard()
{
SelectedPropertiesNode left = SelectedPropertiesNode.Create("*");
SelectedPropertiesNode right = SelectedPropertiesNode.Create("Fake");
- this.VerifyCombination(left, right, n => n.Should().HaveStreams(this.cityType, "Photo").And.HaveNavigations(this.cityType, "Districts"));
+ this.VerifyCombination(left, right,
+ n => n.Should().HaveStreams(this.cityType, "Photo")
+ .And.HaveProperties(this.cityType, "Id", "Name", "Size", "Photo", "Districts")
+ .And.HaveNavigations(this.cityType, "Districts"));
}
[Fact]
@@ -343,6 +364,7 @@ public void CombiningPartialNodesShouldCombineProperties()
Action verify = n => n.Should()
.HaveStreams(this.cityType, "Photo")
+ .And.HaveProperties(this.cityType, "Photo")
.And.HaveNavigations(this.cityType, "Districts")
.And.HaveChild(this.cityType, "Districts", c => c.Should().HaveOnlyStreams(this.districtType, "Thumbnail"));
@@ -362,6 +384,7 @@ public void CombiningDeepPartialNodesShouldCombineRecursively()
"Districts",
c => c.Should()
.HaveStreams(this.districtType, "Thumbnail")
+ .And.HaveProperties(this.districtType, "Thumbnail")
.And.HaveNavigations(this.districtType, "City")
.And.HaveChild(this.districtType, "City", c2 => c2.Should().HaveOnlyNavigations(this.cityType, "Districts")));
@@ -567,6 +590,12 @@ internal SelectedPropertiesNodeAssertions(SelectedPropertiesNode node) : base(no
{
}
+ internal AndConstraint HaveProperties(IEdmEntityType entityType, params string[] propertyNames)
+ {
+ this.Subject.As().GetSelectedProperties(entityType).Select(p => p.Name).Should().BeEquivalentTo(propertyNames);
+ return new AndConstraint(this);
+ }
+
internal AndConstraint HaveStreams(IEdmEntityType entityType, params string[] streamPropertyNames)
{
this.Subject.As().GetSelectedStreamProperties(entityType).Keys.Should().BeEquivalentTo(streamPropertyNames);
diff --git a/test/FunctionalTests/Tests/DataOData/Tests/OData.Common.Tests/PublicApi/PublicApi.bsl b/test/FunctionalTests/Tests/DataOData/Tests/OData.Common.Tests/PublicApi/PublicApi.bsl
index 3d56b2704f..e653f25dfd 100644
--- a/test/FunctionalTests/Tests/DataOData/Tests/OData.Common.Tests/PublicApi/PublicApi.bsl
+++ b/test/FunctionalTests/Tests/DataOData/Tests/OData.Common.Tests/PublicApi/PublicApi.bsl
@@ -4552,6 +4552,7 @@ public sealed class Microsoft.OData.ODataConstants {
public static string MethodPost = "POST"
public static string MethodPut = "PUT"
public static string ODataVersionHeader = "OData-Version"
+ public static string OmitValuesNulls = "nulls"
}
[
@@ -4719,6 +4720,7 @@ public class Microsoft.OData.ODataPreferenceHeader {
string AnnotationFilter { public get; public set; }
bool ContinueOnError { public get; public set; }
System.Nullable`1[[System.Int32]] MaxPageSize { public get; public set; }
+ string OmitValues { public get; public set; }
bool RespondAsync { public get; public set; }
System.Nullable`1[[System.Boolean]] ReturnContent { public get; public set; }
bool TrackChanges { public get; public set; }
diff --git a/test/FunctionalTests/Tests/DataOData/Tests/OData.Writer.Tests/JsonLight/JsonLightEntryWriterTests.OmitNullPropertiesInEntryTest.approved.txt b/test/FunctionalTests/Tests/DataOData/Tests/OData.Writer.Tests/JsonLight/JsonLightEntryWriterTests.OmitNullPropertiesInEntryTest.approved.txt
new file mode 100644
index 0000000000..a1f0970148
--- /dev/null
+++ b/test/FunctionalTests/Tests/DataOData/Tests/OData.Writer.Tests/JsonLight/JsonLightEntryWriterTests.OmitNullPropertiesInEntryTest.approved.txt
@@ -0,0 +1,32 @@
+Combination: 1; TestConfiguration = Format: JsonLight, Request: True, Synchronous: True
+Model Present: true
+{"@odata.context":"http://odata.org/test/$metadata#CustomerSet/$entity","ID":42,"Hobby":"Hiking"}
+
+Combination: 2; TestConfiguration = Format: JsonLight, Request: False, Synchronous: True
+Model Present: true
+{"@odata.context":"http://odata.org/test/$metadata#CustomerSet/$entity","ID":42,"Hobby":"Hiking"}
+
+Combination: 3; TestConfiguration = Format: JsonLight, Request: True, Synchronous: False
+Model Present: true
+{"@odata.context":"http://odata.org/test/$metadata#CustomerSet/$entity","ID":42,"Hobby":"Hiking"}
+
+Combination: 4; TestConfiguration = Format: JsonLight, Request: False, Synchronous: False
+Model Present: true
+{"@odata.context":"http://odata.org/test/$metadata#CustomerSet/$entity","ID":42,"Hobby":"Hiking"}
+
+Combination: 5; TestConfiguration = Format: JsonLight, Request: True, Synchronous: True
+Model Present: true
+{"@odata.context":"http://odata.org/test/$metadata#CustomerSet/$entity","ID":44,"Hobby":null}
+
+Combination: 6; TestConfiguration = Format: JsonLight, Request: False, Synchronous: True
+Model Present: true
+{"@odata.context":"http://odata.org/test/$metadata#CustomerSet/$entity","ID":44}
+
+Combination: 7; TestConfiguration = Format: JsonLight, Request: True, Synchronous: False
+Model Present: true
+{"@odata.context":"http://odata.org/test/$metadata#CustomerSet/$entity","ID":44,"Hobby":null}
+
+Combination: 8; TestConfiguration = Format: JsonLight, Request: False, Synchronous: False
+Model Present: true
+{"@odata.context":"http://odata.org/test/$metadata#CustomerSet/$entity","ID":44}
+
diff --git a/test/FunctionalTests/Tests/DataOData/Tests/OData.Writer.Tests/JsonLight/JsonLightEntryWriterTests.cs b/test/FunctionalTests/Tests/DataOData/Tests/OData.Writer.Tests/JsonLight/JsonLightEntryWriterTests.cs
index f7491a77e3..11aae8bd24 100644
--- a/test/FunctionalTests/Tests/DataOData/Tests/OData.Writer.Tests/JsonLight/JsonLightEntryWriterTests.cs
+++ b/test/FunctionalTests/Tests/DataOData/Tests/OData.Writer.Tests/JsonLight/JsonLightEntryWriterTests.cs
@@ -48,7 +48,7 @@ public void PayloadOrderTest()
var nonMLEBaseType = new EdmEntityType("TestModel", "NonMLEBaseType");
nonMLEBaseType.AddKeys(nonMLEBaseType.AddStructuralProperty("ID", EdmCoreModel.Instance.GetInt32(false)));
model.AddElement(nonMLEBaseType);
- var nonMLESet = container.AddEntitySet("NonMLESet", nonMLEBaseType);
+ var nonMLESet = container.AddEntitySet("NonMLESet", nonMLEBaseType);
var nonMLEType = new EdmEntityType("TestModel", "NonMLEType", nonMLEBaseType);
nonMLEType.AddStructuralProperty("Name", EdmCoreModel.Instance.GetString(true));
@@ -66,13 +66,13 @@ public void PayloadOrderTest()
var mleType = new EdmEntityType("TestModel", "MLEType", mleBaseType, false, false, true);
mleType.AddStructuralProperty("Name", EdmCoreModel.Instance.GetString(true));
mleType.AddStructuralProperty("Description", EdmCoreModel.Instance.GetString(true));
- mleType.AddStructuralProperty("StreamProperty", EdmPrimitiveTypeKind.Stream, isNullable:false);
+ mleType.AddStructuralProperty("StreamProperty", EdmPrimitiveTypeKind.Stream, isNullable: false);
var mleNav = mleType.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo { Name = "NavProp", Target = otherType, TargetMultiplicity = EdmMultiplicity.Many });
mleSet.AddNavigationTarget(mleNav, otherset);
model.AddElement(mleType);
IEnumerable testCases = new[]
- {
+ {
new EntryPayloadTestCase
{
DebugDescription = "TypeName at the beginning, nothing else",
@@ -96,7 +96,7 @@ public void PayloadOrderTest()
new ODataProperty { Name = "Name", Value = "test" },
}
}
- .WithAnnotation(new WriteEntryCallbacksAnnotation
+ .WithAnnotation(new WriteEntryCallbacksAnnotation
{
BeforeWriteStartCallback = (entry) => { entry.TypeName = "TestModel.MLEType"; },
BeforeWriteEndCallback = (entry) => { entry.TypeName = "NonExistingType"; }
@@ -127,7 +127,7 @@ public void PayloadOrderTest()
{
DebugDescription = "TypeName at the beginning, ID and ETag at the end, ID and ETag are not written and are ignored at the end",
Items = new[] { new ODataResource() { TypeName = "TestModel.NonMLEType" }
- .WithAnnotation(new WriteEntryCallbacksAnnotation
+ .WithAnnotation(new WriteEntryCallbacksAnnotation
{
BeforeWriteStartCallback = (entry) => { entry.Id = null; entry.ETag = null; },
BeforeWriteEndCallback = (entry) => { entry.Id = new Uri("urn:id"); entry.ETag = "etag"; }
@@ -143,7 +143,7 @@ public void PayloadOrderTest()
new EntryPayloadTestCase
{
DebugDescription = "Everything at the beginning",
- Items = new[] { new ODataResource() {
+ Items = new[] { new ODataResource() {
TypeName = "TestModel.MLEType",
Id = new Uri("urn:id"),
ETag = "etag",
@@ -183,7 +183,7 @@ public void PayloadOrderTest()
new EntryPayloadTestCase
{
DebugDescription = "TypeName, Id, ETag and ReadLinks at the beginning, the rest at the end",
- Items = new[] { new ODataResource() {
+ Items = new[] { new ODataResource() {
TypeName = "TestModel.MLEType",
Id = new Uri("urn:id"),
ETag = "etag",
@@ -197,16 +197,16 @@ public void PayloadOrderTest()
new ODataProperty { Name = "ID", Value = (int)42 },
new ODataProperty { Name = "Name", Value = "test" },
}
- }.WithAnnotation(new WriteEntryCallbacksAnnotation
+ }.WithAnnotation(new WriteEntryCallbacksAnnotation
{
BeforeWriteStartCallback = (entry) =>
- {
+ {
entry.EditLink = null;
entry.MediaResource.EditLink = null;
entry.MediaResource.ETag = null;
entry.MediaResource.ContentType = null;
},
- BeforeWriteEndCallback = (entry) =>
+ BeforeWriteEndCallback = (entry) =>
{
entry.EditLink = new Uri("http://odata.org/editlink");
entry.MediaResource.EditLink = new Uri("http://odata.org/mediaeditlink");
@@ -240,7 +240,7 @@ public void PayloadOrderTest()
testCase.Items,
tc => new JsonWriterTestExpectedResults(this.Settings.ExpectedResultSettings)
{
- Json = string.Format(CultureInfo.InvariantCulture, testCase.Json,
+ Json = string.Format(CultureInfo.InvariantCulture, testCase.Json,
string.Empty,
JsonLightWriterUtils.GetMetadataUrlPropertyForEntry(testCase.EntitySet.Name) + ","),
FragmentExtractor = (result) => result.RemoveAllAnnotations(true)
@@ -338,7 +338,7 @@ public void ActionAndFunctionPayloadOrderTest()
model.AddElement(nonMLEType);
container.AddEntitySet("NonMLEType", nonMLEType);
- ODataAction action = new ODataAction
+ ODataAction action = new ODataAction
{
Metadata = new Uri("http://odata.org/test/$metadata#defaultAction"),
Title = "Default Action",
@@ -432,6 +432,96 @@ public void ActionAndFunctionPayloadOrderTest()
});
}
+ [TestMethod, Variation(Description = "Test skipping null values when writing JSON Lite entries with OmitNullValues = true.")]
+ public void OmitNullPropertiesInEntryTest()
+ {
+ EdmModel model = new EdmModel();
+ var container = new EdmEntityContainer("TestModel", "TestContainer");
+ model.AddElement(container);
+
+ var customerType = new EdmEntityType("TestModel", "CustomerType", null, false, isOpen: false);
+ customerType.AddKeys(customerType.AddStructuralProperty("ID", EdmCoreModel.Instance.GetInt32(false)));
+ customerType.AddKeys(customerType.AddStructuralProperty("Hobby", EdmCoreModel.Instance.GetString(true)));
+ model.AddElement(customerType);
+ var customerSet = container.AddEntitySet("CustomerSet", customerType);
+
+ var addressType = new EdmComplexType("TestModel", "AddressType");
+ addressType.AddStructuralProperty("Street", EdmCoreModel.Instance.GetStream(true));
+ model.AddElement(addressType);
+
+ IEnumerable testCases = new[]
+ {
+ new EntryPayloadTestCase
+ {
+ DebugDescription = "Customer instance with all properties set.",
+ Items = new[] { new ODataResource()
+ {
+ TypeName = "TestModel.CustomerType",
+ Properties = new ODataProperty[]
+ {
+ new ODataProperty { Name = "ID", Value = (int)42 },
+ new ODataProperty { Name = "Hobby", Value = "Hiking" },
+ }
+ } },
+ Model = model,
+ EntitySet = customerSet,
+ Json = string.Join("$(NL)",
+ "{{",
+ "{0}\"ID\":\"42\", \"Hobby\":\"Hiking\"",
+ "}}")
+ },
+ new EntryPayloadTestCase
+ {
+ DebugDescription = "Customer instance without Hobby.",
+ Items = new[] {new ODataResource()
+ {
+ TypeName = "TestModel.CustomerType",
+ Properties = new ODataProperty[]
+ {
+ new ODataProperty { Name = "ID", Value = (int)44 },
+ new ODataProperty { Name = "Hobby", Value = null },
+ }
+ } },
+ Model = model,
+ EntitySet = customerSet,
+ Json = string.Join("$(NL)",
+ "{{",
+ "{0}\"ID\":\"44\"",
+ "}}")
+ },
+
+ };
+
+ IEnumerable> testDescriptors = testCases.Select(testCase =>
+ new PayloadWriterTestDescriptor(
+ this.Settings,
+ testCase.Items,
+ tc => new JsonWriterTestExpectedResults(this.Settings.ExpectedResultSettings)
+ {
+ Json = string.Format(
+ CultureInfo.InvariantCulture,
+ testCase.Json,
+ JsonLightWriterUtils.GetMetadataUrlPropertyForEntry(testCase.EntitySet.Name) + ","),
+ FragmentExtractor = (result) => result.RemoveAllAnnotations(true)
+ })
+ {
+ DebugDescription = testCase.DebugDescription,
+ Model = testCase.Model,
+ PayloadEdmElementContainer = testCase.EntitySet,
+ PayloadEdmElementType = testCase.EntityType,
+ SkipTestConfiguration = testCase.SkipTestConfiguration
+ });
+
+ this.CombinatorialEngineProvider.RunCombinations(
+ testDescriptors,
+ this.WriterTestConfigurationProvider.JsonLightFormatConfigurationsWithIndent,
+ (testDescriptor, testConfiguration) =>
+ {
+ testConfiguration.MessageWriterSettings.OmitNullValues = true;
+ TestWriterUtils.WriteAndVerifyODataEdmPayload(testDescriptor, testConfiguration, this.Assert, this.Logger);
+ });
+ }
+
[TestMethod, Variation(Description = "Test correct serialization format when writing JSON Lite entries with open properties.")]
public void OpenPropertiesInEntryTest()
{
@@ -439,7 +529,7 @@ public void OpenPropertiesInEntryTest()
var container = new EdmEntityContainer("TestModel", "TestContainer");
model.AddElement(container);
- var openCustomerType = new EdmEntityType("TestModel", "OpenCustomerType", null, false, isOpen:true);
+ var openCustomerType = new EdmEntityType("TestModel", "OpenCustomerType", null, false, isOpen: true);
openCustomerType.AddKeys(openCustomerType.AddStructuralProperty("ID", EdmCoreModel.Instance.GetInt32(false)));
model.AddElement(openCustomerType);
var customerSet = container.AddEntitySet("CustomerSet", openCustomerType);
@@ -455,9 +545,9 @@ public void OpenPropertiesInEntryTest()
new EntryPayloadTestCase
{
DebugDescription = "Customer instance with open primitive property.",
- Items = new[] { new ODataResource()
- {
- TypeName = "TestModel.OpenCustomerType",
+ Items = new[] { new ODataResource()
+ {
+ TypeName = "TestModel.OpenCustomerType",
Properties = new ODataProperty[]
{
new ODataProperty { Name = "Age", Value = (long)42 }
@@ -473,9 +563,9 @@ public void OpenPropertiesInEntryTest()
new EntryPayloadTestCase
{
DebugDescription = "Customer instance with open spatial property.",
- Items = new[] { new ODataResource()
- {
- TypeName = "TestModel.OpenCustomerType",
+ Items = new[] { new ODataResource()
+ {
+ TypeName = "TestModel.OpenCustomerType",
Properties = new ODataProperty[]
{
new ODataProperty { Name = "Location", Value = pointValue }
@@ -494,7 +584,7 @@ public void OpenPropertiesInEntryTest()
"\"type\":\"name\",\"properties\":{{",
"\"name\":\"EPSG:4326\"",
"}}",
- "}}",
+ "}}",
"}}",
"}}")
},
@@ -526,7 +616,7 @@ public void OpenPropertiesInEntryTest()
tc => new JsonWriterTestExpectedResults(this.Settings.ExpectedResultSettings)
{
Json = string.Format(
- CultureInfo.InvariantCulture,
+ CultureInfo.InvariantCulture,
testCase.Json,
JsonLightWriterUtils.GetMetadataUrlPropertyForEntry(testCase.EntitySet.Name) + ","),
FragmentExtractor = (result) => result.RemoveAllAnnotations(true)
@@ -561,7 +651,7 @@ public void SpatialPropertiesInEntryTest()
customerType.AddStructuralProperty("Location2", EdmCoreModel.Instance.GetSpatial(EdmPrimitiveTypeKind.GeographyPoint, false));
model.AddElement(customerType);
var customerSet = container.AddEntitySet("CustomerSet", customerType);
-
+
ISpatial pointValue = GeographyFactory.Point(32.0, -100.0).Build();
IEnumerable testCases = new[]
@@ -569,9 +659,9 @@ public void SpatialPropertiesInEntryTest()
new EntryPayloadTestCase
{
DebugDescription = "Customer instance with spatial property (expected and payload type don't match).",
- Items = new[] { new ODataResource()
- {
- TypeName = "TestModel.CustomerType",
+ Items = new[] { new ODataResource()
+ {
+ TypeName = "TestModel.CustomerType",
Properties = new ODataProperty[]
{
new ODataProperty { Name = "Location1", Value = pointValue }
@@ -590,16 +680,16 @@ public void SpatialPropertiesInEntryTest()
"\"type\":\"name\",\"properties\":{{",
"\"name\":\"EPSG:4326\"",
"}}",
- "}}",
+ "}}",
"}}",
"}}")
},
new EntryPayloadTestCase
{
DebugDescription = "Customer instance with spatial property (expected and payload type match).",
- Items = new[] { new ODataResource()
- {
- TypeName = "TestModel.CustomerType",
+ Items = new[] { new ODataResource()
+ {
+ TypeName = "TestModel.CustomerType",
Properties = new ODataProperty[]
{
new ODataProperty { Name = "Location2", Value = pointValue }
@@ -617,7 +707,7 @@ public void SpatialPropertiesInEntryTest()
"\"type\":\"name\",\"properties\":{{",
"\"name\":\"EPSG:4326\"",
"}}",
- "}}",
+ "}}",
"}}",
"}}")
},
@@ -686,7 +776,7 @@ public void TopLevelOpenComplexProperties()
tc => new JsonWriterTestExpectedResults(this.Settings.ExpectedResultSettings)
{
Json = string.Format(
- CultureInfo.InvariantCulture,
+ CultureInfo.InvariantCulture,
testCase.Json,
JsonLightWriterUtils.GetMetadataUrlPropertyForProperty(testCase.EntityType.FullTypeName()) + ","),
FragmentExtractor = (result) => result.RemoveAllAnnotations(true)
@@ -706,7 +796,7 @@ public void TopLevelOpenComplexProperties()
{
TestWriterUtils.WriteAndVerifyODataEdmPayload(testDescriptor, testConfiguration, this.Assert, this.Logger);
});
-
+
}
private sealed class EntryPayloadTestCase
{
diff --git a/test/FunctionalTests/Tests/DataOData/Tests/OData.Writer.Tests/Microsoft.Test.Taupo.OData.Writer.Tests.csproj b/test/FunctionalTests/Tests/DataOData/Tests/OData.Writer.Tests/Microsoft.Test.Taupo.OData.Writer.Tests.csproj
index d9feb236ef..5f09f08824 100644
--- a/test/FunctionalTests/Tests/DataOData/Tests/OData.Writer.Tests/Microsoft.Test.Taupo.OData.Writer.Tests.csproj
+++ b/test/FunctionalTests/Tests/DataOData/Tests/OData.Writer.Tests/Microsoft.Test.Taupo.OData.Writer.Tests.csproj
@@ -182,6 +182,9 @@
PreserveNewest
+
+ PreserveNewest
+
PreserveNewest
diff --git a/test/FunctionalTests/Tests/DataOData/Tests/OData.Writer.Tests/TestWriterUtils.cs b/test/FunctionalTests/Tests/DataOData/Tests/OData.Writer.Tests/TestWriterUtils.cs
index 4c1a4f7a4d..585ae784c5 100644
--- a/test/FunctionalTests/Tests/DataOData/Tests/OData.Writer.Tests/TestWriterUtils.cs
+++ b/test/FunctionalTests/Tests/DataOData/Tests/OData.Writer.Tests/TestWriterUtils.cs
@@ -166,6 +166,29 @@ internal static void WriteActionAndVerifyODataPayload(Action(
this PayloadWriterTestDescriptor testDescriptor,
WriterTestConfiguration testConfiguration,
bool alwaysSpecifyOwningContainer = false,
- BaselineLogger baselineLogger = null,
+ BaselineLogger baselineLogger = null,
Action writeAction = null)
{
T property = testDescriptor.PayloadItems.Single();
@@ -488,6 +511,12 @@ public static TestMessage CreateOutputMessageFromStream(
}
responseMessage.StatusCode = 200;
+
+ if (testConfiguration.MessageWriterSettings.OmitNullValues)
+ {
+ responseMessage.PreferenceAppliedHeader().OmitValues = Microsoft.OData.ODataConstants.OmitValuesNulls;
+ }
+
message = responseMessage;
}
@@ -679,9 +708,9 @@ private static void WritePayload(ODataMessageWriterTestWrapper messageWriter, OD
/// A boolean flag indicating whether to flush the writer after writing.
/// The payload items to write.
///
- /// The list of is interpreted in the following way:
+ /// The list of is interpreted in the following way:
/// for every non-null item we call WriteStart; for every null item we call WriteEnd.
- /// null items can be omitted at the end of the list, e.g., a list of a feed and an entry
+ /// null items can be omitted at the end of the list, e.g., a list of a feed and an entry
/// item would be 'auto-completed' with two null entries.
///
internal static void WritePayload(ODataMessageWriterTestWrapper messageWriter, ODataWriter writer, bool flush, IList items, int throwUserExceptionAt = -1)