diff --git a/src/Microsoft.AspNet.OData.Shared/DeltaSetOfT.cs b/src/Microsoft.AspNet.OData.Shared/DeltaSetOfT.cs index b9f014a6df..c316dbca7d 100644 --- a/src/Microsoft.AspNet.OData.Shared/DeltaSetOfT.cs +++ b/src/Microsoft.AspNet.OData.Shared/DeltaSetOfT.cs @@ -17,7 +17,7 @@ namespace Microsoft.AspNet.OData /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")] [NonValidatingParameterBinding] - internal class DeltaSet : Collection, IDeltaSet where TStructuralType : class + public class DeltaSet : Collection, IDeltaSet where TStructuralType : class { private Type _clrType; private IList _keys; diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/ODataOutputFormatterHelper.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/ODataOutputFormatterHelper.cs index d59728abe4..21aa46d8ef 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/ODataOutputFormatterHelper.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/ODataOutputFormatterHelper.cs @@ -95,7 +95,8 @@ internal static bool CanWriteType( ODataPayloadKind? payloadKind; Type elementType; - if (typeof(IEdmObject).IsAssignableFrom(type) || + if (typeof(IEdmObject).IsAssignableFrom(type) || + typeof(IDeltaSet).IsAssignableFrom(type) || (TypeHelper.IsCollection(type, out elementType) && typeof(IEdmObject).IsAssignableFrom(elementType))) { payloadKind = GetEdmObjectPayloadKind(type, internalRequest); @@ -156,7 +157,16 @@ internal static void WriteToStream( ODataMessageWriterSettings writerSettings = internalRequest.WriterSettings; writerSettings.BaseUri = baseAddress; - writerSettings.Version = version; + + if (serializer.ODataPayloadKind == ODataPayloadKind.Delta) + { + writerSettings.Version = ODataVersion.V401; + } + else + { + writerSettings.Version = version; + } + writerSettings.Validations = writerSettings.Validations & ~ValidationKinds.ThrowOnUndeclaredPropertyForNonOpenType; string metadataLink = internaUrlHelper.CreateODataLink(MetadataSegment.Instance); @@ -207,6 +217,7 @@ internal static void WriteToStream( writeContext.Path = path; writeContext.MetadataLevel = metadataLevel; writeContext.QueryOptions = internalRequest.Context.QueryOptions; + writeContext.Type = type; //Set the SelectExpandClause on the context if it was explicitly specified. if (selectExpandDifferentFromQueryOptions != null) @@ -251,7 +262,7 @@ internal static void WriteToStream( { return ODataPayloadKind.ResourceSet; } - else if (typeof(IEdmChangedObject).IsAssignableFrom(elementType)) + else if (typeof(IDeltaSetItem).IsAssignableFrom(elementType) || typeof(IEdmChangedObject).IsAssignableFrom(elementType)) { return ODataPayloadKind.Delta; } diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/DefaultODataSerializerProvider.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/DefaultODataSerializerProvider.cs index fcefb7c6fd..7f28816ec2 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/DefaultODataSerializerProvider.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/DefaultODataSerializerProvider.cs @@ -112,6 +112,10 @@ internal ODataSerializer GetODataPayloadSerializerImpl(Type type, Func(); } + else if (TypeHelper.IsTypeAssignableFrom(typeof(IDeltaSet), type)) + { + return _rootContainer.GetRequiredService(); + } // Get the model. Using a Func to delay evaluation of the model // until after the above checks have passed. diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataDeltaFeedSerializer.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataDeltaFeedSerializer.cs index d041abeb41..e7134c03e7 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataDeltaFeedSerializer.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataDeltaFeedSerializer.cs @@ -6,8 +6,11 @@ //------------------------------------------------------------------------------ using System; +using System.CodeDom; using System.Collections; +using System.Collections.Generic; using System.Diagnostics.Contracts; +using System.Reflection; using System.Runtime.Serialization; using System.Threading.Tasks; using Microsoft.AspNet.OData.Builder; @@ -25,6 +28,7 @@ namespace Microsoft.AspNet.OData.Formatter.Serialization public class ODataDeltaFeedSerializer : ODataEdmTypeSerializer { private const string DeltaFeed = "deltafeed"; + IEdmStructuredTypeReference _elementType; /// /// Initializes a new instance of . @@ -60,6 +64,7 @@ public override void WriteObject(object graph, Type type, ODataMessageWriter mes } IEdmTypeReference feedType = writeContext.GetEdmType(graph, type); + Contract.Assert(feedType != null); IEdmEntityTypeReference entityType = GetResourceType(feedType).AsEntity(); @@ -93,6 +98,7 @@ public override async Task WriteObjectAsync(object graph, Type type, ODataMessag } IEdmTypeReference feedType = writeContext.GetEdmType(graph, type); + Contract.Assert(feedType != null); IEdmEntityTypeReference entityType = GetResourceType(feedType).AsEntity(); @@ -186,6 +192,7 @@ private void WriteFeed(IEnumerable enumerable, IEdmTypeReference feedType, OData Contract.Assert(feedType != null); IEdmStructuredTypeReference elementType = GetResourceType(feedType); + _elementType = elementType; if (elementType.IsComplex()) { @@ -234,13 +241,9 @@ private void WriteFeed(IEnumerable enumerable, IEdmTypeReference feedType, OData } lastResource = entry; - IEdmChangedObject edmChangedObject = entry as IEdmChangedObject; - if (edmChangedObject == null) - { - throw new SerializationException(Error.Format(SRResources.CannotWriteType, GetType().Name, enumerable.GetType().FullName)); - } + EdmDeltaEntityKind deltaEntityKind = GetDeltaEntityKind(enumerable, entry, writeContext); - switch (edmChangedObject.DeltaKind) + switch (deltaEntityKind) { case EdmDeltaEntityKind.DeletedEntry: WriteDeltaDeletedEntry(entry, writer, writeContext); @@ -254,6 +257,7 @@ private void WriteFeed(IEnumerable enumerable, IEdmTypeReference feedType, OData case EdmDeltaEntityKind.Entry: { ODataResourceSerializer entrySerializer = SerializerProvider.GetEdmTypeSerializer(elementType) as ODataResourceSerializer; + if (entrySerializer == null) { throw new SerializationException( @@ -289,6 +293,7 @@ private async Task WriteFeedAsync(IEnumerable enumerable, IEdmTypeReference feed Contract.Assert(feedType != null); IEdmStructuredTypeReference elementType = GetResourceType(feedType); + _elementType = elementType; if (elementType.IsComplex()) { @@ -337,13 +342,9 @@ private async Task WriteFeedAsync(IEnumerable enumerable, IEdmTypeReference feed } lastResource = entry; - IEdmChangedObject edmChangedObject = entry as IEdmChangedObject; - if (edmChangedObject == null) - { - throw new SerializationException(Error.Format(SRResources.CannotWriteType, GetType().Name, enumerable.GetType().FullName)); - } + EdmDeltaEntityKind deltaEntityKind = GetDeltaEntityKind(enumerable, entry, writeContext); - switch (edmChangedObject.DeltaKind) + switch (deltaEntityKind) { case EdmDeltaEntityKind.DeletedEntry: await WriteDeltaDeletedEntryAsync(entry, writer, writeContext); @@ -441,12 +442,25 @@ public virtual ODataDeltaResourceSet CreateODataDeltaFeed(IEnumerable feedInstan /// The . public virtual void WriteDeltaDeletedEntry(object graph, ODataWriter writer, ODataSerializerContext writeContext) { - ODataDeletedResource deletedResource = GetDeletedResource(graph); + ODataResourceSerializer serializer = SerializerProvider.GetEdmTypeSerializer(_elementType) as ODataResourceSerializer; - if (deletedResource != null) + if (serializer == null) + { + throw new SerializationException( + Error.Format(SRResources.TypeCannotBeSerialized, _elementType.FullName())); + } + else { - writer.WriteStart(deletedResource); - writer.WriteEnd(); + ResourceContext resourceContext = serializer.GetResourceContext(graph, writeContext); + SelectExpandNode selectExpandNode = serializer.CreateSelectExpandNode(resourceContext); + + if (selectExpandNode != null) + { + ODataDeletedResource deletedResource = GetDeletedResource(graph, resourceContext, serializer, selectExpandNode, writeContext.IsUntyped); + writer.WriteStart(deletedResource); + serializer.WriteDeltaComplexProperties(selectExpandNode, resourceContext, writer); + writer.WriteEnd(); + } } } @@ -459,10 +473,22 @@ public virtual void WriteDeltaDeletedEntry(object graph, ODataWriter writer, ODa /// The . public virtual async Task WriteDeltaDeletedEntryAsync(object graph, ODataWriter writer, ODataSerializerContext writeContext) { - ODataDeletedResource deletedResource = GetDeletedResource(graph); - if (deletedResource != null) + ODataResourceSerializer serializer = SerializerProvider.GetEdmTypeSerializer(_elementType) as ODataResourceSerializer; + + if (serializer == null) + { + throw new SerializationException( + Error.Format(SRResources.TypeCannotBeSerialized, _elementType.FullName())); + } + + ResourceContext resourceContext = serializer.GetResourceContext(graph, writeContext); + SelectExpandNode selectExpandNode = serializer.CreateSelectExpandNode(resourceContext); + + if (selectExpandNode != null) { + ODataDeletedResource deletedResource = GetDeletedResource(graph, resourceContext, serializer, selectExpandNode, writeContext.IsUntyped); await writer.WriteStartAsync(deletedResource); + await serializer.WriteDeltaComplexPropertiesAsync(selectExpandNode, resourceContext, writer); await writer.WriteEndAsync(); } } @@ -531,22 +557,41 @@ public async Task WriteDeltaLinkAsync(object graph, ODataWriter writer, ODataSer } } - private ODataDeletedResource GetDeletedResource(object graph) + private ODataDeletedResource GetDeletedResource(object graph, ResourceContext resourceContext, ODataResourceSerializer serializer, SelectExpandNode selectExpandNode, bool isUntyped) { - EdmDeltaDeletedEntityObject edmDeltaDeletedEntity = graph as EdmDeltaDeletedEntityObject; - if (edmDeltaDeletedEntity == null) + string navigationSource; + ODataDeletedResource deletedResource = serializer.CreateDeletedResource(selectExpandNode, resourceContext); + + if (isUntyped) { - throw new SerializationException(Error.Format(SRResources.CannotWriteType, GetType().Name, graph.GetType().FullName)); + EdmDeltaDeletedEntityObject edmDeltaDeletedEntity = graph as EdmDeltaDeletedEntityObject; + if (edmDeltaDeletedEntity == null) + { + throw new SerializationException(Error.Format(SRResources.CannotWriteType, GetType().Name, graph.GetType().FullName)); + } + + deletedResource.Id = StringToUri(edmDeltaDeletedEntity.Id ?? string.Empty); + deletedResource.Reason = edmDeltaDeletedEntity.Reason; + navigationSource = edmDeltaDeletedEntity.NavigationSource?.Name; } + else + { + IDeltaDeletedEntityObject deltaDeletedEntity = graph as IDeltaDeletedEntityObject; + if (deltaDeletedEntity == null) + { + throw new SerializationException(Error.Format(SRResources.CannotWriteType, GetType().Name, graph.GetType().FullName)); + } - Uri id = StringToUri(edmDeltaDeletedEntity.Id); - ODataDeletedResource deletedResource = new ODataDeletedResource(id, edmDeltaDeletedEntity.Reason); + deletedResource.Id = deltaDeletedEntity.Id; + deletedResource.Reason = deltaDeletedEntity.Reason; + navigationSource = deltaDeletedEntity.NavigationSource; + } - if (edmDeltaDeletedEntity.NavigationSource != null) + if (navigationSource != null) { ODataResourceSerializationInfo serializationInfo = new ODataResourceSerializationInfo { - NavigationSourceName = edmDeltaDeletedEntity.NavigationSource.Name + NavigationSourceName = navigationSource }; deletedResource.SetSerializationInfo(serializationInfo); } @@ -621,5 +666,35 @@ internal static Uri StringToUri(string uriString) return uri; } + + private EdmDeltaEntityKind GetDeltaEntityKind(IEnumerable enumerable, object entry, ODataSerializerContext writeContext) + { + EdmDeltaEntityKind deltaEntityKind; + + if (writeContext.IsUntyped) + { + IEdmChangedObject edmChangedObject = entry as IEdmChangedObject; + + if (edmChangedObject == null) + { + throw new SerializationException(Error.Format(SRResources.CannotWriteType, GetType().Name, enumerable.GetType().FullName)); + } + + deltaEntityKind = edmChangedObject.DeltaKind; + } + else + { + IDeltaSetItem deltaSetItem = entry as IDeltaSetItem; + + if (deltaSetItem == null) + { + throw new SerializationException(Error.Format(SRResources.CannotWriteType, GetType().Name, enumerable.GetType().FullName)); + } + + deltaEntityKind = deltaSetItem.DeltaKind; + } + + return deltaEntityKind; + } } } diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataResourceSerializer.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataResourceSerializer.cs index 8d4c2c5a92..faeab963e3 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataResourceSerializer.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataResourceSerializer.cs @@ -19,7 +19,6 @@ using Microsoft.AspNet.OData.Query.Expressions; using Microsoft.OData; using Microsoft.OData.Edm; -using Microsoft.OData.Edm.Vocabularies; using Microsoft.OData.UriParser; namespace Microsoft.AspNet.OData.Formatter.Serialization @@ -184,61 +183,17 @@ public virtual Task WriteDeltaObjectInlineAsync(object graph, IEdmTypeReference { throw new SerializationException(Error.Format(SRResources.CannotSerializerNull, Resource)); } - - return WriteDeltaResourceAsync(graph, writer, writeContext); - } - - private void WriteDeltaResource(object graph, ODataWriter writer, ODataSerializerContext writeContext) - { - Contract.Assert(writeContext != null); - - ResourceContext resourceContext = GetResourceContext(graph, writeContext); - SelectExpandNode selectExpandNode = CreateSelectExpandNode(resourceContext); - if (selectExpandNode != null) - { - ODataResource resource = CreateResource(selectExpandNode, resourceContext); - - if (resource != null) - { - writer.WriteStart(resource); - WriteDeltaComplexProperties(selectExpandNode, resourceContext, writer); - //TODO: Need to add support to write Navigation Links, etc. using Delta Writer - //https://github.com/OData/odata.net/issues/155 - //CLEANUP: merge delta logic with regular logic; requires common base between ODataWriter and ODataDeltaWriter - //WriteDynamicComplexProperties(resourceContext, writer); - //WriteNavigationLinks(selectExpandNode.SelectedNavigationProperties, resourceContext, writer); - //WriteExpandedNavigationProperties(selectExpandNode.ExpandedNavigationProperties, resourceContext, writer); - - writer.WriteEnd(); - } - } - } - - private async Task WriteDeltaResourceAsync(object graph, ODataWriter writer, ODataSerializerContext writeContext) - { - ResourceContext resourceContext = GetResourceContext(graph, writeContext); - SelectExpandNode selectExpandNode = CreateSelectExpandNode(resourceContext); - if (selectExpandNode != null) - { - ODataResource resource = CreateResource(selectExpandNode, resourceContext); - - if (resource != null) - { - await writer.WriteStartAsync(resource); - await WriteDeltaComplexPropertiesAsync(selectExpandNode, resourceContext, writer); - //TODO: Need to add support to write Navigation Links, etc. using Delta Writer - //https://github.com/OData/odata.net/issues/155 - //CLEANUP: merge delta logic with regular logic; requires common base between ODataWriter and ODataDeltaWriter - //WriteDynamicComplexProperties(resourceContext, writer); - //WriteNavigationLinks(selectExpandNode.SelectedNavigationProperties, resourceContext, writer); - //WriteExpandedNavigationProperties(selectExpandNode.ExpandedNavigationProperties, resourceContext, writer); - await writer.WriteEndAsync(); - } - } + return WriteDeltaResourceAsync(graph, writer, writeContext); } - private ResourceContext GetResourceContext(object graph, ODataSerializerContext writeContext) + /// + /// Gets the resource context for the resource being written. + /// + /// The object to be written. + /// The . + /// The . + internal ResourceContext GetResourceContext(object graph, ODataSerializerContext writeContext) { Contract.Assert(writeContext != null); @@ -253,7 +208,13 @@ private ResourceContext GetResourceContext(object graph, ODataSerializerContext return resourceContext; } - private void WriteDeltaComplexProperties(SelectExpandNode selectExpandNode, + /// + /// Writes the delta complex properties. + /// + /// Contains the set of properties and actions to use to select and expand while writing an entity. + /// The resource context for the resource being written. + /// The ODataWriter. + internal void WriteDeltaComplexProperties(SelectExpandNode selectExpandNode, ResourceContext resourceContext, ODataWriter writer) { Contract.Assert(resourceContext != null); @@ -275,7 +236,69 @@ private void WriteDeltaComplexProperties(SelectExpandNode selectExpandNode, } } - private async Task WriteDeltaComplexPropertiesAsync(SelectExpandNode selectExpandNode, + /// + /// Writes the delta navigation properties. + /// + /// Contains the set of properties and actions to use to select and expand while writing an entity. + /// The resource context for the resource being written. + /// The ODataWriter. + internal void WriteDeltaNavigationProperties(SelectExpandNode selectExpandNode, ResourceContext resourceContext, ODataWriter writer) + { + Contract.Assert(resourceContext != null, "The resource context cannot be null"); + Contract.Assert(writer != null, "The writer cannot be null"); + + IEnumerable> navigationProperties = GetNavigationPropertiesToWrite(selectExpandNode, resourceContext); + + foreach (KeyValuePair navigationProperty in navigationProperties) + { + ODataNestedResourceInfo nestedResourceInfo = new ODataNestedResourceInfo + { + IsCollection = navigationProperty.Key.Type.IsCollection(), + Name = navigationProperty.Key.Name + }; + + writer.WriteStart(nestedResourceInfo); + WriteDeltaComplexAndExpandedNavigationProperty(navigationProperty.Key, null, resourceContext, writer, navigationProperty.Value); + writer.WriteEnd(); + } + } + + /// + /// Writes delta navigation properties asyncronously. + /// + /// Contains the set of properties and actions to use to select and expand while writing an entity. + /// The resource context for the resource being written. + /// The ODataWriter. + /// + internal 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"); + + IEnumerable> navigationProperties = GetNavigationPropertiesToWrite(selectExpandNode, resourceContext); + + foreach (KeyValuePair navigationProperty in navigationProperties) + { + ODataNestedResourceInfo nestedResourceInfo = new ODataNestedResourceInfo + { + IsCollection = navigationProperty.Key.Type.IsCollection(), + Name = navigationProperty.Key.Name + }; + + await writer.WriteStartAsync(nestedResourceInfo); + await WriteDeltaComplexAndExpandedNavigationPropertyAsync(navigationProperty.Key, null, resourceContext, writer, navigationProperty.Value); + await writer.WriteEndAsync(); + } + } + + /// + /// Writes delta complex properties asyncronously. + /// + /// Contains the set of properties and actions to use to select and expand while writing an entity. + /// The resource context for the resource being written. + /// The ODataWriter. + /// + internal async Task WriteDeltaComplexPropertiesAsync(SelectExpandNode selectExpandNode, ResourceContext resourceContext, ODataWriter writer) { Contract.Assert(resourceContext != null); @@ -297,770 +320,1151 @@ private async Task WriteDeltaComplexPropertiesAsync(SelectExpandNode selectExpan } } - private void WriteDeltaComplexAndExpandedNavigationProperty(IEdmProperty edmProperty, SelectExpandClause selectExpandClause, - ResourceContext resourceContext, ODataWriter writer) + /// + /// 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) { - Contract.Assert(edmProperty != null); - Contract.Assert(resourceContext != null); - Contract.Assert(writer != null); + if (resourceContext == null) + { + throw Error.ArgumentNull("resourceContext"); + } - object propertyValue = resourceContext.GetPropertyValue(edmProperty.Name); + ODataSerializerContext writeContext = resourceContext.SerializerContext; + IEdmStructuredType structuredType = resourceContext.StructuredType; - if (propertyValue == null || propertyValue is NullEdmComplexObject) - { - if (edmProperty.Type.IsCollection()) - { - // A complex or navigation property whose Type attribute specifies a collection, the collection always exists, - // it may just be empty. - // If a collection of complex or entities can be related, it is represented as a JSON array. An empty - // collection of resources (one that contains no resource) is represented as an empty JSON array. - writer.WriteStart(new ODataResourceSet - { - TypeName = edmProperty.Type.FullName() - }); - } - else - { - // If at most one resource can be related, the value is null if no resource is currently related. - writer.WriteStart(resource: null); - } + object selectExpandNode; - writer.WriteEnd(); - } - else + Tuple key = Tuple.Create(writeContext.SelectExpandClause, structuredType); + if (!writeContext.Items.TryGetValue(key, out selectExpandNode)) { - // create the serializer context for the complex and expanded item. - ODataSerializerContext nestedWriteContext = new ODataSerializerContext(resourceContext, selectExpandClause, edmProperty); + // 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; + } - // write object. + return selectExpandNode as SelectExpandNode; + } - // TODO: enable overriding serializer based on type. Currentlky 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())); - // } - if (edmProperty.Type.IsCollection()) - { - ODataDeltaFeedSerializer serializer = new ODataDeltaFeedSerializer(SerializerProvider); - serializer.WriteDeltaFeedInline(propertyValue, edmProperty.Type, writer, nestedWriteContext); - } - else - { - ODataResourceSerializer serializer = new ODataResourceSerializer(SerializerProvider); - serializer.WriteDeltaObjectInline(propertyValue, edmProperty.Type, writer, nestedWriteContext); - } - } + /// + /// Creates the to be written while writing this resource. + /// + /// The describing the response graph. + /// The context for the resource instance being written. + /// The created . + public virtual ODataResource CreateResource(SelectExpandNode selectExpandNode, ResourceContext resourceContext) + { + ODataResource resource = CreateResource(selectExpandNode, resourceContext, false) as ODataResource; + return resource; } - private async Task WriteDeltaComplexAndExpandedNavigationPropertyAsync(IEdmProperty edmProperty, SelectExpandClause selectExpandClause, - ResourceContext resourceContext, ODataWriter writer) + private ODataResourceBase CreateResource(SelectExpandNode selectExpandNode, ResourceContext resourceContext, bool isDeletedResource) { - Contract.Assert(edmProperty != null); - Contract.Assert(resourceContext != null); - Contract.Assert(writer != null); + if (selectExpandNode == null) + { + throw Error.ArgumentNull("selectExpandNode"); + } - object propertyValue = resourceContext.GetPropertyValue(edmProperty.Name); + if (resourceContext == null) + { + throw Error.ArgumentNull("resourceContext"); + } - if (propertyValue == null || propertyValue is NullEdmComplexObject) + if (resourceContext.SerializerContext.ExpandReference) { - if (edmProperty.Type.IsCollection()) + if (isDeletedResource) { - // A complex or navigation property whose Type attribute specifies a collection, the collection always exists, - // it may just be empty. - // If a collection of complex or entities can be related, it is represented as a JSON array. An empty - // collection of resources (one that contains no resource) is represented as an empty JSON array. - await writer.WriteStartAsync(new ODataResourceSet + return new ODataDeletedResource { - TypeName = edmProperty.Type.FullName() - }); + Id = resourceContext.GenerateSelfLink(false) + }; } - else + + return new ODataResource { - // If at most one resource can be related, the value is null if no resource is currently related. - await writer.WriteStartAsync(resource: null); - } + Id = resourceContext.GenerateSelfLink(false) + }; + } - await writer.WriteEndAsync(); + string typeName = resourceContext.StructuredType.FullTypeName(); + ODataResourceBase resource; + + if (isDeletedResource) + { + resource = new ODataDeletedResource + { + TypeName = typeName, + Properties = CreateStructuralPropertyBag(selectExpandNode, resourceContext), + }; } else { - // create the serializer context for the complex and expanded item. - ODataSerializerContext nestedWriteContext = new ODataSerializerContext(resourceContext, selectExpandClause, edmProperty); + resource = new ODataResource + { + TypeName = typeName, + Properties = CreateStructuralPropertyBag(selectExpandNode, resourceContext), + }; + } - // write object. - // TODO: enable overriding serializer based on type. Currentlky 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())); - // } - if (edmProperty.Type.IsCollection()) - { - ODataDeltaFeedSerializer serializer = new ODataDeltaFeedSerializer(SerializerProvider); - await serializer.WriteDeltaFeedInlineAsync(propertyValue, edmProperty.Type, writer, nestedWriteContext); - } - else + if (resourceContext.EdmObject is EdmDeltaEntityObject && 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) { - ODataResourceSerializer serializer = new ODataResourceSerializer(SerializerProvider); - await serializer.WriteDeltaObjectInlineAsync(propertyValue, edmProperty.Type, writer, nestedWriteContext); + serializationInfo.NavigationSourceEntityTypeName = sourceType.Name; } + resource.SetSerializationInfo(serializationInfo); } - } - private static IEnumerable CreateODataPropertiesFromDynamicType(EdmEntityType entityType, object graph, - Dictionary dynamicTypeProperties) - { - Contract.Assert(dynamicTypeProperties != null); + // Try to add the dynamic properties if the structural type is open. + AppendDynamicProperties(resource, selectExpandNode, resourceContext); - var properties = new List(); - var dynamicObject = graph as DynamicTypeWrapper; - if (dynamicObject == null) + // Try to add instance annotations + AppendInstanceAnnotations(resource, resourceContext); + + if (selectExpandNode.SelectedActions != null) { - var dynamicEnumerable = (graph as IEnumerable); - if (dynamicEnumerable != null) + IEnumerable actions = CreateODataActions(selectExpandNode.SelectedActions, resourceContext); + foreach (ODataAction action in actions) { - dynamicObject = dynamicEnumerable.SingleOrDefault(); + resource.AddAction(action); } } - if (dynamicObject != null) + + if (selectExpandNode.SelectedFunctions != null) { - foreach (var prop in dynamicObject.Values) + IEnumerable functions = CreateODataFunctions(selectExpandNode.SelectedFunctions, resourceContext); + foreach (ODataFunction function in functions) { - IEdmProperty edmProperty = entityType?.Properties() - .FirstOrDefault(p => p.Name.Equals(prop.Key)); - if (prop.Value != null - && (prop.Value is DynamicTypeWrapper || (prop.Value is IEnumerable))) - { - if (edmProperty != null) - { - dynamicTypeProperties.Add(edmProperty, prop.Value); - } + resource.AddFunction(function); + } + } + + IEdmStructuredType pathType = GetODataPathType(resourceContext.SerializerContext); + if (resourceContext.StructuredType.TypeKind == EdmTypeKind.Complex) + { + AddTypeNameAnnotationAsNeededForComplex(resource, resourceContext.SerializerContext.MetadataLevel); + } + else + { + AddTypeNameAnnotationAsNeeded(resource, pathType, resourceContext.SerializerContext.MetadataLevel); + } + + if (!isDeletedResource && resourceContext.StructuredType.TypeKind == EdmTypeKind.Entity && resourceContext.NavigationSource != null) + { + if (!(resourceContext.NavigationSource is IEdmContainedEntitySet)) + { + IEdmModel model = resourceContext.SerializerContext.Model; + NavigationSourceLinkBuilderAnnotation linkBuilder = model.GetNavigationSourceLinkBuilder(resourceContext.NavigationSource); + EntitySelfLinks selfLinks = linkBuilder.BuildEntitySelfLinks(resourceContext, resourceContext.SerializerContext.MetadataLevel); + + if (selfLinks.IdLink != null) + { + resource.Id = selfLinks.IdLink; } - else + + if (selfLinks.ReadLink != null) { - ODataProperty property; - if (prop.Value == null) - { - property = new ODataProperty - { - Name = prop.Key, - Value = new ODataNullValue() - }; - } - else + resource.ReadLink = selfLinks.ReadLink; + } + + if (selfLinks.EditLink != null) + { + resource.EditLink = selfLinks.EditLink; + } + } + + string etag = CreateETag(resourceContext); + if (etag != null) + { + resource.ETag = etag; + } + } + + return resource; + } + + /// + /// Creates the to be written while writing this resource. + /// + /// The describing the response graph. + /// The context for the resource instance being written. + /// The created . + public virtual ODataDeletedResource CreateDeletedResource(SelectExpandNode selectExpandNode, ResourceContext resourceContext) + { + return CreateResource(selectExpandNode, resourceContext, true) as ODataDeletedResource; + } + + /// + /// 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(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)) + { + return; + } + + bool nullDynamicPropertyEnabled = false; + if (resourceContext.EdmObject is EdmDeltaComplexObject || resourceContext.EdmObject is EdmDeltaEntityObject) + { + nullDynamicPropertyEnabled = true; + } + else if (resourceContext.InternalRequest != null) + { + nullDynamicPropertyEnabled = resourceContext.InternalRequest.Options.NullDynamicPropertyIsEnabled; + } + + IEdmStructuredType structuredType = resourceContext.StructuredType; + IEdmStructuredObject structuredObject = resourceContext.EdmObject; + object value; + IDelta delta = structuredObject as IDelta; + if (delta == null) + { + PropertyInfo dynamicPropertyInfo = EdmLibHelpers.GetDynamicPropertyDictionary(structuredType, + resourceContext.EdmModel); + if (dynamicPropertyInfo == null || structuredObject == null || + !structuredObject.TryGetPropertyValue(dynamicPropertyInfo.Name, out value) || value == null) + { + return; + } + } + else + { + value = ((EdmStructuredObject)structuredObject).TryGetDynamicProperties(); + } + + IDictionary dynamicPropertyDictionary = (IDictionary)value; + + // Build a HashSet to store the declared property names. + // It is used to make sure the dynamic property name is different from all declared property names. + HashSet declaredPropertyNameSet = new HashSet(resource.Properties.Select(p => p.Name)); + List dynamicProperties = new List(); + + // To test SelectedDynamicProperties == null is enough to filter the dynamic properties. + // Because if SelectAllDynamicProperties == true, SelectedDynamicProperties should be null always. + // So `selectExpandNode.SelectedDynamicProperties == null` covers `SelectAllDynamicProperties == true` scenario. + // If `selectExpandNode.SelectedDynamicProperties != null`, then we should test whether the property is selected or not using "Contains(...)". + IEnumerable> dynamicPropertiesToSelect = + dynamicPropertyDictionary.Where(x => selectExpandNode.SelectedDynamicProperties == null || selectExpandNode.SelectedDynamicProperties.Contains(x.Key)); + foreach (KeyValuePair dynamicProperty in dynamicPropertiesToSelect) + { + if (String.IsNullOrEmpty(dynamicProperty.Key)) + { + continue; + } + + if (dynamicProperty.Value == null) + { + if (nullDynamicPropertyEnabled) + { + dynamicProperties.Add(new ODataProperty { - if (edmProperty != null) - { - property = new ODataProperty - { - Name = prop.Key, - Value = ODataPrimitiveSerializer.ConvertPrimitiveValue(prop.Value, edmProperty.Type.AsPrimitive()) - }; - } - else - { - property = new ODataProperty - { - Name = prop.Key, - Value = prop.Value - }; - } - } + Name = dynamicProperty.Key, + Value = new ODataNullValue() + }); + } - properties.Add(property); + continue; + } + + if (declaredPropertyNameSet.Contains(dynamicProperty.Key)) + { + throw Error.InvalidOperation(SRResources.DynamicPropertyNameAlreadyUsedAsDeclaredPropertyName, + dynamicProperty.Key, structuredType.FullTypeName()); + } + + IEdmTypeReference edmTypeReference = resourceContext.SerializerContext.GetEdmType(dynamicProperty.Value, + dynamicProperty.Value.GetType()); + if (edmTypeReference == null) + { + throw Error.NotSupported(SRResources.TypeOfDynamicPropertyNotSupported, + dynamicProperty.Value.GetType().FullName, dynamicProperty.Key); + } + + if (edmTypeReference.IsStructured() || + (edmTypeReference.IsCollection() && edmTypeReference.AsCollection().ElementType().IsStructured())) + { + if (resourceContext.DynamicComplexProperties == null) + { + resourceContext.DynamicComplexProperties = new ConcurrentDictionary(); } + + resourceContext.DynamicComplexProperties.Add(dynamicProperty); + } + else + { + ODataEdmTypeSerializer propertySerializer = SerializerProvider.GetEdmTypeSerializer(edmTypeReference); + if (propertySerializer == null) + { + throw Error.NotSupported(SRResources.DynamicPropertyCannotBeSerialized, dynamicProperty.Key, + edmTypeReference.FullName()); + } + + dynamicProperties.Add(propertySerializer.CreateProperty( + dynamicProperty.Value, edmTypeReference, dynamicProperty.Key, resourceContext.SerializerContext)); } } - return properties; + + if (dynamicProperties.Any()) + { + resource.Properties = resource.Properties.Concat(dynamicProperties); + } } - private void WriteDynamicTypeResource(object graph, ODataWriter writer, IEdmTypeReference expectedType, - ODataSerializerContext writeContext) + /// + /// Method to append InstanceAnnotations to the ODataResource and Property. + /// Instance annotations are annotations for a resource or a property and couldb be of contain a primitive, comple , enum or collection type + /// These will be saved in to an Instance annotation dictionary + /// + /// The describing the resource, which is being annotated. + /// The context for the resource instance, which is being annotated. + public virtual void AppendInstanceAnnotations(ODataResourceBase resource, ResourceContext resourceContext) { - var dynamicTypeProperties = new Dictionary(); - var entityType = expectedType.Definition as EdmEntityType; - var resource = new ODataResource() + IEdmStructuredType structuredType = resourceContext.StructuredType; + IEdmStructuredObject structuredObject = resourceContext.EdmObject; + + //For appending transient and persistent instance annotations for both enity object and normal resources + + PropertyInfo instanceAnnotationInfo = EdmLibHelpers.GetInstanceAnnotationsContainer(structuredType, + resourceContext.EdmModel); + + object instanceAnnotations = null; + IODataInstanceAnnotationContainer transientAnnotations = null; + + if (resourceContext.SerializerContext.IsDeltaOfT && resourceContext.ResourceInstance is IDelta delta) { - TypeName = expectedType.FullName(), - Properties = CreateODataPropertiesFromDynamicType(entityType, graph, dynamicTypeProperties) - }; + if (instanceAnnotationInfo != null) + { + delta.TryGetPropertyValue(instanceAnnotationInfo.Name, out instanceAnnotations); + } - resource.IsTransient = true; - writer.WriteStart(resource); - foreach (var property in dynamicTypeProperties.Keys) + if (resourceContext.ResourceInstance is IDeltaSetItem deltaitem) + { + transientAnnotations = deltaitem.TransientInstanceAnnotationContainer; + } + } + else + { + if (structuredObject != null && (instanceAnnotationInfo == null || !structuredObject.TryGetPropertyValue(instanceAnnotationInfo.Name, out instanceAnnotations) || instanceAnnotations == null)) + { + if (structuredObject is EdmEntityObject edmEntityObject) + { + instanceAnnotations = edmEntityObject.PersistentInstanceAnnotationsContainer; + transientAnnotations = edmEntityObject.TransientInstanceAnnotationContainer; + } + } + } + + ODataSerializerHelper.AppendInstanceAnnotations(resource, resourceContext, instanceAnnotations as IODataInstanceAnnotationContainer, SerializerProvider); + + ODataSerializerHelper.AppendInstanceAnnotations(resource, resourceContext, transientAnnotations, 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.InternalRequest != null) + { + IEdmModel model = resourceContext.EdmModel; + IEdmNavigationSource navigationSource = resourceContext.NavigationSource; + + IEnumerable concurrencyProperties; + if (model != null && navigationSource != null) + { + concurrencyProperties = model.GetConcurrencyProperties(navigationSource).OrderBy(c => c.Name); + } + else + { + concurrencyProperties = Enumerable.Empty(); + } + + IDictionary properties = new Dictionary(); + foreach (IEdmStructuralProperty etagProperty in concurrencyProperties) + { + properties.Add(etagProperty.Name, resourceContext.GetPropertyValue(etagProperty.Name)); + } + + return resourceContext.InternalRequest.CreateETag(properties); + } + + return null; + } + + /// + /// Creates the to be written while writing this dynamic complex property. + /// + /// The dynamic property name. + /// The dynamic property value. + /// The edm type reference. + /// The context for the complex instance being written. + /// The nested resource info to be written. Returns 'null' will omit this serialization. + /// It enables customer to get more control by overriding this method. + public virtual ODataNestedResourceInfo CreateDynamicComplexNestedResourceInfo(string propertyName, object propertyValue, IEdmTypeReference edmType, ResourceContext resourceContext) + { + ODataNestedResourceInfo nestedInfo = null; + if (propertyName != null && edmType != null) + { + nestedInfo = new ODataNestedResourceInfo + { + IsCollection = edmType.IsCollection(), + Name = propertyName, + }; + } + + return nestedInfo; + } + + /// + /// Creates the to be written while writing this complex property. + /// + /// The complex property for which the nested resource info is being created. + /// The corresponding sub select item belongs to this complex property. + /// The context for the complex instance being written. + /// The nested resource info to be written. Returns 'null' will omit this complex serialization. + /// It enables customer to get more control by overriding this method. + public virtual ODataNestedResourceInfo CreateComplexNestedResourceInfo(IEdmStructuralProperty complexProperty, PathSelectItem pathSelectItem, ResourceContext resourceContext) + { + if (complexProperty == null) + { + throw Error.ArgumentNull(nameof(complexProperty)); + } + + ODataNestedResourceInfo nestedInfo = null; + + if (complexProperty.Type != null) + { + nestedInfo = new ODataNestedResourceInfo + { + IsCollection = complexProperty.Type.IsCollection(), + Name = complexProperty.Name + }; + } + + return nestedInfo; + } + + /// + /// Creates the to be written while writing this entity. + /// + /// The navigation property for which the navigation link is being created. + /// The context for the entity instance being written. + /// The navigation link to be written. + public virtual ODataNestedResourceInfo CreateNavigationLink(IEdmNavigationProperty navigationProperty, ResourceContext resourceContext) + { + if (navigationProperty == null) + { + throw Error.ArgumentNull("navigationProperty"); + } + + if (resourceContext == null) + { + throw Error.ArgumentNull("resourceContext"); + } + + ODataSerializerContext writeContext = resourceContext.SerializerContext; + IEdmNavigationSource navigationSource = writeContext.NavigationSource; + ODataNestedResourceInfo navigationLink = null; + + if (navigationSource != null) + { + IEdmTypeReference propertyType = navigationProperty.Type; + IEdmModel model = writeContext.Model; + NavigationSourceLinkBuilderAnnotation linkBuilder = model.GetNavigationSourceLinkBuilder(navigationSource); + Uri navigationUrl = linkBuilder.BuildNavigationLink(resourceContext, navigationProperty, writeContext.MetadataLevel); + + navigationLink = new ODataNestedResourceInfo + { + IsCollection = propertyType.IsCollection(), + Name = navigationProperty.Name, + }; + + if (navigationUrl != null) + { + navigationLink.Url = navigationUrl; + } + } + + return navigationLink; + } + + /// + /// Creates the to be written for the given stream property. + /// + /// The EDM structural property being written. + /// The context for the entity instance being written. + /// The to write. + internal virtual ODataStreamPropertyInfo CreateStreamProperty(IEdmStructuralProperty structuralProperty, ResourceContext resourceContext) + { + if (structuralProperty == null) + { + throw Error.ArgumentNull("structuralProperty"); + } + + if (resourceContext == null) + { + throw Error.ArgumentNull("resourceContext"); + } + + if (structuralProperty.Type == null || !structuralProperty.Type.IsStream()) + { + return null; + } + + if (resourceContext.SerializerContext.MetadataLevel != ODataMetadataLevel.FullMetadata) + { + return null; + } + + // TODO: we need to return ODataStreamReferenceValue if + // 1) If we have the EditLink link builder + // 2) If we have the ReadLink link builder + // 3) If we have the Core.AcceptableMediaTypes annotation associated with the Stream property + + // We need a way for the user to specify a mediatype for an instance of a stream property. + // If specified, we should explicitly write the streamreferencevalue and not let ODL fill it in. + + // Although the mediatype is represented as an instance annotation in JSON, it's really control information. + // So we shouldn't use instance annotations to tell us the media type, but have a separate way to specify the media type. + // Perhaps we define an interface (and stream wrapper class that derives from stream and implements the interface) that exposes a MediaType property. + // If the stream property implements this interface, and it specifies a media-type other than application/octet-stream, we explicitly create and write a StreamReferenceValue with that media type. + // We could also use this type to expose properties for things like ReadLink and WriteLink(and even ETag) + // that the user could specify to something other than the default convention + // if they wanted to provide custom routes for reading/writing the stream values or custom ETag values for the stream. + + // So far, let's return null and let OData.lib to calculate the ODataStreamReferenceValue by conventions. + return null; + } + + /// + /// 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("structuralProperty"); + } + if (resourceContext == null) + { + throw Error.ArgumentNull("resourceContext"); + } + + ODataSerializerContext writeContext = resourceContext.SerializerContext; + + ODataEdmTypeSerializer 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) { - var resourceContext = new ResourceContext(writeContext, expectedType.AsEntity(), graph); - if (entityType.NavigationProperties().Any(p => p.Type.Equals(property.Type)) && !(property.Type is EdmCollectionTypeReference)) + if (!propertyType.IsPrimitive() && !propertyType.IsEnum()) { - var navigationProperty = entityType.NavigationProperties().FirstOrDefault(p => p.Type.Equals(property.Type)); - var navigationLink = CreateNavigationLink(navigationProperty, resourceContext); - if (navigationLink != null) + IEdmTypeReference actualType = writeContext.GetEdmType(propertyValue, propertyValue.GetType()); + if (propertyType != null && propertyType != actualType) { - writer.WriteStart(navigationLink); - WriteDynamicTypeResource(dynamicTypeProperties[property], writer, property.Type, writeContext); - writer.WriteEnd(); + propertyType = actualType; } } - else - { - ODataNestedResourceInfo nestedResourceInfo = new ODataNestedResourceInfo - { - IsCollection = property.Type.IsCollection(), - Name = property.Name - }; - - writer.WriteStart(nestedResourceInfo); - WriteDynamicComplexProperty(dynamicTypeProperties[property], property.Type, resourceContext, writer); - writer.WriteEnd(); - } } - writer.WriteEnd(); + return serializer.CreateProperty(propertyValue, propertyType, structuralProperty.Name, writeContext); } - private async Task WriteDynamicTypeResourceAsync(object graph, ODataWriter writer, IEdmTypeReference expectedType, - ODataSerializerContext writeContext) + /// + /// Creates an to be written for the given action and the entity instance. + /// + /// The OData action. + /// The context for the entity instance being written. + /// The created action or null if the action should not be written. + [SuppressMessage("Microsoft.Usage", "CA2234: Pass System.Uri objects instead of strings", Justification = "This overload is equally good")] + public virtual ODataAction CreateODataAction(IEdmAction action, ResourceContext resourceContext) { - var dynamicTypeProperties = new Dictionary(); - var entityType = expectedType.Definition as EdmEntityType; - var resource = new ODataResource() + if (action == null) { - TypeName = expectedType.FullName(), - Properties = CreateODataPropertiesFromDynamicType(entityType, graph, dynamicTypeProperties) - }; + throw Error.ArgumentNull("action"); + } - resource.IsTransient = true; - await writer.WriteStartAsync(resource); - foreach (var property in dynamicTypeProperties.Keys) + if (resourceContext == null) { - var resourceContext = new ResourceContext(writeContext, expectedType.AsEntity(), graph); - if (entityType.NavigationProperties().Any(p => p.Type.Equals(property.Type)) && !(property.Type is EdmCollectionTypeReference)) - { - var navigationProperty = entityType.NavigationProperties().FirstOrDefault(p => p.Type.Equals(property.Type)); - var navigationLink = CreateNavigationLink(navigationProperty, resourceContext); - if (navigationLink != null) - { - await writer.WriteStartAsync(navigationLink); - await WriteDynamicTypeResourceAsync(dynamicTypeProperties[property], writer, property.Type, writeContext); - await writer.WriteEndAsync(); - } - } - else - { - ODataNestedResourceInfo nestedResourceInfo = new ODataNestedResourceInfo - { - IsCollection = property.Type.IsCollection(), - Name = property.Name - }; + throw Error.ArgumentNull("resourceContext"); + } - await writer.WriteStartAsync(nestedResourceInfo); - await WriteDynamicComplexPropertyAsync(dynamicTypeProperties[property], property.Type, resourceContext, writer); - await writer.WriteEndAsync(); - } + IEdmModel model = resourceContext.EdmModel; + OperationLinkBuilder builder = model.GetOperationLinkBuilder(action); + + if (builder == null) + { + return null; } - await writer.WriteEndAsync(); + return CreateODataOperation(action, builder, resourceContext) as ODataAction; } - private void WriteResource(object graph, ODataWriter writer, ODataSerializerContext writeContext, - IEdmTypeReference expectedType) + /// + /// Creates an to be written for the given action and the entity instance. + /// + /// The OData function. + /// The context for the entity instance being written. + /// The created function or null if the action should not be written. + [SuppressMessage("Microsoft.Usage", "CA2234: Pass System.Uri objects instead of strings", + Justification = "This overload is equally good")] + [SuppressMessage("Microsoft.Naming", "CA1716: Use function as parameter name", Justification = "Function")] + public virtual ODataFunction CreateODataFunction(IEdmFunction function, ResourceContext resourceContext) { - Contract.Assert(writeContext != null); + if (function == null) + { + throw Error.ArgumentNull("function"); + } - if (EdmLibHelpers.IsDynamicTypeWrapper(graph.GetType())) + if (resourceContext == null) { - WriteDynamicTypeResource(graph, writer, expectedType, writeContext); - return; + throw Error.ArgumentNull("resourceContext"); } - IEdmStructuredTypeReference structuredType = GetResourceType(graph, writeContext); - ResourceContext resourceContext = new ResourceContext(writeContext, structuredType, graph); + IEdmModel model = resourceContext.EdmModel; + OperationLinkBuilder builder = model.GetOperationLinkBuilder(function); - SelectExpandNode selectExpandNode = CreateSelectExpandNode(resourceContext); - if (selectExpandNode != null) + if (builder == null) { - ODataResource resource = CreateResource(selectExpandNode, resourceContext); - if (resource != null) - { - if (resourceContext.SerializerContext.ExpandReference) - { - writer.WriteEntityReferenceLink(new ODataEntityReferenceLink - { - Url = resource.Id - }); - } - else - { - writer.WriteStart(resource); - WriteStreamProperties(selectExpandNode, resourceContext, writer); - WriteComplexProperties(selectExpandNode, resourceContext, writer); - WriteDynamicComplexProperties(resourceContext, writer); - WriteNavigationLinks(selectExpandNode, resourceContext, writer); - WriteExpandedNavigationProperties(selectExpandNode, resourceContext, writer); - WriteReferencedNavigationProperties(selectExpandNode, resourceContext, writer); - writer.WriteEnd(); - } - } + return null; } + + return CreateODataOperation(function, builder, resourceContext) as ODataFunction; } - private async Task WriteResourceAsync(object graph, ODataWriter writer, ODataSerializerContext writeContext, - IEdmTypeReference expectedType) + internal static void EmitTitle(IEdmModel model, IEdmOperation operation, ODataOperation odataOperation) { - Contract.Assert(writeContext != null); - - if (EdmLibHelpers.IsDynamicTypeWrapper(graph.GetType())) + // The title should only be emitted in full metadata. + OperationTitleAnnotation titleAnnotation = model.GetOperationTitleAnnotation(operation); + if (titleAnnotation != null) { - await WriteDynamicTypeResourceAsync(graph, writer, expectedType, writeContext); - return; + odataOperation.Title = titleAnnotation.Title; + } + else + { + odataOperation.Title = operation.Name; } + } - IEdmStructuredTypeReference structuredType = GetResourceType(graph, writeContext); - ResourceContext resourceContext = new ResourceContext(writeContext, structuredType, graph); + internal static string CreateMetadataFragment(IEdmOperation operation) + { + // There can only be one entity container in OData V4. + string actionName = operation.Name; + string fragment = operation.Namespace + "." + actionName; - SelectExpandNode selectExpandNode = CreateSelectExpandNode(resourceContext); - if (selectExpandNode != null) + return fragment; + } + + 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 + // null when values should not be serialized. The TypeName property is different and should always be + // provided to ODataLib to enable model validation. A separate annotation is used to decide whether or not + // to serialize the type name (a null value prevents serialization). + + // Note: In the current version of ODataLib the default behavior likely now matches the requirements for + // minimal metadata mode. However, there have been behavior changes/bugs there in the past, so the safer + // option is for this class to take control of type name serialization in minimal metadata mode. + + Contract.Assert(resource != null); + + string typeName = null; // Set null to force the type name not to serialize. + + // Provide the type name to serialize. + if (!ShouldSuppressTypeNameSerialization(resource, odataPathType, metadataLevel)) { - ODataResource resource = CreateResource(selectExpandNode, resourceContext); - if (resource != null) - { - if (resourceContext.SerializerContext.ExpandReference) - { - await writer.WriteEntityReferenceLinkAsync(new ODataEntityReferenceLink - { - Url = resource.Id - }); - } - else - { - await writer.WriteStartAsync(resource); - await WriteStreamPropertiesAsync(selectExpandNode, resourceContext, writer); - await WriteComplexPropertiesAsync(selectExpandNode, resourceContext, writer); - await WriteDynamicComplexPropertiesAsync(resourceContext, writer); - await WriteNavigationLinksAsync(selectExpandNode, resourceContext, writer); - await WriteExpandedNavigationPropertiesAsync(selectExpandNode, resourceContext, writer); - await WriteReferencedNavigationPropertiesAsync(selectExpandNode, resourceContext, writer); - await writer.WriteEndAsync(); - } - } + typeName = resource.TypeName; } + + resource.TypeAnnotation = new ODataTypeAnnotation(typeName); } - /// - /// 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) + internal static void AddTypeNameAnnotationAsNeededForComplex(ODataResourceBase resource, ODataMetadataLevel metadataLevel) { - if (resourceContext == null) + // 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 + // provided to ODataLib to enable model validation. A separate annotation is used to decide whether or not + // to serialize the type name (a null value prevents serialization). + Contract.Assert(resource != null); + + // Only add an annotation if we want to override ODataLib's default type name serialization behavior. + if (ShouldAddTypeNameAnnotationForComplex(metadataLevel)) { - throw Error.ArgumentNull("resourceContext"); - } + string typeName; - ODataSerializerContext writeContext = resourceContext.SerializerContext; - IEdmStructuredType structuredType = resourceContext.StructuredType; + // Provide the type name to serialize (or null to force it not to serialize). + if (ShouldSuppressTypeNameSerializationForComplex(metadataLevel)) + { + typeName = null; + } + else + { + typeName = resource.TypeName; + } - object selectExpandNode; + resource.TypeAnnotation = new ODataTypeAnnotation(typeName); + } + } - Tuple key = Tuple.Create(writeContext.SelectExpandClause, structuredType); - if (!writeContext.Items.TryGetValue(key, out selectExpandNode)) + internal static bool ShouldAddTypeNameAnnotationForComplex(ODataMetadataLevel metadataLevel) + { + switch (metadataLevel) { - // 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; + // For complex types, the default behavior matches the requirements for minimal metadata mode, so no + // annotation is necessary. + case ODataMetadataLevel.MinimalMetadata: + return false; + // In other cases, this class must control the type name serialization behavior. + case ODataMetadataLevel.FullMetadata: + case ODataMetadataLevel.NoMetadata: + default: // All values already specified; just keeping the compiler happy. + return true; } - - return selectExpandNode as SelectExpandNode; } - /// - /// Creates the to be written while writing this resource. - /// - /// The describing the response graph. - /// The context for the resource instance being written. - /// The created . - public virtual ODataResource CreateResource(SelectExpandNode selectExpandNode, ResourceContext resourceContext) + internal static bool ShouldSuppressTypeNameSerializationForComplex(ODataMetadataLevel metadataLevel) { - if (selectExpandNode == null) - { - throw Error.ArgumentNull("selectExpandNode"); - } + Contract.Assert(metadataLevel != ODataMetadataLevel.MinimalMetadata); - if (resourceContext == null) + switch (metadataLevel) { - throw Error.ArgumentNull("resourceContext"); + case ODataMetadataLevel.NoMetadata: + return true; + case ODataMetadataLevel.FullMetadata: + default: // All values already specified; just keeping the compiler happy. + return false; } + } - if (resourceContext.SerializerContext.ExpandReference) + internal static bool ShouldOmitOperation(IEdmOperation operation, OperationLinkBuilder builder, + ODataMetadataLevel metadataLevel) + { + Contract.Assert(builder != null); + + switch (metadataLevel) { - return new ODataResource - { - Id = resourceContext.GenerateSelfLink(false) - }; + case ODataMetadataLevel.MinimalMetadata: + case ODataMetadataLevel.NoMetadata: + return operation.IsBound && builder.FollowsConventions; + + case ODataMetadataLevel.FullMetadata: + default: // All values already specified; just keeping the compiler happy. + return false; } + } - string typeName = resourceContext.StructuredType.FullTypeName(); - ODataResource resource = new ODataResource - { - TypeName = typeName, - Properties = CreateStructuralPropertyBag(selectExpandNode, resourceContext), - }; + internal static bool ShouldSuppressTypeNameSerialization(ODataResourceBase resource, IEdmStructuredType edmType, + ODataMetadataLevel metadataLevel) + { + Contract.Assert(resource != null); - if (resourceContext.EdmObject is EdmDeltaEntityObject && resourceContext.NavigationSource != null) + switch (metadataLevel) { - 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); + case ODataMetadataLevel.NoMetadata: + return true; + case ODataMetadataLevel.FullMetadata: + return false; + case ODataMetadataLevel.MinimalMetadata: + default: // All values already specified; just keeping the compiler happy. + string pathTypeName = null; + if (edmType != null) + { + pathTypeName = edmType.FullTypeName(); + } + string resourceTypeName = resource.TypeName; + return String.Equals(resourceTypeName, pathTypeName, StringComparison.Ordinal); } + } - // Try to add the dynamic properties if the structural type is open. - AppendDynamicProperties(resource, selectExpandNode, resourceContext); + private void WriteDeltaComplexAndExpandedNavigationProperty(IEdmProperty edmProperty, SelectExpandClause selectExpandClause, + ResourceContext resourceContext, ODataWriter writer, Type navigationPropertyType = null) + { + Contract.Assert(edmProperty != null); + Contract.Assert(resourceContext != null); + Contract.Assert(writer != null); - // Try to add instance annotations - AppendInstanceAnnotations(resource, resourceContext); + object propertyValue = resourceContext.GetPropertyValue(edmProperty.Name); - if (selectExpandNode.SelectedActions != null) + if (propertyValue == null || propertyValue is NullEdmComplexObject) { - IEnumerable actions = CreateODataActions(selectExpandNode.SelectedActions, resourceContext); - foreach (ODataAction action in actions) + if (edmProperty.Type.IsCollection()) { - resource.AddAction(action); + // A complex or navigation property whose Type attribute specifies a collection, the collection always exists, + // it may just be empty. + // If a collection of complex or entities can be related, it is represented as a JSON array. An empty + // collection of resources (one that contains no resource) is represented as an empty JSON array. + writer.WriteStart(new ODataResourceSet + { + TypeName = edmProperty.Type.FullName() + }); } - } - - if (selectExpandNode.SelectedFunctions != null) - { - IEnumerable functions = CreateODataFunctions(selectExpandNode.SelectedFunctions, resourceContext); - foreach (ODataFunction function in functions) + else { - resource.AddFunction(function); + // If at most one resource can be related, the value is null if no resource is currently related. + writer.WriteStart(resource: null); } - } - IEdmStructuredType pathType = GetODataPathType(resourceContext.SerializerContext); - if (resourceContext.StructuredType.TypeKind == EdmTypeKind.Complex) - { - AddTypeNameAnnotationAsNeededForComplex(resource, resourceContext.SerializerContext.MetadataLevel); + writer.WriteEnd(); } else { - AddTypeNameAnnotationAsNeeded(resource, pathType, resourceContext.SerializerContext.MetadataLevel); - } - - if (resourceContext.StructuredType.TypeKind == EdmTypeKind.Entity && resourceContext.NavigationSource != null) - { - if (!(resourceContext.NavigationSource is IEdmContainedEntitySet)) - { - IEdmModel model = resourceContext.SerializerContext.Model; - NavigationSourceLinkBuilderAnnotation linkBuilder = model.GetNavigationSourceLinkBuilder(resourceContext.NavigationSource); - EntitySelfLinks selfLinks = linkBuilder.BuildEntitySelfLinks(resourceContext, resourceContext.SerializerContext.MetadataLevel); - - if (selfLinks.IdLink != null) - { - resource.Id = selfLinks.IdLink; - } + // create the serializer context for the complex and expanded item. + ODataSerializerContext nestedWriteContext = new ODataSerializerContext(resourceContext, selectExpandClause, edmProperty); + nestedWriteContext.Type = navigationPropertyType; - if (selfLinks.ReadLink != null) - { - resource.ReadLink = selfLinks.ReadLink; - } + // write object. - if (selfLinks.EditLink != null) - { - resource.EditLink = selfLinks.EditLink; - } + // TODO: enable overriding serializer based on type. Currentlky 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())); + // } + if (edmProperty.Type.IsCollection()) + { + ODataDeltaFeedSerializer serializer = new ODataDeltaFeedSerializer(SerializerProvider); + serializer.WriteDeltaFeedInline(propertyValue, edmProperty.Type, writer, nestedWriteContext); } - - string etag = CreateETag(resourceContext); - if (etag != null) + else { - resource.ETag = etag; + ODataResourceSerializer serializer = new ODataResourceSerializer(SerializerProvider); + serializer.WriteDeltaObjectInline(propertyValue, edmProperty.Type, writer, nestedWriteContext); } } - - 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(ODataResource resource, SelectExpandNode selectExpandNode, - ResourceContext resourceContext) + private async Task WriteDeltaComplexAndExpandedNavigationPropertyAsync(IEdmProperty edmProperty, SelectExpandClause selectExpandClause, + ResourceContext resourceContext, ODataWriter writer, Type navigationPropertyType = null) { - Contract.Assert(resource != null); - Contract.Assert(selectExpandNode != null); - Contract.Assert(resourceContext != null); - - if (!resourceContext.StructuredType.IsOpen || // non-open type - (!selectExpandNode.SelectAllDynamicProperties && selectExpandNode.SelectedDynamicProperties == null)) - { - return; - } + Contract.Assert(edmProperty != null); + Contract.Assert(resourceContext != null); + Contract.Assert(writer != null); - bool nullDynamicPropertyEnabled = false; - if (resourceContext.EdmObject is EdmDeltaComplexObject || resourceContext.EdmObject is EdmDeltaEntityObject) - { - nullDynamicPropertyEnabled = true; - } - else if (resourceContext.InternalRequest != null) - { - nullDynamicPropertyEnabled = resourceContext.InternalRequest.Options.NullDynamicPropertyIsEnabled; - } + object propertyValue = resourceContext.GetPropertyValue(edmProperty.Name); - IEdmStructuredType structuredType = resourceContext.StructuredType; - IEdmStructuredObject structuredObject = resourceContext.EdmObject; - object value; - IDelta delta = structuredObject as IDelta; - if (delta == null) + if (propertyValue == null || propertyValue is NullEdmComplexObject) { - PropertyInfo dynamicPropertyInfo = EdmLibHelpers.GetDynamicPropertyDictionary(structuredType, - resourceContext.EdmModel); - if (dynamicPropertyInfo == null || structuredObject == null || - !structuredObject.TryGetPropertyValue(dynamicPropertyInfo.Name, out value) || value == null) + if (edmProperty.Type.IsCollection()) { - return; + // A complex or navigation property whose Type attribute specifies a collection, the collection always exists, + // it may just be empty. + // If a collection of complex or entities can be related, it is represented as a JSON array. An empty + // collection of resources (one that contains no resource) is represented as an empty JSON array. + await writer.WriteStartAsync(new ODataResourceSet + { + TypeName = edmProperty.Type.FullName() + }); + } + else + { + // If at most one resource can be related, the value is null if no resource is currently related. + await writer.WriteStartAsync(resource: null); } + + await writer.WriteEndAsync(); } else { - value = ((EdmStructuredObject)structuredObject).TryGetDynamicProperties(); - } + // create the serializer context for the complex and expanded item. + ODataSerializerContext nestedWriteContext = new ODataSerializerContext(resourceContext, selectExpandClause, edmProperty); + nestedWriteContext.Type = navigationPropertyType; - IDictionary dynamicPropertyDictionary = (IDictionary)value; + // write object. - // Build a HashSet to store the declared property names. - // It is used to make sure the dynamic property name is different from all declared property names. - HashSet declaredPropertyNameSet = new HashSet(resource.Properties.Select(p => p.Name)); - List dynamicProperties = new List(); + // TODO: enable overriding serializer based on type. Currentlky 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())); + // } + if (edmProperty.Type.IsCollection()) + { + ODataDeltaFeedSerializer serializer = new ODataDeltaFeedSerializer(SerializerProvider); + await serializer.WriteDeltaFeedInlineAsync(propertyValue, edmProperty.Type, writer, nestedWriteContext); + } + else + { + ODataResourceSerializer serializer = new ODataResourceSerializer(SerializerProvider); + await serializer.WriteDeltaObjectInlineAsync(propertyValue, edmProperty.Type, writer, nestedWriteContext); + } + } + } + + private static IEnumerable CreateODataPropertiesFromDynamicType(EdmEntityType entityType, object graph, + Dictionary dynamicTypeProperties) + { + Contract.Assert(dynamicTypeProperties != null); - // To test SelectedDynamicProperties == null is enough to filter the dynamic properties. - // Because if SelectAllDynamicProperties == true, SelectedDynamicProperties should be null always. - // So `selectExpandNode.SelectedDynamicProperties == null` covers `SelectAllDynamicProperties == true` scenario. - // If `selectExpandNode.SelectedDynamicProperties != null`, then we should test whether the property is selected or not using "Contains(...)". - IEnumerable> dynamicPropertiesToSelect = - dynamicPropertyDictionary.Where(x => selectExpandNode.SelectedDynamicProperties == null || selectExpandNode.SelectedDynamicProperties.Contains(x.Key)); - foreach (KeyValuePair dynamicProperty in dynamicPropertiesToSelect) + var properties = new List(); + var dynamicObject = graph as DynamicTypeWrapper; + if (dynamicObject == null) { - if (String.IsNullOrEmpty(dynamicProperty.Key)) + var dynamicEnumerable = (graph as IEnumerable); + if (dynamicEnumerable != null) { - continue; + dynamicObject = dynamicEnumerable.SingleOrDefault(); } - - if (dynamicProperty.Value == null) + } + if (dynamicObject != null) + { + foreach (var prop in dynamicObject.Values) { - if (nullDynamicPropertyEnabled) + IEdmProperty edmProperty = entityType?.Properties() + .FirstOrDefault(p => p.Name.Equals(prop.Key)); + if (prop.Value != null + && (prop.Value is DynamicTypeWrapper || (prop.Value is IEnumerable))) { - dynamicProperties.Add(new ODataProperty + if (edmProperty != null) { - Name = dynamicProperty.Key, - Value = new ODataNullValue() - }); + dynamicTypeProperties.Add(edmProperty, prop.Value); + } } + else + { + ODataProperty property; + if (prop.Value == null) + { + property = new ODataProperty + { + Name = prop.Key, + Value = new ODataNullValue() + }; + } + else + { + if (edmProperty != null) + { + property = new ODataProperty + { + Name = prop.Key, + Value = ODataPrimitiveSerializer.ConvertPrimitiveValue(prop.Value, edmProperty.Type.AsPrimitive()) + }; + } + else + { + property = new ODataProperty + { + Name = prop.Key, + Value = prop.Value + }; + } + } - continue; - } - - if (declaredPropertyNameSet.Contains(dynamicProperty.Key)) - { - throw Error.InvalidOperation(SRResources.DynamicPropertyNameAlreadyUsedAsDeclaredPropertyName, - dynamicProperty.Key, structuredType.FullTypeName()); + properties.Add(property); + } } + } + return properties; + } - IEdmTypeReference edmTypeReference = resourceContext.SerializerContext.GetEdmType(dynamicProperty.Value, - dynamicProperty.Value.GetType()); - if (edmTypeReference == null) - { - throw Error.NotSupported(SRResources.TypeOfDynamicPropertyNotSupported, - dynamicProperty.Value.GetType().FullName, dynamicProperty.Key); - } + private void WriteDynamicTypeResource(object graph, ODataWriter writer, IEdmTypeReference expectedType, + ODataSerializerContext writeContext) + { + var dynamicTypeProperties = new Dictionary(); + var entityType = expectedType.Definition as EdmEntityType; + var resource = new ODataResource() + { + TypeName = expectedType.FullName(), + Properties = CreateODataPropertiesFromDynamicType(entityType, graph, dynamicTypeProperties) + }; - if (edmTypeReference.IsStructured() || - (edmTypeReference.IsCollection() && edmTypeReference.AsCollection().ElementType().IsStructured())) + resource.IsTransient = true; + writer.WriteStart(resource); + foreach (var property in dynamicTypeProperties.Keys) + { + var resourceContext = new ResourceContext(writeContext, expectedType.AsEntity(), graph); + if (entityType.NavigationProperties().Any(p => p.Type.Equals(property.Type)) && !(property.Type is EdmCollectionTypeReference)) { - if (resourceContext.DynamicComplexProperties == null) + var navigationProperty = entityType.NavigationProperties().FirstOrDefault(p => p.Type.Equals(property.Type)); + var navigationLink = CreateNavigationLink(navigationProperty, resourceContext); + if (navigationLink != null) { - resourceContext.DynamicComplexProperties = new ConcurrentDictionary(); + writer.WriteStart(navigationLink); + WriteDynamicTypeResource(dynamicTypeProperties[property], writer, property.Type, writeContext); + writer.WriteEnd(); } - - resourceContext.DynamicComplexProperties.Add(dynamicProperty); } else { - ODataEdmTypeSerializer propertySerializer = SerializerProvider.GetEdmTypeSerializer(edmTypeReference); - if (propertySerializer == null) + ODataNestedResourceInfo nestedResourceInfo = new ODataNestedResourceInfo { - throw Error.NotSupported(SRResources.DynamicPropertyCannotBeSerialized, dynamicProperty.Key, - edmTypeReference.FullName()); - } + IsCollection = property.Type.IsCollection(), + Name = property.Name + }; - dynamicProperties.Add(propertySerializer.CreateProperty( - dynamicProperty.Value, edmTypeReference, dynamicProperty.Key, resourceContext.SerializerContext)); + writer.WriteStart(nestedResourceInfo); + WriteDynamicComplexProperty(dynamicTypeProperties[property], property.Type, resourceContext, writer); + writer.WriteEnd(); } } - if (dynamicProperties.Any()) - { - resource.Properties = resource.Properties.Concat(dynamicProperties); - } + writer.WriteEnd(); } - /// - /// Method to append InstanceAnnotations to the ODataResource and Property. - /// Instance annotations are annotations for a resource or a property and couldb be of contain a primitive, comple , enum or collection type - /// These will be saved in to an Instance annotation dictionary - /// - /// The describing the resource, which is being annotated. - /// The context for the resource instance, which is being annotated. - public virtual void AppendInstanceAnnotations(ODataResource resource, ResourceContext resourceContext) + private async Task WriteDynamicTypeResourceAsync(object graph, ODataWriter writer, IEdmTypeReference expectedType, + ODataSerializerContext writeContext) { - IEdmStructuredType structuredType = resourceContext.StructuredType; - IEdmStructuredObject structuredObject = resourceContext.EdmObject; - PropertyInfo instanceAnnotationInfo = EdmLibHelpers.GetInstanceAnnotationsContainer(structuredType, - resourceContext.EdmModel); - - object value; - - if (instanceAnnotationInfo == null || structuredObject == null || - !structuredObject.TryGetPropertyValue(instanceAnnotationInfo.Name, out value) || value == null) + var dynamicTypeProperties = new Dictionary(); + var entityType = expectedType.Definition as EdmEntityType; + var resource = new ODataResource() { - return; - } - - IODataInstanceAnnotationContainer instanceAnnotationContainer = value as IODataInstanceAnnotationContainer; + TypeName = expectedType.FullName(), + Properties = CreateODataPropertiesFromDynamicType(entityType, graph, dynamicTypeProperties) + }; - if (instanceAnnotationContainer != null) + resource.IsTransient = true; + await writer.WriteStartAsync(resource); + foreach (var property in dynamicTypeProperties.Keys) { - IDictionary clrAnnotations = instanceAnnotationContainer.GetResourceAnnotations(); - - if (clrAnnotations != null) + var resourceContext = new ResourceContext(writeContext, expectedType.AsEntity(), graph); + if (entityType.NavigationProperties().Any(p => p.Type.Equals(property.Type)) && !(property.Type is EdmCollectionTypeReference)) { - foreach (KeyValuePair annotation in clrAnnotations) + var navigationProperty = entityType.NavigationProperties().FirstOrDefault(p => p.Type.Equals(property.Type)); + var navigationLink = CreateNavigationLink(navigationProperty, resourceContext); + if (navigationLink != null) { - AddODataAnnotations(resource.InstanceAnnotations, resourceContext, annotation); + await writer.WriteStartAsync(navigationLink); + await WriteDynamicTypeResourceAsync(dynamicTypeProperties[property], writer, property.Type, writeContext); + await writer.WriteEndAsync(); } } - - foreach(ODataProperty property in resource.Properties) + else { - string propertyName = property.Name; - - if (property.InstanceAnnotations == null) + ODataNestedResourceInfo nestedResourceInfo = new ODataNestedResourceInfo { - property.InstanceAnnotations = new List(); - } - - IDictionary propertyAnnotations = instanceAnnotationContainer.GetPropertyAnnotations(propertyName); + IsCollection = property.Type.IsCollection(), + Name = property.Name + }; - if (propertyAnnotations != null) - { - foreach (KeyValuePair annotation in propertyAnnotations) - { - AddODataAnnotations(property.InstanceAnnotations, resourceContext, annotation); - } - } - } + await writer.WriteStartAsync(nestedResourceInfo); + await WriteDynamicComplexPropertyAsync(dynamicTypeProperties[property], property.Type, resourceContext, writer); + await writer.WriteEndAsync(); + } } + + await writer.WriteEndAsync(); } - private void AddODataAnnotations(ICollection InstanceAnnotations, ResourceContext resourceContext, KeyValuePair annotation) + private void WriteResource(object graph, ODataWriter writer, ODataSerializerContext writeContext, + IEdmTypeReference expectedType) { - ODataValue annotationValue = null; + Contract.Assert(writeContext != null); - if (annotation.Value != null) + if (EdmLibHelpers.IsDynamicTypeWrapper(graph.GetType())) { - IEdmTypeReference edmTypeReference = resourceContext.SerializerContext.GetEdmType(annotation.Value, - annotation.Value.GetType()); + WriteDynamicTypeResource(graph, writer, expectedType, writeContext); + return; + } - ODataEdmTypeSerializer edmTypeSerializer = GetEdmTypeSerializer(edmTypeReference); + IEdmStructuredTypeReference structuredType = GetResourceType(graph, writeContext); + ResourceContext resourceContext = new ResourceContext(writeContext, structuredType, graph); - if (edmTypeSerializer != null) + SelectExpandNode selectExpandNode = CreateSelectExpandNode(resourceContext); + if (selectExpandNode != null) + { + ODataResource resource = CreateResource(selectExpandNode, resourceContext); + if (resource != null) { - annotationValue = edmTypeSerializer.CreateODataValue(annotation.Value, edmTypeReference, resourceContext.SerializerContext); + if (resourceContext.SerializerContext.ExpandReference) + { + writer.WriteEntityReferenceLink(new ODataEntityReferenceLink + { + Url = resource.Id + }); + } + else + { + writer.WriteStart(resource); + WriteStreamProperties(selectExpandNode, resourceContext, writer); + WriteComplexProperties(selectExpandNode, resourceContext, writer); + WriteDynamicComplexProperties(resourceContext, writer); + WriteNavigationLinks(selectExpandNode, resourceContext, writer); + WriteExpandedNavigationProperties(selectExpandNode, resourceContext, writer); + WriteReferencedNavigationProperties(selectExpandNode, resourceContext, writer); + writer.WriteEnd(); + } } } - else - { - annotationValue = new ODataNullValue(); - } - - if (annotationValue != null) - { - InstanceAnnotations.Add(new ODataInstanceAnnotation(annotation.Key, annotationValue)); - } } - private ODataEdmTypeSerializer GetEdmTypeSerializer(IEdmTypeReference edmTypeReference) + private async Task WriteResourceAsync(object graph, ODataWriter writer, ODataSerializerContext writeContext, + IEdmTypeReference expectedType) { - ODataEdmTypeSerializer edmTypeSerializer; - - if (edmTypeReference.IsCollection()) - { - edmTypeSerializer = new ODataCollectionSerializer(SerializerProvider, true); - } - else if (edmTypeReference.IsStructured()) - { - edmTypeSerializer = new ODataResourceValueSerializer(SerializerProvider); - } - else - { - edmTypeSerializer = SerializerProvider.GetEdmTypeSerializer(edmTypeReference); - } - - return edmTypeSerializer; - } + Contract.Assert(writeContext != null); - /// - /// 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.InternalRequest != null) + if (EdmLibHelpers.IsDynamicTypeWrapper(graph.GetType())) { - IEdmModel model = resourceContext.EdmModel; - IEdmNavigationSource navigationSource = resourceContext.NavigationSource; + await WriteDynamicTypeResourceAsync(graph, writer, expectedType, writeContext); + return; + } - IEnumerable concurrencyProperties; - if (model != null && navigationSource != null) - { - concurrencyProperties = model.GetConcurrencyProperties(navigationSource).OrderBy(c => c.Name); - } - else - { - concurrencyProperties = Enumerable.Empty(); - } + IEdmStructuredTypeReference structuredType = GetResourceType(graph, writeContext); + ResourceContext resourceContext = new ResourceContext(writeContext, structuredType, graph); - IDictionary properties = new Dictionary(); - foreach (IEdmStructuralProperty etagProperty in concurrencyProperties) + SelectExpandNode selectExpandNode = CreateSelectExpandNode(resourceContext); + if (selectExpandNode != null) + { + ODataResource resource = CreateResource(selectExpandNode, resourceContext); + if (resource != null) { - properties.Add(etagProperty.Name, resourceContext.GetPropertyValue(etagProperty.Name)); + if (resourceContext.SerializerContext.ExpandReference) + { + await writer.WriteEntityReferenceLinkAsync(new ODataEntityReferenceLink + { + Url = resource.Id + }); + } + else + { + await writer.WriteStartAsync(resource); + await WriteStreamPropertiesAsync(selectExpandNode, resourceContext, writer); + await WriteComplexPropertiesAsync(selectExpandNode, resourceContext, writer); + await WriteDynamicComplexPropertiesAsync(resourceContext, writer); + await WriteNavigationLinksAsync(selectExpandNode, resourceContext, writer); + await WriteExpandedNavigationPropertiesAsync(selectExpandNode, resourceContext, writer); + await WriteReferencedNavigationPropertiesAsync(selectExpandNode, resourceContext, writer); + await writer.WriteEndAsync(); + } } - - return resourceContext.InternalRequest.CreateETag(properties); } - - return null; } /// @@ -1329,7 +1733,8 @@ private IEnumerable> GetPro if (complexProperties != null) { IEnumerable changedProperties = null; - if (null != resourceContext.EdmObject && resourceContext.EdmObject.IsDeltaResource()) + + if (resourceContext.EdmObject != null && resourceContext.EdmObject.IsDeltaResource()) { IDelta deltaObject = resourceContext.EdmObject as IDelta; changedProperties = deltaObject.GetChangedPropertyNames(); @@ -1339,12 +1744,65 @@ private IEnumerable> GetPro { if (changedProperties == null || changedProperties.Contains(complexProperty.Key.Name)) { + IEdmTypeReference type = complexProperty.Key == null ? null : complexProperty.Key.Type; + + if (type != null && resourceContext.EdmModel != null) + { + Type clrType = EdmLibHelpers.GetClrType(type.AsStructured(), resourceContext.EdmModel); + + if (clrType != null && clrType == typeof(ODataIdContainer)) + { + continue; + } + } + yield return complexProperty; } } } } + private IEnumerable> GetNavigationPropertiesToWrite(SelectExpandNode selectExpandNode, ResourceContext resourceContext) + { + ISet navigationProperties = selectExpandNode.SelectedNavigationProperties; + + if (navigationProperties == null) + { + yield break; + } + + if (resourceContext.EdmObject is IDelta changedObject) + { + IEnumerable changedProperties = changedObject.GetChangedPropertyNames(); + + foreach (IEdmNavigationProperty navigationProperty in navigationProperties) + { + if (changedProperties != null && changedProperties.Contains(navigationProperty.Name)) + { + yield return new KeyValuePair(navigationProperty, typeof(IEdmChangedObject)); + } + } + } + else if (resourceContext.ResourceInstance is IDelta deltaObject) + { + IEnumerable changedProperties = deltaObject.GetChangedPropertyNames(); + dynamic delta = deltaObject; + + foreach (IEdmNavigationProperty navigationProperty in navigationProperties) + { + object obj = null; + + if (changedProperties != null && changedProperties.Contains(navigationProperty.Name) && delta.DeltaNestedResources.TryGetValue(navigationProperty.Name, out obj)) + { + if (obj != null) + { + yield return new KeyValuePair(navigationProperty, obj.GetType()); + } + } + } + } + } + private void WriteExpandedNavigationProperties(SelectExpandNode selectExpandNode, ResourceContext resourceContext, ODataWriter writer) { Contract.Assert(resourceContext != null); @@ -1498,7 +1956,7 @@ private async Task WriteComplexAndExpandedNavigationPropertyAsync(IEdmProperty e object propertyValue = resourceContext.GetPropertyValue(edmProperty.Name); - if (propertyValue == null || propertyValue is NullEdmComplexObject) + if (propertyValue == null || propertyValue is NullEdmComplexObject || propertyValue is ODataIdContainer) { if (edmProperty.Type.IsCollection()) { @@ -1551,103 +2009,6 @@ private IEnumerable CreateNavigationLinks( } } - /// - /// Creates the to be written while writing this dynamic complex property. - /// - /// The dynamic property name. - /// The dynamic property value. - /// The edm type reference. - /// The context for the complex instance being written. - /// The nested resource info to be written. Returns 'null' will omit this serialization. - /// It enables customer to get more control by overriding this method. - public virtual ODataNestedResourceInfo CreateDynamicComplexNestedResourceInfo(string propertyName, object propertyValue, IEdmTypeReference edmType, ResourceContext resourceContext) - { - ODataNestedResourceInfo nestedInfo = null; - if (propertyName != null && edmType != null) - { - nestedInfo = new ODataNestedResourceInfo - { - IsCollection = edmType.IsCollection(), - Name = propertyName, - }; - } - - return nestedInfo; - } - - /// - /// Creates the to be written while writing this complex property. - /// - /// The complex property for which the nested resource info is being created. - /// The corresponding sub select item belongs to this complex property. - /// The context for the complex instance being written. - /// The nested resource info to be written. Returns 'null' will omit this complex serialization. - /// It enables customer to get more control by overriding this method. - public virtual ODataNestedResourceInfo CreateComplexNestedResourceInfo(IEdmStructuralProperty complexProperty, PathSelectItem pathSelectItem, ResourceContext resourceContext) - { - if (complexProperty == null) - { - throw Error.ArgumentNull(nameof(complexProperty)); - } - - ODataNestedResourceInfo nestedInfo = null; - - if (complexProperty.Type != null) - { - nestedInfo = new ODataNestedResourceInfo - { - IsCollection = complexProperty.Type.IsCollection(), - Name = complexProperty.Name - }; - } - - return nestedInfo; - } - - /// - /// Creates the to be written while writing this entity. - /// - /// The navigation property for which the navigation link is being created. - /// The context for the entity instance being written. - /// The navigation link to be written. - public virtual ODataNestedResourceInfo CreateNavigationLink(IEdmNavigationProperty navigationProperty, ResourceContext resourceContext) - { - if (navigationProperty == null) - { - throw Error.ArgumentNull("navigationProperty"); - } - - if (resourceContext == null) - { - throw Error.ArgumentNull("resourceContext"); - } - - ODataSerializerContext writeContext = resourceContext.SerializerContext; - IEdmNavigationSource navigationSource = writeContext.NavigationSource; - ODataNestedResourceInfo navigationLink = null; - - if (navigationSource != null) - { - IEdmTypeReference propertyType = navigationProperty.Type; - IEdmModel model = writeContext.Model; - NavigationSourceLinkBuilderAnnotation linkBuilder = model.GetNavigationSourceLinkBuilder(navigationSource); - Uri navigationUrl = linkBuilder.BuildNavigationLink(resourceContext, navigationProperty, writeContext.MetadataLevel); - - navigationLink = new ODataNestedResourceInfo - { - IsCollection = propertyType.IsCollection(), - Name = navigationProperty.Name, - }; - - if (navigationUrl != null) - { - navigationLink.Url = navigationUrl; - } - } - - return navigationLink; - } - private IEnumerable CreateStructuralPropertyBag(SelectExpandNode selectExpandNode, ResourceContext resourceContext) { Contract.Assert(selectExpandNode != null); @@ -1658,126 +2019,38 @@ private IEnumerable CreateStructuralPropertyBag(SelectExpandNode { IEnumerable structuralProperties = selectExpandNode.SelectedStructuralProperties; - if (null != resourceContext.EdmObject && resourceContext.EdmObject.IsDeltaResource()) - { - IDelta deltaObject = resourceContext.EdmObject as IDelta; - IEnumerable changedProperties = deltaObject.GetChangedPropertyNames(); - structuralProperties = structuralProperties.Where(p => changedProperties.Contains(p.Name)); - } - - 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; - } - - ODataProperty property = CreateStructuralProperty(structuralProperty, resourceContext); - if (property != null) - { - properties.Add(property); - } - } - } - - return properties; - } - - /// - /// Creates the to be written for the given stream property. - /// - /// The EDM structural property being written. - /// The context for the entity instance being written. - /// The to write. - internal virtual ODataStreamPropertyInfo CreateStreamProperty(IEdmStructuralProperty structuralProperty, ResourceContext resourceContext) - { - if (structuralProperty == null) - { - throw Error.ArgumentNull("structuralProperty"); - } - - if (resourceContext == null) - { - throw Error.ArgumentNull("resourceContext"); - } - - if (structuralProperty.Type == null || !structuralProperty.Type.IsStream()) - { - return null; - } - - if (resourceContext.SerializerContext.MetadataLevel != ODataMetadataLevel.FullMetadata) - { - return null; - } - - // TODO: we need to return ODataStreamReferenceValue if - // 1) If we have the EditLink link builder - // 2) If we have the ReadLink link builder - // 3) If we have the Core.AcceptableMediaTypes annotation associated with the Stream property - - // We need a way for the user to specify a mediatype for an instance of a stream property. - // If specified, we should explicitly write the streamreferencevalue and not let ODL fill it in. - - // Although the mediatype is represented as an instance annotation in JSON, it's really control information. - // So we shouldn't use instance annotations to tell us the media type, but have a separate way to specify the media type. - // Perhaps we define an interface (and stream wrapper class that derives from stream and implements the interface) that exposes a MediaType property. - // If the stream property implements this interface, and it specifies a media-type other than application/octet-stream, we explicitly create and write a StreamReferenceValue with that media type. - // We could also use this type to expose properties for things like ReadLink and WriteLink(and even ETag) - // that the user could specify to something other than the default convention - // if they wanted to provide custom routes for reading/writing the stream values or custom ETag values for the stream. - - // So far, let's return null and let OData.lib to calculate the ODataStreamReferenceValue by conventions. - return null; - } - - /// - /// 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("structuralProperty"); - } - if (resourceContext == null) - { - throw Error.ArgumentNull("resourceContext"); - } - - ODataSerializerContext writeContext = resourceContext.SerializerContext; - - ODataEdmTypeSerializer serializer = SerializerProvider.GetEdmTypeSerializer(structuralProperty.Type); - if (serializer == null) - { - throw new SerializationException( - Error.Format(SRResources.TypeCannotBeSerialized, structuralProperty.Type.FullName())); - } - - object propertyValue = resourceContext.GetPropertyValue(structuralProperty.Name); + if (resourceContext.EdmObject != null && resourceContext.EdmObject.IsDeltaResource()) + { + IDelta deltaObject = resourceContext.EdmObject as IDelta; + IEnumerable changedProperties = deltaObject.GetChangedPropertyNames(); + structuralProperties = structuralProperties.Where(p => changedProperties.Contains(p.Name)); + } - IEdmTypeReference propertyType = structuralProperty.Type; - if (propertyValue != null) - { - if (!propertyType.IsPrimitive() && !propertyType.IsEnum()) + bool isDeletedEntity = resourceContext.EdmObject is EdmDeltaDeletedEntityObject; + + foreach (IEdmStructuralProperty structuralProperty in structuralProperties) { - IEdmTypeReference actualType = writeContext.GetEdmType(propertyValue, propertyValue.GetType()); - if (propertyType != null && propertyType != actualType) + if (structuralProperty.Type != null && structuralProperty.Type.IsStream()) { - propertyType = actualType; + // skip the stream property, the stream property is written in its own logic + continue; + } + + ODataProperty property = CreateStructuralProperty(structuralProperty, resourceContext); + if (property == null || (isDeletedEntity && property.Value == null)) + { + continue; } + + properties.Add(property); } } - return serializer.CreateProperty(propertyValue, propertyType, structuralProperty.Name, writeContext); + return properties; } private IEnumerable CreateODataActions( - IEnumerable actions, ResourceContext resourceContext) + IEnumerable actions, ResourceContext resourceContext) { Contract.Assert(actions != null); Contract.Assert(resourceContext != null); @@ -1792,82 +2065,58 @@ private IEnumerable CreateODataActions( } } - private IEnumerable CreateODataFunctions( - IEnumerable functions, ResourceContext resourceContext) + private void WriteDeltaResource(object graph, ODataWriter writer, ODataSerializerContext writeContext) { - Contract.Assert(functions != null); - Contract.Assert(resourceContext != null); + Contract.Assert(writeContext != null); - foreach (IEdmFunction function in functions) + ResourceContext resourceContext = GetResourceContext(graph, writeContext); + SelectExpandNode selectExpandNode = CreateSelectExpandNode(resourceContext); + if (selectExpandNode != null) { - ODataFunction oDataFunction = CreateODataFunction(function, resourceContext); - if (oDataFunction != null) + ODataResource resource = CreateResource(selectExpandNode, resourceContext); + + if (resource != null) { - yield return oDataFunction; + writer.WriteStart(resource); + WriteDeltaComplexProperties(selectExpandNode, resourceContext, writer); + WriteDeltaNavigationProperties(selectExpandNode, resourceContext, writer); + writer.WriteEnd(); } } } - /// - /// Creates an to be written for the given action and the entity instance. - /// - /// The OData action. - /// The context for the entity instance being written. - /// The created action or null if the action should not be written. - [SuppressMessage("Microsoft.Usage", "CA2234: Pass System.Uri objects instead of strings", Justification = "This overload is equally good")] - public virtual ODataAction CreateODataAction(IEdmAction action, ResourceContext resourceContext) + private async Task WriteDeltaResourceAsync(object graph, ODataWriter writer, ODataSerializerContext writeContext) { - if (action == null) - { - throw Error.ArgumentNull("action"); - } - - if (resourceContext == null) + ResourceContext resourceContext = GetResourceContext(graph, writeContext); + SelectExpandNode selectExpandNode = CreateSelectExpandNode(resourceContext); + if (selectExpandNode != null) { - throw Error.ArgumentNull("resourceContext"); - } - - IEdmModel model = resourceContext.EdmModel; - OperationLinkBuilder builder = model.GetOperationLinkBuilder(action); + ODataResource resource = CreateResource(selectExpandNode, resourceContext); - if (builder == null) - { - return null; + if (resource != null) + { + await writer.WriteStartAsync(resource); + await WriteDeltaComplexPropertiesAsync(selectExpandNode, resourceContext, writer); + await WriteDeltaNavigationPropertiesAsync(selectExpandNode, resourceContext, writer); + await writer.WriteEndAsync(); + } } - - return CreateODataOperation(action, builder, resourceContext) as ODataAction; } - /// - /// Creates an to be written for the given action and the entity instance. - /// - /// The OData function. - /// The context for the entity instance being written. - /// The created function or null if the action should not be written. - [SuppressMessage("Microsoft.Usage", "CA2234: Pass System.Uri objects instead of strings", - Justification = "This overload is equally good")] - [SuppressMessage("Microsoft.Naming", "CA1716: Use function as parameter name", Justification = "Function")] - public virtual ODataFunction CreateODataFunction(IEdmFunction function, ResourceContext resourceContext) + private IEnumerable CreateODataFunctions( + IEnumerable functions, ResourceContext resourceContext) { - if (function == null) - { - throw Error.ArgumentNull("function"); - } - - if (resourceContext == null) - { - throw Error.ArgumentNull("resourceContext"); - } - - IEdmModel model = resourceContext.EdmModel; - OperationLinkBuilder builder = model.GetOperationLinkBuilder(function); + Contract.Assert(functions != null); + Contract.Assert(resourceContext != null); - if (builder == null) + foreach (IEdmFunction function in functions) { - return null; + ODataFunction oDataFunction = CreateODataFunction(function, resourceContext); + if (oDataFunction != null) + { + yield return oDataFunction; + } } - - return CreateODataOperation(function, builder, resourceContext) as ODataFunction; } private static ODataOperation CreateODataOperation(IEdmOperation operation, OperationLinkBuilder builder, ResourceContext resourceContext) @@ -1919,29 +2168,6 @@ private static ODataOperation CreateODataOperation(IEdmOperation operation, Oper return odataOperation; } - internal static void EmitTitle(IEdmModel model, IEdmOperation operation, ODataOperation odataOperation) - { - // The title should only be emitted in full metadata. - OperationTitleAnnotation titleAnnotation = model.GetOperationTitleAnnotation(operation); - if (titleAnnotation != null) - { - odataOperation.Title = titleAnnotation.Title; - } - else - { - odataOperation.Title = operation.Name; - } - } - - internal static string CreateMetadataFragment(IEdmOperation operation) - { - // There can only be one entity container in OData V4. - string actionName = operation.Name; - string fragment = operation.Namespace + "." + actionName; - - return fragment; - } - private static IEdmStructuredType GetODataPathType(ODataSerializerContext serializerContext) { Contract.Assert(serializerContext != null); @@ -1996,128 +2222,6 @@ private static IEdmStructuredType GetODataPathType(ODataSerializerContext serial } } - internal static void AddTypeNameAnnotationAsNeeded(ODataResource resource, IEdmStructuredType odataPathType, - 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 - // provided to ODataLib to enable model validation. A separate annotation is used to decide whether or not - // to serialize the type name (a null value prevents serialization). - - // Note: In the current version of ODataLib the default behavior likely now matches the requirements for - // minimal metadata mode. However, there have been behavior changes/bugs there in the past, so the safer - // option is for this class to take control of type name serialization in minimal metadata mode. - - Contract.Assert(resource != null); - - string typeName = null; // Set null to force the type name not to serialize. - - // Provide the type name to serialize. - if (!ShouldSuppressTypeNameSerialization(resource, odataPathType, metadataLevel)) - { - typeName = resource.TypeName; - } - - resource.TypeAnnotation = new ODataTypeAnnotation(typeName); - } - - internal static void AddTypeNameAnnotationAsNeededForComplex(ODataResource 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 - // provided to ODataLib to enable model validation. A separate annotation is used to decide whether or not - // to serialize the type name (a null value prevents serialization). - Contract.Assert(resource != null); - - // Only add an annotation if we want to override ODataLib's default type name serialization behavior. - if (ShouldAddTypeNameAnnotationForComplex(metadataLevel)) - { - string typeName; - - // Provide the type name to serialize (or null to force it not to serialize). - if (ShouldSuppressTypeNameSerializationForComplex(metadataLevel)) - { - typeName = null; - } - else - { - typeName = resource.TypeName; - } - - resource.TypeAnnotation = new ODataTypeAnnotation(typeName); - } - } - - internal static bool ShouldAddTypeNameAnnotationForComplex(ODataMetadataLevel metadataLevel) - { - switch (metadataLevel) - { - // For complex types, the default behavior matches the requirements for minimal metadata mode, so no - // annotation is necessary. - case ODataMetadataLevel.MinimalMetadata: - return false; - // In other cases, this class must control the type name serialization behavior. - case ODataMetadataLevel.FullMetadata: - case ODataMetadataLevel.NoMetadata: - default: // All values already specified; just keeping the compiler happy. - return true; - } - } - - internal static bool ShouldSuppressTypeNameSerializationForComplex(ODataMetadataLevel metadataLevel) - { - Contract.Assert(metadataLevel != ODataMetadataLevel.MinimalMetadata); - - switch (metadataLevel) - { - case ODataMetadataLevel.NoMetadata: - return true; - case ODataMetadataLevel.FullMetadata: - default: // All values already specified; just keeping the compiler happy. - return false; - } - } - - internal static bool ShouldOmitOperation(IEdmOperation operation, OperationLinkBuilder builder, - ODataMetadataLevel metadataLevel) - { - Contract.Assert(builder != null); - - switch (metadataLevel) - { - case ODataMetadataLevel.MinimalMetadata: - case ODataMetadataLevel.NoMetadata: - return operation.IsBound && builder.FollowsConventions; - - case ODataMetadataLevel.FullMetadata: - default: // All values already specified; just keeping the compiler happy. - return false; - } - } - - internal static bool ShouldSuppressTypeNameSerialization(ODataResource resource, IEdmStructuredType edmType, - ODataMetadataLevel metadataLevel) - { - Contract.Assert(resource != null); - - switch (metadataLevel) - { - case ODataMetadataLevel.NoMetadata: - return true; - case ODataMetadataLevel.FullMetadata: - return false; - case ODataMetadataLevel.MinimalMetadata: - default: // All values already specified; just keeping the compiler happy. - string pathTypeName = null; - if (edmType != null) - { - pathTypeName = edmType.FullTypeName(); - } - string resourceTypeName = resource.TypeName; - return String.Equals(resourceTypeName, pathTypeName, StringComparison.Ordinal); - } - } - private IEdmStructuredTypeReference GetResourceType(object graph, ODataSerializerContext writeContext) { Contract.Assert(graph != null); diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataSerializerContext.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataSerializerContext.cs index f72ee955b8..cb1d21a285 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataSerializerContext.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataSerializerContext.cs @@ -27,6 +27,8 @@ public partial class ODataSerializerContext private ODataQueryContext _queryContext; private SelectExpandClause _selectExpandClause; private bool _isSelectExpandClauseSet; + private bool? _isUntyped; + private bool? _isDeltaOfT; /// /// Initializes a new instance of the class. @@ -163,6 +165,35 @@ internal ODataQueryContext QueryContext /// public ODataPath Path { get; set; } + internal Type Type { get; set; } + + internal bool IsUntyped + { + get + { + if (_isUntyped == null) + { + _isUntyped = typeof(IEdmObject).IsAssignableFrom(Type) || typeof(EdmChangedObjectCollection).IsAssignableFrom(Type); + } + + return _isUntyped.Value; + } + } + + internal bool IsDeltaOfT + { + get + { + if (_isDeltaOfT == null) + { + _isDeltaOfT = Type != null && TypeHelper.IsGenericType(Type) && (Type.GetGenericTypeDefinition() == typeof(Delta<>) || + Type.GetGenericTypeDefinition() == typeof(DeltaSet<>) || Type.GetGenericTypeDefinition() == typeof(DeltaDeletedEntityObject<>)); + } + + return _isDeltaOfT.Value; + } + } + /// /// Gets or sets the root element name which is used when writing primitive and enum types /// @@ -300,6 +331,11 @@ internal IEdmTypeReference GetEdmType(object instance, Type type) } else { + if (typeof(IDeltaSet).IsAssignableFrom(type)) + { + return EdmLibHelpers.ToEdmTypeReference(Path.EdmType, isNullable: false); + } + if (Model == null) { throw Error.InvalidOperation(SRResources.RequestMustHaveModel); @@ -312,7 +348,14 @@ internal IEdmTypeReference GetEdmType(object instance, Type type) { if (instance != null) { - edmType = _typeMappingCache.GetEdmType(instance.GetType(), Model); + if (instance is TypedDelta delta) + { + edmType = _typeMappingCache.GetEdmType(delta.ExpectedClrType, Model); + } + else + { + edmType = _typeMappingCache.GetEdmType(instance.GetType(), Model); + } } if (edmType == null) diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataSerializerHelper.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataSerializerHelper.cs new file mode 100644 index 0000000000..d1a4ff78b4 --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataSerializerHelper.cs @@ -0,0 +1,118 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System.Collections.Generic; +using Microsoft.AspNet.OData.Builder; +using Microsoft.OData; +using Microsoft.OData.Edm; + +namespace Microsoft.AspNet.OData.Formatter.Serialization +{ + /// + /// Helper class for OData Serialization + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1053:StaticHolderTypesShouldNotHaveConstructors")] + internal class ODataSerializerHelper + { + /// + /// Appends instance annotations to an ODataResource. + /// + /// The ODataResource being annotated + /// The context for the resource instance to be annotated. + /// The annotations to write. + /// The SerializerProvider to use to write annotations + internal static void AppendInstanceAnnotations(ODataResourceBase resource, ResourceContext resourceContext, IODataInstanceAnnotationContainer instanceAnnotationContainer, ODataSerializerProvider serializerProvider) + { + if (instanceAnnotationContainer == null) + { + return; + } + + IDictionary clrAnnotations = instanceAnnotationContainer.GetResourceAnnotations(); + + if (clrAnnotations != null) + { + foreach (KeyValuePair annotation in clrAnnotations) + { + AddODataAnnotations(resource.InstanceAnnotations, resourceContext, annotation, serializerProvider); + } + } + + if (resource.Properties != null) + { + foreach (ODataProperty property in resource.Properties) + { + string propertyName = property.Name; + + IDictionary propertyAnnotations = instanceAnnotationContainer.GetPropertyAnnotations(propertyName); + + if (propertyAnnotations != null) + { + foreach (KeyValuePair annotation in propertyAnnotations) + { + AddODataAnnotations(property.InstanceAnnotations, resourceContext, annotation, serializerProvider); + } + } + } + } + } + + /// + /// Adds instance annotations to the ODataInstanceAnnotation container. + /// + /// A collection of instance annotations. + /// The context for the resource instance to be annotated. + /// The annotation to be added to the instance annotations container. + /// The SerializerProvider to use to write annotations. + private static void AddODataAnnotations(ICollection instanceAnnotations, ResourceContext resourceContext, KeyValuePair annotation, ODataSerializerProvider serializerProvider) + { + ODataValue annotationValue = null; + + if (annotation.Value != null) + { + IEdmTypeReference edmTypeReference = resourceContext.SerializerContext.GetEdmType(annotation.Value, + annotation.Value.GetType()); + + ODataEdmTypeSerializer edmTypeSerializer = GetEdmTypeSerializer(edmTypeReference, serializerProvider); + + if (edmTypeSerializer != null) + { + annotationValue = edmTypeSerializer.CreateODataValue(annotation.Value, edmTypeReference, resourceContext.SerializerContext); + } + } + else + { + annotationValue = new ODataNullValue(); + } + + if (annotationValue != null) + { + instanceAnnotations.Add(new ODataInstanceAnnotation(annotation.Key, annotationValue)); + } + } + + private static ODataEdmTypeSerializer GetEdmTypeSerializer(IEdmTypeReference edmTypeReference, ODataSerializerProvider serializerProvider) + { + ODataEdmTypeSerializer edmTypeSerializer; + + if (edmTypeReference.IsCollection()) + { + edmTypeSerializer = new ODataCollectionSerializer(serializerProvider, true); + } + else if (edmTypeReference.IsStructured()) + { + edmTypeSerializer = new ODataResourceValueSerializer(serializerProvider); + } + else + { + edmTypeSerializer = serializerProvider.GetEdmTypeSerializer(edmTypeReference); + } + + return edmTypeSerializer; + } + } +} diff --git a/src/Microsoft.AspNet.OData.Shared/IDeltaSetItem.cs b/src/Microsoft.AspNet.OData.Shared/IDeltaSetItem.cs index 169071918e..45523b11e8 100644 --- a/src/Microsoft.AspNet.OData.Shared/IDeltaSetItem.cs +++ b/src/Microsoft.AspNet.OData.Shared/IDeltaSetItem.cs @@ -13,7 +13,7 @@ namespace Microsoft.AspNet.OData /// /// Basic interface for representing a delta item like delta, deleted entity, etc. /// - internal interface IDeltaSetItem + public interface IDeltaSetItem { /// /// Gets the kind of object within the delta payload. diff --git a/src/Microsoft.AspNet.OData.Shared/Microsoft.AspNet.OData.Shared.projitems b/src/Microsoft.AspNet.OData.Shared/Microsoft.AspNet.OData.Shared.projitems index dce9ef5274..7c8741acdc 100644 --- a/src/Microsoft.AspNet.OData.Shared/Microsoft.AspNet.OData.Shared.projitems +++ b/src/Microsoft.AspNet.OData.Shared/Microsoft.AspNet.OData.Shared.projitems @@ -78,6 +78,7 @@ + diff --git a/src/Microsoft.AspNet.OData.Shared/ResourceContext.cs b/src/Microsoft.AspNet.OData.Shared/ResourceContext.cs index 2950e17097..6cc9baa80a 100644 --- a/src/Microsoft.AspNet.OData.Shared/ResourceContext.cs +++ b/src/Microsoft.AspNet.OData.Shared/ResourceContext.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; +using System.Net.Http; using Microsoft.AspNet.OData.Common; using Microsoft.AspNet.OData.Formatter; using Microsoft.AspNet.OData.Formatter.Deserialization; @@ -195,6 +196,15 @@ public object GetPropertyValue(string propertyName) } object value; + + if (SerializerContext.IsDeltaOfT) + { + if (ResourceInstance is IDelta delta && delta.TryGetPropertyValue(propertyName, out value)) + { + return value; + } + } + if (EdmObject.TryGetPropertyValue(propertyName, out value)) { return value; diff --git a/src/Microsoft.AspNet.OData.Shared/TypeHelper.cs b/src/Microsoft.AspNet.OData.Shared/TypeHelper.cs index edb77fc6b4..bedcbaa0d6 100644 --- a/src/Microsoft.AspNet.OData.Shared/TypeHelper.cs +++ b/src/Microsoft.AspNet.OData.Shared/TypeHelper.cs @@ -574,7 +574,7 @@ internal static PropertyInfo GetProperty(Type targetType, string propertyName) /// true if the type has a default constructor; otherwise returns false. internal static bool HasDefaultConstructor(Type type) { - return (!type.IsClass) || (!type.IsAbstract) || type.GetConstructors(BindingFlags.Public | BindingFlags.Instance) + return (!type.IsClass) || type.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.Static) .Any(x => x.GetParameters().Length == 0); } } diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNet/BulkOperation/BulkOperationController.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNet/BulkOperation/BulkOperationController.cs new file mode 100644 index 0000000000..33ae2ab4f1 --- /dev/null +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNet/BulkOperation/BulkOperationController.cs @@ -0,0 +1,288 @@ +//----------------------------------------------------------------------------- +// +// 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.Linq; +using Microsoft.AspNet.OData; +using Microsoft.AspNet.OData.Extensions; +using Microsoft.AspNet.OData.Routing; +using Microsoft.OData.Edm; +using Microsoft.Test.E2E.AspNet.OData.Common.Controllers; +using Xunit; + +namespace Microsoft.Test.E2E.AspNet.OData.BulkOperation +{ + public class EmployeesController : TestODataController + { + public EmployeesController() + { + if (null == Employees) + { + InitEmployees(); + } + } + + /// + /// static so that the data is shared among requests. + /// + public static IList Employees = null; + + public static IList EmployeesTypeless = null; + + private List Friends = null; + + private void InitEmployees() + { + Friends = new List { new Friend { Id = 1, Name = "Test0", Age = 33 }, new Friend { Id = 2, Name = "Test1", Orders = new List() { new Order { Id = 1, Price = 2 } } }, new Friend { Id = 3, Name = "Test3" }, new Friend { Id = 4, Name = "Test4" } }; + + Employees = new List + { + new Employee() + { + ID=1, + Name="Name1", + SkillSet=new List{Skill.CSharp,Skill.Sql}, + Gender=Gender.Female, + AccessLevel=AccessLevel.Execute, + FavoriteSports = new FavoriteSports{Sport ="Football"}, + NewFriends = new List(){new NewFriend {Id =1, Name ="NewFriendTest1", Age=33, NewOrders= new List() { new NewOrder {Id=1, Price =101 } } } }, + Friends = this.Friends.Where(x=>x.Id ==1 || x.Id==2).ToList() + }, + new Employee() + { + ID=2,Name="Name2", + SkillSet=new List(), + Gender=Gender.Female, + AccessLevel=AccessLevel.Read, + NewFriends = new List(){ new MyNewFriend { Id = 2, MyNewOrders = new List() { new MyNewOrder { Id = 2, Price = 444 , Quantity=2 } } } }, + Friends = this.Friends.Where(x=>x.Id ==3 || x.Id==4).ToList() + }, + new Employee(){ + ID=3,Name="Name3", + SkillSet=new List{Skill.Web,Skill.Sql}, + Gender=Gender.Female, + AccessLevel=AccessLevel.Read|AccessLevel.Write + + }, + }; + } + + private void InitTypeLessEmployees(IEdmEntityType entityType) + { + EmployeesTypeless = new List(); + var emp1 = new EdmEntityObject(entityType); + emp1.TrySetPropertyValue("ID", 1); + emp1.TrySetPropertyValue("Name", "Test1"); + + var friendType = entityType.DeclaredNavigationProperties().First().Type.Definition.AsElementType() as IEdmEntityType; + + var friends = new List(); + var friend1 = new EdmEntityObject(friendType); + friend1.TrySetPropertyValue("Id", 1); + friend1.TrySetPropertyValue("Age", 33); + friend1.TrySetPropertyValue("Name", "Test1"); + + var friend2 = new EdmEntityObject(friendType); + friend2.TrySetPropertyValue("Id", 2); + friend2.TrySetPropertyValue("Name", "Test2"); + + friends.Add(friend1); + friends.Add(friend2); + + emp1.TrySetPropertyValue("UnTypedFriends", friends); + + var emp2 = new EdmEntityObject(entityType); + emp2.TrySetPropertyValue("ID", 2); + emp2.TrySetPropertyValue("Name", "Test2"); + + var friends2 = new List(); + var friend3 = new EdmEntityObject(friendType); + friend3.TrySetPropertyValue("Id", 3); + friend3.TrySetPropertyValue("Name", "Test3"); + + var friend4 = new EdmEntityObject(friendType); + friend4.TrySetPropertyValue("Id", 4); + friend4.TrySetPropertyValue("Name", "Test4"); + + friends2.Add(friend3); + friends2.Add(friend4); + + emp2.TrySetPropertyValue("UnTypedFriends", friends2); + + var emp3 = new EdmEntityObject(entityType); + emp3.TrySetPropertyValue("ID", 3); + emp3.TrySetPropertyValue("Name", "Test3"); + + var friends35 = new List(); + var friend5 = new EdmEntityObject(friendType); + friend5.TrySetPropertyValue("Id", 5); + friend5.TrySetPropertyValue("Name", "Test5"); + + friends35.Add(friend5); + + emp3.TrySetPropertyValue("UnTypedFriends", friends35); + + EmployeesTypeless.Add(emp1); + EmployeesTypeless.Add(emp2); + EmployeesTypeless.Add(emp3); + } + + public DeltaSet PatchWithUsersMethod(DeltaSet friendColl) + { + return friendColl; + } + public EdmChangedObjectCollection PatchWithUsersMethodTypeLess(int key, EdmChangedObjectCollection friendColl) + { + return friendColl; + } + + public EdmChangedObjectCollection EmployeePatchMethodTypeLess(EdmChangedObjectCollection empColl) + { + return empColl; + } + + private void ValidateSuccessfulTypeless() + { + object obj; + Assert.True(EmployeesTypeless.First().TryGetPropertyValue("UnTypedFriends", out obj)); + + var friends = obj as ICollection; + Assert.NotNull(friends); + + object obj1; + + friends.First().TryGetPropertyValue("Name", out obj1); + + Assert.Equal("Friend1", obj1.ToString()); + + } + + [EnableQuery(PageSize = 10, MaxExpansionDepth = 5)] + public ITestActionResult Get() + { + return Ok(Employees.AsQueryable()); + } + + public ITestActionResult Get(int key) + { + var emp = Employees.SingleOrDefault(e => e.ID == key); + return Ok(emp); + } + + [ODataRoute("Employees({key})/Friends")] + public ITestActionResult GetFriends(int key) + { + var emp = Employees.SingleOrDefault(e => e.ID == key); + return Ok(emp.Friends); + } + + [ODataRoute("Employees({key})/UnTypedFriends")] + public ITestActionResult GetUnTypedFriends(int key) + { + var entity = Request.GetModel().FindDeclaredType("Microsoft.Test.E2E.AspNet.OData.BulkInsert.UnTypedEmployee") as IEdmEntityType; + InitTypeLessEmployees(entity); + + foreach (var emp in EmployeesTypeless) + { + object obj; + emp.TryGetPropertyValue("ID", out obj); + + if (Equals(key, obj)) + { + object friends; + emp.TryGetPropertyValue("UntypedFriends", out friends); + return Ok(friends); + } + } + return Ok(); + } + + [ODataRoute("Employees")] + [HttpPatch] + public ITestActionResult PatchEmployees([FromBody] DeltaSet coll) + { + Assert.NotNull(coll); + return Ok(coll); + } + } + + public class CompanyController : TestODataController + { + public static IList Companies = null; + public static IList OverdueOrders = null; + public static IList MyOverdueOrders = null; + + public CompanyController() + { + if (null == Companies) + { + InitCompanies(); + } + } + + private void InitCompanies() + { + OverdueOrders = new List() { new NewOrder { Id = 1, Price = 10, Quantity = 1 }, new NewOrder { Id = 2, Price = 20, Quantity = 2 }, new NewOrder { Id = 3, Price = 30 }, new NewOrder { Id = 4, Price = 40 } }; + MyOverdueOrders = new List() { new MyNewOrder { Id = 1, Price = 10, Quantity = 1 }, new MyNewOrder { Id = 2, Price = 20, Quantity = 2 }, new MyNewOrder { Id = 3, Price = 30 }, new MyNewOrder { Id = 4, Price = 40 } }; + + Companies = new List() { new Company { Id = 1, Name = "Company1", OverdueOrders = OverdueOrders.Where(x => x.Id == 2).ToList(), MyOverdueOrders = MyOverdueOrders.Where(x => x.Id == 2).ToList() } , + new Company { Id = 2, Name = "Company2", OverdueOrders = OverdueOrders.Where(x => x.Id == 3 || x.Id == 4).ToList() } }; + } + + [ODataRoute("Companies")] + [HttpPost] + public ITestActionResult Post([FromBody] Company company) + { + + InitCompanies(); + InitEmployees(); + + if (company.Id == 4) + { + AddNewOrder(company); + } + + Companies.Add(company); + + if (company.Id == 4) + { + ValidateOverdueOrders1(4, 4, 0, 30); + } + else + { + ValidateOverdueOrders1(3, 1); + } + + return Ok(company); + } + + private static void AddNewOrder(Company company) + { + var newOrder = new NewOrder { Id = 4, Price = company.OverdueOrders[1].Price, Quantity = company.OverdueOrders[1].Quantity }; + OverdueOrders.Add(newOrder); + company.OverdueOrders[1] = newOrder; + } + + private void InitEmployees() + { + var cntrl = new EmployeesController(); + } + + private void ValidateOverdueOrders1(int companyId, int orderId, int quantity = 0, int price = 101) + { + var comp = Companies.FirstOrDefault(x => x.Id == companyId); + Assert.NotNull(comp); + + NewOrder order = comp.OverdueOrders.FirstOrDefault(x => x.Id == orderId); + Assert.NotNull(order); + Assert.Equal(orderId, order.Id); + Assert.Equal(price, order.Price); + Assert.Equal(quantity, order.Quantity); + } + } +} \ No newline at end of file diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNet/BulkOperation/BulkOperationDataModel.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNet/BulkOperation/BulkOperationDataModel.cs new file mode 100644 index 0000000000..92a23b73c8 --- /dev/null +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNet/BulkOperation/BulkOperationDataModel.cs @@ -0,0 +1,232 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNet.OData; +using Microsoft.AspNet.OData.Builder; + +namespace Microsoft.Test.E2E.AspNet.OData.BulkOperation +{ + public class Employee + { + [Key] + public int ID { get; set; } + public String Name { get; set; } + public List SkillSet { get; set; } + public Gender Gender { get; set; } + public AccessLevel AccessLevel { get; set; } + + public List Friends { get; set; } + + public List NewFriends { get; set; } + + public List UnTypedFriends { get; set; } + + public FavoriteSports FavoriteSports { get; set; } + + public IODataInstanceAnnotationContainer InstanceAnnotations { get; set; } + } + + [Flags] + public enum AccessLevel + { + Read = 1, + Write = 2, + Execute = 4 + } + + public enum Gender + { + Male = 1, + Female = 2 + } + + public enum Skill + { + CSharp, + Sql, + Web, + } + + public enum Sport + { + Pingpong, + Basketball + } + + public class FavoriteSports + { + public string Sport { get; set; } + } + + public class Friend + { + [Key] + public int Id { get; set; } + + public string Name { get; set; } + + public int Age { get; set; } + + public List Orders { get; set; } + + } + + + public class Order + { + [Key] + public int Id { get; set; } + + public int Price { get; set; } + } + + public class NewFriend + { + [Key] + public int Id { get; set; } + + public string Name { get; set; } + + public int Age { get; set; } + public IODataInstanceAnnotationContainer InstanceAnnotations { get; set; } + + [Contained] + public List NewOrders { get; set; } + + } + + public class MyNewFriend: NewFriend + { + public string MyName { get; set; } + + [Contained] + public List MyNewOrders { get; set; } + } + + public class MyNewOrder + { + [Key] + public int Id { get; set; } + + public int Price { get; set; } + + public int Quantity { get; set; } + } + + public class NewOrder + { + [Key] + public int Id { get; set; } + + public int Price { get; set; } + + public int Quantity { get; set; } + } + + + public class Company + { + [Key] + public int Id { get; set; } + + public string Name { get; set; } + + public List OverdueOrders { get; set; } + + public List MyOverdueOrders { get; set; } + } + + public class UnTypedEmployee + { + [Key] + public int ID { get; set; } + public String Name { get; set; } + + public List UnTypedFriends { get; set; } + } + + public class UnTypedFriend + { + [Key] + public int Id { get; set; } + + public string Name { get; set; } + + public int Age { get; set; } + + public UnTypedAddress Address { get; set; } + + public IODataInstanceAnnotationContainer InstanceAnnotations { get; set; } + } + + public class UnTypedAddress + { + [Key] + public int Id { get; set; } + + public string Street { get; set; } + } + + public class FriendColl : ICollection + { + public FriendColl() { _items = new List(); } + + private IList _items; + + public int Count => _items.Count; + + public bool IsReadOnly => _items.IsReadOnly; + + public void Add(T item) + { + var _item = item as NewFriend; + if (_item != null && _item.Age < 10) + { + throw new NotImplementedException(); + } + + _items.Add(item); + } + + public void Clear() + { + _items.Clear(); + } + + public bool Contains(T item) + { + return _items.Contains(item); + } + + public void CopyTo(T[] array, int arrayIndex) + { + _items.CopyTo(array, arrayIndex); + } + + public IEnumerator GetEnumerator() + { + return _items.GetEnumerator(); + } + + public bool Remove(T item) + { + throw new NotImplementedException(); + + //return _items.Remove(item); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)_items).GetEnumerator(); + } + } + +} \ No newline at end of file diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNet/BulkOperation/BulkOperationEdmModel.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNet/BulkOperation/BulkOperationEdmModel.cs new file mode 100644 index 0000000000..0ccc5aef7e --- /dev/null +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNet/BulkOperation/BulkOperationEdmModel.cs @@ -0,0 +1,113 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using Microsoft.AspNet.OData.Builder; +using Microsoft.OData.Edm; +using Microsoft.Test.E2E.AspNet.OData.Common.Execution; + +namespace Microsoft.Test.E2E.AspNet.OData.BulkOperation +{ + internal class BulkOperationEdmModel + { + public static IEdmModel GetExplicitModel(WebRouteConfiguration configuration) + { + ODataModelBuilder builder = new ODataModelBuilder(); + var employee = builder.EntityType(); + employee.HasKey(c => c.ID); + employee.Property(c => c.Name); + employee.CollectionProperty(c => c.SkillSet); + employee.EnumProperty(c => c.Gender); + employee.EnumProperty(c => c.AccessLevel); + + employee.CollectionProperty(c => c.Friends); + employee.CollectionProperty(c => c.NewFriends); + employee.CollectionProperty(c => c.UnTypedFriends); + + + var skill = builder.EnumType(); + skill.Member(Skill.CSharp); + skill.Member(Skill.Sql); + skill.Member(Skill.Web); + + var gender = builder.EnumType(); + gender.Member(Gender.Female); + gender.Member(Gender.Male); + + var accessLevel = builder.EnumType(); + accessLevel.Member(AccessLevel.Execute); + accessLevel.Member(AccessLevel.Read); + accessLevel.Member(AccessLevel.Write); + + var sport = builder.EnumType(); + sport.Member(Sport.Basketball); + sport.Member(Sport.Pingpong); + + AddBoundActionsAndFunctions(employee); + AddUnboundActionsAndFunctions(builder); + + EntitySetConfiguration employees = builder.EntitySet("Employees"); + builder.Namespace = typeof(Employee).Namespace; + return builder.GetEdmModel(); + } + + public static IEdmModel GetConventionModel(WebRouteConfiguration configuration) + { + ODataConventionModelBuilder builder = configuration.CreateConventionModelBuilder(); + EntitySetConfiguration employees = builder.EntitySet("Employees"); + EntityTypeConfiguration employee = employees.EntityType; + + EntitySetConfiguration friends = builder.EntitySet("Friends"); + EntitySetConfiguration orders = builder.EntitySet("Orders"); + EntitySetConfiguration fnewriends = builder.EntitySet("NewFriends"); + EntitySetConfiguration funtypenewriends = builder.EntitySet("UnTypedFriends"); + EntitySetConfiguration addresses = builder.EntitySet("Address"); + + EntitySetConfiguration unemployees = builder.EntitySet("UnTypedEmployees"); + EntityTypeConfiguration unemployee = unemployees.EntityType; + + EntitySetConfiguration companies = builder.EntitySet("Companies"); + EntitySetConfiguration overdueorders = builder.EntitySet("OverdueOrders"); + EntitySetConfiguration myoverdueorders = builder.EntitySet("MyOverdueOrders"); + EntitySetConfiguration myNewOrders = builder.EntitySet("MyNewOrders"); + + // maybe following lines are not required once bug #1587 is fixed. + // 1587: It's better to support automatically adding actions and functions in ODataConventionModelBuilder. + AddBoundActionsAndFunctions(employee); + AddUnboundActionsAndFunctions(builder); + + builder.Namespace = typeof(Employee).Namespace; + builder.MaxDataServiceVersion = EdmConstants.EdmVersion401; + builder.DataServiceVersion = EdmConstants.EdmVersion401; + + var edmModel = builder.GetEdmModel(); + return edmModel; + } + + private static void AddBoundActionsAndFunctions(EntityTypeConfiguration employee) + { + var actionConfiguration = employee.Action("AddSkill"); + actionConfiguration.Parameter("skill"); + actionConfiguration.ReturnsCollection(); + + var functionConfiguration = employee.Function("GetAccessLevel"); + functionConfiguration.Returns(); + } + + private static void AddUnboundActionsAndFunctions(ODataModelBuilder odataModelBuilder) + { + var actionConfiguration = odataModelBuilder.Action("SetAccessLevel"); + actionConfiguration.Parameter("ID"); + actionConfiguration.Parameter("accessLevel"); + actionConfiguration.Returns(); + + var functionConfiguration = odataModelBuilder.Function("HasAccessLevel"); + functionConfiguration.Parameter("ID"); + functionConfiguration.Parameter("AccessLevel"); + functionConfiguration.Returns(); + } + } +} \ No newline at end of file diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNet/BulkOperation/BulkOperationTest.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNet/BulkOperation/BulkOperationTest.cs new file mode 100644 index 0000000000..2ec20c8ddf --- /dev/null +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNet/BulkOperation/BulkOperationTest.cs @@ -0,0 +1,103 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNet.OData; +using Microsoft.AspNet.OData.Extensions; +using Microsoft.AspNet.OData.Routing; +using Microsoft.AspNet.OData.Routing.Conventions; +using Microsoft.Test.E2E.AspNet.OData.Common.Execution; +using Xunit; + +namespace Microsoft.Test.E2E.AspNet.OData.BulkOperation +{ + public class BulkOperationTest : WebHostTestBase + { + public BulkOperationTest(WebHostTestFixture fixture) + : base(fixture) + { + } + + protected override void UpdateConfiguration(WebRouteConfiguration configuration) + { + var controllers = new[] { typeof(EmployeesController), typeof(CompanyController), typeof(MetadataController) }; + configuration.AddControllers(controllers); + + configuration.Routes.Clear(); + configuration.Count().Filter().OrderBy().Expand().MaxTop(null).Select(); + configuration.MapODataServiceRoute("convention", "convention", BulkOperationEdmModel.GetConventionModel(configuration)); + configuration.MapODataServiceRoute("explicit", "explicit", BulkOperationEdmModel.GetExplicitModel(configuration), new DefaultODataPathHandler(), ODataRoutingConventions.CreateDefault()); + configuration.EnsureInitialized(); + } + + [Fact] + public async Task PatchEmployee_WithNestedFriends_WithNestedOrders_IsSerializedSuccessfully() + { + //Arrange + string requestUri = this.BaseAddress + "/convention/Employees"; + var content = @"{ + '@odata.context':'http://host/service/$metadata#Employees/$delta', + 'value':[ + {'ID':1,'Name':'Employee1','Friends@odata.delta':[{'Id':1,'Name':'Friend1','Orders@odata.delta':[{'Id':1,'Price': 10},{'Id':2,'Price':20} ]},{'Id':2,'Name':'Friend2'}]}, + {'ID':2,'Name':'Employee2','Friends@odata.delta':[{'Id':3,'Name':'Friend3','Orders@odata.delta' :[{'Id':3,'Price': 30}, {'Id':4,'Price': 40} ]},{'Id':4,'Name':'Friend4'}]} + ]}"; + + var requestForPatch = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + + string expectedResponse = "{" + + "\"@context\":\""+ this.BaseAddress + "/convention/$metadata#Employees/$delta\"," + + "\"value\":[" + + "{\"ID\":1,\"Name\":\"Employee1\",\"SkillSet\":[],\"Gender\":\"0\",\"AccessLevel\":\"0\",\"FavoriteSports\":null,\"Friends@delta\":[{\"Id\":1,\"Name\":\"Friend1\",\"Age\":0,\"Orders@delta\":[{\"Id\":1,\"Price\":10},{\"Id\":2,\"Price\":20}]},{\"Id\":2,\"Name\":\"Friend2\",\"Age\":0}]}," + + "{\"ID\":2,\"Name\":\"Employee2\",\"SkillSet\":[],\"Gender\":\"0\",\"AccessLevel\":\"0\",\"FavoriteSports\":null,\"Friends@delta\":[{\"Id\":3,\"Name\":\"Friend3\",\"Age\":0,\"Orders@delta\":[{\"Id\":3,\"Price\":30},{\"Id\":4,\"Price\":40}]},{\"Id\":4,\"Name\":\"Friend4\",\"Age\":0}]}]}"; + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPatch.Content = stringContent; + + // Act & Assert + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPatch)) + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(expectedResponse.ToString().ToLower(), json.ToString().ToLower()); + Assert.Contains("Employee1", json); + Assert.Contains("Employee2", json); + } + } + + [Fact] + public async Task PatchEmployee_WithDeletedAndODataId_IsSerializedSuccessfully() + { + //Arrange + string requestUri = this.BaseAddress + "/convention/Employees"; + + var content = @"{ + '@odata.context':'http://host/service/$metadata#Employees/$delta', + 'value':[ + {'ID':1,'Name':'Employee1','Friends@odata.delta':[{'@odata.removed':{'reason':'changed'},'Id':1}]}, + {'ID':2,'Name':'Employee2','Friends@odata.delta':[{'@odata.id':'Friends(1)'}]} + ]}"; + + string expectedResponse = "{\"@context\":\""+ this.BaseAddress + "/convention/$metadata#Employees/$delta\",\"value\":[{\"ID\":1,\"Name\":\"Employee1\",\"SkillSet\":[],\"Gender\":\"0\",\"AccessLevel\":\"0\",\"FavoriteSports\":null,\"Friends@delta\":[{\"@removed\":{\"reason\":\"changed\"},\"@id\":\"http://host/service/Friends(1)\",\"Id\":1,\"Name\":null,\"Age\":0}]},{\"ID\":2,\"Name\":\"Employee2\",\"SkillSet\":[],\"Gender\":\"0\",\"AccessLevel\":\"0\",\"FavoriteSports\":null,\"Friends@delta\":[{\"Id\":0,\"Name\":null,\"Age\":0}]}]}"; + + var requestForUpdate = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForUpdate.Content = stringContent; + + //Act & Assert + using (HttpResponseMessage response = await this.Client.SendAsync(requestForUpdate)) + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(expectedResponse.ToString().ToLower(), json.ToString().ToLower()); + } + } + } +} diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNet/Microsoft.Test.E2E.AspNet.OData.csproj b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNet/Microsoft.Test.E2E.AspNet.OData.csproj index 272f5397ff..4e6f47d4a0 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNet/Microsoft.Test.E2E.AspNet.OData.csproj +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNet/Microsoft.Test.E2E.AspNet.OData.csproj @@ -1895,6 +1895,10 @@ Validation\DeltaOfTValidationTests.cs + + + + diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/DeltaQueryTests/DeltaQueryTests.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/DeltaQueryTests/DeltaQueryTests.cs index 7b84f34710..c76069ecc3 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/DeltaQueryTests/DeltaQueryTests.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/DeltaQueryTests/DeltaQueryTests.cs @@ -28,7 +28,7 @@ namespace Microsoft.Test.E2E.AspNet.OData public class DeltaQueryTests : WebHostTestBase { public DeltaQueryTests(WebHostTestFixture fixture) - :base(fixture) + : base(fixture) { } @@ -51,6 +51,7 @@ public async Task DeltaVerifyReslt() { HttpRequestMessage get = new HttpRequestMessage(HttpMethod.Get, BaseAddress + "/odata/TestCustomers?$deltaToken=abc"); get.Headers.Add("Accept", "application/json;odata.metadata=minimal"); + get.Headers.Add("OData-Version", "4.01"); HttpResponseMessage response = await Client.SendAsync(get); Assert.True(response.IsSuccessStatusCode); dynamic results = await response.Content.ReadAsObject(); @@ -59,7 +60,7 @@ public async Task DeltaVerifyReslt() var changeEntity = results.value[0]; Assert.True(((JToken)changeEntity).Count() == 7, "The changed customer should have 6 properties plus type written."); - string changeEntityType = changeEntity["@odata.type"].Value as string; + string changeEntityType = changeEntity["@type"].Value as string; Assert.True(changeEntityType != null, "The changed customer should have type written"); Assert.True(changeEntityType.Contains("#Microsoft.Test.E2E.AspNet.OData.TestCustomerWithAddress"), "The changed order should be a TestCustomerWithAddress"); Assert.True(changeEntity.Id.Value == 1, "The ID Of changed customer should be 1."); @@ -98,22 +99,22 @@ public async Task DeltaVerifyReslt() var newOrder = results.value[2]; Assert.True(((JToken)newOrder).Count() == 3, "The new order should have 2 properties plus context written"); - string newOrderContext = newOrder["@odata.context"].Value as string; + 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"); Assert.True(newOrder.Id.Value == 27, "The ID of the new order should be 27"); Assert.True(newOrder.Amount.Value == 100, "The amount of the new order should be 100"); var deletedEntity = results.value[3]; - Assert.True(deletedEntity.id.Value == "7", "The ID of the deleted customer should be 7"); - Assert.True(deletedEntity.reason.Value == "changed", "The reason for the deleted customer should be 'changed'"); + Assert.True(deletedEntity["@id"].Value == "7", "The ID of the deleted customer should be 7"); + Assert.True(deletedEntity["@removed"].reason.Value == "changed", "The reason for the deleted customer should be 'changed'"); var deletedOrder = results.value[4]; - string deletedOrderContext = deletedOrder["@odata.context"].Value as string; + string deletedOrderContext = deletedOrder["@context"].Value as string; Assert.True(deletedOrderContext != null, "The deleted order should have a context written"); Assert.True(deletedOrderContext.Contains("$metadata#TestOrders"), "The deleted order should come from the TestOrders entity set"); - Assert.True(deletedOrder.id.Value == "12", "The ID of the deleted order should be 12"); - Assert.True(deletedOrder.reason.Value == "deleted", "The reason for the deleted order should be 'deleted'"); + Assert.True(deletedOrder["@id"].Value == "12", "The ID of the deleted order should be 12"); + Assert.True(deletedOrder["@removed"].reason.Value == "deleted", "The reason for the deleted order should be 'deleted'"); var deletedLink = results.value[5]; Assert.True(deletedLink.source.Value == "http://localhost/odata/DeltaCustomers(1)", "The source of the deleted link should be 'http://localhost/odata/DeltaCustomers(1)'"); @@ -152,7 +153,7 @@ public ITestActionResult Get() changedEntity.TrySetPropertyValue("NullOpenProperty", null); changedObjects.Add(changedEntity); - EdmComplexObjectCollection places = new EdmComplexObjectCollection(new EdmCollectionTypeReference(new EdmCollectionType(new EdmComplexTypeReference(addressType,true)))); + EdmComplexObjectCollection places = new EdmComplexObjectCollection(new EdmCollectionTypeReference(new EdmCollectionType(new EdmComplexTypeReference(addressType, true)))); EdmDeltaComplexObject b = new EdmDeltaComplexObject(addressType); b.TrySetPropertyValue("City", "City2"); b.TrySetPropertyValue("State", "State2"); @@ -167,7 +168,7 @@ public ITestActionResult Get() newCustomer.TrySetPropertyValue("Name", "NewCustomer"); newCustomer.TrySetPropertyValue("FavoritePlaces", places); changedObjects.Add(newCustomer); - + var newOrder = new EdmDeltaEntityObject(orderType); newOrder.NavigationSource = ordersSet; newOrder.TrySetPropertyValue("Id", 27); @@ -206,7 +207,7 @@ public class TestCustomer public int Id { get; set; } public string Name { get; set; } public int Age { get; set; } - public virtual IList PhoneNumbers {get; set;} + public virtual IList PhoneNumbers { get; set; } public virtual IList Orders { get; set; } public virtual IList FavoritePlaces { get; set; } public IDictionary DynamicProperties { get; set; } diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Formatter/Untyped/UntypedDeltaSerializationTests.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Formatter/Untyped/UntypedDeltaSerializationTests.cs index 85704de575..7b209485aa 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Formatter/Untyped/UntypedDeltaSerializationTests.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Formatter/Untyped/UntypedDeltaSerializationTests.cs @@ -27,7 +27,7 @@ namespace Microsoft.Test.E2E.AspNet.OData.Formatter.Untyped public class UntypedDeltaSerializationTests : WebHostTestBase { public UntypedDeltaSerializationTests(WebHostTestFixture fixture) - :base(fixture) + : base(fixture) { } @@ -62,17 +62,15 @@ public async Task UntypedDeltaWorksInAllFormats(string acceptHeader) Assert.True(((dynamic)returnedObject).value.Count == 15); //Verification of content to validate Payload - for (int i = 0 ; i < 10 ; i++) + for (int i = 0; i < 10; i++) { string name = string.Format("Name {0}", i); Assert.True(name.Equals(((dynamic)returnedObject).value[i]["Name"].Value)); } - for (int i=10 ; i < 15 ; i++) + for (int i = 10; i < 15; i++) { - string contextUrl = BaseAddress.ToLowerInvariant() + "/untyped/$metadata#UntypedDeltaCustomers/$deletedEntity"; - Assert.True(contextUrl.Equals(((dynamic)returnedObject).value[i]["@odata.context"].Value)); - Assert.True(i.ToString().Equals(((dynamic)returnedObject).value[i]["id"].Value)); + Assert.True(i.ToString().Equals(((dynamic)returnedObject).value[i]["@id"].Value)); } } } diff --git a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Formatter/Serialization/ODataDeltaFeedSerializerTests.cs b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Formatter/Serialization/ODataDeltaFeedSerializerTests.cs index 0effa38b27..d070f0e42b 100644 --- a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Formatter/Serialization/ODataDeltaFeedSerializerTests.cs +++ b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Formatter/Serialization/ODataDeltaFeedSerializerTests.cs @@ -7,6 +7,7 @@ using System; using System.Collections; +using System.Collections.Generic; using System.IO; using System.Runtime.Serialization; using Microsoft.AspNet.OData.Formatter; @@ -71,7 +72,7 @@ public ODataDeltaFeedSerializerTests() newCustomer.TrySetPropertyValue("HomeAddress", newCustomerAddress); _deltaFeedCustomers.Add(newCustomer); - _customersType = _model.GetEdmTypeReference(typeof(Customer[])).AsCollection(); + _customersType = _model.GetEdmTypeReference(typeof(Customer[])).AsCollection(); _writeContext = new ODataSerializerContext() { NavigationSource = _customerSet, Model = _model, Path = _path }; _serializerProvider = ODataSerializerProviderFactory.Create(); @@ -275,6 +276,7 @@ public void WriteDeltaFeedInline_Can_WriteCollectionOfIEdmChangedObjects() serializerProvider.Setup(s => s.GetEdmTypeSerializer(edmType)).Returns(customerSerializer.Object); ODataDeltaFeedSerializer serializer = new ODataDeltaFeedSerializer(serializerProvider.Object); + _writeContext.Type = typeof(IEdmObject); // Act serializer.WriteDeltaFeedInline(new[] { edmObject.Object }, feedType, mockWriter.Object, _writeContext); @@ -292,6 +294,7 @@ public void WriteDeltaFeedInline_WritesEachEntityInstance() var mockWriter = new Mock(); customerSerializer.Setup(s => s.WriteDeltaObjectInline(_deltaFeedCustomers[0], _customersType.ElementType(), mockWriter.Object, _writeContext)).Verifiable(); _serializer = new ODataDeltaFeedSerializer(provider); + _writeContext.Type = typeof(IEdmObject); // Act _serializer.WriteDeltaFeedInline(_deltaFeedCustomers, _customersType, mockWriter.Object, _writeContext); @@ -354,6 +357,33 @@ public void WriteDeltaFeedInline_Sets_DeltaLink() mockWriter.Verify(); } + [Fact] + public void WriteDeltaFeedInline_Sets_DeltaResource_WithAnnotations() + { + // Arrange + IEnumerable instance = new object[0]; + ODataDeltaResourceSet deltafeed = new ODataDeltaResourceSet { DeltaLink = new Uri("http://deltalink.com/"), InstanceAnnotations = new List() { new ODataInstanceAnnotation("NS.Test", new ODataPrimitiveValue(1)) } }; + Mock serializer = new Mock(_serializerProvider); + serializer.CallBase = true; + serializer.Setup(s => s.CreateODataDeltaFeed(instance, _customersType, _writeContext)).Returns(deltafeed); + var mockWriter = new Mock(); + + mockWriter.Setup(m => m.WriteStart(deltafeed)); + mockWriter + .Setup(m => m.WriteEnd()) + .Callback(() => + { + Assert.Equal("http://deltalink.com/", deltafeed.DeltaLink.AbsoluteUri); + }) + .Verifiable(); + + // Act + serializer.Object.WriteDeltaFeedInline(instance, _customersType, mockWriter.Object, _writeContext); + + // Assert + mockWriter.Verify(); + } + [Fact] public void CreateODataDeltaFeed_Sets_CountValueForPageResult() { diff --git a/test/UnitTest/Microsoft.AspNet.OData.Test/PublicApi/Microsoft.AspNet.OData.PublicApi.bsl b/test/UnitTest/Microsoft.AspNet.OData.Test/PublicApi/Microsoft.AspNet.OData.PublicApi.bsl index b3ab6b066d..1059ca8987 100644 --- a/test/UnitTest/Microsoft.AspNet.OData.Test/PublicApi/Microsoft.AspNet.OData.PublicApi.bsl +++ b/test/UnitTest/Microsoft.AspNet.OData.Test/PublicApi/Microsoft.AspNet.OData.PublicApi.bsl @@ -31,6 +31,13 @@ public interface Microsoft.AspNet.OData.IDeltaDeletedEntityObject { System.Nullable`1[[Microsoft.OData.DeltaDeletedEntryReason]] Reason { public abstract get; public abstract set; } } +public interface Microsoft.AspNet.OData.IDeltaSetItem { + EdmDeltaEntityKind DeltaKind { public abstract get; } + ODataIdContainer ODataIdContainer { public abstract get; public abstract set; } + Microsoft.OData.UriParser.ODataPath ODataPath { public abstract get; public abstract set; } + IODataInstanceAnnotationContainer TransientInstanceAnnotationContainer { public abstract get; public abstract set; } +} + public interface Microsoft.AspNet.OData.IEdmChangedObject : IEdmObject, IEdmStructuredObject { EdmDeltaEntityKind DeltaKind { public abstract get; } } @@ -305,6 +312,15 @@ public class Microsoft.AspNet.OData.DeltaDeletedEntityObject`1 : Delta`1, IDynam System.Nullable`1[[Microsoft.OData.DeltaDeletedEntryReason]] Reason { public virtual get; public virtual set; } } +[ +NonValidatingParameterBindingAttribute(), +] +public class Microsoft.AspNet.OData.DeltaSet`1 : System.Collections.ObjectModel.Collection`1[[Microsoft.AspNet.OData.IDeltaSetItem]], ICollection, IEnumerable, IList, IDeltaSet, ICollection`1, IEnumerable`1, IList`1, IReadOnlyCollection`1, IReadOnlyList`1 { + public DeltaSet`1 (System.Collections.Generic.IList`1[[System.String]] keys) + + protected virtual void InsertItem (int index, IDeltaSetItem item) +} + [ NonValidatingParameterBindingAttribute(), ] @@ -3575,9 +3591,10 @@ public class Microsoft.AspNet.OData.Formatter.Serialization.ODataRawValueSeriali public class Microsoft.AspNet.OData.Formatter.Serialization.ODataResourceSerializer : ODataEdmTypeSerializer { public ODataResourceSerializer (ODataSerializerProvider serializerProvider) - public virtual void AppendDynamicProperties (Microsoft.OData.ODataResource resource, SelectExpandNode selectExpandNode, ResourceContext resourceContext) - public virtual void AppendInstanceAnnotations (Microsoft.OData.ODataResource resource, ResourceContext resourceContext) + public virtual void AppendDynamicProperties (Microsoft.OData.ODataResourceBase resource, SelectExpandNode selectExpandNode, ResourceContext resourceContext) + public virtual void AppendInstanceAnnotations (Microsoft.OData.ODataResourceBase resource, ResourceContext resourceContext) public virtual Microsoft.OData.ODataNestedResourceInfo CreateComplexNestedResourceInfo (Microsoft.OData.Edm.IEdmStructuralProperty complexProperty, Microsoft.OData.UriParser.PathSelectItem pathSelectItem, ResourceContext resourceContext) + public virtual Microsoft.OData.ODataDeletedResource CreateDeletedResource (SelectExpandNode selectExpandNode, ResourceContext resourceContext) public virtual Microsoft.OData.ODataNestedResourceInfo CreateDynamicComplexNestedResourceInfo (string propertyName, object propertyValue, Microsoft.OData.Edm.IEdmTypeReference edmType, ResourceContext resourceContext) public virtual string CreateETag (ResourceContext resourceContext) public virtual Microsoft.OData.ODataNestedResourceInfo CreateNavigationLink (Microsoft.OData.Edm.IEdmNavigationProperty navigationProperty, ResourceContext resourceContext) diff --git a/test/UnitTest/Microsoft.AspNetCore.OData.Test/PublicApi/Microsoft.AspNetCore.OData.PublicApi.bsl b/test/UnitTest/Microsoft.AspNetCore.OData.Test/PublicApi/Microsoft.AspNetCore.OData.PublicApi.bsl index 5b6680e597..b54a1f418b 100644 --- a/test/UnitTest/Microsoft.AspNetCore.OData.Test/PublicApi/Microsoft.AspNetCore.OData.PublicApi.bsl +++ b/test/UnitTest/Microsoft.AspNetCore.OData.Test/PublicApi/Microsoft.AspNetCore.OData.PublicApi.bsl @@ -31,6 +31,13 @@ public interface Microsoft.AspNet.OData.IDeltaDeletedEntityObject { System.Nullable`1[[Microsoft.OData.DeltaDeletedEntryReason]] Reason { public abstract get; public abstract set; } } +public interface Microsoft.AspNet.OData.IDeltaSetItem { + EdmDeltaEntityKind DeltaKind { public abstract get; } + ODataIdContainer ODataIdContainer { public abstract get; public abstract set; } + Microsoft.OData.UriParser.ODataPath ODataPath { public abstract get; public abstract set; } + IODataInstanceAnnotationContainer TransientInstanceAnnotationContainer { public abstract get; public abstract set; } +} + public interface Microsoft.AspNet.OData.IEdmChangedObject : IEdmObject, IEdmStructuredObject { EdmDeltaEntityKind DeltaKind { public abstract get; } } @@ -320,6 +327,15 @@ public class Microsoft.AspNet.OData.DeltaDeletedEntityObject`1 : Delta`1, IDynam System.Nullable`1[[Microsoft.OData.DeltaDeletedEntryReason]] Reason { public virtual get; public virtual set; } } +[ +NonValidatingParameterBindingAttribute(), +] +public class Microsoft.AspNet.OData.DeltaSet`1 : System.Collections.ObjectModel.Collection`1[[Microsoft.AspNet.OData.IDeltaSetItem]], ICollection, IEnumerable, IList, IDeltaSet, ICollection`1, IEnumerable`1, IList`1, IReadOnlyCollection`1, IReadOnlyList`1 { + public DeltaSet`1 (System.Collections.Generic.IList`1[[System.String]] keys) + + protected virtual void InsertItem (int index, IDeltaSetItem item) +} + [ NonValidatingParameterBindingAttribute(), ] @@ -3788,9 +3804,10 @@ public class Microsoft.AspNet.OData.Formatter.Serialization.ODataRawValueSeriali public class Microsoft.AspNet.OData.Formatter.Serialization.ODataResourceSerializer : ODataEdmTypeSerializer { public ODataResourceSerializer (ODataSerializerProvider serializerProvider) - public virtual void AppendDynamicProperties (Microsoft.OData.ODataResource resource, SelectExpandNode selectExpandNode, ResourceContext resourceContext) - public virtual void AppendInstanceAnnotations (Microsoft.OData.ODataResource resource, ResourceContext resourceContext) + public virtual void AppendDynamicProperties (Microsoft.OData.ODataResourceBase resource, SelectExpandNode selectExpandNode, ResourceContext resourceContext) + public virtual void AppendInstanceAnnotations (Microsoft.OData.ODataResourceBase resource, ResourceContext resourceContext) public virtual Microsoft.OData.ODataNestedResourceInfo CreateComplexNestedResourceInfo (Microsoft.OData.Edm.IEdmStructuralProperty complexProperty, Microsoft.OData.UriParser.PathSelectItem pathSelectItem, ResourceContext resourceContext) + public virtual Microsoft.OData.ODataDeletedResource CreateDeletedResource (SelectExpandNode selectExpandNode, ResourceContext resourceContext) public virtual Microsoft.OData.ODataNestedResourceInfo CreateDynamicComplexNestedResourceInfo (string propertyName, object propertyValue, Microsoft.OData.Edm.IEdmTypeReference edmType, ResourceContext resourceContext) public virtual string CreateETag (ResourceContext resourceContext) public virtual Microsoft.OData.ODataNestedResourceInfo CreateNavigationLink (Microsoft.OData.Edm.IEdmNavigationProperty navigationProperty, ResourceContext resourceContext) diff --git a/test/UnitTest/Microsoft.AspNetCore.OData.Test/PublicApi/Microsoft.AspNetCore3x.OData.PublicApi.bsl b/test/UnitTest/Microsoft.AspNetCore.OData.Test/PublicApi/Microsoft.AspNetCore3x.OData.PublicApi.bsl index db1f522863..7b4e5c9219 100644 --- a/test/UnitTest/Microsoft.AspNetCore.OData.Test/PublicApi/Microsoft.AspNetCore3x.OData.PublicApi.bsl +++ b/test/UnitTest/Microsoft.AspNetCore.OData.Test/PublicApi/Microsoft.AspNetCore3x.OData.PublicApi.bsl @@ -31,6 +31,13 @@ public interface Microsoft.AspNet.OData.IDeltaDeletedEntityObject { System.Nullable`1[[Microsoft.OData.DeltaDeletedEntryReason]] Reason { public abstract get; public abstract set; } } +public interface Microsoft.AspNet.OData.IDeltaSetItem { + EdmDeltaEntityKind DeltaKind { public abstract get; } + ODataIdContainer ODataIdContainer { public abstract get; public abstract set; } + Microsoft.OData.UriParser.ODataPath ODataPath { public abstract get; public abstract set; } + IODataInstanceAnnotationContainer TransientInstanceAnnotationContainer { public abstract get; public abstract set; } +} + public interface Microsoft.AspNet.OData.IEdmChangedObject : IEdmObject, IEdmStructuredObject { EdmDeltaEntityKind DeltaKind { public abstract get; } } @@ -324,6 +331,15 @@ public class Microsoft.AspNet.OData.DeltaDeletedEntityObject`1 : Delta`1, IDynam System.Nullable`1[[Microsoft.OData.DeltaDeletedEntryReason]] Reason { public virtual get; public virtual set; } } +[ +NonValidatingParameterBindingAttribute(), +] +public class Microsoft.AspNet.OData.DeltaSet`1 : System.Collections.ObjectModel.Collection`1[[Microsoft.AspNet.OData.IDeltaSetItem]], ICollection, IEnumerable, IList, IDeltaSet, ICollection`1, IEnumerable`1, IList`1, IReadOnlyCollection`1, IReadOnlyList`1 { + public DeltaSet`1 (System.Collections.Generic.IList`1[[System.String]] keys) + + protected virtual void InsertItem (int index, IDeltaSetItem item) +} + [ NonValidatingParameterBindingAttribute(), ] @@ -3987,9 +4003,10 @@ public class Microsoft.AspNet.OData.Formatter.Serialization.ODataRawValueSeriali public class Microsoft.AspNet.OData.Formatter.Serialization.ODataResourceSerializer : ODataEdmTypeSerializer { public ODataResourceSerializer (ODataSerializerProvider serializerProvider) - public virtual void AppendDynamicProperties (Microsoft.OData.ODataResource resource, SelectExpandNode selectExpandNode, ResourceContext resourceContext) - public virtual void AppendInstanceAnnotations (Microsoft.OData.ODataResource resource, ResourceContext resourceContext) + public virtual void AppendDynamicProperties (Microsoft.OData.ODataResourceBase resource, SelectExpandNode selectExpandNode, ResourceContext resourceContext) + public virtual void AppendInstanceAnnotations (Microsoft.OData.ODataResourceBase resource, ResourceContext resourceContext) public virtual Microsoft.OData.ODataNestedResourceInfo CreateComplexNestedResourceInfo (Microsoft.OData.Edm.IEdmStructuralProperty complexProperty, Microsoft.OData.UriParser.PathSelectItem pathSelectItem, ResourceContext resourceContext) + public virtual Microsoft.OData.ODataDeletedResource CreateDeletedResource (SelectExpandNode selectExpandNode, ResourceContext resourceContext) public virtual Microsoft.OData.ODataNestedResourceInfo CreateDynamicComplexNestedResourceInfo (string propertyName, object propertyValue, Microsoft.OData.Edm.IEdmTypeReference edmType, ResourceContext resourceContext) public virtual string CreateETag (ResourceContext resourceContext) public virtual Microsoft.OData.ODataNestedResourceInfo CreateNavigationLink (Microsoft.OData.Edm.IEdmNavigationProperty navigationProperty, ResourceContext resourceContext)