From 95b79d7945463a9b0ecdcba18b8b903d57f304bc Mon Sep 17 00:00:00 2001 From: mikepizzo Date: Tue, 3 Dec 2024 03:03:31 -0800 Subject: [PATCH] Fix Writing Delta Responses -only write properties in deltaset items that have been set -support writing additional properties for deleted entities -nested resource sets that aren't deltasets should be written w/o @delta -factored out ODataDeletedResourceSerializer -support writing delta payloads w/out knowing navigation source (i.e., when serializing results from a function) --- .../ODataDeletedResourceSerializer.cs | 432 ++++++++++++++++++ .../ODataDeltaResourceSetSerializer.cs | 51 ++- .../Serialization/ODataResourceSerializer.cs | 196 +++++--- .../ODataResourceSetSerializer.cs | 55 +-- .../Formatter/Value/EdmObjectExtensions.cs | 4 +- .../PublicAPI.Unshipped.txt | 11 + .../BulkOperation/BulkOperationTest.cs | 2 +- .../DeltaToken/DeltaTokenQueryTests.cs | 12 +- .../ODataDeltaResourceSetSerializerTests.cs | 6 +- ...rosoft.AspNetCore.OData.PublicApi.Net8.bsl | 26 ++ 10 files changed, 671 insertions(+), 124 deletions(-) create mode 100644 src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeletedResourceSerializer.cs diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeletedResourceSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeletedResourceSerializer.cs new file mode 100644 index 000000000..a6efe2c5c --- /dev/null +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeletedResourceSerializer.cs @@ -0,0 +1,432 @@ +//----------------------------------------------------------------------------- +// +// 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.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Linq; +using System.Runtime.Serialization; +using Microsoft.AspNetCore.OData.Edm; +using Microsoft.AspNetCore.OData.Extensions; +using Microsoft.AspNetCore.OData.Formatter.Value; +using Microsoft.AspNetCore.OData.Routing; +using Microsoft.OData; +using Microsoft.OData.ModelBuilder; +using Microsoft.OData.Edm; +using Microsoft.OData.UriParser; +using System.Threading.Tasks; +using Microsoft.AspNetCore.OData.Deltas; + +namespace Microsoft.AspNetCore.OData.Formatter.Serialization; + +/// +/// ODataSerializer for serializing instances of /> +/// +[SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Relies on many ODataLib classes.")] +public class ODataDeletedResourceSerializer : ODataEdmTypeSerializer +{ + private const string Resource = "DeletedResource"; + + /// + public ODataDeletedResourceSerializer(IODataSerializerProvider serializerProvider) + : base(ODataPayloadKind.Resource, serializerProvider) + { + } + + /// + public override async Task WriteObjectAsync(object graph, Type type, ODataMessageWriter messageWriter, + ODataSerializerContext writeContext) + { + if (messageWriter == null) + { + throw Error.ArgumentNull(nameof(messageWriter)); + } + + if (writeContext == null) + { + throw Error.ArgumentNull(nameof(writeContext)); + } + + bool isUntypedPath = writeContext.Path.IsUntypedPropertyPath(); + IEdmTypeReference edmType = writeContext.GetEdmType(graph, type, isUntypedPath); + Contract.Assert(edmType != null); + + IEdmNavigationSource navigationSource = writeContext.NavigationSource; + ODataWriter writer = await messageWriter.CreateODataResourceWriterAsync(navigationSource, edmType.ToStructuredType()) + .ConfigureAwait(false); + await WriteObjectInlineAsync(graph, edmType, writer, writeContext).ConfigureAwait(false); + } + + /// + public override async Task WriteObjectInlineAsync(object graph, IEdmTypeReference expectedType, ODataWriter writer, + ODataSerializerContext writeContext) + { + if (writer == null) + { + throw Error.ArgumentNull(nameof(writer)); + } + + if (writeContext == null) + { + throw Error.ArgumentNull(nameof(writeContext)); + } + + if (graph == null || graph is NullEdmComplexObject) + { + throw new SerializationException(Error.Format(SRResources.CannotSerializerNull, Resource)); + } + else + { + await WriteDeletedResourceAsync(graph, writer, writeContext, expectedType).ConfigureAwait(false); + } + } + + private async Task WriteDeletedResourceAsync(object graph, ODataWriter writer, ODataSerializerContext writeContext, + IEdmTypeReference expectedType) + { + Contract.Assert(writeContext != null); + + //TODO: do we need this? + //if (graph.GetType().IsDynamicTypeWrapper()) + //{ + // await new ODataResourceSerializer(SerializerProvider).WriteDynamicTypeResourceAsync(graph, writer, expectedType, writeContext).ConfigureAwait(false); + // return; + //} + + IEdmStructuredTypeReference structuredType = ODataResourceSerializer.GetResourceType(graph, writeContext); + ResourceContext resourceContext = new ResourceContext(writeContext, structuredType, graph); + + SelectExpandNode selectExpandNode = CreateSelectExpandNode(resourceContext); + if (selectExpandNode != null) + { + ODataDeletedResource odataDeletedResource; + + if (graph is EdmDeltaDeletedResourceObject edmDeltaDeletedEntity) + { + odataDeletedResource = CreateDeletedResource(edmDeltaDeletedEntity.Id, edmDeltaDeletedEntity.Reason ?? DeltaDeletedEntryReason.Deleted, selectExpandNode, resourceContext); + if (edmDeltaDeletedEntity.NavigationSource != null) + { + 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 + { + throw new SerializationException(Error.Format(SRResources.CannotWriteType, GetType().Name, graph?.GetType().FullName)); + } + + bool isDelta = (graph is IDelta || graph is IEdmChangedObject); + await writer.WriteStartAsync(odataDeletedResource).ConfigureAwait(false); + await new ODataResourceSerializer(SerializerProvider).WriteResourceContent(writer, selectExpandNode, resourceContext, isDelta); + await writer.WriteEndAsync().ConfigureAwait(false); + } + } + + /// + /// Creates the that describes the set of properties and actions to select and expand while writing this entity. + /// + /// Contains the entity instance being written and the context. + /// + /// The that describes the set of properties and actions to select and expand while writing this entity. + /// + public virtual SelectExpandNode CreateSelectExpandNode(ResourceContext resourceContext) + { + if (resourceContext == null) + { + throw Error.ArgumentNull(nameof(resourceContext)); + } + + ODataSerializerContext writeContext = resourceContext.SerializerContext; + IEdmStructuredType structuredType = resourceContext.StructuredType; + + object selectExpandNode; + + Tuple key = Tuple.Create(writeContext.SelectExpandClause, structuredType); + if (!writeContext.Items.TryGetValue(key, out selectExpandNode)) + { + // cache the selectExpandNode so that if we are writing a feed we don't have to construct it again. + selectExpandNode = new SelectExpandNode(structuredType, writeContext); + writeContext.Items[key] = selectExpandNode; + } + + return selectExpandNode as SelectExpandNode; + } + + /// + /// 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 + }; + + ODataResourceSerializer.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. + AppendDynamicProperties(resource, selectExpandNode, resourceContext); + + return resource; + } + + /// + /// 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. + [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Relies on many classes.")] + [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "These are simple conversion function and cannot be split up.")] + public virtual void AppendDynamicProperties(ODataDeletedResource resource, SelectExpandNode selectExpandNode, + ResourceContext resourceContext) + { + Contract.Assert(resource != null); + Contract.Assert(selectExpandNode != null); + Contract.Assert(resourceContext != null); + + ODataResourceSerializer.AppendDynamicPropertiesInternal(resource, selectExpandNode, resourceContext, SerializerProvider); + } + + /// + /// Creates the ETag for the given entity. + /// + /// The context for the resource instance being written. + /// The created ETag. + public virtual string CreateETag(ResourceContext resourceContext) + { + if (resourceContext == null) + { + throw Error.ArgumentNull(nameof(resourceContext)); + } + + if (resourceContext.Request != null) + { + IEdmModel model = resourceContext.EdmModel; + IEdmNavigationSource navigationSource = resourceContext.NavigationSource; + + IEnumerable concurrencyProperties; + if (model != null && navigationSource != null) + { + concurrencyProperties = model.GetConcurrencyProperties(navigationSource); + } + else + { + concurrencyProperties = Enumerable.Empty(); + } + + IDictionary properties = null; + foreach (IEdmStructuralProperty etagProperty in concurrencyProperties) + { + properties ??= new SortedDictionary(); + + properties.Add(etagProperty.Name, resourceContext.GetPropertyValue(etagProperty.Name)); + } + + if (properties != null) + { + return resourceContext.Request.CreateETag(properties, resourceContext.TimeZone); + } + } + + return null; + } + + //TODO: call method in ODataResourceSerializer + private IEnumerable CreateStructuralPropertyBag(SelectExpandNode selectExpandNode, ResourceContext resourceContext) + { + Contract.Assert(selectExpandNode != null); + Contract.Assert(resourceContext != null); + + int propertiesCount = (selectExpandNode.SelectedStructuralProperties?.Count ?? 0) + (selectExpandNode.SelectedComputedProperties?.Count ?? 0); + List properties = new List(propertiesCount); + + if (selectExpandNode.SelectedStructuralProperties != null) + { + IEnumerable structuralProperties = selectExpandNode.SelectedStructuralProperties; + + if (null != resourceContext.EdmObject && resourceContext.EdmObject.IsDeltaResource()) + { + 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) || p.IsKey()); + } + } + + foreach (IEdmStructuralProperty structuralProperty in structuralProperties) + { + if (structuralProperty.Type != null && structuralProperty.Type.IsStream()) + { + // skip the stream property, the stream property is written in its own logic + continue; + } + + if (structuralProperty.Type != null && + (structuralProperty.Type.IsUntyped() || structuralProperty.Type.IsCollectionUntyped())) + { + // skip it here, we use a different method to write all 'declared' untyped properties + continue; + } + + ODataProperty property = CreateStructuralProperty(structuralProperty, resourceContext); + if (property != null) + { + properties.Add(property); + } + } + } + + // Try to add computed properties + if (selectExpandNode.SelectedComputedProperties != null) + { + foreach (string propertyName in selectExpandNode.SelectedComputedProperties) + { + ODataProperty property = CreateComputedProperty(propertyName, resourceContext); + if (property != null) + { + properties.Add(property); + } + } + } + + return properties; + } + + /// + /// Creates the to be written for the given resource. + /// + /// The computed property being written. + /// The context for the resource instance being written. + /// The to write. + public virtual ODataProperty CreateComputedProperty(string propertyName, ResourceContext resourceContext) + { + if (string.IsNullOrWhiteSpace(propertyName)) + { + throw Error.ArgumentNullOrEmpty(nameof(propertyName)); + } + + if (resourceContext == null) + { + throw Error.ArgumentNull(nameof(resourceContext)); + } + + // The computed value is from the Linq expression binding. + object propertyValue = resourceContext.GetPropertyValue(propertyName); + if (propertyValue == null) + { + return new ODataProperty { Name = propertyName, Value = null }; + } + + ODataSerializerContext writeContext = resourceContext.SerializerContext; + + IEdmTypeReference edmTypeReference = resourceContext.SerializerContext.GetEdmType(propertyValue, propertyValue.GetType()); + if (edmTypeReference == null) + { + throw Error.NotSupported(SRResources.TypeOfDynamicPropertyNotSupported, propertyValue.GetType().FullName, propertyName); + } + + IODataEdmTypeSerializer serializer = SerializerProvider.GetEdmTypeSerializer(edmTypeReference); + if (serializer == null) + { + throw new SerializationException(Error.Format(SRResources.TypeCannotBeSerialized, edmTypeReference.FullName())); + } + + return serializer.CreateProperty(propertyValue, edmTypeReference, propertyName, writeContext); + } + + /// + /// Creates the to be written for the given entity and the structural property. + /// + /// The EDM structural property being written. + /// The context for the entity instance being written. + /// The to write. + public virtual ODataProperty CreateStructuralProperty(IEdmStructuralProperty structuralProperty, ResourceContext resourceContext) + { + if (structuralProperty == null) + { + throw Error.ArgumentNull(nameof(structuralProperty)); + } + if (resourceContext == null) + { + throw Error.ArgumentNull(nameof(resourceContext)); + } + + ODataSerializerContext writeContext = resourceContext.SerializerContext; + + IODataEdmTypeSerializer serializer = SerializerProvider.GetEdmTypeSerializer(structuralProperty.Type); + if (serializer == null) + { + throw new SerializationException( + Error.Format(SRResources.TypeCannotBeSerialized, structuralProperty.Type.FullName())); + } + + object propertyValue = resourceContext.GetPropertyValue(structuralProperty.Name); + + IEdmTypeReference propertyType = structuralProperty.Type; + if (propertyValue != null) + { + if (!propertyType.IsPrimitive() && !propertyType.IsEnum()) + { + IEdmTypeReference actualType = writeContext.GetEdmType(propertyValue, propertyValue.GetType()); + if (propertyType != null && propertyType != actualType) + { + propertyType = actualType; + } + } + } + + return serializer.CreateProperty(propertyValue, propertyType, structuralProperty.Name, writeContext); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeltaResourceSetSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeltaResourceSetSerializer.cs index 25b700958..55112ca2f 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,27 @@ 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)); + } + + //todo:use serializer provider + ODataDeletedResourceSerializer deletedResourceSerializer = new ODataDeletedResourceSerializer(SerializerProvider); + + 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 +410,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 +442,11 @@ private static IEdmStructuredTypeReference GetResourceType(IEdmTypeReference fee string message = Error.Format(SRResources.CannotWriteType, typeof(ODataDeltaResourceSetSerializer).Name, feedType.FullName()); throw new SerializationException(message); } + + 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..4f6e48ef5 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)); @@ -235,8 +218,21 @@ await writer.WriteStartAsync(new ODataResourceSet // } if (edmProperty.Type.IsCollection()) { - ODataDeltaResourceSetSerializer serializer = new ODataDeltaResourceSetSerializer(SerializerProvider); - await serializer.WriteObjectInlineAsync(propertyValue, edmProperty.Type, writer, nestedWriteContext).ConfigureAwait(false); + if (IsDeltaCollection(propertyValue)) + { + ODataEdmTypeSerializer serializer = new ODataDeltaResourceSetSerializer(SerializerProvider); + EdmCollectionTypeReference edmType = new EdmCollectionTypeReference(new EdmDeltaCollectionType(new EdmEntityTypeReference(edmProperty.Type.GetElementType() as IEdmEntityType, false))); + await serializer.WriteObjectInlineAsync( + propertyValue, + edmType, + writer, + nestedWriteContext).ConfigureAwait(false); + } + else + { + ODataEdmTypeSerializer serializer = new ODataResourceSetSerializer(SerializerProvider); + await serializer.WriteObjectInlineAsync(propertyValue, edmProperty.Type, writer, nestedWriteContext).ConfigureAwait(false); + } } else { @@ -276,11 +272,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,6 +438,12 @@ 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) { @@ -457,18 +459,40 @@ await writer.WriteEntityReferenceLinkAsync(new ODataEntityReferenceLink } else { + bool isDelta = graph is IDelta || graph is IEdmChangedObject; 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 WriteResourceContent(writer, selectExpandNode, resourceContext, isDelta); await writer.WriteEndAsync().ConfigureAwait(false); } } } + + writeContext.NavigationSource = originalNavigationSource; + } + + internal 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 +559,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 +588,24 @@ public virtual ODataResource CreateResource(SelectExpandNode selectExpandNode, R } } + return resource; + } + + internal static 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 +635,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 . @@ -641,8 +670,14 @@ public virtual void AppendDynamicProperties(ODataResource resource, SelectExpand Contract.Assert(selectExpandNode != null); Contract.Assert(resourceContext != null); + AppendDynamicPropertiesInternal(resource,selectExpandNode, resourceContext, SerializerProvider); + } + + internal static void AppendDynamicPropertiesInternal(ODataResourceBase resource, SelectExpandNode selectExpandNode, + ResourceContext resourceContext, IODataSerializerProvider serializerProvider) + { if (!resourceContext.StructuredType.IsOpen || // non-open type - (!selectExpandNode.SelectAllDynamicProperties && selectExpandNode.SelectedDynamicProperties == null)) + (!selectExpandNode.SelectAllDynamicProperties && selectExpandNode.SelectedDynamicProperties == null)) { return; } @@ -735,7 +770,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, @@ -1252,7 +1287,7 @@ public virtual ODataNestedResourceInfo CreateNavigationLink(IEdmNavigationProper return navigationLink; } - private IEnumerable CreateStructuralPropertyBag(SelectExpandNode selectExpandNode, ResourceContext resourceContext) + internal IEnumerable CreateStructuralPropertyBag(SelectExpandNode selectExpandNode, ResourceContext resourceContext) { Contract.Assert(selectExpandNode != null); Contract.Assert(resourceContext != null); @@ -1266,10 +1301,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 +1779,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 +1804,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 +1878,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 +1901,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 +1916,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/PublicAPI.Unshipped.txt b/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt index e69de29bb..16988f248 100644 --- a/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt +++ b/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt @@ -0,0 +1,11 @@ +Microsoft.AspNetCore.OData.Formatter.Serialization.ODataDeletedResourceSerializer +Microsoft.AspNetCore.OData.Formatter.Serialization.ODataDeletedResourceSerializer.ODataDeletedResourceSerializer(Microsoft.AspNetCore.OData.Formatter.Serialization.IODataSerializerProvider serializerProvider) -> void +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..25df67e1d 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/BulkOperation/BulkOperationTest.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/BulkOperation/BulkOperationTest.cs @@ -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 = "{\"@context\":\"http://localhost/convention/$metadata#Employees/$delta\",\"value\":[{\"ID\":1,\"Name\":\"Employee1\",\"Friends@delta\":[{\"@removed\":{\"reason\":\"changed\"},\"@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.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/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net8.bsl b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net8.bsl index 60b3e8ea4..1b34a3a4b 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 @@ -2166,16 +2166,42 @@ public class Microsoft.AspNetCore.OData.Formatter.Serialization.ODataCollectionS public virtual System.Threading.Tasks.Task WriteObjectAsync (object graph, System.Type type, Microsoft.OData.ODataMessageWriter messageWriter, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) } +public class Microsoft.AspNetCore.OData.Formatter.Serialization.ODataDeletedResourceSerializer : Microsoft.AspNetCore.OData.Formatter.Serialization.ODataEdmTypeSerializer, IODataEdmTypeSerializer, IODataSerializer { + public ODataDeletedResourceSerializer (Microsoft.AspNetCore.OData.Formatter.Serialization.IODataSerializerProvider serializerProvider) + + public virtual void AppendDynamicProperties (Microsoft.OData.ODataDeletedResource resource, Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode selectExpandNode, 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 string CreateETag (Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) + public virtual Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode CreateSelectExpandNode (Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) + public virtual Microsoft.OData.ODataProperty CreateStructuralProperty (Microsoft.OData.Edm.IEdmStructuralProperty structuralProperty, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) + [ + AsyncStateMachineAttribute(), + ] + public virtual System.Threading.Tasks.Task WriteObjectAsync (object graph, System.Type type, Microsoft.OData.ODataMessageWriter messageWriter, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) + + [ + AsyncStateMachineAttribute(), + ] + public virtual System.Threading.Tasks.Task WriteObjectInlineAsync (object graph, Microsoft.OData.Edm.IEdmTypeReference expectedType, Microsoft.OData.ODataWriter writer, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) +} + public class Microsoft.AspNetCore.OData.Formatter.Serialization.ODataDeltaResourceSetSerializer : Microsoft.AspNetCore.OData.Formatter.Serialization.ODataEdmTypeSerializer, IODataEdmTypeSerializer, IODataSerializer { 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)