diff --git a/src/Microsoft.AspNetCore.OData/Formatter/LinkGenerationHelpers.cs b/src/Microsoft.AspNetCore.OData/Formatter/LinkGenerationHelpers.cs index 5dc4593e5..ea52de72a 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/LinkGenerationHelpers.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/LinkGenerationHelpers.cs @@ -37,6 +37,11 @@ public static Uri GenerateSelfLink(this ResourceContext resourceContext, bool in throw Error.ArgumentNull(nameof(resourceContext)); } + if (resourceContext.Request == null) + { + return null; + } + IList idLinkPathSegments = resourceContext.GenerateBaseODataPathSegments(); bool isSameType = resourceContext.StructuredType == resourceContext.NavigationSource?.EntityType; diff --git a/src/Microsoft.AspNetCore.OData/Formatter/ODataOutputFormatterHelper.cs b/src/Microsoft.AspNetCore.OData/Formatter/ODataOutputFormatterHelper.cs index 8c778adce..baa00320f 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/ODataOutputFormatterHelper.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/ODataOutputFormatterHelper.cs @@ -89,8 +89,14 @@ internal static async Task WriteToStreamAsync( writerSettings.BaseUri = baseAddress; //use v401 to write delta payloads. - if (serializer.ODataPayloadKind == ODataPayloadKind.Delta) + if (serializer.ODataPayloadKind == ODataPayloadKind.Delta && version < ODataVersion.V401) { + // Preserve setting of OmitODataPrefix + if (writerSettings.Version.GetValueOrDefault() == ODataVersion.V4) + { + writerSettings.SetOmitODataPrefix(writerSettings.GetOmitODataPrefix(ODataVersion.V4), ODataVersion.V401); + } + writerSettings.Version = ODataVersion.V401; } else diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeltaResourceSetSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeltaResourceSetSerializer.cs index 25b700958..89e3cbd1d 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeltaResourceSetSerializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeltaResourceSetSerializer.cs @@ -8,6 +8,7 @@ using System; using System.Collections; using System.Diagnostics.Contracts; +using System.Reflection; using System.Runtime.Serialization; using System.Threading.Tasks; using Microsoft.AspNetCore.OData.Abstracts; @@ -56,10 +57,6 @@ public override async Task WriteObjectAsync(object graph, Type type, ODataMessag } IEdmEntitySetBase entitySet = writeContext.NavigationSource as IEdmEntitySetBase; - if (entitySet == null) - { - throw new SerializationException(SRResources.EntitySetMissingDuringSerialization); - } IEdmTypeReference feedType = writeContext.GetEdmType(graph, type); Contract.Assert(feedType != null); @@ -162,11 +159,19 @@ private async Task WriteDeltaResourceSetAsync(IEnumerable enumerable, IEdmTypeRe } lastResource = item; - DeltaItemKind kind = GetDelteItemKind(item); + DeltaItemKind kind = GetDeltaItemKind(item); switch (kind) { case DeltaItemKind.DeletedResource: - await WriteDeltaDeletedResourceAsync(item, writer, writeContext).ConfigureAwait(false); + // hack. if the WriteDeltaDeletedResourceAsync isn't overridden, call the new version + if (WriteDeltaDeletedResourceAsyncIsOverridden()) + { + await WriteDeltaDeletedResourceAsync(item, writer, writeContext).ConfigureAwait(false); + } + else + { + await WriteDeletedResourceAsync(item, elementType, writer, writeContext).ConfigureAwait(false); + } break; case DeltaItemKind.DeltaDeletedLink: await WriteDeltaDeletedLinkAsync(item, writer, writeContext).ConfigureAwait(false); @@ -212,7 +217,7 @@ await entrySerializer.WriteDeltaObjectInlineAsync(item, elementType, writer, wri /// The serializer context. /// The function that generates the NextLink from an object. /// - internal static Func GetNextLinkGenerator(ODataDeltaResourceSet deltaResourceSet, IEnumerable enumerable, ODataSerializerContext writeContext) + internal static Func GetNextLinkGenerator(ODataResourceSetBase deltaResourceSet, IEnumerable enumerable, ODataSerializerContext writeContext) { return ODataResourceSetSerializer.GetNextLinkGenerator(deltaResourceSet, enumerable, writeContext); } @@ -266,6 +271,8 @@ public virtual ODataDeltaResourceSet CreateODataDeltaResourceSet(IEnumerable fee /// The object to be written. /// The to be used for writing. /// The . + [Obsolete("WriteDeltaDeletedResourceAsync(object, ODataWriter, ODataSerializerContext) is Deprecated and will be removed in the next version." + + "Please use WriteDeletedResourceAsync(object, IEdmEntityTypeReference, ODataWriter, ODataSerializerContext)")] public virtual async Task WriteDeltaDeletedResourceAsync(object value, ODataWriter writer, ODataSerializerContext writeContext) { if (writer == null) @@ -304,6 +311,31 @@ public virtual async Task WriteDeltaDeletedResourceAsync(object value, ODataWrit } } + /// + /// Writes the given deltaDeletedEntry specified by the parameter graph as a part of an existing OData message using the given + /// messageWriter and the writeContext. + /// + /// The object to be written. + /// The expected type of the deleted resource. + /// The to be used for writing. + /// The . + public virtual async Task WriteDeletedResourceAsync(object value, IEdmStructuredTypeReference expectedType, ODataWriter writer, ODataSerializerContext writeContext) + { + if (writer == null) + { + throw Error.ArgumentNull(nameof(writer)); + } + + IODataEdmTypeSerializer deletedResourceSerializer = SerializerProvider.GetEdmTypeSerializer(expectedType); + if (deletedResourceSerializer == null) + { + throw new SerializationException( + Error.Format(SRResources.TypeCannotBeSerialized, expectedType.FullName())); + } + + await deletedResourceSerializer.WriteObjectInlineAsync(value, expectedType, writer, writeContext); + } + /// /// Writes the given deltaDeletedLink specified by the parameter graph as a part of an existing OData message using the given /// messageWriter and the writeContext. @@ -382,7 +414,7 @@ public virtual async Task WriteDeltaLinkAsync(object value, ODataWriter writer, } } - internal DeltaItemKind GetDelteItemKind(object item) + internal DeltaItemKind GetDeltaItemKind(object item) { IEdmChangedObject edmChangedObject = item as IEdmChangedObject; if (edmChangedObject != null) @@ -414,4 +446,17 @@ private static IEdmStructuredTypeReference GetResourceType(IEdmTypeReference fee string message = Error.Format(SRResources.CannotWriteType, typeof(ODataDeltaResourceSetSerializer).Name, feedType.FullName()); throw new SerializationException(message); } + + // Discover whether or not WriteDeltaDeletedResourceAsync is overridden in a derived class. + // WriteDeltaDeletedResourceAsync is deprecated in favor of WriteDeletedResourceAsync, but + // to avoid breaking changes, this retains the behavior of calling a custom + // WriteDeltaDeletedResourceAsync method for the case that the service has overriden that + // method with a custom implementation. In the next breaking change, WriteDeltaDeletedResourceAsync + // should be removed, and this private method can be deleted. + private bool WriteDeltaDeletedResourceAsyncIsOverridden() + { + MethodInfo method = GetType().GetMethod("WriteDeltaDeletedResourceAsync", new Type[] { typeof(object), typeof(ODataWriter), typeof(ODataSerializerContext) }); + Contract.Assert(method != null, "WriteDeltaDeletedResourceAsync is not defined."); + return method.DeclaringType != typeof(ODataDeltaResourceSetSerializer); + } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs index 9397fe89c..16dd486f8 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs @@ -117,35 +117,7 @@ public virtual async Task WriteDeltaObjectInlineAsync(object graph, IEdmTypeRefe } else { - await WriteDeltaResourceAsync(graph, writer, writeContext).ConfigureAwait(false); - } - } - - private async Task WriteDeltaResourceAsync(object graph, ODataWriter writer, ODataSerializerContext writeContext) - { - Contract.Assert(writeContext != null); - - IEdmStructuredTypeReference structuredType = GetResourceType(graph, writeContext); - ResourceContext resourceContext = new ResourceContext(writeContext, structuredType, graph); - EdmDeltaResourceObject deltaResource = graph as EdmDeltaResourceObject; - if (deltaResource != null && deltaResource.NavigationSource != null) - { - resourceContext.NavigationSource = deltaResource.NavigationSource; - } - - SelectExpandNode selectExpandNode = CreateSelectExpandNode(resourceContext); - if (selectExpandNode != null) - { - ODataResource resource = CreateResource(selectExpandNode, resourceContext); - - if (resource != null) - { - await writer.WriteStartAsync(resource).ConfigureAwait(false); - await WriteDeltaComplexPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); - await WriteDynamicComplexPropertiesAsync(resourceContext, writer).ConfigureAwait(false); - await WriteDeltaNavigationPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); - await writer.WriteEndAsync().ConfigureAwait(false); - } + await WriteResourceAsync(graph, writer, writeContext, expectedType).ConfigureAwait(false); } } @@ -159,11 +131,22 @@ private async Task WriteDeltaComplexPropertiesAsync(SelectExpandNode selectExpan { return; } + IEnumerable complexProperties = selectExpandNode.SelectedComplexProperties.Keys; if (null != resourceContext.EdmObject && resourceContext.EdmObject.IsDeltaResource()) { - if (resourceContext.EdmObject is TypedEdmEntityObject obj && obj.Instance is IDelta deltaObject) + IDelta deltaObject = null; + if (resourceContext.EdmObject is TypedEdmEntityObject obj) + { + deltaObject = obj.Instance as IDelta; + } + else + { + deltaObject = resourceContext.EdmObject as IDelta; + } + + if (deltaObject != null) { IEnumerable changedProperties = deltaObject.GetChangedPropertyNames(); complexProperties = complexProperties.Where(p => changedProperties.Contains(p.Name)); @@ -172,14 +155,11 @@ private async Task WriteDeltaComplexPropertiesAsync(SelectExpandNode selectExpan foreach (IEdmStructuralProperty complexProperty in complexProperties) { - ODataNestedResourceInfo nestedResourceInfo = new ODataNestedResourceInfo - { - IsCollection = complexProperty.Type.IsCollection(), - Name = complexProperty.Name - }; + PathSelectItem selectItem = selectExpandNode.SelectedComplexProperties[complexProperty]; + ODataNestedResourceInfo nestedResourceInfo = CreateComplexNestedResourceInfo(complexProperty, selectExpandNode.SelectedComplexProperties[complexProperty], resourceContext); await writer.WriteStartAsync(nestedResourceInfo).ConfigureAwait(false); - await WriteDeltaComplexAndExpandedNavigationPropertyAsync(complexProperty, null, resourceContext, writer) + await WriteDeltaComplexAndExpandedNavigationPropertyAsync(complexProperty, selectItem, resourceContext, writer) .ConfigureAwait(false); await writer.WriteEndAsync().ConfigureAwait(false); } @@ -187,7 +167,7 @@ await WriteDeltaComplexAndExpandedNavigationPropertyAsync(complexProperty, null, private async Task WriteDeltaComplexAndExpandedNavigationPropertyAsync( IEdmProperty edmProperty, - SelectExpandClause selectExpandClause, + SelectItem selectItem, ResourceContext resourceContext, ODataWriter writer, Type navigationPropertyType = null) @@ -222,26 +202,50 @@ await writer.WriteStartAsync(new ODataResourceSet else { // create the serializer context for the complex and expanded item. - ODataSerializerContext nestedWriteContext = new ODataSerializerContext(resourceContext, selectExpandClause, edmProperty); + ODataSerializerContext nestedWriteContext = new ODataSerializerContext(resourceContext, edmProperty, resourceContext.SerializerContext.QueryContext, selectItem); nestedWriteContext.Type = navigationPropertyType; - // write object. - // TODO: enable overriding serializer based on type. Currently requires serializer supports WriteDeltaObjectinline, because it takes an ODataDeltaWriter - // ODataEdmTypeSerializer serializer = SerializerProvider.GetEdmTypeSerializer(edmProperty.Type); - // if (serializer == null) - // { - // throw new SerializationException( - // Error.Format(SRResources.TypeCannotBeSerialized, edmProperty.Type.ToTraceString())); - // } + // write object. if (edmProperty.Type.IsCollection()) { - ODataDeltaResourceSetSerializer serializer = new ODataDeltaResourceSetSerializer(SerializerProvider); - await serializer.WriteObjectInlineAsync(propertyValue, edmProperty.Type, writer, nestedWriteContext).ConfigureAwait(false); + if (IsDeltaCollection(propertyValue)) + { + // TODO: enable overriding serializer based on type. Currently requires serializer supports WriteDeltaObjectinline, because it takes an ODataDeltaWriter + // ODataEdmTypeSerializer serializer = SerializerProvider.GetEdmTypeSerializer(edmProperty.Type); + // if (serializer == null) + // { + // throw new SerializationException( + // Error.Format(SRResources.TypeCannotBeSerialized, edmProperty.Type.ToTraceString())); + // } + ODataEdmTypeSerializer serializer = new ODataDeltaResourceSetSerializer(SerializerProvider); + IEdmEntityType itemType = edmProperty.Type.GetElementType() as IEdmEntityType; + if(itemType == null) + { + throw new SerializationException( + Error.Format(SRResources.TypeCannotBeSerialized, edmProperty.Type.ToTraceString())); + } + + await serializer.WriteObjectInlineAsync( + propertyValue, + edmProperty.Type, + writer, + nestedWriteContext).ConfigureAwait(false); + } + else + { + IODataEdmTypeSerializer serializer = SerializerProvider.GetEdmTypeSerializer(edmProperty.Type); + if (serializer == null) + { + throw new SerializationException( + Error.Format(SRResources.TypeCannotBeSerialized, edmProperty.Type.ToTraceString())); + } + + await serializer.WriteObjectInlineAsync(propertyValue, edmProperty.Type, writer, nestedWriteContext).ConfigureAwait(false); + } } else { - ODataResourceSerializer serializer = new ODataResourceSerializer(SerializerProvider); - await serializer.WriteDeltaObjectInlineAsync(propertyValue, edmProperty.Type, writer, nestedWriteContext).ConfigureAwait(false); + await WriteDeltaObjectInlineAsync(propertyValue, edmProperty.Type, writer, nestedWriteContext).ConfigureAwait(false); } } } @@ -253,7 +257,7 @@ await writer.WriteStartAsync(new ODataResourceSet /// The resource context for the resource being written. /// The ODataWriter. /// A task that represents the asynchronous write operation - internal async Task WriteDeltaNavigationPropertiesAsync(SelectExpandNode selectExpandNode, ResourceContext resourceContext, ODataWriter writer) + private async Task WriteDeltaNavigationPropertiesAsync(SelectExpandNode selectExpandNode, ResourceContext resourceContext, ODataWriter writer) { Contract.Assert(resourceContext != null, "The ResourceContext cannot be null"); Contract.Assert(writer != null, "The ODataWriter cannot be null"); @@ -262,12 +266,7 @@ internal async Task WriteDeltaNavigationPropertiesAsync(SelectExpandNode selectE foreach (KeyValuePair navigationProperty in navigationProperties) { - ODataNestedResourceInfo nestedResourceInfo = new ODataNestedResourceInfo - { - IsCollection = navigationProperty.Key.Type.IsCollection(), - Name = navigationProperty.Key.Name - }; - + ODataNestedResourceInfo nestedResourceInfo = CreateNavigationLink(navigationProperty.Key, resourceContext); await writer.WriteStartAsync(nestedResourceInfo).ConfigureAwait(false); await WriteDeltaComplexAndExpandedNavigationPropertyAsync(navigationProperty.Key, null, resourceContext, writer, navigationProperty.Value).ConfigureAwait(false); await writer.WriteEndAsync().ConfigureAwait(false); @@ -276,11 +275,11 @@ internal async Task WriteDeltaNavigationPropertiesAsync(SelectExpandNode selectE private IEnumerable> GetNavigationPropertiesToWrite(SelectExpandNode selectExpandNode, ResourceContext resourceContext) { - ISet navigationProperties = selectExpandNode.SelectedNavigationProperties; + IEnumerable navigationProperties = selectExpandNode.ExpandedProperties?.Keys; if (navigationProperties == null) { - yield break; + navigationProperties = resourceContext.StructuredType.DeclaredNavigationProperties(); } if (resourceContext.EdmObject is IDelta changedObject) @@ -442,32 +441,100 @@ private async Task WriteResourceAsync(object graph, ODataWriter writer, ODataSer IEdmStructuredTypeReference structuredType = GetResourceType(graph, writeContext); ResourceContext resourceContext = new ResourceContext(writeContext, structuredType, graph); + IEdmNavigationSource originalNavigationSource = writeContext.NavigationSource; + if (graph is EdmDeltaResourceObject deltaResource && deltaResource?.NavigationSource != null) + { + resourceContext.NavigationSource = deltaResource.NavigationSource; + } + SelectExpandNode selectExpandNode = CreateSelectExpandNode(resourceContext); if (selectExpandNode != null) { - ODataResource resource = CreateResource(selectExpandNode, resourceContext); - if (resource != null) + if (graph is IDeltaDeletedResource || graph is IEdmDeltaDeletedResourceObject) { - if (resourceContext.SerializerContext.ExpandReference) + ODataDeletedResource odataDeletedResource; + + if (graph is EdmDeltaDeletedResourceObject edmDeltaDeletedEntity) { - await writer.WriteEntityReferenceLinkAsync(new ODataEntityReferenceLink + odataDeletedResource = CreateDeletedResource(edmDeltaDeletedEntity.Id, edmDeltaDeletedEntity.Reason ?? DeltaDeletedEntryReason.Deleted, selectExpandNode, resourceContext); + if (edmDeltaDeletedEntity.NavigationSource != null) { - Url = resource.Id - }).ConfigureAwait(false); + resourceContext.NavigationSource = edmDeltaDeletedEntity.NavigationSource; + ODataResourceSerializationInfo serializationInfo = new ODataResourceSerializationInfo + { + NavigationSourceName = edmDeltaDeletedEntity.NavigationSource.Name + }; + odataDeletedResource.SetSerializationInfo(serializationInfo); + } + } + else if (graph is IDeltaDeletedResource deltaDeletedResource) + { + odataDeletedResource = CreateDeletedResource(deltaDeletedResource.Id, deltaDeletedResource.Reason ?? DeltaDeletedEntryReason.Deleted, selectExpandNode, resourceContext); } else { - await writer.WriteStartAsync(resource).ConfigureAwait(false); - await WriteUntypedPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); - await WriteStreamPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); - await WriteComplexPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); - await WriteDynamicComplexPropertiesAsync(resourceContext, writer).ConfigureAwait(false); - await WriteNavigationLinksAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); - await WriteExpandedNavigationPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); - await WriteReferencedNavigationPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); - await writer.WriteEndAsync().ConfigureAwait(false); + throw new SerializationException(Error.Format(SRResources.CannotWriteType, GetType().Name, graph?.GetType().FullName)); } + + await writer.WriteStartAsync(odataDeletedResource).ConfigureAwait(false); + await WriteResourceContent(writer, selectExpandNode, resourceContext, /*isDelta*/ true).ConfigureAwait(false); + await writer.WriteEndAsync().ConfigureAwait(false); } + else + { + ODataResource resource = CreateResource(selectExpandNode, resourceContext); + if (resource != null) + { + if (resourceContext.SerializerContext.ExpandReference) + { + await writer.WriteEntityReferenceLinkAsync(new ODataEntityReferenceLink + { + Url = resource.Id + }).ConfigureAwait(false); + } + else + { + bool isDelta = graph is IDelta || graph is IEdmChangedObject; + await writer.WriteStartAsync(resource).ConfigureAwait(false); + await WriteResourceContent(writer, selectExpandNode, resourceContext, isDelta).ConfigureAwait(false); + await writer.WriteEndAsync().ConfigureAwait(false); + } + } + } + + writeContext.NavigationSource = originalNavigationSource; + } + } + + /// + /// Writes the context of a Resource + /// + /// The to use to write the resource contents + /// The describing the response graph. + /// The context for the resource instance being written. + /// Whether to only write changed properties of the resource + private async Task WriteResourceContent(ODataWriter writer, SelectExpandNode selectExpandNode, ResourceContext resourceContext, bool isDelta) + { + // TODO: These should be aligned; do we need different methods for delta versus non-delta complex/navigation properties? + if (isDelta) + { + await WriteUntypedPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); + await WriteStreamPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); + await WriteDeltaComplexPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); + //await WriteComplexPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); + await WriteDynamicComplexPropertiesAsync(resourceContext, writer).ConfigureAwait(false); + await WriteDeltaNavigationPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); + //await WriteExpandedNavigationPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); + } + else + { + await WriteUntypedPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); + await WriteStreamPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); + await WriteComplexPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); + await WriteDynamicComplexPropertiesAsync(resourceContext, writer).ConfigureAwait(false); + await WriteNavigationLinksAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); + await WriteExpandedNavigationPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); + await WriteReferencedNavigationPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); } } @@ -535,17 +602,12 @@ public virtual ODataResource CreateResource(SelectExpandNode selectExpandNode, R Properties = CreateStructuralPropertyBag(selectExpandNode, resourceContext), }; - if (resourceContext.EdmObject is EdmDeltaResourceObject && resourceContext.NavigationSource != null) + InitializeODataResource(selectExpandNode, resource, resourceContext); + + string etag = CreateETag(resourceContext); + if (etag != null) { - ODataResourceSerializationInfo serializationInfo = new ODataResourceSerializationInfo(); - serializationInfo.NavigationSourceName = resourceContext.NavigationSource.Name; - serializationInfo.NavigationSourceKind = resourceContext.NavigationSource.NavigationSourceKind(); - IEdmEntityType sourceType = resourceContext.NavigationSource.EntityType; - if (sourceType != null) - { - serializationInfo.NavigationSourceEntityTypeName = sourceType.Name; - } - resource.SetSerializationInfo(serializationInfo); + resource.ETag = etag; } // Try to add the dynamic properties if the structural type is open. @@ -569,6 +631,74 @@ public virtual ODataResource CreateResource(SelectExpandNode selectExpandNode, R } } + return resource; + } + + /// + /// Creates the to be written while writing this resource. + /// + /// The id of the Deleted Resource to be written (may be null if properties contains all key properties) + /// The for the removal of the resource. + /// The describing the response graph. + /// The context for the resource instance being written. + /// The created . + public virtual ODataDeletedResource CreateDeletedResource(Uri id, DeltaDeletedEntryReason reason, SelectExpandNode selectExpandNode, ResourceContext resourceContext) + { + if (selectExpandNode == null) + { + throw Error.ArgumentNull(nameof(selectExpandNode)); + } + + if (resourceContext == null) + { + throw Error.ArgumentNull(nameof(resourceContext)); + } + + string typeName = resourceContext.StructuredType.FullTypeName(); + + ODataDeletedResource resource = new ODataDeletedResource + { + Id = id ?? (resourceContext.NavigationSource == null ? null : resourceContext.GenerateSelfLink(false)), + TypeName = typeName ?? "Edm.Untyped", + Properties = CreateStructuralPropertyBag(selectExpandNode, resourceContext), + Reason = reason + }; + + InitializeODataResource(selectExpandNode, resource, resourceContext); + + string etag = CreateETag(resourceContext); + if (etag != null) + { + resource.ETag = etag; + } + + // Try to add the dynamic properties if the structural type is open. + AppendDynamicPropertiesInternal(resource, selectExpandNode, resourceContext); + + return resource; + } + + /// + /// Initializes an ODataResource to be written + /// + /// The describing the response graph. + /// The resource that will be initialized + /// The context for the resource instance being written. + private void InitializeODataResource(SelectExpandNode selectExpandNode, ODataResourceBase resource, ResourceContext resourceContext) + { + if ((resourceContext.EdmObject is EdmDeltaResourceObject || resourceContext.EdmObject is IEdmDeltaDeletedResourceObject) && resourceContext.NavigationSource != null) + { + ODataResourceSerializationInfo serializationInfo = new ODataResourceSerializationInfo(); + serializationInfo.NavigationSourceName = resourceContext.NavigationSource.Name; + serializationInfo.NavigationSourceKind = resourceContext.NavigationSource.NavigationSourceKind(); + IEdmEntityType sourceType = resourceContext.NavigationSource.EntityType; + if (sourceType != null) + { + serializationInfo.NavigationSourceEntityTypeName = sourceType.Name; + } + resource.SetSerializationInfo(serializationInfo); + } + IEdmStructuredType pathType = GetODataPathType(resourceContext.SerializerContext); if (resourceContext.StructuredType.TypeKind == EdmTypeKind.Complex) { @@ -598,31 +728,23 @@ public virtual ODataResource CreateResource(SelectExpandNode selectExpandNode, R NavigationSourceLinkBuilderAnnotation linkBuilder = EdmModelLinkBuilderExtensions.GetNavigationSourceLinkBuilder(model, resourceContext.NavigationSource); EntitySelfLinks selfLinks = linkBuilder.BuildEntitySelfLinks(resourceContext, resourceContext.SerializerContext.MetadataLevel); - if (selfLinks.IdLink != null) + if (resource.Id == null && selfLinks.IdLink != null) { resource.Id = selfLinks.IdLink; } - if (selfLinks.ReadLink != null) + if (resource.ReadLink == null && selfLinks.ReadLink != null) { resource.ReadLink = selfLinks.ReadLink; } - if (selfLinks.EditLink != null) + if (resource.EditLink == null && selfLinks.EditLink != null) { resource.EditLink = selfLinks.EditLink; } } - - string etag = CreateETag(resourceContext); - if (etag != null) - { - resource.ETag = etag; - } } - - return resource; - } + } /// /// Appends the dynamic properties of primitive, enum or the collection of them into the given . @@ -636,13 +758,27 @@ public virtual ODataResource CreateResource(SelectExpandNode selectExpandNode, R [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "These are simple conversion function and cannot be split up.")] public virtual void AppendDynamicProperties(ODataResource resource, SelectExpandNode selectExpandNode, ResourceContext resourceContext) + { + AppendDynamicPropertiesInternal(resource, selectExpandNode, resourceContext); + } + + /// + /// Appends the dynamic properties of primitive, enum or the collection of them into the given . + /// If the dynamic property is a property of the complex or collection of complex, it will be saved into + /// the dynamic complex properties dictionary of and be written later. + /// + /// The describing the resource. + /// The describing the response graph. + /// The context for the resource instance being written. + private void AppendDynamicPropertiesInternal(ODataResourceBase resource, SelectExpandNode selectExpandNode, + ResourceContext resourceContext) { Contract.Assert(resource != null); Contract.Assert(selectExpandNode != null); Contract.Assert(resourceContext != null); if (!resourceContext.StructuredType.IsOpen || // non-open type - (!selectExpandNode.SelectAllDynamicProperties && selectExpandNode.SelectedDynamicProperties == null)) + (!selectExpandNode.SelectAllDynamicProperties && selectExpandNode.SelectedDynamicProperties == null)) { return; } @@ -735,7 +871,7 @@ public virtual void AppendDynamicProperties(ODataResource resource, SelectExpand } else { - IODataEdmTypeSerializer propertySerializer = SerializerProvider.GetEdmTypeSerializer(edmTypeReference); + IODataEdmTypeSerializer propertySerializer =SerializerProvider.GetEdmTypeSerializer(edmTypeReference); if (propertySerializer == null) { throw Error.NotSupported(SRResources.DynamicPropertyCannotBeSerialized, dynamicProperty.Key, @@ -784,7 +920,7 @@ public virtual string CreateETag(ResourceContext resourceContext) foreach (IEdmStructuralProperty etagProperty in concurrencyProperties) { properties ??= new SortedDictionary(); - + properties.Add(etagProperty.Name, resourceContext.GetPropertyValue(etagProperty.Name)); } @@ -797,7 +933,6 @@ public virtual string CreateETag(ResourceContext resourceContext) return null; } - /// /// Write the navigation link for the select navigation properties. /// @@ -1142,7 +1277,7 @@ private IEnumerable CreateNavigationLinks( /// The navigation link to be written. /// The resource context for the resource whose navigation link is being written. /// true if navigation link should be written; otherwise false. - protected virtual bool ShouldWriteNavigation(ODataNestedResourceInfo navigationLink, ResourceContext resourceContext) + private bool ShouldWriteNavigation(ODataNestedResourceInfo navigationLink, ResourceContext resourceContext) { if (navigationLink?.Url != null || (navigationLink != null && resourceContext.SerializerContext.MetadataLevel == ODataMetadataLevel.Full)) { @@ -1252,6 +1387,13 @@ public virtual ODataNestedResourceInfo CreateNavigationLink(IEdmNavigationProper return navigationLink; } + /// + /// Creates the s to be written while writing this entity. + /// + /// The to determine the properties to be written + /// The context for the entity instance being written. + /// The navigation link to be written. + /// ODataProperties to be written private IEnumerable CreateStructuralPropertyBag(SelectExpandNode selectExpandNode, ResourceContext resourceContext) { Contract.Assert(selectExpandNode != null); @@ -1266,10 +1408,20 @@ private IEnumerable CreateStructuralPropertyBag(SelectExpandNode if (null != resourceContext.EdmObject && resourceContext.EdmObject.IsDeltaResource()) { - if (resourceContext.EdmObject is TypedEdmEntityObject obj && obj.Instance is IDelta deltaObject) + IDelta deltaObject = null; + if (resourceContext.EdmObject is TypedEdmEntityObject obj) + { + deltaObject = obj.Instance as IDelta; + } + else { + deltaObject = resourceContext.EdmObject as IDelta; + } + + if(deltaObject != null) + { IEnumerable changedProperties = deltaObject.GetChangedPropertyNames(); - structuralProperties = structuralProperties.Where(p => changedProperties.Contains(p.Name)); + structuralProperties = structuralProperties.Where(p => changedProperties.Contains(p.Name) || p.IsKey()); } } @@ -1734,7 +1886,7 @@ private static IEdmStructuredType GetODataPathType(ODataSerializerContext serial } } - internal static void AddTypeNameAnnotationAsNeeded(ODataResource resource, IEdmStructuredType odataPathType, + internal static void AddTypeNameAnnotationAsNeeded(ODataResourceBase resource, IEdmStructuredType odataPathType, ODataMetadataLevel metadataLevel) { // ODataLib normally has the caller decide whether or not to serialize properties by leaving properties @@ -1759,7 +1911,7 @@ internal static void AddTypeNameAnnotationAsNeeded(ODataResource resource, IEdmS resource.TypeAnnotation = new ODataTypeAnnotation(typeName); } - internal static void AddTypeNameAnnotationAsNeededForComplex(ODataResource resource, ODataMetadataLevel metadataLevel) + internal static void AddTypeNameAnnotationAsNeededForComplex(ODataResourceBase resource, ODataMetadataLevel metadataLevel) { // ODataLib normally has the caller decide whether or not to serialize properties by leaving properties // null when values should not be serialized. The TypeName property is different and should always be @@ -1833,7 +1985,7 @@ internal static bool ShouldOmitOperation(IEdmOperation operation, OperationLinkB } } - internal static bool ShouldSuppressTypeNameSerialization(ODataResource resource, IEdmStructuredType edmType, + internal static bool ShouldSuppressTypeNameSerialization(ODataResourceBase resource, IEdmStructuredType edmType, ODataMetadataLevel metadataLevel) { Contract.Assert(resource != null); @@ -1856,7 +2008,7 @@ internal static bool ShouldSuppressTypeNameSerialization(ODataResource resource, } } - private IEdmStructuredTypeReference GetResourceType(object graph, ODataSerializerContext writeContext) + internal static IEdmStructuredTypeReference GetResourceType(object graph, ODataSerializerContext writeContext) { Contract.Assert(graph != null); @@ -1871,9 +2023,14 @@ private IEdmStructuredTypeReference GetResourceType(object graph, ODataSerialize if (!edmType.IsStructured()) { throw new SerializationException( - Error.Format(SRResources.CannotWriteType, GetType().Name, edmType.FullName())); + Error.Format(SRResources.CannotWriteType, typeof(ODataResourceSerializer).Name, edmType.FullName())); } return edmType.AsStructured(); } + + private bool IsDeltaCollection(object collection) + { + return (collection is IDeltaSet || collection is EdmChangedObjectCollection); + } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSetSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSetSerializer.cs index bcb53069e..5afccc1ee 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSetSerializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSetSerializer.cs @@ -66,7 +66,7 @@ public override async Task WriteObjectAsync(object graph, Type type, ODataMessag Contract.Assert(resourceSetType != null); IEdmStructuredTypeReference resourceType = GetResourceType(resourceSetType); - + ODataWriter writer = await messageWriter.CreateODataResourceSetWriterAsync(entitySet, resourceType.StructuredDefinition()) .ConfigureAwait(false); await WriteObjectInlineAsync(graph, resourceSetType, writer, writeContext) @@ -167,7 +167,7 @@ private async Task WriteResourceSetAsync(IAsyncEnumerable asyncEnumerabl Func nextLinkGenerator = GetNextLinkGenerator(resourceSet, asyncEnumerable, writeContext); WriteResourceSetInternal(resourceSet, elementType, resourceSetType, writeContext, out bool isUntypedCollection, out IODataEdmTypeSerializer resourceSerializer); - + await writer.WriteStartAsync(resourceSet).ConfigureAwait(false); object lastResource = null; @@ -427,20 +427,7 @@ public virtual ODataResourceSet CreateResourceSet(IEnumerable resourceSetInstanc WriteEntityTypeOperations(resourceSet, resourceSetContext, structuredType, writeContext); } - if (writeContext.ExpandedResource == null) - { - // If we have more OData format specific information apply it now, only if we are the root feed. - PageResult odataResourceSetAnnotations = resourceSetInstance as PageResult; - ApplyODataResourceSetAnnotations(resourceSet, odataResourceSetAnnotations, writeContext); - } - else - { - ICountOptionCollection countOptionCollection = resourceSetInstance as ICountOptionCollection; - if (countOptionCollection != null && countOptionCollection.TotalCount != null) - { - resourceSet.Count = countOptionCollection.TotalCount; - } - } + WriteResourceSetInformation(resourceSet, resourceSetInstance, writeContext); return resourceSet; } @@ -472,20 +459,7 @@ public virtual ODataResourceSet CreateResourceSet(IAsyncEnumerable resou WriteEntityTypeOperations(resourceSet, resourceSetContext, structuredType, writeContext); } - if (writeContext.ExpandedResource == null) - { - // If we have more OData format specific information apply it now, only if we are the root feed. - PageResult odataResourceSetAnnotations = resourceSetInstance as PageResult; - ApplyODataResourceSetAnnotations(resourceSet, odataResourceSetAnnotations, writeContext); - } - else - { - ICountOptionCollection countOptionCollection = resourceSetInstance as ICountOptionCollection; - if (countOptionCollection != null && countOptionCollection.TotalCount != null) - { - resourceSet.Count = countOptionCollection.TotalCount; - } - } + WriteResourceSetInformation(resourceSet, resourceSetInstance, writeContext); return resourceSet; } @@ -513,6 +487,27 @@ private void WriteEntityTypeOperations( } } + private void WriteResourceSetInformation( + ODataResourceSet resourceSet, + object resourceSetInstance, + ODataSerializerContext writeContext) + { + if (writeContext.ExpandedResource == null) + { + // If we have more OData format specific information apply it now, only if we are the root feed. + PageResult odataResourceSetAnnotations = resourceSetInstance as PageResult; + ApplyODataResourceSetAnnotations(resourceSet, odataResourceSetAnnotations, writeContext); + } + else + { + ICountOptionCollection countOptionCollection = resourceSetInstance as ICountOptionCollection; + if (countOptionCollection != null && countOptionCollection.TotalCount != null) + { + resourceSet.Count = countOptionCollection.TotalCount; + } + } + } + private void ApplyODataResourceSetAnnotations( ODataResourceSet resourceSet, PageResult odataResourceSetAnnotations, diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmObjectExtensions.cs b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmObjectExtensions.cs index 5082e15ec..50d306928 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmObjectExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmObjectExtensions.cs @@ -19,7 +19,7 @@ public static class EdmTypeExtensions /// Method to determine whether the current type is a Delta resource set. /// /// IEdmType to be compared - /// True or False if type is same as + /// True or False if type is a or a public static bool IsDeltaResourceSet(this IEdmType type) { if (type == null) @@ -27,7 +27,7 @@ public static bool IsDeltaResourceSet(this IEdmType type) throw Error.ArgumentNull(nameof(type)); } - return (type.GetType() == typeof(EdmDeltaCollectionType)); + return (type is EdmDeltaCollectionType || type is IDeltaSet); } /// diff --git a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.csproj b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.csproj index 695ab03b3..6aa7c2ffc 100644 --- a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.csproj +++ b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -29,9 +29,9 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml index 2ec713bde..a569624f9 100644 --- a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml +++ b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml @@ -4525,7 +4525,7 @@ - + Creates a function that takes in an object and generates nextlink uri. @@ -4553,6 +4553,16 @@ The to be used for writing. The . + + + Writes the given deltaDeletedEntry specified by the parameter graph as a part of an existing OData message using the given + messageWriter and the writeContext. + + The object to be written. + The expected type of the deleted resource. + The to be used for writing. + The . + Writes the given deltaDeletedLink specified by the parameter graph as a part of an existing OData message using the given @@ -4764,6 +4774,7 @@ The ODataWriter. A task that represents the asynchronous write operation + Creates the that describes the set of properties and actions to select and expand while writing this entity. @@ -4781,6 +4792,24 @@ The context for the resource instance being written. The created . + + + Creates the to be written while writing this resource. + + The id of the Deleted Resource to be written (may be null if properties contains all key properties) + The for the removal of the resource. + The describing the response graph. + The context for the resource instance being written. + The created . + + + + Initializes an ODataResource to be written + + The describing the response graph. + The resource that will be initialized + The context for the resource instance being written. + Appends the dynamic properties of primitive, enum or the collection of them into the given . @@ -4791,6 +4820,16 @@ The describing the response graph. The context for the resource instance being written. + + + Appends the dynamic properties of primitive, enum or the collection of them into the given . + If the dynamic property is a property of the complex or collection of complex, it will be saved into + the dynamic complex properties dictionary of and be written later. + + The describing the resource. + The describing the response graph. + The context for the resource instance being written. + Creates the ETag for the given entity. @@ -4857,6 +4896,7 @@ The context for the entity instance being written. The navigation link to be written. + Creates the to be written for the given resource. @@ -5799,7 +5839,7 @@ Method to determine whether the current type is a Delta resource set. IEdmType to be compared - True or False if type is same as + True or False if type is a or a @@ -7053,11 +7093,6 @@ Looks up a localized string similar to The Uri '{0}' in the parameter is invalid.. - - - Looks up a localized string similar to The related entity set could not be found from the OData path. The related entity set is required to serialize the payload.. - - Looks up a localized string similar to The entity type '{0}' does not match the expected entity type '{1}' as set on the query context.. diff --git a/src/Microsoft.AspNetCore.OData/Properties/SRResources.Designer.cs b/src/Microsoft.AspNetCore.OData/Properties/SRResources.Designer.cs index 0c6fdbb8f..1e08d2a2c 100644 --- a/src/Microsoft.AspNetCore.OData/Properties/SRResources.Designer.cs +++ b/src/Microsoft.AspNetCore.OData/Properties/SRResources.Designer.cs @@ -627,15 +627,6 @@ internal static string EntityReferenceMustHasKeySegment { } } - /// - /// Looks up a localized string similar to The related entity set could not be found from the OData path. The related entity set is required to serialize the payload.. - /// - internal static string EntitySetMissingDuringSerialization { - get { - return ResourceManager.GetString("EntitySetMissingDuringSerialization", resourceCulture); - } - } - /// /// Looks up a localized string similar to The entity type '{0}' does not match the expected entity type '{1}' as set on the query context.. /// diff --git a/src/Microsoft.AspNetCore.OData/Properties/SRResources.resx b/src/Microsoft.AspNetCore.OData/Properties/SRResources.resx index 0f72c1dc9..9939b4be1 100644 --- a/src/Microsoft.AspNetCore.OData/Properties/SRResources.resx +++ b/src/Microsoft.AspNetCore.OData/Properties/SRResources.resx @@ -597,9 +597,6 @@ The type '{0}' is not supported by the ODataErrorSerializer. The type must be ODataError or HttpError. - - The related entity set could not be found from the OData path. The related entity set is required to serialize the payload. - A segment '{0}' within the select or expand query option is not supported. diff --git a/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt b/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt index e69de29bb..ae1569e03 100644 --- a/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt +++ b/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt @@ -0,0 +1,15 @@ +Microsoft.AspNetCore.OData.Formatter.Serialization.ODataDeletedResourceSerializer +Microsoft.AspNetCore.OData.Formatter.Serialization.ODataDeletedResourceSerializer.ODataDeletedResourceSerializer(Microsoft.AspNetCore.OData.Formatter.Serialization.IODataSerializerProvider serializerProvider) -> void +Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.AppendDynamicPropertiesInternal(Microsoft.OData.ODataResourceBase resource, Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode selectExpandNode, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) -> void +Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.CreateStructuralPropertyBag(Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode selectExpandNode, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext, System.Func createStructuralProperty, System.Func createComputedProperty) -> System.Collections.Generic.IEnumerable +Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.InitializeODataResource(Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode selectExpandNode, Microsoft.OData.ODataResourceBase resource, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) -> void +Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.WriteResourceContent(Microsoft.OData.ODataWriter writer, Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode selectExpandNode, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext, bool isDelta) -> System.Threading.Tasks.Task +override Microsoft.AspNetCore.OData.Formatter.Serialization.ODataDeletedResourceSerializer.WriteObjectAsync(object graph, System.Type type, Microsoft.OData.ODataMessageWriter messageWriter, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) -> System.Threading.Tasks.Task +override Microsoft.AspNetCore.OData.Formatter.Serialization.ODataDeletedResourceSerializer.WriteObjectInlineAsync(object graph, Microsoft.OData.Edm.IEdmTypeReference expectedType, Microsoft.OData.ODataWriter writer, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) -> System.Threading.Tasks.Task +virtual Microsoft.AspNetCore.OData.Formatter.Serialization.ODataDeletedResourceSerializer.AppendDynamicProperties(Microsoft.OData.ODataDeletedResource resource, Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode selectExpandNode, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) -> void +virtual Microsoft.AspNetCore.OData.Formatter.Serialization.ODataDeletedResourceSerializer.CreateComputedProperty(string propertyName, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) -> Microsoft.OData.ODataProperty +virtual Microsoft.AspNetCore.OData.Formatter.Serialization.ODataDeletedResourceSerializer.CreateDeletedResource(System.Uri id, Microsoft.OData.DeltaDeletedEntryReason reason, Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode selectExpandNode, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) -> Microsoft.OData.ODataDeletedResource +virtual Microsoft.AspNetCore.OData.Formatter.Serialization.ODataDeletedResourceSerializer.CreateETag(Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) -> string +virtual Microsoft.AspNetCore.OData.Formatter.Serialization.ODataDeletedResourceSerializer.CreateSelectExpandNode(Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) -> Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode +virtual Microsoft.AspNetCore.OData.Formatter.Serialization.ODataDeletedResourceSerializer.CreateStructuralProperty(Microsoft.OData.Edm.IEdmStructuralProperty structuralProperty, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) -> Microsoft.OData.ODataProperty +virtual Microsoft.AspNetCore.OData.Formatter.Serialization.ODataDeltaResourceSetSerializer.WriteDeletedResourceAsync(object value, Microsoft.OData.Edm.IEdmStructuredTypeReference expectedType, Microsoft.OData.ODataWriter writer, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) -> System.Threading.Tasks.Task \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/BulkOperation/BulkOperationTest.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/BulkOperation/BulkOperationTest.cs index 2f31510d6..f66ec18cd 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/BulkOperation/BulkOperationTest.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/BulkOperation/BulkOperationTest.cs @@ -47,7 +47,7 @@ public async Task DeltaSet_WithNestedFriends_WithNestedOrders_IsSerializedSucces ]}"; string expectedResponse = "{" + - "\"@context\":\"http://localhost/convention/$metadata#Employees/$delta\"," + + "\"@odata.context\":\"http://localhost/convention/$metadata#Employees/$delta\"," + "\"value\":[" + "{\"ID\":1,\"Name\":\"Employee1\",\"Friends@delta\":[{\"Id\":1,\"Name\":\"Friend1\",\"Orders@delta\":[{\"Id\":1,\"Price\":10},{\"Id\":2,\"Price\":20}]},{\"Id\":2,\"Name\":\"Friend2\"}]}," + "{\"ID\":2,\"Name\":\"Employee2\",\"Friends@delta\":[{\"Id\":3,\"Name\":\"Friend3\",\"Orders@delta\":[{\"Id\":3,\"Price\":30},{\"Id\":4,\"Price\":40}]},{\"Id\":4,\"Name\":\"Friend4\"}]}]}"; @@ -75,7 +75,7 @@ public async Task DeltaSet_WithDeletedAndODataId_IsSerializedSuccessfully() {'ID':2,'Name':'Employee2','Friends@odata.delta':[{'@id':'Friends(1)'}]} ]}"; - string expectedResponse = "{\"@context\":\"http://localhost/convention/$metadata#Employees/$delta\",\"value\":[{\"ID\":1,\"Name\":\"Employee1\",\"Friends@delta\":[{\"@removed\":{\"reason\":\"changed\"},\"@id\":\"http://host/service/Friends(1)\"}]},{\"ID\":2,\"Name\":\"Employee2\",\"Friends@delta\":[{\"Id\":1}]}]}"; + string expectedResponse = "{\"@odata.context\":\"http://localhost/convention/$metadata#Employees/$delta\",\"value\":[{\"ID\":1,\"Name\":\"Employee1\",\"Friends@delta\":[{\"@odata.removed\":{\"reason\":\"changed\"},\"@odata.id\":\"http://host/service/Friends(1)\",\"id\":1}]},{\"ID\":2,\"Name\":\"Employee2\",\"Friends@delta\":[{\"Id\":1}]}]}"; var requestForUpdate = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DeltaToken/DeltaTokenQueryTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DeltaToken/DeltaTokenQueryTests.cs index f04856b46..9db00d571 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DeltaToken/DeltaTokenQueryTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DeltaToken/DeltaTokenQueryTests.cs @@ -53,7 +53,7 @@ public async Task DeltaVerifyReslt() Assert.True(results.value.Count == 7, "There should be 7 entries in the response"); var changeEntity = results.value[0]; - Assert.True(((JToken)changeEntity).Count() == 9, "The changed customer should have 6 properties plus type written. But now it contains non-changed properties, it's regression bug?"); + Assert.True(((JToken)changeEntity).Count() == 7, "The changed customer should have 6 properties plus type written. But now it contains non-changed properties, it's regression bug?"); string changeEntityType = changeEntity["@type"].Value as string; Assert.True(changeEntityType != null, "The changed customer should have type written"); Assert.True(changeEntityType.Contains("#Microsoft.AspNetCore.OData.E2E.Tests.DeltaToken.TestCustomerWithAddress"), "The changed order should be a TestCustomerWithAddress"); @@ -61,7 +61,7 @@ public async Task DeltaVerifyReslt() Assert.True(changeEntity.OpenProperty.Value == 10, "The OpenProperty property of changed customer should be 10."); Assert.True(changeEntity.NullOpenProperty.Value == null, "The NullOpenProperty property of changed customer should be null."); Assert.True(changeEntity.Name.Value == "Name", "The Name of changed customer should be 'Name'"); - Assert.True(((JToken)changeEntity.Address).Count() == 3, "The changed entity's Address should have 2 properties written. But now it contains non-changed properties, it's regression bug?"); + Assert.True(((JToken)changeEntity.Address).Count() == 2, "The changed entity's Address should have 2 properties written. But now it contains non-changed properties, it's regression bug?"); Assert.True(changeEntity.Address.State.Value == "State", "The changed customer's Address.State should be 'State'."); Assert.True(changeEntity.Address.ZipCode.Value == (int?)null, "The changed customer's Address.ZipCode should be null."); @@ -71,7 +71,7 @@ public async Task DeltaVerifyReslt() Assert.True(phoneNumbers[1].Value == "765-4321", "The second phone number should be '765-4321'"); var newCustomer = results.value[1]; - Assert.True(((JToken)newCustomer).Count() == 5, "The new customer should have 3 properties written, But now it contains 2 non-changed properties, it's regression bug?"); + Assert.True(((JToken)newCustomer).Count() == 3, "The new customer should have 3 properties written, But now it contains 2 non-changed properties, it's regression bug?"); Assert.True(newCustomer.Id.Value == 10, "The ID of the new customer should be 10"); Assert.True(newCustomer.Name.Value == "NewCustomer", "The name of the new customer should be 'NewCustomer'"); @@ -79,7 +79,7 @@ public async Task DeltaVerifyReslt() Assert.True(((JToken)places).Count() == 2, "The new customer should have 2 favorite places"); var place1 = places[0]; - Assert.True(((JToken)place1).Count() == 3, "The first favorite place should have 2 properties written.But now it contains non-changed properties, it's regression bug?"); + Assert.True(((JToken)place1).Count() == 2, "The first favorite place should have 2 properties written.But now it contains non-changed properties, it's regression bug?"); Assert.True(place1.State.Value == "State", "The first favorite place's state should be 'State'."); Assert.True(place1.ZipCode.Value == (int?)null, "The first favorite place's Address.ZipCode should be null."); @@ -92,7 +92,7 @@ public async Task DeltaVerifyReslt() Assert.True(place2.NullOpenProperty.Value == null, "The second favorite place's Address.NullOpenProperty should be null."); var newOrder = results.value[2]; - Assert.True(((JToken)newOrder).Count() == 4, "The new order should have 2 properties plus context written, , But now it contains one non-changed properties, it's regression bug?"); + Assert.True(((JToken)newOrder).Count() == 3, "The new order should have 2 properties plus context written, , But now it contains one non-changed properties, it's regression bug?"); string newOrderContext = newOrder["@context"].Value as string; Assert.True(newOrderContext != null, "The new order should have a context written"); Assert.True(newOrderContext.Contains("$metadata#TestOrders"), "The new order should come from the TestOrders entity set"); @@ -139,13 +139,11 @@ public async Task DeltaVerifyReslt_ContainsDynamicComplexProperties() "\"Amount\":42," + "\"Location\":{" + "\"State\":\"State\"," + - "\"City\":null," + "\"ZipCode\":null," + "\"OpenProperty\":10," + "\"key-samplelist\":{" + "\"@type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.DeltaToken.TestAddress\"," + "\"State\":\"sample state\"," + - "\"City\":null," + "\"ZipCode\":9," + "\"title\":\"sample title\"" + "}" + diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Typeless/TypelessDeltaSerializationTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Typeless/TypelessDeltaSerializationTests.cs index b5572fd93..cecca1bd2 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Typeless/TypelessDeltaSerializationTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Typeless/TypelessDeltaSerializationTests.cs @@ -59,6 +59,7 @@ public async Task TypelessDeltaWorksInAllFormats(string acceptHeader) HttpClient client = CreateClient(); HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url); request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(acceptHeader)); + request.Headers.Add("OData-Version", "4.01"); // Act HttpResponseMessage response = await client.SendAsync(request); diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataFormatterHelpers.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataFormatterHelpers.cs index 10e04854e..ed7a6c6fa 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataFormatterHelpers.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataFormatterHelpers.cs @@ -157,7 +157,7 @@ private static IServiceProvider BuildServiceProvider() services.AddSingleton(); services.AddSingleton(); - // services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataTestUtil.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataTestUtil.cs index 03b0153df..1971fdc7a 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataTestUtil.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataTestUtil.cs @@ -18,16 +18,18 @@ namespace Microsoft.AspNetCore.OData.Tests.Formatter; internal static class ODataTestUtil { - public static ODataMessageWriter GetMockODataMessageWriter() + public static ODataMessageWriter GetMockODataMessageWriter(ODataVersion version = ODataVersion.V4) { MockODataRequestMessage requestMessage = new MockODataRequestMessage(); - return new ODataMessageWriter(requestMessage); + ODataMessageWriterSettings settings = new ODataMessageWriterSettings(version); + return new ODataMessageWriter(requestMessage, settings); } - public static ODataMessageReader GetMockODataMessageReader() + public static ODataMessageReader GetMockODataMessageReader(ODataVersion version = ODataVersion.V4) { MockODataRequestMessage requestMessage = new MockODataRequestMessage(); - return new ODataMessageReader(requestMessage); + ODataMessageReaderSettings settings = new ODataMessageReaderSettings(version); + return new ODataMessageReader(requestMessage, settings); } public static IODataSerializerProvider GetMockODataSerializerProvider(ODataEdmTypeSerializer serializer) diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataDeletedResourceSerializerTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataDeletedResourceSerializerTests.cs new file mode 100644 index 000000000..cad21f9d1 --- /dev/null +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataDeletedResourceSerializerTests.cs @@ -0,0 +1,2705 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.Serialization; +using System.Threading.Tasks; +using Microsoft.AspNetCore.OData.Abstracts; +using Microsoft.AspNetCore.OData.Deltas; +using Microsoft.AspNetCore.OData.Edm; +using Microsoft.AspNetCore.OData.Extensions; +using Microsoft.AspNetCore.OData.Formatter; +using Microsoft.AspNetCore.OData.Formatter.Serialization; +using Microsoft.AspNetCore.OData.Formatter.Value; +using Microsoft.AspNetCore.OData.Tests.Commons; +using Microsoft.AspNetCore.OData.Tests.Edm; +using Microsoft.AspNetCore.OData.Tests.Extensions; +using Microsoft.AspNetCore.OData.Tests.Models; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Net.Http.Headers; +using Microsoft.OData; +using Microsoft.OData.Edm; +using Microsoft.OData.Edm.Vocabularies; +using Microsoft.OData.ModelBuilder; +using Microsoft.OData.UriParser; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization; + +public class ODataDeletedResourceSerializerTests +{ + private IEdmModel _model; + private IEdmEntitySet _customerSet; + private IEdmEntitySet _orderSet; + private DeltaDeletedResource _customer; + private DeltaDeletedResource _order; + private ODataResourceSerializer _serializer; + private ODataSerializerContext _writeContext; + private ResourceContext _entityContext; + private IODataSerializerProvider _serializerProvider; + private IEdmEntityTypeReference _customerType; + private IEdmEntityTypeReference _orderType; + private IEdmEntityTypeReference _specialCustomerType; + private IEdmEntityTypeReference _specialOrderType; + private IEdmComplexTypeReference _sizeType; + private IEdmNavigationProperty _ordersNavigation; + private DeltaSet _orders; + private Size _size; + private ODataPath _path; + + public ODataDeletedResourceSerializerTests() + { + _model = SerializationTestsHelpers.SimpleCustomerOrderModel(); + + _model.SetAnnotationValue(_model.FindType("Default.Customer"), new ClrTypeAnnotation(typeof(Customer))); + _model.SetAnnotationValue(_model.FindType("Default.Order"), new ClrTypeAnnotation(typeof(Order))); + _model.SetAnnotationValue(_model.FindType("Default.SpecialCustomer"), new ClrTypeAnnotation(typeof(SpecialCustomer))); + _model.SetAnnotationValue(_model.FindType("Default.SpecialOrder"), new ClrTypeAnnotation(typeof(SpecialOrder))); + _model.SetAnnotationValue(_model.FindType("Default.Size"), new ClrTypeAnnotation(typeof(Size))); + + _customerSet = _model.EntityContainer.FindEntitySet("Customers"); + _customer = new DeltaDeletedResource(); + _customer.TrySetPropertyValue("ID", 10); + _customer.TrySetPropertyValue("FirstName", "Foo"); + _customer.TrySetPropertyValue("LastName", "Bar"); + _customer.Id = new Uri("http://customers/10"); + + _size = new Size { Height = 72, Weight = 180 }; + _customer.TrySetPropertyValue("Size", _size); + + _orderSet = _model.EntityContainer.FindEntitySet("Orders"); + _order = new DeltaDeletedResource{Id = new Uri("http://customers/1")}; + _order.TrySetPropertyValue("ID", 20); + + _orders = new DeltaSet { _order }; + _customer.TrySetPropertyValue("Orders", _orders); + + _serializerProvider = GetServiceProvider().GetService(); + _customerType = _model.GetEdmTypeReference(typeof(Customer)).AsEntity(); + _orderType = _model.GetEdmTypeReference(typeof(Order)).AsEntity(); + _specialCustomerType = _model.GetEdmTypeReference(typeof(SpecialCustomer)).AsEntity(); + _specialOrderType = _model.GetEdmTypeReference(typeof(SpecialOrder)).AsEntity(); + _sizeType = _model.GetEdmTypeReference(typeof(Size)).AsComplex(); + _ordersNavigation = _customerType.FindNavigationProperty("Orders"); + _serializer = new ODataResourceSerializer(_serializerProvider); + _path = new ODataPath(new EntitySetSegment(_customerSet)); + _writeContext = new ODataSerializerContext() { NavigationSource = _customerSet, Model = _model, Path = _path, Type = typeof(DeltaDeletedResource) }; + _entityContext = new ResourceContext(_writeContext, _customerSet.EntityType.AsReference(), _customer); + } + + [Fact] + public void Ctor_ThrowsArgumentNull_SerializerProvider() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new ODataResourceSerializer(serializerProvider: null), "serializerProvider"); + } + + [Fact] + public async Task WriteObjectAsync_ThrowsArgumentNull_MessageWriter() + { + // Arrange & Act + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + + // Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => serializer.WriteObjectAsync(graph: _customer, type: typeof(Customer), messageWriter: null, writeContext: null), + "messageWriter"); + } + + [Fact] + public async Task WriteObjectAsync_ThrowsArgumentNull_WriteContext() + { + // Arrange & Act + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + ODataMessageWriter messageWriter = new ODataMessageWriter(new Mock().Object); + + // Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => serializer.WriteObjectAsync(graph: _customer, type: typeof(Customer), messageWriter: messageWriter, writeContext: null), + "writeContext"); + } + + [Fact] + public async Task WriteDeletedResourceCallsWriteDeltaDeletedResourceAsyncIfOverridden() + { + // Arrange + Mock serializerProvider = new Mock(); + var serializer = new Mock(serializerProvider.Object); + serializer.Setup(s=>s.WriteDeltaDeletedResourceAsync(_orders.First(),It.IsAny(), It.IsAny())) + .Verifiable(); + serializer.CallBase = true; + + // Act + await serializer.Object.WriteObjectAsync(_orders, typeof(DeltaSet), ODataTestUtil.GetMockODataMessageWriter(ODataVersion.V401), _writeContext); + + // Assert + serializer.Verify(); + } + + public class MyDeltaResourceSetSerializer : ODataDeltaResourceSetSerializer + { + public MyDeltaResourceSetSerializer(IODataSerializerProvider serializerProvider) : base(serializerProvider) { } + + public override async Task WriteDeltaDeletedResourceAsync(object value, ODataWriter writer, ODataSerializerContext writeContext) + { + await base.WriteDeltaDeletedResourceAsync(value, writer, writeContext).ConfigureAwait(false); + } + } + + [Fact] + public async Task WriteObjectAsync_Calls_WriteObjectInline_WithRightEntityType() + { + // Arrange + Mock serializerProvider = new Mock(); + serializerProvider.Setup(s => + s.GetEdmTypeSerializer(It.Is(e => _orderType.Definition == e.Definition))).Returns(new ODataResourceSerializer(serializerProvider.Object)); + serializerProvider.Setup(s => + s.GetEdmTypeSerializer(It.IsAny())).Returns(new ODataPrimitiveSerializer()); + Mock serializer = new Mock(serializerProvider.Object); + serializer + .Setup(s => s.WriteObjectInlineAsync(_customer, It.Is(e => _customerType.Definition == e.Definition), + It.IsAny(), _writeContext)) + .Verifiable(); + serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(new SelectExpandNode()); + serializer.CallBase = true; + + // Act + await serializer.Object.WriteObjectAsync(_customer, typeof(Customer), ODataTestUtil.GetMockODataMessageWriter(ODataVersion.V401), _writeContext); + + // Assert + serializer.Verify(); + } + + [Fact] + public async Task WriteObjectInlineAsync_ThrowsArgumentNull_Writer() + { + // Arrange & Act + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + + // Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => serializer.WriteObjectInlineAsync(graph: null, expectedType: null, writer: null, writeContext: new ODataSerializerContext()), + "writer"); + } + + [Fact] + public async Task WriteObjectInlineAsync_ThrowsArgumentNull_WriteContext() + { + // Arrange & Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => _serializer.WriteObjectInlineAsync(graph: null, expectedType: null, writer: new Mock().Object, writeContext: null), + "writeContext"); + } + + [Fact] + public async Task WriteObjectInlineAsync_ThrowsSerializationException_WhenGraphIsNull() + { + // Arrange & Act + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + ODataWriter messageWriter = new Mock().Object; + + // Assert + await ExceptionAssert.ThrowsAsync( + () => serializer.WriteObjectInlineAsync(graph: null, expectedType: null, writer: messageWriter, writeContext: new ODataSerializerContext()), + "Cannot serialize a null 'Resource'."); + } + + [Fact] + public async Task WriteObjectInlineAsync_Calls_CreateSelectExpandNode() + { + // Arrange + Mock serializerProvider = new Mock(); + serializerProvider.Setup(s => s.GetEdmTypeSerializer(It.IsAny())).Returns(new ODataResourceSerializer(serializerProvider.Object)); + serializerProvider.Setup(s => s.GetEdmTypeSerializer(It.IsAny())).Returns(new ODataPrimitiveSerializer()); + Mock serializer = new Mock(serializerProvider.Object); + ODataWriter writer = new Mock().Object; + + serializer.Setup(s => s.CreateSelectExpandNode(It.Is(e => + e.StructuredType == _customer ? Verify(e, _customer, _writeContext) : + e.StructuredType == _sizeType ? Verify(e, _size, _writeContext) : true))).Verifiable(); + serializer.CallBase = true; + + // Act + await serializer.Object.WriteObjectInlineAsync(_customer, _customerType, writer, _writeContext); + + // Assert + serializer.Verify(); + } + + [Fact] + public async Task WriteObjectInlineAsync_Calls_CreateDeletedResource() + { + // Arrange + SelectExpandNode selectExpandNode = new SelectExpandNode(); + Mock serializerProvider = new Mock(); + serializerProvider.Setup(s => s.GetEdmTypeSerializer(It.IsAny())).Returns(new ODataResourceSerializer(serializerProvider.Object)); + serializerProvider.Setup(s => s.GetEdmTypeSerializer(It.IsAny())).Returns(new ODataPrimitiveSerializer()); + Mock serializer = new Mock(serializerProvider.Object); + ODataWriter writer = new Mock().Object; + + serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); + serializer.Setup(s => s.CreateDeletedResource(It.IsAny(), DeltaDeletedEntryReason.Deleted, selectExpandNode, It.Is(e => Verify(e, _customer, _writeContext)))).Verifiable(); + serializer.CallBase = true; + + // Act + await serializer.Object.WriteObjectInlineAsync(_customer, _customerType, writer, _writeContext); + + // Assert + serializer.Verify(); + } + + [Fact] + public async Task WriteObjectInlineAsync_WritesODataResourceFrom_CreateResource() + { + // Arrange + ODataDeletedResource entry = new ODataDeletedResource(); + Mock serializerProvider = new Mock(); + serializerProvider.Setup(s => + s.GetEdmTypeSerializer(It.Is(e => _orderType.Definition == e.Definition))).Returns(new ODataResourceSerializer(serializerProvider.Object)); + serializerProvider.Setup(s => + s.GetEdmTypeSerializer(It.IsAny())).Returns(new ODataPrimitiveSerializer()); + Mock serializer = new Mock(serializerProvider.Object); + Mock writer = new Mock(); + + serializer.Setup(s => s.CreateDeletedResource(It.IsAny(), DeltaDeletedEntryReason.Deleted, It.IsAny(), It.IsAny())).Returns(entry); + serializer.CallBase = true; + + writer.Setup(s => s.WriteStartAsync(entry)).Verifiable(); + + // Act + await serializer.Object.WriteObjectInlineAsync(_customer, _customerType, writer.Object, _writeContext); + + // Assert + writer.Verify(); + } + + [Fact] + public async Task WriteObjectInlineAsync_Calls_CreateUntypedPropertyValue_ForUntypedProperty() + { + // Arrange + Mock untyped = new Mock(); + untyped.Setup(u => u.TypeKind).Returns(EdmTypeKind.Untyped); + + Mock untypedRef = new Mock(); + untypedRef.Setup(p => p.Definition).Returns(untyped.Object); + + Mock property1 = new Mock(); + property1.Setup(p => p.Type).Returns(untypedRef.Object); + + SelectExpandNode selectExpandNode = new SelectExpandNode + { + SelectedStructuralProperties = new HashSet + { + property1.Object + } + }; + + Mock writer = new Mock(); + Mock serializer = new Mock(_serializerProvider); + serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); + serializer.CallBase = true; + IEdmTypeReference a; + + serializer.Setup(s => s.CreateUntypedPropertyValue(property1.Object, It.IsAny(), out a)).Returns("a").Verifiable(); + + // Act + await serializer.Object.WriteObjectInlineAsync(_customer, _customerType, writer.Object, _writeContext); + + // Assert + serializer.Verify(); + } + + [Fact] + public async Task WriteObjectInlineAsync_Calls_CreateComplexNestedResourceInfo_ForEachSelectedComplexProperty() + { + // Arrange + SelectExpandNode selectExpandNode = new SelectExpandNode + { + SelectedComplexProperties = new Dictionary + { + { new Mock( + (IEdmStructuredType)_customerType.Definition, + "Size", + _sizeType).Object, + null + }, + } + }; + + Mock writer = new Mock(); + Mock serializer = new Mock(_serializerProvider); + serializer.Setup(s => s.CreateSelectExpandNode(It.Is(c=>c.StructuredType == _customerType.Definition))).Returns(selectExpandNode); + serializer.CallBase = true; + + serializer.Setup(s => s.CreateComplexNestedResourceInfo(selectExpandNode.SelectedComplexProperties.ElementAt(0).Key, null, It.IsAny())).Verifiable(); + + // Act + await serializer.Object.WriteObjectInlineAsync(_customer, _customerType, writer.Object, _writeContext); + + // Assert + serializer.Verify(); + } + + [Fact] + public async Task WriteObjectInlineAsync_Calls_CreateNavigationLink_ForEachSelectedNavigationProperty() + { + // Arrange + SelectExpandNode selectExpandNode = new SelectExpandNode + { + SelectedNavigationProperties = new HashSet + { + _ordersNavigation + } + }; + Mock writer = new Mock(); + Mock serializer = new Mock(_serializerProvider); + serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); + serializer.CallBase = true; + + serializer.Setup(s => s.CreateNavigationLink(selectExpandNode.SelectedNavigationProperties.ElementAt(0), It.IsAny())).Verifiable(); + + ODataSerializerContext writeContext = new ODataSerializerContext() { NavigationSource = _customerSet, Model = _model, Path = _path, Type = typeof(DeltaDeletedResource) }; + writeContext.MetadataLevel = ODataMetadataLevel.Full; + + // Act + await serializer.Object.WriteObjectInlineAsync(_customer, _customerType, writer.Object, _writeContext); + + // Assert + serializer.Verify(); + } + + [Fact] + public async Task WriteObjectInlineAsync_WritesNavigationLinksReturnedBy_CreateNavigationLink_ForEachSelectedNavigationProperty() + { + // Arrange + SelectExpandNode selectExpandNode = new SelectExpandNode + { + SelectedNavigationProperties = new HashSet + { + _ordersNavigation + } + }; + ODataNestedResourceInfo[] navigationLinks = new[] + { + new ODataNestedResourceInfo() + }; + Mock serializer = new Mock(_serializerProvider); + serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); + serializer + .Setup(s => s.CreateNavigationLink(selectExpandNode.SelectedNavigationProperties.ElementAt(0), It.IsAny())) + .Returns(navigationLinks[0]); + serializer.CallBase = true; + + Mock writer = new Mock(); + writer.Setup(w => w.WriteStartAsync(navigationLinks[0])).Verifiable(); + + ODataSerializerContext writeContext = new ODataSerializerContext() { NavigationSource = _customerSet, Model = _model, Path = _path, Type = typeof(DeltaDeletedResource) }; + writeContext.MetadataLevel = ODataMetadataLevel.Full; + + // Act + await serializer.Object.WriteObjectInlineAsync(_customer, _customerType, writer.Object, writeContext); + + // Assert + writer.Verify(); + } + + [Fact] + public async Task WriteObjectInlineAsync_Calls_CreateNavigationLink_ForEachExpandedNavigationProperty() + { + // Arrange + SelectExpandNode selectExpandNode = new SelectExpandNode + { + ExpandedProperties = new Dictionary + { + { _ordersNavigation, null }, + } + }; + Mock writer = new Mock(); + Mock serializer = new Mock(_serializerProvider); + serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); + var expandedNavigationProperties = selectExpandNode.ExpandedProperties.Keys; + + serializer.Setup(s => s.CreateNavigationLink(expandedNavigationProperties.First(), It.IsAny())).Verifiable(); + serializer.CallBase = true; + + // Act + await serializer.Object.WriteObjectInlineAsync(_customer, _customerType, writer.Object, _writeContext); + + // Assert + serializer.Verify(); + } + + [Fact] + public async Task WriteObjectInlineAsync_ExpandsUsingInnerSerializerUsingRightContext_ExpandedNavigationProperties() + { + // Arrange + IEdmEntityType customerType = _customerSet.EntityType; + IEdmNavigationProperty ordersProperty = customerType.NavigationProperties().Single(p => p.Name == "Orders"); + + ODataQueryOptionParser parser = new ODataQueryOptionParser(_model, customerType, _customerSet, + new Dictionary { { "$select", "Orders" }, { "$expand", "Orders" } }); + SelectExpandClause selectExpandClause = parser.ParseSelectAndExpand(); + + SelectExpandNode selectExpandNode = new SelectExpandNode + { + ExpandedProperties = new Dictionary + { + { _ordersNavigation, selectExpandClause.SelectedItems.OfType().Single() } + }, + }; + Mock writer = new Mock(); + + Mock innerSerializer = new Mock(ODataPayloadKind.Resource); + + //innerSerializer + // .Setup(s => s.WriteObjectInlineAsync(_orders, ordersProperty.Type, writer.Object, It.IsAny())) + // .Callback((object o, IEdmTypeReference t, ODataWriter w, ODataSerializerContext context) => + // { + // Assert.Same(context.NavigationSource.Name, "Orders"); + // Assert.Same(context.SelectExpandClause, selectExpandNode.ExpandedProperties.Single().Value.SelectAndExpand); + // }) + // .Returns(Task.CompletedTask) + // .Verifiable(); + + Mock serializerProvider = new Mock(); + serializerProvider.Setup( + p => p.GetEdmTypeSerializer(It.IsAny())) + .Returns(innerSerializer.Object); + Mock serializer = new Mock(serializerProvider.Object); + serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); + serializer.CallBase = true; + _writeContext.SelectExpandClause = selectExpandClause; + + // Act + await serializer.Object.WriteObjectInlineAsync(_customer, _customerType, writer.Object, _writeContext); + + // Assert + innerSerializer.Verify(); + // check that the context is rolled back + Assert.Same(_writeContext.NavigationSource.Name, "Customers"); + Assert.Same(_writeContext.SelectExpandClause, selectExpandClause); + } + + [Fact] + public async Task WriteObjectInlineAsync_CanExpandNavigationProperty_ContainingEdmObject() + { + // Arrange + IEdmEntityType customerType = _customerSet.EntityType; + IEdmNavigationProperty ordersProperty = customerType.NavigationProperties().Single(p => p.Name == "Orders"); + + Mock orders = new Mock(); + orders.Setup(o => o.GetEdmType()).Returns(ordersProperty.Type); + object ordersValue = orders.Object; + + Mock customer = new Mock(); + customer.Setup(c => c.TryGetPropertyValue("Orders", out ordersValue)).Returns(true); + customer.Setup(c => c.GetEdmType()).Returns(customerType.AsReference()); + + ODataQueryOptionParser parser = new ODataQueryOptionParser(_model, customerType, _customerSet, + new Dictionary { { "$select", "Orders" }, { "$expand", "Orders" } }); + SelectExpandClause selectExpandClause = parser.ParseSelectAndExpand(); + + SelectExpandNode selectExpandNode = new SelectExpandNode + { + ExpandedProperties = new Dictionary() + }; + selectExpandNode.ExpandedProperties[ordersProperty] = selectExpandClause.SelectedItems.OfType().Single(); + + Mock writer = new Mock(); + + Mock ordersSerializer = new Mock(ODataPayloadKind.Resource); + ordersSerializer.Setup(s => s.WriteObjectInlineAsync(ordersValue, ordersProperty.Type, writer.Object, It.IsAny())).Verifiable(); + + Mock serializerProvider = new Mock(); + serializerProvider.Setup(p => p.GetEdmTypeSerializer(ordersProperty.Type)).Returns(ordersSerializer.Object); + + Mock serializer = new Mock(serializerProvider.Object); + serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); + serializer.CallBase = true; + + // Act + await serializer.Object.WriteObjectInlineAsync(customer.Object, _customerType, writer.Object, _writeContext); + + //Assert + ordersSerializer.Verify(); + } + + [Fact] + public async Task WriteObjectInlineAsync_CanWriteExpandedNavigationProperty_ExpandedCollectionValuedNavigationPropertyIsNull() + { + // Arrange + IEdmEntityType customerType = _customerSet.EntityType; + IEdmNavigationProperty ordersProperty = customerType.NavigationProperties().Single(p => p.Name == "Orders"); + + Mock customer = new Mock(); + object ordersValue = null; + customer.Setup(c => c.TryGetPropertyValue("Orders", out ordersValue)).Returns(true); + customer.Setup(c => c.GetEdmType()).Returns(customerType.AsReference()); + + ODataQueryOptionParser parser = new ODataQueryOptionParser(_model, customerType, _customerSet, + new Dictionary { { "$select", "Orders" }, { "$expand", "Orders" } }); + SelectExpandClause selectExpandClause = parser.ParseSelectAndExpand(); + + SelectExpandNode selectExpandNode = new SelectExpandNode + { + ExpandedProperties = new Dictionary() + }; + selectExpandNode.ExpandedProperties[ordersProperty] = + selectExpandClause.SelectedItems.OfType().Single(); + + Mock writer = new Mock(); + writer.Setup(w => w.WriteStartAsync(It.IsAny())).Callback( + (ODataResourceSet feed) => + { + Assert.Null(feed.Count); + Assert.Null(feed.DeltaLink); + Assert.Null(feed.Id); + Assert.Empty(feed.InstanceAnnotations); + Assert.Null(feed.NextPageLink); + }).Returns(Task.CompletedTask).Verifiable(); + Mock ordersSerializer = new Mock(ODataPayloadKind.Resource); + Mock serializerProvider = new Mock(); + serializerProvider.Setup(p => p.GetEdmTypeSerializer(ordersProperty.Type)).Returns(ordersSerializer.Object); + + Mock serializer = new Mock(serializerProvider.Object); + serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); + serializer.CallBase = true; + + // Act + await serializer.Object.WriteObjectInlineAsync(customer.Object, _customerType, writer.Object, _writeContext); + + // Assert + writer.Verify(); + } + + [Fact] + public async Task WriteObjectInlineAsync_CanWriteExpandedNavigationProperty_ExpandedSingleValuedNavigationPropertyIsNull() + { + // Arrange + IEdmEntityType orderType = _orderSet.EntityType; + IEdmNavigationProperty customerProperty = orderType.NavigationProperties().Single(p => p.Name == "Customer"); + + Mock order = new Mock(); + object customerValue = null; + order.Setup(c => c.TryGetPropertyValue("Customer", out customerValue)).Returns(true); + order.Setup(c => c.GetEdmType()).Returns(orderType.AsReference()); + + ODataQueryOptionParser parser = new ODataQueryOptionParser(_model, orderType, _orderSet, + new Dictionary { { "$select", "Customer" }, { "$expand", "Customer" } }); + SelectExpandClause selectExpandClause = parser.ParseSelectAndExpand(); + + SelectExpandNode selectExpandNode = new SelectExpandNode + { + ExpandedProperties = new Dictionary() + }; + selectExpandNode.ExpandedProperties[customerProperty] = + selectExpandClause.SelectedItems.OfType().Single(); + + Mock writer = new Mock(); + + writer.Setup(w => w.WriteStartAsync(null as ODataResource)).Verifiable(); + Mock ordersSerializer = new Mock(ODataPayloadKind.Resource); + Mock serializerProvider = new Mock(); + serializerProvider.Setup(p => p.GetEdmTypeSerializer(customerProperty.Type)) + .Returns(ordersSerializer.Object); + + Mock serializer = new Mock(serializerProvider.Object); + serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); + serializer.CallBase = true; + + // Act + await serializer.Object.WriteObjectInlineAsync(order.Object, _orderType, writer.Object, _writeContext); + + // Assert + writer.Verify(); + } + + [Fact] + public async Task WriteObjectInlineAsync_CanWriteExpandedNavigationProperty_DerivedExpandedCollectionValuedNavigationPropertyIsNull() + { + // Arrange + IEdmEntityType specialCustomerType = (IEdmEntityType)_specialCustomerType.Definition; + IEdmNavigationProperty specialOrdersProperty = + specialCustomerType.NavigationProperties().Single(p => p.Name == "SpecialOrders"); + + Mock customer = new Mock(); + object specialOrdersValue = null; + customer.Setup(c => c.TryGetPropertyValue("SpecialOrders", out specialOrdersValue)).Returns(true); + customer.Setup(c => c.GetEdmType()).Returns(_specialCustomerType); + + IEdmEntityType customerType = _customerSet.EntityType; + ODataQueryOptionParser parser = new ODataQueryOptionParser( + _model, + customerType, + _customerSet, + new Dictionary + { + { "$select", "Default.SpecialCustomer/SpecialOrders" }, + { "$expand", "Default.SpecialCustomer/SpecialOrders" } + }); + SelectExpandClause selectExpandClause = parser.ParseSelectAndExpand(); + + SelectExpandNode selectExpandNode = new SelectExpandNode + { + ExpandedProperties = new Dictionary() + }; + selectExpandNode.ExpandedProperties[specialOrdersProperty] = + selectExpandClause.SelectedItems.OfType().Single(); + + Mock writer = new Mock(); + writer.Setup(w => w.WriteStartAsync(It.IsAny())).Callback( + (ODataResourceSet feed) => + { + Assert.Null(feed.Count); + Assert.Null(feed.DeltaLink); + Assert.Null(feed.Id); + Assert.Empty(feed.InstanceAnnotations); + Assert.Null(feed.NextPageLink); + }).Returns(Task.CompletedTask).Verifiable(); + Mock ordersSerializer = new Mock(ODataPayloadKind.Resource); + Mock serializerProvider = new Mock(); + serializerProvider.Setup(p => p.GetEdmTypeSerializer(specialOrdersProperty.Type)) + .Returns(ordersSerializer.Object); + + Mock serializer = new Mock(serializerProvider.Object); + serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); + serializer.CallBase = true; + + // Act + await serializer.Object.WriteObjectInlineAsync(customer.Object, _customerType, writer.Object, _writeContext); + + // Assert + writer.Verify(); + } + + [Fact] + public async Task WriteObjectInlineAsync_CanWriteExpandedNavigationProperty_DerivedExpandedSingleValuedNavigationPropertyIsNull() + { + // Arrange + IEdmEntityType specialOrderType = (IEdmEntityType)_specialOrderType.Definition; + IEdmNavigationProperty customerProperty = + specialOrderType.NavigationProperties().Single(p => p.Name == "SpecialCustomer"); + + Mock order = new Mock(); + object customerValue = null; + order.Setup(c => c.TryGetPropertyValue("SpecialCustomer", out customerValue)).Returns(true); + order.Setup(c => c.GetEdmType()).Returns(_specialOrderType); + + IEdmEntityType orderType = (IEdmEntityType)_orderType.Definition; + ODataQueryOptionParser parser = new ODataQueryOptionParser( + _model, + orderType, + _orderSet, + new Dictionary + { + { "$select", "Default.SpecialOrder/SpecialCustomer" }, + { "$expand", "Default.SpecialOrder/SpecialCustomer" } + }); + SelectExpandClause selectExpandClause = parser.ParseSelectAndExpand(); + + SelectExpandNode selectExpandNode = new SelectExpandNode + { + ExpandedProperties = new Dictionary() + }; + selectExpandNode.ExpandedProperties[customerProperty] = + selectExpandClause.SelectedItems.OfType().Single(); + + Mock writer = new Mock(); + + writer.Setup(w => w.WriteStartAsync(null as ODataResource)).Verifiable(); + Mock ordersSerializer = new Mock(ODataPayloadKind.Resource); + Mock serializerProvider = new Mock(); + serializerProvider.Setup(p => p.GetEdmTypeSerializer(customerProperty.Type)) + .Returns(ordersSerializer.Object); + + Mock serializer = new Mock(serializerProvider.Object); + serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); + serializer.CallBase = true; + + // Act + await serializer.Object.WriteObjectInlineAsync(order.Object, _orderType, writer.Object, _writeContext); + + // Assert + writer.Verify(); + } + + [Fact] + public void CreateResource_ThrowsArgumentNull_SelectExpandNode() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => serializer.CreateResource(selectExpandNode: null, resourceContext: _entityContext), + "selectExpandNode"); + } + + [Fact] + public void CreateResource_ThrowsArgumentNull_EntityContext() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => serializer.CreateResource(new SelectExpandNode(), resourceContext: null), + "resourceContext"); + } + + [Fact] + public void CreateComputedProperty_ThrowsArgumentNull_ForInputs() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNullOrEmpty(() => serializer.CreateComputedProperty(null, null), "propertyName"); + ExceptionAssert.ThrowsArgumentNull(() => serializer.CreateComputedProperty("any", null), "resourceContext"); + } + + [Fact] + public void CreateResource_Calls_CreateComputedProperty_ForEachSelectComputedProperty() + { + // Arrange + SelectExpandNode selectExpandNode = new SelectExpandNode(); + selectExpandNode.SelectedComputedProperties.Add("Computed1"); + selectExpandNode.SelectedComputedProperties.Add("Computed2"); + + ODataProperty[] properties = new ODataProperty[] { new ODataProperty(), new ODataProperty() }; + Mock serializer = new Mock(_serializerProvider); + serializer.CallBase = true; + + serializer.Setup(s => s.CreateComputedProperty("Computed1", _entityContext)).Returns(properties[0]).Verifiable(); + serializer.Setup(s => s.CreateComputedProperty("Computed2", _entityContext)).Returns(properties[1]).Verifiable(); + + // Act + ODataResource entry = serializer.Object.CreateResource(selectExpandNode, _entityContext); + + // Assert + serializer.Verify(); + Assert.Equal(properties, entry.Properties); + } + + [Fact] + public void CreateResource_Calls_CreateStructuralProperty_ForEachSelectedStructuralProperty() + { + // Arrange + SelectExpandNode selectExpandNode = new SelectExpandNode + { + SelectedStructuralProperties = new HashSet + { + new Mock((IEdmStructuredType)_customerType.Definition,"FirstName", EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.String, false)).Object, + new Mock((IEdmStructuredType)_customerType.Definition,"LastName", EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.String, false)).Object + } + }; + ODataProperty[] properties = new ODataProperty[] { new ODataProperty{Name="FirstName"}, new ODataProperty{Name="LastName"} }; + Mock serializer = new Mock(_serializerProvider); + serializer.CallBase = true; + + serializer + .Setup(s => s.CreateStructuralProperty(selectExpandNode.SelectedStructuralProperties.ElementAt(0), _entityContext)) + .Returns(properties[0]) + .Verifiable(); + serializer + .Setup(s => s.CreateStructuralProperty(selectExpandNode.SelectedStructuralProperties.ElementAt(1), _entityContext)) + .Returns(properties[1]) + .Verifiable(); + + // Act + ODataDeletedResource entry = serializer.Object.CreateDeletedResource(new Uri("http://customers/1",UriKind.Absolute), DeltaDeletedEntryReason.Deleted, selectExpandNode, _entityContext); + + // Assert + serializer.Verify(); + Assert.Equal(properties, entry.Properties); + } + + [Fact] + public void CreateResource_SetsETagToNull_IfRequestIsNull() + { + // Arrange + SelectExpandNode selectExpandNode = new SelectExpandNode + { + SelectedStructuralProperties = new HashSet + { + new Mock().Object, new Mock().Object + } + }; + ODataProperty[] properties = new[] { new ODataProperty(), new ODataProperty() }; + Mock serializer = new Mock(_serializerProvider); + serializer.CallBase = true; + + serializer + .Setup(s => s.CreateStructuralProperty(selectExpandNode.SelectedStructuralProperties.ElementAt(0), _entityContext)) + .Returns(properties[0]); + serializer + .Setup(s => s.CreateStructuralProperty(selectExpandNode.SelectedStructuralProperties.ElementAt(1), _entityContext)) + .Returns(properties[1]); + + // Act + ODataResource entry = serializer.Object.CreateResource(selectExpandNode, _entityContext); + + // Assert + Assert.Null(entry.ETag); + } + + [Fact] + public void CreateResource_SetsETagToNull_IfModelNotHaveConcurrencyProperty() + { + // Arrange + IEdmEntitySet orderSet = _model.EntityContainer.FindEntitySet("Orders"); + Order order = new Order() + { + Name = "Foo", + Shipment = "Bar", + ID = 10, + }; + + _writeContext.NavigationSource = orderSet; + _entityContext = new ResourceContext(_writeContext, orderSet.EntityType.AsReference(), order); + + SelectExpandNode selectExpandNode = new SelectExpandNode + { + SelectedStructuralProperties = new HashSet + { + new Mock().Object, new Mock().Object + } + }; + ODataProperty[] properties = new[] { new ODataProperty(), new ODataProperty() }; + Mock serializer = new Mock(_serializerProvider); + serializer.CallBase = true; + + serializer + .Setup(s => s.CreateStructuralProperty(selectExpandNode.SelectedStructuralProperties.ElementAt(0), _entityContext)) + .Returns(properties[0]); + serializer + .Setup(s => s.CreateStructuralProperty(selectExpandNode.SelectedStructuralProperties.ElementAt(1), _entityContext)) + .Returns(properties[1]); + + //var config = RoutingConfigurationFactory.CreateWithRootContainer("Route"); + var request = RequestFactory.Create(opt => opt.AddRouteComponents("route", _model)); + request.ODataFeature().RoutePrefix = "route"; + _entityContext.Request = request; + + // Act + ODataResource entry = serializer.Object.CreateResource(selectExpandNode, _entityContext); + + // Assert + Assert.Null(entry.ETag); + } + + [Fact] + public void CreateResource_SetsEtagToNotNull_IfWithConcurrencyProperty() + { + // Arrange + Mock mockConcurrencyProperty = new Mock(); + mockConcurrencyProperty.SetupGet(s => s.Name).Returns("City"); + SelectExpandNode selectExpandNode = new SelectExpandNode + { + SelectedStructuralProperties = new HashSet { new Mock().Object, mockConcurrencyProperty.Object } + }; + ODataProperty[] properties = new[] { new ODataProperty(), new ODataProperty() }; + Mock serializer = new Mock(_serializerProvider); + serializer.CallBase = true; + serializer + .Setup(s => s.CreateStructuralProperty(selectExpandNode.SelectedStructuralProperties.ElementAt(0), _entityContext)) + .Returns(properties[0]); + serializer + .Setup(s => s.CreateStructuralProperty(selectExpandNode.SelectedStructuralProperties.ElementAt(1), _entityContext)) + .Returns(properties[1]); + + Mock mockETagHandler = new Mock(); + string tag = "\"'anycity'\""; + EntityTagHeaderValue etagHeaderValue = new EntityTagHeaderValue(tag, isWeak: true); + mockETagHandler.Setup(e => e.CreateETag(It.IsAny>(), It.IsAny())).Returns(etagHeaderValue); + + var request = RequestFactory.Create(opt => opt.AddRouteComponents("route", _model, services => + { + services.AddSingleton(sp => mockETagHandler.Object); + })); + request.ODataFeature().RoutePrefix = "route"; + _entityContext.Request = request; + + // Act + ODataDeletedResource resource = serializer.Object.CreateDeletedResource(new Uri("http://customer/1"),DeltaDeletedEntryReason.Deleted, selectExpandNode, _entityContext); + + // Assert + Assert.Equal(etagHeaderValue.ToString(), resource.ETag); + } + + [Fact] + public void CreateResource_IgnoresProperty_IfCreateStructuralPropertyReturnsNull() + { + // Arrange + SelectExpandNode selectExpandNode = new SelectExpandNode + { + SelectedStructuralProperties = new HashSet { new Mock().Object } + }; + Mock serializer = new Mock(_serializerProvider); + serializer.CallBase = true; + + serializer + .Setup(s => s.CreateStructuralProperty(selectExpandNode.SelectedStructuralProperties.ElementAt(0), _entityContext)) + .Returns(null); + + // Act + ODataResource entry = serializer.Object.CreateResource(selectExpandNode, _entityContext); + + // Assert + serializer.Verify(); + Assert.Empty(entry.Properties); + } + + [Fact] + public void CreateResource_Calls_CreateODataAction_ForEachSelectAction() + { + // Arrange + ODataAction[] actions = new ODataAction[] { new ODataAction(), new ODataAction() }; + SelectExpandNode selectExpandNode = new SelectExpandNode + { + SelectedActions = new HashSet { new Mock().Object, new Mock().Object } + }; + Mock serializer = new Mock(_serializerProvider); + serializer.CallBase = true; + + serializer.Setup(s => s.CreateODataAction(selectExpandNode.SelectedActions.ElementAt(0), _entityContext)).Returns(actions[0]).Verifiable(); + serializer.Setup(s => s.CreateODataAction(selectExpandNode.SelectedActions.ElementAt(1), _entityContext)).Returns(actions[1]).Verifiable(); + + // Act + ODataResource entry = serializer.Object.CreateResource(selectExpandNode, _entityContext); + + // Assert + Assert.Equal(actions, entry.Actions); + serializer.Verify(); + } + + [Fact] + public void CreateResource_Works_ToAppendDynamicProperties_ForOpenEntityType() + { + // Arrange + IEdmModel model = SerializationTestsHelpers.SimpleOpenTypeModel(); + + IEdmEntitySet customers = model.EntityContainer.FindEntitySet("Customers"); + + IEdmEntityType customerType = model.FindDeclaredType("Default.Customer") as IEdmEntityType; + Type simpleOpenCustomer = typeof(SimpleOpenCustomer); + model.SetAnnotationValue(customerType, new ClrTypeAnnotation(simpleOpenCustomer)); + + IEdmComplexType addressType = model.FindDeclaredType("Default.Address") as IEdmComplexType; + Type simpleOpenAddress = typeof(SimpleOpenAddress); + model.SetAnnotationValue(addressType, new ClrTypeAnnotation(simpleOpenAddress)); + + IEdmEnumType enumType = model.FindDeclaredType("Default.SimpleEnum") as IEdmEnumType; + Type simpleEnumType = typeof(SimpleEnum); + model.SetAnnotationValue(enumType, new ClrTypeAnnotation(simpleEnumType)); + + model.SetAnnotationValue(customerType, new DynamicPropertyDictionaryAnnotation( + simpleOpenCustomer.GetProperty("CustomerProperties"))); + + model.SetAnnotationValue(addressType, new DynamicPropertyDictionaryAnnotation( + simpleOpenAddress.GetProperty("Properties"))); + + ODataResourceSerializer serializer = new ODataResourceSerializer(_serializerProvider); + + SelectExpandNode selectExpandNode = new SelectExpandNode(null, customerType, model); + ODataSerializerContext writeContext = new ODataSerializerContext + { + Model = model, + Path = new ODataPath(new EntitySetSegment(customers)) + }; + + SimpleOpenCustomer customer = new SimpleOpenCustomer() + { + CustomerId = 991, + Name = "Name #991", + Address = new SimpleOpenAddress + { + City = "a city", + Street = "a street", + Properties = new Dictionary { { "ArrayProperty", new[] { "15", "14", "13" } } } + }, + CustomerProperties = new Dictionary() + }; + DateTime dateTime = new DateTime(2014, 10, 24, 0, 0, 0, DateTimeKind.Utc); + customer.CustomerProperties.Add("EnumProperty", SimpleEnum.Fourth); + customer.CustomerProperties.Add("GuidProperty", new Guid("181D3A20-B41A-489F-9F15-F91F0F6C9ECA")); + customer.CustomerProperties.Add("ListProperty", new List { 5, 4, 3, 2, 1 }); + customer.CustomerProperties.Add("DateTimeProperty", dateTime); + + ResourceContext resourceContext = new ResourceContext(writeContext, + customerType.ToEdmTypeReference(false) as IEdmEntityTypeReference, customer); + + // Act + ODataResource resource = serializer.CreateResource(selectExpandNode, resourceContext); + + // Assert + Assert.Equal("Default.Customer", resource.TypeName); + Assert.Equal(6, resource.Properties.Count()); + + // Verify the declared properties + ODataProperty street = Assert.IsType(Assert.Single(resource.Properties.Where(p => p.Name == "CustomerId"))); + Assert.Equal(991, street.Value); + + ODataProperty city = Assert.IsType(Assert.Single(resource.Properties.Where(p => p.Name == "Name"))); + Assert.Equal("Name #991", city.Value); + + // Verify the nested open complex property + Assert.Empty(resource.Properties.Where(p => p.Name == "Address")); + + // Verify the dynamic properties + ODataProperty enumProperty = Assert.IsType(Assert.Single(resource.Properties.Where(p => p.Name == "EnumProperty"))); + ODataEnumValue enumValue = Assert.IsType(enumProperty.Value); + Assert.Equal("Fourth", enumValue.Value); + Assert.Equal("Default.SimpleEnum", enumValue.TypeName); + + ODataProperty guidProperty = Assert.IsType(Assert.Single(resource.Properties.Where(p => p.Name == "GuidProperty"))); + Assert.Equal(new Guid("181D3A20-B41A-489F-9F15-F91F0F6C9ECA"), guidProperty.Value); + + ODataProperty listProperty = Assert.IsType(Assert.Single(resource.Properties.Where(p => p.Name == "ListProperty"))); + ODataCollectionValue collectionValue = Assert.IsType(listProperty.Value); + Assert.Equal(new List { 5, 4, 3, 2, 1 }, collectionValue.Items.OfType().ToList()); + Assert.Equal("Collection(Edm.Int32)", collectionValue.TypeName); + + ODataProperty dateTimeProperty = Assert.IsType(Assert.Single(resource.Properties.Where(p => p.Name == "DateTimeProperty"))); + Assert.Equal(new DateTimeOffset(dateTime), dateTimeProperty.Value); + } + + [Fact] + public void CreateResource_Works_ToAppendNullDynamicProperties_ForOpenEntityType() + { + // Arrange + IEdmModel model = SerializationTestsHelpers.SimpleOpenTypeModel(); + + IEdmEntitySet customers = model.EntityContainer.FindEntitySet("Customers"); + + IEdmEntityType customerType = model.FindDeclaredType("Default.Customer") as IEdmEntityType; + Type simpleOpenCustomer = typeof(SimpleOpenCustomer); + model.SetAnnotationValue(customerType, new ClrTypeAnnotation(simpleOpenCustomer)); + + IEdmComplexType addressType = model.FindDeclaredType("Default.Address") as IEdmComplexType; + Type simpleOpenAddress = typeof(SimpleOpenAddress); + model.SetAnnotationValue(addressType, new ClrTypeAnnotation(simpleOpenAddress)); + + IEdmEnumType enumType = model.FindDeclaredType("Default.SimpleEnum") as IEdmEnumType; + Type simpleEnumType = typeof(SimpleEnum); + model.SetAnnotationValue(enumType, new ClrTypeAnnotation(simpleEnumType)); + + model.SetAnnotationValue(customerType, new DynamicPropertyDictionaryAnnotation( + simpleOpenCustomer.GetProperty("CustomerProperties"))); + + model.SetAnnotationValue(addressType, new DynamicPropertyDictionaryAnnotation( + simpleOpenAddress.GetProperty("Properties"))); + + ODataResourceSerializer serializer = new ODataResourceSerializer(_serializerProvider); + + var request = RequestFactory.Create(opt => opt.AddRouteComponents("route", model)); + request.ODataFeature().RoutePrefix = "route"; + SelectExpandNode selectExpandNode = new SelectExpandNode(null, customerType, model); + ODataSerializerContext writeContext = new ODataSerializerContext + { + Model = model, + Path = new ODataPath(new EntitySetSegment(customers)), + Request = request + }; + + SimpleOpenCustomer customer = new SimpleOpenCustomer() + { + CustomerId = 991, + Name = "Name #991", + Address = new SimpleOpenAddress + { + City = "a city", + Street = "a street", + Properties = new Dictionary { { "ArrayProperty", new[] { "15", "14", "13" } } } + }, + CustomerProperties = new Dictionary() + }; + + customer.CustomerProperties.Add("GuidProperty", new Guid("181D3A20-B41A-489F-9F15-F91F0F6C9ECA")); + customer.CustomerProperties.Add("NullProperty", null); + + ResourceContext entityContext = new ResourceContext(writeContext, + customerType.ToEdmTypeReference(false) as IEdmEntityTypeReference, customer); + + // Act + ODataResource resource = serializer.CreateResource(selectExpandNode, entityContext); + + // Assert + Assert.Equal("Default.Customer", resource.TypeName); + Assert.Equal(4, resource.Properties.Count()); + + // Verify the declared properties + ODataProperty street = Assert.IsType(Assert.Single(resource.Properties.Where(p => p.Name == "CustomerId"))); + Assert.Equal(991, street.Value); + + ODataProperty city = Assert.IsType(Assert.Single(resource.Properties.Where(p => p.Name == "Name"))); + Assert.Equal("Name #991", city.Value); + + // Verify the nested open complex property + Assert.Empty(resource.Properties.Where(p => p.Name == "Address")); + + // Verify the dynamic properties + ODataProperty guidProperty = Assert.IsType(Assert.Single(resource.Properties.Where(p => p.Name == "GuidProperty"))); + Assert.Equal(new Guid("181D3A20-B41A-489F-9F15-F91F0F6C9ECA"), guidProperty.Value); + + ODataProperty nullProperty = resource.Properties.OfType().Single(p => p.Name == "NullProperty"); + Assert.Null(nullProperty.Value); + } + + [Fact] + public void CreateResource_Works_ToIgnoreDynamicPropertiesWithEmptyNames_ForOpenEntityType() + { + // Arrange + IEdmModel model = SerializationTestsHelpers.SimpleOpenTypeModel(); + + IEdmEntitySet customers = model.EntityContainer.FindEntitySet("Customers"); + + IEdmEntityType customerType = model.FindDeclaredType("Default.Customer") as IEdmEntityType; + Type simpleOpenCustomer = typeof(SimpleOpenCustomer); + model.SetAnnotationValue(customerType, new ClrTypeAnnotation(simpleOpenCustomer)); + + IEdmComplexType addressType = model.FindDeclaredType("Default.Address") as IEdmComplexType; + Type simpleOpenAddress = typeof(SimpleOpenAddress); + model.SetAnnotationValue(addressType, new ClrTypeAnnotation(simpleOpenAddress)); + + IEdmEnumType enumType = model.FindDeclaredType("Default.SimpleEnum") as IEdmEnumType; + Type simpleEnumType = typeof(SimpleEnum); + model.SetAnnotationValue(enumType, new ClrTypeAnnotation(simpleEnumType)); + + model.SetAnnotationValue(customerType, new DynamicPropertyDictionaryAnnotation( + simpleOpenCustomer.GetProperty("CustomerProperties"))); + + model.SetAnnotationValue(addressType, new DynamicPropertyDictionaryAnnotation( + simpleOpenAddress.GetProperty("Properties"))); + + ODataResourceSerializer serializer = new ODataResourceSerializer(_serializerProvider); + + var request = RequestFactory.Create(opt => opt.AddRouteComponents("route", model)); + request.ODataFeature().RoutePrefix = "route"; + SelectExpandNode selectExpandNode = new SelectExpandNode(null, customerType, model); + ODataSerializerContext writeContext = new ODataSerializerContext + { + Model = model, + Path = new ODataPath(new EntitySetSegment(customers)), + Request = request + }; + + SimpleOpenCustomer customer = new SimpleOpenCustomer() + { + CustomerId = 991, + Name = "Name #991", + Address = new SimpleOpenAddress + { + City = "a city", + Street = "a street", + Properties = new Dictionary { { "ArrayProperty", new[] { "15", "14", "13" } } } + }, + CustomerProperties = new Dictionary() + }; + + customer.CustomerProperties.Add("GuidProperty", new Guid("181D3A20-B41A-489F-9F15-F91F0F6C9ECA")); + customer.CustomerProperties.Add("", "EmptyProperty"); + + ResourceContext entityContext = new ResourceContext(writeContext, + customerType.ToEdmTypeReference(false) as IEdmEntityTypeReference, customer); + + // Act + ODataResource resource = serializer.CreateResource(selectExpandNode, entityContext); + + // Assert + Assert.Equal("Default.Customer", resource.TypeName); + Assert.Equal(3, resource.Properties.Count()); + + // Verify properties with empty names are ignored + ODataProperty emptyProperty = resource.Properties.OfType().SingleOrDefault(p => p.Name == ""); + Assert.Null(emptyProperty); + } + + [Fact] + public void CreateResource_Throws_IfDynamicPropertyUsesExistingName_ForOpenType() + { + // Arrange + IEdmModel model = SerializationTestsHelpers.SimpleOpenTypeModel(); + + IEdmEntitySet customers = model.EntityContainer.FindEntitySet("Customers"); + + IEdmEntityType customerType = model.FindDeclaredType("Default.Customer") as IEdmEntityType; + Type simpleOpenCustomer = typeof(SimpleOpenCustomer); + model.SetAnnotationValue(customerType, new ClrTypeAnnotation(simpleOpenCustomer)); + + IEdmComplexType addressType = model.FindDeclaredType("Default.Address") as IEdmComplexType; + Type simpleOpenAddress = typeof(SimpleOpenAddress); + model.SetAnnotationValue(addressType, new ClrTypeAnnotation(simpleOpenAddress)); + + IEdmEnumType enumType = model.FindDeclaredType("Default.SimpleEnum") as IEdmEnumType; + Type simpleEnumType = typeof(SimpleEnum); + model.SetAnnotationValue(enumType, new ClrTypeAnnotation(simpleEnumType)); + + model.SetAnnotationValue(customerType, new DynamicPropertyDictionaryAnnotation( + simpleOpenCustomer.GetProperty("CustomerProperties"))); + + model.SetAnnotationValue(addressType, new DynamicPropertyDictionaryAnnotation( + simpleOpenAddress.GetProperty("Properties"))); + + ODataResourceSerializer serializer = new ODataResourceSerializer(_serializerProvider); + + var request = RequestFactory.Create(opt => opt.AddRouteComponents("route", model)); + request.ODataFeature().RoutePrefix = "route"; + SelectExpandNode selectExpandNode = new SelectExpandNode(null, customerType, model); + ODataSerializerContext writeContext = new ODataSerializerContext + { + Model = model, + Path = new ODataPath(new EntitySetSegment(customers)), + Request = request + }; + + SimpleOpenCustomer customer = new SimpleOpenCustomer() + { + CustomerId = 991, + Name = "Name #991", + Address = new SimpleOpenAddress + { + City = "a city", + Street = "a street", + Properties = new Dictionary { { "ArrayProperty", new[] { "15", "14", "13" } } } + }, + CustomerProperties = new Dictionary() + }; + + customer.CustomerProperties.Add("GuidProperty", new Guid("181D3A20-B41A-489F-9F15-F91F0F6C9ECA")); + customer.CustomerProperties.Add("Name", "DynamicName"); + + ResourceContext entityContext = new ResourceContext(writeContext, + customerType.ToEdmTypeReference(false) as IEdmEntityTypeReference, customer); + + // Act & Assert + ExceptionAssert.Throws( + () => serializer.CreateResource(selectExpandNode, entityContext), + "Name", + partialMatch: true); + } + + [Fact] + public void CreateResource_Throws_IfNullDynamicPropertyUsesExistingName_ForOpenType() + { + // Arrange + IEdmModel model = SerializationTestsHelpers.SimpleOpenTypeModel(); + + IEdmEntitySet customers = model.EntityContainer.FindEntitySet("Customers"); + + IEdmEntityType customerType = model.FindDeclaredType("Default.Customer") as IEdmEntityType; + Type simpleOpenCustomer = typeof(SimpleOpenCustomer); + model.SetAnnotationValue(customerType, new ClrTypeAnnotation(simpleOpenCustomer)); + + IEdmComplexType addressType = model.FindDeclaredType("Default.Address") as IEdmComplexType; + Type simpleOpenAddress = typeof(SimpleOpenAddress); + model.SetAnnotationValue(addressType, new ClrTypeAnnotation(simpleOpenAddress)); + + IEdmEnumType enumType = model.FindDeclaredType("Default.SimpleEnum") as IEdmEnumType; + Type simpleEnumType = typeof(SimpleEnum); + model.SetAnnotationValue(enumType, new ClrTypeAnnotation(simpleEnumType)); + + model.SetAnnotationValue(customerType, new DynamicPropertyDictionaryAnnotation( + simpleOpenCustomer.GetProperty("CustomerProperties"))); + + model.SetAnnotationValue(addressType, new DynamicPropertyDictionaryAnnotation( + simpleOpenAddress.GetProperty("Properties"))); + + ODataResourceSerializer serializer = new ODataResourceSerializer(_serializerProvider); + + var request = RequestFactory.Create(opt => opt.AddRouteComponents("route", model)); + request.ODataFeature().RoutePrefix = "route"; + SelectExpandNode selectExpandNode = new SelectExpandNode(null, customerType, model); + ODataSerializerContext writeContext = new ODataSerializerContext + { + Model = model, + Path = new ODataPath(new EntitySetSegment(customers)), + Request = request + }; + + SimpleOpenCustomer customer = new SimpleOpenCustomer() + { + CustomerId = 991, + Name = "Name #991", + Address = new SimpleOpenAddress + { + City = "a city", + Street = "a street", + Properties = new Dictionary { { "ArrayProperty", new[] { "15", "14", "13" } } } + }, + CustomerProperties = new Dictionary() + }; + + customer.CustomerProperties.Add("GuidProperty", new Guid("181D3A20-B41A-489F-9F15-F91F0F6C9ECA")); + customer.CustomerProperties.Add("Name", null); + + ResourceContext entityContext = new ResourceContext(writeContext, + customerType.ToEdmTypeReference(false) as IEdmEntityTypeReference, customer); + + // Act & Assert + ExceptionAssert.Throws( + () => serializer.CreateResource(selectExpandNode, entityContext), + "Name", + partialMatch: true); + } + [Fact] + public void CreateUntypedPropertyValue_ThrowsArgumentNull_StructuralProperty() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => serializer.CreateUntypedPropertyValue(structuralProperty: null, resourceContext: null, out _), + "structuralProperty"); + } + + [Fact] + public void CreateUntypedPropertyValue_ThrowsArgumentNull_ResourceContext() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + Mock property = new Mock(); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => serializer.CreateUntypedPropertyValue(property.Object, resourceContext: null, out _), + "resourceContext"); + } + + [Fact] + public void CreateUntypedPropertyValue_ReturnsNull_ForNonUntypedProperty() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + Mock property = new Mock(); + property.Setup(p => p.Type).Returns(EdmCoreModel.Instance.GetInt32(true)); + + // Act + object result = serializer.CreateUntypedPropertyValue(property.Object, new ResourceContext(), out _); + + // Assert + Assert.Null(result); + } + + [Fact] + public void CreateUntypedPropertyValue_ReturnsRealValue_ForUntypedPropertyWithRealType() + { + // Arrange + Mock property = new Mock(); + property.Setup(p => p.Name).Returns("PropertyName"); + Mock serializerProvider = new Mock(MockBehavior.Strict); + object propertyValue = new List(); + var entity = new { PropertyName = propertyValue }; + Mock innerSerializer = new Mock(ODataPayloadKind.Property); + IEdmTypeReference propertyType = EdmUntypedStructuredTypeReference.NullableTypeReference; + + property.Setup(p => p.Type).Returns(propertyType); + + var serializer = new ODataResourceSerializer(serializerProvider.Object); + ResourceContext entityContext = new ResourceContext(_writeContext, _customerType, entity); + + // Act + object result = serializer.CreateUntypedPropertyValue(property.Object, entityContext, out IEdmTypeReference actualType); + + // Assert + innerSerializer.Verify(); + Assert.Same(propertyValue, result); + Assert.Same(EdmUntypedHelpers.NullableUntypedCollectionReference, actualType); + } + + [Fact] + public void CreateUntypedPropertyValue_Calls_CreateODataValueOnInnerSerializer() + { + // Arrange + Mock property = new Mock(); + property.Setup(p => p.Name).Returns("PropertyName"); + Mock serializerProvider = new Mock(MockBehavior.Strict); + var entity = new { PropertyName = 42 }; + Mock innerSerializer = new Mock(ODataPayloadKind.Property); + ODataPrimitiveValue propertyValue = new ODataPrimitiveValue(42); + IEdmTypeReference propertyType = EdmUntypedStructuredTypeReference.NullableTypeReference; + IEdmTypeReference actualType = _writeContext.GetEdmType(propertyValue, typeof(int)); + + property.Setup(p => p.Type).Returns(propertyType); + serializerProvider.Setup(s => s.GetEdmTypeSerializer(actualType)).Returns(innerSerializer.Object); + innerSerializer.Setup(s => s.CreateODataValue(42, actualType, _writeContext)).Returns(propertyValue).Verifiable(); + + var serializer = new ODataResourceSerializer(serializerProvider.Object); + ResourceContext entityContext = new ResourceContext(_writeContext, _customerType, entity); + + // Act + object createdProperty = serializer.CreateUntypedPropertyValue(property.Object, entityContext, out _); + + // Assert + innerSerializer.Verify(); + ODataProperty odataProperty = Assert.IsType(createdProperty); + Assert.Equal("PropertyName", odataProperty.Name); + Assert.Equal(42, odataProperty.Value); + } + + [Fact] + public void CreateStructuralProperty_ThrowsArgumentNull_StructuralProperty() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => serializer.CreateStructuralProperty(structuralProperty: null, resourceContext: null), + "structuralProperty"); + } + + [Fact] + public void CreateStructuralProperty_ThrowsArgumentNull_ResourceContext() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + Mock property = new Mock(); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => serializer.CreateStructuralProperty(property.Object, resourceContext: null), + "resourceContext"); + } + + [Fact] + public void CreateStructuralProperty_ThrowsSerializationException_TypeCannotBeSerialized() + { + // Arrange + Mock propertyType = new Mock(); + propertyType.Setup(t => t.Definition).Returns(new EdmEntityType("Namespace", "Name")); + Mock property = new Mock(); + Mock serializerProvider = new Mock(MockBehavior.Strict); + IEdmEntityObject entity = new Mock().Object; + property.Setup(p => p.Type).Returns(propertyType.Object); + serializerProvider.Setup(s => s.GetEdmTypeSerializer(propertyType.Object)).Returns(null); + + var serializer = new ODataResourceSerializer(serializerProvider.Object); + + // Act & Assert + ExceptionAssert.Throws( + () => serializer.CreateStructuralProperty(property.Object, new ResourceContext { EdmObject = entity }), + "'Namespace.Name' cannot be serialized using the OData output formatter."); + } + + [Fact] + public void CreateStructuralProperty_Calls_CreateODataValueOnInnerSerializer() + { + // Arrange + Mock property = new Mock(); + property.Setup(p => p.Name).Returns("PropertyName"); + Mock serializerProvider = new Mock(MockBehavior.Strict); + var entity = new { PropertyName = 42 }; + Mock innerSerializer = new Mock(ODataPayloadKind.Property); + ODataValue propertyValue = new Mock().Object; + IEdmTypeReference propertyType = _writeContext.GetEdmType(propertyValue, typeof(int)); + + property.Setup(p => p.Type).Returns(propertyType); + serializerProvider.Setup(s => s.GetEdmTypeSerializer(propertyType)).Returns(innerSerializer.Object); + innerSerializer.Setup(s => s.CreateODataValue(42, propertyType, _writeContext)).Returns(propertyValue).Verifiable(); + + var serializer = new ODataResourceSerializer(serializerProvider.Object); + ResourceContext entityContext = new ResourceContext(_writeContext, _customerType, entity); + + // Act + ODataProperty createdProperty = serializer.CreateStructuralProperty(property.Object, entityContext); + + // Assert + innerSerializer.Verify(); + Assert.Equal("PropertyName", createdProperty.Name); + Assert.Equal(propertyValue, createdProperty.Value); + } + + private bool Verify(ResourceContext instanceContext, object instance, ODataSerializerContext writeContext) + { + Assert.Same(instance, (instanceContext.EdmObject as TypedEdmStructuredObject).Instance); + Assert.Equal(writeContext.Model, instanceContext.EdmModel); + Assert.Equal(writeContext.NavigationSource, instanceContext.NavigationSource); + Assert.Equal(writeContext.Request, instanceContext.Request); + Assert.Equal(writeContext.SkipExpensiveAvailabilityChecks, instanceContext.SkipExpensiveAvailabilityChecks); + return true; + } + + [Fact] + public void CreateUntypedNestedResourceInfo_ThrowsArgumentNull_StructuralProperty() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => serializer.CreateUntypedNestedResourceInfo(null, null, null, null, resourceContext: _entityContext), + "structuralProperty"); + } + + [Fact] + public void CreateUntypedNestedResourceInfo_CreatesCorrectNestedResourceInfo() + { + // Arrange + Mock property = new Mock(); + property.Setup(p => p.Name).Returns("AnyUntypedName"); + + Mock serializer = new Mock(_serializerProvider); + serializer.CallBase = true; + + // Act + ODataNestedResourceInfo untypedNestedResourceInfo + = serializer.Object.CreateUntypedNestedResourceInfo(property.Object, + It.IsAny(), EdmUntypedHelpers.NullableUntypedCollectionReference, null, _entityContext); + + // Assert + Assert.NotNull(untypedNestedResourceInfo); + Assert.Equal("AnyUntypedName", untypedNestedResourceInfo.Name); + Assert.True(untypedNestedResourceInfo.IsCollection); + } + + [Fact] + public void CreateNavigationLink_ThrowsArgumentNull_NavigationProperty() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => serializer.CreateNavigationLink(navigationProperty: null, resourceContext: _entityContext), + "navigationProperty"); + } + + [Fact] + public void CreateNavigationLink_ThrowsArgumentNull_EntityContext() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + IEdmNavigationProperty navigationProperty = new Mock().Object; + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => serializer.CreateNavigationLink(navigationProperty, resourceContext: null), + "resourceContext"); + } + + [Fact] + public void CreateNavigationLink_CreatesCorrectNavigationLink() + { + // Arrange + Uri navigationLinkUri = new Uri("http://navigation_link"); + IEdmNavigationProperty property1 = CreateFakeNavigationProperty("Property1", _customerType); + OData.Edm.NavigationSourceLinkBuilderAnnotation linkAnnotation = new MockNavigationSourceLinkBuilderAnnotation + { + NavigationLinkBuilder = (ctxt, property, metadataLevel) => navigationLinkUri + }; + _model.SetNavigationSourceLinkBuilder(_customerSet, linkAnnotation); + + Mock serializer = new Mock(_serializerProvider); + serializer.CallBase = true; + + // Act + ODataNestedResourceInfo navigationLink = serializer.Object.CreateNavigationLink(property1, _entityContext); + + // Assert + Assert.Equal("Property1", navigationLink.Name); + Assert.Equal(navigationLinkUri, navigationLink.Url); + } + + [Fact] + public void CreateResource_UsesCorrectTypeName() + { + ResourceContext instanceContext = + new ResourceContext { StructuredType = _customerType.EntityDefinition(), SerializerContext = _writeContext }; + Mock serializer = new Mock(_serializerProvider); + serializer.CallBase = true; + SelectExpandNode selectExpandNode = new SelectExpandNode(); + + // Act + ODataResource entry = serializer.Object.CreateResource(selectExpandNode, instanceContext); + + // Assert + Assert.Equal("Default.Customer", entry.TypeName); + } + + [Fact] + public void CreateODataAction_ThrowsArgumentNull_Action() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => serializer.CreateODataAction(action: null, resourceContext: null), + "action"); + } + + [Fact] + public void CreateODataAction_ThrowsArgumentNull_EntityContext() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + IEdmAction action = new Mock().Object; + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => serializer.CreateODataAction(action, resourceContext: null), + "resourceContext"); + } + + [Fact] + public void CreateResource_WritesCorrectIdLink() + { + // Arrange + ResourceContext instanceContext = new ResourceContext + { + SerializerContext = _writeContext, + StructuredType = _customerType.EntityDefinition() + }; + + bool customIdLinkbuilderCalled = false; + OData.Edm.NavigationSourceLinkBuilderAnnotation linkAnnotation = new MockNavigationSourceLinkBuilderAnnotation + { + IdLinkBuilder = new SelfLinkBuilder((ResourceContext context) => + { + Assert.Same(instanceContext, context); + customIdLinkbuilderCalled = true; + return new Uri("http://sample_id_link"); + }, + followsConventions: false) + }; + _model.SetNavigationSourceLinkBuilder(_customerSet, linkAnnotation); + + Mock serializer = new Mock(_serializerProvider); + serializer.CallBase = true; + SelectExpandNode selectExpandNode = new SelectExpandNode(); + + // Act + ODataResource entry = serializer.Object.CreateResource(selectExpandNode, instanceContext); + + // Assert + Assert.True(customIdLinkbuilderCalled); + } + + [Fact] + public void WriteObjectInline_WritesCorrectEditLink() + { + // Arrange + ResourceContext instanceContext = new ResourceContext + { + SerializerContext = _writeContext, + StructuredType = _customerType.EntityDefinition() + }; + bool customEditLinkbuilderCalled = false; + OData.Edm.NavigationSourceLinkBuilderAnnotation linkAnnotation = new MockNavigationSourceLinkBuilderAnnotation + { + EditLinkBuilder = new SelfLinkBuilder((ResourceContext context) => + { + Assert.Same(instanceContext, context); + customEditLinkbuilderCalled = true; + return new Uri("http://sample_edit_link"); + }, + followsConventions: false) + }; + _model.SetNavigationSourceLinkBuilder(_customerSet, linkAnnotation); + + Mock serializer = new Mock(_serializerProvider); + serializer.CallBase = true; + SelectExpandNode selectExpandNode = new SelectExpandNode(); + + // Act + ODataResource entry = serializer.Object.CreateResource(selectExpandNode, instanceContext); + + // Assert + Assert.True(customEditLinkbuilderCalled); + } + + [Fact] + public void WriteObjectInline_WritesCorrectReadLink() + { + // Arrange + ResourceContext instanceContext = new ResourceContext(_writeContext, _customerType, 42); + bool customReadLinkbuilderCalled = false; + OData.Edm.NavigationSourceLinkBuilderAnnotation linkAnnotation = new MockNavigationSourceLinkBuilderAnnotation + { + ReadLinkBuilder = new SelfLinkBuilder((ResourceContext context) => + { + Assert.Same(instanceContext, context); + customReadLinkbuilderCalled = true; + return new Uri("http://sample_read_link"); + }, + followsConventions: false) + }; + + _model.SetNavigationSourceLinkBuilder(_customerSet, linkAnnotation); + + Mock serializer = new Mock(_serializerProvider); + serializer.CallBase = true; + SelectExpandNode selectExpandNode = new SelectExpandNode(); + + // Act + ODataResource entry = serializer.Object.CreateResource(selectExpandNode, instanceContext); + + // Assert + Assert.True(customReadLinkbuilderCalled); + } + + [Fact] + public void CreateSelectExpandNode_ThrowsArgumentNull_EntityContext() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + + ExceptionAssert.ThrowsArgumentNull( + () => serializer.CreateSelectExpandNode(resourceContext: null), + "resourceContext"); + } + + [Fact] + public void AddTypeNameAnnotationAsNeeded_AddsAnnotation_IfTypeOfPathDoesNotMatchEntryType() + { + // Arrange + string expectedTypeName = "TypeName"; + ODataResource entry = new ODataResource + { + TypeName = expectedTypeName + }; + + // Act + ODataResourceSerializer.AddTypeNameAnnotationAsNeeded(entry, _customerType.EntityDefinition(), ODataMetadataLevel.Minimal); + + // Assert + ODataTypeAnnotation annotation = entry.TypeAnnotation; + Assert.NotNull(annotation); // Guard + Assert.Equal(expectedTypeName, annotation.TypeName); + } + + [Fact] // Issue 984: Redundant type name serialization in OData JSON light minimal metadata mode + public void AddTypeNameAnnotationAsNeeded_AddsAnnotationWithNullValue_IfTypeOfPathMatchesEntryType() + { + // Arrange + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + ODataResource entry = new ODataResource + { + TypeName = model.SpecialCustomer.FullName() + }; + + // Act + ODataResourceSerializer.AddTypeNameAnnotationAsNeeded(entry, model.SpecialCustomer, ODataMetadataLevel.Minimal); + + // Assert + ODataTypeAnnotation annotation = entry.TypeAnnotation; + Assert.NotNull(annotation); // Guard + Assert.Null(annotation.TypeName); + } + + [Theory] + [InlineData("MatchingType", "MatchingType", ODataMetadataLevel.Full, false)] + [InlineData("DoesNotMatch1", "DoesNotMatch2", ODataMetadataLevel.Full, false)] + [InlineData("MatchingType", "MatchingType", ODataMetadataLevel.Minimal, true)] + [InlineData("DoesNotMatch1", "DoesNotMatch2", ODataMetadataLevel.Minimal, false)] + [InlineData("MatchingType", "MatchingType", ODataMetadataLevel.None, true)] + [InlineData("DoesNotMatch1", "DoesNotMatch2", ODataMetadataLevel.None, true)] + public void ShouldSuppressTypeNameSerialization(string resourceType, string entitySetType, + ODataMetadataLevel metadataLevel, bool expectedResult) + { + // Arrange + ODataResource resource = new ODataResource + { + // The caller uses a namespace-qualified name, which this test leaves empty. + TypeName = "NS." + resourceType + }; + IEdmEntityType edmType = CreateEntityTypeWithName(entitySetType); + + // Act + bool actualResult = ODataResourceSerializer.ShouldSuppressTypeNameSerialization(resource, edmType, metadataLevel); + + // Assert + Assert.Equal(expectedResult, actualResult); + } + + [Fact] + public void CreateODataAction_IncludesEverything_ForFullMetadata() + { + // Arrange + string expectedContainerName = "Container"; + string expectedNamespace = "NS"; + string expectedActionName = "Action"; + string expectedTarget = "aa://Target"; + string expectedMetadataPrefix = "http://Metadata"; + + IEdmEntityContainer container = CreateFakeContainer(expectedContainerName); + IEdmAction action = CreateFakeAction(expectedNamespace, expectedActionName, isBindable: true); + + OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri(expectedTarget), + followsConventions: false); + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + annotationsManager.SetOperationLinkBuilder(action, linkBuilder); + annotationsManager.SetIsAlwaysBindable(action); + IEdmModel model = CreateFakeModel(annotationsManager); + + ResourceContext context = CreateContext(model, expectedMetadataPrefix); + context.SerializerContext.MetadataLevel = ODataMetadataLevel.Full; + + // Act + ODataAction actualAction = _serializer.CreateODataAction(action, context); + + // Assert + // In ASP.NET Core, it's using the global UriHelper to pick up the first Router to generate the Uri. + // Owing that there's no router created in this case, a default OData router will be used to generate the Uri. + // The default OData router will add '$metadata' after the route prefix. + string expectedMetadata = expectedMetadataPrefix + "/OData/$metadata#" + expectedNamespace + "." + expectedActionName; + ODataAction expectedAction = new ODataAction + { + Metadata = new Uri(expectedMetadata), + Target = new Uri(expectedTarget), + Title = expectedActionName + }; + + AssertEqual(expectedAction, actualAction); + } + + [Fact] + public void CreateODataAction_OmitsAction_WheOperationLinkBuilderReturnsNull() + { + // Arrange + IEdmAction action = CreateFakeAction("IgnoreAction"); + + OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => null, followsConventions: false); + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + annotationsManager.SetOperationLinkBuilder(action, linkBuilder); + + IEdmModel model = CreateFakeModel(annotationsManager); + + ResourceContext context = CreateContext(model); + context.SerializerContext.MetadataLevel = ODataMetadataLevel.Minimal; + + // Act + ODataAction actualAction = _serializer.CreateODataAction(action, context); + + // Assert + Assert.Null(actualAction); + } + + [Fact] + public void CreateODataAction_ForJsonLight_OmitsContainerName_PerCreateMetadataFragment() + { + // Arrange + string expectedMetadataPrefix = "http://Metadata"; + string expectedNamespace = "NS"; + string expectedActionName = "Action"; + + IEdmEntityContainer container = CreateFakeContainer("ContainerShouldNotAppearInResult"); + IEdmAction action = CreateFakeAction(expectedNamespace, expectedActionName); + + OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri("aa://IgnoreTarget"), + followsConventions: false); + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + annotationsManager.SetOperationLinkBuilder(action, linkBuilder); + + IEdmModel model = CreateFakeModel(annotationsManager); + + ResourceContext context = CreateContext(model, expectedMetadataPrefix); + context.SerializerContext.MetadataLevel = ODataMetadataLevel.Minimal; + + // Act + ODataAction actualAction = _serializer.CreateODataAction(action, context); + + // Assert + Assert.NotNull(actualAction); + // Assert + // In ASP.NET Core, it's using the global UriHelper to pick up the first Router to generate the Uri. + // Owing that there's no router created in this case, a default OData router will be used to generate the Uri. + // The default OData router will add '$metadata' after the route prefix. + string expectedMetadata = expectedMetadataPrefix + "/OData/$metadata#" + expectedNamespace + "." + expectedActionName; + AssertEqual(new Uri(expectedMetadata), actualAction.Metadata); + } + + [Fact] + public void CreateODataAction_SkipsAlwaysAvailableAction_PerShouldOmitAction() + { + // Arrange + IEdmAction action = CreateFakeAction("action"); + + OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri("aa://IgnoreTarget"), + followsConventions: true); + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + annotationsManager.SetOperationLinkBuilder(action, linkBuilder); + annotationsManager.SetIsAlwaysBindable(action); + + IEdmModel model = CreateFakeModel(annotationsManager); + + ResourceContext context = CreateContext(model); + context.SerializerContext.MetadataLevel = ODataMetadataLevel.Minimal; + + // Act + ODataAction actualAction = _serializer.CreateODataAction(action, context); + + // Assert + Assert.Null(actualAction); + } + + [Fact] + public void CreateODataAction_IncludesTitle() + { + // Arrange + string expectedActionName = "Action"; + + IEdmAction action = CreateFakeAction(expectedActionName); + + OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri("aa://IgnoreTarget"), + followsConventions: false); + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + annotationsManager.SetOperationLinkBuilder(action, linkBuilder); + + IEdmModel model = CreateFakeModel(annotationsManager); + + ResourceContext context = CreateContext(model, "http://IgnoreMetadataPath"); + context.SerializerContext.MetadataLevel = ODataMetadataLevel.Full; + + // Act + ODataAction actualAction = _serializer.CreateODataAction(action, context); + + // Assert + Assert.NotNull(actualAction); + Assert.Equal(expectedActionName, actualAction.Title); + } + + [Theory] + [InlineData(ODataMetadataLevel.Minimal)] + [InlineData(ODataMetadataLevel.None)] + public void CreateODataAction_OmitsTitle(ODataMetadataLevel metadataLevel) + { + // Arrange + IEdmAction action = CreateFakeAction("IgnoreAction"); + + OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri("aa://Ignore"), + followsConventions: false); + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + annotationsManager.SetOperationLinkBuilder(action, linkBuilder); + + IEdmModel model = CreateFakeModel(annotationsManager); + + ResourceContext context = CreateContext(model, "http://IgnoreMetadataPath"); + context.SerializerContext.MetadataLevel = (ODataMetadataLevel)metadataLevel; + + // Act + ODataAction actualAction = _serializer.CreateODataAction(action, context); + + // Assert + Assert.NotNull(actualAction); + Assert.Null(actualAction.Title); + } + + [Fact] + public void CreateODataAction_IncludesTarget_IfDoesnotFollowODataConvention() + { + // Arrange + Uri expectedTarget = new Uri("aa://Target"); + + IEdmAction action = CreateFakeAction("IgnoreAction"); + + OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => expectedTarget, followsConventions: false); + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + annotationsManager.SetOperationLinkBuilder(action, linkBuilder); + + IEdmModel model = CreateFakeModel(annotationsManager); + + ResourceContext context = CreateContext(model, "http://IgnoreMetadataPath"); + context.SerializerContext.MetadataLevel = ODataMetadataLevel.Full; + + // Act + ODataAction actualAction = _serializer.CreateODataAction(action, context); + + // Assert + Assert.NotNull(actualAction); + Assert.Equal(expectedTarget, actualAction.Target); + } + + [Theory] + [InlineData(ODataMetadataLevel.Minimal)] + [InlineData(ODataMetadataLevel.None)] + public void CreateODataAction_OmitsAction_WhenFollowingConventions(ODataMetadataLevel metadataLevel) + { + // Arrange + IEdmAction action = CreateFakeAction("IgnoreAction", isBindable: true); + + OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri("aa://Ignore"), + followsConventions: true); + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + annotationsManager.SetOperationLinkBuilder(action, linkBuilder); + + IEdmModel model = CreateFakeModel(annotationsManager); + + ResourceContext context = CreateContext(model, "http://IgnoreMetadataPath"); + context.SerializerContext.MetadataLevel = metadataLevel; + + // Act + ODataAction actualAction = _serializer.CreateODataAction(action, context); + + // Assert + Assert.Null(actualAction); + } + + [Fact] + public void CreateODataFunction_IncludesEverything_ForFullMetadata() + { + // Arrange + string expectedTarget = "aa://Target"; + string expectedMetadataPrefix = "http://Metadata"; + + IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); + IEdmFunction function = new EdmFunction("NS", "Function", returnType, isBound: true, entitySetPathExpression: null, isComposable: false); + + OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri(expectedTarget), followsConventions: false); + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + annotationsManager.SetOperationLinkBuilder(function, linkBuilder); + annotationsManager.SetIsAlwaysBindable(function); + IEdmModel model = CreateFakeModel(annotationsManager); + + ResourceContext context = CreateContext(model, expectedMetadataPrefix); + context.SerializerContext.MetadataLevel = ODataMetadataLevel.Full; + + // Act + ODataFunction actualFunction = _serializer.CreateODataFunction(function, context); + + // Assert + string expectedMetadata = expectedMetadataPrefix + "#NS.Function"; + ODataFunction expectedFunction = new ODataFunction + { + Metadata = new Uri(expectedMetadata), + Target = new Uri(expectedTarget), + Title = "Function" + }; + + AssertEqual(actualFunction, actualFunction); + } + + [Fact] + public void CreateODataFunction_IncludesTitle() + { + // Arrange + string expectedActionName = "Function"; + + IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); + IEdmFunction function = new EdmFunction("NS", "Function", returnType, isBound: true, entitySetPathExpression: null, isComposable: false); + + OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri("aa://IgnoreTarget"), + followsConventions: false); + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + annotationsManager.SetOperationLinkBuilder(function, linkBuilder); + + IEdmModel model = CreateFakeModel(annotationsManager); + + ResourceContext context = CreateContext(model, "http://IgnoreMetadataPath"); + context.SerializerContext.MetadataLevel = ODataMetadataLevel.Full; + + // Act + ODataFunction actualFunction = _serializer.CreateODataFunction(function, context); + + // Assert + Assert.NotNull(actualFunction); + Assert.Equal(expectedActionName, actualFunction.Title); + } + + [Theory] + [InlineData(ODataMetadataLevel.Minimal)] + [InlineData(ODataMetadataLevel.None)] + public void CreateODataFunction_OmitsTitle(ODataMetadataLevel metadataLevel) + { + // Arrange + IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); + IEdmFunction function = new EdmFunction("NS", "Function", returnType, isBound: true, entitySetPathExpression: null, isComposable: false); + + OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri("aa://Ignore"), + followsConventions: false); + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + annotationsManager.SetOperationLinkBuilder(function, linkBuilder); + + IEdmModel model = CreateFakeModel(annotationsManager); + + ResourceContext context = CreateContext(model, "http://IgnoreMetadataPath"); + context.SerializerContext.MetadataLevel = metadataLevel; + + // Act + ODataFunction actualFunction = _serializer.CreateODataFunction(function, context); + + // Assert + Assert.NotNull(actualFunction); + Assert.Null(actualFunction.Title); + } + + [Fact] + public void CreateODataFunction_IncludesTarget_IfDoesnotFollowODataConvention() + { + // Arrange + Uri expectedTarget = new Uri("aa://Target"); + IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); + IEdmFunction function = new EdmFunction("NS", "Function", returnType, isBound: true, entitySetPathExpression: null, isComposable: false); + + OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => expectedTarget, followsConventions: false); + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + annotationsManager.SetOperationLinkBuilder(function, linkBuilder); + + IEdmModel model = CreateFakeModel(annotationsManager); + + ResourceContext context = CreateContext(model, "http://IgnoreMetadataPath"); + context.SerializerContext.MetadataLevel = ODataMetadataLevel.Full; + + // Act + ODataFunction actualFunction = _serializer.CreateODataFunction(function, context); + + // Assert + Assert.NotNull(actualFunction); + Assert.Equal(expectedTarget, actualFunction.Target); + } + + [Theory] + [InlineData(ODataMetadataLevel.Minimal)] + [InlineData(ODataMetadataLevel.None)] + public void CreateODataFunction_OmitsAction_WhenFollowingConventions(ODataMetadataLevel metadataLevel) + { + // Arrange + IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); + IEdmFunction function = new EdmFunction("NS", "Function", returnType, isBound: true, entitySetPathExpression: null, isComposable: false); + + OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri("aa://Ignore"), + followsConventions: true); + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + annotationsManager.SetOperationLinkBuilder(function, linkBuilder); + + IEdmModel model = CreateFakeModel(annotationsManager); + + ResourceContext context = CreateContext(model, "http://IgnoreMetadataPath"); + context.SerializerContext.MetadataLevel = metadataLevel; + + // Act + ODataFunction actualFunction = _serializer.CreateODataFunction(function, context); + + // Assert + Assert.Null(actualFunction); + } + + [Fact] + public void CreateMetadataFragment_IncludesNamespaceAndName() + { + // Arrange + string expectedActionName = "Action"; + string expectedNamespace = "NS"; + + IEdmEntityContainer container = CreateFakeContainer("ContainerShouldNotAppearInResult"); + IEdmAction action = CreateFakeAction(expectedNamespace, expectedActionName); + + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + IEdmModel model = CreateFakeModel(annotationsManager); + + // Act + string actualFragment = ODataResourceSerializer.CreateMetadataFragment(action); + + // Assert + Assert.Equal(expectedNamespace + "." + expectedActionName, actualFragment); + } + + [Theory] + [InlineData(ODataMetadataLevel.Full, false, false)] + [InlineData(ODataMetadataLevel.Full, true, false)] + [InlineData(ODataMetadataLevel.Minimal, false, false)] + [InlineData(ODataMetadataLevel.Minimal, true, true)] + [InlineData(ODataMetadataLevel.None, false, false)] + [InlineData(ODataMetadataLevel.None, true, true)] + public void TestShouldOmitAction(ODataMetadataLevel metadataLevel, + bool followsConventions, bool expectedResult) + { + // Arrange + IEdmActionImport action = CreateFakeActionImport(true); + IEdmDirectValueAnnotationsManager annonationsManager = CreateFakeAnnotationsManager(); + + IEdmModel model = CreateFakeModel(annonationsManager); + + OperationLinkBuilder builder = new OperationLinkBuilder((ResourceContext a) => { throw new NotImplementedException(); }, + followsConventions); + + // Act + bool actualResult = ODataResourceSerializer.ShouldOmitOperation(action.Action, builder, metadataLevel); + + // Assert + Assert.Equal(expectedResult, actualResult); + } + + [Fact] + public void TestSetTitleAnnotation() + { + // Arrange + IEdmActionImport action = CreateFakeActionImport(true); + IEdmDirectValueAnnotationsManager annonationsManager = CreateFakeAnnotationsManager(); + IEdmModel model = CreateFakeModel(annonationsManager); + string expectedTitle = "The title"; + model.SetOperationTitleAnnotation(action.Operation, new OperationTitleAnnotation(expectedTitle)); + ODataAction odataAction = new ODataAction(); + + // Act + ODataResourceSerializer.EmitTitle(model, action.Operation, odataAction); + + // Assert + Assert.Equal(expectedTitle, odataAction.Title); + } + + [Fact] + public void TestSetTitleAnnotation_UsesNameIfNoTitleAnnotationIsPresent() + { + // Arrange + IEdmActionImport action = CreateFakeActionImport(CreateFakeContainer("Container"), "Action"); + IEdmDirectValueAnnotationsManager annonationsManager = CreateFakeAnnotationsManager(); + IEdmModel model = CreateFakeModel(annonationsManager); + ODataAction odataAction = new ODataAction(); + + // Act + ODataResourceSerializer.EmitTitle(model, action.Operation, odataAction); + + // Assert + Assert.Equal(action.Operation.Name, odataAction.Title); + } + + [Fact] + public async Task WriteObjectInlineAsync_SetsParentContext_ForExpandedNavigationProperties() + { + // Arrange + ODataWriter mockWriter = new Mock().Object; + Mock expandedItemSerializer = new Mock(ODataPayloadKind.ResourceSet); + Mock serializerProvider = new Mock(); + serializerProvider.Setup(p => p.GetEdmTypeSerializer(It.IsAny())) + .Returns(expandedItemSerializer.Object); + + SelectExpandNode selectExpandNode = new SelectExpandNode + { + ExpandedProperties = new Dictionary + { + { _ordersNavigation, null } + } + }; + Mock serializer = new Mock(serializerProvider.Object); + serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); + serializer.Setup(s => s.CreateResource(selectExpandNode, _entityContext)).Returns(new ODataResource()); + serializer.CallBase = true; + + // Act + await serializer.Object.WriteObjectInlineAsync(_customer, _customerType, mockWriter, _writeContext); + + // Assert + expandedItemSerializer.Verify( + s => s.WriteObjectInlineAsync(It.IsAny(), It.IsAny(), mockWriter, + It.Is(c => c.ExpandedResource.SerializerContext == _writeContext))); + } + + [Fact] + public async Task WriteObjectInlineAsync_Writes_Nested_Entities_Without_NavigationSource() + { + // Arrange + ODataModelBuilder builder = new ODataConventionModelBuilder(); + builder.Namespace = "Default"; + builder.EntityType(); + builder.ComplexType(); + var model = builder.GetEdmModel(); + + var result = new Result + { + Title = "myResult", + Products = new Product[] + { + new Product + { + ProductID = 1 + }, + new Product + { + ProductID = 2 + } + } + }; + + var resultType = model.FindType("Default.Result") as IEdmComplexType; + var resultTypeReference = new EdmComplexTypeReference(resultType as IEdmComplexType, false); + var titleProperty = resultTypeReference.FindProperty("Title") as IEdmStructuralProperty; + var productsProperty = resultTypeReference.FindNavigationProperty("Products"); + var selectExpand = new SelectExpandClause(new SelectItem[] + { + new PathSelectItem(new ODataSelectPath(new PropertySegment(titleProperty))), + new ExpandedNavigationSelectItem(new ODataExpandPath(new NavigationPropertySegment(productsProperty, null)),null,null) + }, + false); + + var writeContext = new ODataSerializerContext() + { + Model = model, + SelectExpandClause = selectExpand + }; + + using (var stream = new MemoryStream()) + { + IODataResponseMessage responseMessage = new ODataMessageWrapper(stream); + ODataUri uri = new ODataUri { ServiceRoot = new Uri("http://myService", UriKind.Absolute) }; + ODataMessageWriterSettings settings = new ODataMessageWriterSettings + { + ODataUri = uri + }; + + using (ODataMessageWriter messageWriter = new ODataMessageWriter(responseMessage, settings)) + { + ODataWriter writer = await messageWriter.CreateODataResourceWriterAsync(null, resultType as IEdmComplexType); + ODataResourceSerializer serializer = new ODataResourceSerializer(_serializerProvider); + + // Act + await serializer.WriteObjectInlineAsync(result, resultTypeReference, writer, writeContext); + + // Assert + stream.Position = 0; + using (StreamReader reader = new StreamReader(stream, leaveOpen: true)) + { + string response = reader.ReadToEnd(); + Assert.Contains(@"""ProductID"":1", response); + Assert.Contains(@"""ProductID"":2", response); + } + } + } + } + + [Fact] + public void CreateSelectExpandNode_Caches_SelectExpandNode() + { + // Arrange + IEdmEntityTypeReference customerType = _customerSet.EntityType.AsReference(); + ResourceContext entity1 = new ResourceContext(_writeContext, customerType, new Customer()); + ResourceContext entity2 = new ResourceContext(_writeContext, customerType, new Customer()); + + // Act + var selectExpandNode1 = _serializer.CreateSelectExpandNode(entity1); + var selectExpandNode2 = _serializer.CreateSelectExpandNode(entity2); + + // Assert + Assert.Same(selectExpandNode1, selectExpandNode2); + } + + [Fact] + public void CreateSelectExpandNode_ReturnsDifferentSelectExpandNode_IfEntityTypeIsDifferent() + { + // Arrange + IEdmEntityType customerType = _customerSet.EntityType; + IEdmEntityType derivedCustomerType = new EdmEntityType("NS", "DerivedCustomer", customerType); + + ResourceContext entity1 = new ResourceContext(_writeContext, customerType.AsReference(), new Customer()); + ResourceContext entity2 = new ResourceContext(_writeContext, derivedCustomerType.AsReference(), new Customer()); + + // Act + var selectExpandNode1 = _serializer.CreateSelectExpandNode(entity1); + var selectExpandNode2 = _serializer.CreateSelectExpandNode(entity2); + + // Assert + Assert.NotSame(selectExpandNode1, selectExpandNode2); + } + + [Fact] + public void CreateSelectExpandNode_ReturnsDifferentSelectExpandNode_IfSelectExpandClauseIsDifferent() + { + // Arrange + IEdmEntityType customerType = _customerSet.EntityType; + + ResourceContext entity1 = new ResourceContext(_writeContext, customerType.AsReference(), new Customer()); + ResourceContext entity2 = new ResourceContext(_writeContext, customerType.AsReference(), new Customer()); + + // Act + _writeContext.SelectExpandClause = new SelectExpandClause(new SelectItem[0], allSelected: true); + var selectExpandNode1 = _serializer.CreateSelectExpandNode(entity1); + _writeContext.SelectExpandClause = new SelectExpandClause(new SelectItem[0], allSelected: false); + var selectExpandNode2 = _serializer.CreateSelectExpandNode(entity2); + + // Assert + Assert.NotSame(selectExpandNode1, selectExpandNode2); + } + + private static IEdmNavigationProperty CreateFakeNavigationProperty(string name, IEdmTypeReference type) + { + Mock property = new Mock(); + property.Setup(p => p.Name).Returns(name); + property.Setup(p => p.Type).Returns(type); + return property.Object; + } + + private static void AssertEqual(ODataOperation expected, ODataOperation actual) + { + if (expected == null) + { + Assert.Null(actual); + return; + } + + Assert.NotNull(actual); + AssertEqual(expected.Metadata, actual.Metadata); + AssertEqual(expected.Target, actual.Target); + Assert.Equal(expected.Title, actual.Title); + } + + private static void AssertEqual(Uri expected, Uri actual) + { + if (expected == null) + { + Assert.Null(actual); + return; + } + + Assert.NotNull(actual); + Assert.Equal(expected.AbsoluteUri, actual.AbsoluteUri); + } + + private static ResourceContext CreateContext(IEdmModel model) + { + return new ResourceContext + { + EdmModel = model + }; + } + + private static ResourceContext CreateContext(IEdmModel model, string expectedMetadataPrefix) + { + var request = RequestFactory.Create("get", expectedMetadataPrefix, opt => opt.AddRouteComponents("OData", model)); + request.ODataFeature().RoutePrefix = "OData"; + request.ODataFeature().Model = model; + return new ResourceContext + { + EdmModel = model, + Request = request + }; + } + + private static IEdmEntityType CreateEntityTypeWithName(string typeName) + { + IEdmEntityType entityType = new EdmEntityType("NS", typeName); + return entityType; + } + + private static IEdmDirectValueAnnotationsManager CreateFakeAnnotationsManager() + { + return new FakeAnnotationsManager(); + } + + private static IEdmEntityContainer CreateFakeContainer(string name) + { + Mock mock = new Mock(); + mock.Setup(o => o.Name).Returns(name); + return mock.Object; + } + + private static IEdmActionImport CreateFakeActionImport(IEdmEntityContainer container, string name) + { + Mock mockAction = new Mock(); + mockAction.Setup(o => o.IsBound).Returns(true); + mockAction.Setup(o => o.Name).Returns(name); + Mock mock = new Mock(); + mock.Setup(o => o.Container).Returns(container); + mock.Setup(o => o.Name).Returns(name); + mock.Setup(o => o.Action).Returns(mockAction.Object); + mock.Setup(o => o.Operation).Returns(mockAction.Object); + return mock.Object; + } + + private static IEdmActionImport CreateFakeActionImport(IEdmEntityContainer container, string name, bool isBindable) + { + Mock mock = new Mock(); + mock.Setup(o => o.Container).Returns(container); + mock.Setup(o => o.Name).Returns(name); + Mock mockAction = new Mock(); + mockAction.Setup(o => o.IsBound).Returns(isBindable); + mock.Setup(o => o.Action).Returns(mockAction.Object); + return mock.Object; + } + + private static IEdmActionImport CreateFakeActionImport(bool isBindable) + { + Mock mock = new Mock(); + Mock mockAction = new Mock(); + mockAction.Setup(o => o.IsBound).Returns(isBindable); + mock.Setup(o => o.Action).Returns(mockAction.Object); + mock.Setup(o => o.Operation).Returns(mockAction.Object); + return mock.Object; + } + + private static IEdmAction CreateFakeAction(string name) + { + return CreateFakeAction(nameSpace: null, name: name, isBindable: true); + } + + private static IEdmAction CreateFakeAction(string name, bool isBindable) + { + return CreateFakeAction(nameSpace: null, name: name, isBindable: isBindable); + } + + private static IEdmAction CreateFakeAction(string nameSpace, string name) + { + return CreateFakeAction(nameSpace, name, isBindable: true); + } + + private static IEdmAction CreateFakeAction(string nameSpace, string name, bool isBindable) + { + Mock mockAction = new Mock(); + mockAction.SetupGet(o => o.Namespace).Returns(nameSpace); + mockAction.SetupGet(o => o.Name).Returns(name); + mockAction.Setup(o => o.IsBound).Returns(isBindable); + Mock mockParameter = new Mock(); + mockParameter.SetupGet(o => o.DeclaringOperation).Returns(mockAction.Object); + Mock mockEntityTyeRef = new Mock(); + mockEntityTyeRef.Setup(o => o.Definition).Returns(new Mock().Object); + mockParameter.SetupGet(o => o.Type).Returns(mockEntityTyeRef.Object); + mockAction.SetupGet(o => o.Parameters).Returns(new[] { mockParameter.Object }); + return mockAction.Object; + } + + private static IEdmModel CreateFakeModel(IEdmDirectValueAnnotationsManager annotationsManager) + { + Mock model = new Mock(); + model.Setup(m => m.DirectValueAnnotationsManager).Returns(annotationsManager); + return model.Object; + } + + private static IServiceProvider GetServiceProvider() + { + IServiceCollection services = new ServiceCollection(); + + services.AddSingleton(); + + // Serializers. + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + return services.BuildServiceProvider(); + } + + private class Customer + { + public Customer() + { + this.Orders = new List(); + } + public int ID { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string City { get; set; } + public Size Size { get; set; } + public IList Orders { get; private set; } + } + + private class Size() + { + public decimal Height { get; set; } + public decimal Weight { get; set; } + } + + + private class SpecialCustomer + { + public IList SpecialOrders { get; private set; } + } + + private class Order + { + public int ID { get; set; } + public string Name { get; set; } + public string Shipment { get; set; } + public Customer Customer { get; set; } + } + + private class Result + { + public string Title { get; set; } + public IList Products { get; set; } + } + + private class SpecialOrder + { + public SpecialCustomer SpecialCustomer { get; set; } + } + + private class FakeBindableOperationFinder : BindableOperationFinder + { + private IEdmOperation[] _operations; + + public FakeBindableOperationFinder(params IEdmOperation[] operations) + : base(EdmCoreModel.Instance) + { + _operations = operations; + } + + public override IEnumerable FindOperations(IEdmEntityType entityType) + { + return _operations; + } + } + + private class FakeAnnotationsManager : IEdmDirectValueAnnotationsManager + { + IDictionary, object> annotations = + new Dictionary, object>(); + + public object GetAnnotationValue(IEdmElement element, string namespaceName, string localName) + { + object value; + + if (!annotations.TryGetValue(CreateKey(element, namespaceName, localName), out value)) + { + return null; + } + + return value; + } + + public object[] GetAnnotationValues(IEnumerable annotations) + { + throw new NotImplementedException(); + } + + public IEnumerable GetDirectValueAnnotations(IEdmElement element) + { + throw new NotImplementedException(); + } + + public void SetAnnotationValue(IEdmElement element, string namespaceName, string localName, object value) + { + annotations[CreateKey(element, namespaceName, localName)] = value; + } + + public void SetAnnotationValues(IEnumerable annotations) + { + throw new NotImplementedException(); + } + + private static Tuple CreateKey(IEdmElement element, string namespaceName, + string localName) + { + return new Tuple(element, namespaceName, localName); + } + } + +} diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataDeltaResourceSetSerializerTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataDeltaResourceSetSerializerTests.cs index 12d5eb378..20b57601c 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataDeltaResourceSetSerializerTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataDeltaResourceSetSerializerTests.cs @@ -111,7 +111,7 @@ await ExceptionAssert.ThrowsArgumentNullAsync( } [Fact] - public async Task WriteObjectAsync_Throws_EntitySetMissingDuringSerialization() + public async Task WriteObjectAsync_Throws_ModelMissingDuringSerialization() { // Arrange object graph = new object(); @@ -119,9 +119,9 @@ public async Task WriteObjectAsync_Throws_EntitySetMissingDuringSerialization() ODataDeltaResourceSetSerializer serializer = new ODataDeltaResourceSetSerializer(provider.Object); // Act & Assert - await ExceptionAssert.ThrowsAsync( + await ExceptionAssert.ThrowsAsync( () => serializer.WriteObjectAsync(graph: graph, type: null, messageWriter: ODataTestUtil.GetMockODataMessageWriter(), writeContext: new ODataSerializerContext()), - "The related entity set could not be found from the OData path. The related entity set is required to serialize the payload."); + "The request must have an associated EDM model. Consider registering Edm model calling AddOData()."); } [Fact] diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataResourceSerializerTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataResourceSerializerTests.cs index 5d9b46cdc..aabacb3cf 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataResourceSerializerTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataResourceSerializerTests.cs @@ -9,8 +9,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Reflection.Metadata; -using System.Runtime.CompilerServices; using System.Runtime.Serialization; using System.Threading.Tasks; using Microsoft.AspNetCore.OData.Abstracts; @@ -23,7 +21,6 @@ using Microsoft.AspNetCore.OData.Tests.Edm; using Microsoft.AspNetCore.OData.Tests.Extensions; using Microsoft.AspNetCore.OData.Tests.Models; -using Microsoft.AspNetCore.Rewrite; using Microsoft.Extensions.DependencyInjection; using Microsoft.Net.Http.Headers; using Microsoft.OData; @@ -69,6 +66,7 @@ public ODataResourceSerializerTests() FirstName = "Foo", LastName = "Bar", ID = 10, + Size = new Size { Weight = 180, Height = 72 } }; _orderSet = _model.EntityContainer.FindEntitySet("Orders"); @@ -183,7 +181,10 @@ public async Task WriteObjectInlineAsync_Calls_CreateSelectExpandNode() { // Arrange Mock serializerProvider = new Mock(); - serializerProvider.Setup(s => s.GetEdmTypeSerializer(It.IsAny())).Returns(new ODataPrimitiveSerializer()); + serializerProvider.Setup( + s => s.GetEdmTypeSerializer(It.IsAny())).Returns(new ODataResourceSerializer(serializerProvider.Object)); + serializerProvider.Setup( + s => s.GetEdmTypeSerializer(It.IsAny())).Returns(new ODataPrimitiveSerializer()); Mock serializer = new Mock(serializerProvider.Object); ODataWriter writer = new Mock().Object; @@ -223,6 +224,11 @@ public async Task WriteObjectInlineAsync_WritesODataResourceFrom_CreateResource( // Arrange ODataResource entry = new ODataResource(); Mock serializerProvider = new Mock(); + serializerProvider.Setup(s => + s.GetEdmTypeSerializer(It.IsAny())).Returns(new ODataResourceSerializer(serializerProvider.Object)); + serializerProvider.Setup(s => + s.GetEdmTypeSerializer(It.IsAny())).Returns(new ODataPrimitiveSerializer()); + Mock serializer = new Mock(serializerProvider.Object); Mock writer = new Mock(); @@ -2565,6 +2571,7 @@ public Customer() public string FirstName { get; set; } public string LastName { get; set; } public string City { get; set; } + public Size Size { get; set; } public IList Orders { get; private set; } } @@ -2581,6 +2588,12 @@ private class Order public Customer Customer { get; set; } } + private class Size + { + public Decimal Height { get; set; } + public Decimal Weight { get; set; } + } + private class Result { public string Title { get; set; } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/SerializationTestsHelpers.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/SerializationTestsHelpers.cs index 6ad3de765..27d4417e4 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/SerializationTestsHelpers.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/SerializationTestsHelpers.cs @@ -17,10 +17,17 @@ internal class SerializationTestsHelpers public static IEdmModel SimpleCustomerOrderModel() { var model = new EdmModel(); + + var sizeType = new EdmComplexType("Default", "Size"); + sizeType.AddStructuralProperty("Height", EdmPrimitiveTypeKind.Decimal); + sizeType.AddStructuralProperty("Weight", EdmPrimitiveTypeKind.Decimal); + model.AddElement(sizeType); + var customerType = new EdmEntityType("Default", "Customer"); customerType.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32); customerType.AddStructuralProperty("FirstName", EdmPrimitiveTypeKind.String); customerType.AddStructuralProperty("LastName", EdmPrimitiveTypeKind.String); + customerType.AddStructuralProperty("Size", new EdmComplexTypeReference(sizeType,true)); IEdmTypeReference primitiveTypeReference = EdmCoreModel.Instance.GetPrimitive( EdmPrimitiveTypeKind.String, isNullable: true); diff --git a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net8.bsl b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net8.bsl index 60b3e8ea4..97d705abe 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net8.bsl +++ b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net8.bsl @@ -2170,12 +2170,18 @@ public class Microsoft.AspNetCore.OData.Formatter.Serialization.ODataDeltaResour public ODataDeltaResourceSetSerializer (Microsoft.AspNetCore.OData.Formatter.Serialization.IODataSerializerProvider serializerProvider) public virtual Microsoft.OData.ODataDeltaResourceSet CreateODataDeltaResourceSet (System.Collections.IEnumerable feedInstance, Microsoft.OData.Edm.IEdmCollectionTypeReference feedType, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) + [ + AsyncStateMachineAttribute(), + ] + public virtual System.Threading.Tasks.Task WriteDeletedResourceAsync (object value, Microsoft.OData.Edm.IEdmStructuredTypeReference expectedType, Microsoft.OData.ODataWriter writer, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) + [ AsyncStateMachineAttribute(), ] public virtual System.Threading.Tasks.Task WriteDeltaDeletedLinkAsync (object value, Microsoft.OData.ODataWriter writer, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) [ + ObsoleteAttribute(), AsyncStateMachineAttribute(), ] public virtual System.Threading.Tasks.Task WriteDeltaDeletedResourceAsync (object value, Microsoft.OData.ODataWriter writer, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) @@ -2269,6 +2275,7 @@ public class Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSer public virtual void AppendDynamicProperties (Microsoft.OData.ODataResource resource, Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode selectExpandNode, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) public virtual Microsoft.OData.ODataNestedResourceInfo CreateComplexNestedResourceInfo (Microsoft.OData.Edm.IEdmStructuralProperty complexProperty, Microsoft.OData.UriParser.PathSelectItem pathSelectItem, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) public virtual Microsoft.OData.ODataProperty CreateComputedProperty (string propertyName, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) + public virtual Microsoft.OData.ODataDeletedResource CreateDeletedResource (System.Uri id, Microsoft.OData.DeltaDeletedEntryReason reason, Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode selectExpandNode, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) public virtual Microsoft.OData.ODataNestedResourceInfo CreateDynamicComplexNestedResourceInfo (string propertyName, object propertyValue, Microsoft.OData.Edm.IEdmTypeReference edmType, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) public virtual string CreateETag (Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) public virtual Microsoft.OData.ODataNestedResourceInfo CreateNavigationLink (Microsoft.OData.Edm.IEdmNavigationProperty navigationProperty, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) @@ -2280,7 +2287,6 @@ public class Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSer public virtual Microsoft.OData.ODataProperty CreateStructuralProperty (Microsoft.OData.Edm.IEdmStructuralProperty structuralProperty, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) public virtual Microsoft.OData.ODataNestedResourceInfo CreateUntypedNestedResourceInfo (Microsoft.OData.Edm.IEdmStructuralProperty structuralProperty, object propertyValue, Microsoft.OData.Edm.IEdmTypeReference valueType, Microsoft.OData.UriParser.PathSelectItem pathSelectItem, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) public virtual object CreateUntypedPropertyValue (Microsoft.OData.Edm.IEdmStructuralProperty structuralProperty, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext, out Microsoft.OData.Edm.IEdmTypeReference& actualType) - protected virtual bool ShouldWriteNavigation (Microsoft.OData.ODataNestedResourceInfo navigationLink, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) [ AsyncStateMachineAttribute(), ]