diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/ODataOutputFormatterHelper.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/ODataOutputFormatterHelper.cs index d59728abe4..eca447f439 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/ODataOutputFormatterHelper.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/ODataOutputFormatterHelper.cs @@ -95,7 +95,7 @@ internal static bool CanWriteType( ODataPayloadKind? payloadKind; Type elementType; - if (typeof(IEdmObject).IsAssignableFrom(type) || + if (typeof(IDeltaSet).IsAssignableFrom(type) || typeof(IEdmObject).IsAssignableFrom(type) || (TypeHelper.IsCollection(type, out elementType) && typeof(IEdmObject).IsAssignableFrom(elementType))) { payloadKind = GetEdmObjectPayloadKind(type, internalRequest); @@ -156,7 +156,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 +216,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 +261,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..1dfd9820a6 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,31 @@ private void WriteFeed(IEnumerable enumerable, IEdmTypeReference feedType, OData } lastResource = entry; - IEdmChangedObject edmChangedObject = entry as IEdmChangedObject; - if (edmChangedObject == null) + + 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 { - throw new SerializationException(Error.Format(SRResources.CannotWriteType, GetType().Name, enumerable.GetType().FullName)); + IDeltaSetItem deltaSetItem = entry as IDeltaSetItem; + + if (deltaSetItem == null) + { + throw new SerializationException(Error.Format(SRResources.CannotWriteType, GetType().Name, enumerable.GetType().FullName)); + } + + deltaEntityKind = deltaSetItem.DeltaKind; } - switch (edmChangedObject.DeltaKind) + switch (deltaEntityKind) { case EdmDeltaEntityKind.DeletedEntry: WriteDeltaDeletedEntry(entry, writer, writeContext); @@ -254,6 +279,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 +315,7 @@ private async Task WriteFeedAsync(IEnumerable enumerable, IEdmTypeReference feed Contract.Assert(feedType != null); IEdmStructuredTypeReference elementType = GetResourceType(feedType); + _elementType = elementType; if (elementType.IsComplex()) { @@ -337,13 +364,32 @@ private async Task WriteFeedAsync(IEnumerable enumerable, IEdmTypeReference feed } lastResource = entry; - IEdmChangedObject edmChangedObject = entry as IEdmChangedObject; - if (edmChangedObject == null) + + EdmDeltaEntityKind deltaEntityKind; + if (writeContext.IsUntyped) { - throw new SerializationException(Error.Format(SRResources.CannotWriteType, GetType().Name, enumerable.GetType().FullName)); + 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; } - switch (edmChangedObject.DeltaKind) + + switch (deltaEntityKind) { case EdmDeltaEntityKind.DeletedEntry: await WriteDeltaDeletedEntryAsync(entry, writer, writeContext); @@ -438,15 +484,23 @@ public virtual ODataDeltaResourceSet CreateODataDeltaFeed(IEnumerable feedInstan /// /// The object to be written. /// The to be used for writing. - /// The . + /// The . public virtual void WriteDeltaDeletedEntry(object graph, ODataWriter writer, ODataSerializerContext writeContext) { - ODataDeletedResource deletedResource = GetDeletedResource(graph); + ODataResourceSerializer serializer = SerializerProvider.GetEdmTypeSerializer(_elementType) as ODataResourceSerializer; + ResourceContext resourceContext = serializer.GetResourceContext(graph, writeContext); + SelectExpandNode selectExpandNode = serializer.CreateSelectExpandNode(resourceContext); - if (deletedResource != null) + if (selectExpandNode != null) { - writer.WriteStart(deletedResource); - writer.WriteEnd(); + ODataDeletedResource deletedResource = GetDeletedResource(graph, resourceContext, serializer, selectExpandNode, writeContext.IsUntyped); + + if (deletedResource != null) + { + writer.WriteStart(deletedResource); + serializer.WriteDeltaComplexProperties(selectExpandNode, resourceContext, writer); + writer.WriteEnd(); + } } } @@ -456,14 +510,22 @@ public virtual void WriteDeltaDeletedEntry(object graph, ODataWriter writer, ODa /// /// The object to be written. /// The to be used for writing. - /// The . + /// 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; + ResourceContext resourceContext = serializer.GetResourceContext(graph, writeContext); + SelectExpandNode selectExpandNode = serializer.CreateSelectExpandNode(resourceContext); + + if (selectExpandNode != null) { - await writer.WriteStartAsync(deletedResource); - await writer.WriteEndAsync(); + ODataDeletedResource deletedResource = GetDeletedResource(graph, resourceContext, serializer, selectExpandNode, writeContext.IsUntyped); + + if (deletedResource != null) + { + await writer.WriteStartAsync(deletedResource); + await writer.WriteEndAsync(); + } } } @@ -473,7 +535,7 @@ public virtual async Task WriteDeltaDeletedEntryAsync(object graph, ODataWriter /// /// The object to be written. /// The to be used for writing. - /// The . + /// The . public virtual void WriteDeltaDeletedLink(object graph, ODataWriter writer, ODataSerializerContext writeContext) { ODataDeltaDeletedLink deltaDeletedLink = GetDeletedLink(graph); @@ -489,7 +551,7 @@ public virtual void WriteDeltaDeletedLink(object graph, ODataWriter writer, ODat /// /// The object to be written. /// The to be used for writing. - /// The . + /// The . public virtual async Task WriteDeltaDeletedLinkAsync(object graph, ODataWriter writer, ODataSerializerContext writeContext) { ODataDeltaDeletedLink deltaDeletedLink = GetDeletedLink(graph); @@ -505,7 +567,7 @@ public virtual async Task WriteDeltaDeletedLinkAsync(object graph, ODataWriter w /// /// The object to be written. /// The to be used for writing. - /// The . + /// The . public virtual void WriteDeltaLink(object graph, ODataWriter writer, ODataSerializerContext writeContext) { ODataDeltaLink deltaLink = GetDeltaLink(graph); @@ -521,7 +583,7 @@ public virtual void WriteDeltaLink(object graph, ODataWriter writer, ODataSerial /// /// The object to be written. /// The to be used for writing. - /// The . + /// The . public async Task WriteDeltaLinkAsync(object graph, ODataWriter writer, ODataSerializerContext writeContext) { ODataDeltaLink deltaLink = GetDeltaLink(graph); @@ -531,22 +593,42 @@ 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); } diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataResourceSerializer.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataResourceSerializer.cs index 8d4c2c5a92..31a2641d74 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataResourceSerializer.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataResourceSerializer.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Linq; @@ -184,7 +185,7 @@ public virtual Task WriteDeltaObjectInlineAsync(object graph, IEdmTypeReference { throw new SerializationException(Error.Format(SRResources.CannotSerializerNull, Resource)); } - + return WriteDeltaResourceAsync(graph, writer, writeContext); } @@ -202,12 +203,13 @@ private void WriteDeltaResource(object graph, ODataWriter writer, ODataSerialize { writer.WriteStart(resource); WriteDeltaComplexProperties(selectExpandNode, resourceContext, writer); - //TODO: Need to add support to write Navigation Links, etc. using Delta Writer + WriteDeltaNavigationProperties(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); + //WriteExpandedNavigationProperties(selectExpandNode, resourceContext, writer); writer.WriteEnd(); } @@ -226,6 +228,7 @@ private async Task WriteDeltaResourceAsync(object graph, ODataWriter writer, ODa { await writer.WriteStartAsync(resource); await WriteDeltaComplexPropertiesAsync(selectExpandNode, resourceContext, writer); + await WriteDeltaNavigationPropertiesAsync(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 @@ -238,7 +241,7 @@ private async Task WriteDeltaResourceAsync(object graph, ODataWriter writer, ODa } } - private ResourceContext GetResourceContext(object graph, ODataSerializerContext writeContext) + internal ResourceContext GetResourceContext(object graph, ODataSerializerContext writeContext) { Contract.Assert(writeContext != null); @@ -253,7 +256,7 @@ private ResourceContext GetResourceContext(object graph, ODataSerializerContext return resourceContext; } - private void WriteDeltaComplexProperties(SelectExpandNode selectExpandNode, + internal void WriteDeltaComplexProperties(SelectExpandNode selectExpandNode, ResourceContext resourceContext, ODataWriter writer) { Contract.Assert(resourceContext != null); @@ -275,6 +278,48 @@ private void WriteDeltaComplexProperties(SelectExpandNode selectExpandNode, } } + internal void WriteDeltaNavigationProperties(SelectExpandNode selectExpandNode, ResourceContext resourceContext, ODataWriter writer) + { + Contract.Assert(resourceContext != null); + Contract.Assert(writer != 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(); + } + } + + internal async Task WriteDeltaNavigationPropertiesAsync(SelectExpandNode selectExpandNode, ResourceContext resourceContext, ODataWriter writer) + { + Contract.Assert(resourceContext != null); + Contract.Assert(writer != 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(); + } + } + private async Task WriteDeltaComplexPropertiesAsync(SelectExpandNode selectExpandNode, ResourceContext resourceContext, ODataWriter writer) { @@ -298,7 +343,7 @@ private async Task WriteDeltaComplexPropertiesAsync(SelectExpandNode selectExpan } private void WriteDeltaComplexAndExpandedNavigationProperty(IEdmProperty edmProperty, SelectExpandClause selectExpandClause, - ResourceContext resourceContext, ODataWriter writer) + ResourceContext resourceContext, ODataWriter writer, Type type = null) { Contract.Assert(edmProperty != null); Contract.Assert(resourceContext != null); @@ -331,6 +376,7 @@ private void WriteDeltaComplexAndExpandedNavigationProperty(IEdmProperty edmProp { // create the serializer context for the complex and expanded item. ODataSerializerContext nestedWriteContext = new ODataSerializerContext(resourceContext, selectExpandClause, edmProperty); + nestedWriteContext.Type = type; // write object. @@ -355,7 +401,7 @@ private void WriteDeltaComplexAndExpandedNavigationProperty(IEdmProperty edmProp } private async Task WriteDeltaComplexAndExpandedNavigationPropertyAsync(IEdmProperty edmProperty, SelectExpandClause selectExpandClause, - ResourceContext resourceContext, ODataWriter writer) + ResourceContext resourceContext, ODataWriter writer, Type type = null) { Contract.Assert(edmProperty != null); Contract.Assert(resourceContext != null); @@ -388,6 +434,7 @@ await writer.WriteStartAsync(new ODataResourceSet { // create the serializer context for the complex and expanded item. ODataSerializerContext nestedWriteContext = new ODataSerializerContext(resourceContext, selectExpandClause, edmProperty); + nestedWriteContext.Type = type; // write object. @@ -434,7 +481,7 @@ private static IEnumerable CreateODataPropertiesFromDynamicType(E .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); @@ -451,7 +498,7 @@ private static IEnumerable CreateODataPropertiesFromDynamicType(E Value = new ODataNullValue() }; } - else + else { if (edmProperty != null) { @@ -601,6 +648,7 @@ private void WriteResource(object graph, ODataWriter writer, ODataSerializerCont WriteDynamicComplexProperties(resourceContext, writer); WriteNavigationLinks(selectExpandNode, resourceContext, writer); WriteExpandedNavigationProperties(selectExpandNode, resourceContext, writer); + WriteNestedNavigationProperties(selectExpandNode, resourceContext, writer); WriteReferencedNavigationProperties(selectExpandNode, resourceContext, writer); writer.WriteEnd(); } @@ -643,6 +691,7 @@ await writer.WriteEntityReferenceLinkAsync(new ODataEntityReferenceLink await WriteDynamicComplexPropertiesAsync(resourceContext, writer); await WriteNavigationLinksAsync(selectExpandNode, resourceContext, writer); await WriteExpandedNavigationPropertiesAsync(selectExpandNode, resourceContext, writer); + await WriteNestedNavigationPropertiesAsync(selectExpandNode, resourceContext, writer); await WriteReferencedNavigationPropertiesAsync(selectExpandNode, resourceContext, writer); await writer.WriteEndAsync(); } @@ -687,6 +736,13 @@ public virtual SelectExpandNode CreateSelectExpandNode(ResourceContext resourceC /// The context for the resource instance being written. /// The created . public virtual ODataResource CreateResource(SelectExpandNode selectExpandNode, ResourceContext resourceContext) + { + ODataResource resource = CreateResourceBase(selectExpandNode, resourceContext, false) as ODataResource; + return resource; + } + + + private ODataResourceBase CreateResourceBase(SelectExpandNode selectExpandNode, ResourceContext resourceContext, bool isDeletedResource) { if (selectExpandNode == null) { @@ -700,6 +756,14 @@ public virtual ODataResource CreateResource(SelectExpandNode selectExpandNode, R if (resourceContext.SerializerContext.ExpandReference) { + if (isDeletedResource) + { + return new ODataDeletedResource + { + Id = resourceContext.GenerateSelfLink(false) + }; + } + return new ODataResource { Id = resourceContext.GenerateSelfLink(false) @@ -707,11 +771,25 @@ public virtual ODataResource CreateResource(SelectExpandNode selectExpandNode, R } string typeName = resourceContext.StructuredType.FullTypeName(); - ODataResource resource = new ODataResource + ODataResourceBase resource; + + if (isDeletedResource) { - TypeName = typeName, - Properties = CreateStructuralPropertyBag(selectExpandNode, resourceContext), - }; + resource = new ODataDeletedResource + { + TypeName = typeName, + Properties = CreateStructuralPropertyBag(selectExpandNode, resourceContext), + }; + } + else + { + resource = new ODataResource + { + TypeName = typeName, + Properties = CreateStructuralPropertyBag(selectExpandNode, resourceContext), + }; + } + if (resourceContext.EdmObject is EdmDeltaEntityObject && resourceContext.NavigationSource != null) { @@ -760,7 +838,7 @@ public virtual ODataResource CreateResource(SelectExpandNode selectExpandNode, R AddTypeNameAnnotationAsNeeded(resource, pathType, resourceContext.SerializerContext.MetadataLevel); } - if (resourceContext.StructuredType.TypeKind == EdmTypeKind.Entity && resourceContext.NavigationSource != null) + if (!isDeletedResource && resourceContext.StructuredType.TypeKind == EdmTypeKind.Entity && resourceContext.NavigationSource != null) { if (!(resourceContext.NavigationSource is IEdmContainedEntitySet)) { @@ -794,6 +872,19 @@ public virtual ODataResource CreateResource(SelectExpandNode selectExpandNode, R 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) + { + ODataDeletedResource resource = CreateResourceBase(selectExpandNode, resourceContext, true) as ODataDeletedResource; + 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 @@ -804,12 +895,12 @@ public virtual ODataResource CreateResource(SelectExpandNode selectExpandNode, R /// 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, + public virtual void AppendDynamicProperties(ODataResourceBase resource, SelectExpandNode selectExpandNode, ResourceContext resourceContext) { Contract.Assert(resource != null); Contract.Assert(selectExpandNode != null); - Contract.Assert(resourceContext != null); + Contract.Assert(resourceContext != null); if (!resourceContext.StructuredType.IsOpen || // non-open type (!selectExpandNode.SelectAllDynamicProperties && selectExpandNode.SelectedDynamicProperties == null)) @@ -931,102 +1022,60 @@ public virtual void AppendDynamicProperties(ODataResource resource, SelectExpand /// /// 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) + public virtual void AppendInstanceAnnotations(ODataResourceBase resource, ResourceContext resourceContext) { 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 value; - - if (instanceAnnotationInfo == null || structuredObject == null || - !structuredObject.TryGetPropertyValue(instanceAnnotationInfo.Name, out value) || value == null) - { - return; - } + EdmEntityObject edmEntityObject = null; + object instanceAnnotations = null; + IODataInstanceAnnotationContainer transientAnnotations = null; - IODataInstanceAnnotationContainer instanceAnnotationContainer = value as IODataInstanceAnnotationContainer; + IDelta delta = null; - if (instanceAnnotationContainer != null) + if (resourceContext.SerializerContext.IsDeltaOfT) { - IDictionary clrAnnotations = instanceAnnotationContainer.GetResourceAnnotations(); - - if (clrAnnotations != null) - { - foreach (KeyValuePair annotation in clrAnnotations) - { - AddODataAnnotations(resource.InstanceAnnotations, resourceContext, annotation); - } - } - - foreach(ODataProperty property in resource.Properties) - { - string propertyName = property.Name; - - if (property.InstanceAnnotations == null) - { - property.InstanceAnnotations = new List(); - } - - IDictionary propertyAnnotations = instanceAnnotationContainer.GetPropertyAnnotations(propertyName); - - if (propertyAnnotations != null) - { - foreach (KeyValuePair annotation in propertyAnnotations) - { - AddODataAnnotations(property.InstanceAnnotations, resourceContext, annotation); - } - } - } + delta = resourceContext.ResourceInstance as IDelta; } - } - - private void AddODataAnnotations(ICollection InstanceAnnotations, ResourceContext resourceContext, KeyValuePair annotation) - { - ODataValue annotationValue = null; - if (annotation.Value != null) + if (delta != null) { - IEdmTypeReference edmTypeReference = resourceContext.SerializerContext.GetEdmType(annotation.Value, - annotation.Value.GetType()); + if (instanceAnnotationInfo != null) + { + delta.TryGetPropertyValue(instanceAnnotationInfo.Name, out instanceAnnotations); + + } - ODataEdmTypeSerializer edmTypeSerializer = GetEdmTypeSerializer(edmTypeReference); + IDeltaSetItem deltaitem = resourceContext.ResourceInstance as IDeltaSetItem; - if (edmTypeSerializer != null) + if (deltaitem != null) { - annotationValue = edmTypeSerializer.CreateODataValue(annotation.Value, edmTypeReference, resourceContext.SerializerContext); + transientAnnotations = deltaitem.TransientInstanceAnnotationContainer; } } else { - annotationValue = new ODataNullValue(); - } - - if (annotationValue != null) - { - InstanceAnnotations.Add(new ODataInstanceAnnotation(annotation.Key, annotationValue)); - } - } + if (instanceAnnotationInfo == null || structuredObject == null || + !structuredObject.TryGetPropertyValue(instanceAnnotationInfo.Name, out instanceAnnotations) || instanceAnnotations == null) + { + edmEntityObject = structuredObject as EdmEntityObject; - private ODataEdmTypeSerializer GetEdmTypeSerializer(IEdmTypeReference edmTypeReference) - { - ODataEdmTypeSerializer edmTypeSerializer; - - if (edmTypeReference.IsCollection()) - { - edmTypeSerializer = new ODataCollectionSerializer(SerializerProvider, true); - } - else if (edmTypeReference.IsStructured()) - { - edmTypeSerializer = new ODataResourceValueSerializer(SerializerProvider); - } - else - { - edmTypeSerializer = SerializerProvider.GetEdmTypeSerializer(edmTypeReference); + if (edmEntityObject != null) + { + instanceAnnotations = edmEntityObject.PersistentInstanceAnnotationsContainer; + transientAnnotations = edmEntityObject.TransientInstanceAnnotationContainer; + } + } } - return edmTypeSerializer; + ODataSerializerHelper.AppendInstanceAnnotations(resource, resourceContext, instanceAnnotations, SerializerProvider); + + ODataSerializerHelper.AppendInstanceAnnotations(resource, resourceContext, transientAnnotations, SerializerProvider); } /// @@ -1071,7 +1120,7 @@ private void WriteNavigationLinks(SelectExpandNode selectExpandNode, ResourceCon Contract.Assert(selectExpandNode != null); Contract.Assert(resourceContext != null); - if (selectExpandNode.SelectedNavigationProperties == null) + if (selectExpandNode.SelectedNavigationProperties == null || resourceContext.IsPostRequest) { return; } @@ -1092,7 +1141,7 @@ private async Task WriteNavigationLinksAsync(SelectExpandNode selectExpandNode, Contract.Assert(selectExpandNode != null); Contract.Assert(resourceContext != null); - if (selectExpandNode.SelectedNavigationProperties == null) + if (selectExpandNode.SelectedNavigationProperties == null || resourceContext.IsPostRequest) { return; } @@ -1329,7 +1378,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(); @@ -1345,6 +1395,73 @@ private IEnumerable> GetPro } } + 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()); + } + } + } + } + // Serializing nested navigation properties from a deep insert request. + // We currently don't deserialize Deep insert nested resources as Delta but as T. If this was to change in the future, logic in this method will have to change. + else if (resourceContext.IsPostRequest) + { + object instance = resourceContext.ResourceInstance; + PropertyInfo[] properties = instance.GetType().GetProperties(); + Dictionary propertyNamesAndValues = new Dictionary(); + + foreach (PropertyInfo propertyInfo in properties) + { + string name = propertyInfo.Name; + object value = propertyInfo.GetValue(instance); + propertyNamesAndValues.Add(name, value); + } + + foreach (IEdmNavigationProperty navigationProperty in navigationProperties) + { + if (propertyNamesAndValues.TryGetValue(navigationProperty.Name, out object 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 +1615,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()) { @@ -1535,6 +1652,58 @@ await writer.WriteStartAsync(new ODataResourceSet } } + private void WriteNestedNavigationProperties(SelectExpandNode selectExpandNode, ResourceContext resourceContext, ODataWriter writer) + { + Debug.Assert(resourceContext != null, "resourceContext != null"); + Debug.Assert(writer != null, "writer != null"); + + if (!resourceContext.IsPostRequest) + { + return; + } + + 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); + WriteComplexAndExpandedNavigationProperty(navigationProperty.Key, null, resourceContext, writer); + writer.WriteEnd(); + } + } + + private async Task WriteNestedNavigationPropertiesAsync(SelectExpandNode selectExpandNode, ResourceContext resourceContext, ODataWriter writer) + { + Debug.Assert(resourceContext != null, "resourceContext != null"); + Debug.Assert(writer != null, "writer != null"); + + if (!resourceContext.IsPostRequest) + { + return; + } + + 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 WriteComplexAndExpandedNavigationPropertyAsync(navigationProperty.Key, null, resourceContext, writer); + await writer.WriteEndAsync(); + } + } + private IEnumerable CreateNavigationLinks( IEnumerable navigationProperties, ResourceContext resourceContext) { @@ -1658,13 +1827,15 @@ private IEnumerable CreateStructuralPropertyBag(SelectExpandNode { IEnumerable structuralProperties = selectExpandNode.SelectedStructuralProperties; - if (null != resourceContext.EdmObject && resourceContext.EdmObject.IsDeltaResource()) + 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)); } + bool isDeletedEntity = resourceContext.EdmObject is EdmDeltaDeletedEntityObject; + foreach (IEdmStructuralProperty structuralProperty in structuralProperties) { if (structuralProperty.Type != null && structuralProperty.Type.IsStream()) @@ -1674,10 +1845,12 @@ private IEnumerable CreateStructuralPropertyBag(SelectExpandNode } ODataProperty property = CreateStructuralProperty(structuralProperty, resourceContext); - if (property != null) + if (property == null || (isDeletedEntity && property.Value == null)) { - properties.Add(property); + continue; } + + properties.Add(property); } } @@ -1996,7 +2169,7 @@ private static IEdmStructuredType GetODataPathType(ODataSerializerContext serial } } - internal static void AddTypeNameAnnotationAsNeeded(ODataResource resource, IEdmStructuredType odataPathType, + internal static void AddTypeNameAnnotationAsNeeded(ODataResourceBase resource, IEdmStructuredType odataPathType, ODataMetadataLevel metadataLevel) { // ODataLib normally has the caller decide whether or not to serialize properties by leaving properties @@ -2021,7 +2194,7 @@ internal static void AddTypeNameAnnotationAsNeeded(ODataResource resource, IEdmS resource.TypeAnnotation = new ODataTypeAnnotation(typeName); } - internal static void AddTypeNameAnnotationAsNeededForComplex(ODataResource resource, ODataMetadataLevel metadataLevel) + internal static void AddTypeNameAnnotationAsNeededForComplex(ODataResourceBase resource, ODataMetadataLevel metadataLevel) { // ODataLib normally has the caller decide whether or not to serialize properties by leaving properties // null when values should not be serialized. The TypeName property is different and should always be @@ -2095,7 +2268,7 @@ internal static bool ShouldOmitOperation(IEdmOperation operation, OperationLinkB } } - internal static bool ShouldSuppressTypeNameSerialization(ODataResource resource, IEdmStructuredType edmType, + internal static bool ShouldSuppressTypeNameSerialization(ODataResourceBase resource, IEdmStructuredType edmType, ODataMetadataLevel metadataLevel) { Contract.Assert(resource != null); diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataSerializerContext.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataSerializerContext.cs index f72ee955b8..ee6a9736b6 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(DeltaSet<>) || + Type.GetGenericTypeDefinition() == typeof(Delta<>) || 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,16 @@ internal IEdmTypeReference GetEdmType(object instance, Type type) { if (instance != null) { - edmType = _typeMappingCache.GetEdmType(instance.GetType(), Model); + TypedDelta delta = instance as TypedDelta; + + if (delta != null) + { + 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..6bfc755d10 --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataSerializerHelper.cs @@ -0,0 +1,109 @@ +//----------------------------------------------------------------------------- +// +// 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 + { + internal static void AppendInstanceAnnotations(ODataResourceBase resource, ResourceContext resourceContext, object value, ODataSerializerProvider SerializerProvider) + { + IODataInstanceAnnotationContainer instanceAnnotationContainer = value as IODataInstanceAnnotationContainer; + + if (instanceAnnotationContainer != null) + { + 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; + + if (property.InstanceAnnotations == null) + { + property.InstanceAnnotations = new List(); + } + + IDictionary propertyAnnotations = instanceAnnotationContainer.GetPropertyAnnotations(propertyName); + + if (propertyAnnotations != null) + { + foreach (KeyValuePair annotation in propertyAnnotations) + { + AddODataAnnotations(property.InstanceAnnotations, resourceContext, annotation, SerializerProvider); + } + } + } + } + } + } + + internal 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/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..46b32b284c 100644 --- a/src/Microsoft.AspNet.OData.Shared/ResourceContext.cs +++ b/src/Microsoft.AspNet.OData.Shared/ResourceContext.cs @@ -95,6 +95,18 @@ public IEdmModel EdmModel } } + internal bool IsPostRequest + { + get + { +#if NETCORE + return Request == null ? false : String.Equals(Request.Method, "post", StringComparison.OrdinalIgnoreCase); +#else + return Request == null ? false : String.Equals(Request.Method.ToString(), "post", StringComparison.OrdinalIgnoreCase); +#endif + } + } + /// /// Gets or sets the to which this instance belongs. /// @@ -195,6 +207,15 @@ public object GetPropertyValue(string propertyName) } object value; + if (SerializerContext.IsDeltaOfT) + { + IDelta delta = ResourceInstance as IDelta; + if (delta != null && 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..b087183c1d 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.NonPublic | 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..a3f63a0a9d --- /dev/null +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNet/BulkOperation/BulkOperationController.cs @@ -0,0 +1,259 @@ +//----------------------------------------------------------------------------- +// +// 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); + } + + [EnableQuery(PageSize = 10, MaxExpansionDepth = 5)] + public ITestActionResult Get() + { + return Ok(Employees.AsQueryable()); + } + + [EnableQuery] + 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.BulkOperation.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")] + [HttpPost] + public ITestActionResult Post([FromBody] Employee employee) + { + InitEmployees(); + return Ok(employee); + } + } + + 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..7d4d0427e0 --- /dev/null +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNet/BulkOperation/BulkOperationTest.cs @@ -0,0 +1,218 @@ +//----------------------------------------------------------------------------- +// +// 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.Net.Http.Headers; +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 Microsoft.Test.E2E.AspNet.OData.Common.Extensions; +using Newtonsoft.Json.Linq; +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(); + } + + #region Post + /* [Fact] + public async Task PostCompany_WithODataId() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Companies"; + + var content = @"{'Id':3,'Name':'Company03', + 'OverdueOrders':[{'@odata.id':'Employees(1)/NewFriends(1)/NewOrders(1)'}] + }"; + + var requestForPost = new HttpRequestMessage(new HttpMethod("POST"), requestUri); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPost.Content = stringContent; + + //Act & Assert + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + }*/ + + /* [Fact] + public async Task PostCompany_WithODataId_AndWithout() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Companies"; + + var content = @"{'Id':4,'Name':'Company04', + 'OverdueOrders':[{'@odata.id':'Employees(1)/NewFriends(1)/NewOrders(1)'},{Price:30}] + }"; + + var requestForPost = new HttpRequestMessage(new HttpMethod("POST"), requestUri); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPost.Content = stringContent; + + //Act & Assert + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + }*/ + + [Fact] + public async Task PostEmployee_WithCreateFriends() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Employees"; + + var content = @"{ + 'Name':'SqlUD', + 'Friends':[{ 'Id':1001, 'Name' : 'Friend 1001', 'Age': 31},{ 'Id':1002, 'Name' : 'Friend 1002', 'Age': 32},{ 'Id':1003, 'Name' : 'Friend 1003', 'Age': 33}] + }"; + + var requestForPatch = new HttpRequestMessage(new HttpMethod("POST"), requestUri); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPatch.Content = stringContent; + + var expected = "Friends\":[{\"Id\":1001,\"Name\":\"Friend 1001\",\"Age\":31},{\"Id\":1002,\"Name\":\"Friend 1002\",\"Age\":32},{\"Id\":1003,\"Name\":\"Friend 1003\",\"Age\":33}]"; + + //Act & Assert + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPatch)) + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var json = response.Content.ReadAsStringAsync().Result; + Assert.Contains("SqlUD", json); + Assert.Contains(expected, json); + } + } + + [Fact] + public async Task PostEmployee_WithCreateFriendsFullMetadata() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Employees?$format=application/json;odata.metadata=full"; + + string content = @"{ + 'Name':'SqlUD', + 'Friends':[{ 'Id':1001, 'Name' : 'Friend 1001', 'Age': 31},{ 'Id':1002, 'Name' : 'Friend 1002', 'Age': 32},{ 'Id':1003, 'Name' : 'Friend 1003', 'Age': 33}] + }"; + + var requestForPatch = new HttpRequestMessage(new HttpMethod("POST"), requestUri); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPatch.Content = stringContent; + + string friendsNavigationLink = "Friends@odata.navigationLink"; + string newFriendsNavigationLink = "NewFriends@odata.navigationLink"; + string untypedFriendsNavigationLink = "UnTypedFriends@odata.navigationLink"; + + string expected = "Friends\":[{\"@odata.type\":\"#Microsoft.Test.E2E.AspNet.OData.BulkOperation.Friend\""; + + //Act & Assert + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPatch)) + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var json = response.Content.ReadAsStringAsync().Result; + Assert.Contains("SqlUD", json); + Assert.Contains(expected, json); + Assert.Contains(friendsNavigationLink, json); + Assert.Contains(newFriendsNavigationLink, json); + Assert.Contains(untypedFriendsNavigationLink, json); + } + } + + [Fact] + public async Task PostEmployee_WithFullMetadata() + { + //Arrange + string requestUri = this.BaseAddress + "/convention/Employees?$format=application/json;odata.metadata=full"; + + var content = @"{ + 'Name':'SqlUD' + }"; + + var requestForPatch = new HttpRequestMessage(new HttpMethod("POST"), requestUri); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPatch.Content = stringContent; + + string friendsNavigationLink = "Friends@odata.navigationLink"; + string newFriendsNavigationLink = "NewFriends@odata.navigationLink"; + string untypedFriendsNavigationLink = "UnTypedFriends@odata.navigationLink"; + + //Act & Assert + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPatch)) + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var json = response.Content.ReadAsStringAsync().Result; + Assert.Contains("SqlUD", json); + Assert.Contains(friendsNavigationLink, json); + Assert.Contains(newFriendsNavigationLink, json); + Assert.Contains(untypedFriendsNavigationLink, json); + } + } + + [Fact] + public async Task GetEmployee_WithFullMetadata() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Employees(1)?$format=application/json;odata.metadata=full"; + + var requestForPatch = new HttpRequestMessage(new HttpMethod("GET"), requestUri); + + string friendsNavigationLink = "Friends@odata.navigationLink"; + string newFriendsNavigationLink = "NewFriends@odata.navigationLink"; + string untypedFriendsNavigationLink = "UnTypedFriends@odata.navigationLink"; + + string notexpected = "Friends\":[{\"@odata.type\":\"#Microsoft.Test.E2E.AspNet.OData.BulkOperation.Friend\""; + + //Act & Assert + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPatch)) + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var json = response.Content.ReadAsStringAsync().Result; + Assert.DoesNotContain(notexpected, json); + Assert.Contains(friendsNavigationLink, json); + Assert.Contains(newFriendsNavigationLink, json); + Assert.Contains(untypedFriendsNavigationLink, json); + } + } + + #endregion + } +} 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/E2ETest/Microsoft.Test.E2E.AspNet.OData/QueryComposition/SelectExpandTests.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/QueryComposition/SelectExpandTests.cs index 951a34f038..68a451adc1 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/QueryComposition/SelectExpandTests.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/QueryComposition/SelectExpandTests.cs @@ -293,11 +293,13 @@ public async Task QueryForAnEntryAnIncludeTheRelatedEntriesForASetOfNavigationPr Assert.Equal((int)customer["Id"], orders.Count); foreach (JObject order in (IEnumerable)orders) { - Assert.Equal(3, order.Properties().Count()); + Assert.Equal(4, order.Properties().Count()); } + JArray bonuses = customer["Bonuses"] as JArray; Assert.Equal((int)customer["Id"], bonuses.Count); + foreach (JObject bonus in (IEnumerable)bonuses) { Assert.Equal(2, bonus.Properties().Count()); 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..4d0edc85f8 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 @@ -3575,9 +3575,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..a2cd42cfd7 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 @@ -3788,9 +3788,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..a786a28b3d 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 @@ -3987,9 +3987,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)