diff --git a/samples/AspNetODataSample.Web/AspNetODataSample.Web.csproj b/samples/AspNetODataSample.Web/AspNetODataSample.Web.csproj index d602164144..d21a9e28eb 100644 --- a/samples/AspNetODataSample.Web/AspNetODataSample.Web.csproj +++ b/samples/AspNetODataSample.Web/AspNetODataSample.Web.csproj @@ -98,7 +98,6 @@ ..\..\sln\packages\Microsoft.Spatial.7.12.2\lib\net45\Microsoft.Spatial.dll - True ..\..\sln\packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll diff --git a/src/Microsoft.AspNet.OData.Shared/Common/Error.cs b/src/Microsoft.AspNet.OData.Shared/Common/Error.cs index 31c2285e9f..8d6e2f6aa1 100644 --- a/src/Microsoft.AspNet.OData.Shared/Common/Error.cs +++ b/src/Microsoft.AspNet.OData.Shared/Common/Error.cs @@ -103,6 +103,7 @@ internal static ArgumentNullException PropertyNull() /// Creates an with a default message. /// /// The logged . + [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly")] internal static ArgumentException PropertyNullOrWhiteSpace() { return new ArgumentException(CommonWebApiResources.PropertyNullOrWhiteSpace, "value"); diff --git a/src/Microsoft.AspNet.OData.Shared/Common/ODataPathHelper.cs b/src/Microsoft.AspNet.OData.Shared/Common/ODataPathHelper.cs new file mode 100644 index 0000000000..1e4c27b23a --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/Common/ODataPathHelper.cs @@ -0,0 +1,70 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System.Collections.Generic; +using System.Linq; +using Microsoft.OData.UriParser; + +namespace Microsoft.AspNet.OData.Common +{ + /// + /// Helper methods for . + /// + internal static class ODataPathHelper + { + /// + /// Get the keys from a . + /// + /// The to extract the keys. + /// Dictionary of keys. + public static Dictionary KeySegmentAsDictionary(KeySegment keySegment) + { + if (keySegment == null) + { + throw Error.ArgumentNull(nameof(keySegment)); + } + + return keySegment.Keys.ToDictionary(d => d.Key, d => d.Value); + } + + /// + /// Get the position of the next in a list of . + /// + /// List of . + /// Current position in the list of . + /// Position of the next if it exists, or -1 otherwise. + public static int GetNextKeySegmentPosition(IReadOnlyList pathSegments, int currentPosition) + { + if (pathSegments == null) + { + throw Error.ArgumentNull(nameof(pathSegments)); + } + + if (currentPosition < 0 || currentPosition >= pathSegments.Count) + { + return -1; + } + + if (pathSegments[currentPosition] is KeySegment) + { + currentPosition++; + } + + for (int i = currentPosition; i < pathSegments.Count; i++) + { + ODataPathSegment currentSegment = pathSegments[i]; + + if (currentSegment is KeySegment) + { + return i; + } + } + + return -1; + } + } +} diff --git a/src/Microsoft.AspNet.OData.Shared/Common/SRResources.Designer.cs b/src/Microsoft.AspNet.OData.Shared/Common/SRResources.Designer.cs index de6a9e0d09..5d350ccf7c 100644 --- a/src/Microsoft.AspNet.OData.Shared/Common/SRResources.Designer.cs +++ b/src/Microsoft.AspNet.OData.Shared/Common/SRResources.Designer.cs @@ -498,6 +498,17 @@ internal static string CantFindEdmType } } + /// + /// Looks up a localized string similar to Cannot use Changed Object of type '{0}' on an entity of type '{1}'.. + /// + internal static string ChangedObjectTypeMismatch + { + get + { + return ResourceManager.GetString("ChangedObjectTypeMismatch", resourceCulture); + } + } + /// /// Looks up a localized string similar to The given model does not contain the type '{0}'.. /// @@ -564,6 +575,17 @@ internal static string CollectionShouldHaveClearMethod } } + /// + /// Looks up a localized string similar to ContentID. + /// + internal static string ContentID + { + get + { + return ResourceManager.GetString("ContentID", resourceCulture); + } + } + /// /// Looks up a localized string similar to Type {0} already added as derived type constraint. /// @@ -597,6 +619,17 @@ internal static string CreateODataValueNotSupported } } + /// + /// Looks up a localized string similar to the error DataModificationException + /// + internal static string DataModificationException + { + get + { + return ResourceManager.GetString("DataModificationException", resourceCulture); + } + } + /// /// Looks up a localized string similar to The actual entity type '{0}' is not assignable to the expected type '{1}'.. /// @@ -608,6 +641,17 @@ internal static string DeltaEntityTypeNotAssignable } } + /// + /// Looks up a localized string similar to The actual error DeltaLinkNotSupported + /// + internal static string DeltaLinkNotSupported + { + get + { + return ResourceManager.GetString("DeltaLinkNotSupported", resourceCulture); + } + } + /// /// Looks up a localized string similar to Cannot find nested resource name '{0}' in parent resource type '{1}'. /// @@ -2721,6 +2765,28 @@ internal static string RequestUriTooShortForODataPath } } + /// + /// Looks up a localized string similar to Cannot find the resource type '{0}' in the model.. + /// + internal static string ResourcesShouldbePresent + { + get + { + return ResourceManager.GetString("ResourcesShouldbePresent", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cannot find the resource wrapper type '{0}' in the model.. + /// + internal static string ResourceSetWrapperSupported + { + get + { + return ResourceManager.GetString("ResourceSetWrapperSupported", resourceCulture); + } + } + /// /// Looks up a localized string similar to Cannot find the resource type '{0}' in the model.. /// diff --git a/src/Microsoft.AspNet.OData.Shared/Common/SRResources.resx b/src/Microsoft.AspNet.OData.Shared/Common/SRResources.resx index 0d7be880c7..f94864ec2e 100644 --- a/src/Microsoft.AspNet.OData.Shared/Common/SRResources.resx +++ b/src/Microsoft.AspNet.OData.Shared/Common/SRResources.resx @@ -988,4 +988,22 @@ A navigation property expand path should have navigation property in the path. + + ResourceSetWrapper should have ResourceWrappers in it + + + Can only add ResourceWrapper to ResourceSetWrapper + + + Cannot use Changed Object of type '{0}' on an entity of type '{1}'. + + + Core.DataModificationException + + + Core.ContentID + + + DeltaLinks are not supported + \ No newline at end of file diff --git a/src/Microsoft.AspNet.OData.Shared/DataModificationExceptionType.cs b/src/Microsoft.AspNet.OData.Shared/DataModificationExceptionType.cs new file mode 100644 index 0000000000..9c95d769bb --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/DataModificationExceptionType.cs @@ -0,0 +1,119 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System; +using Microsoft.AspNet.OData.Builder; + +namespace Org.OData.Core.V1 +{ + /// + /// Represents a Message Type + /// + public class MessageType + { + /// + /// Code of message + /// + public string Code { get; set; } + + /// + /// Actual message + /// + public string Message { get; set; } + + /// + /// Severity of message + /// + public string Severity { get; set; } + + /// + /// Target of message + /// + public string Target { get; set; } + + /// + /// Details of message + /// + public string Details { get; set; } + } + + /// + /// Represents an Exception Type + /// + public abstract class ExceptionType + { + /// + /// Represents a MessageType + /// + public MessageType MessageType { get; set; } + } + + /// + /// Represents an Exception for Data modification Operation. + /// + public class DataModificationExceptionType : ExceptionType + { + /// + /// Initializes a new instance of the class. + /// + public DataModificationExceptionType(DataModificationOperationKind failedOperation) + { + this.FailedOperation = failedOperation; + } + + /// + /// Represents kind of type of operation + /// + public DataModificationOperationKind FailedOperation { get; } + + /// + /// Represents response code + /// + public Int16 ResponseCode { get; set; } + } + + /// + /// Enumerates the DataModificationOperation for the operation kind + /// + public enum DataModificationOperationKind + { + /// + /// Insert new Instance + /// + Insert, + + /// + /// Update existing Instance + /// + Update, + + /// + /// Insert new instance or update it if it already exists + /// + Upsert, + + /// + /// Delete existing instance + /// + Delete, + + /// + /// Invoke action or function + /// + Invoke, + + /// + /// Add link between entities + /// + Link, + + /// + /// Remove link between entities + /// + Unlink + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.OData.Shared/DefaultEdmPatchMethodHandler.cs b/src/Microsoft.AspNet.OData.Shared/DefaultEdmPatchMethodHandler.cs new file mode 100644 index 0000000000..9b13f3ca21 --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/DefaultEdmPatchMethodHandler.cs @@ -0,0 +1,165 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Linq; +using Microsoft.OData.Edm; + +namespace Microsoft.AspNet.OData +{ + /// + /// This is the default Patch Handler for non CLR type. This calss has default Get, Create and Update + /// and will do these actions. This will be used when the original collection to be Patched is provided. + /// + internal class DefaultEdmODataAPIHandler : EdmODataAPIHandler + { + IEdmEntityType entityType; + ICollection originalList; + + public DefaultEdmODataAPIHandler(ICollection originalList, IEdmEntityType entityType) + { + Contract.Assert(entityType != null); + + this.entityType = entityType; + this.originalList = originalList?? new List(); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out IEdmStructuredObject originalObject, out string errorMessage) + { + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; + errorMessage = string.Empty; + originalObject = null; + + Contract.Assert(keyValues != null); + + try + { + originalObject = GetFilteredItem(keyValues); + + if (originalObject == null) + { + status = ODataAPIResponseStatus.NotFound; + } + } + catch (Exception ex) + { + status = ODataAPIResponseStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + public override ODataAPIResponseStatus TryCreate(IEdmChangedObject changedObject, out IEdmStructuredObject createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + createdObject = new EdmEntityObject(entityType); + originalList.Add(createdObject); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + EdmStructuredObject originalObject = GetFilteredItem(keyValues); + + if (originalObject != null) + { + originalList.Remove(originalObject); + } + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override EdmODataAPIHandler GetNestedHandler(IEdmStructuredObject parent, string navigationPropertyName) + { + IEdmNavigationProperty navProperty = entityType.NavigationProperties().FirstOrDefault(navProp => navProp.Name == navigationPropertyName); + + if(navProperty == null) + { + return null; + } + + IEdmEntityType nestedEntityType = navProperty.ToEntityType(); + + object obj; + if(parent.TryGetPropertyValue(navigationPropertyName, out obj)) + { + ICollection nestedList = obj as ICollection; + + return new DefaultEdmODataAPIHandler(nestedList, nestedEntityType); + } + + return null; + } + + + private EdmStructuredObject GetFilteredItem(IDictionary keyValues) + { + //This logic is for filtering the object based on the set of keys, + //There will only be very few key elements usually, mostly 1, so performance wont be impacted. + + if(originalList == null) + { + return null; + } + + foreach (EdmStructuredObject item in originalList) + { + bool isMatch = true; + + foreach (KeyValuePair keyValue in keyValues) + { + object value; + if (item.TryGetPropertyValue(keyValue.Key, out value)) + { + if (!Equals(value, keyValue.Value)) + { + // Not a match, so try the next one + isMatch = false; + break; + } + } + } + + if (isMatch) + { + return item; + } + } + + return null; + } + } +} diff --git a/src/Microsoft.AspNet.OData.Shared/DefaultODataAPIHandler.cs b/src/Microsoft.AspNet.OData.Shared/DefaultODataAPIHandler.cs new file mode 100644 index 0000000000..34cf87bbb8 --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/DefaultODataAPIHandler.cs @@ -0,0 +1,157 @@ +//----------------------------------------------------------------------------- +// +// 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.Reflection; + +namespace Microsoft.AspNet.OData +{ + /// + /// This is the default ODataAPIHandler for CLR type. This calss has default Get, Create and Update + /// and will do these actions. This will be used when the original collection to be Patched is provided. + /// + /// + internal class DefaultODataAPIHandler : ODataAPIHandler where TStructuralType :class + { + Type _clrType; + ICollection originalList; + + public DefaultODataAPIHandler(ICollection originalList) + { + this._clrType = typeof(TStructuralType); + this.originalList = originalList?? new List(); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out TStructuralType originalObject, out string errorMessage) + { + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; + errorMessage = string.Empty; + originalObject = default(TStructuralType); + + try + { + originalObject = GetFilteredItem(keyValues); + + if (originalObject == null) + { + status = ODataAPIResponseStatus.NotFound; + } + } + catch (Exception ex) + { + status = ODataAPIResponseStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out TStructuralType createdObject, out string errorMessage) + { + createdObject = default(TStructuralType); + errorMessage = string.Empty; + + try + { + if(originalList != null) + { + originalList = new List(); + } + + createdObject = Activator.CreateInstance(_clrType) as TStructuralType; + originalList.Add(createdObject); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + TStructuralType originalObject = GetFilteredItem(keyValues); + originalList.Remove(originalObject); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override IODataAPIHandler GetNestedHandler(TStructuralType parent, string navigationPropertyName) + { + foreach (PropertyInfo property in _clrType.GetProperties()) + { + if (property.Name == navigationPropertyName) + { + Type type = typeof(DefaultODataAPIHandler<>).MakeGenericType(property.PropertyType.GetGenericArguments()[0]); + + return Activator.CreateInstance(type, property.GetValue(parent)) as IODataAPIHandler; + } + } + + return null; + } + + + private TStructuralType GetFilteredItem(IDictionary keyValues) + { + //This logic is for filtering the object based on the set of keys, + //There will only be very few key elements usually, mostly 1, so performance wont be impacted. + + if(originalList == null || originalList.Count == 0) + { + return default(TStructuralType); + } + + Dictionary propertyInfos = new Dictionary(); + + foreach (string key in keyValues.Keys) + { + propertyInfos.Add(key, _clrType.GetProperty(key)); + } + + foreach (TStructuralType item in originalList) + { + bool isMatch = true; + + foreach (KeyValuePair keyValue in keyValues) + { + if (!Equals(propertyInfos[keyValue.Key].GetValue(item), keyValue.Value)) + { + // Not a match, so try the next one + isMatch = false; + break; + } + } + + if (isMatch) + { + return item; + } + } + + return default(TStructuralType); + } + } +} diff --git a/src/Microsoft.AspNet.OData.Shared/DeltaDeletedEntityObjectOfT.cs b/src/Microsoft.AspNet.OData.Shared/DeltaDeletedEntityObjectOfT.cs new file mode 100644 index 0000000000..fefa09cca0 --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/DeltaDeletedEntityObjectOfT.cs @@ -0,0 +1,129 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Reflection; +using System.Threading; +using Microsoft.AspNet.OData.Builder; +using Microsoft.AspNet.OData.Common; +using Microsoft.OData; +using Microsoft.OData.Edm; + +namespace Microsoft.AspNet.OData +{ + /// + /// Represents an with a backing CLR . + /// Used to hold the Deleted Entry object in the Delta Feed Payload. + /// + [NonValidatingParameterBinding] + public class DeltaDeletedEntityObject : Delta, IDeltaDeletedEntityObject where TStructuralType : class + { + /// + /// Initializes a new instance of . + /// + public DeltaDeletedEntityObject() + : this(typeof(TStructuralType)) + { + } + + /// + /// Initializes a new instance of . + /// + /// The derived entity type or complex type for which the changes would be tracked. + /// should be assignable to instances of . + /// + public DeltaDeletedEntityObject(Type structuralType) + : this(structuralType, dynamicDictionaryPropertyInfo: null, instanceAnnotationsPropertyInfo: null) + { + } + + /// + /// Initializes a new instance of . + /// + /// The derived entity type or complex type for which the changes would be tracked. + /// should be assignable to instances of . + /// + /// Properties to update + public DeltaDeletedEntityObject(Type structuralType, IEnumerable updatableProperties) + : this(structuralType, updatableProperties, dynamicDictionaryPropertyInfo: null, instanceAnnotationsPropertyInfo: null) + { + + } + + /// + /// Initializes a new instance of . + /// + /// The derived entity type or complex type for which the changes would be tracked. + /// should be assignable to instances of . + /// + /// The property info that is used as container for Instance Annotations + public DeltaDeletedEntityObject(Type structuralType, PropertyInfo instanceAnnotationsPropertyInfo) + : this(structuralType, dynamicDictionaryPropertyInfo: null, instanceAnnotationsPropertyInfo) + { + + } + + /// + /// Initializes a new instance of . + /// + /// The derived entity type or complex type for which the changes would be tracked. + /// should be assignable to instances of . + /// + /// The property info that is used as dictionary of dynamic + /// properties. null means this entity type is not open. + /// The property info that is used as container for Instance Annotations + public DeltaDeletedEntityObject(Type structuralType, PropertyInfo dynamicDictionaryPropertyInfo, PropertyInfo instanceAnnotationsPropertyInfo) + : this(structuralType, updatableProperties: null , dynamicDictionaryPropertyInfo, instanceAnnotationsPropertyInfo) + { + + } + + /// + /// Initializes a new instance of . + /// + /// The derived entity type or complex type for which the changes would be tracked. + /// should be assignable to instances of . + /// + /// Properties that can be updated + /// The property info that is used as dictionary of dynamic + /// properties. null means this entity type is not open. + /// The property info that is used as container for Instance Annotations + public DeltaDeletedEntityObject(Type structuralType, IEnumerable updatableProperties, PropertyInfo dynamicDictionaryPropertyInfo, PropertyInfo instanceAnnotationsPropertyInfo) + : this(structuralType, updatableProperties, dynamicDictionaryPropertyInfo, false, instanceAnnotationsPropertyInfo) + { + + } + + /// + /// Initializes a new instance of . + /// + /// The derived entity type or complex type for which the changes would be tracked. + /// should be assignable to instances of . + /// + /// Properties that can be updated + /// The property info that is used as dictionary of dynamic + /// properties. null means this entity type is not open. + /// To determine if the entity is a complex type + /// The property info that is used as container for Instance Annotations + public DeltaDeletedEntityObject(Type structuralType, IEnumerable updatableProperties, PropertyInfo dynamicDictionaryPropertyInfo, bool isComplexType, PropertyInfo instanceAnnotationsPropertyInfo) + : base(structuralType, updatableProperties, dynamicDictionaryPropertyInfo, isComplexType, instanceAnnotationsPropertyInfo) + { + DeltaKind = EdmDeltaEntityKind.DeletedEntry; + } + + /// + public Uri Id { get; set; } + + /// + public DeltaDeletedEntryReason? Reason { get; set; } + + /// + public IEdmNavigationSource NavigationSource { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.OData.Shared/DeltaOfTStructuralType.cs b/src/Microsoft.AspNet.OData.Shared/DeltaOfTStructuralType.cs index 4dd0590eb2..618fbc7ef5 100644 --- a/src/Microsoft.AspNet.OData.Shared/DeltaOfTStructuralType.cs +++ b/src/Microsoft.AspNet.OData.Shared/DeltaOfTStructuralType.cs @@ -16,8 +16,11 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Serialization; +using Microsoft.AspNet.OData.Builder; using Microsoft.AspNet.OData.Common; +using Microsoft.AspNet.OData.Extensions; using Microsoft.AspNet.OData.Formatter; +using Microsoft.OData.UriParser; namespace Microsoft.AspNet.OData { @@ -26,7 +29,7 @@ namespace Microsoft.AspNet.OData /// /// TStructuralType is the type of the instance this delta tracks changes for. [NonValidatingParameterBinding] - public class Delta : TypedDelta, IDelta where TStructuralType : class + public class Delta : TypedDelta, IDelta, IDeltaSetItem where TStructuralType : class { // cache property accessors for this type and all its derived types. private static readonly ConcurrentDictionary>> _propertyCache @@ -44,6 +47,7 @@ private static readonly ConcurrentDictionary _changedDynamicProperties; private IDictionary _dynamicDictionaryCache; @@ -79,6 +83,22 @@ public Delta(Type structuralType, IEnumerable updatableProperties) { } + /// + /// Initializes a new instance of . + /// + /// The derived entity type or complex type for which the changes would be tracked. + /// should be assignable to instances of . + /// + /// The set of properties that can be updated or reset. Unknown property + /// names, including those of dynamic properties, are ignored. + /// The property info that is used as dictionary of dynamic + /// properties. null means this entity type is not open. + public Delta(Type structuralType, IEnumerable updatableProperties, PropertyInfo dynamicDictionaryPropertyInfo) + : this(structuralType, updatableProperties: updatableProperties, dynamicDictionaryPropertyInfo, isComplexType: false) + { + + } + /// /// Initializes a new instance of . /// @@ -89,10 +109,12 @@ public Delta(Type structuralType, IEnumerable updatableProperties) /// names, including those of dynamic properties, are ignored. /// The property info that is used as dictionary of dynamic /// properties. null means this entity type is not open. + /// Boolean value to determine if its a complex type public Delta(Type structuralType, IEnumerable updatableProperties, - PropertyInfo dynamicDictionaryPropertyInfo) - : this(structuralType, updatableProperties: updatableProperties, dynamicDictionaryPropertyInfo, false) + PropertyInfo dynamicDictionaryPropertyInfo, bool isComplexType) + : this(structuralType, updatableProperties: updatableProperties, dynamicDictionaryPropertyInfo, isComplexType, instanceAnnotationsPropertyInfo: null) { + } /// @@ -106,20 +128,31 @@ public Delta(Type structuralType, IEnumerable updatableProperties, /// The property info that is used as dictionary of dynamic /// properties. null means this entity type is not open. /// Boolean value to determine if its a complex type + /// The property info that is used as container for Instance Annotations public Delta(Type structuralType, IEnumerable updatableProperties, - PropertyInfo dynamicDictionaryPropertyInfo, bool isComplexType) + PropertyInfo dynamicDictionaryPropertyInfo, bool isComplexType, PropertyInfo instanceAnnotationsPropertyInfo) { _dynamicDictionaryPropertyinfo = dynamicDictionaryPropertyInfo; Reset(structuralType); InitializeProperties(updatableProperties); + TransientInstanceAnnotationContainer = new ODataInstanceAnnotationContainer(); + _instanceAnnotationsPropertyInfo = instanceAnnotationsPropertyInfo; + DeltaKind = EdmDeltaEntityKind.Entry; IsComplexType = isComplexType; } /// - public override Type StructuredType => _structuredType; + public override Type StructuredType + => _structuredType; + + internal IDictionary DeltaNestedResources + { + get { return _deltaNestedResources; } + } /// - public override Type ExpectedClrType => typeof(TStructuralType); + public override Type ExpectedClrType + => typeof(TStructuralType); /// /// The list of property names that can be updated. @@ -133,6 +166,23 @@ public Delta(Type structuralType, IEnumerable updatableProperties, /// public bool IsComplexType { get; private set; } + /// + /// Gets the enum type of . + /// + public EdmDeltaEntityKind DeltaKind { get; protected set; } + + /// + public IODataInstanceAnnotationContainer TransientInstanceAnnotationContainer { get; set; } + + /// + public IODataIdContainer ODataIdContainer { get; set; } + + /// + public ODataPath ODataPath { get; set; } + + /// + internal PropertyInfo InstanceAnnotationsPropertyInfo { get { return _instanceAnnotationsPropertyInfo; } } + /// public override void Clear() { @@ -147,6 +197,19 @@ public override bool TrySetPropertyValue(string name, object value) throw Error.ArgumentNull(nameof(name)); } + if (_instanceAnnotationsPropertyInfo != null && name == _instanceAnnotationsPropertyInfo.Name) + { + IODataInstanceAnnotationContainer annotationValue = value as IODataInstanceAnnotationContainer; + if (value != null && annotationValue == null) + { + return false; + } + + _instanceAnnotationsPropertyInfo.SetValue(_instance, annotationValue); + + return true; + } + if (_dynamicDictionaryPropertyinfo != null) { // Dynamic property can have the same name as the dynamic property dictionary. @@ -165,7 +228,7 @@ public override bool TrySetPropertyValue(string name, object value) } } - if (value is IDelta) + if (value is IDelta || value is IDeltaSet) { return TrySetNestedResourceInternal(name, value); } @@ -183,6 +246,19 @@ public override bool TryGetPropertyValue(string name, out object value) throw Error.ArgumentNull(nameof(name)); } + if (_instanceAnnotationsPropertyInfo != null && name == _instanceAnnotationsPropertyInfo.Name) + { + object propertyValue = _instanceAnnotationsPropertyInfo.GetValue(_instance); + if (propertyValue != null) + { + value = (IODataInstanceAnnotationContainer)propertyValue; + return true; + } + + value = null; + return false; + } + if (_dynamicDictionaryPropertyinfo != null) { if (_dynamicDictionaryCache == null) @@ -203,6 +279,14 @@ public override bool TryGetPropertyValue(string name, out object value) object deltaNestedResource = _deltaNestedResources[name]; Contract.Assert(deltaNestedResource != null, "deltaNestedResource != null"); + + //If DeltaSet collection, we are handling delta collections so the value will be that itself and no need to get instance value + if (deltaNestedResource is IDeltaSet) + { + value = deltaNestedResource; + return true; + } + Contract.Assert(IsDeltaOfT(deltaNestedResource.GetType())); // Get the Delta<{NestedResourceType}>._instance using Reflection. @@ -344,11 +428,18 @@ public override IEnumerable GetUnchangedPropertyNames() /// to the entity recursively. /// /// The entity to be updated. - public TStructuralType CopyChangedValues(TStructuralType original) + public void CopyChangedValues(TStructuralType original) + { + CopyChangedValues(original, null, null); + } + + [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")] + [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily")] + internal void CopyChangedValues(TStructuralType original, ODataAPIHandler apiHandler = null, ODataAPIHandlerFactory apiHandlerFactory = null) { if (original == null) { - throw Error.ArgumentNull(nameof(original)); + throw Error.ArgumentNull("original"); } // Delta parameter type cannot be derived type of original @@ -358,6 +449,19 @@ public TStructuralType CopyChangedValues(TStructuralType original) throw Error.Argument(nameof(original), SRResources.DeltaTypeMismatch, _structuredType, original.GetType()); } + //To apply ODataId based handler to apply properties on original. + if (apiHandlerFactory != null) + { + IODataAPIHandler refapiHandler = apiHandlerFactory.GetHandler(ODataPath); + + if (refapiHandler != null && ODataPath.Any() && apiHandler.ToString() != refapiHandler.ToString()) + { + ODataAPIHandler refapiHandlerOfT = refapiHandler as ODataAPIHandler; + + ApplyPropertiesBasedOnOdataId(original, refapiHandlerOfT, ODataPath.GetKeys()); + } + } + RuntimeHelpers.EnsureSufficientExecutionStack(); // For regular non-structural properties at current level. @@ -376,97 +480,53 @@ public TStructuralType CopyChangedValues(TStructuralType original) // Patch for each nested resource changed under this TStructuralType. dynamic deltaNestedResource = _deltaNestedResources[nestedResourceName]; dynamic originalNestedResource = null; - if (!TryGetPropertyRef(original, nestedResourceName, out originalNestedResource)) - { - throw Error.Argument(nestedResourceName, SRResources.DeltaNestedResourceNameNotFound, - nestedResourceName, original.GetType()); - } - if (originalNestedResource == null) + if (deltaNestedResource is IDeltaSet) { - // When patching original target of null value, directly set nested resource. - dynamic deltaObject = _deltaNestedResources[nestedResourceName]; - dynamic instance = deltaObject.GetInstance(); - - // Recursively patch up the instance with the nested resources. - deltaObject.CopyChangedValues(instance); + IODataAPIHandler apiHandlerNested = apiHandler.GetNestedHandler(original, nestedResourceName); - _allProperties[nestedResourceName].SetValue(original, instance); + if (apiHandlerNested != null) + { + deltaNestedResource.CopyChangedValues(apiHandlerNested, apiHandlerFactory); + } } else { - // Recursively patch the subtree. - Contract.Assert(TypedDelta.IsDeltaOfT(((object)deltaNestedResource).GetType()), nestedResourceName + "'s corresponding value should be Delta type but is not."); - - Type newType = deltaNestedResource.StructuredType; - Type originalType = originalNestedResource.GetType(); - - if (deltaNestedResource.IsComplexType && newType != originalType) + if (!TryGetPropertyRef(original, nestedResourceName, out originalNestedResource)) { - originalNestedResource = ReAssignComplexDerivedType(originalNestedResource, newType, originalType, deltaNestedResource.ExpectedClrType); - _structuredType.GetProperty(nestedResourceName).SetValue(original, (object)originalNestedResource); + throw Error.Argument(nestedResourceName, SRResources.DeltaNestedResourceNameNotFound, + nestedResourceName, original.GetType()); } - deltaNestedResource.CopyChangedValues(originalNestedResource); - } - } - - return original; - } - - private static dynamic ReAssignComplexDerivedType(dynamic originalValue, Type newType, Type originalType, Type declaredType) - { - //As per OASIS discussion, changing a complex type from 1 derived type to another is allowed if both derived type have a common ancestor and the property - //is declared in terms of a common ancestor. The logic below checks for a common ancestor. Create a new object of the derived type in delta request. - //And copy the common properties. - - if(newType == originalType) - { - return originalValue; - } - - Type newBaseType = newType; - HashSet newBaseTypes = new HashSet(); - - //Iterate till you find the declaring base type and add all that to hashset - while (newBaseType != null && newBaseType != declaredType) - { - newBaseTypes.Add(newBaseType); - newBaseType = newBaseType.BaseType; - } - - newBaseTypes.Add(declaredType); + if (originalNestedResource == null) + { + // When patching original target of null value, directly set nested resource. + dynamic deltaObject = _deltaNestedResources[nestedResourceName]; + dynamic instance = deltaObject.GetInstance(); - //Here original type is the type for original (T) resource. - //We will keep going to base types and finally will get the Common Basetype for the derived complex types in to the originalType variable. + // Recursively patch up the instance with the nested resources. + deltaObject.CopyChangedValues(instance); - //The new Original type, means the new complex type (T) which will replace the current complex type. - dynamic newOriginalNestedResource = originalValue; + _allProperties[nestedResourceName].SetValue(original, instance); + } + else + { + // Recursively patch the subtree. + Contract.Assert(TypedDelta.IsDeltaOfT(((object)deltaNestedResource).GetType()), nestedResourceName + "'s corresponding value should be Delta type but is not."); - while (originalType != null) - { - if (newBaseTypes.Contains(originalType)) - { - //Now originalType = common base type of the derived complex types. - //OriginalNested Resource = T(of current Complex type). We are creating newOriginalNestedResource (T - new complex type). - newOriginalNestedResource = Activator.CreateInstance(newType); + Type newType = deltaNestedResource.StructuredType; + Type originalType = originalNestedResource.GetType(); - //Here we get all the properties of common base type and get value from original complex type(T) and - //copy it to the new complex type newOriginalNestedResource(came as a part of Delta) + if (deltaNestedResource.IsComplexType && newType != originalType) + { + originalNestedResource = ReAssignComplexDerivedType(original, nestedResourceName, originalNestedResource, newType, originalType, deltaNestedResource.ExpectedClrType); + } - foreach (PropertyInfo property in originalType.GetProperties()) - { - object value = property.GetValue(originalValue); - property.SetValue(newOriginalNestedResource, value); + deltaNestedResource.CopyChangedValues(originalNestedResource); } - - break; } - originalType = originalType.BaseType; } - - return newOriginalNestedResource; } /// @@ -495,21 +555,52 @@ public void CopyUnchangedValues(TStructuralType original) CopyUnchangedDynamicValues(original); } + /// + /// Overwrites the entity with the changes tracked by this Delta. + /// The semantics of this operation are equivalent to a HTTP PATCH operation, hence the name. + /// + /// The entity to be updated. + public void Patch(TStructuralType original) + { + CopyChangedValues(original); + } /// /// Overwrites the entity with the changes tracked by this Delta. /// The semantics of this operation are equivalent to a HTTP PATCH operation, hence the name. /// /// The entity to be updated. - /// The original value after Patching - public TStructuralType Patch(TStructuralType original) + /// API Handler for the entity. + /// API Handler Factory + internal void Patch(TStructuralType original, IODataAPIHandler apiHandler, ODataAPIHandlerFactory apiHandlerFactory) + { + Debug.Assert(apiHandler != null); + + CopyChangedValues(original, apiHandler as ODataAPIHandler, apiHandlerFactory); + } + + /// + /// This is basically Patch on ODataId. This applies ODataId parsed Navigation paths, get the value identified by that and copy it on original object + /// + private void ApplyPropertiesBasedOnOdataId(TStructuralType original, ODataAPIHandler refapiHandlerOfT, Dictionary keyProperties) { - if (IsComplexType) + Debug.Assert(refapiHandlerOfT != null); + + TStructuralType referencedObj; + string error; + + //todo: this logic feels brittle to me + //Checking to get the referenced entity, get the properties and apply it on original object + if (refapiHandlerOfT.TryGet(keyProperties, out referencedObj, out error) == ODataAPIResponseStatus.Success) { - original = ReAssignComplexDerivedType(original, _structuredType, original.GetType(), ExpectedClrType) as TStructuralType; - } + foreach (string property in _updatableProperties) + { + PropertyInfo propertyInfo = _structuredType.GetProperty(property); - return CopyChangedValues(original); + object value = propertyInfo.GetValue(referencedObj); + propertyInfo.SetValue(original, value); + } + } } /// @@ -523,6 +614,58 @@ public void Put(TStructuralType original) CopyUnchangedValues(original); } + private dynamic ReAssignComplexDerivedType(TStructuralType parent, string nestedPropertyName, dynamic originalValue, Type newType, Type originalType, Type declaredType) + { + //As per OASIS discussion, changing a complex type from 1 derived type to another is allowed if both derived type have a common ancestor and the property + //is declared in terms of a common ancestor. The logic below checks for a common ancestor. Create a new object of the derived type in delta request. + //And copy the common properties. + + Type newBaseType = newType; + HashSet newBaseTypes = new HashSet(); + + //Iterate till you find the declaring base type and add all that to hashset + while (newBaseType != null && newBaseType != declaredType) + { + newBaseTypes.Add(newBaseType); + newBaseType = newBaseType.BaseType; + } + + newBaseTypes.Add(declaredType); + + //Here original type is the type for original (T) resource. + //We will keep going to base types and finally will get the Common Basetype for the derived complex types in to the originalType variable. + + //The new Original type, means the new complex type (T) which will replace the current complex type. + dynamic newOriginalNestedResource = originalValue; + + while (originalType != null) + { + if (newBaseTypes.Contains(originalType)) + { + //Now originalType = common base type of the derived complex types. + //OriginalNested Resource = T(of current Complex type). We are creating newOriginalNestedResource (T - new complex type). + newOriginalNestedResource = Activator.CreateInstance(newType); + + //Here we get all the properties of common base type and get value from original complex type(T) and + //copy it to the new complex type newOriginalNestedResource(came as a part of Delta) + + foreach (PropertyInfo property in originalType.GetProperties()) + { + object value = property.GetValue(originalValue); + property.SetValue(newOriginalNestedResource, value); + } + + _structuredType.GetProperty(nestedPropertyName).SetValue(parent, (object)newOriginalNestedResource); + + break; + } + + originalType = originalType.BaseType; + } + + return newOriginalNestedResource; + } + private static void CopyDynamicPropertyDictionary(IDictionary source, IDictionary dest, PropertyInfo dynamicPropertyInfo, TStructuralType targetEntity) { @@ -820,11 +963,16 @@ private bool TrySetNestedResourceInternal(string name, object deltaNestedResourc return false; } - PropertyAccessor cacheHit = _allProperties[name]; - // Get the Delta<{NestedResourceType}>._instance using Reflection. - FieldInfo field = deltaNestedResource.GetType().GetField("_instance", BindingFlags.NonPublic | BindingFlags.Instance); - Contract.Assert(field != null, "field != null"); - cacheHit.SetValue(_instance, field.GetValue(deltaNestedResource)); + //If Edmchangedobject collection, we are handling delta collections so the instance value need not be set, + //as we consider the value as collection of Delta itself and not instance value of the field + if (!(deltaNestedResource is IDeltaSet)) + { + PropertyAccessor cacheHit = _allProperties[name]; + // Get the Delta<{NestedResourceType}>._instance using Reflection. + FieldInfo field = deltaNestedResource.GetType().GetField("_instance", BindingFlags.NonPublic | BindingFlags.Instance); + Contract.Assert(field != null, "field != null"); + cacheHit.SetValue(_instance, field.GetValue(deltaNestedResource)); + } // Add the nested resource in the hierarchy. // Note: We shouldn't add the structural properties to the _changedProperties, which @@ -834,4 +982,4 @@ private bool TrySetNestedResourceInternal(string name, object deltaNestedResourc return true; } } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.OData.Shared/DeltaSetOfT.cs b/src/Microsoft.AspNet.OData.Shared/DeltaSetOfT.cs new file mode 100644 index 0000000000..61c56a8b74 --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/DeltaSetOfT.cs @@ -0,0 +1,343 @@ +//----------------------------------------------------------------------------- +// +// 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.Collections.ObjectModel; +using System.Diagnostics; +using System.Diagnostics.Contracts; +using System.Linq; +using System.Reflection; +using Microsoft.AspNet.OData.Builder; +using Microsoft.AspNet.OData.Common; +using Microsoft.AspNet.OData.Extensions; +using Org.OData.Core.V1; + +namespace Microsoft.AspNet.OData +{ + /// + /// Represents an that is a collection of s. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")] + [NonValidatingParameterBinding] + public class DeltaSet : Collection, IDeltaSet where TStructuralType : class + { + private Type _clrType; + IList _keys; + + /// + /// Initializes a new instance of the class. + /// + /// List of key names for the type + public DeltaSet(IList keys) + { + _keys = keys; + _clrType = typeof(TStructuralType); + } + + + /// + protected override void InsertItem(int index, IDeltaSetItem item) + { + Delta deltaItem = item as Delta; + + //To ensure we dont insert null or a non related type to deltaset + if (deltaItem == null) + { + throw Error.Argument("item", SRResources.ChangedObjectTypeMismatch, item.GetType(), _clrType); + } + + base.InsertItem(index, item); + } + + + /// + /// Patch for DeltaSet, a collection for Delta + /// + /// Original collection of the Type which needs to be updated + /// /// DeltaSet response + public DeltaSet Patch(ICollection originalCollection) + { + ODataAPIHandler apiHandler = new DefaultODataAPIHandler(originalCollection); + + return CopyChangedValues(apiHandler); + } + + + /// + /// Patch for DeltaSet, a collection for Delta + /// + /// DeltaSet response + public DeltaSet Patch(ODataAPIHandler apiHandlerOfT, ODataAPIHandlerFactory apiHandlerFactory) + { + Debug.Assert(apiHandlerOfT != null); + + return CopyChangedValues(apiHandlerOfT, apiHandlerFactory); + } + + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + internal DeltaSet CopyChangedValues(IODataAPIHandler apiHandler, ODataAPIHandlerFactory apiHandlerFactory = null) + { + //Here we are getting the keys and using the keys to find the original object + //to patch from the list of collection + + ODataAPIHandler apiHandlerOfT = apiHandler as ODataAPIHandler; + + Debug.Assert(apiHandlerOfT != null); + + DeltaSet deltaSet = CreateDeltaSet(); + + foreach (Delta changedObj in Items) + { + DataModificationOperationKind operation = DataModificationOperationKind.Update; + + //Get filtered item based on keys + TStructuralType original = null; + string errorMessage = string.Empty; + string getErrorMessage = string.Empty; + + Dictionary keyValues = new Dictionary(); + + foreach (string key in _keys) + { + object value; + + if (changedObj.TryGetPropertyValue(key, out value)) + { + keyValues.Add(key, value); + } + } + + try + { + ODataAPIResponseStatus ODataAPIResponseStatus = apiHandlerOfT.TryGet(changedObj.ODataPath.GetKeys(), out original, out getErrorMessage); + + DeltaDeletedEntityObject deletedObj = changedObj as DeltaDeletedEntityObject; + + if (ODataAPIResponseStatus == ODataAPIResponseStatus.Failure || (deletedObj != null && ODataAPIResponseStatus == ODataAPIResponseStatus.NotFound)) + { + IDeltaSetItem deltaSetItem = changedObj; + + DataModificationExceptionType dataModificationExceptionType = new DataModificationExceptionType(operation); + dataModificationExceptionType.MessageType = new MessageType { Message = getErrorMessage }; + + deltaSetItem.TransientInstanceAnnotationContainer.AddResourceAnnotation(SRResources.DataModificationException, dataModificationExceptionType); + + deltaSet.Add(deltaSetItem); + + continue; + } + + if (deletedObj != null) + { + operation = DataModificationOperationKind.Delete; + + changedObj.CopyChangedValues(original, apiHandlerOfT, apiHandlerFactory); + + if (apiHandlerOfT.TryDelete(keyValues, out errorMessage) != ODataAPIResponseStatus.Success) + { + //Handle Failed Operation - Delete + + if (ODataAPIResponseStatus == ODataAPIResponseStatus.Success) + { + IDeltaSetItem changedObject = HandleFailedOperation(changedObj, operation, original, errorMessage); + deltaSet.Add(changedObject); + continue; + } + } + + deltaSet.Add(deletedObj); + } + else + { + if (ODataAPIResponseStatus == ODataAPIResponseStatus.NotFound) + { + operation = DataModificationOperationKind.Insert; + + if (apiHandlerOfT.TryCreate(keyValues, out original, out errorMessage) != ODataAPIResponseStatus.Success) + { + //Handle failed Opreataion - create + IDeltaSetItem changedObject = HandleFailedOperation(changedObj, operation, original, errorMessage); + deltaSet.Add(changedObject); + continue; + } + } + else if (ODataAPIResponseStatus == ODataAPIResponseStatus.Success) + { + operation = DataModificationOperationKind.Update; + } + else + { + //Handle failed operation + IDeltaSetItem changedObject = HandleFailedOperation(changedObj, operation, original, getErrorMessage); + deltaSet.Add(changedObject); + continue; + } + + //Patch for addition/update. This will call Delta for each item in the collection + // This will work in case we use delegates for using users method to create an object + changedObj.CopyChangedValues(original, apiHandlerOfT, apiHandlerFactory); + + deltaSet.Add(changedObj); + } + } + catch (Exception ex) + { + //For handling the failed operations. + IDeltaSetItem changedObject = HandleFailedOperation(changedObj, operation, original, ex.Message); + deltaSet.Add(changedObject); + } + } + + return deltaSet; + } + + private DeltaSet CreateDeltaSet() + { + Type type = typeof(DeltaSet<>).MakeGenericType(_clrType); + + return Activator.CreateInstance(type, _keys) as DeltaSet; + } + + private IDeltaSetItem HandleFailedOperation(Delta changedObj, DataModificationOperationKind operation, TStructuralType originalObj, string errorMessage) + { + IDeltaSetItem deltaSetItem = null; + DataModificationExceptionType dataModificationExceptionType = new DataModificationExceptionType(operation); + dataModificationExceptionType.MessageType = new MessageType { Message = errorMessage }; + + // This handles the Data Modification exception. This adds Core.DataModificationException annotation and also copy other instance annotations. + //The failed operation will be based on the protocol + switch (operation) + { + case DataModificationOperationKind.Update: + deltaSetItem = changedObj; + break; + case DataModificationOperationKind.Insert: + { + deltaSetItem = CreateDeletedEntityForFailedOperation(changedObj); + + break; + } + case DataModificationOperationKind.Delete: + { + deltaSetItem = CreateEntityObjectForFailedOperation(changedObj, originalObj); + break; + } + } + + + deltaSetItem.TransientInstanceAnnotationContainer = changedObj.TransientInstanceAnnotationContainer; + deltaSetItem.TransientInstanceAnnotationContainer.AddResourceAnnotation(SRResources.DataModificationException, dataModificationExceptionType); + + Contract.Assert(deltaSetItem != null); + + return deltaSetItem; + } + + private IDeltaSetItem CreateEntityObjectForFailedOperation(Delta changedObj, TStructuralType originalObj) + { + Type type = typeof(Delta<>).MakeGenericType(_clrType); + + Delta deltaObject = Activator.CreateInstance(type, _clrType, null, null,false, + changedObj.InstanceAnnotationsPropertyInfo) as Delta; + + SetProperties(originalObj, deltaObject); + + if (deltaObject.InstanceAnnotationsPropertyInfo != null) + { + object instAnnValue; + changedObj.TryGetPropertyValue(deltaObject.InstanceAnnotationsPropertyInfo.Name, out instAnnValue); + if (instAnnValue != null) + { + IODataInstanceAnnotationContainer instanceAnnotations = instAnnValue as IODataInstanceAnnotationContainer; + + if (instanceAnnotations != null) + { + deltaObject.TrySetPropertyValue(deltaObject.InstanceAnnotationsPropertyInfo.Name, instanceAnnotations); + } + } + } + + return deltaObject; + } + + private void SetProperties(TStructuralType originalObj, Delta edmDeltaEntityObject) + { + foreach (string property in edmDeltaEntityObject.GetUnchangedPropertyNames()) + { + edmDeltaEntityObject.TrySetPropertyValue(property, _clrType.GetProperty(property).GetValue(originalObj)); + } + } + + private DeltaDeletedEntityObject CreateDeletedEntityForFailedOperation(Delta changedObj) + { + Type type = typeof(DeltaDeletedEntityObject<>).MakeGenericType(changedObj.ExpectedClrType); + + DeltaDeletedEntityObject deletedObject = Activator.CreateInstance(type, true, changedObj.InstanceAnnotationsPropertyInfo) as DeltaDeletedEntityObject; + + foreach (string property in changedObj.GetChangedPropertyNames()) + { + SetPropertyValues(changedObj, deletedObject, property); + } + + foreach (string property in changedObj.GetUnchangedPropertyNames()) + { + SetPropertyValues(changedObj, deletedObject, property); + } + + object annValue; + if (changedObj.TryGetPropertyValue(changedObj.InstanceAnnotationsPropertyInfo.Name, out annValue)) + { + IODataInstanceAnnotationContainer instanceAnnotations = annValue as IODataInstanceAnnotationContainer; + + if (instanceAnnotations != null) + { + deletedObject.TrySetPropertyValue(changedObj.InstanceAnnotationsPropertyInfo.Name, instanceAnnotations); + } + } + + deletedObject.TransientInstanceAnnotationContainer = changedObj.TransientInstanceAnnotationContainer; + + ValidateForDeletedEntityId(_keys, deletedObject); + + return deletedObject; + } + + //This is for ODL to work to set id as empty, because if there are missing keys, id wouldnt be set and we need to set it as empty. + private static void ValidateForDeletedEntityId(IList keys, DeltaDeletedEntityObject edmDeletedObject) + { + bool hasnullKeys = false; + for (int i = 0; i < keys.Count; i++) + { + object value; + edmDeletedObject.TryGetPropertyValue(keys[i], out value); + + if (value == null) + { + hasnullKeys = true; + break; + } + } + + if (hasnullKeys) + { + edmDeletedObject.Id = new Uri(string.Empty); + } + } + + private static void SetPropertyValues(Delta changedObj, DeltaDeletedEntityObject edmDeletedObject, string property) + { + object objectVal; + if (changedObj.TryGetPropertyValue(property, out objectVal)) + { + edmDeletedObject.TrySetPropertyValue(property, objectVal); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.OData.Shared/EdmChangedObjectCollection.cs b/src/Microsoft.AspNet.OData.Shared/EdmChangedObjectCollection.cs index 26f2851eea..f8244dd51f 100644 --- a/src/Microsoft.AspNet.OData.Shared/EdmChangedObjectCollection.cs +++ b/src/Microsoft.AspNet.OData.Shared/EdmChangedObjectCollection.cs @@ -5,11 +5,16 @@ // //------------------------------------------------------------------------------ +using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics.Contracts; using System.Linq; using Microsoft.AspNet.OData.Common; +using Microsoft.AspNet.OData.Extensions; using Microsoft.OData.Edm; +using Microsoft.OData.UriParser; +using Org.OData.Core.V1; namespace Microsoft.AspNet.OData { @@ -22,7 +27,7 @@ public class EdmChangedObjectCollection : Collection, IEdmObj private IEdmEntityType _entityType; private EdmDeltaCollectionType _edmType; private IEdmCollectionTypeReference _edmTypeReference; - + /// /// Initializes a new instance of the class. /// @@ -32,7 +37,7 @@ public EdmChangedObjectCollection(IEdmEntityType entityType) { Initialize(entityType); } - + /// /// Initializes a new instance of the class. /// @@ -43,7 +48,13 @@ public EdmChangedObjectCollection(IEdmEntityType entityType, IList + /// Represents EntityType of the changedobject + /// + public IEdmEntityType EntityType { get { return _entityType; } } + /// public IEdmTypeReference GetEdmType() { @@ -61,5 +72,316 @@ private void Initialize(IEdmEntityType entityType) _edmType = new EdmDeltaCollectionType(new EdmEntityTypeReference(_entityType, isNullable: true)); _edmTypeReference = new EdmCollectionTypeReference(_edmType); } + + /// + /// Patch for Types without underlying CLR types + /// + /// + /// ChangedObjectCollection response + internal EdmChangedObjectCollection Patch(ICollection originalCollection) + { + EdmODataAPIHandler apiHandler = new DefaultEdmODataAPIHandler(originalCollection, _entityType); + + return CopyChangedValues(apiHandler); + } + + /// + /// Patch for EdmChangedObjectCollection, a collection for IEdmChangedObject + /// + /// ChangedObjectCollection response + internal EdmChangedObjectCollection Patch(EdmODataAPIHandler apiHandler, ODataEdmAPIHandlerFactory apiHandlerFactory) + { + return CopyChangedValues(apiHandler, apiHandlerFactory); + } + + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling")] + internal EdmChangedObjectCollection CopyChangedValues(EdmODataAPIHandler apiHandler, ODataEdmAPIHandlerFactory apiHandlerFactory = null) + { + EdmChangedObjectCollection changedObjectCollection = new EdmChangedObjectCollection(_entityType); + string[] keys = _entityType.Key().Select(x => x.Name).ToArray(); + + foreach (IEdmChangedObject changedObj in Items) + { + DataModificationOperationKind operation = DataModificationOperationKind.Update; + EdmStructuredObject originalObj = null; + string errorMessage = string.Empty; + string getErrorMessage = string.Empty; + IDictionary keyValues = GetKeyValues(keys, changedObj); + + try + { + IEdmStructuredObject original = null; + EdmEntityObject deltaEntityObject = changedObj as EdmEntityObject; + + ODataAPIResponseStatus ODataAPIResponseStatus = apiHandler.TryGet(deltaEntityObject.ODataPath.GetKeys(), out original, out getErrorMessage); + + EdmDeltaDeletedEntityObject deletedObj = changedObj as EdmDeltaDeletedEntityObject; + if (ODataAPIResponseStatus == ODataAPIResponseStatus.Failure || (deletedObj != null && ODataAPIResponseStatus == ODataAPIResponseStatus.NotFound)) + { + DataModificationExceptionType dataModificationExceptionType = new DataModificationExceptionType(operation); + dataModificationExceptionType.MessageType = new MessageType { Message = getErrorMessage }; + + deletedObj.TransientInstanceAnnotationContainer.AddResourceAnnotation(SRResources.DataModificationException, dataModificationExceptionType); + + changedObjectCollection.Add(deletedObj); + + continue; + } + + if (deletedObj != null) + { + operation = DataModificationOperationKind.Delete; + + PatchItem(deletedObj, original as EdmStructuredObject, apiHandler, apiHandlerFactory); + + if (apiHandler.TryDelete(keyValues, out errorMessage) != ODataAPIResponseStatus.Success) + { + //Handle Failed Operation - Delete + if (ODataAPIResponseStatus == ODataAPIResponseStatus.Success) + { + IEdmChangedObject changedObject = HandleFailedOperation(deletedObj, operation, original, keys, errorMessage, apiHandler); + changedObjectCollection.Add(changedObject); + continue; + } + } + + + + changedObjectCollection.Add(deletedObj); + } + else + { + if (ODataAPIResponseStatus == ODataAPIResponseStatus.NotFound) + { + operation = DataModificationOperationKind.Insert; + + if (apiHandler.TryCreate(changedObj, out original, out errorMessage) != ODataAPIResponseStatus.Success) + { + //Handle failed Opreataion - create + IEdmChangedObject changedObject = HandleFailedOperation(deltaEntityObject, operation, original, keys, errorMessage, apiHandler); + changedObjectCollection.Add(changedObject); + continue; + } + } + else if (ODataAPIResponseStatus == ODataAPIResponseStatus.Success) + { + operation = DataModificationOperationKind.Update; + } + else + { + //Handle failed operation + IEdmChangedObject changedObject = HandleFailedOperation(deltaEntityObject, operation, null, keys, getErrorMessage, apiHandler); + changedObjectCollection.Add(changedObject); + continue; + } + + //Patch for addition/update. + PatchItem(deltaEntityObject, original as EdmStructuredObject, apiHandler, apiHandlerFactory); + + changedObjectCollection.Add(changedObj); + } + } + catch (Exception ex) + { + //Handle Failed Operation + IEdmChangedObject changedObject = HandleFailedOperation(changedObj as EdmEntityObject, operation, originalObj, keys, ex.Message, apiHandler); + + Contract.Assert(changedObject != null); + changedObjectCollection.Add(changedObject); + } + } + + return changedObjectCollection; + } + + private static IDictionary GetKeyValues(string[] keys, IEdmChangedObject changedObj) + { + IDictionary keyValues = new Dictionary(); + + foreach (string key in keys) + { + object value; + changedObj.TryGetPropertyValue(key, out value); + + if (value != null) + { + keyValues.Add(key, value); + } + } + + return keyValues; + } + + private void PatchItem(EdmStructuredObject changedObj, EdmStructuredObject originalObj, EdmODataAPIHandler apiHandler, ODataEdmAPIHandlerFactory apiHandlerFactory = null) + { + if (apiHandlerFactory != null && changedObj is EdmEntityObject entityObject && entityObject.ODataPath != null) + { + ApplyODataId(entityObject.ODataPath, originalObj, apiHandlerFactory); + } + + foreach (string propertyName in changedObj.GetChangedPropertyNames()) + { + ApplyProperties(changedObj, originalObj, propertyName, apiHandler, apiHandlerFactory); + } + } + + /// + /// This applies ODataId parsed Navigation paths, get the value identified by that and copy it on original object, for typeless entities + /// + private void ApplyODataId(ODataPath oDataPath, EdmStructuredObject original, ODataEdmAPIHandlerFactory apiHandlerFactory) + { + EdmODataAPIHandler edmApiHandler = apiHandlerFactory.GetHandler(oDataPath); + + if (edmApiHandler == null) + { + return; + } + + IEdmStructuredObject referencedObj; + string error; + + if (edmApiHandler.TryGet(oDataPath.GetKeys(), out referencedObj, out error) == ODataAPIResponseStatus.Success) + { + EdmStructuredObject structuredObj = referencedObj as EdmStructuredObject; + + foreach (string propertyName in structuredObj.GetChangedPropertyNames()) + { + ApplyProperties(structuredObj, original, propertyName, edmApiHandler, apiHandlerFactory); + } + + foreach (string propertyName in structuredObj.GetUnchangedPropertyNames()) + { + ApplyProperties(structuredObj, original, propertyName, edmApiHandler, apiHandlerFactory); + } + } + } + + + private void ApplyProperties(EdmStructuredObject changedObj, EdmStructuredObject originalObj, string propertyName, EdmODataAPIHandler apiHandler, ODataEdmAPIHandlerFactory apiHandlerFactory = null) + { + object value; + if (changedObj.TryGetPropertyValue(propertyName, out value)) + { + EdmChangedObjectCollection changedColl = value as EdmChangedObjectCollection; + if (changedColl != null) + { + EdmODataAPIHandler apiHandlerNested = apiHandler.GetNestedHandler(originalObj, propertyName); + if (apiHandlerNested != null) + { + changedColl.CopyChangedValues(apiHandlerNested, apiHandlerFactory); + } + else + { + object obj; + originalObj.TryGetPropertyValue(propertyName, out obj); + + ICollection edmColl = obj as ICollection; + + changedColl.Patch(edmColl); + } + } + else + { + //call patchitem if its single structuredobj + EdmStructuredObject structuredObj = value as EdmStructuredObject; + + if (structuredObj != null) + { + object obj; + originalObj.TryGetPropertyValue(propertyName, out obj); + + EdmStructuredObject origStructuredObj = obj as EdmStructuredObject; + + if (origStructuredObj == null) + { + if (structuredObj is EdmComplexObject) + { + origStructuredObj = new EdmComplexObject(structuredObj.ActualEdmType as IEdmComplexType); + } + else + { + origStructuredObj = new EdmEntityObject(structuredObj.ActualEdmType as IEdmEntityType); + } + + originalObj.TrySetPropertyValue(propertyName, origStructuredObj); + } + + PatchItem(structuredObj, origStructuredObj, apiHandler, apiHandlerFactory); + } + else + { + originalObj.TrySetPropertyValue(propertyName, value); + } + } + } + } + + private IEdmChangedObject HandleFailedOperation(EdmEntityObject changedObj, DataModificationOperationKind operation, IEdmStructuredObject originalObj, + string[] keys, string errorMessage, EdmODataAPIHandler apiHandler) + { + IEdmChangedObject edmChangedObject = null; + DataModificationExceptionType dataModificationExceptionType = new DataModificationExceptionType(operation); + dataModificationExceptionType.MessageType = new MessageType { Message = errorMessage }; + + // This handles the Data Modification exception. This adds Core.DataModificationException annotation and also copy other instance annotations. + //The failed operation will be based on the protocol + switch (operation) + { + case DataModificationOperationKind.Update: + edmChangedObject = changedObj as IEdmChangedObject; + break; + case DataModificationOperationKind.Insert: + { + EdmDeltaDeletedEntityObject edmDeletedObject = new EdmDeltaDeletedEntityObject(EntityType); + PatchItem(edmDeletedObject, changedObj, apiHandler); + + ValidateForDeletedEntityId(keys, edmDeletedObject); + + edmDeletedObject.TransientInstanceAnnotationContainer = changedObj.TransientInstanceAnnotationContainer; + edmDeletedObject.PersistentInstanceAnnotationsContainer = changedObj.PersistentInstanceAnnotationsContainer; + + edmDeletedObject.AddDataException(dataModificationExceptionType); + edmChangedObject = edmDeletedObject; + break; + } + case DataModificationOperationKind.Delete: + { + EdmDeltaEntityObject edmEntityObject = new EdmDeltaEntityObject(EntityType); + PatchItem(originalObj as EdmStructuredObject, edmEntityObject, apiHandler); + + edmEntityObject.TransientInstanceAnnotationContainer = changedObj.TransientInstanceAnnotationContainer; + edmEntityObject.PersistentInstanceAnnotationsContainer = changedObj.PersistentInstanceAnnotationsContainer; + + edmEntityObject.AddDataException(dataModificationExceptionType); + edmChangedObject = edmEntityObject; + break; + } + } + + return edmChangedObject; + } + + //This is for ODL to work to set id as empty, because if there are missing keys, id wouldnt be set and we need to set it as empty. + private static void ValidateForDeletedEntityId(string[] keys, EdmDeltaDeletedEntityObject edmDeletedObject) + { + bool hasNullKeys = false; + for (int i = 0; i < keys.Length; i++) + { + object value; + if (edmDeletedObject.TryGetPropertyValue(keys[i], out value)) + { + hasNullKeys = true; + break; + } + } + + if (hasNullKeys) + { + edmDeletedObject.Id = string.Empty; + } + } } } diff --git a/src/Microsoft.AspNet.OData.Shared/EdmDeltaDeletedEntityObject.cs b/src/Microsoft.AspNet.OData.Shared/EdmDeltaDeletedEntityObject.cs index bc9326d13f..3cf8d0f952 100644 --- a/src/Microsoft.AspNet.OData.Shared/EdmDeltaDeletedEntityObject.cs +++ b/src/Microsoft.AspNet.OData.Shared/EdmDeltaDeletedEntityObject.cs @@ -21,7 +21,6 @@ public class EdmDeltaDeletedEntityObject : EdmEntityObject, IEdmDeltaDeletedEnti { private string _id; private DeltaDeletedEntryReason _reason; - private EdmDeltaType _edmType; private IEdmNavigationSource _navigationSource; /// @@ -50,7 +49,7 @@ public EdmDeltaDeletedEntityObject(IEdmEntityTypeReference entityTypeReference) public EdmDeltaDeletedEntityObject(IEdmEntityType entityType, bool isNullable) : base(entityType, isNullable) { - _edmType = new EdmDeltaType(entityType, EdmDeltaEntityKind.DeletedEntry); + } /// @@ -80,12 +79,11 @@ public DeltaDeletedEntryReason Reason } /// - public EdmDeltaEntityKind DeltaKind + public override EdmDeltaEntityKind DeltaKind { get - { - Contract.Assert(_edmType != null); - return _edmType.DeltaKind; + { + return EdmDeltaEntityKind.DeletedEntry; } } diff --git a/src/Microsoft.AspNet.OData.Shared/EdmDeltaDeletedLink.cs b/src/Microsoft.AspNet.OData.Shared/EdmDeltaDeletedLink.cs index df81698dfc..ba9011822b 100644 --- a/src/Microsoft.AspNet.OData.Shared/EdmDeltaDeletedLink.cs +++ b/src/Microsoft.AspNet.OData.Shared/EdmDeltaDeletedLink.cs @@ -21,7 +21,6 @@ public class EdmDeltaDeletedLink : EdmEntityObject, IEdmDeltaDeletedLink private Uri _source; private Uri _target; private string _relationship; - private EdmDeltaType _edmType; /// /// Initializes a new instance of the class. @@ -49,7 +48,7 @@ public EdmDeltaDeletedLink(IEdmEntityTypeReference entityTypeReference) public EdmDeltaDeletedLink(IEdmEntityType entityType, bool isNullable) : base(entityType, isNullable) { - _edmType = new EdmDeltaType(entityType, EdmDeltaEntityKind.DeletedLinkEntry); + } /// @@ -92,12 +91,11 @@ public string Relationship } /// - public EdmDeltaEntityKind DeltaKind + public override EdmDeltaEntityKind DeltaKind { get - { - Contract.Assert(_edmType != null); - return _edmType.DeltaKind; + { + return EdmDeltaEntityKind.DeletedLinkEntry; } } } diff --git a/src/Microsoft.AspNet.OData.Shared/EdmDeltaEntityObject.cs b/src/Microsoft.AspNet.OData.Shared/EdmDeltaEntityObject.cs index e90a72ffaf..ce8131b476 100644 --- a/src/Microsoft.AspNet.OData.Shared/EdmDeltaEntityObject.cs +++ b/src/Microsoft.AspNet.OData.Shared/EdmDeltaEntityObject.cs @@ -17,8 +17,7 @@ namespace Microsoft.AspNet.OData /// [NonValidatingParameterBinding] public class EdmDeltaEntityObject : EdmEntityObject, IEdmChangedObject - { - private EdmDeltaType _edmType; + { private IEdmNavigationSource _navigationSource; /// @@ -47,16 +46,15 @@ public EdmDeltaEntityObject(IEdmEntityTypeReference entityTypeReference) public EdmDeltaEntityObject(IEdmEntityType entityType, bool isNullable) : base(entityType, isNullable) { - _edmType = new EdmDeltaType(entityType, EdmDeltaEntityKind.Entry); + } /// - public EdmDeltaEntityKind DeltaKind + public override EdmDeltaEntityKind DeltaKind { get { - Contract.Assert(_edmType != null); - return _edmType.DeltaKind; + return EdmDeltaEntityKind.Entry; } } diff --git a/src/Microsoft.AspNet.OData.Shared/EdmDeltaLink.cs b/src/Microsoft.AspNet.OData.Shared/EdmDeltaLink.cs index 2d329f4b21..d79a158407 100644 --- a/src/Microsoft.AspNet.OData.Shared/EdmDeltaLink.cs +++ b/src/Microsoft.AspNet.OData.Shared/EdmDeltaLink.cs @@ -92,12 +92,11 @@ public string Relationship } /// - public EdmDeltaEntityKind DeltaKind + public override EdmDeltaEntityKind DeltaKind { get - { - Contract.Assert(_edmType != null); - return _edmType.DeltaKind; + { + return EdmDeltaEntityKind.LinkEntry; } } } diff --git a/src/Microsoft.AspNet.OData.Shared/EdmEntityObject.cs b/src/Microsoft.AspNet.OData.Shared/EdmEntityObject.cs index d0f436fa02..39a4268580 100644 --- a/src/Microsoft.AspNet.OData.Shared/EdmEntityObject.cs +++ b/src/Microsoft.AspNet.OData.Shared/EdmEntityObject.cs @@ -6,7 +6,13 @@ //------------------------------------------------------------------------------ using System; +using System.Diagnostics.Contracts; +using System.Reflection; +using Microsoft.AspNet.OData.Builder; +using Microsoft.AspNet.OData.Common; using Microsoft.OData.Edm; +using Microsoft.OData.UriParser; +using Org.OData.Core.V1; namespace Microsoft.AspNet.OData { @@ -14,7 +20,7 @@ namespace Microsoft.AspNet.OData /// Represents an with no backing CLR . /// [NonValidatingParameterBinding] - public class EdmEntityObject : EdmStructuredObject, IEdmEntityObject + public class EdmEntityObject : EdmStructuredObject, IEdmEntityObject, IEdmChangedObject { /// /// Initializes a new instance of the class. @@ -41,7 +47,57 @@ public EdmEntityObject(IEdmEntityTypeReference edmType) /// true if this object can be nullable; otherwise, false. public EdmEntityObject(IEdmEntityType edmType, bool isNullable) : base(edmType, isNullable) + { + PersistentInstanceAnnotationsContainer = new ODataInstanceAnnotationContainer(); + TransientInstanceAnnotationContainer = new ODataInstanceAnnotationContainer(); + } + + /// + /// Instance Annotation container to hold Transient Annotations + /// + internal IODataInstanceAnnotationContainer TransientInstanceAnnotationContainer { get; set; } + + /// + /// Instance Annotation container to hold Persistent Annotations + /// + public IODataInstanceAnnotationContainer PersistentInstanceAnnotationsContainer { get; set; } + + /// + /// Container to hold ODataId + /// + public IODataIdContainer ODataIdContainer { get; set; } + + /// + /// OData Path for the Item + /// + internal ODataPath ODataPath { get; set; } + + /// + /// DeltaKind as Entry + /// + public virtual EdmDeltaEntityKind DeltaKind { get { return EdmDeltaEntityKind.Entry; } } + + /// + /// Method to Add Data Modification Exception + /// + public void AddDataException(DataModificationExceptionType dataModificationException) { + Contract.Assert(TransientInstanceAnnotationContainer != null); + + TransientInstanceAnnotationContainer.AddResourceAnnotation(SRResources.DataModificationException, dataModificationException); + } + + /// + /// Method to Add Data Modification Exception + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")] + public DataModificationExceptionType GetDataException() + { + Contract.Assert(TransientInstanceAnnotationContainer != null); + + DataModificationExceptionType dataModificationExceptionType = TransientInstanceAnnotationContainer.GetResourceAnnotation(SRResources.DataModificationException) as DataModificationExceptionType; + + return dataModificationExceptionType; } } } diff --git a/src/Microsoft.AspNet.OData.Shared/EdmODataAPIHandler.cs b/src/Microsoft.AspNet.OData.Shared/EdmODataAPIHandler.cs new file mode 100644 index 0000000000..8d6472d95a --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/EdmODataAPIHandler.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; + +namespace Microsoft.AspNet.OData +{ + + /// + /// Handler Class to handle users methods for create, delete and update. + /// This is the handler for data modification where there is no CLR type. + /// + internal abstract class EdmODataAPIHandler + { + /// + /// TryCreate method to create a new object. + /// + /// Changed object which can be appied on creted object, optional + /// The created object (Typeless) + /// Any error message in case of an exception + /// The status of the TryCreate Method, statuses are + public abstract ODataAPIResponseStatus TryCreate(IEdmChangedObject changedObject, out IEdmStructuredObject createdObject, out string errorMessage); + + /// + /// TryGet method which tries to get the Origignal object based on a keyvalues. + /// + /// Key value pair for the entity keys + /// Object to return + /// Any error message in case of an exception + /// The status of the TryGet Method, statuses are + public abstract ODataAPIResponseStatus TryGet(IDictionary keyValues, out IEdmStructuredObject originalObject, out string errorMessage); + + /// + /// TryDelete Method which will delete the object based on keyvalue pairs. + /// + /// + /// + /// The status of the TryDelete Method, statuses are + public abstract ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage); + + /// + /// Get the API handler for the nested type + /// + /// Parent instance. + /// The name of the navigation property for the handler + /// Nested Patch Method handler for the navigation property + public abstract EdmODataAPIHandler GetNestedHandler(IEdmStructuredObject parent, string navigationPropertyName); + } +} diff --git a/src/Microsoft.AspNet.OData.Shared/EdmStructuredObject.cs b/src/Microsoft.AspNet.OData.Shared/EdmStructuredObject.cs index 0355462163..094d3a5ecf 100644 --- a/src/Microsoft.AspNet.OData.Shared/EdmStructuredObject.cs +++ b/src/Microsoft.AspNet.OData.Shared/EdmStructuredObject.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Linq; +using System.Reflection; using Microsoft.AspNet.OData.Common; using Microsoft.AspNet.OData.Formatter; using Microsoft.OData.Edm; @@ -224,8 +225,15 @@ internal static object GetDefaultValue(IEdmTypeReference propertyType) if (propertyType.IsPrimitive() || (isCollection && propertyType.AsCollection().ElementType().IsPrimitive())) { + + bool hasDefaultConstructor = (!clrType.IsClass) || clrType.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static). + Any(x => x.GetParameters().Count() == 0); + // primitive or primitive collection - return Activator.CreateInstance(clrType); + if (hasDefaultConstructor) + { + return Activator.CreateInstance(clrType); + } } else { diff --git a/src/Microsoft.AspNet.OData.Shared/EdmTypeExtensions.cs b/src/Microsoft.AspNet.OData.Shared/EdmTypeExtensions.cs index 568dfb263b..489d68f49c 100644 --- a/src/Microsoft.AspNet.OData.Shared/EdmTypeExtensions.cs +++ b/src/Microsoft.AspNet.OData.Shared/EdmTypeExtensions.cs @@ -26,7 +26,7 @@ public static bool IsDeltaFeed(this IEdmType type) { throw Error.ArgumentNull("type"); } - return (type.GetType() == typeof(EdmDeltaCollectionType)); + return (type.GetType() == typeof(EdmDeltaCollectionType)) || (type.GetType() == typeof(IDeltaSet)); } /// diff --git a/src/Microsoft.AspNet.OData.Shared/Extensions/ODataPathExtensions.cs b/src/Microsoft.AspNet.OData.Shared/Extensions/ODataPathExtensions.cs new file mode 100644 index 0000000000..70467cf20d --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/Extensions/ODataPathExtensions.cs @@ -0,0 +1,89 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.OData.Common; +using Microsoft.OData.UriParser; + +namespace Microsoft.AspNet.OData.Extensions +{ + /// + /// Extensions method for . + /// + internal static class ODataPathExtensions + { + /// + /// Get keys from the last . + /// + /// . + /// Dictionary of keys. + internal static Dictionary GetKeys(this ODataPath path) + { + Dictionary keys = new Dictionary(); + + if (path == null) + { + throw Error.ArgumentNull(nameof(path)); + } + + if (path.Count == 0) + { + return keys; + } + + List pathSegments = path.AsList(); + + KeySegment keySegment = pathSegments.OfType().LastOrDefault(); + + if (keySegment == null) + { + return keys; + } + + keys = ODataPathHelper.KeySegmentAsDictionary(keySegment); + + return keys; + } + + /// + /// Return the last segment in the path, which is not a or . + /// + /// The . + /// An . + public static ODataPathSegment GetLastNonTypeNonKeySegment(this ODataPath path) + { + if (path == null) + { + throw Error.ArgumentNull(nameof(path)); + } + + // If the path is Employees(2)/NewFriends(2)/Namespace.MyNewFriend where Namespace.MyNewFriend is a type segment, + // This method will return NewFriends NavigationPropertySegment. + + List pathSegments = path.AsList(); + int position = path.Count - 1; + + while (position >= 0 && (pathSegments[position] is TypeSegment || pathSegments[position] is KeySegment)) + { + --position; + } + + return position < 0 ? null : pathSegments[position]; + } + + /// + /// Returns a list of in an . + /// + /// The . + /// List of . + public static List GetSegments(this ODataPath path) + { + return path.AsList(); + } + } +} diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/CollectionDeserializationHelpers.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/CollectionDeserializationHelpers.cs index 53fdece31f..285f8b1f11 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/CollectionDeserializationHelpers.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/CollectionDeserializationHelpers.cs @@ -8,6 +8,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics.Contracts; using System.Linq; using System.Reflection; @@ -119,10 +120,26 @@ public static void Clear(this IEnumerable collection, string propertyName, Type clearMethod.Invoke(collection, _emptyObjectArray); } - public static bool TryCreateInstance(Type collectionType, IEdmCollectionTypeReference edmCollectionType, Type elementType, out IEnumerable instance) + public static bool TryCreateInstance(Type collectionType, IEdmCollectionTypeReference edmCollectionType, Type elementType, out IEnumerable instance, bool isDelta = false) { Contract.Assert(collectionType != null); + //For Delta Collection requests + if (isDelta) + { + if (elementType == typeof(IEdmEntityObject)) + { + instance = new EdmChangedObjectCollection(edmCollectionType.ElementType().AsEntity().Definition as IEdmEntityType); + } + else + { + Type type = typeof(DeltaSet<>).MakeGenericType(elementType); + instance = Activator.CreateInstance(type, edmCollectionType.ElementType().AsEntity().Key().Select(x => x.Name).ToList()) as ICollection; + } + + return true; + } + if (collectionType == typeof(EdmComplexObjectCollection)) { instance = new EdmComplexObjectCollection(edmCollectionType); diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/DefaultODataDeserializerProvider.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/DefaultODataDeserializerProvider.cs index fd25f376da..b89f1355c9 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/DefaultODataDeserializerProvider.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/DefaultODataDeserializerProvider.cs @@ -92,8 +92,6 @@ internal ODataDeserializer GetODataDeserializerImpl(Type type, Func m return _rootContainer.GetRequiredService(); } - // Get the model. Using a Func to delay evaluation of the model - // until after the above checks have passed. IEdmModel model = modelFunction(); ClrTypeCache typeMappingCache = model.GetTypeMappingCache(); IEdmTypeReference edmType = typeMappingCache.GetEdmType(type, model); diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/DeserializationHelpers.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/DeserializationHelpers.cs index 235c05c44f..cd6378a176 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/DeserializationHelpers.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/DeserializationHelpers.cs @@ -87,18 +87,82 @@ internal static void ApplyProperty(ODataProperty property, IEdmStructuredTypeRef } } - internal static void ApplyInstanceAnnotations(object resource, IEdmStructuredTypeReference structuredType, ODataResource oDataResource, + internal static void ApplyInstanceAnnotations(object resource, IEdmStructuredTypeReference structuredType, ODataResourceBase oDataResource, ODataDeserializerProvider deserializerProvider, ODataDeserializerContext readContext) { - PropertyInfo propertyInfo = EdmLibHelpers.GetInstanceAnnotationsContainer(structuredType.StructuredDefinition(), readContext.Model); - if (propertyInfo == null) + //Apply instance annotations for both entityobject/changedobject/delta and normal resources + + IODataInstanceAnnotationContainer instanceAnnotationContainer = null; + IODataInstanceAnnotationContainer transientAnnotationContainer = null; + + EdmEntityObject edmObject = resource as EdmEntityObject; + + if (edmObject != null) + { + instanceAnnotationContainer = edmObject.PersistentInstanceAnnotationsContainer; + transientAnnotationContainer = edmObject.TransientInstanceAnnotationContainer; + } + else + { + PropertyInfo propertyInfo = EdmLibHelpers.GetInstanceAnnotationsContainer(structuredType.StructuredDefinition(), readContext.Model); + if (propertyInfo != null) + { + instanceAnnotationContainer = GetAnnotationContainer(propertyInfo, resource); + } + + IDeltaSetItem deltaItem = resource as IDeltaSetItem; + + if (deltaItem != null) + { + transientAnnotationContainer = deltaItem.TransientInstanceAnnotationContainer; + } + } + + if (instanceAnnotationContainer == null && transientAnnotationContainer == null) { return; } - IODataInstanceAnnotationContainer instanceAnnotationContainer = GetAnnotationContainer(propertyInfo, resource); + SetInstanceAnnotations(oDataResource, instanceAnnotationContainer, transientAnnotationContainer, deserializerProvider, readContext); + } + + internal static void ApplyODataIdContainer(object resource, IEdmStructuredTypeReference structuredType, ODataResourceBase oDataResource, + ODataDeserializerProvider deserializerProvider, ODataDeserializerContext readContext) + { + //Apply instance annotations for both entityobject/changedobject/delta and normal resources + + IODataInstanceAnnotationContainer instanceAnnotationContainer = null; + IODataInstanceAnnotationContainer transientAnnotationContainer = null; + + EdmEntityObject edmObject = resource as EdmEntityObject; + + if (edmObject != null) + { + instanceAnnotationContainer = edmObject.PersistentInstanceAnnotationsContainer; + transientAnnotationContainer = edmObject.TransientInstanceAnnotationContainer; + } + else + { + PropertyInfo propertyInfo = EdmLibHelpers.GetInstanceAnnotationsContainer(structuredType.StructuredDefinition(), readContext.Model); + if (propertyInfo != null) + { + instanceAnnotationContainer = GetAnnotationContainer(propertyInfo, resource); + } + + IDeltaSetItem deltaItem = resource as IDeltaSetItem; + + if (deltaItem != null) + { + transientAnnotationContainer = deltaItem.TransientInstanceAnnotationContainer; + } + } + + if (instanceAnnotationContainer == null && transientAnnotationContainer == null) + { + return; + } - SetInstanceAnnotations(oDataResource, instanceAnnotationContainer, deserializerProvider, readContext); + SetInstanceAnnotations(oDataResource, instanceAnnotationContainer, transientAnnotationContainer, deserializerProvider, readContext); } internal static void SetDynamicProperty(object resource, IEdmStructuredTypeReference resourceType, @@ -140,15 +204,15 @@ internal static void SetDeclaredProperty(object resource, EdmTypeKind propertyKi } } - internal static void SetCollectionProperty(object resource, IEdmProperty edmProperty, object value, string propertyName) + internal static void SetCollectionProperty(object resource, IEdmProperty edmProperty, object value, string propertyName, bool isDelta = false) { Contract.Assert(edmProperty != null); - SetCollectionProperty(resource, propertyName, edmProperty.Type.AsCollection(), value, clearCollection: false); + SetCollectionProperty(resource, propertyName, edmProperty.Type.AsCollection(), value, clearCollection: false, isDelta); } internal static void SetCollectionProperty(object resource, string propertyName, - IEdmCollectionTypeReference edmPropertyType, object value, bool clearCollection) + IEdmCollectionTypeReference edmPropertyType, object value, bool clearCollection, bool isDelta = false) { if (value != null) { @@ -168,7 +232,7 @@ internal static void SetCollectionProperty(object resource, string propertyName, IEnumerable newCollection; if (CanSetProperty(resource, propertyName) && - CollectionDeserializationHelpers.TryCreateInstance(propertyType, edmPropertyType, elementType, out newCollection)) + CollectionDeserializationHelpers.TryCreateInstance(propertyType, edmPropertyType, elementType, out newCollection, isDelta)) { // settable collections collection.AddToCollection(newCollection, elementType, resourceType, propertyName, propertyType); @@ -279,14 +343,21 @@ internal static void SetDynamicProperty(object resource, string propertyName, ob } } - internal static void SetInstanceAnnotations(ODataResource oDataResource, IODataInstanceAnnotationContainer instanceAnnotationContainer, - ODataDeserializerProvider deserializerProvider, ODataDeserializerContext readContext) + internal static void SetInstanceAnnotations(ODataResourceBase oDataResource, IODataInstanceAnnotationContainer instanceAnnotationContainer, + IODataInstanceAnnotationContainer transientAnnotationContainer, ODataDeserializerProvider deserializerProvider, ODataDeserializerContext readContext) { if(oDataResource.InstanceAnnotations != null) { foreach (ODataInstanceAnnotation annotation in oDataResource.InstanceAnnotations) { - AddInstanceAnnotationToContainer(instanceAnnotationContainer, deserializerProvider, readContext, annotation,string.Empty); + if (!TransientAnnotations.TransientAnnotationTerms.Contains(annotation.Name)) + { + AddInstanceAnnotationToContainer(instanceAnnotationContainer, deserializerProvider, readContext, annotation, string.Empty); + } + else + { + AddInstanceAnnotationToContainer(transientAnnotationContainer, deserializerProvider, readContext, annotation, string.Empty); + } } } @@ -318,11 +389,12 @@ private static void AddInstanceAnnotationToContainer(IODataInstanceAnnotationCon instanceAnnotationContainer.AddPropertyAnnotation(propertyName,annotation.Name, annotationValue); } } - + public static IODataInstanceAnnotationContainer GetAnnotationContainer(PropertyInfo propertyInfo, object resource) - { + { + object value; IDelta delta = resource as IDelta; - object value; + if (delta != null) { delta.TryGetPropertyValue(propertyInfo.Name, out value); @@ -330,7 +402,7 @@ public static IODataInstanceAnnotationContainer GetAnnotationContainer(PropertyI else { value = propertyInfo.GetValue(resource); - } + } IODataInstanceAnnotationContainer instanceAnnotationContainer = value as IODataInstanceAnnotationContainer; @@ -346,8 +418,8 @@ public static IODataInstanceAnnotationContainer GetAnnotationContainer(PropertyI { instanceAnnotationContainer = Activator.CreateInstance(propertyInfo.PropertyType) as IODataInstanceAnnotationContainer; } - - if(delta != null) + + if (delta != null) { delta.TrySetPropertyValue(propertyInfo.Name, instanceAnnotationContainer); } diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataDeltaResourceSetWrapper.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataDeltaResourceSetWrapper.cs new file mode 100644 index 0000000000..d3a53e2771 --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataDeltaResourceSetWrapper.cs @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------------- +// +// 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.OData; + +namespace Microsoft.AspNet.OData.Formatter.Deserialization +{ + /// + /// Encapsulates an and the 's that are part of it. + /// + public sealed class ODataDeltaResourceSetWrapper : ODataResourceSetWrapperBase + { + /// + /// Initializes a new instance of . + /// + /// The wrapped item. + public ODataDeltaResourceSetWrapper(ODataDeltaResourceSet item) + : base(item) + { + + } + + internal override ResourceSetType ResourceSetType => ResourceSetType.DeltaResourceSet; + } +} diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataDeserializerContext.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataDeserializerContext.cs index 75d5506a71..0dabf8e822 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataDeserializerContext.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataDeserializerContext.cs @@ -18,7 +18,11 @@ namespace Microsoft.AspNet.OData.Formatter.Deserialization public partial class ODataDeserializerContext { private bool? _isDeltaOfT; + private bool? _isDeletedDeltaOfT; private bool? _isUntyped; + private bool? _isChangedObjectCollection; + private bool? _isDeltaEntity; + private bool? _isDeltaDeletedEntity; /// /// Gets or sets the type of the top-level object the request needs to be deserialized into. @@ -62,20 +66,75 @@ internal bool IsDeltaOfT { if (!_isDeltaOfT.HasValue) { - _isDeltaOfT = ResourceType != null && TypeHelper.IsGenericType(ResourceType) && ResourceType.GetGenericTypeDefinition() == typeof(Delta<>); + _isDeltaOfT = ResourceType != null && TypeHelper.IsGenericType(ResourceType) && (ResourceType.GetGenericTypeDefinition() == typeof(Delta<>) || + ResourceType.GetGenericTypeDefinition() == typeof(DeltaDeletedEntityObject<>)); } return _isDeltaOfT.Value; } } + internal bool IsDeletedDeltaOfT + { + get + { + if (!_isDeletedDeltaOfT.HasValue) + { + _isDeletedDeltaOfT = ResourceType != null && TypeHelper.IsGenericType(ResourceType) && (ResourceType.GetGenericTypeDefinition() == typeof(DeltaDeletedEntityObject<>) || + ResourceType.GetGenericTypeDefinition() == typeof(DeltaDeletedEntityObject<>)); + } + + return _isDeletedDeltaOfT.Value; + } + } + + internal bool IsDeltaEntity + { + get + { + if (!_isDeltaEntity.HasValue) + { + _isDeltaEntity = ResourceType != null && (ResourceType == typeof(EdmDeltaEntityObject) || ResourceType == typeof(EdmDeltaDeletedEntityObject)); + } + + return _isDeltaEntity.Value; + } + } + + internal bool IsDeltaDeletedEntity + { + get + { + if (!_isDeltaDeletedEntity.HasValue) + { + _isDeltaDeletedEntity = ResourceType != null && ResourceType == typeof(EdmDeltaDeletedEntityObject); + } + + return _isDeltaDeletedEntity.Value; + } + } + + internal bool IsChangedObjectCollection + { + get + { + if (!_isChangedObjectCollection.HasValue) + { + _isChangedObjectCollection = ResourceType != null && (ResourceType == typeof(EdmChangedObjectCollection) || (TypeHelper.IsGenericType(ResourceType) && + ResourceType.GetGenericTypeDefinition() == typeof(DeltaSet<>) )); + } + + return _isChangedObjectCollection.Value; + } + } + internal bool IsUntyped { get { if (!_isUntyped.HasValue) { - _isUntyped = TypeHelper.IsTypeAssignableFrom(typeof(IEdmObject), ResourceType) || + _isUntyped = IsChangedObjectCollection ? !TypeHelper.IsGenericType(ResourceType) : (TypeHelper.IsTypeAssignableFrom(typeof(IEdmObject), ResourceType) && !IsDeltaOfT) || typeof(ODataUntypedActionParameters) == ResourceType; } diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataEntityReferenceLinkBase.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataEntityReferenceLinkBase.cs index 1fd669a07c..ebbbf435f9 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataEntityReferenceLinkBase.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataEntityReferenceLinkBase.cs @@ -21,17 +21,13 @@ public class ODataEntityReferenceLinkBase : ODataItemBase public ODataEntityReferenceLinkBase(ODataEntityReferenceLink item) : base(item) { + EntityReferenceLink = item; } /// /// Gets the wrapped . /// - public ODataEntityReferenceLink EntityReferenceLink - { - get - { - return Item as ODataEntityReferenceLink; - } - } + public ODataEntityReferenceLink EntityReferenceLink { get; } + } } diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataNestedResourceInfoWrapper.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataNestedResourceInfoWrapper.cs index 3d7a5d5d24..1793290957 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataNestedResourceInfoWrapper.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataNestedResourceInfoWrapper.cs @@ -44,5 +44,6 @@ public ODataNestedResourceInfo NestedResourceInfo /// Gets the nested items that are part of this nested resource info. /// public IList NestedItems { get; private set; } + } } diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataReaderExtensions.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataReaderExtensions.cs index d2cc2b3427..a7d1d069b7 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataReaderExtensions.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataReaderExtensions.cs @@ -89,30 +89,31 @@ private static void ReadCollectionItem(ODataReader reader, Stack } else { - ODataItemBase parentItem = itemsStack.Peek(); - ODataResourceSetWrapper parentResourceSet = parentItem as ODataResourceSetWrapper; - if (parentResourceSet != null) - { - parentResourceSet.Resources.Add(resourceWrapper); - } - else - { - ODataNestedResourceInfoWrapper parentNestedResource = (ODataNestedResourceInfoWrapper)parentItem; - Contract.Assert(parentNestedResource.NestedResourceInfo.IsCollection == false, "Only singleton nested properties can contain resource as their child."); - Contract.Assert(parentNestedResource.NestedItems.Count == 0, "Each nested property can contain only one resource as its direct child."); - parentNestedResource.NestedItems.Add(resourceWrapper); - } + AddResourceToParent(itemsStack, resourceWrapper); } itemsStack.Push(resourceWrapper); break; case ODataReaderState.ResourceEnd: + case ODataReaderState.DeletedResourceEnd: Contract.Assert( itemsStack.Count > 0 && (reader.Item == null || itemsStack.Peek().Item == reader.Item), "The resource which is ending should be on the top of the items stack."); itemsStack.Pop(); break; + case ODataReaderState.DeletedResourceStart: + ODataDeletedResource deletedResource = (ODataDeletedResource)reader.Item; + Contract.Assert(deletedResource != null, "Deleted resource should not be null"); + + ODataResourceWrapper deletedResourceWrapper = new ODataResourceWrapper(deletedResource); + + Contract.Assert(itemsStack.Count != 0, "Deleted Resource should not be top level item"); + + AddResourceToParent(itemsStack, deletedResourceWrapper); + + itemsStack.Push(deletedResourceWrapper); + break; case ODataReaderState.NestedResourceInfoStart: ODataNestedResourceInfo nestedResourceInfo = (ODataNestedResourceInfo)reader.Item; @@ -139,12 +140,10 @@ private static void ReadCollectionItem(ODataReader reader, Stack Contract.Assert(resourceSet != null, "ResourceSet should never be null."); ODataResourceSetWrapper resourceSetWrapper = new ODataResourceSetWrapper(resourceSet); + if (itemsStack.Count > 0) { - ODataNestedResourceInfoWrapper parentNestedResourceInfo = (ODataNestedResourceInfoWrapper)itemsStack.Peek(); - Contract.Assert(parentNestedResourceInfo != null, "this has to be an inner resource set. inner resource sets always have a nested resource info."); - Contract.Assert(parentNestedResourceInfo.NestedResourceInfo.IsCollection == true, "Only collection nested properties can contain resource set as their child."); - parentNestedResourceInfo.NestedItems.Add(resourceSetWrapper); + AddNestedResourceInfo(itemsStack, resourceSetWrapper); } else { @@ -155,6 +154,7 @@ private static void ReadCollectionItem(ODataReader reader, Stack break; case ODataReaderState.ResourceSetEnd: + case ODataReaderState.DeltaResourceSetEnd: Contract.Assert(itemsStack.Count > 0 && itemsStack.Peek().Item == reader.Item, "The resource set which is ending should be on the top of the items stack."); itemsStack.Pop(); break; @@ -171,11 +171,58 @@ private static void ReadCollectionItem(ODataReader reader, Stack } break; + case ODataReaderState.DeltaResourceSetStart: + ODataDeltaResourceSet deltaResourceSet = (ODataDeltaResourceSet)reader.Item; + Contract.Assert(deltaResourceSet != null, "ResourceSet should never be null."); + + ODataDeltaResourceSetWrapper deltaResourceSetWrapper = new ODataDeltaResourceSetWrapper(deltaResourceSet); + + if (itemsStack.Count > 0) + { + AddNestedResourceInfo(itemsStack, deltaResourceSetWrapper); + } + else + { + topLevelItem = deltaResourceSetWrapper; + } + + itemsStack.Push(deltaResourceSetWrapper); + break; + case ODataReaderState.DeltaLink: + case ODataReaderState.DeltaDeletedLink: + + //Throw error if Delta Link appears + throw Error.NotSupported(SRResources.DeltaLinkNotSupported); + default: Contract.Assert(false, "We should never get here, it means the ODataReader reported a wrong state."); break; } } + + + private static void AddNestedResourceInfo(Stack itemsStack, ODataResourceSetWrapperBase resourceSetWrapper) + { + ODataNestedResourceInfoWrapper parentNestedResourceInfo = (ODataNestedResourceInfoWrapper)itemsStack.Peek(); + Contract.Assert(parentNestedResourceInfo != null, "this has to be an inner resource set. inner resource sets always have a nested resource info."); + Contract.Assert(parentNestedResourceInfo.NestedResourceInfo.IsCollection == true, "Only collection nested properties can contain resource set as their child."); + parentNestedResourceInfo.NestedItems.Add(resourceSetWrapper); + } + + private static void AddResourceToParent(Stack itemsStack, ODataResourceWrapper resourceWrapper) + { + ODataItemBase parentItem = itemsStack.Peek(); + ODataResourceSetWrapperBase parentResourceSet = parentItem as ODataResourceSetWrapperBase; + if (parentResourceSet != null) + { + parentResourceSet.Resources.Add(resourceWrapper); + } + else + { + ODataNestedResourceInfoWrapper parentNestedResource = (ODataNestedResourceInfoWrapper)parentItem; + parentNestedResource.NestedItems.Add(resourceWrapper); + } + } } } diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializer.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializer.cs index c75ba4747f..b0384a4d50 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializer.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializer.cs @@ -15,15 +15,17 @@ using System.Runtime.Serialization; using System.Threading.Tasks; using Microsoft.AspNet.OData.Common; -using Microsoft.AspNet.OData.Formatter.Serialization; +using Microsoft.AspNet.OData.Interfaces; using Microsoft.OData; using Microsoft.OData.Edm; +using Microsoft.OData.UriParser; namespace Microsoft.AspNet.OData.Formatter.Deserialization { /// /// Represents an for reading OData resource payloads. /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling")] public class ODataResourceDeserializer : ODataEdmTypeDeserializer { /// @@ -87,7 +89,8 @@ public sealed override object ReadInline(object item, IEdmTypeReference edmType, throw Error.ArgumentNull("item"); } - if (!edmType.IsStructured()) + IEdmStructuredTypeReference structuredType = edmType.AsStructured(); + if (structuredType == null) { throw Error.Argument("edmType", SRResources.ArgumentMustBeOfType, "Entity or Complex"); } @@ -98,10 +101,13 @@ public sealed override object ReadInline(object item, IEdmTypeReference edmType, throw Error.Argument("item", SRResources.ArgumentMustBeOfType, typeof(ODataResource).Name); } + // Create the appropriate nested context + ODataDeserializerContext nestedContext = BuildNestedContextFromCurrentContext(readContext, structuredType.IsEntity() ? ApplyIdToPath(readContext, resourceWrapper) : readContext.Path); + // Recursion guard to avoid stack overflows RuntimeHelpers.EnsureSufficientExecutionStack(); - return ReadResource(resourceWrapper, edmType.AsStructured(), readContext); + return ReadResource(resourceWrapper, edmType.AsStructured(), nestedContext); } /// @@ -123,8 +129,8 @@ public virtual object ReadResource(ODataResourceWrapper resourceWrapper, IEdmStr { throw Error.ArgumentNull("readContext"); } - - if (!String.IsNullOrEmpty(resourceWrapper.Resource.TypeName) && structuredType.FullName() != resourceWrapper.Resource.TypeName) + + if (!String.IsNullOrEmpty(resourceWrapper.ResourceBase.TypeName) && structuredType.FullName() != resourceWrapper.ResourceBase.TypeName) { // received a derived type in a base type deserializer. delegate it to the appropriate derived type deserializer. IEdmModel model = readContext.Model; @@ -134,20 +140,21 @@ public virtual object ReadResource(ODataResourceWrapper resourceWrapper, IEdmStr throw Error.Argument("readContext", SRResources.ModelMissingFromReadContext); } - IEdmStructuredType actualType = model.FindType(resourceWrapper.Resource.TypeName) as IEdmStructuredType; + IEdmStructuredType actualType = model.FindType(resourceWrapper.ResourceBase.TypeName) as IEdmStructuredType; if (actualType == null) { - throw new ODataException(Error.Format(SRResources.ResourceTypeNotInModel, resourceWrapper.Resource.TypeName)); + throw new ODataException(Error.Format(SRResources.ResourceTypeNotInModel, resourceWrapper.ResourceBase.TypeName)); } if (actualType.IsAbstract) { - string message = Error.Format(SRResources.CannotInstantiateAbstractResourceType, resourceWrapper.Resource.TypeName); + string message = Error.Format(SRResources.CannotInstantiateAbstractResourceType, resourceWrapper.ResourceBase.TypeName); throw new ODataException(message); } IEdmTypeReference actualStructuredType; IEdmEntityType actualEntityType = actualType as IEdmEntityType; + if (actualEntityType != null) { actualStructuredType = new EdmEntityTypeReference(actualEntityType, isNullable: false); @@ -178,10 +185,19 @@ public virtual object ReadResource(ODataResourceWrapper resourceWrapper, IEdmStr { object resource = CreateResourceInstance(structuredType, readContext); ApplyResourceProperties(resource, resourceWrapper, structuredType, readContext); + + ODataDeletedResource deletedResource = resourceWrapper.ResourceBase as ODataDeletedResource; + + if (deletedResource != null) + { + AppendDeletedProperties(resource, deletedResource, readContext.IsUntyped); + } + return resource; } } + /// /// Creates a new instance of the backing CLR object for the given resource type. /// @@ -210,6 +226,11 @@ public virtual object CreateResourceInstance(IEdmStructuredTypeReference structu { if (structuredType.IsEntity()) { + if (readContext.IsDeltaDeletedEntity) + { + return new EdmDeltaDeletedEntityObject(structuredType.AsEntity()); + } + return new EdmEntityObject(structuredType.AsEntity()); } @@ -226,20 +247,23 @@ public virtual object CreateResourceInstance(IEdmStructuredTypeReference structu if (readContext.IsDeltaOfT) { - IEnumerable structuralProperties = structuredType.StructuralProperties() + IEnumerable structuralProperties = structuredType.StructuredDefinition().Properties() .Select(edmProperty => EdmLibHelpers.GetClrPropertyName(edmProperty, model)); + PropertyInfo instanceAnnotationProperty = EdmLibHelpers.GetInstanceAnnotationsContainer( + structuredType.StructuredDefinition(), model); + if (structuredType.IsOpen()) { PropertyInfo dynamicDictionaryPropertyInfo = EdmLibHelpers.GetDynamicPropertyDictionary( structuredType.StructuredDefinition(), model); return Activator.CreateInstance(readContext.ResourceType, clrType, structuralProperties, - dynamicDictionaryPropertyInfo, structuredType.IsComplex()); + dynamicDictionaryPropertyInfo, structuredType.IsComplex(), instanceAnnotationProperty); } else { - return Activator.CreateInstance(readContext.ResourceType, clrType, structuralProperties, null, structuredType.IsComplex()); + return Activator.CreateInstance(readContext.ResourceType, clrType, structuralProperties, null, structuredType.IsComplex(), instanceAnnotationProperty); } } else @@ -249,6 +273,20 @@ public virtual object CreateResourceInstance(IEdmStructuredTypeReference structu } } + private static void AppendDeletedProperties(dynamic resource, ODataDeletedResource deletedResource, bool isUntyped) + { + if (isUntyped) + { + resource.Id = deletedResource.Id.ToString(); + } + else + { + resource.Id = deletedResource.Id; + } + + resource.Reason = deletedResource.Reason.Value; + } + /// /// Deserializes the nested properties from into . /// @@ -301,7 +339,41 @@ public virtual void ApplyNestedProperty(object resource, ODataNestedResourceInfo } } - foreach (ODataItemBase childItem in resourceInfoWrapper.NestedItems) + IList nestedItems; + ODataEntityReferenceLinkBase[] referenceLinks = resourceInfoWrapper.NestedItems.OfType().ToArray(); + if (referenceLinks.Length > 0) + { + // Be noted: + // 1) OData v4.0, it's "Orders@odata.bind", and we get "ODataEntityReferenceLinkWrapper"(s) for that. + // 2) OData v4.01, it's {"odata.id" ...}, and we get "ODataResource"(s) for that. + // So, in OData v4, if it's a single, NestedItems contains one ODataEntityReferenceLinkWrapper, + // if it's a collection, NestedItems contains multiple ODataEntityReferenceLinkWrapper(s) + // We can use the following code to adjust the `ODataEntityReferenceLinkWrapper` to `ODataResourceWrapper`. + // In OData v4.01, we will not be here. + // Only supports declared property + Contract.Assert(edmProperty != null); + + nestedItems = new List(); + if (edmProperty.Type.IsCollection()) + { + IEdmCollectionTypeReference edmCollectionTypeReference = edmProperty.Type.AsCollection(); + ODataResourceSetWrapper resourceSetWrapper = CreateResourceSetWrapper(edmCollectionTypeReference, referenceLinks, readContext); + nestedItems.Add(resourceSetWrapper); + } + else + { + ODataResourceWrapper resourceWrapper = CreateResourceWrapper(edmProperty.Type, referenceLinks[0], readContext); + nestedItems.Add(resourceWrapper); + } + } + else + { + nestedItems = resourceInfoWrapper.NestedItems; + } + + ODataDeserializerContext nestedReadContext = GenerateNestedReadContext(resourceInfoWrapper, readContext, edmProperty); + + foreach (ODataItemBase childItem in nestedItems) { // it maybe null. if (childItem == null) @@ -310,32 +382,25 @@ public virtual void ApplyNestedProperty(object resource, ODataNestedResourceInfo { // for the dynamic, OData.net has a bug. see https://github.com/OData/odata.net/issues/977 ApplyDynamicResourceInNestedProperty(resourceInfoWrapper.NestedResourceInfo.Name, resource, - structuredType, null, readContext); + structuredType, null, nestedReadContext); } else { - ApplyResourceInNestedProperty(edmProperty, resource, null, readContext); + ApplyResourceInNestedProperty(edmProperty, resource, null, nestedReadContext); } } - ODataEntityReferenceLinkBase entityReferenceLink = childItem as ODataEntityReferenceLinkBase; - if (entityReferenceLink != null) - { - // ignore entity reference links. - continue; - } - - ODataResourceSetWrapper resourceSetWrapper = childItem as ODataResourceSetWrapper; + ODataResourceSetWrapperBase resourceSetWrapper = childItem as ODataResourceSetWrapperBase; if (resourceSetWrapper != null) { if (edmProperty == null) { ApplyDynamicResourceSetInNestedProperty(resourceInfoWrapper.NestedResourceInfo.Name, - resource, structuredType, resourceSetWrapper, readContext); + resource, structuredType, resourceSetWrapper, nestedReadContext); } else { - ApplyResourceSetInNestedProperty(edmProperty, resource, resourceSetWrapper, readContext); + ApplyResourceSetInNestedProperty(edmProperty, resource, resourceSetWrapper, nestedReadContext); } continue; @@ -348,14 +413,361 @@ public virtual void ApplyNestedProperty(object resource, ODataNestedResourceInfo if (edmProperty == null) { ApplyDynamicResourceInNestedProperty(resourceInfoWrapper.NestedResourceInfo.Name, resource, - structuredType, resourceWrapper, readContext); + structuredType, resourceWrapper, nestedReadContext); + } + else + { + ApplyResourceInNestedProperty(edmProperty, resource, resourceWrapper, nestedReadContext); + } + } + } + } + + private static ODataDeserializerContext GenerateNestedReadContext(ODataNestedResourceInfoWrapper resourceInfoWrapper, ODataDeserializerContext readContext, IEdmProperty edmProperty) + { + ODataDeserializerContext nestedReadContext = null; + Routing.ODataPath path = readContext.Path; + + try + { + + // this code attempts to make sure that the path is always correct for the level that we are reading. + if (edmProperty == null) + { + ODataNestedResourceInfo nestedInfo = resourceInfoWrapper.NestedResourceInfo; + + IEdmType segmentType = null; + string propertyTypeName = nestedInfo.TypeAnnotation?.TypeName; + if (!string.IsNullOrEmpty(propertyTypeName)) + { + segmentType = readContext.Model.FindType(propertyTypeName); + } + + // could it be a problem later that the navigationSource is null? + DynamicPathSegment pathSegment = new DynamicPathSegment( + nestedInfo.Name, + segmentType, + null, + nestedInfo.IsCollection != true + ); + + path = AppendToPath(path, pathSegment); + } + else + { + if (edmProperty.PropertyKind == EdmPropertyKind.Navigation) + { + Contract.Assert(readContext.Path.NavigationSource != null, "Navigation property segment with null navigationSource"); + IEdmNavigationProperty navigationProperty = edmProperty as IEdmNavigationProperty; + IEdmNavigationSource parentNavigationSource = readContext.Path.NavigationSource; + IEdmNavigationSource navigationSource = parentNavigationSource.FindNavigationTarget(navigationProperty); + + if (navigationProperty.ContainsTarget) + { + path = AppendToPath(path, new NavigationPropertySegment(navigationProperty, navigationSource), navigationProperty.DeclaringType, parentNavigationSource); + } + else + { + path = new Routing.ODataPath(new ODataUriParser(readContext.Model, new Uri(navigationSource.Path.Path, UriKind.Relative)).ParsePath()); + } } else { - ApplyResourceInNestedProperty(edmProperty, resource, resourceWrapper, readContext); + IEdmStructuralProperty structuralProperty = edmProperty as IEdmStructuralProperty; + path = AppendToPath(path, new PropertySegment(structuralProperty), structuralProperty.DeclaringType, null); } } + + nestedReadContext = BuildNestedContextFromCurrentContext(readContext, path); + } + catch + { + nestedReadContext = BuildNestedContextFromCurrentContext(readContext); + } + + return nestedReadContext; + } + + private static ODataDeserializerContext BuildNestedContextFromCurrentContext(ODataDeserializerContext readContext, Routing.ODataPath path =null, Type resourceType = null) + { + return new ODataDeserializerContext + { + Path = path??readContext.Path, + Model = readContext.Model, + Request = readContext.Request, + ResourceType = resourceType??readContext.ResourceType + }; + } + + //Appends a new segment to an ODataPath + private static Routing.ODataPath AppendToPath(Routing.ODataPath path, ODataPathSegment segment) + { + return AppendToPath(path, segment, null, null); + } + + //Appends a new segment to an ODataPath, adding a type segment if required + private static Routing.ODataPath AppendToPath(Routing.ODataPath path, ODataPathSegment segment, IEdmType declaringType, IEdmNavigationSource navigationSource) + { + List segments = new List(path.Segments); + + // Append type cast segment if required + if (declaringType != null && path.EdmType != declaringType) + { + segments.Add(new TypeSegment(declaringType, navigationSource)); + } + + segments.Add(segment); + + return new Routing.ODataPath(segments); + } + + private ODataResourceSetWrapper CreateResourceSetWrapper(IEdmCollectionTypeReference edmPropertyType, + IList refLinks, ODataDeserializerContext readContext) + { + ODataResourceSet resourceSet = new ODataResourceSet + { + TypeName = edmPropertyType.FullName(), + }; + + IEdmTypeReference elementType = edmPropertyType.ElementType(); + ODataResourceSetWrapper resourceSetWrapper = new ODataResourceSetWrapper(resourceSet); + foreach (ODataEntityReferenceLinkBase refLinkWrapper in refLinks) + { + ODataResourceWrapper resourceWrapper = CreateResourceWrapper(elementType, refLinkWrapper, readContext); + resourceSetWrapper.Resources.Add(resourceWrapper); + } + + return resourceSetWrapper; + } + + private ODataResourceWrapper CreateResourceWrapper(IEdmTypeReference edmPropertyType, ODataEntityReferenceLinkBase refLink, ODataDeserializerContext readContext) + { + Contract.Assert(readContext != null); + + ODataResource resource = new ODataResource + { + TypeName = edmPropertyType.FullName(), + }; + + resource.Properties = CreateKeyProperties(refLink.EntityReferenceLink.Url, readContext); + + if (refLink.EntityReferenceLink.InstanceAnnotations != null) + { + foreach (ODataInstanceAnnotation instanceAnnotation in refLink.EntityReferenceLink.InstanceAnnotations) + { + resource.InstanceAnnotations.Add(instanceAnnotation); + }; } + + return new ODataResourceWrapper(resource); + } + + + /// + /// Do uri parsing to get the key values. + /// + /// The key Id. + /// The reader context. + /// The key properties. + private static IList CreateKeyProperties(Uri id, ODataDeserializerContext readContext) + { + Contract.Assert(id != null); + Contract.Assert(readContext != null); + IList properties = new List(); + if (readContext.Request == null) + { + return properties; + } + + ODataPath odataPath = GetODataPath(id.OriginalString, readContext); + if (odataPath != null) + { + KeySegment keySegment = odataPath.OfType().LastOrDefault(); + + if (keySegment != null) + { + foreach (KeyValuePair key in keySegment.Keys) + { + properties.Add(new ODataProperty + { + Name = key.Key, + Value = key.Value + }); + } + } + } + + return properties; + } + + private static ODataPath GetODataPath(string id, ODataDeserializerContext readContext) + { + // should we just use ODataUriParser? + /* + try + { + return new ODataUriParser(readContext.Model, new Uri(id, UriKind.Relative)).ParsePath(); + } + catch + { + return null; + } + */ + + try + { + Routing.IODataPathHandler pathHandler = readContext.InternalRequest.PathHandler; + IWebApiRequestMessage internalRequest = readContext.InternalRequest; + IWebApiUrlHelper urlHelper = readContext.InternalUrlHelper; + + string serviceRoot = urlHelper.CreateODataLink( + internalRequest.Context.RouteName, + internalRequest.PathHandler, + new List()); + ODataPath odataPath = pathHandler.Parse(serviceRoot, id, internalRequest.RequestContainer).Path; + + return odataPath; + } + catch (Exception) + { + return null; + } + } + + private static void ApplyODataIDContainer(object resource, ODataResourceWrapper resourceWrapper, + ODataDeserializerContext readContext) + { + Routing.ODataPath path = readContext.Path; + if (path == null) + { + return; + } + + IEdmEntityType entityType = path.EdmType.AsElementType() as IEdmEntityType; + + if (entityType != null) + { + //Setting Odataid , for POCO classes, as a property in the POCO object itself(if user has OdataIDContainer property), + //for Delta and EdmEntity object setting as an added property ODataIdcontianer in those classes + + ODataPath ODataPath = new ODataPath(path.Segments); + + // if there is no Id on the resource, try to compute one from path + if (resourceWrapper.ResourceBase.Id == null) + { + ODataUri odataUri = new ODataUri { Path = ODataPath }; + resourceWrapper.ResourceBase.Id = odataUri.BuildUri(ODataUrlKeyDelimiter.Parentheses); + } + + if (resourceWrapper.ResourceBase.Id != null) + { + string odataId = resourceWrapper.ResourceBase.Id.OriginalString; + + IODataIdContainer container = new ODataIdContainer(); + container.ODataId = odataId; + + if (resource is EdmEntityObject edmObject) + { + edmObject.ODataIdContainer = container; + edmObject.ODataPath = ODataPath; + } + else if (resource is IDeltaSetItem deltasetItem) + { + deltasetItem.ODataIdContainer = container; + deltasetItem.ODataPath = ODataPath; + } + else + { + PropertyInfo containerPropertyInfo = EdmLibHelpers.GetClrType(entityType, readContext.Model).GetProperties().Where(x => x.PropertyType == typeof(IODataIdContainer)).FirstOrDefault(); + if (containerPropertyInfo != null) + { + IODataIdContainer resourceContainer = containerPropertyInfo.GetValue(resource) as IODataIdContainer; + if (resourceContainer != null) + { + containerPropertyInfo.SetValue(resource, resourceContainer); + } + else + { + containerPropertyInfo.SetValue(resource, container); + } + } + } + } + } + } + + private static Routing.ODataPath ApplyIdToPath(ODataDeserializerContext readContext, ODataResourceWrapper resourceWrapper) + { + // If an odata.id is provided, try to parse it as an OData Url. + // This could fail (as the id is not required to be a valid OData Url) + // in which case we fall back to building the path based on the current path and segments. + if (resourceWrapper.ResourceBase.Id != null) + { + try + { + IWebApiRequestMessage internalRequest = readContext.InternalRequest; + IWebApiUrlHelper urlHelper = readContext.InternalUrlHelper; + + string serviceRoot = urlHelper.CreateODataLink( + internalRequest.Context.RouteName, + internalRequest.PathHandler, + new List()); + + ODataUriParser parser = new ODataUriParser(readContext.Model, new Uri(serviceRoot), resourceWrapper.ResourceBase.Id); + + ODataPath odataPath = parser.ParsePath(); + if (odataPath != null) + { + return new Routing.ODataPath(odataPath); + } + } + catch + { + }; + } + + Routing.ODataPath path = readContext.Path; + + if(path == null) + { + return null; + } + + IEdmEntityType entityType = path.EdmType.AsElementType() as IEdmEntityType; + + if (entityType != null && path.EdmType.TypeKind == EdmTypeKind.Collection) + { + // create the uri for the current object, using path and key values + List> keys = new List>(); + foreach (IEdmStructuralProperty keyProperty in entityType.Key()) + { + string keyName = keyProperty.Name; + ODataProperty property = resourceWrapper.ResourceBase.Properties.Where(p => p.Name == keyName).FirstOrDefault(); + if (property == null && !readContext.DisableCaseInsensitiveRequestPropertyBinding) + { + //try case insensitive + List candidates = resourceWrapper.ResourceBase.Properties.Where(p => String.Equals(p.Name, keyName, StringComparison.InvariantCultureIgnoreCase)).ToList(); + property = candidates.Count == 1 ? candidates.First() : null; + } + + object keyValue = property?.Value; + if (keyValue == null) + { + // Note: may be null if the payload did not include key values, + // but still need to add the key so the path is semantically correct. + // Key value type is not validated, so just use string. + // Consider adding tests to ODL to ensure we don't validate key property type in future. + keyValue = "Null"; + } + + keys.Add(new KeyValuePair(keyName, keyValue)); + } + + KeySegment keySegment = new KeySegment(keys, entityType, path.NavigationSource); + return AppendToPath(path, keySegment); + } + + return path; } /// @@ -373,7 +785,7 @@ public virtual void ApplyStructuralProperties(object resource, ODataResourceWrap throw Error.ArgumentNull("resourceWrapper"); } - foreach (ODataProperty property in resourceWrapper.Resource.Properties) + foreach (ODataProperty property in resourceWrapper.ResourceBase.Properties) { ApplyStructuralProperty(resource, property, structuredType, readContext); } @@ -394,7 +806,7 @@ public virtual void ApplyInstanceAnnotations(object resource, ODataResourceWrapp throw Error.ArgumentNull("resourceWrapper"); } - DeserializationHelpers.ApplyInstanceAnnotations(resource, structuredType, resourceWrapper.Resource,DeserializerProvider, readContext); + DeserializationHelpers.ApplyInstanceAnnotations(resource, structuredType, resourceWrapper.ResourceBase, DeserializerProvider, readContext); } /// @@ -406,7 +818,7 @@ public virtual void ApplyInstanceAnnotations(object resource, ODataResourceWrapp /// The deserializer context. public virtual void ApplyStructuralProperty(object resource, ODataProperty structuralProperty, IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext) - { + { if (resource == null) { throw Error.ArgumentNull("resource"); @@ -426,6 +838,7 @@ private void ApplyResourceProperties(object resource, ODataResourceWrapper resou ApplyStructuralProperties(resource, resourceWrapper, structuredType, readContext); ApplyNestedProperties(resource, resourceWrapper, structuredType, readContext); ApplyInstanceAnnotations(resource, resourceWrapper, structuredType, readContext); + ApplyODataIDContainer(resource, resourceWrapper, readContext); } private void ApplyResourceInNestedProperty(IEdmProperty nestedProperty, object resource, @@ -435,17 +848,6 @@ private void ApplyResourceInNestedProperty(IEdmProperty nestedProperty, object r Contract.Assert(resource != null); Contract.Assert(readContext != null); - if (readContext.IsDeltaOfT) - { - IEdmNavigationProperty navigationProperty = nestedProperty as IEdmNavigationProperty; - if (navigationProperty != null) - { - string message = Error.Format(SRResources.CannotPatchNavigationProperties, navigationProperty.Name, - navigationProperty.DeclaringEntityType().FullName()); - throw new ODataException(message); - } - } - object value = ReadNestedResourceInline(resourceWrapper, nestedProperty.Type, readContext); // First resolve Data member alias or annotation, then set the regular @@ -464,7 +866,7 @@ private void ApplyDynamicResourceInNestedProperty(string propertyName, object re object value = null; if (resourceWrapper != null) { - IEdmSchemaType elementType = readContext.Model.FindDeclaredType(resourceWrapper.Resource.TypeName); + IEdmSchemaType elementType = readContext.Model.FindDeclaredType(resourceWrapper.ResourceBase.TypeName); IEdmTypeReference edmTypeReference = elementType.ToEdmTypeReference(true); value = ReadNestedResourceInline(resourceWrapper, edmTypeReference, readContext); @@ -492,18 +894,11 @@ private object ReadNestedResourceInline(ODataResourceWrapper resourceWrapper, IE IEdmStructuredTypeReference structuredType = edmType.AsStructured(); - var nestedReadContext = new ODataDeserializerContext - { - Path = readContext.Path, - Model = readContext.Model, - Request = readContext.Request, - }; - - Type clrType = null; + Type clrType; if (readContext.IsUntyped) { clrType = structuredType.IsEntity() - ? typeof(EdmEntityObject) + ? (readContext.IsDeltaEntity ? (readContext.IsDeltaDeletedEntity ? typeof(EdmDeltaDeletedEntityObject) : typeof(EdmDeltaEntityObject)) : typeof(EdmEntityObject)) : typeof(EdmComplexObject); } else @@ -517,50 +912,40 @@ private object ReadNestedResourceInline(ODataResourceWrapper resourceWrapper, IE } } - nestedReadContext.ResourceType = readContext.IsDeltaOfT + ODataDeserializerContext nestedContext = BuildNestedContextFromCurrentContext(readContext, null, readContext.IsDeltaOfT ? typeof(Delta<>).MakeGenericType(clrType) - : clrType; - return deserializer.ReadInline(resourceWrapper, edmType, nestedReadContext); + : clrType); + + return deserializer.ReadInline(resourceWrapper, edmType, nestedContext); } private void ApplyResourceSetInNestedProperty(IEdmProperty nestedProperty, object resource, - ODataResourceSetWrapper resourceSetWrapper, ODataDeserializerContext readContext) + ODataResourceSetWrapperBase resourceSetWrapper, ODataDeserializerContext readContext) { Contract.Assert(nestedProperty != null); Contract.Assert(resource != null); Contract.Assert(readContext != null); - if (readContext.IsDeltaOfT) - { - IEdmNavigationProperty navigationProperty = nestedProperty as IEdmNavigationProperty; - if (navigationProperty != null) - { - string message = Error.Format(SRResources.CannotPatchNavigationProperties, navigationProperty.Name, - navigationProperty.DeclaringEntityType().FullName()); - throw new ODataException(message); - } - } - object value = ReadNestedResourceSetInline(resourceSetWrapper, nestedProperty.Type, readContext); string propertyName = EdmLibHelpers.GetClrPropertyName(nestedProperty, readContext.Model); - DeserializationHelpers.SetCollectionProperty(resource, nestedProperty, value, propertyName); + DeserializationHelpers.SetCollectionProperty(resource, nestedProperty, value, propertyName, resourceSetWrapper.ResourceSetType == ResourceSetType.DeltaResourceSet); } private void ApplyDynamicResourceSetInNestedProperty(string propertyName, object resource, IEdmStructuredTypeReference structuredType, - ODataResourceSetWrapper resourceSetWrapper, ODataDeserializerContext readContext) + ODataResourceSetWrapperBase resourceSetWrapper, ODataDeserializerContext readContext) { Contract.Assert(resource != null); Contract.Assert(readContext != null); - if (String.IsNullOrEmpty(resourceSetWrapper.ResourceSet.TypeName)) + if (String.IsNullOrEmpty(resourceSetWrapper.ResourceSetBase.TypeName)) { string message = Error.Format(SRResources.DynamicResourceSetTypeNameIsRequired, propertyName); throw new ODataException(message); } string elementTypeName = - DeserializationHelpers.GetCollectionElementTypeName(resourceSetWrapper.ResourceSet.TypeName, + DeserializationHelpers.GetCollectionElementTypeName(resourceSetWrapper.ResourceSetBase.TypeName, isNested: false); IEdmSchemaType elementType = readContext.Model.FindDeclaredType(elementTypeName); @@ -587,12 +972,12 @@ private void ApplyDynamicResourceSetInNestedProperty(string propertyName, object result, collectionType, readContext.Model); } - private object ReadNestedResourceSetInline(ODataResourceSetWrapper resourceSetWrapper, IEdmTypeReference edmType, - ODataDeserializerContext readContext) + private object ReadNestedResourceSetInline(ODataResourceSetWrapperBase resourceSetWrapper, IEdmTypeReference edmType, + ODataDeserializerContext nestedReadContext) { Contract.Assert(resourceSetWrapper != null); Contract.Assert(edmType != null); - Contract.Assert(readContext != null); + Contract.Assert(nestedReadContext != null); ODataEdmTypeDeserializer deserializer = DeserializerProvider.GetEdmTypeDeserializer(edmType); if (deserializer == null) @@ -601,18 +986,13 @@ private object ReadNestedResourceSetInline(ODataResourceSetWrapper resourceSetWr } IEdmStructuredTypeReference structuredType = edmType.AsCollection().ElementType().AsStructured(); - var nestedReadContext = new ODataDeserializerContext - { - Path = readContext.Path, - Model = readContext.Model, - Request = readContext.Request, - }; - if (readContext.IsUntyped) + if (nestedReadContext.IsUntyped) { if (structuredType.IsEntity()) { - nestedReadContext.ResourceType = typeof(EdmEntityObjectCollection); + nestedReadContext.ResourceType = (nestedReadContext.IsDeltaOfT && resourceSetWrapper.ResourceSetType == ResourceSetType.DeltaResourceSet) ? + typeof(EdmChangedObjectCollection) : typeof(EdmEntityObjectCollection); } else { @@ -621,7 +1001,7 @@ private object ReadNestedResourceSetInline(ODataResourceSetWrapper resourceSetWr } else { - Type clrType = EdmLibHelpers.GetClrType(structuredType, readContext.Model); + Type clrType = EdmLibHelpers.GetClrType(structuredType, nestedReadContext.Model); if (clrType == null) { @@ -629,7 +1009,8 @@ private object ReadNestedResourceSetInline(ODataResourceSetWrapper resourceSetWr Error.Format(SRResources.MappingDoesNotContainResourceType, structuredType.FullName())); } - nestedReadContext.ResourceType = typeof(List<>).MakeGenericType(clrType); + nestedReadContext.ResourceType = (nestedReadContext.IsDeltaOfT && resourceSetWrapper.ResourceSetType == ResourceSetType.DeltaResourceSet) + ? typeof(DeltaSet<>).MakeGenericType(clrType) : typeof(List<>).MakeGenericType(clrType); } return deserializer.ReadInline(resourceSetWrapper, edmType, nestedReadContext); @@ -673,4 +1054,4 @@ private static IEdmNavigationSource GetNavigationSource(IEdmStructuredTypeRefere return navigationSource; } } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceSetDeserializer.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceSetDeserializer.cs index 1fe34b9bc0..27aa8972e8 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceSetDeserializer.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceSetDeserializer.cs @@ -7,6 +7,8 @@ using System; using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics.Contracts; using System.Linq; using System.Reflection; @@ -52,7 +54,7 @@ public override object Read(ODataMessageReader messageReader, Type type, ODataDe throw Error.Argument("edmType", SRResources.ArgumentMustBeOfType, EdmTypeKind.Complex + " or " + EdmTypeKind.Entity); } - ODataReader resourceSetReader = messageReader.CreateODataResourceSetReader(); + ODataReader resourceSetReader = readContext.IsChangedObjectCollection ? messageReader.CreateODataDeltaResourceSetReader() : messageReader.CreateODataResourceSetReader(); object resourceSet = resourceSetReader.ReadResourceOrResourceSet(); return ReadInline(resourceSet, edmType, readContext); } @@ -74,12 +76,13 @@ public override async Task ReadAsync(ODataMessageReader messageReader, T throw Error.Argument("edmType", SRResources.ArgumentMustBeOfType, EdmTypeKind.Complex + " or " + EdmTypeKind.Entity); } - ODataReader resourceSetReader = await messageReader.CreateODataResourceSetReaderAsync(); + ODataReader resourceSetReader = readContext.IsChangedObjectCollection ? await messageReader.CreateODataDeltaResourceSetReaderAsync() : await messageReader.CreateODataResourceSetReaderAsync(); object resourceSet = await resourceSetReader.ReadResourceOrResourceSetAsync(); return ReadInline(resourceSet, edmType, readContext); } /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling")] public sealed override object ReadInline(object item, IEdmTypeReference edmType, ODataDeserializerContext readContext) { if (item == null) @@ -97,20 +100,46 @@ public sealed override object ReadInline(object item, IEdmTypeReference edmType, throw Error.Argument("edmType", SRResources.TypeMustBeResourceSet, edmType.ToTraceString()); } - ODataResourceSetWrapper resourceSet = item as ODataResourceSetWrapper; + ODataResourceSetWrapperBase resourceSet = item as ODataResourceSetWrapperBase; if (resourceSet == null) { - throw Error.Argument("item", SRResources.ArgumentMustBeOfType, typeof(ODataResourceSetWrapper).Name); + throw Error.Argument("item", SRResources.ArgumentMustBeOfType, typeof(ODataResourceSetWrapperBase).Name); } // Recursion guard to avoid stack overflows RuntimeHelpers.EnsureSufficientExecutionStack(); - IEdmStructuredTypeReference elementType = edmType.AsCollection().ElementType().AsStructured(); + IEdmStructuredTypeReference elementType = edmType.AsCollection().ElementType().AsStructured(); IEnumerable result = ReadResourceSet(resourceSet, elementType, readContext); - if (result != null && elementType.IsComplex()) + + //Handle Delta requests to create EdmChangedObjectCollection + if (resourceSet.ResourceSetType == ResourceSetType.DeltaResourceSet) { + IEdmEntityType actualType = elementType.AsEntity().Definition as IEdmEntityType; + + if (readContext.IsUntyped) + { + EdmChangedObjectCollection edmCollection = new EdmChangedObjectCollection(actualType); + + foreach (IEdmChangedObject changedObject in result) + { + edmCollection.Add(changedObject); + } + + return edmCollection; + } + else + { + ICollection deltaSet; + deltaSet = CreateDeltaSet(actualType.Key().Select(x=>x.Name).ToList(), readContext, elementType, result); + + return deltaSet; + } + } + + if (result != null && elementType.IsComplex()) + { if (readContext.IsUntyped) { EdmComplexObjectCollection complexCollection = new EdmComplexObjectCollection(edmType.AsCollection()); @@ -128,13 +157,29 @@ public sealed override object ReadInline(object item, IEdmTypeReference edmType, IEnumerable; return castedResult; } - } + } else { return result; } } + private static ICollection CreateDeltaSet(IList keys, ODataDeserializerContext readContext, IEdmStructuredTypeReference elementType, IEnumerable result) + { + ICollection deltaSet; + Type type = EdmLibHelpers.GetClrType(elementType, readContext.Model); + Type changedObjCollType = typeof(DeltaSet<>).MakeGenericType(type); + + deltaSet = Activator.CreateInstance(changedObjCollType, keys) as ICollection; + + foreach (IDeltaSetItem changedObject in result) + { + deltaSet.Add(changedObject); + } + + return deltaSet; + } + /// /// Deserializes the given under the given . /// @@ -142,7 +187,7 @@ public sealed override object ReadInline(object item, IEdmTypeReference edmType, /// The deserializer context. /// The element type of the resource set being read. /// The deserialized resource set object. - public virtual IEnumerable ReadResourceSet(ODataResourceSetWrapper resourceSet, IEdmStructuredTypeReference elementType, ODataDeserializerContext readContext) + public virtual IEnumerable ReadResourceSet(ODataResourceSetWrapperBase resourceSet, IEdmStructuredTypeReference elementType, ODataDeserializerContext readContext) { ODataEdmTypeDeserializer deserializer = DeserializerProvider.GetEdmTypeDeserializer(elementType); if (deserializer == null) @@ -151,10 +196,38 @@ public virtual IEnumerable ReadResourceSet(ODataResourceSetWrapper resourceSet, Error.Format(SRResources.TypeCannotBeDeserialized, elementType.FullName())); } - foreach (ODataResourceWrapper resourceWrapper in resourceSet.Resources) + //Ideally we don't need to special case ResourceSetType.ResourceSet, since the code that handles a deltaresourcesetwrapper will also handle a ResourceSetWrapper, + //but it may be more efficient for the common case. + + if (resourceSet.ResourceSetType == ResourceSetType.ResourceSet) { - yield return deserializer.ReadInline(resourceWrapper, elementType, readContext); + foreach (ODataResourceWrapper resourceWrapper in resourceSet.Resources) + { + yield return deserializer.ReadInline(resourceWrapper, elementType, readContext); + } + } + else + { + Type clrType = EdmLibHelpers.GetClrType(elementType, readContext.Model); + + foreach (ODataResourceWrapper resourceWrapper in resourceSet.Resources) + { + if (readContext.IsUntyped) + { + readContext.ResourceType = resourceWrapper.ResourceBase is ODataDeletedResource ? typeof(EdmDeltaDeletedEntityObject) : typeof(EdmEntityObject); + } + else + { + readContext.ResourceType = resourceWrapper.ResourceBase is ODataDeletedResource ? typeof(DeltaDeletedEntityObject<>).MakeGenericType(clrType) : typeof(Delta<>).MakeGenericType(clrType); + } + + if (resourceWrapper != null) + { + yield return deserializer.ReadInline(resourceWrapper, elementType, readContext); + } + } } } + } } diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceSetWrapper.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceSetWrapper.cs index 4305b4312f..fca2a4cc6b 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceSetWrapper.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceSetWrapper.cs @@ -5,7 +5,9 @@ // //------------------------------------------------------------------------------ +using System; using System.Collections.Generic; +using Microsoft.AspNet.OData.Common; using Microsoft.OData; namespace Microsoft.AspNet.OData.Formatter.Deserialization @@ -13,32 +15,24 @@ namespace Microsoft.AspNet.OData.Formatter.Deserialization /// /// Encapsulates an and the 's that are part of it. /// - public sealed class ODataResourceSetWrapper : ODataItemBase + public sealed class ODataResourceSetWrapper : ODataResourceSetWrapperBase { /// /// Initializes a new instance of . /// - /// The wrapped item. + /// The wrapped item. public ODataResourceSetWrapper(ODataResourceSet item) : base(item) { - Resources = new List(); + ResourceSet = item; } + internal override ResourceSetType ResourceSetType => ResourceSetType.ResourceSet; + /// /// Gets the wrapped . /// - public ODataResourceSet ResourceSet - { - get - { - return Item as ODataResourceSet; - } - } + public ODataResourceSet ResourceSet { get; } - /// - /// Gets the nested resources of this ResourceSet. - /// - public IList Resources { get; private set; } } } diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceSetWrapperBase.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceSetWrapperBase.cs new file mode 100644 index 0000000000..3b0f2823b1 --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceSetWrapperBase.cs @@ -0,0 +1,45 @@ +//----------------------------------------------------------------------------- +// +// 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.OData; + +namespace Microsoft.AspNet.OData.Formatter.Deserialization +{ + /// + /// Encapsulates an or and the 's that are part of it. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1012:AbstractTypesShouldNotHaveConstructors")] + public abstract class ODataResourceSetWrapperBase : ODataItemBase + { + /// + /// To determine the type of Resource Set + /// + internal abstract ResourceSetType ResourceSetType { get; } + + /// + /// Initializes a new instance of . + /// + /// The wrapped item. + public ODataResourceSetWrapperBase(ODataResourceSetBase item) + : base(item) + { + Resources = new List(); + ResourceSetBase = item; + } + + /// + /// Gets the wrapped . + /// + public ODataResourceSetBase ResourceSetBase { get; } + + /// + /// Gets the members of this ResourceSet. + /// + public IList Resources { get; } + } +} diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceWrapper.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceWrapper.cs index 0500d972b9..7a5949a27b 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceWrapper.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceWrapper.cs @@ -5,7 +5,9 @@ // //------------------------------------------------------------------------------ +using System; using System.Collections.Generic; +using Microsoft.AspNet.OData.Common; using Microsoft.OData; namespace Microsoft.AspNet.OData.Formatter.Deserialization @@ -14,20 +16,27 @@ namespace Microsoft.AspNet.OData.Formatter.Deserialization /// Encapsulates an and the inner nested resource infos. /// public sealed class ODataResourceWrapper : ODataItemBase - { + { /// /// Initializes a new instance of . /// /// The wrapped item. - public ODataResourceWrapper(ODataResource item) + public ODataResourceWrapper(ODataResourceBase item) : base(item) { NestedResourceInfos = new List(); + ResourceBase = item; } + + /// + /// Gets the wrapped . + /// + public ODataResourceBase ResourceBase {get;} /// - /// Gets the wrapped . + /// Gets the wrapped . This will return null for deleted resources. /// + [Obsolete("Please use ResourceBase instead")] public ODataResource Resource { get diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ResourceSetType.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ResourceSetType.cs new file mode 100644 index 0000000000..450d946f86 --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ResourceSetType.cs @@ -0,0 +1,25 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.AspNet.OData.Formatter.Deserialization +{ + /// + /// Enum to determine the type of Resource Set + /// + internal enum ResourceSetType + { + /// + /// A normal ResourceSet + /// + ResourceSet, + + /// + /// A Delta Resource Set + /// + DeltaResourceSet + } +} diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/EdmLibHelpers.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/EdmLibHelpers.cs index fb3f83110f..cdeb02d13a 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/EdmLibHelpers.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/EdmLibHelpers.cs @@ -6,6 +6,7 @@ //------------------------------------------------------------------------------ using System; +using System.Collections; using System.Collections.Generic; #if NETFX // System.Data.Linq.Binary is only supported in the AspNet version. using System.Data.Linq; @@ -134,13 +135,23 @@ private static IEdmType GetEdmType(IEdmModel edmModel, Type clrType, bool testCo { if (testCollections) { + Type entityType; + + if (IsDeltaSetWrapper(clrType, out entityType)) + { + IEdmType elementType = GetEdmType(edmModel, entityType, testCollections: false); + if (elementType != null) + { + return new EdmCollectionType(elementType.ToEdmTypeReference(IsNullable(entityType))); + } + } + Type enumerableOfT = ExtractGenericInterface(clrType, typeof(IEnumerable<>)); if (enumerableOfT != null) { Type elementClrType = enumerableOfT.GetGenericArguments()[0]; - // IEnumerable> is a collection of T. - Type entityType; + // IEnumerable> is a collection of T. if (IsSelectExpandWrapper(elementClrType, out entityType)) { elementClrType = entityType; @@ -977,14 +988,17 @@ internal static IEdmTypeReference GetExpectedPayloadType(Type type, ODataPath pa { IEdmTypeReference expectedPayloadType = null; - if (typeof(IEdmObject).IsAssignableFrom(type)) + if (typeof(IEdmObject).IsAssignableFrom(type) || typeof(IDeltaSet).IsAssignableFrom(type)) { // typeless mode. figure out the expected payload type from the OData Path. IEdmType edmType = path.EdmType; if (edmType != null) { expectedPayloadType = EdmLibHelpers.ToEdmTypeReference(edmType, isNullable: false); - if (expectedPayloadType.TypeKind() == EdmTypeKind.Collection) + + //This loop should execute only if its not a type of edmchangedobjectcollection, In case of edmchangedobjectcollection, + //Expectedpayloadtype should not be of elementytype, but of collection. + if (expectedPayloadType.TypeKind() == EdmTypeKind.Collection && !(typeof(ICollection).IsAssignableFrom(type) || typeof(IDeltaSet).IsAssignableFrom(type))) { IEdmTypeReference elementType = expectedPayloadType.AsCollection().ElementType(); if (elementType.IsEntity()) @@ -1117,6 +1131,7 @@ private static IEdmPrimitiveType GetPrimitiveType(EdmPrimitiveTypeKind primitive } private static bool IsSelectExpandWrapper(Type type, out Type entityType) => IsTypeWrapper(typeof(SelectExpandWrapper<>), type, out entityType); + private static bool IsDeltaSetWrapper(Type type, out Type entityType) => IsTypeWrapper(typeof(DeltaSet<>), type, out entityType); internal static bool IsComputeWrapper(Type type, out Type entityType) => IsTypeWrapper(typeof(ComputeWrapper<>), type, out entityType); diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/ODataInputFormatterHelper.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/ODataInputFormatterHelper.cs index 8ecd8ed6ce..334056d1c6 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/ODataInputFormatterHelper.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/ODataInputFormatterHelper.cs @@ -80,8 +80,9 @@ internal static object ReadFromStream( oDataReaderSettings.BaseUri = baseAddress; oDataReaderSettings.Validations = oDataReaderSettings.Validations & ~ValidationKinds.ThrowOnUndeclaredPropertyForNonOpenType; oDataReaderSettings.Version = version; + oDataReaderSettings.MaxProtocolVersion = version; - IODataRequestMessage oDataRequestMessage = getODataRequestMessage(); + IODataRequestMessage oDataRequestMessage = getODataRequestMessage(); string preferHeader = RequestPreferenceHelpers.GetRequestPreferHeader(internalRequest.Headers); string annotationFilter = null; diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/ODataModelBinderConverter.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/ODataModelBinderConverter.cs index ac70bbd32d..f7eea7088c 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/ODataModelBinderConverter.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/ODataModelBinderConverter.cs @@ -337,7 +337,7 @@ private static object ConvertResource(ODataMessageReader oDataMessageReader, IEd if (edmTypeReference.IsEntity()) { IEdmEntityTypeReference entityType = edmTypeReference.AsEntity(); - return CovertResourceId(value, topLevelResource.Resource, entityType, readContext); + return CovertResourceId(value, topLevelResource.ResourceBase, entityType, readContext); } return value; @@ -350,14 +350,14 @@ private static IEnumerable CovertResourceSetIds(IEnumerable sources, ODataResour int i = 0; foreach (object item in sources) { - object newItem = CovertResourceId(item, resourceSet.Resources[i].Resource, entityTypeReference, + object newItem = CovertResourceId(item, resourceSet.Resources[i].ResourceBase, entityTypeReference, readContext); i++; yield return newItem; } } - private static object CovertResourceId(object source, ODataResource resource, + private static object CovertResourceId(object source, ODataResourceBase resource, IEdmEntityTypeReference entityTypeReference, ODataDeserializerContext readContext) { Contract.Assert(resource != null); 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..f814563e25 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..e0e0aafb9e 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) { - 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; - switch (edmChangedObject.DeltaKind) + if (deltaSetItem == null) + { + throw new SerializationException(Error.Format(SRResources.CannotWriteType, GetType().Name, enumerable.GetType().FullName)); + } + + deltaEntityKind = deltaSetItem.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) + { + 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: 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); - - if (deletedResource != null) + ODataResourceSerializer serializer = SerializerProvider.GetEdmTypeSerializer(_elementType) as ODataResourceSerializer; + ResourceContext resourceContext = serializer.GetResourceContext(graph, writeContext); + SelectExpandNode selectExpandNode = serializer.CreateSelectExpandNode(resourceContext); + + 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,26 +593,46 @@ 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) + IEdmNavigationSource 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)); + } - Uri id = StringToUri(edmDeltaDeletedEntity.Id); - ODataDeletedResource deletedResource = new ODataDeletedResource(id, edmDeltaDeletedEntity.Reason); + deletedResource.Id = StringToUri(edmDeltaDeletedEntity.Id??string.Empty); + deletedResource.Reason = edmDeltaDeletedEntity.Reason; + navigationSource = edmDeltaDeletedEntity.NavigationSource; + } + else + { + IDeltaDeletedEntityObject deltaDeletedEntity = graph as IDeltaDeletedEntityObject; + if (deltaDeletedEntity == null) + { + throw new SerializationException(Error.Format(SRResources.CannotWriteType, GetType().Name, graph.GetType().FullName)); + } - if (edmDeltaDeletedEntity.NavigationSource != null) + deletedResource.Id = deltaDeletedEntity.Id; + deletedResource.Reason = deltaDeletedEntity.Reason; + navigationSource = deltaDeletedEntity.NavigationSource; + } + + if (navigationSource != null) { ODataResourceSerializationInfo serializationInfo = new ODataResourceSerializationInfo { - NavigationSourceName = edmDeltaDeletedEntity.NavigationSource.Name + NavigationSourceName = navigationSource.Name }; deletedResource.SetSerializationInfo(serializationInfo); } - + return deletedResource; } diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataResourceSerializer.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataResourceSerializer.cs index 8d4c2c5a92..3d41c1a261 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; @@ -202,12 +203,13 @@ private void WriteDeltaResource(object graph, ODataWriter writer, ODataSerialize { writer.WriteStart(resource); WriteDeltaComplexProperties(selectExpandNode, resourceContext, 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. @@ -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(); } @@ -688,6 +737,14 @@ public virtual SelectExpandNode CreateSelectExpandNode(ResourceContext resourceC /// 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) { throw Error.ArgumentNull("selectExpandNode"); @@ -700,6 +757,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 +772,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 +839,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 +873,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,7 +896,7 @@ 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); @@ -931,102 +1023,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); + } - if (edmTypeSerializer != null) + IDeltaSetItem deltaitem = resourceContext.ResourceInstance as IDeltaSetItem; + + 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 +1121,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 +1142,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 +1379,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 +1396,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 +1616,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 +1653,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 +1828,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,11 +1846,13 @@ 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); + } } return properties; @@ -1996,7 +2170,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 +2195,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 +2269,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..bde531d415 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,34 @@ 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 +330,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 +347,15 @@ 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..4c3cdecf7b --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataSerializerHelper.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 System; +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/IDeltaDeletedEntityObject.cs b/src/Microsoft.AspNet.OData.Shared/IDeltaDeletedEntityObject.cs new file mode 100644 index 0000000000..02ff2441aa --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/IDeltaDeletedEntityObject.cs @@ -0,0 +1,35 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System; +using Microsoft.OData; +using Microsoft.OData.Edm; + +namespace Microsoft.AspNet.OData +{ + /// + /// Base interface to represented a typed deleted entity object + /// + public interface IDeltaDeletedEntityObject + { + /// + /// The id of the deleted entity (same as the odata.id returned or computed when calling GET on resource), which may be absolute or relative. + /// + Uri Id { get; set; } + + /// + /// Optional. Either deleted, if the entity was deleted (destroyed), or changed if the entity was removed from membership in the result (i.e., due to a data change). + /// + DeltaDeletedEntryReason? Reason { get; set; } + + /// + /// The navigation source of the deleted entity. If null, then the deleted entity is from the current feed. + /// + IEdmNavigationSource NavigationSource { get; set; } + + } +} diff --git a/src/Microsoft.AspNet.OData.Shared/IDeltaSet.cs b/src/Microsoft.AspNet.OData.Shared/IDeltaSet.cs new file mode 100644 index 0000000000..33c7a25214 --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/IDeltaSet.cs @@ -0,0 +1,20 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.AspNet.OData +{ + /// + /// Basic interface to reperesent a deltaset which is a collection of Deltas. + /// This is being implemented by DeltaSet. Since its being implementd by a generic type and + /// since we need to check in a few places(like deserializer) where the object is a DeltaSet and the {TStructuralType} is not available, + /// we need a marker interface which can be used in these checks. + /// + public interface IDeltaSet + { + + } +} diff --git a/src/Microsoft.AspNet.OData.Shared/IDeltaSetItem.cs b/src/Microsoft.AspNet.OData.Shared/IDeltaSetItem.cs new file mode 100644 index 0000000000..b8bff3f003 --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/IDeltaSetItem.cs @@ -0,0 +1,42 @@ +//----------------------------------------------------------------------------- +// +// 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.UriParser; +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; + +namespace Microsoft.AspNet.OData +{ + /// + /// Basic Interface for representing a delta item like delta, deletedentity etc + /// + public interface IDeltaSetItem + { + /// + /// Entry or Deleted Entry for Delta Set Item + /// + EdmDeltaEntityKind DeltaKind { get; } + + /// + /// Annotation container to hold Transient Instance Annotations + /// + IODataInstanceAnnotationContainer TransientInstanceAnnotationContainer { get; set; } + + /// + /// Container to hold ODataId + /// + IODataIdContainer ODataIdContainer { get; set; } + + /// + /// OData Path for the Item + /// + ODataPath ODataPath { get; set; } + } +} diff --git a/src/Microsoft.AspNet.OData.Shared/IODataAPIHandler.cs b/src/Microsoft.AspNet.OData.Shared/IODataAPIHandler.cs new file mode 100644 index 0000000000..b2051f4b97 --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/IODataAPIHandler.cs @@ -0,0 +1,19 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.AspNet.OData +{ + /// + /// Base Interface for ODataAPIHandler. + /// This is being implemented by ODataAPIHandler{TStructuralType} which has a method returning nested ODataApiHandler. + /// A generic empty interface is needed since the nestedpatch handler will be of different type. + /// + public interface IODataAPIHandler + { + + } +} 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 d071c08f42..4b881021d9 100644 --- a/src/Microsoft.AspNet.OData.Shared/Microsoft.AspNet.OData.Shared.projitems +++ b/src/Microsoft.AspNet.OData.Shared/Microsoft.AspNet.OData.Shared.projitems @@ -25,6 +25,7 @@ + @@ -53,12 +54,20 @@ + + + + + + + + @@ -71,8 +80,21 @@ + + + + + + + + + + + + + diff --git a/src/Microsoft.AspNet.OData.Shared/ODataAPIHandler.cs b/src/Microsoft.AspNet.OData.Shared/ODataAPIHandler.cs new file mode 100644 index 0000000000..d7ee5186a7 --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/ODataAPIHandler.cs @@ -0,0 +1,186 @@ +//----------------------------------------------------------------------------- +// +// 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.Linq; +using System.Reflection; +using Microsoft.OData.Edm; +using Microsoft.OData.UriParser; + +namespace Microsoft.AspNet.OData +{ + /// + /// Handler Class to handle users methods for create, delete and update. + /// This is the handler for data modification where there is a CLR type. + /// + public abstract class ODataAPIHandler: IODataAPIHandler where TStructuralType : class + { + /// + /// TryCreate method to create a new object. + /// + /// TheKey value pair of the objecct to be created. Optional + /// The created object (CLR or Typeless) + /// Any error message in case of an exception + /// The status of the TryCreate method + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#")] + public abstract ODataAPIResponseStatus TryCreate(IDictionary keyValues, out TStructuralType createdObject, out string errorMessage); + + /// + /// TryGet method which tries to get the Origignal object based on a keyvalues. + /// + /// Key value pair for the entity keys + /// Object to return + /// Any error message in case of an exception + /// The status of the TryGet method + public abstract ODataAPIResponseStatus TryGet(IDictionary keyValues, out TStructuralType originalObject, out string errorMessage); + + /// + /// TryDelete Method which will delete the object based on keyvalue pairs. + /// + /// + /// + /// The status of the TryGet method + public abstract ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage); + + /// + /// Get the ODataAPIHandler for the nested type + /// + /// Parent instance. + /// The name of the navigation property for the handler + /// The type of Nested ODataAPIHandler + public abstract IODataAPIHandler GetNestedHandler(TStructuralType parent, string navigationPropertyName); + + /// + /// The parent object + /// + internal TStructuralType ParentObject { get; set; } + + /// + /// Apply OdataId for a resource with OdataID container + /// + /// resource to apply odata id on + /// The model. + public virtual void UpdateLinkedObjects(TStructuralType resource, IEdmModel model) + { + if (resource != null && model != null) + { + this.ParentObject = resource; + CheckAndApplyODataId(resource, model); + } + } + + private ODataPath GetODataPath(string path, IEdmModel model) + { + ODataUriParser parser = new ODataUriParser(model, new Uri(path, UriKind.Relative)); + ODataPath odataPath = parser.ParsePath(); + + return odataPath; + } + + private void CheckAndApplyODataId(object obj, IEdmModel model) + { + Type type = obj.GetType(); + PropertyInfo property = type.GetProperties().FirstOrDefault(s => s.PropertyType == typeof(IODataIdContainer)); + if (property != null && property.GetValue(obj) is IODataIdContainer container && container != null) + { + ODataPath odataPath = GetODataPath(container.ODataId, model); + object res = ApplyODataIdOnContainer(obj, odataPath); + foreach (PropertyInfo prop in type.GetProperties()) + { + object resVal = prop.GetValue(res); + + if (resVal != null) + { + prop.SetValue(obj, resVal); + } + } + } + else + { + foreach (PropertyInfo prop in type.GetProperties().Where(p => !p.PropertyType.IsPrimitive)) + { + object propVal = prop.GetValue(obj); + if (propVal == null) + { + continue; + } + + if (propVal is IEnumerable lst) + { + foreach (object item in lst) + { + if (item.GetType().IsPrimitive) + { + break; + } + + CheckAndApplyODataId(item, model); + } + } + else + { + CheckAndApplyODataId(propVal, model); + } + } + } + } + + private object ApplyODataIdOnContainer(object obj, ODataPath odataPath) + { + KeySegment keySegment = odataPath.LastOrDefault() as KeySegment; + Dictionary keys = keySegment?.Keys.ToDictionary(x => x.Key, x => x.Value); + if (keys != null) + { + TStructuralType returnedObject; + string error; + if (this.ParentObject.Equals(obj)) + { + if (this.TryGet(keys, out returnedObject, out error) == ODataAPIResponseStatus.Success) + { + return returnedObject; + } + else + { + if (this.TryCreate(keys, out returnedObject, out error) == ODataAPIResponseStatus.Success) + { + return returnedObject; + } + else + { + return null; + } + } + } + else + { + IODataAPIHandler apiHandlerNested = this.GetNestedHandler(this.ParentObject, odataPath.LastSegment.Identifier); + object[] getParams = new object[] { keys, null, null }; + if (apiHandlerNested.GetType().GetMethod(nameof(TryGet)).Invoke(apiHandlerNested, getParams).Equals(ODataAPIResponseStatus.Success)) + { + return getParams[1]; + } + else + { + if (apiHandlerNested.GetType().GetMethod(nameof(TryCreate)).Invoke(apiHandlerNested, getParams).Equals(ODataAPIResponseStatus.Success)) + { + return getParams[1]; + } + else + { + return null; + } + } + } + } + + return null; + } + } +} diff --git a/src/Microsoft.AspNet.OData.Shared/ODataAPIHandlerFactory.cs b/src/Microsoft.AspNet.OData.Shared/ODataAPIHandlerFactory.cs new file mode 100644 index 0000000000..6c925271ee --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/ODataAPIHandlerFactory.cs @@ -0,0 +1,53 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System; +using Microsoft.OData.Edm; +using Microsoft.OData.UriParser; + +namespace Microsoft.AspNet.OData +{ + /// + /// Factory class for ODataAPIHandlers. + /// + public abstract class ODataAPIHandlerFactory + { + /// + /// Creates an instance of an ODataAPIHandlerFactory with the given model. + /// + /// The IEdmModel for the API Handler Factory. + protected ODataAPIHandlerFactory(IEdmModel model) + { + Model = model; + } + + /// + /// The IEdmModel for the Factory. + /// + public IEdmModel Model { get; private set; } + + /// + /// Get the handler depending on OData path. + /// + /// OData path corresponding to an odataid. + /// ODataAPIHandler for the specified OData path. + public abstract IODataAPIHandler GetHandler(ODataPath odataPath); + + /// + /// Get the handler based on the OData path uri string. + /// + /// OData path uri string. + /// ODataAPIHandler for the specified odata path uri string. + public IODataAPIHandler GetHandler(string path) + { + ODataUriParser parser = new ODataUriParser(this.Model, new Uri(path, UriKind.Relative)); + ODataPath odataPath = parser.ParsePath(); + + return this.GetHandler(odataPath); + } + } +} diff --git a/src/Microsoft.AspNet.OData.Shared/ODataAPIResponseStatus.cs b/src/Microsoft.AspNet.OData.Shared/ODataAPIResponseStatus.cs new file mode 100644 index 0000000000..3f6f0123d7 --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/ODataAPIResponseStatus.cs @@ -0,0 +1,28 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.AspNet.OData +{ + /// + /// Enum for Patch Status + /// + public enum ODataAPIResponseStatus + { + /// + /// Success Status + /// + Success, + /// + /// Failure Status + /// + Failure, + /// + /// Resource Not Found + /// + NotFound + } +} diff --git a/src/Microsoft.AspNet.OData.Shared/ODataEdmAPIHandlerFactory.cs b/src/Microsoft.AspNet.OData.Shared/ODataEdmAPIHandlerFactory.cs new file mode 100644 index 0000000000..674525fd99 --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/ODataEdmAPIHandlerFactory.cs @@ -0,0 +1,49 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System; +using Microsoft.OData.Edm; +using Microsoft.OData.UriParser; + +namespace Microsoft.AspNet.OData +{ + /// + /// Factory class for ODataAPIHandlers for typeless entities + /// + internal abstract class ODataEdmAPIHandlerFactory + { + protected ODataEdmAPIHandlerFactory(IEdmModel model) + { + Model = model; + } + + /// + /// The IEdmModel for the Factory. + /// + public IEdmModel Model { get; private set; } + + /// + /// Get the handler depending on OData path. + /// + /// OData path corresponding to an odataid. + /// ODataAPIHandler for the specified OData path. + public abstract EdmODataAPIHandler GetHandler(ODataPath odataPath); + + /// + /// Get the handler based on the OData path uri string. + /// + /// OData path uri string. + /// ODataAPIHandler for the specified odata path uri string. + public EdmODataAPIHandler GetHandler(string path) + { + ODataUriParser parser = new ODataUriParser(this.Model, new Uri(path, UriKind.Relative)); + ODataPath odataPath = parser.ParsePath(); + + return this.GetHandler(odataPath); + } + } +} diff --git a/src/Microsoft.AspNet.OData.Shared/ODataIdContainer.cs b/src/Microsoft.AspNet.OData.Shared/ODataIdContainer.cs new file mode 100644 index 0000000000..6db116c571 --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/ODataIdContainer.cs @@ -0,0 +1,29 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.AspNet.OData +{ + /// + /// Interface to hold ODataID in parsed format, it will be used by POCO objects as well as Delta{TStructuralType} + /// + public interface IODataIdContainer + { + /// + /// OdataId path string + /// + string ODataId { set; get; } + } + + /// + /// Default implementation of IOdataIdContainer + /// + public class ODataIdContainer : IODataIdContainer + { + /// + public string ODataId { set; get; } + } +} diff --git a/src/Microsoft.AspNet.OData.Shared/ResourceContext.cs b/src/Microsoft.AspNet.OData.Shared/ResourceContext.cs index 2950e17097..d0a2b504db 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,12 +207,21 @@ 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; } else - { + { IEdmTypeReference edmType = EdmObject.GetEdmType(); if (edmType == null) { diff --git a/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/EntitySetRoutingConvention.cs b/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/EntitySetRoutingConvention.cs index 4d85011cfe..38b775a6a4 100644 --- a/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/EntitySetRoutingConvention.cs +++ b/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/EntitySetRoutingConvention.cs @@ -46,6 +46,13 @@ internal static string SelectActionImpl(ODataPath odataPath, IWebApiControllerCo "Post" + entitySet.EntityType().Name, "Post"); } + else if (ODataRequestMethod.Patch == controllerContext.Request.Method) + { + // e.g. Try PatchCustomers first, then fall back to Patch action name + return actionMap.FindMatchingAction( + "Patch" + entitySet.Name, + "Patch"); + } } else if (odataPath.PathTemplate == "~/entityset/$count" && ODataRequestMethod.Get == controllerContext.Request.GetRequestMethodOrPreflightMethod()) diff --git a/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/NavigationRoutingConvention.cs b/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/NavigationRoutingConvention.cs index c2273693fb..591714dd27 100644 --- a/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/NavigationRoutingConvention.cs +++ b/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/NavigationRoutingConvention.cs @@ -50,9 +50,9 @@ internal static string SelectActionImpl(ODataPath odataPath, IWebApiControllerCo return null; } - // It is not valid to *Put/Patch" to any collection-valued navigation property. + // It is not valid to *Put" to any collection-valued navigation property. if (navigationProperty.TargetMultiplicity() == EdmMultiplicity.Many && - (ODataRequestMethod.Put == method || ODataRequestMethod.Patch == method)) + ODataRequestMethod.Put == method) { return null; } diff --git a/src/Microsoft.AspNet.OData.Shared/Routing/ODataPath.cs b/src/Microsoft.AspNet.OData.Shared/Routing/ODataPath.cs index 7a7eb799a1..2d5ded2382 100644 --- a/src/Microsoft.AspNet.OData.Shared/Routing/ODataPath.cs +++ b/src/Microsoft.AspNet.OData.Shared/Routing/ODataPath.cs @@ -114,5 +114,10 @@ public override string ToString() /// Gets the ODL path. /// public Semantic.ODataPath Path { get; internal set; } + + internal IList SegmentList + { + get { return _segments.ToList(); } + } } } diff --git a/src/Microsoft.AspNet.OData.Shared/TransientAnnotations.cs b/src/Microsoft.AspNet.OData.Shared/TransientAnnotations.cs new file mode 100644 index 0000000000..e8a04c79fa --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/TransientAnnotations.cs @@ -0,0 +1,17 @@ +//----------------------------------------------------------------------------- +// +// 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.Common; + +namespace Microsoft.AspNet.OData +{ + internal static class TransientAnnotations + { + internal static HashSet TransientAnnotationTerms = new HashSet() { SRResources.ContentID, SRResources.DataModificationException }; + } +} diff --git a/src/Microsoft.AspNet.OData.Shared/TypedDelta.cs b/src/Microsoft.AspNet.OData.Shared/TypedDelta.cs index 9ee9b11a6c..ad88b237bb 100644 --- a/src/Microsoft.AspNet.OData.Shared/TypedDelta.cs +++ b/src/Microsoft.AspNet.OData.Shared/TypedDelta.cs @@ -32,7 +32,7 @@ public abstract class TypedDelta : Delta /// True if it is a Delta generic type; false otherwise. internal static bool IsDeltaOfT(Type type) { - return type != null && TypeHelper.IsGenericType(type) && type.GetGenericTypeDefinition() == typeof(Delta<>); + return type != null && TypeHelper.IsGenericType(type) && typeof(Delta<>).IsAssignableFrom(type.GetGenericTypeDefinition()); } } } diff --git a/src/Microsoft.AspNet.OData/GlobalSuppressions.cs b/src/Microsoft.AspNet.OData/GlobalSuppressions.cs index 1d8ee02d50..32e5422a99 100644 --- a/src/Microsoft.AspNet.OData/GlobalSuppressions.cs +++ b/src/Microsoft.AspNet.OData/GlobalSuppressions.cs @@ -65,4 +65,88 @@ [assembly: SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Scope = "member", Target = "Microsoft.AspNet.OData.Builder.IODataInstanceAnnotationContainer.#GetAllResourceAnnotations()")] [assembly: SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Scope = "member", Target = "Microsoft.AspNet.OData.Builder.IODataInstanceAnnotationContainer.#GetResourceAnnotations()")] [assembly: SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Scope = "member", Target = "Microsoft.AspNet.OData.Common.Error.#PropertyNullOrWhiteSpace()")] + [assembly: SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Scope = "member", Target = "Microsoft.AspNet.OData.Delta`1.#CopyChangedValues(!0)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Scope = "member", Target = "Microsoft.AspNet.OData.Delta`1.#Microsoft.AspNet.OData.IEdmObject.GetEdmType()")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Upsert", Scope = "member", Target = "Microsoft.AspNet.OData.DataModificationOperationKind.#Upsert")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Upsert", Scope = "member", Target = "Org.OData.Core.V1.DataModificationOperationKind.#Upsert")] +[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "Org.OData.Core.V1")] + +[assembly: SuppressMessage("Microsoft.Design", "CA1040:AvoidEmptyInterfaces", Scope = "type", Target = "Microsoft.AspNet.OData.IDeltaSet")] +[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Scope = "member", Target = "Microsoft.AspNet.OData.ODataAPIHandler`1.#TryGet(System.Collections.Generic.IDictionary`2,!0&,System.String&)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.ODataAPIHandler`1.#TryDelete(System.Collections.Generic.IDictionary`2,System.String&)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.ODataAPIHandler`1.#TryGet(System.Collections.Generic.IDictionary`2,!0&,System.String&)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1040:AvoidEmptyInterfaces", Scope = "type", Target = "Microsoft.AspNet.OData.IODataAPIHandler")] +[assembly: SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Scope = "member", Target = "Microsoft.AspNet.OData.Formatter.Serialization.DefaultODataSerializerProvider.#GetODataPayloadSerializerImpl(System.Type,System.Func`1,Microsoft.AspNet.OData.Routing.ODataPath,System.Type)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.EdmODataAPIHandler.#TryDelete(System.Collections.Generic.IDictionary`2,System.String&)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.EdmODataAPIHandler.#TryGet(System.Collections.Generic.IDictionary`2,Microsoft.AspNet.OData.EdmStructuredObject&,System.String&)")] +[assembly: SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider", MessageId = "System.String.Format(System.String,System.Object,System.Object)", Scope = "member", Target = "Microsoft.AspNet.OData.Formatter.ClrTypeCache+EdmTypeCacheItemComparer.#GetHashCode(Microsoft.AspNet.OData.Formatter.ClrTypeCache+EdmTypeCacheItem)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.EdmODataAPIHandler.#TryCreate(Microsoft.AspNet.OData.IEdmChangedObject,Microsoft.AspNet.OData.IEdmStructuredObject&,System.String&)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Scope = "member", Target = "Microsoft.AspNet.OData.EdmODataAPIHandler.#TryCreate(Microsoft.AspNet.OData.IEdmChangedObject,Microsoft.AspNet.OData.IEdmStructuredObject&,System.String&)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Scope = "member", Target = "Microsoft.AspNet.OData.EdmODataAPIHandler.#TryGet(System.Collections.Generic.IDictionary`2,Microsoft.AspNet.OData.IEdmStructuredObject&,System.String&)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.EdmODataAPIHandler.#TryGet(System.Collections.Generic.IDictionary`2,Microsoft.AspNet.OData.IEdmStructuredObject&,System.String&)")] +[assembly: SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields", Scope = "member", Target = "Microsoft.AspNet.OData.EdmDeltaDeletedEntityObject.#_edmType")] +[assembly: SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields", Scope = "member", Target = "Microsoft.AspNet.OData.EdmDeltaDeletedLink.#_edmType")] +[assembly: SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields", Scope = "member", Target = "Microsoft.AspNet.OData.EdmDeltaEntityObject.#_edmType")] +[assembly: SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields", Scope = "member", Target = "Microsoft.AspNet.OData.EdmDeltaLink.#_edmType")] +[assembly: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope = "member", Target = "Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceDeserializer.#CreateKeyProperties(System.Uri,Microsoft.AspNet.OData.Formatter.Deserialization.ODataDeserializerContext)")] +[assembly: SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Scope = "member", Target = "Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceDeserializer.#CreateResourceWrapper(Microsoft.OData.Edm.IEdmTypeReference,Microsoft.AspNet.OData.Formatter.Deserialization.ODataEntityReferenceLinkBase,Microsoft.AspNet.OData.Formatter.Deserialization.ODataDeserializerContext)")] +[assembly: SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Scope = "member", Target = "Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceDeserializer.#UpdateResourceWrapper(Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceWrapper,Microsoft.AspNet.OData.Formatter.Deserialization.ODataDeserializerContext)")] +[assembly: SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Scope = "member", Target = "Microsoft.AspNet.OData.Delta`1.#CopyChangedValues(!0,Microsoft.AspNet.OData.ODataAPIHandler`1,Microsoft.AspNet.OData.ODataAPIHandlerFactory)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope = "member", Target = "Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceDeserializer.#GetODataPath(System.String,Microsoft.AspNet.OData.Formatter.Deserialization.ODataDeserializerContext)")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "API", Scope = "type", Target = "Microsoft.AspNet.OData.ODataAPIHandler`1")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "API", Scope = "type", Target = "Microsoft.AspNet.OData.IODataAPIHandler")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "API", Scope = "type", Target = "Microsoft.AspNet.OData.ODataEdmAPIHandlerFactory")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "API", Scope = "type", Target = "Microsoft.AspNet.OData.ODataAPIResponseStatus")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "API", Scope = "type", Target = "Microsoft.AspNet.OData.EdmODataAPIHandler")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "API", Scope = "type", Target = "Microsoft.AspNet.OData.ODataAPIHandlerFactory")] + +[assembly: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope = "member", Target = "Microsoft.AspNet.OData.DeltaSet`1.#TryDeleteObject(System.Collections.Generic.IDictionary`2,System.String&)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope = "member", Target = "Microsoft.AspNet.OData.DeltaSet`1.#TryGetObject(System.Collections.Generic.IDictionary`2,System.Object&,System.String&)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope = "member", Target = "Microsoft.AspNet.OData.EdmChangedObjectCollection.#TryCreateObject(System.Object&,System.String&)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope = "member", Target = "Microsoft.AspNet.OData.EdmChangedObjectCollection.#TryDeleteObject(System.Collections.Generic.IDictionary`2,System.String&)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope = "member", Target = "Microsoft.AspNet.OData.EdmChangedObjectCollection.#TryGetObject(System.Collections.Generic.IDictionary`2,System.Object&,System.String&)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Scope = "type", Target = "Microsoft.AspNet.OData.PatchMethodHandler+TryCreate")] +[assembly: SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Scope = "type", Target = "Microsoft.AspNet.OData.PatchMethodHandler+TryDelete")] +[assembly: SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Scope = "type", Target = "Microsoft.AspNet.OData.PatchMethodHandler+TryGet")] +[assembly: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope = "member", Target = "Microsoft.AspNet.OData.DeltaSet`1.#TryCreateObject(System.Object&,System.String&)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "0#", Scope = "member", Target = "Microsoft.AspNet.OData.PatchMethodHandler+TryCreate.#Invoke(System.Object&,System.String&)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.PatchMethodHandler+TryCreate.#Invoke(System.Object&,System.String&)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.PatchMethodHandler+TryDelete.#Invoke(System.Collections.Generic.IDictionary`2,System.String&)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Scope = "member", Target = "Microsoft.AspNet.OData.PatchMethodHandler+TryGet.#Invoke(System.Collections.Generic.IDictionary`2,System.Object&,System.String&)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.PatchMethodHandler+TryGet.#Invoke(System.Collections.Generic.IDictionary`2,System.Object&,System.String&)")] + +[assembly: SuppressMessage("Microsoft.Design", "CA1040:AvoidEmptyInterfaces", Scope = "type", Target = "Microsoft.AspNet.OData.IDeltaSet")] +[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Scope = "member", Target = "Microsoft.AspNet.OData.ODataAPIHandler`1.#TryGet(System.Collections.Generic.IDictionary`2,!0&,System.String&)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.ODataAPIHandler`1.#TryDelete(System.Collections.Generic.IDictionary`2,System.String&)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.ODataAPIHandler`1.#TryGet(System.Collections.Generic.IDictionary`2,!0&,System.String&)")] +[assembly: SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Scope = "member", Target = "Microsoft.AspNet.OData.ODataAPIHandler`1.#GetODataPath(System.String,Microsoft.OData.Edm.IEdmModel)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1040:AvoidEmptyInterfaces", Scope = "type", Target = "Microsoft.AspNet.OData.IODataAPIHandler")] +[assembly: SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Scope = "member", Target = "Microsoft.AspNet.OData.Formatter.Serialization.DefaultODataSerializerProvider.#GetODataPayloadSerializerImpl(System.Type,System.Func`1,Microsoft.AspNet.OData.Routing.ODataPath,System.Type)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.EdmODataAPIHandler.#TryDelete(System.Collections.Generic.IDictionary`2,System.String&)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.EdmODataAPIHandler.#TryGet(System.Collections.Generic.IDictionary`2,Microsoft.AspNet.OData.EdmStructuredObject&,System.String&)")] +[assembly: SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider", MessageId = "System.String.Format(System.String,System.Object,System.Object)", Scope = "member", Target = "Microsoft.AspNet.OData.Formatter.ClrTypeCache+EdmTypeCacheItemComparer.#GetHashCode(Microsoft.AspNet.OData.Formatter.ClrTypeCache+EdmTypeCacheItem)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.EdmODataAPIHandler.#TryCreate(Microsoft.AspNet.OData.IEdmChangedObject,Microsoft.AspNet.OData.IEdmStructuredObject&,System.String&)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Scope = "member", Target = "Microsoft.AspNet.OData.EdmODataAPIHandler.#TryCreate(Microsoft.AspNet.OData.IEdmChangedObject,Microsoft.AspNet.OData.IEdmStructuredObject&,System.String&)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Scope = "member", Target = "Microsoft.AspNet.OData.EdmODataAPIHandler.#TryGet(System.Collections.Generic.IDictionary`2,Microsoft.AspNet.OData.IEdmStructuredObject&,System.String&)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.EdmODataAPIHandler.#TryGet(System.Collections.Generic.IDictionary`2,Microsoft.AspNet.OData.IEdmStructuredObject&,System.String&)")] +[assembly: SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields", Scope = "member", Target = "Microsoft.AspNet.OData.EdmDeltaDeletedEntityObject.#_edmType")] +[assembly: SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields", Scope = "member", Target = "Microsoft.AspNet.OData.EdmDeltaDeletedLink.#_edmType")] +[assembly: SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields", Scope = "member", Target = "Microsoft.AspNet.OData.EdmDeltaEntityObject.#_edmType")] +[assembly: SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields", Scope = "member", Target = "Microsoft.AspNet.OData.EdmDeltaLink.#_edmType")] +[assembly: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope = "member", Target = "Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceDeserializer.#CreateKeyProperties(System.Uri,Microsoft.AspNet.OData.Formatter.Deserialization.ODataDeserializerContext)")] +[assembly: SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Scope = "member", Target = "Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceDeserializer.#CreateResourceWrapper(Microsoft.OData.Edm.IEdmTypeReference,Microsoft.AspNet.OData.Formatter.Deserialization.ODataEntityReferenceLinkBase,Microsoft.AspNet.OData.Formatter.Deserialization.ODataDeserializerContext)")] +[assembly: SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Scope = "member", Target = "Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceDeserializer.#UpdateResourceWrapper(Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceWrapper,Microsoft.AspNet.OData.Formatter.Deserialization.ODataDeserializerContext)")] +[assembly: SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Scope = "member", Target = "Microsoft.AspNet.OData.Delta`1.#CopyChangedValues(!0,Microsoft.AspNet.OData.ODataAPIHandler`1,Microsoft.AspNet.OData.ODataAPIHandlerFactory)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope = "member", Target = "Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceDeserializer.#GetODataPath(System.String,Microsoft.AspNet.OData.Formatter.Deserialization.ODataDeserializerContext)")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "API", Scope = "type", Target = "Microsoft.AspNet.OData.ODataAPIHandler`1")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "API", Scope = "type", Target = "Microsoft.AspNet.OData.IODataAPIHandler")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "API", Scope = "type", Target = "Microsoft.AspNet.OData.ODataEdmAPIHandlerFactory")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "API", Scope = "type", Target = "Microsoft.AspNet.OData.ODataAPIResponseStatus")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "API", Scope = "type", Target = "Microsoft.AspNet.OData.EdmODataAPIHandler")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "API", Scope = "type", Target = "Microsoft.AspNet.OData.ODataAPIHandlerFactory")] +[assembly: SuppressMessage("Microsoft.Globalization", "CA1309:UseOrdinalStringComparison", MessageId = "System.String.Equals(System.String,System.String,System.StringComparison)", Scope = "member", Target = "Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceDeserializer.#CreateResourceId(Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceWrapper,Microsoft.AspNet.OData.Formatter.Deserialization.ODataDeserializerContext,Microsoft.OData.UriParser.ODataPathSegment,Microsoft.OData.Edm.IEdmEntityType)")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix", Scope = "type", Target = "Microsoft.AspNet.OData.NavigationPath")] +[assembly: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope = "member", Target = "Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceDeserializer.#GenerateNestedReadContext(Microsoft.AspNet.OData.Formatter.Deserialization.ODataNestedResourceInfoWrapper,Microsoft.AspNet.OData.Formatter.Deserialization.ODataDeserializerContext,Microsoft.OData.Edm.IEdmProperty)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope = "member", Target = "Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceDeserializer.#ApplyIdToPath(Microsoft.AspNet.OData.Formatter.Deserialization.ODataDeserializerContext,Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceWrapper)")] +[assembly: SuppressMessage("Microsoft.Globalization", "CA1309:UseOrdinalStringComparison", MessageId = "System.String.Equals(System.String,System.String,System.StringComparison)", Scope = "member", Target = "Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceDeserializer.#ApplyIdToPath(Microsoft.AspNet.OData.Formatter.Deserialization.ODataDeserializerContext,Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceWrapper)")] diff --git a/src/Microsoft.AspNet.OData/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.OData/Properties/AssemblyInfo.cs index 4bd46c88f7..6db41a4b01 100644 --- a/src/Microsoft.AspNet.OData/Properties/AssemblyInfo.cs +++ b/src/Microsoft.AspNet.OData/Properties/AssemblyInfo.cs @@ -12,3 +12,6 @@ [assembly: AssemblyDescription("")] [assembly: InternalsVisibleTo("Microsoft.AspNet.OData.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] +[assembly: InternalsVisibleTo("Microsoft.Test.E2E.AspNet.OData, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] +[assembly: InternalsVisibleTo("Microsoft.Test.E2E.AspNetCore.OData, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] +[assembly: InternalsVisibleTo("Microsoft.Test.E2E.AspNetCore3x.OData, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.OData/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.OData/Properties/AssemblyInfo.cs index e09883cfac..cf56834d77 100644 --- a/src/Microsoft.AspNetCore.OData/Properties/AssemblyInfo.cs +++ b/src/Microsoft.AspNetCore.OData/Properties/AssemblyInfo.cs @@ -12,3 +12,6 @@ [assembly: AssemblyDescription("")] [assembly: InternalsVisibleTo("Microsoft.AspNetCore.OData.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] +[assembly: InternalsVisibleTo("Microsoft.Test.E2E.AspNet.OData, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] +[assembly: InternalsVisibleTo("Microsoft.Test.E2E.AspNetCore.OData, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] +[assembly: InternalsVisibleTo("Microsoft.Test.E2E.AspNetCore3x.OData, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] \ No newline at end of file diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNet/Formatter/ODataEntityReferenceLinkE2ETests.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNet/Formatter/ODataEntityReferenceLinkE2ETests.cs new file mode 100644 index 0000000000..40ff2933b3 --- /dev/null +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNet/Formatter/ODataEntityReferenceLinkE2ETests.cs @@ -0,0 +1,276 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Data.Entity; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +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 Microsoft.Test.E2E.AspNet.OData.Common.Execution; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace Microsoft.Test.E2E.AspNet.OData.Formatter +{ + public class ODataEntityReferenceLinkE2ETests : WebHostTestBase + { + public ODataEntityReferenceLinkE2ETests(WebHostTestFixture fixture) + : base(fixture) + { + } + protected override void UpdateConfiguration(WebRouteConfiguration configuration) + { + var controllers = new[] { typeof(BooksController)}; + configuration.AddControllers(controllers); + configuration.Count().Filter().OrderBy().Expand().MaxTop(null).Select(); + configuration.MapODataServiceRoute("odata", "odata", BuildEdmModel(configuration)); + } + private static IEdmModel BuildEdmModel(WebRouteConfiguration configuration) + { + var builder = configuration.CreateConventionModelBuilder(); + builder.EntitySet("Books"); + builder.EntitySet("Authors"); + builder.Action("ResetDataSource"); + builder.Action("RelateToExistingEntityAndUpdate").ReturnsFromEntitySet("Books").EntityParameter("book"); + + return builder.GetEdmModel(); + } + + [Fact] + public async Task CanCreate_ANewEntityAndRelateToAnExistingEntity_UsingODataBind() + { + await ResetDataSource(); + // Arrange + const string Payload = "{" + + "\"Id\":\"1\"," + + "\"Name\":\"BookA\"," + + "\"Author@odata.bind\":\"Authors(1)\"}"; + + string Uri = BaseAddress + "/odata/Books"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, Uri); + + request.Content = new StringContent(Payload); + request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); + + // Act + HttpResponseMessage response = await Client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + Assert.NotNull(response.Content); + + //Get the above saved entity from the database + //and expand the navigation property to see if + //it was correctly created with the existing entity + //attached to it. + string query = string.Format("{0}/odata/Books?$expand=Author", BaseAddress); + HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Get, query); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); + + // Act + HttpResponseMessage res = await Client.SendAsync(requestMessage); + + // Assert + Assert.True(res.IsSuccessStatusCode); + var responseObject = JObject.Parse(await res.Content.ReadAsStringAsync()); + var result = responseObject["value"] as JArray; + var expandProp = result[0]["Author"] as JObject; + Assert.Equal(1, expandProp["Id"]); + + } + + [Fact] + public async Task CanUpdate_TheRelatedEntitiesProperties() + { + await ResetDataSource(); + // Arrange + const string Payload = "{" + + "\"book\":{" + + "\"Id\":\"1\"," + + "\"Name\":\"BookA\"," + + "\"Author\":{" + + "\"@odata.id\":\"Authors(1)\"," + + "\"Name\":\"UpdatedAuthor\"}}}"; + + string Uri = BaseAddress + "/odata/RelateToExistingEntityAndUpdate"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, Uri); + + request.Content = new StringContent(Payload); + request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); + + // Act + HttpResponseMessage response = await Client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + Assert.NotNull(response.Content); + + //Get the above saved entity from the database + //and expand its navigation property to see if it was created with + //the existing entity correctly. + //Also note that we were able to update the name property + //of the existing entity + string query = string.Format("{0}/odata/Books?$expand=Author", BaseAddress); + HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Get, query); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); + + // Act + HttpResponseMessage res = await Client.SendAsync(requestMessage); + + // Assert + Assert.True(res.IsSuccessStatusCode); + var responseObject = JObject.Parse(await res.Content.ReadAsStringAsync()); + var result = responseObject["value"] as JArray; + var expandProp = result[0]["Author"] as JObject; + Assert.Equal(1, expandProp["Id"]); + Assert.Equal("UpdatedAuthor", expandProp["Name"]); + } + + private async Task ResetDataSource() + { + string requestUri = BaseAddress + "/odata/ResetDataSource"; + HttpClient client = new HttpClient(); + HttpResponseMessage response = await client.GetAsync(requestUri); + response.EnsureSuccessStatusCode(); + } + } + + public class BooksController : TestODataController +#if NETCORE + , IDisposable +#endif + { + private ODataEntityReferenceLinkContext db = new ODataEntityReferenceLinkContext(); + + [EnableQuery] + public ITestActionResult Get() + { + return Ok(db.Books); + } + public ITestActionResult Post([FromBody] Book book) + { + if (!ModelState.IsValid) + { + return BadRequest(); + } + + db.Authors.Attach(book.Author); + db.Books.Add(book); + db.SaveChanges(); + + return Created(book); + } + + [HttpPost] + [ODataRoute("RelateToExistingEntityAndUpdate")] + public ITestActionResult RelateToExistingEntityAndUpdate(ODataActionParameters odataActionParameters) + { + if (!ModelState.IsValid) + { + return BadRequest(); + } + + Book book = (Book)odataActionParameters["book"]; + string authorName = book.Author.Name; + Author author = new Author() + { + Id = book.Author.Id + }; + db.Authors.Attach(author); + book.Author = author; + book.Author.Name = authorName; + db.Books.Add(book); + + db.SaveChanges(); + return Created(book); + } + + [HttpGet] + [ODataRoute("ResetDataSource")] + public ITestActionResult ResetDataSource() + { + db.Database.Delete(); // Start from scratch so that tests aren't broken by schema changes. + CreateDatabase(); + return Ok(); + } + + private static void CreateDatabase() + { + using (ODataEntityReferenceLinkContext db = new ODataEntityReferenceLinkContext()) + { + if (!db.Authors.Any()) + { + IList authors = new List() + { + new Author() + { + Id = 1, + Name = "AuthorA" + }, + new Author() + { + Id = 2, + Name = "AuthorB" + }, + new Author() + { + Id = 3, + Name = "AuthorC" + } + }; + + foreach (var author in authors) + { + db.Authors.Add(author); + } + db.SaveChanges(); + } + } + } + +#if NETCORE + public void Dispose() + { + //_db.Dispose(); + } +#endif + + } + + public class Book + { + [Key] + public int Id { get; set; } + public string Name { get; set; } + public Author Author { get; set; } + public IList AuthorList { get; set; } + } + + public class Author + { + [Key] + public int Id { get; set; } + public string Name { get; set; } + } + + public class ODataEntityReferenceLinkContext : DbContext + { + public static string ConnectionString = @"Data Source=(LocalDb)\MSSQLLocalDB;Integrated Security=True;Initial Catalog=ODataEntityReferenceLinkContext"; + public ODataEntityReferenceLinkContext() + : base(ConnectionString) + { + } + public DbSet Books { get; set; } + public DbSet Authors { get; set; } + } +} 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..3aa60bfcee 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 @@ -57,6 +57,9 @@ ..\..\..\..\sln\packages\Microsoft.Owin.4.2.2\lib\net45\Microsoft.Owin.dll + + + ..\..\..\..\sln\packages\Microsoft.Owin.2.0.2\lib\net45\Microsoft.Owin.dll True @@ -163,6 +166,21 @@ Aggregation\AggregationTests.cs + + BulkOperation\BulkOperationTest.cs + + + BulkOperation\BulkOperationDataModel.cs + + + BulkOperation\BulkOperationEdmModel.cs + + + BulkOperation\BulkOperationController.cs + + + BulkOperation\BulkOperationPatchHandlers.cs + EntitySetAggregation\EntitySetAggregationController.cs diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore/Microsoft.Test.E2E.AspNetCore.OData.csproj b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore/Microsoft.Test.E2E.AspNetCore.OData.csproj index 40c41bbdde..6909cd715d 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore/Microsoft.Test.E2E.AspNetCore.OData.csproj +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore/Microsoft.Test.E2E.AspNetCore.OData.csproj @@ -1402,6 +1402,21 @@ OpenType\TypedTest.cs + + BulkOperation\BulkOperationTest.cs + + + BulkOperation\BulkOperationDataModel.cs + + + BulkOperation\BulkOperationEdmModel.cs + + + BulkOperation\BulkOperationController.cs + + + BulkOperation\BulkOperationPatchHandlers.cs + ParameterAlias\ParameterAliasDataSource.cs diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/BulkOperation/EFTests/BulkOperationControllerEF.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/BulkOperation/EFTests/BulkOperationControllerEF.cs new file mode 100644 index 0000000000..aa87466576 --- /dev/null +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/BulkOperation/EFTests/BulkOperationControllerEF.cs @@ -0,0 +1,200 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Linq; +using Microsoft.AspNet.OData; +using Microsoft.AspNet.OData.Extensions; +using Microsoft.AspNet.OData.Routing; +using Microsoft.EntityFrameworkCore; +using Microsoft.Test.E2E.AspNet.OData.Common.Controllers; +using Xunit; +using static Microsoft.Test.E2E.AspNet.OData.BulkOperation.APIHandlerFactoryEF; + +namespace Microsoft.Test.E2E.AspNet.OData.BulkOperation +{ + public class EmployeesControllerEF : TestODataController + { + public EmployeesControllerEF() + { + + } + + public static List employees; + public static List friends; + + internal DbSet GenerateData(EmployeeDBContext context) + { + if (context.Employees.Any()) + { + return context.Employees; + } + + var friends = GenerateDataOrders(context); + + employees = new List(); + employees.Add(new Employee { ID = 1, Name = "Employee1", Friends = friends.Where(x => x.Id == 1 || x.Id == 2).ToList() }); + employees.Add(new Employee { ID = 2, Name = "Employee2", Friends = friends.Where(x => x.Id == 3 || x.Id == 4).ToList() }); + employees.Add(new Employee { ID = 3, Name = "Employee3", Friends = friends.Where(x => x.Id == 5 || x.Id == 6).ToList() }); + + context.Employees.AddRange(employees); + + context.SaveChanges(); + + return context.Employees; + } + + internal DbSet GenerateDataOrders(EmployeeDBContext context) + { + if (context.Friends.Any()) + { + return context.Friends; + } + + friends = new List(); + friends.Add(new Friend { Id = 1, Age = 10, Orders = new List() { new Order { Id = 1, Price = 5 }, new Order { Id = 2, Price = 5 } } }); + friends.Add(new Friend { Id = 2, Age = 20, Orders = new List() { new Order { Id = 10, Price = 5 }, new Order { Id = 20, Price = 5 } } }); + friends.Add(new Friend { Id = 3, Age = 30, Orders = new List() { new Order { Id = 3, Price = 5 }, new Order { Id = 4, Price = 5 } } }); + friends.Add(new Friend { Id = 4, Age = 40, Orders = new List() { new Order { Id = 30, Price = 5 }, new Order { Id = 40, Price = 5 } } }); + friends.Add(new Friend { Id = 5, Age = 50, Orders = new List() { new Order { Id = 5, Price = 5 }, new Order { Id = 6, Price = 5 } } }); + friends.Add(new Friend { Id = 6, Age = 60, Orders = new List() { new Order { Id = 50, Price = 5 }, new Order { Id = 60, Price = 5 } } }); + + context.Friends.AddRange(friends); + + context.SaveChanges(); + + return context.Friends; + } + + + [ODataRoute("Employees")] + [HttpPatch] + public ITestActionResult PatchEmployees([FromBody] DeltaSet coll) + { + using (var dbContext = CreateDbContext()) + { + GenerateData(dbContext); + + Assert.NotNull(coll); + + var returncoll = coll.Patch(new EmployeeEFPatchHandler(dbContext), new APIHandlerFactoryEF(Request.GetModel(), dbContext)); + + + return Ok(returncoll); + } + } + + private EmployeeDBContext CreateDbContext() + { + var buiilder = new DbContextOptionsBuilder().UseInMemoryDatabase(Guid.NewGuid().ToString()); + var dbContext = new EmployeeDBContext(buiilder.Options); + return dbContext; + } + + [ODataRoute("Employees({key})")] + public ITestActionResult Patch(int key, [FromBody] Delta delta) + { + using (var dbContext = CreateDbContext()) + { + GenerateData(dbContext); + + delta.TrySetPropertyValue("ID", key); // It is the key property, and should not be updated. + object obj; + delta.TryGetPropertyValue("Friends", out obj); + + var employee = dbContext.Employees.First(x => x.ID == key); + + try + { + delta.Patch(employee, new EmployeeEFPatchHandler(dbContext), new APIHandlerFactoryEF(Request.GetModel(), dbContext)); + + } + catch (ArgumentException ae) + { + return BadRequest(ae.Message); + } + + employee = dbContext.Employees.First(x => x.ID == key); + + ValidateFriends(key, employee); + + return Ok(employee); + } + } + + private static void ValidateFriends(int key, Employee employee) + { + if (key == 1 && employee.Name == "SqlUD") + { + Contract.Assert(employee.Friends.Count == 2); + Contract.Assert(employee.Friends[0].Id == 2); + Contract.Assert(employee.Friends[1].Id == 3); + } + else if (key == 1 && employee.Name == "SqlFU") + { + Contract.Assert(employee.Friends.Count == 3); + Contract.Assert(employee.Friends[0].Id == 345); + Contract.Assert(employee.Friends[1].Id == 400); + Contract.Assert(employee.Friends[2].Id == 900); + } + else if (key == 1 && employee.Name == "SqlMU") + { + Contract.Assert(employee.Friends.Count == 3); + Contract.Assert(employee.Friends[0].Id == 2); + Contract.Assert(employee.Friends[1].Id == 1); + Contract.Assert(employee.Friends[1].Name == "Test_1"); + Contract.Assert(employee.Friends[2].Id == 3); + } + else if (key == 1 && employee.Name == "SqlMU1") + { + Contract.Assert(employee.Friends.Count == 2); + Contract.Assert(employee.Friends[0].Id == 2); + Contract.Assert(employee.Friends[1].Id == 3); + } + } + + [ODataRoute("Employees({key})/Friends")] + [HttpPatch] + public ITestActionResult PatchFriends(int key, [FromBody] DeltaSet friendColl) + { + using (var dbContext = CreateDbContext()) + { + GenerateData(dbContext); + + Employee originalEmployee = dbContext.Employees.SingleOrDefault(c => c.ID == key); + Assert.NotNull(originalEmployee); + + var changedObjColl = friendColl.Patch(originalEmployee.Friends); + + return Ok(changedObjColl); + } + } + + public ITestActionResult Get(int key) + { + using (var dbContext = CreateDbContext()) + { + var emp = dbContext.Employees.SingleOrDefault(e => e.ID == key); + return Ok(emp); + } + } + + [ODataRoute("Employees({key})/Friends")] + public ITestActionResult GetFriends(int key) + { + using (var dbContext = CreateDbContext()) + { + var emp = dbContext.Employees.SingleOrDefault(e => e.ID == key); + return Ok(emp.Friends); + } + } + + + } +} \ No newline at end of file diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/BulkOperation/EFTests/BulkOperationPatchHandlersEF.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/BulkOperation/EFTests/BulkOperationPatchHandlersEF.cs new file mode 100644 index 0000000000..a09f22adeb --- /dev/null +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/BulkOperation/EFTests/BulkOperationPatchHandlersEF.cs @@ -0,0 +1,410 @@ +//----------------------------------------------------------------------------- +// +// 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.EntityFrameworkCore; +using Microsoft.OData.Edm; +using Microsoft.OData.UriParser; + +namespace Microsoft.Test.E2E.AspNet.OData.BulkOperation +{ + internal class EmployeeDBContext : DbContext + { + public EmployeeDBContext() + { + + } + + public EmployeeDBContext(DbContextOptions options) : base(options) + { + + } + + public DbSet Employees { get; set; } + public DbSet Friends { get; set; } + + protected override void OnModelCreating(Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder) + { + modelBuilder.Entity().HasKey(c => c.ID); + modelBuilder.Entity().Ignore(c => c.SkillSet); + modelBuilder.Entity().Ignore(c => c.NewFriends); + modelBuilder.Entity().Ignore(c => c.UnTypedFriends); + modelBuilder.Entity().Ignore(c => c.InstanceAnnotations); + modelBuilder.Entity().Ignore(c => c.FavoriteSports); + + modelBuilder.Entity().Ignore(c => c.InstanceAnnotations); + modelBuilder.Entity().Ignore(c => c.InstanceAnnotations); + + modelBuilder.Entity().HasKey(c => c.Id); + + modelBuilder.Entity().Ignore(c => c.Container); + modelBuilder.Entity().Ignore(c => c.Container); + } + } + + internal class APIHandlerFactoryEF : ODataAPIHandlerFactory + { + EmployeeDBContext dbContext; + + public APIHandlerFactoryEF(IEdmModel model, EmployeeDBContext dbContext) : base(model) + { + this.dbContext = dbContext; + } + + public override IODataAPIHandler GetHandler(ODataPath odataPath) + { + if (odataPath != null) + { + var pathItems = odataPath; + } + + return null; + } + + internal class EmployeeEFPatchHandler : ODataAPIHandler + { + EmployeeDBContext dbContext = null; + + public EmployeeEFPatchHandler(EmployeeDBContext dbContext) + { + this.dbContext = dbContext; + } + + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out Employee createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + createdObject = new Employee(); + dbContext.Employees.Add(createdObject); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + var id = keyValues.First().Value.ToString(); + var customer = dbContext.Employees.First(x => x.ID == Int32.Parse(id)); + + dbContext.Employees.Remove(customer); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out Employee originalObject, out string errorMessage) + { + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; + errorMessage = string.Empty; + originalObject = null; + + try + { + var id = keyValues["ID"].ToString(); + originalObject = dbContext.Employees.First(x => x.ID == Int32.Parse(id)); + + if (originalObject == null) + { + status = ODataAPIResponseStatus.NotFound; + } + + } + catch (Exception ex) + { + status = ODataAPIResponseStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override IODataAPIHandler GetNestedHandler(Employee parent, string navigationPropertyName) + { + switch (navigationPropertyName) + { + case "Friends": + return new FriendEFPatchHandler(parent); + case "NewFriends": + return new NewFriendEFPatchHandler(parent); + default: + return null; + } + } + } + + internal class FriendEFPatchHandler : ODataAPIHandler + { + Employee employee; + public FriendEFPatchHandler(Employee employee) + { + this.employee = employee; + } + + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out Friend createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + createdObject = new Friend(); + employee.Friends.Add(createdObject); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + var id = keyValues.First().Value.ToString(); + var friend = employee.Friends.First(x => x.Id == Int32.Parse(id)); + + employee.Friends.Remove(friend); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out Friend originalObject, out string errorMessage) + { + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; + errorMessage = string.Empty; + originalObject = null; + + try + { + var id = keyValues["Id"].ToString(); + if (employee.Friends == null) + { + status = ODataAPIResponseStatus.NotFound; + } + else + { + originalObject = employee.Friends.FirstOrDefault(x => x.Id == Int32.Parse(id)); + } + + if (originalObject == null) + { + status = ODataAPIResponseStatus.NotFound; + } + + } + catch (Exception ex) + { + status = ODataAPIResponseStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override IODataAPIHandler GetNestedHandler(Friend parent, string navigationPropertyName) + { + return new OrderEFPatchHandler(parent); + } + } + + internal class NewFriendEFPatchHandler : ODataAPIHandler + { + Employee employee; + public NewFriendEFPatchHandler(Employee employee) + { + this.employee = employee; + } + + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out NewFriend createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + createdObject = new NewFriend(); + employee.NewFriends.Add(createdObject); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + var id = keyValues.First().Value.ToString(); + var friend = employee.NewFriends.First(x => x.Id == Int32.Parse(id)); + + employee.NewFriends.Remove(friend); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out NewFriend originalObject, out string errorMessage) + { + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; + errorMessage = string.Empty; + originalObject = null; + + try + { + var id = keyValues["Id"].ToString(); + originalObject = employee.NewFriends.First(x => x.Id == Int32.Parse(id)); + + if (originalObject == null) + { + status = ODataAPIResponseStatus.NotFound; + } + + } + catch (Exception ex) + { + status = ODataAPIResponseStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override IODataAPIHandler GetNestedHandler(NewFriend parent, string navigationPropertyName) + { + return null; + } + } + + internal class OrderEFPatchHandler : ODataAPIHandler + { + Friend friend; + public OrderEFPatchHandler(Friend friend) + { + this.friend = friend; + } + + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out Order createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + createdObject = new Order(); + friend.Orders.Add(createdObject); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + var id = keyValues.First().Value.ToString(); + var order = friend.Orders.First(x => x.Id == Int32.Parse(id)); + + friend.Orders.Remove(order); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out Order originalObject, out string errorMessage) + { + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; + errorMessage = string.Empty; + originalObject = null; + + try + { + var id = keyValues["Id"].ToString(); + originalObject = friend.Orders.First(x => x.Id == Int32.Parse(id)); + + if (originalObject == null) + { + status = ODataAPIResponseStatus.NotFound; + } + } + catch (Exception ex) + { + status = ODataAPIResponseStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override IODataAPIHandler GetNestedHandler(Order parent, string navigationPropertyName) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/BulkOperation/EFTests/BulkOperationTestEF.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/BulkOperation/EFTests/BulkOperationTestEF.cs new file mode 100644 index 0000000000..eed66da892 --- /dev/null +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/BulkOperation/EFTests/BulkOperationTestEF.cs @@ -0,0 +1,285 @@ +//----------------------------------------------------------------------------- +// +// 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 Microsoft.Test.E2E.AspNet.OData.Common.Extensions; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace Microsoft.Test.E2E.AspNet.OData.BulkOperation +{ + public class BulkOperationTestEF : WebHostTestBase + { + public BulkOperationTestEF(WebHostTestFixture fixture) + : base(fixture) + { + } + + protected override void UpdateConfiguration(WebRouteConfiguration configuration) + { + var controllers = new[] { typeof(EmployeesControllerEF), 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 Update + + [Fact] + public async Task PatchEmployee_WithUpdates() + { + //Arrange + string requestUri = this.BaseAddress + "/convention/Employees(1)"; + + var content = @"{ + 'Name':'Sql' , + 'Friends@odata.delta':[{'Id':1,'Name':'Test2'},{'Id':2,'Name':'Test3'}] + }"; + + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), 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 = await response.Content.ReadAsObject(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains("Sql", json.ToString()); + } + } + + [Fact] + public async Task PatchEmployee_WithUpdates_WithEmployees() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Employees(1)"; + + var content = @"{ + 'Name':'SqlFU' , + 'Friends':[{'Id':345,'Name':'Test2'},{'Id':400,'Name':'Test3'},{'Id':900,'Name':'Test93'}] + }"; + + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), 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 = await response.Content.ReadAsObject(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains("SqlFU", json.ToString()); + } + } + + [Fact] + public async Task PatchEmployee_WithUpdates_Employees() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Employees"; + + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees/$delta', + 'value':[{ '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkOperation.Employee', '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'}] + }, + { '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkOperation.Employee', '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 requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + requestForPost.Headers.Add("OData-Version", "4.01"); + requestForPost.Headers.Add("OData-MaxVersion", "4.01"); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPost.Content = stringContent; + + // Act & Assert + var expected = "$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}]}]}"; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost)) + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var json = response.Content.ReadAsStringAsync().Result; + Assert.Contains(expected, json.ToString()); + Assert.Contains("Employee1", json); + Assert.Contains("Employee2", json); + } + } + + [Fact] + public async Task PatchEmployee_WithUpdates_Employees_InV4() + { + //Arrange + string requestUri = this.BaseAddress + "/convention/Employees"; + + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees/$delta', + 'value':[{ '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkOperation.Employee', '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'}] + }, + { '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkOperation.Employee', '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); + requestForPatch.Headers.Add("OData-Version", "4.0"); + requestForPatch.Headers.Add("OData-MaxVersion", "4.0"); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPatch.Content = stringContent; + + // Act & Assert + var expected = "$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}]}]}"; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPatch)) + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var json = response.Content.ReadAsStringAsync().Result; + Assert.Contains(expected, json.ToString()); + Assert.Contains("Employee1", json); + Assert.Contains("Employee2", json); + } + } + + [Fact] + public async Task PatchEmployee_WithDelete() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Employees(1)"; + + var content = @"{ + 'Name':'Sql', + 'Friends@odata.delta':[{ '@odata.removed' : {'reason':'changed'}, 'Id':1}] + }"; + + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), 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)) + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var json = response.Content.ReadAsStringAsync().Result; + Assert.Contains("Sql", json); + } + } + + [Fact] + public async Task PatchEmployee_WithAddUpdateAndDelete() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Employees(1)"; + + var content = @"{ + 'Name':'SqlUD', + 'Friends@odata.delta':[{ '@odata.removed' : {'reason':'changed'}, 'Id':1},{'Id':2,'Name':'Test3'},{'Id':3,'Name':'Test4'}] + }"; + + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), 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)) + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var json = response.Content.ReadAsStringAsync().Result; + Assert.Contains("SqlUD", json); + } + } + + [Fact] + public async Task PatchEmployee_WithMultipleUpdatesinOrder1() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Employees(1)"; + + var content = @"{ + 'Name':'SqlMU' , + 'Friends@odata.delta':[{ '@odata.removed' : {'reason':'changed'}, 'Id':1},{'Id':1,'Name':'Test_1'},{'Id':2,'Name':'Test3'},{'Id':3,'Name':'Test4'}] + }"; + + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), 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)) + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var json = response.Content.ReadAsStringAsync().Result; + Assert.Contains("SqlMU", json); + } + } + + [Fact] + public async Task PatchEmployee_WithMultipleUpdatesinOrder2() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Employees(1)"; + + var content = @"{ + 'Name':'SqlMU1' , + 'Friends@odata.delta':[{ '@odata.removed' : {'reason':'changed'}, 'Id':1},{'Id':1,'Name':'Test_1'},{'Id':2,'Name':'Test3'},{'Id':3,'Name':'Test4'},{ '@odata.removed' : {'reason':'changed'}, 'Id':1}] + }"; + + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), 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)) + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var json = response.Content.ReadAsStringAsync().Result; + Assert.Contains("SqlMU1", json); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/Microsoft.Test.E2E.AspNetCore3x.OData.csproj b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/Microsoft.Test.E2E.AspNetCore3x.OData.csproj index 81b6c9853a..85570102b4 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/Microsoft.Test.E2E.AspNetCore3x.OData.csproj +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/Microsoft.Test.E2E.AspNetCore3x.OData.csproj @@ -27,6 +27,7 @@ + @@ -1664,6 +1665,21 @@ Validation\DeltaOfTValidationTests.cs + + BulkOperation\BulkOperationTest.cs + + + BulkOperation\BulkOperationDataModel.cs + + + BulkOperation\BulkOperationEdmModel.cs + + + BulkOperation\BulkOperationController.cs + + + BulkOperation\BulkOperationPatchHandlers.cs + @@ -1677,4 +1693,5 @@ + diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationController.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationController.cs new file mode 100644 index 0000000000..bcc043edd6 --- /dev/null +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationController.cs @@ -0,0 +1,517 @@ +//----------------------------------------------------------------------------- +// +// 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, Employee employee) + { + var changedObjColl = friendColl.Patch(new NewFriendAPIHandler(employee), new APIHandlerFactory(Request.GetModel())); + + return changedObjColl; + } + public EdmChangedObjectCollection PatchWithUsersMethodTypeLess(int key, EdmChangedObjectCollection friendColl) + { + var entity = Request.GetModel().FindDeclaredType("Microsoft.Test.E2E.AspNet.OData.BulkOperation.UnTypedEmployee") as IEdmEntityType; + InitTypeLessEmployees(entity); + + var entity1 = Request.GetModel().FindDeclaredType("Microsoft.Test.E2E.AspNet.OData.BulkOperation.UnTypedFriend") as IEdmEntityType; + + var changedObjColl = friendColl.Patch(new FriendTypelessAPIHandler(EmployeesTypeless[key - 1], entity), new TypelessAPIHandlerFactory(Request.GetModel(), entity)); + + return changedObjColl; + } + + public EdmChangedObjectCollection EmployeePatchMethodTypeLess(EdmChangedObjectCollection empColl) + { + var entity = Request.GetModel().FindDeclaredType("Microsoft.Test.E2E.AspNet.OData.BulkOperation.UnTypedEmployee") as IEdmEntityType; + InitTypeLessEmployees(entity); + + var changedObjColl = empColl.Patch(new EmployeeEdmAPIHandler(entity), new TypelessAPIHandlerFactory(Request.GetModel(), entity)); + ValidateSuccessfulTypeless(); + + return changedObjColl; + } + + 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); + + object name; + + if (EmployeesTypeless.First().TryGetPropertyValue("Name", out name) && name.ToString() == "Employeeabcd") + { + Assert.Equal("abcd", obj1.ToString()); + + object age; + friends.First().TryGetPropertyValue("Age", out age); + + Assert.Equal(33, (int)age); + } + else + { + Assert.Equal("Friend1", obj1.ToString()); + } + + } + + [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")] + [HttpPatch] + [EnableQuery] + public ITestActionResult PatchEmployees([FromBody] DeltaSet coll) + { + InitEmployees(); + + Assert.NotNull(coll); + + var returncoll = coll.Patch(new EmployeeAPIHandler(), new APIHandlerFactory(Request.GetModel())); + + return Ok(returncoll); + } + + [ODataRoute("Employees")] + [HttpPost] + public ITestActionResult Post([FromBody] Employee employee) + { + InitEmployees(); + + var handler = new EmployeeAPIHandler(); + + handler.UpdateLinkedObjects(employee, Request.GetModel()); + + return Ok(employee); + } + + [ODataRoute("Employees({key})/Friends")] + [HttpPatch] + public ITestActionResult PatchFriends(int key, [FromBody] DeltaSet friendColl) + { + InitEmployees(); + + Employee originalEmployee = Employees.SingleOrDefault(c => c.ID == key); + Assert.NotNull(originalEmployee); + + var changedObjColl = friendColl.Patch(originalEmployee.Friends); + + return Ok(changedObjColl); + } + + [ODataRoute("Employees({key})/NewFriends")] + [HttpPatch] + public ITestActionResult PatchNewFriends(int key, [FromBody] DeltaSet friendColl) + { + InitEmployees(); + + if (key == 1) + { + var deltaSet = PatchWithUsersMethod(friendColl, Employees.First(x => x.ID == key)); + + return Ok(deltaSet); + } + { + Employee originalEmployee = Employees.SingleOrDefault(c => c.ID == key); + Assert.NotNull(originalEmployee); + + var friendCollection = new FriendColl() { new NewFriend { Id = 2, Age = 15 } }; + + var changedObjColl = friendColl.Patch(friendCollection); + + return Ok(changedObjColl); + } + } + + [ODataRoute("Employees({key})/UnTypedFriends")] + [HttpPatch] + public ITestActionResult PatchUnTypedFriends(int key, [FromBody] EdmChangedObjectCollection friendColl) + { + if (key == 1) + { + var changedObjColl = PatchWithUsersMethodTypeLess(key, friendColl); + + var emp = EmployeesTypeless[key - 1]; + object obj; + emp.TryGetPropertyValue("UnTypedFriends", out obj); + var lst = obj as List; + + if (lst != null && lst.Count > 1) + { + object obj1; + if (lst[1].TryGetPropertyValue("Name", out obj1) && Equals("Friend007", obj1)) + { + lst[1].TryGetPropertyValue("Address", out obj1); + Assert.NotNull(obj1); + object obj2; + (obj1 as EdmStructuredObject).TryGetPropertyValue("Street", out obj2); + + Assert.Equal("Abc 123", obj2); + } + } + + return Ok(changedObjColl); + } + else if (key == 2) + { + var entitytype = Request.GetModel().FindDeclaredType("Microsoft.Test.E2E.AspNet.OData.BulkOperation.UnTypedEmployee") as IEdmEntityType; + var entity = new EdmEntityObject(friendColl[0].GetEdmType().AsEntity()); + entity.TrySetPropertyValue("Id", 2); + + var friendCollection = new FriendColl() { entity }; + + var changedObjColl = PatchWithUsersMethodTypeLess(key, friendColl); + + object obj; + Assert.Single(changedObjColl); + + changedObjColl.First().TryGetPropertyValue("Age", out obj); + Assert.Equal(35, obj); + + return Ok(changedObjColl); + } + else + { + var changedObjColl = PatchWithUsersMethodTypeLess(key, friendColl); + + return Ok(changedObjColl); + } + } + + [ODataRoute("UnTypedEmployees")] + [HttpPatch] + public ITestActionResult PatchUnTypedEmployees([FromBody] EdmChangedObjectCollection empColl) + { + var changedObjColl = EmployeePatchMethodTypeLess(empColl); + + return Ok(changedObjColl); + } + + [ODataRoute("Employees({key})")] + [EnableQuery] + public ITestActionResult Patch(int key, [FromBody] Delta delta) + { + InitEmployees(); + + delta.TrySetPropertyValue("ID", key); // It is the key property, and should not be updated. + + Employee employee = Employees.FirstOrDefault(e => e.ID == key); + + if (employee == null) + { + employee = new Employee(); + delta.Patch(employee, new EmployeeAPIHandler(), new APIHandlerFactory(Request.GetModel())); + return Created(employee); + } + + try + { + // todo: put APIHandlerFactory in request context + delta.Patch(employee, new EmployeeAPIHandler(), new APIHandlerFactory(Request.GetModel())); + + if (employee.Name == "Bind1") + { + Assert.NotNull(employee.Friends.Single(x => x.Id == 3)); + } + } + catch (ArgumentException ae) + { + return BadRequest(ae.Message); + } + + 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")] + [HttpPatch] + public ITestActionResult PatchCompanies([FromBody] DeltaSet coll) + { + InitCompanies(); + + Assert.NotNull(coll); + + var returncoll = coll.Patch(new CompanyAPIHandler(), new APIHandlerFactory(Request.GetModel())); + + var comp = coll.First() as Delta; + object val; + if (comp.TryGetPropertyValue("Name", out val)) + { + if (val.ToString() == "Company02") + { + ValidateOverdueOrders2(1, 2, 9); + } + else + { + ValidateOverdueOrders1(1, 1, 9); + } + } + + return Ok(returncoll); + } + + [ODataRoute("Companies")] + [HttpPost] + public ITestActionResult Post([FromBody] Company company) + { + + InitCompanies(); + InitEmployees(); + + if (company.Id == 4) + { + AddNewOrder(company); + } + + var handler = new CompanyAPIHandler(); + + handler.UpdateLinkedObjects(company, Request.GetModel()); + + 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); + } + + private void ValidateOverdueOrders2(int companyId, int orderId, int quantity = 0) + { + var comp = Companies.FirstOrDefault(x => x.Id == companyId); + Assert.NotNull(comp); + + MyNewOrder order = comp.MyOverdueOrders.FirstOrDefault(x => x.Id == orderId); + Assert.NotNull(order); + Assert.Equal(orderId, order.Id); + Assert.Equal(444, order.Price); + Assert.Equal(quantity, order.Quantity); + } + } +} \ No newline at end of file diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationDataModel.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationDataModel.cs new file mode 100644 index 0000000000..39aeb11bd5 --- /dev/null +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationDataModel.cs @@ -0,0 +1,236 @@ +//----------------------------------------------------------------------------- +// +// 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 IODataIdContainer Container { get; set; } + } + + public class NewOrder + { + [Key] + public int Id { get; set; } + + public int Price { get; set; } + + public int Quantity { get; set; } + + public IODataIdContainer Container {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/BulkOperation/BulkOperationEdmModel.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationEdmModel.cs new file mode 100644 index 0000000000..0ccc5aef7e --- /dev/null +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/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/BulkOperation/BulkOperationPatchHandlers.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationPatchHandlers.cs new file mode 100644 index 0000000000..8670cce221 --- /dev/null +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationPatchHandlers.cs @@ -0,0 +1,1211 @@ +//----------------------------------------------------------------------------- +// +// 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.Common; +using Microsoft.AspNet.OData.Extensions; +using Microsoft.OData.Edm; +using Microsoft.OData.UriParser; +using Microsoft.Test.E2E.AspNet.OData.BulkOperation; + +namespace Microsoft.Test.E2E.AspNet.OData.BulkOperation +{ + internal class APIHandlerFactory : ODataAPIHandlerFactory + { + public APIHandlerFactory(IEdmModel model) : base(model) + { + } + + public override IODataAPIHandler GetHandler(ODataPath odataPath) + { + if (odataPath != null) + { + int currentPosition = 0; + + if (odataPath.Count == 1) + { + GetHandlerInternal(odataPath.FirstSegment.Identifier, currentPosition); + } + + List pathSegments = odataPath.GetSegments(); + + ODataPathSegment currentPathSegment = pathSegments[currentPosition]; + + if (currentPathSegment is EntitySetSegment || currentPathSegment is NavigationPropertySegment || currentPathSegment is SingletonSegment) + { + int keySegmentPosition = ODataPathHelper.GetNextKeySegmentPosition(pathSegments, currentPosition); + KeySegment keySegment = (KeySegment)pathSegments[keySegmentPosition]; + + currentPosition = keySegmentPosition; + + return GetHandlerInternal( + currentPathSegment.Identifier, + currentPosition, + ODataPathHelper.KeySegmentAsDictionary(keySegment), + pathSegments); + } + } + + return null; + } + + private IODataAPIHandler GetHandlerInternal( + string pathName, + int currentPosition, + Dictionary keys = null, + List pathSegments = null) + { + switch (pathName) + { + case "Employees": + Employee employee; + string msg; + if ((new EmployeeAPIHandler().TryGet(keys, out employee, out msg)) == ODataAPIResponseStatus.Success) + { + return GetNestedHandlerForEmployee(pathSegments, currentPosition, employee); + } + return null; + case "Companies": + return new CompanyAPIHandler(); + + default: + return null; + } + } + + private static IODataAPIHandler GetNestedHandlerForEmployee(List pathSegments, int currentPosition, Employee employee) + { + ++currentPosition; + + if (pathSegments.Count <= currentPosition) + { + return null; + } + + ODataPathSegment currentPathSegment = pathSegments[currentPosition]; + + if (currentPathSegment is NavigationPropertySegment) + { + int keySegmentPosition = ODataPathHelper.GetNextKeySegmentPosition(pathSegments, currentPosition); + KeySegment keySegment = (KeySegment)pathSegments[keySegmentPosition]; + Dictionary keys = ODataPathHelper.KeySegmentAsDictionary(keySegment); + + currentPosition = keySegmentPosition; + + switch (currentPathSegment.Identifier) + { + case "NewFriends": + ODataPathSegment nextPathSegment = pathSegments[++currentPosition]; + + if (nextPathSegment is TypeSegment) + { + currentPosition++; + TypeSegment typeSegment = nextPathSegment as TypeSegment; + + if (typeSegment.Identifier == "Microsoft.Test.E2E.AspNet.OData.BulkOperation.MyNewFriend") + { + MyNewFriend friend = employee.NewFriends.FirstOrDefault(x => x.Id == (int)keys["Id"]) as MyNewFriend; + + if (friend != null) + { + switch (pathSegments[++currentPosition].Identifier) + { + case "MyNewOrders": + return new MyNewOrderAPIHandler(friend); + + default: + return null; + + } + } + } + } + else + { + NewFriend friend = employee.NewFriends.FirstOrDefault(x => x.Id == (int)keys["Id"]); + + if (friend != null) + { + switch (pathSegments[++currentPosition].Identifier) + { + case "NewOrders": + return new NewOrderAPIHandler(friend); + + default: + return null; + } + } + } + return null; + + default: + return null; + } + } + return null; + } + } + + internal class TypelessAPIHandlerFactory : ODataEdmAPIHandlerFactory + { + IEdmEntityType entityType; + + public TypelessAPIHandlerFactory(IEdmModel model, IEdmEntityType entityType) : base(model) + { + this.entityType = entityType; + } + + public override EdmODataAPIHandler GetHandler(ODataPath odataPath) + { + if (odataPath != null) + { + string pathName = odataPath.GetLastNonTypeNonKeySegment().Identifier; + + switch (pathName) + { + case "UnTypedEmployees": + return new EmployeeEdmAPIHandler(entityType); + + default: + return null; + } + } + + return null; + } + } + + internal class CompanyAPIHandler : ODataAPIHandler + { + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out Company createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + createdObject = new Company(); + CompanyController.Companies.Add(createdObject); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + var id = keyValues.First().Value.ToString(); + var company = CompanyController.Companies.First(x => x.Id == Int32.Parse(id)); + + CompanyController.Companies.Remove(company); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out Company originalObject, out string errorMessage) + { + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; + errorMessage = string.Empty; + originalObject = null; + + try + { + var id = keyValues["Id"].ToString(); + originalObject = CompanyController.Companies.First(x => x.Id == Int32.Parse(id)); + + if (originalObject == null) + { + status = ODataAPIResponseStatus.NotFound; + } + } + catch (Exception ex) + { + status = ODataAPIResponseStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override IODataAPIHandler GetNestedHandler(Company parent, string navigationPropertyName) + { + switch (navigationPropertyName) + { + case "OverdueOrders": + return new OverdueOrderAPIHandler(parent); + case "MyOverdueOrders": + return new MyOverdueOrderAPIHandler(parent); + default: + return null; + } + } + } + + internal class OverdueOrderAPIHandler : ODataAPIHandler + { + Company parent; + + public OverdueOrderAPIHandler(Company parent) + { + this.parent = parent; + } + + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out NewOrder createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + createdObject = new NewOrder(); + parent.OverdueOrders.Add(createdObject); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + var id = keyValues.First().Value.ToString(); + var newOrders = CompanyController.OverdueOrders.First(x => x.Id == Int32.Parse(id)); + + parent.OverdueOrders.Remove(newOrders); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out NewOrder originalObject, out string errorMessage) + { + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; + errorMessage = string.Empty; + originalObject = null; + + try + { + var id = keyValues["Id"].ToString(); + originalObject = parent.OverdueOrders.FirstOrDefault(x => x.Id == Int32.Parse(id)); + + if (originalObject == null) + { + status = ODataAPIResponseStatus.NotFound; + } + } + catch (Exception ex) + { + status = ODataAPIResponseStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override IODataAPIHandler GetNestedHandler(NewOrder parent, string navigationPropertyName) + { + switch (navigationPropertyName) + { + default: + return null; + } + } + } + + internal class MyOverdueOrderAPIHandler : ODataAPIHandler + { + Company parent; + + public MyOverdueOrderAPIHandler(Company parent) + { + this.parent = parent; + } + + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out MyNewOrder createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + createdObject = new MyNewOrder(); + parent.MyOverdueOrders.Add(createdObject); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + var id = keyValues.First().Value.ToString(); + var newOrders = CompanyController.MyOverdueOrders.First(x => x.Id == Int32.Parse(id)); + + parent.MyOverdueOrders.Remove(newOrders); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out MyNewOrder originalObject, out string errorMessage) + { + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; + errorMessage = string.Empty; + originalObject = null; + + try + { + var id = keyValues["Id"].ToString(); + originalObject = parent.MyOverdueOrders.FirstOrDefault(x => x.Id == Int32.Parse(id)); + + + if (originalObject == null) + { + status = ODataAPIResponseStatus.NotFound; + } + } + catch (Exception ex) + { + status = ODataAPIResponseStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override IODataAPIHandler GetNestedHandler(MyNewOrder parent, string navigationPropertyName) + { + switch (navigationPropertyName) + { + + default: + return null; + } + } + } + + internal class EmployeeAPIHandler : ODataAPIHandler + { + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out Employee createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = null; + + try + { + createdObject = new Employee(); + EmployeesController.Employees.Add(createdObject); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + errorMessage = null; + + try + { + var id = keyValues.First().Value.ToString(); + var employee = EmployeesController.Employees.First(x => x.ID == Int32.Parse(id)); + + EmployeesController.Employees.Remove(employee); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out Employee originalObject, out string errorMessage) + { + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; + errorMessage = null; + originalObject = null; + + try + { + var id = keyValues["ID"].ToString(); + originalObject = EmployeesController.Employees.First(x => x.ID == Int32.Parse(id)); + + if (originalObject == null) + { + status = ODataAPIResponseStatus.NotFound; + } + } + catch (Exception ex) + { + status = ODataAPIResponseStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override IODataAPIHandler GetNestedHandler(Employee parent, string navigationPropertyName) + { + switch (navigationPropertyName) + { + case "Friends": + return new FriendAPIHandler(parent); + case "NewFriends": + return new NewFriendAPIHandler(parent); + default: + return null; + } + } + } + + internal class FriendAPIHandler : ODataAPIHandler + { + Employee employee; + public FriendAPIHandler(Employee employee) + { + this.employee = employee; + } + + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out Friend createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + createdObject = new Friend(); + employee.Friends.Add(createdObject); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + var id = keyValues.First().Value.ToString(); + var friend = employee.Friends.FirstOrDefault(x => x.Id == Int32.Parse(id)); + + employee.Friends.Remove(friend); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out Friend originalObject, out string errorMessage) + { + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; + errorMessage = string.Empty; + originalObject = null; + + try + { + var id = keyValues["Id"].ToString(); + originalObject = employee.Friends.FirstOrDefault(x => x.Id == Int32.Parse(id)); + + + if (originalObject == null) + { + status = ODataAPIResponseStatus.NotFound; + } + } + catch (Exception ex) + { + status = ODataAPIResponseStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override IODataAPIHandler GetNestedHandler(Friend parent, string navigationPropertyName) + { + switch (navigationPropertyName) + { + case "Orders": + return new OrderAPIHandler(parent); + default: + return null; + } + } + } + + internal class NewOrderAPIHandler : ODataAPIHandler + { + NewFriend friend; + public NewOrderAPIHandler(NewFriend friend) + { + this.friend = friend; + } + + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out NewOrder createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + createdObject = new NewOrder(); + + if (friend.NewOrders == null) + { + friend.NewOrders = new List(); + } + + friend.NewOrders.Add(createdObject); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + var id = keyValues.First().Value.ToString(); + var friend = this.friend.NewOrders.FirstOrDefault(x => x.Id == int.Parse(id)); + + this.friend.NewOrders.Remove(friend); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out NewOrder originalObject, out string errorMessage) + { + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; + errorMessage = string.Empty; + originalObject = null; + + try + { + if (friend.NewOrders != null) + { + var id = keyValues["Id"].ToString(); + originalObject = friend.NewOrders.FirstOrDefault(x => x.Id == Int32.Parse(id)); + } + + if (originalObject == null) + { + status = ODataAPIResponseStatus.NotFound; + } + } + catch (Exception ex) + { + status = ODataAPIResponseStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override IODataAPIHandler GetNestedHandler(NewOrder parent, string navigationPropertyName) + { + throw new NotImplementedException(); + } + } + + internal class MyNewOrderAPIHandler : ODataAPIHandler + { + MyNewFriend friend; + public MyNewOrderAPIHandler(MyNewFriend friend) + { + this.friend = friend; + } + + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out MyNewOrder createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + createdObject = new MyNewOrder(); + + if (friend.MyNewOrders == null) + { + friend.MyNewOrders = new List(); + } + + friend.MyNewOrders.Add(createdObject); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + var id = keyValues.First().Value.ToString(); + var friend = this.friend.MyNewOrders.FirstOrDefault(x => x.Id == int.Parse(id)); + + this.friend.MyNewOrders.Remove(friend); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out MyNewOrder originalObject, out string errorMessage) + { + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; + errorMessage = string.Empty; + originalObject = null; + + try + { + if (friend.MyNewOrders != null) + { + var id = keyValues["Id"].ToString(); + originalObject = friend.MyNewOrders.FirstOrDefault(x => x.Id == Int32.Parse(id)); + } + + if (originalObject == null) + { + status = ODataAPIResponseStatus.NotFound; + } + } + catch (Exception ex) + { + status = ODataAPIResponseStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override IODataAPIHandler GetNestedHandler(MyNewOrder parent, string navigationPropertyName) + { + throw new NotImplementedException(); + } + } + + internal class OrderAPIHandler : ODataAPIHandler + { + Friend friend; + public OrderAPIHandler(Friend friend) + { + this.friend = friend; + } + + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out Order createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + createdObject = new Order(); + + if (friend.Orders == null) + { + friend.Orders = new List(); + } + + friend.Orders.Add(createdObject); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + var id = keyValues.First().Value.ToString(); + var friend = this.friend.Orders.FirstOrDefault(x => x.Id == int.Parse(id)); + + this.friend.Orders.Remove(friend); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out Order originalObject, out string errorMessage) + { + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; + errorMessage = string.Empty; + originalObject = null; + + try + { + if (friend.Orders != null) + { + var id = keyValues["Id"].ToString(); + originalObject = friend.Orders.FirstOrDefault(x => x.Id == Int32.Parse(id)); + } + + if (originalObject == null) + { + status = ODataAPIResponseStatus.NotFound; + } + } + catch (Exception ex) + { + status = ODataAPIResponseStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override IODataAPIHandler GetNestedHandler(Order parent, string navigationPropertyName) + { + throw new NotImplementedException(); + } + } + + internal class NewFriendAPIHandler : ODataAPIHandler + { + Employee employee; + public NewFriendAPIHandler(Employee employee) + { + this.employee = employee; + } + + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out NewFriend createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + createdObject = new NewFriend(); + + if (employee.NewFriends == null) + { + employee.NewFriends = new List(); + } + + employee.NewFriends.Add(createdObject); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + var id = keyValues.First().Value.ToString(); + var friend = employee.NewFriends.First(x => x.Id == Int32.Parse(id)); + + employee.NewFriends.Remove(friend); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out NewFriend originalObject, out string errorMessage) + { + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; + errorMessage = string.Empty; + originalObject = null; + + try + { + var id = keyValues["Id"].ToString(); + + if (employee.NewFriends == null) + { + return ODataAPIResponseStatus.NotFound; + } + + originalObject = employee.NewFriends.FirstOrDefault(x => x.Id == Int32.Parse(id)); + + if (originalObject == null) + { + status = ODataAPIResponseStatus.NotFound; + } + } + catch (Exception ex) + { + status = ODataAPIResponseStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override IODataAPIHandler GetNestedHandler(NewFriend parent, string navigationPropertyName) + { + throw new NotImplementedException(); + } + } + + internal class EmployeeEdmAPIHandler : EdmODataAPIHandler + { + IEdmEntityType entityType; + public EmployeeEdmAPIHandler(IEdmEntityType entityType) + { + this.entityType = entityType; + } + + public override ODataAPIResponseStatus TryCreate(IEdmChangedObject changedObject, out IEdmStructuredObject createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + createdObject = new EdmEntityObject(entityType); + EmployeesController.EmployeesTypeless.Add(createdObject as EdmStructuredObject); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + var id = keyValues.First().Value.ToString(); + foreach (var emp in EmployeesController.EmployeesTypeless) + { + object id1; + emp.TryGetPropertyValue("ID", out id1); + + if (id == id1.ToString()) + { + EmployeesController.EmployeesTypeless.Remove(emp); + break; + } + } + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out IEdmStructuredObject originalObject, out string errorMessage) + { + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; + errorMessage = string.Empty; + originalObject = null; + + try + { + var id = keyValues["ID"].ToString(); + foreach (var emp in EmployeesController.EmployeesTypeless) + { + object id1; + emp.TryGetPropertyValue("ID", out id1); + + if (id == id1.ToString()) + { + originalObject = emp; + break; + } + } + + if (originalObject == null) + { + status = ODataAPIResponseStatus.NotFound; + } + } + catch (Exception ex) + { + status = ODataAPIResponseStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override EdmODataAPIHandler GetNestedHandler(IEdmStructuredObject parent, string navigationPropertyName) + { + switch (navigationPropertyName) + { + case "UnTypedFriends": + return new FriendTypelessAPIHandler(parent, entityType.DeclaredNavigationProperties().First().Type.Definition.AsElementType() as IEdmEntityType); + + default: + return null; + } + } + } + + internal class FriendTypelessAPIHandler : EdmODataAPIHandler + { + IEdmEntityType entityType; + EdmStructuredObject employee; + + public FriendTypelessAPIHandler(IEdmStructuredObject employee, IEdmEntityType entityType) + { + this.employee = employee as EdmStructuredObject; + this.entityType = entityType; + } + + public override ODataAPIResponseStatus TryCreate(IEdmChangedObject changedObject, out IEdmStructuredObject createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + object empid; + if (employee.TryGetPropertyValue("ID", out empid) && empid as int? == 3) + { + throw new Exception("Testing Error"); + } + + createdObject = new EdmEntityObject(entityType); + object obj; + employee.TryGetPropertyValue("UnTypedFriends", out obj); + + var friends = obj as ICollection; + + if (friends == null) + { + friends = new List(); + } + + friends.Add(createdObject); + + employee.TrySetPropertyValue("UnTypedFriends", friends); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + var id = keyValues.First().Value.ToString(); + if (id == "5") + { + throw new Exception("Testing Error"); + } + foreach (var emp in EmployeesController.EmployeesTypeless) + { + object id1; + emp.TryGetPropertyValue("ID", out id1); + + if (id == id1.ToString()) + { + object obj; + employee.TryGetPropertyValue("UnTypedFriends", out obj); + + var friends = obj as IList; + + friends.Remove(emp); + + employee.TrySetPropertyValue("UnTypedFriends", friends); + break; + } + } + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out IEdmStructuredObject originalObject, out string errorMessage) + { + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; + errorMessage = string.Empty; + originalObject = null; + + try + { + var id = keyValues["Id"].ToString(); + object obj; + employee.TryGetPropertyValue("UnTypedFriends", out obj); + + var friends = obj as IList; + + if (friends == null) + { + return ODataAPIResponseStatus.NotFound; + } + + foreach (var friend in friends) + { + object id1; + friend.TryGetPropertyValue("Id", out id1); + + if (id == id1.ToString()) + { + originalObject = friend; + break; + } + } + + if (originalObject == null) + { + status = ODataAPIResponseStatus.NotFound; + } + } + catch (Exception ex) + { + status = ODataAPIResponseStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override EdmODataAPIHandler GetNestedHandler(IEdmStructuredObject parent, string navigationPropertyName) + { + return null; + } + } +} diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationTest.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationTest.cs new file mode 100644 index 0000000000..224bd6df49 --- /dev/null +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationTest.cs @@ -0,0 +1,1124 @@ +//----------------------------------------------------------------------------- +// +// 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 Update + + [Fact] + public async Task PatchEmployee_WithUpdates() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Employees(1)"; + + var content = @"{ + 'Name':'Sql' , 'FavoriteSports' :{'Sport': 'Cricket'}, + 'Friends@odata.delta':[{'Id':1,'Name':'Test2'},{'Id':2,'Name':'Test3'}] + }"; + + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPost.Content = stringContent; + + //Act + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + //Assert + requestUri = this.BaseAddress + "/convention/Employees(1)/Friends"; + using (HttpResponseMessage response = await this.Client.GetAsync(requestUri)) + { + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsObject(); + var result = json.GetValue("value") as JArray; + + Assert.Equal(2, result.Count); + Assert.Contains("Test2", result.ToString()); + } + } + + [Fact] + public async Task PatchEmployee_WithUpdates_WithEmployees() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Employees(1)"; + + var content = @"{ + 'Name':'Sql' , + 'Friends':[{'Id':345,'Name':'Test2'},{'Id':400,'Name':'Test3'},{'Id':900,'Name':'Test93'}] + }"; + + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPost.Content = stringContent; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + //Assert + requestUri = this.BaseAddress + "/convention/Employees(1)/Friends"; + using (HttpResponseMessage response = await this.Client.GetAsync(requestUri)) + { + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsObject(); + var result = json.GetValue("value") as JArray; + + Assert.Equal(3, result.Count); + Assert.Contains("345", result.ToString()); + Assert.Contains("400", result.ToString()); + Assert.Contains("900", result.ToString()); + } + } + + [Fact] + public async Task PatchEmployee_WithUpdates_Friends() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Employees(1)/Friends"; + + var content = @"{'@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkOperation.Friend', + '@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(1)/Friends/$delta', + 'value':[{ 'Id':1,'Name':'Friend1'}, { 'Id':2,'Name':'Friend2'}] + }"; + + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + requestForPost.Headers.Add("OData-Version", "4.01"); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPost.Content = stringContent; + Client.DefaultRequestHeaders.Add("Prefer", @"odata.include-annotations=""*"""); + + var expected = "$delta\",\"value\":[{\"Id\":1,\"Name\":\"Friend1\",\"Age\":0}," + + "{\"Id\":2,\"Name\":\"Friend2\",\"Age\":0}]}"; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains(expected, json.ToString()); + } + + //Assert + requestUri = this.BaseAddress + "/convention/Employees(1)/Friends"; + using (HttpResponseMessage response = await this.Client.GetAsync(requestUri)) + { + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsObject(); + var result = json.GetValue("value") as JArray; + + Assert.Equal(2, result.Count); + Assert.Contains("Friend1", result.ToString()); + Assert.Contains("Friend2", result.ToString()); + } + } + + [Fact] + public async Task PatchEmployee_WithDeletes_Friends() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Employees(1)/Friends"; + + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(1)/Friends/$delta', + 'value':[{ '@odata.removed' : {'reason':'changed'}, 'Id':1},{ 'Id':2,'Name':'Friend2'}] + }"; + + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + requestForPost.Headers.Add("OData-Version", "4.01"); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPost.Content = stringContent; + + var expected = "$delta\",\"value\":[{\"@removed\":{\"reason\":\"changed\"}," + + "\"@id\":\""+this.BaseAddress+"/convention/Friends(1)\",\"Id\":1,\"Name\":null,\"Age\":0},{\"Id\":2,\"Name\":\"Friend2\",\"Age\":0}]}"; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains(expected.ToLower(), json.ToString().ToLower()); + } + + //Assert + requestUri = this.BaseAddress + "/convention/Employees(1)/Friends"; + using (HttpResponseMessage response = await this.Client.GetAsync(requestUri)) + { + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsObject(); + var result = json.GetValue("value") as JArray; + + Assert.Single(result); + Assert.Contains("Friend2", result.ToString()); + } + } + + [Fact] + public async Task PatchEmployee_WithDeletes_Friends_WithNestedTypes() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Employees(1)/Friends"; + + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(1)/Friends/$delta', '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkOperation.Friend', + 'value':[{ '@odata.removed' : {'reason':'changed'}, 'Id':1, 'Orders@odata.delta' :[{'Id':1,'Price': 10}, {'Id':2,'Price': 20} ] },{ 'Id':2,'Name':'Friend2'}] + }"; + + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + requestForPost.Headers.Add("OData-Version", "4.01"); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPost.Content = stringContent; + + var expected = "$delta\",\"value\":[{\"@removed\":{\"reason\":\"changed\"}," + + "\"@id\":\""+this.BaseAddress+"/convention/Friends(1)\",\"Id\":1,\"Name\":null,\"Age\":0},{\"Id\":2,\"Name\":\"Friend2\",\"Age\":0}]}"; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains(expected.ToLower(), json.ToString().ToLower()); + } + + //Assert + requestUri = this.BaseAddress + "/convention/Employees(1)/Friends"; + using (HttpResponseMessage response = await this.Client.GetAsync(requestUri)) + { + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsObject(); + var result = json.GetValue("value") as JArray; + + Assert.Single(result); + Assert.Contains("Friend2", result.ToString()); + } + } + + [Fact] + public async Task PatchEmployee_WithDeletes_Friends_WithNestedDeletes() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Employees(1)/Friends"; + + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(1)/Friends/$delta', '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkOperation.Friend', + 'value':[{ '@odata.removed' : {'reason':'changed'}, 'Id':1, 'Orders@odata.delta' :[{'@odata.removed' : {'reason':'changed'}, 'Id':1,'Price': 10}, {'Id':2,'Price': 20} ] },{ 'Id':2,'Name':'Friend2'}] + }"; + + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + requestForPost.Headers.Add("OData-Version", "4.01"); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPost.Content = stringContent; + + //Act & Assert + var expected = "$delta\",\"value\":[{\"@removed\":{\"reason\":\"changed\"}," + + "\"@id\":\""+ this.BaseAddress +"/convention/Friends(1)\",\"Id\":1,\"Name\":null,\"Age\":0},{\"Id\":2,\"Name\":\"Friend2\",\"Age\":0}]}"; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains(expected.ToLower(), json.ToString().ToLower()); + } + + requestUri = this.BaseAddress + "/convention/Employees(1)/Friends"; + using (HttpResponseMessage response = await this.Client.GetAsync(requestUri)) + { + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsObject(); + var result = json.GetValue("value") as JArray; + + Assert.Contains("Friend2", result.ToString()); + } + } + + [Fact] + public async Task PatchEmployee_WithAdds_Friends_WithAnnotations() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Employees(1)/NewFriends"; + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(1)/NewFriends/$delta', + 'value':[{ 'Id':3, 'Age':35, '@NS.Test':1}] + }"; + + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + requestForPost.Headers.Add("OData-Version", "4.01"); + requestForPost.Content = new StringContent(content); + + requestForPost.Content.Headers.ContentType= MediaTypeWithQualityHeaderValue.Parse("application/json"); + + Client.DefaultRequestHeaders.Add("Prefer", @"odata.include-annotations=""*"""); + + var expected = "$delta\",\"value\":[{\"@NS.Test\":1,\"Id\":3,\"Name\":null,\"Age\":35}]}"; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains(expected, json.ToString()); + } + } + + [Fact] + public async Task PatchEmployee_WithFailedAdds_Friends() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Employees(1)/NewFriends"; + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(1)/NewFriends/$delta', + 'value':[{ 'Id':3, 'Age':3, '@NS.Test':1}] + }"; + + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + requestForPost.Headers.Add("OData-Version", "4.01"); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPost.Content = stringContent; + Client.DefaultRequestHeaders.Add("Prefer", @"odata.include-annotations=""*"""); + + var expected = "$delta\",\"value\":[{\"@NS.Test\":1,\"Id\":3,\"Name\":null,\"Age\":3}]}"; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains(expected, json.ToString()); + } + } + + [Fact] + public async Task PatchEmployee_WithFailedDeletes_Friends() + { + //Arrange + string requestUri = this.BaseAddress + "/convention/Employees(2)/NewFriends"; + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(1)/NewFriends/$delta', + 'value':[{ '@odata.removed' : {'reason':'changed'}, 'Id':2, '@NS.Test':1}] + }"; + + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + requestForPost.Headers.Add("OData-Version", "4.01"); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPost.Content = stringContent; + Client.DefaultRequestHeaders.Add("Prefer", @"odata.include-annotations=""*"""); + + var expected = "$delta\",\"value\":[{\"@NS.Test\":1,\"@Core.DataModificationException\":" + + "{\"@type\":\"#Org.OData.Core.V1.DataModificationExceptionType\"},\"Id\":2,\"Name\":null,\"Age\":15}]}"; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains("$delta", json); + Assert.Contains(expected, json.ToString()); + } + } + + [Fact] + public async Task PatchEmployee_WithFailedOperation_WithAnnotations() + { + //Arrange + string requestUri = this.BaseAddress + "/convention/Employees(2)/NewFriends"; + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(2)/NewFriends/$delta', + 'value':[{ '@odata.removed' : {'reason':'changed'}, 'Id':2, '@Core.ContentID':3, '@NS.Test2':'testing'}] + }"; + + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + requestForPost.Headers.Add("OData-Version", "4.01"); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPost.Content = stringContent; + Client.DefaultRequestHeaders.Add("Prefer", @"odata.include-annotations=""*"""); + + var expected = "/convention/$metadata#NewFriends/$delta\",\"value\":[{\"@NS.Test2\":\"testing\",\"@Core.ContentID\":3," + + "\"@Core.DataModificationException\":{\"@type\":\"#Org.OData.Core.V1.DataModificationExceptionType\"},\"Id\":2,\"Name\":null,\"Age\":15}]}"; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var str = json.ToString(); + Assert.Contains("$delta",str); + Assert.Contains("NS.Test2", str); + Assert.Contains("Core.DataModificationException", str); + Assert.Contains(expected, str); + } + } + + [Fact] + public async Task PatchUntypedEmployee_WithAdds_Friends_Untyped() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/UnTypedEmployees"; + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(2)/UnTypedFriends/$delta', + 'value':[{ 'Id':3, 'Age':35,}] + }"; + + content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#UnTypedEmployees/$delta', + 'value':[{ 'ID':1,'Name':'Employee1', + 'UnTypedFriends@odata.delta':[{'Id':1,'Name':'Friend1'},{'Id':2,'Name':'Friend2'}] + }, + { 'ID':2,'Name':'Employee2', + 'UnTypedFriends@odata.delta':[{'Id':3,'Name':'Friend3'},{'Id':4,'Name':'Friend4'}] + }] + }"; + + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + requestForPost.Headers.Add("OData-Version", "4.01"); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPost.Content = stringContent; + Client.DefaultRequestHeaders.Add("Prefer", @"odata.include-annotations=""*"""); + + var expected = "/convention/$metadata#UnTypedEmployees/$delta\",\"value\":[{\"ID\":1,\"Name\":\"Employee1\",\"UnTypedFriends@delta\":" + + "[{\"Id\":1,\"Name\":\"Friend1\",\"Age\":0},{\"Id\":2,\"Name\":\"Friend2\",\"Age\":0}]},{\"ID\":2,\"Name\":\"Employee2\",\"UnTypedFriends@delta\":" + + "[{\"Id\":3,\"Name\":\"Friend3\",\"Age\":0},{\"Id\":4,\"Name\":\"Friend4\",\"Age\":0}]}]}"; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains(expected, json.ToString()); + } + } + + [Fact] + public async Task PatchEmployee_WithAdds_Friends_WithNested_Untyped() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Employees(1)/UnTypedFriends"; + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(1)/UnTypedFriends/$delta', + 'value':[{ 'Id':2, 'Name': 'Friend007', 'Age':35,'Address@odata.delta':{'Id':1, 'Street' : 'Abc 123'}, '@NS.Test':1}] + }"; + + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPost.Content = stringContent; + Client.DefaultRequestHeaders.Add("Prefer", @"odata.include-annotations=""*"""); + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + json.ToString().Contains("$delta"); + json.ToString().Contains("@NS.Test"); + } + } + + [Fact] + public async Task PatchEmployee_WithAdds_Friends_WithAnnotations_Untyped() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Employees(2)/UnTypedFriends"; + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(2)/UnTypedFriends/$delta', + 'value':[{ 'Id':2, 'Age':35, '@NS.Test':1}] + }"; + + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPost.Content = stringContent; + Client.DefaultRequestHeaders.Add("Prefer", @"odata.include-annotations=""*"""); + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + json.ToString().Contains("$delta"); + json.ToString().Contains("@NS.Test"); + } + } + + [Fact] + public async Task PatchEmployee_WithFailedAdds_Friends_Untyped() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Employees(3)/UnTypedFriends"; + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(3)/UnTypedFriends/$delta', + 'value':[{ 'Id':3, 'Age':3, '@NS.Test':1}] + }"; + + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPost.Content = stringContent; + Client.DefaultRequestHeaders.Add("Prefer", @"odata.include-annotations=""*"""); + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + json.ToString().Contains("$deletedEntity"); + } + } + + [Fact] + public async Task PatchEmployee_WithFailedDeletes_Friends_Untyped() + { + //Arrange + string requestUri = this.BaseAddress + "/convention/Employees(3)/UnTypedFriends"; + //{ '@odata.removed' : {'reason':'changed'}, 'Id':1},{ '@odata.removed' : {'reason':'deleted'}, 'Id':2}, + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(3)/UnTypedFriends/$delta', + 'value':[{ '@odata.removed' : {'reason':'changed'}, 'Id':5, '@NS.Test':1}] + }"; + + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPost.Content = stringContent; + Client.DefaultRequestHeaders.Add("Prefer", @"odata.include-annotations=""*"""); + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains("@Core.DataModificationException", json.ToString()); + Assert.Contains("@NS.Test", json.ToString()); + } + } + + [Fact] + public async Task PatchEmployee_WithFailedOperation_WithAnnotations_Untyped() + { + //Arrange + string requestUri = this.BaseAddress + "/convention/Employees(3)/UnTypedFriends"; + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(3)/UnTypedFriends/$delta', + 'value':[{ '@odata.removed' : {'reason':'changed'}, 'Id':5, '@Core.ContentID':3, '@NS.Test2':'testing'}] + }"; + + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPost.Content = stringContent; + Client.DefaultRequestHeaders.Add("Prefer", @"odata.include-annotations=""*"""); + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var str = json.ToString(); + Assert.Contains("$delta", str); + Assert.Contains("NS.Test2", str); + Assert.Contains("Core.DataModificationException", str); + Assert.Contains("Core.ContentID", str); + } + } + + [Fact] + public async Task PatchEmployee_WithUnchanged_Employee() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Employees"; + + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees/$delta', + 'value':[{ '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkOperation.Employee', 'ID':1,'Name':'Name1', + 'Friends@odata.delta':[{'Id':1,'Name':'Test0','Age':33},{'Id':2,'Name':'Test1'}] + }] + }"; + + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + requestForPost.Headers.Add("OData-Version", "4.01"); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPost.Content = stringContent; + + var expected = "\"value\":[{\"ID\":1,\"Name\":\"Name1\",\"SkillSet\":[],\"Gender\":\"0\",\"AccessLevel\":\"0\",\"FavoriteSports\":null," + + "\"Friends@delta\":[{\"Id\":1,\"Name\":\"Test0\",\"Age\":33}," + + "{\"Id\":2,\"Name\":\"Test1\",\"Age\":0}]}]}"; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains(expected, json.ToString()); + + // Navigation properties absent from the payload are not serialized in the response. + Assert.DoesNotContain("NewFriends@delta", json.ToString()); + Assert.DoesNotContain("UntypedFriends@delta", json.ToString()); + } + } + + [Fact] + public async Task PatchEmployee_WithUpdates_Employees() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Employees"; + + var content = @"{'@odata.context':'"+ this.BaseAddress + @"/convention/$metadata#Employees/$delta', + 'value':[{ '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkOperation.Employee', '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'}] + }, + { '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkOperation.Employee', '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 requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + requestForPost.Headers.Add("OData-Version", "4.01"); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPost.Content = stringContent; + + var expected = "\"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}]}]}"; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains(expected, json.ToString()); + Assert.DoesNotContain("NewFriends@delta", json.ToString()); + Assert.DoesNotContain("UntypedFriends@delta", json.ToString()); + } + + //Assert + requestUri = this.BaseAddress + "/convention/Employees(1)/Friends"; + using (HttpResponseMessage response = await this.Client.GetAsync(requestUri)) + { + response.EnsureSuccessStatusCode(); + + var json = response.Content.ReadAsStringAsync().Result; + + Assert.Contains("Friend1", json.ToString()); + Assert.Contains("Friend2", json.ToString()); + } + + requestUri = this.BaseAddress + "/convention/Employees(2)?$expand=Friends"; + using (HttpResponseMessage response = await this.Client.GetAsync(requestUri)) + { + response.EnsureSuccessStatusCode(); + + var json = response.Content.ReadAsStringAsync().Result; + + Assert.Contains("Friend3", json.ToString()); + Assert.Contains("Friend4", json.ToString()); + } + } + + [Fact] + public async Task PatchEmployee_WithDelete() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Employees(1)"; + + var content = @"{ + 'Name':'Sql' , + 'Friends@odata.delta':[{ '@odata.removed' : {'reason':'changed'}, 'Id':1}] + }"; + + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPost.Content = stringContent; + + var expected = "/convention/$metadata#Employees/$entity\",\"ID\":1,\"Name\":\"Sql\"," + + "\"SkillSet\":[\"CSharp\",\"Sql\"],\"Gender\":\"Female\",\"AccessLevel\":\"Execute\",\"FavoriteSports\":{\"Sport\":\"Football\"}}"; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains(expected, json.ToString()); + } + + //Assert + requestUri = this.BaseAddress + "/convention/Employees(1)/Friends"; + using (HttpResponseMessage response = await this.Client.GetAsync(requestUri)) + { + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsObject(); + var result = json.GetValue("value") as JArray; + + Assert.Single(result); + Assert.DoesNotContain("Test0", result.ToString()); + } + } + + [Fact] + public async Task PatchEmployee_WithODataBind() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Employees(1)?$expand=Friends"; + + var content = @"{ + 'Name':'Bind1' , + 'Friends@odata.bind':['Friends(3)'] + }"; + + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPost.Content = stringContent; + + var expected = "/convention/$metadata#Employees(Friends())/$entity\",\"ID\":1,\"Name\":\"Bind1\"," + + "\"SkillSet\":[\"CSharp\",\"Sql\"],\"Gender\":\"Female\",\"AccessLevel\":\"Execute\",\"FavoriteSports\":{\"Sport\":\"Football\"},\"Friends\":[{\"Id\":3,\"Name\":null,\"Age\":0}]}"; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains(expected, json.ToString()); + } + } + + [Fact] + public async Task PatchEmployee_WithAddUpdateAndDelete() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Employees(1)"; + + var content = @"{ + 'Name':'Sql' , + 'Friends@odata.delta':[{ '@odata.removed' : {'reason':'changed'}, 'Id':1},{'Id':2,'Name':'Test3'},{'Id':3,'Name':'Test4'}] + }"; + + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPost.Content = stringContent; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + //Assert + requestUri = this.BaseAddress + "/convention/Employees(1)/Friends"; + using (HttpResponseMessage response = await this.Client.GetAsync(requestUri)) + { + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsObject(); + var result = json.GetValue("value") as JArray; + + Assert.Equal(2, result.Count); + Assert.DoesNotContain("Test0", result.ToString()); + Assert.Contains("Test3", result.ToString()); + Assert.Contains("Test4", result.ToString()); + } + } + + [Fact] + public async Task PatchEmployee_WithMultipleUpdatesinOrder1() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Employees(1)"; + + var content = @"{ + 'Name':'Sql' , + 'Friends@odata.delta':[{ '@odata.removed' : {'reason':'changed'}, 'Id':1},{'Id':1,'Name':'Test_1'},{'Id':2,'Name':'Test3'},{'Id':3,'Name':'Test4'}] + }"; + + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPost.Content = stringContent; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + //Assert + requestUri = this.BaseAddress + "/convention/Employees(1)/Friends"; + using (HttpResponseMessage response = await this.Client.GetAsync(requestUri)) + { + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsObject(); + var result = json.GetValue("value") as JArray; + + Assert.Equal(3, result.Count); + Assert.DoesNotContain("Test0", result.ToString()); + Assert.Contains("Test_1", result.ToString()); + Assert.Contains("Test3", result.ToString()); + Assert.Contains("Test4", result.ToString()); + } + } + + [Fact] + public async Task PatchEmployee_WithMultipleUpdatesinOrder2() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Employees(1)"; + + var content = @"{ + 'Name':'Sql' , + 'Friends@odata.delta':[{ '@odata.removed' : {'reason':'changed'}, 'Id':1},{'Id':1,'Name':'Test_1'},{'Id':2,'Name':'Test3'},{'Id':3,'Name':'Test4'},{ '@odata.removed' : {'reason':'changed'}, 'Id':1}] + }"; + + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPost.Content = stringContent; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + //Assert + requestUri = this.BaseAddress + "/convention/Employees(1)/Friends"; + using (HttpResponseMessage response = await this.Client.GetAsync(requestUri)) + { + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsObject(); + var result = json.GetValue("value") as JArray; + + Assert.Equal(2, result.Count); + Assert.DoesNotContain("Test0", result.ToString()); + Assert.DoesNotContain("Test_1", result.ToString()); + Assert.Contains("Test3", result.ToString()); + Assert.Contains("Test4", result.ToString()); + } + } + + [Fact] + public async Task PatchCompanies_WithUpdates_ODataId() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Companies"; + + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Companies/$delta', + 'value':[{ '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkOperation.Company', 'Id':1,'Name':'Company01', + 'OverdueOrders@odata.delta':[{'@odata.id':'Employees(1)/NewFriends(1)/NewOrders(1)', 'Quantity': 9}] + }] + }"; + + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + requestForPost.Headers.Add("OData-Version", "4.01"); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPost.Content = stringContent; + + //Act & Assert + var expected = "/convention/$metadata#Companies/$delta\",\"value\":[{\"Id\":1,\"Name\":\"Company01\",\"OverdueOrders@delta\":" + + "[{\"Id\":0,\"Price\":0,\"Quantity\":9,\"Container\":null}]}]}"; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains(expected, json.ToString()); + } + } + + [Fact] + public async Task PatchCompanies_WithUpdates_ODataId_WithCast() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Companies"; + + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Companies/$delta', + 'value':[{ '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkOperation.Company', 'Id':1,'Name':'Company02', + 'MyOverdueOrders@odata.delta':[{'@odata.id':'Employees(2)/NewFriends(2)/Microsoft.Test.E2E.AspNet.OData.BulkOperation.MyNewFriend/MyNewOrders(2)', 'Quantity': 9}] + }] + }"; + + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + requestForPost.Headers.Add("OData-Version", "4.01"); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPost.Content = stringContent; + + //Act & Assert + var expected = "$delta\",\"value\":[{\"Id\":1,\"Name\":\"Company02\",\"MyOverdueOrders@delta\":" + + "[{\"Id\":0,\"Price\":0,\"Quantity\":9,\"Container\":null}]}]}"; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains(expected, json.ToString()); + } + } + + [Fact] + public async Task PatchUntypedEmployee_WithOdataId() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/UnTypedEmployees"; + + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#UnTypedEmployees/$delta', + 'value':[{ 'ID':1,'Name':'Employeeabcd', + 'UnTypedFriends@odata.delta':[{'@odata.id':'UnTypedEmployees(1)/UnTypedFriends(1)', 'Name':'abcd'}] + }] + }"; + + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + requestForPost.Headers.Add("OData-Version", "4.01"); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPost.Content = stringContent; + Client.DefaultRequestHeaders.Add("Prefer", @"odata.include-annotations=""*"""); + + //Act & Assert + var expected = "/convention/$metadata#UnTypedEmployees/$delta\",\"value\":[{\"ID\":1,\"Name\":\"Employeeabcd\"," + + "\"UnTypedFriends@delta\":[{\"Id\":0,\"Name\":\"abcd\",\"Age\":0}]}]}"; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains(expected, json.ToString()); + } + } + + #endregion + + #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 + } +} \ No newline at end of file diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Common/Execution/WebHostTestBase.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Common/Execution/WebHostTestBase.cs index 994308b7bd..f9f8a59ab2 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Common/Execution/WebHostTestBase.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Common/Execution/WebHostTestBase.cs @@ -43,10 +43,10 @@ protected WebHostTestBase(WebHostTestFixture fixture) /// /// protected abstract void UpdateConfiguration(WebRouteConfiguration configuration); - + public void Dispose() { - if(Client != null) + if (Client != null) { Client.Dispose(); } diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Common/Execution/WebHostTestFixture.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Common/Execution/WebHostTestFixture.cs index 276033a607..5b071bc841 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Common/Execution/WebHostTestFixture.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Common/Execution/WebHostTestFixture.cs @@ -65,7 +65,7 @@ namespace Microsoft.Test.E2E.AspNet.OData.Common.Execution public class WebHostTestFixture : IDisposable { private static readonly string NormalBaseAddressTemplate = "http://{0}:{1}"; - + private int _port; private bool disposedValue = false; private Object thisLock = new Object(); @@ -161,7 +161,7 @@ public bool Initialize(Action testConfigurationAction) .Build(); _selfHostServer.Start(); -#else +#else _selfHostServer = WebApp.Start(this.BaseAddress, DefaultKatanaConfigure); #endif } diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/ComplexTypeInheritance/ComplexTypeInheritanceControllers.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/ComplexTypeInheritance/ComplexTypeInheritanceControllers.cs index 967e0c9841..2d88199bf6 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/ComplexTypeInheritance/ComplexTypeInheritanceControllers.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/ComplexTypeInheritance/ComplexTypeInheritanceControllers.cs @@ -79,12 +79,12 @@ public TestSingleResult GetWindow([FromODataUri] int key) return TestSingleResult.Create(_windows.Where(w => w.Id == key).AsQueryable()); } - public ITestActionResult Post([FromBody]Window window) + public ITestActionResult Post([FromBody] Window window) { _windows.Add(window); window.Id = _windows.Count + 1; Rectangle rectangle = window.CurrentShape as Rectangle; - if(rectangle!=null) + if (rectangle != null) { rectangle.Fill(); } @@ -93,7 +93,7 @@ public ITestActionResult Post([FromBody]Window window) } [ODataRoute("Windows({key})")] - public ITestActionResult Patch(int key, [FromBody]Delta delta) + public ITestActionResult Patch(int key, [FromBody] Delta delta) { delta.TrySetPropertyValue("Id", key); // It is the key property, and should not be updated. @@ -117,26 +117,7 @@ public ITestActionResult Patch(int key, [FromBody]Delta delta) return Ok(window); } - [ODataRoute("Windows({key})/CurrentShape")] - public ITestActionResult PatchShape(int key, [FromBody] Delta delta) - { - Window window = _windows.First(e => e.Id == key); - var currShape = window.CurrentShape; - Shape newcurrShape = null; - - try - { - newcurrShape = delta.Patch(currShape); - } - catch (ArgumentException ae) - { - return BadRequest(ae.Message); - } - - return Ok(newcurrShape); - } - - public ITestActionResult Put(int key, [FromBody]Window window) + public ITestActionResult Put(int key, [FromBody] Window window) { if (key != window.Id) { @@ -246,7 +227,7 @@ public ITestActionResult GetOptionalShapesOfCircle(int key) } [HttpPut] - public ITestActionResult PutToCurrentShapeOfCircle(int key, [FromBody]Delta shape) + public ITestActionResult PutToCurrentShapeOfCircle(int key, [FromBody] Delta shape) { Window window = _windows.FirstOrDefault(e => e.Id == key); if (window == null) @@ -280,7 +261,7 @@ public ITestActionResult ReplaceOptionalShapes(int key, IEnumerable shape } [HttpPost] - public ITestActionResult PostToOptionalShapes(int key, [FromBody]Shape newShape) + public ITestActionResult PostToOptionalShapes(int key, [FromBody] Shape newShape) { Window window = _windows.FirstOrDefault(w => w.Id == key); if (window == null) @@ -293,7 +274,7 @@ public ITestActionResult PostToOptionalShapes(int key, [FromBody]Shape newShape) } [HttpPost] - public ITestActionResult PostToPolygonalShapes(int key, [FromBody]Polygon newPolygon) + public ITestActionResult PostToPolygonalShapes(int key, [FromBody] Polygon newPolygon) { Window window = _windows.FirstOrDefault(w => w.Id == key); if (window == null) @@ -306,13 +287,13 @@ public ITestActionResult PostToPolygonalShapes(int key, [FromBody]Polygon newPol } [HttpPatch] - public ITestActionResult PatchToOptionalShapes(int key, [FromBody]Delta shapes) + public ITestActionResult PatchToOptionalShapes(int key, [FromBody] Delta shapes) { return Ok("Not Supported"); } [HttpPatch] - public ITestActionResult PatchToCurrentShapeOfCircle(int key, [FromBody]Delta shape) + public ITestActionResult PatchToCurrentShapeOfCircle(int key, [FromBody] Delta shape) { Window window = _windows.FirstOrDefault(e => e.Id == key); if (window == null) diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/ComplexTypeInheritance/ComplexTypeInheritanceTests.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/ComplexTypeInheritance/ComplexTypeInheritanceTests.cs index 44863061cc..f8e214a2a2 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/ComplexTypeInheritance/ComplexTypeInheritanceTests.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/ComplexTypeInheritance/ComplexTypeInheritanceTests.cs @@ -302,7 +302,13 @@ public async Task PatchContainingEntity(string modelMode) 'Center':{'X':1,'Y':2}, 'HasBorder':true }, - 'OptionalShapes': [ ] + 'OptionalShapes': [ + { + '@odata.type':'#Microsoft.Test.E2E.AspNet.OData.ComplexTypeInheritance.Circle', + 'Radius':1, + 'Center':{'X':1,'Y':2}, + 'HasBorder':true + }] }"; StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); request.Content = stringContent; @@ -324,7 +330,7 @@ public async Task PatchContainingEntity(string modelMode) String.Format("\nExpected that Radius: 2, but actually: {0},\n request uri: {1},\n response payload: {2}", radius, requestUri, contentOfString)); JArray windows = contentOfJObject["OptionalShapes"] as JArray; - Assert.True(0 == windows.Count, + Assert.True(1 == windows.Count, String.Format("\nExpected count: {0},\n actual: {1},\n request uri: {2},\n response payload: {3}", 1, windows.Count, requestUri, contentOfString)); } 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..2a509e9b6c 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/DeltaQueryTests/DeltaQueryTests.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/DeltaQueryTests/DeltaQueryTests.cs @@ -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)'"); diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/DollarLevels/DollarLevelsController.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/DollarLevels/DollarLevelsController.cs index e0a530fd51..efbb31c121 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/DollarLevels/DollarLevelsController.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/DollarLevels/DollarLevelsController.cs @@ -144,7 +144,7 @@ public ITestActionResult Get(int key, ODataQueryOptions queryOptions return BadRequest(responseMessage); } - var employee = _DLEmployees.Single(e=>e.ID == key); + var employee = _DLEmployees.Single(e => e.ID == key); var appliedEmployee = queryOptions.ApplyTo(employee, new ODataQuerySettings()); return Ok(appliedEmployee, appliedEmployee.GetType()); } diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/DollarLevels/DollarLevelsDataModel.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/DollarLevels/DollarLevelsDataModel.cs index 7b8432ed87..ab0b9c5aaf 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/DollarLevels/DollarLevelsDataModel.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/DollarLevels/DollarLevelsDataModel.cs @@ -6,6 +6,7 @@ //------------------------------------------------------------------------------ using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; namespace Microsoft.Test.E2E.AspNet.OData.DollarLevels { @@ -26,4 +27,5 @@ public class DLEmployee public DLEmployee Friend { get; set; } } + } diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Formatter/DeltaTests.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Formatter/DeltaTests.cs index 26ad72d42f..b438fd4f8d 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Formatter/DeltaTests.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Formatter/DeltaTests.cs @@ -45,6 +45,7 @@ using Microsoft.AspNet.OData.Extensions; using Microsoft.AspNet.OData.Routing; using Microsoft.AspNet.OData.Routing.Conventions; +using Microsoft.AspNet.OData.Test.Builder.TestModels.Recursive; using Microsoft.OData.Client; using Microsoft.OData.Edm; using Microsoft.Test.E2E.AspNet.OData.Common; @@ -355,7 +356,8 @@ private static IEdmModel GetModel(WebRouteConfiguration config) } [Fact] - public async Task PutShouldntOverrideNavigationProperties() + //Changing the test from shouldnt to should as it override navigation properties with bulk operations + public async Task PutShouldOverrideNavigationProperties() { string putUri = BaseAddress + "/odata/DeltaCustomers(5)"; ExpandoObject data = new ExpandoObject(); @@ -366,12 +368,13 @@ public async Task PutShouldntOverrideNavigationProperties() response = await Client.SendAsync(get); Assert.True(response.IsSuccessStatusCode); dynamic query = await response.Content.ReadAsObject(); - Assert.Equal(3, query.Orders.Count); + Assert.Equal(0, query.Orders.Count); } } public class PatchtDeltaOfTTests : WebHostTestBase { + static IEdmModel model; public PatchtDeltaOfTTests(WebHostTestFixture fixture) :base(fixture) { @@ -389,15 +392,31 @@ private static IEdmModel GetModel(WebRouteConfiguration config) ODataModelBuilder builder = config.CreateConventionModelBuilder(); builder.EntitySet("DeltaCustomers"); builder.EntitySet("DeltaOrders"); - return builder.GetEdmModel(); + model = builder.GetEdmModel(); + return model; } + [Fact] public async Task PatchShouldSupportNonSettableCollectionProperties() { + var changedEntity = new EdmDeltaEntityObject(model.FindDeclaredType("Microsoft.Test.E2E.AspNet.OData.Formatter.DeltaCustomer") as IEdmEntityType); + changedEntity.TrySetPropertyValue("Id", 1); + changedEntity.TrySetPropertyValue("FathersAge", 3); + HttpRequestMessage patch = new HttpRequestMessage(new HttpMethod("PATCH"), BaseAddress + "/odata/DeltaCustomers(6)"); - dynamic data = new ExpandoObject(); - data.Addresses = Enumerable.Range(10, 3).Select(i => new DeltaAddress { ZipCode = i }); + var data = new ExpandoObject() as IDictionary; + + foreach(var prop in changedEntity.GetChangedPropertyNames()) + { + object val; + if(changedEntity.TryGetPropertyValue(prop, out val)) + { + data.Add(prop, val); + } + + } + string content = JsonConvert.SerializeObject(data); patch.Content = new StringContent(content); patch.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); @@ -405,35 +424,35 @@ public async Task PatchShouldSupportNonSettableCollectionProperties() Assert.True(response.IsSuccessStatusCode); - HttpRequestMessage get = new HttpRequestMessage(HttpMethod.Get, BaseAddress + "/odata/DeltaCustomers(6)?$expand=Orders"); + HttpRequestMessage get = new HttpRequestMessage(HttpMethod.Get, BaseAddress + "/odata/DeltaCustomers(1)?$expand=Orders"); response = await Client.SendAsync(get); Assert.True(response.IsSuccessStatusCode); dynamic query = await response.Content.ReadAsObject(); - Assert.Equal(3, query.Addresses.Count); + Assert.Equal(2, query.Addresses.Count); Assert.Equal(3, query.Orders.Count); } [Fact] public async Task PatchShouldSupportComplexDerivedTypeTransform() { - HttpRequestMessage patch = new HttpRequestMessage(new HttpMethod("MERGE"), BaseAddress + "/odata/DeltaCustomers(6)"); + HttpRequestMessage patch = new HttpRequestMessage(new HttpMethod("PATCH"), BaseAddress + "/odata/DeltaCustomers(7)"); dynamic data = new ExpandoObject(); - data.Addresses = Enumerable.Range(10, 3).Select(i => new DeltaAddress { ZipCode = i }); + data.MyAddress = new PersonalAddress { Street = "abc" }; - string content = JsonConvert.SerializeObject(data); - content = @"{'MyAddress':{'@odata.type': 'Microsoft.Test.E2E.AspNet.OData.Formatter.PersonalAddress','Street': 'abc'}}"; + string content = JsonConvert.SerializeObject(data); patch.Content = new StringContent(content); patch.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); HttpResponseMessage response = await Client.SendAsync(patch); Assert.True(response.IsSuccessStatusCode); - HttpRequestMessage get = new HttpRequestMessage(HttpMethod.Get, BaseAddress + "/odata/DeltaCustomers(6)?$expand=Orders"); + HttpRequestMessage get = new HttpRequestMessage(HttpMethod.Get, BaseAddress + "/odata/DeltaCustomers(7)?$expand=Orders"); response = await Client.SendAsync(get); Assert.True(response.IsSuccessStatusCode); dynamic query = await response.Content.ReadAsObject(); Assert.Equal("abc", query.MyAddress.Street.ToString()); } + } public class DeltaCustomersController : TestODataController @@ -450,12 +469,19 @@ static DeltaCustomersController() customer.Id = 5; customers.Add(customer); - customer = new DeltaCustomer("Original name", + var customer1 = new DeltaCustomer("Original name", Enumerable.Range(0, 2).Select(i => new DeltaAddress { ZipCode = i }), Enumerable.Range(0, 3).Select(i => new DeltaOrder { Details = i.ToString() })); - customer.Id = 6; - customer.MyAddress = new OfficeAddress { Street = "Microsot" }; - customers.Add(customer); + customer1.Id = 6; + customer1.MyAddress = new OfficeAddress { Street = "Microsot" }; + customers.Add(customer1); + + var customer2 = new DeltaCustomer("Original name", + Enumerable.Range(0, 2).Select(i => new DeltaAddress { ZipCode = i }), + Enumerable.Range(0, 3).Select(i => new DeltaOrder { Details = i.ToString() })); + customer2.Id = 7; + customer2.MyAddress = new OfficeAddress { Street = "Microsot" }; + customers.Add(customer2); } [EnableQuery(PageSize = 10, MaxExpansionDepth = 2)] @@ -480,6 +506,7 @@ public ITestActionResult Put([FromODataUri] int key, [FromBody] Delta c.Id == key).FirstOrDefault(); entity.Put(customer); return Ok(customer); + } [AcceptVerbs("PATCH", "MERGE")] @@ -490,9 +517,11 @@ public ITestActionResult Patch([FromODataUri] int key, Delta patc return BadRequest(ModelState); } var customer = customers.Where(c => c.Id == key).FirstOrDefault(); - var newCustomer = patch.Patch(customer); - Assert.True(newCustomer == customer); - return Ok(newCustomer); + if (customer == null) + return Ok(); + + patch.Patch(customer); + return Ok(customer); } } @@ -510,6 +539,7 @@ public DeltaCustomer(string name, IEnumerable addresses, IEnumerab _addresses = addresses.ToList(); Orders = orders.ToList(); } + public int Id { get; set; } private string _name = null; 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..db31d195af 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 @@ -55,6 +55,7 @@ public async Task UntypedDeltaWorksInAllFormats(string acceptHeader) string url = "/untyped/UntypedDeltaCustomers?$deltatoken=abc"; HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, BaseAddress + url); request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(acceptHeader)); + request.Headers.Add("OData-Version", "4.01"); HttpResponseMessage response = await Client.SendAsync(request); Assert.True(response.IsSuccessStatusCode); Assert.NotNull(response.Content); @@ -71,8 +72,7 @@ public async Task UntypedDeltaWorksInAllFormats(string acceptHeader) 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/OpenType/OpenTypeControllers.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/OpenType/OpenTypeControllers.cs index 6d196e01f5..74d4090d01 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/OpenType/OpenTypeControllers.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/OpenType/OpenTypeControllers.cs @@ -56,7 +56,7 @@ public ITestActionResult Get(int key) } [EnableQuery] - public ITestActionResult Post([FromBody]Employee employee) + public ITestActionResult Post([FromBody] Employee employee) { employee.Id = Employees.Count + 1; Employees.Add(employee); @@ -64,7 +64,7 @@ public ITestActionResult Post([FromBody]Employee employee) } [EnableQuery] - public ITestActionResult Put(int key, [FromBody]Employee employee) + public ITestActionResult Put(int key, [FromBody] Employee employee) { Employee originalEmployee = Employees.Single(e => e.Id == key); employee.Id = key; @@ -74,7 +74,7 @@ public ITestActionResult Put(int key, [FromBody]Employee employee) } [EnableQuery] - public ITestActionResult Patch(int key, [FromBody]Delta employee) + public ITestActionResult Patch(int key, [FromBody] Delta employee) { Employee originalEmployee = Employees.Single(e => e.Id == key); employee.Patch(originalEmployee); @@ -98,7 +98,7 @@ public ITestActionResult GetManager(int key) } [EnableQuery] - public ITestActionResult PutManager(int key, [FromBody]Manager employee) + public ITestActionResult PutManager(int key, [FromBody] Manager employee) { Manager originalEmployee = Employees.OfType().Single(e => e.Id == key); employee.Id = key; @@ -109,7 +109,7 @@ public ITestActionResult PutManager(int key, [FromBody]Manager employee) } [EnableQuery] - public ITestActionResult PatchManager(int key, [FromBody]Delta employee) + public ITestActionResult PatchManager(int key, [FromBody] Delta employee) { Manager originalEmployee = Employees.OfType().Single(e => e.Id == key); employee.Patch(originalEmployee); @@ -143,7 +143,7 @@ private static void InitAccounts() { Accounts = new List { - + new PremiumAccount() { Id = 1, @@ -183,7 +183,7 @@ private static void InitAccounts() AccountInfo = new AccountInfo() { NickName = "NickName3" - + }, Address = new Address() { @@ -342,7 +342,7 @@ public ITestActionResult GetTagsAttributeRouting(int key) } [HttpPatch] - public ITestActionResult Patch(int key, [FromBody]Delta patch, ODataQueryOptions queryOptions) + public ITestActionResult Patch(int key, [FromBody] Delta patch, ODataQueryOptions queryOptions) { IEnumerable appliedAccounts = Accounts.Where(a => a.Id == key); @@ -369,7 +369,7 @@ public ITestActionResult Patch(int key, [FromBody]Delta patch, ODataQue [HttpPatch] [ODataRoute("Accounts({key})")] - public ITestActionResult PatchAttributeRouting(int key, [FromBody]Delta patch, ODataQueryOptions queryOptions) + public ITestActionResult PatchAttributeRouting(int key, [FromBody] Delta patch, ODataQueryOptions queryOptions) { IEnumerable appliedAccounts = Accounts.Where(a => a.Id == key); @@ -395,7 +395,7 @@ public ITestActionResult PatchAttributeRouting(int key, [FromBody]Delta } [HttpPut] - public ITestActionResult Put(int key, [FromBody]Account account) + public ITestActionResult Put(int key, [FromBody] Account account) { if (key != account.Id) { @@ -410,7 +410,7 @@ public ITestActionResult Put(int key, [FromBody]Account account) [HttpPut] [ODataRoute("Accounts({key})")] - public ITestActionResult PutAttributeRouting(int key, [FromBody]Account account) + public ITestActionResult PutAttributeRouting(int key, [FromBody] Account account) { if (key != account.Id) { @@ -424,7 +424,7 @@ public ITestActionResult PutAttributeRouting(int key, [FromBody]Account account) } [HttpPost] - public ITestActionResult Post([FromBody]Account account) + public ITestActionResult Post([FromBody] Account account) { account.Id = Accounts.Count + 1; account.DynamicProperties["OwnerGender"] = Gender.Male;// Defect 2371564 odata.type is missed in client payload for dynamic enum type @@ -435,7 +435,7 @@ public ITestActionResult Post([FromBody]Account account) [HttpPost] [ODataRoute("Accounts")] - public ITestActionResult PostAttributeRouting([FromBody]Account account) + public ITestActionResult PostAttributeRouting([FromBody] Account account) { account.Id = Accounts.Count + 1; Accounts.Add(account); @@ -475,7 +475,7 @@ public ITestActionResult DeleteAttributeRouting(int key) } [HttpPatch] - public ITestActionResult PatchToAddress(int key, [FromBody]Delta
address) + public ITestActionResult PatchToAddress(int key, [FromBody] Delta
address) { Account account = Accounts.FirstOrDefault(a => a.Id == key); if (account == null) @@ -488,13 +488,13 @@ public ITestActionResult PatchToAddress(int key, [FromBody]Delta
addres account.Address = new Address(); } - account.Address = address.Patch(account.Address); + address.Patch(account.Address); return Updated(account); } [HttpPatch] - public ITestActionResult PatchToAddressOfGlobalAddress(int key, [FromBody]Delta address) + public ITestActionResult PatchToAddressOfGlobalAddress(int key, [FromBody] Delta address) { Account account = Accounts.FirstOrDefault(a => a.Id == key); if (account == null) @@ -513,7 +513,7 @@ public ITestActionResult PatchToAddressOfGlobalAddress(int key, [FromBody]Delta< } [HttpPut] - public ITestActionResult PutToAddress(int key, [FromBody]Delta
address) + public ITestActionResult PutToAddress(int key, [FromBody] Delta
address) { Account account = Accounts.FirstOrDefault(a => a.Id == key); if (account == null) @@ -577,7 +577,7 @@ public AccountInfo IncreaseAgeActionAttributeRouting(int key) [HttpPost] [ODataRoute("UpdateAddressAction")] - public Address UpdateAddressActionAttributeRouting([FromBody]ODataActionParameters parameters) + public Address UpdateAddressActionAttributeRouting([FromBody] ODataActionParameters parameters) { var id = (int)parameters["ID"]; var address = parameters["Address"] as Address; @@ -589,7 +589,7 @@ public Address UpdateAddressActionAttributeRouting([FromBody]ODataActionParamete [HttpPost] [ODataRoute("Accounts({key})/Microsoft.Test.E2E.AspNet.OData.OpenType.AddShipAddress")] - public ITestActionResult AddShipAddress(int key, [FromBody]ODataActionParameters parameters) + public ITestActionResult AddShipAddress(int key, [FromBody] ODataActionParameters parameters) { Account account = Accounts.Single(c => c.Id == key); if (account.DynamicProperties["ShipAddresses"] == null) diff --git a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/DeltaSetOfTTest.cs b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/DeltaSetOfTTest.cs new file mode 100644 index 0000000000..7448838320 --- /dev/null +++ b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/DeltaSetOfTTest.cs @@ -0,0 +1,430 @@ +//----------------------------------------------------------------------------- +// +// 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.Builder; +using Microsoft.AspNet.OData.Formatter.Deserialization; +using Microsoft.AspNet.OData.Test.Abstraction; +using Microsoft.OData.Edm; +using Microsoft.OData.UriParser; +using Xunit; + +namespace Microsoft.AspNet.OData.Test +{ + public class DeltaSetOfTTest + { + public static List friends; + + [Fact] + public void DeltaSet_Patch() + { + //Arrange + var lstId = new List(); + lstId.Add("Id"); + var deltaSet = new DeltaSet(lstId); + + var edmChangedObj1 = new Delta(); + edmChangedObj1.TrySetPropertyValue("Id", 1); + edmChangedObj1.TrySetPropertyValue("Name", "Friend1"); + + var edmChangedObj2 = new Delta(); + edmChangedObj2.TrySetPropertyValue("Id", 2); + edmChangedObj2.TrySetPropertyValue("Name", "Friend2"); + + ODataConventionModelBuilder builder = ODataConventionModelBuilderFactory.Create(); + var friendsSet = builder.EntitySet("Friends"); + var model = builder.GetEdmModel(); + + var keys = new[] { new KeyValuePair("Id", 1) }; + var lst1 = new List(); + lst1.Add(new EntitySetSegment(model.EntityContainer.FindEntitySet("Friends")) { Identifier = "NewFriends" }); + lst1.Add(new KeySegment(keys, null, null)); + + var keys2 = new[] { new KeyValuePair("Id", 2) }; + var lst2 = new List(); + lst2.Add(new EntitySetSegment(model.EntityContainer.FindEntitySet("Friends")) { Identifier = "NewFriends" }); + lst2.Add(new KeySegment(keys2, null, null)); + + edmChangedObj1.ODataPath = new ODataPath(lst1); + edmChangedObj2.ODataPath = new ODataPath(lst2); + + deltaSet.Add(edmChangedObj1); + deltaSet.Add(edmChangedObj2); + + var friends = new List(); + friends.Add(new Friend { Id = 1, Name = "Test1" }); + friends.Add(new Friend { Id = 2, Name = "Test2" }); + + //Act + deltaSet.Patch(friends); + + //Assert + Assert.Equal(2, friends.Count); + Assert.Equal("Friend1", friends[0].Name); + Assert.Equal("Friend2", friends[1].Name); + + } + + + [Fact] + public void DeltaSet_Add_WrongItem_ThrowsError() + { + //Assign + + var edmChangedObjectcollection = new DeltaSet(new List() { "Id" }); + + var edmChangedObj1 = new Delta(); + edmChangedObj1.TrySetPropertyValue("Id", 1); + edmChangedObj1.TrySetPropertyValue("Name", "Friend1"); + + //Act & Assert + Assert.Throws(() => edmChangedObjectcollection.Add(edmChangedObj1)); + } + + + + [Fact] + public void DeltaSet_Patch_WithDeletes() + { + //Arrange + var deltaSet = new DeltaSet(new List() { "Id" }); + + + ODataConventionModelBuilder builder = ODataConventionModelBuilderFactory.Create(); + var friendsSet = builder.EntitySet("Friends"); + var model = builder.GetEdmModel(); + + var edmChangedObj1 = new Delta(); + edmChangedObj1.TrySetPropertyValue("Id", 1); + edmChangedObj1.TrySetPropertyValue("Name", "Friend1"); + + var edmChangedObj2 = new DeltaDeletedEntityObject(); + edmChangedObj2.TrySetPropertyValue("Id", 2); + + var keys = new[] { new KeyValuePair("Id", 1) }; + var lst1 = new List(); + lst1.Add(new EntitySetSegment(model.EntityContainer.FindEntitySet("Friends")) { Identifier = "NewFriends" }); + lst1.Add(new KeySegment(keys, null, null)); + + var keys2 = new[] { new KeyValuePair("Id", 2) }; + var lst2 = new List(); + lst2.Add(new EntitySetSegment(model.EntityContainer.FindEntitySet("Friends")) { Identifier = "NewFriends" }); + lst2.Add(new KeySegment(keys2, null, null)); + + edmChangedObj1.ODataPath = new ODataPath(lst1); + edmChangedObj2.ODataPath = new ODataPath(lst2); + + + deltaSet.Add(edmChangedObj1); + deltaSet.Add(edmChangedObj2); + + friends = new List(); + friends.Add(new Friend { Id = 1, Name = "Test1" }); + friends.Add(new Friend { Id = 2, Name = "Test2" }); + + + //Act + deltaSet.Patch(new FriendPatchHandler(), new APIHandlerFactory(model)); + + //Assert + Assert.Single(friends); + Assert.Equal("Friend1", friends[0].Name); + } + + [Fact] + public void DeltaSet_Patch_WithInstanceAnnotations() + { + //Arrange + + var deltaSet = new DeltaSet((new List() { "Id" })); + ODataConventionModelBuilder builder = ODataConventionModelBuilderFactory.Create(); + var friendsSet = builder.EntitySet("Friends"); + var model = builder.GetEdmModel(); + + var edmChangedObj1 = new Delta(); + edmChangedObj1.TrySetPropertyValue("Id", 1); + edmChangedObj1.TrySetPropertyValue("Name", "Friend1"); + + var annotation = new ODataInstanceAnnotationContainer(); + annotation.AddResourceAnnotation("NS.Test1", 1); + edmChangedObj1.TrySetPropertyValue("InstanceAnnotations", annotation); + + var edmChangedObj2 = new DeltaDeletedEntityObject(); + edmChangedObj2.TrySetPropertyValue("Id", 2); + + edmChangedObj2.TransientInstanceAnnotationContainer = new ODataInstanceAnnotationContainer(); + edmChangedObj2.TransientInstanceAnnotationContainer.AddResourceAnnotation("Core.ContentID", 3); + + var keys = new[] { new KeyValuePair("Id", 1) }; + var lst1 = new List(); + lst1.Add(new EntitySetSegment(model.EntityContainer.FindEntitySet("Friends")) { Identifier = "NewFriends" }); + lst1.Add(new KeySegment(keys, null, null)); + + var keys2 = new[] { new KeyValuePair("Id", 2) }; + var lst2 = new List(); + lst2.Add(new EntitySetSegment(model.EntityContainer.FindEntitySet("Friends")) { Identifier = "NewFriends" }); + lst2.Add(new KeySegment(keys2, null, null)); + + edmChangedObj1.ODataPath = new ODataPath(lst1); + edmChangedObj2.ODataPath = new ODataPath(lst2); + + deltaSet.Add(edmChangedObj1); + deltaSet.Add(edmChangedObj2); + + friends = new List(); + friends.Add(new Friend { Id = 1, Name = "Test1" }); + friends.Add(new Friend { Id = 2, Name = "Test2" }); + + //Act + var coll = deltaSet.Patch(new FriendPatchHandler(), new APIHandlerFactory(model)).ToArray(); + + //Assert + Assert.Single(friends); + Assert.Equal("Friend1", friends[0].Name); + var changedObj = coll[0] as Delta; + Assert.NotNull(changedObj); + + object obj; + changedObj.TryGetPropertyValue("InstanceAnnotations",out obj); + var annotations = (obj as IODataInstanceAnnotationContainer).GetResourceAnnotations(); + Assert.Equal("NS.Test1", annotations.First().Key); + Assert.Equal(1, annotations.First().Value); + + DeltaDeletedEntityObject changedObj1 = coll[1] as DeltaDeletedEntityObject; + Assert.NotNull(changedObj1); + + annotations = changedObj1.TransientInstanceAnnotationContainer.GetResourceAnnotations(); + Assert.Equal("Core.ContentID", annotations.First().Key); + Assert.Equal(3, annotations.First().Value); + } + + [Fact] + public void DeltaSet_Patch_WithNestedDelta() + { + //Arrange + ODataConventionModelBuilder builder = ODataConventionModelBuilderFactory.Create(); + var friendsSet = builder.EntitySet("Friends"); + var model = builder.GetEdmModel(); + + var lstId = new List(); + lstId.Add("Id"); + + var deltaSet = new DeltaSet(lstId); + + var deltaSet1 = new DeltaSet(lstId); + + var keys = new[] { new KeyValuePair("Id", 1) }; + var lst1 = new List(); + lst1.Add(new EntitySetSegment(model.EntityContainer.FindEntitySet("Friends")) { Identifier = "NewFriends" }); + lst1.Add(new KeySegment(keys, null, null)); + + var keys2 = new[] { new KeyValuePair("Id", 2) }; + var lst2 = new List(); + lst2.Add(new EntitySetSegment(model.EntityContainer.FindEntitySet("Friends")) { Identifier = "NewFriends" }); + lst2.Add(new KeySegment(keys2, null, null)); + + var edmNewObj1 = new Delta(); + edmNewObj1.TrySetPropertyValue("Id", 1); + edmNewObj1.TrySetPropertyValue("Name", "NewFriend1"); + + var edmNewObj2 = new Delta(); + edmNewObj2.TrySetPropertyValue("Id", 2); + edmNewObj2.TrySetPropertyValue("Name", "NewFriend2"); + + edmNewObj1.ODataPath = new ODataPath(lst1); + edmNewObj2.ODataPath = new ODataPath(lst2); + + deltaSet1.Add(edmNewObj1); + deltaSet1.Add(edmNewObj2); + + var deltaSet2 = new DeltaSet(lstId); + + var edmNewObj21 = new Delta(); + edmNewObj21.TrySetPropertyValue("Id", 3); + edmNewObj21.TrySetPropertyValue("Name", "NewFriend3"); + + var edmNewObj22 = new Delta(); + edmNewObj22.TrySetPropertyValue("Id", 4); + edmNewObj22.TrySetPropertyValue("Name", "NewFriend4"); + + var keys3 = new[] { new KeyValuePair("Id", 3) }; + var lst3 = new List(); + lst3.Add(new EntitySetSegment(model.EntityContainer.FindEntitySet("Friends")) { Identifier = "NewFriends" }); + lst3.Add(new KeySegment(keys3, null, null)); + + var keys4 = new[] { new KeyValuePair("Id", 4) }; + var lst4 = new List(); + lst4.Add(new EntitySetSegment(model.EntityContainer.FindEntitySet("Friends")) { Identifier = "NewFriends" }); + lst4.Add(new KeySegment(keys4, null, null)); + + edmNewObj21.ODataPath = new ODataPath(lst3); + edmNewObj22.ODataPath = new ODataPath(lst4); + + + var edmChangedObj1 = new Delta(); + edmChangedObj1.TrySetPropertyValue("Id", 1); + edmChangedObj1.TrySetPropertyValue("Name", "Friend1"); + edmChangedObj1.TrySetPropertyValue("NewFriends", deltaSet1); + + var edmChangedObj2 = new Delta(); + edmChangedObj2.TrySetPropertyValue("Id", 2); + edmChangedObj2.TrySetPropertyValue("Name", "Friend2"); + edmChangedObj2.TrySetPropertyValue("NewFriends", deltaSet2); + + + deltaSet2.Add(edmNewObj21); + deltaSet2.Add(edmNewObj22); + + var keys1 = new[] { new KeyValuePair("Id", 2) }; + var lst = new List(); + lst.Add(new EntitySetSegment(model.EntityContainer.FindEntitySet("Friends")) { Identifier = "Friends" }); + lst.Add(new KeySegment(keys1, null, null)); + + edmChangedObj1.ODataPath = new ODataPath(lst1); + edmChangedObj2.ODataPath = new ODataPath(lst2); + + deltaSet.Add(edmChangedObj1); + deltaSet.Add(edmChangedObj2); + + friends = new List(); + friends.Add(new Friend { Id = 1, Name = "Test1" }); + friends.Add(new Friend { Id = 2, Name = "Test2", NewFriends= new List() { new NewFriend {Id=3, Name="Test33" }, new NewFriend { Id = 4, Name = "Test44" } } }); + + //Act + deltaSet.Patch(new FriendPatchHandler(), new APIHandlerFactory(model)); + + //Assert + Assert.Equal(2, friends.Count); + Assert.Equal("Friend1", friends[0].Name); + Assert.Equal("Friend2", friends[1].Name); + + Assert.Equal(2, friends[0].NewFriends.Count); + Assert.Equal(2, friends[1].NewFriends.Count); + + Assert.Equal("NewFriend1", friends[0].NewFriends[0].Name); + Assert.Equal("NewFriend2", friends[0].NewFriends[1].Name); + Assert.Equal("NewFriend3", friends[1].NewFriends[0].Name); + Assert.Equal("NewFriend4", friends[1].NewFriends[1].Name); + } + + } + + internal class APIHandlerFactory : ODataAPIHandlerFactory + { + public APIHandlerFactory(IEdmModel model): base(model) + { + + } + + public override IODataAPIHandler GetHandler(ODataPath path) + { + if (path != null) + { + switch (path.LastSegment.Identifier) + { + case "Friend": + return new FriendPatchHandler(); + + default: + return null; + } + } + + return null; + } + } + internal class FriendPatchHandler : ODataAPIHandler + { + public override IODataAPIHandler GetNestedHandler(Friend parent, string navigationPropertyName) + { + return new NewFriendPatchHandler(parent); + } + + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out Friend createdObject, out string errorMessage) + { + createdObject = new Friend(); + DeltaSetOfTTest.friends.Add(createdObject); + errorMessage = string.Empty; + return ODataAPIResponseStatus.Success; + } + + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + int id = Int32.Parse( keyValues.First().Value.ToString()); + + DeltaSetOfTTest.friends.Remove(DeltaSetOfTTest.friends.First(x => x.Id == id)); + errorMessage = string.Empty; + + return ODataAPIResponseStatus.Success; + } + + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out Friend originalObject, out string errorMessage) + { + int id = Int32.Parse(keyValues.First().Value.ToString()); + originalObject = DeltaSetOfTTest.friends.First(x => x.Id == id); + errorMessage = string.Empty; + + return ODataAPIResponseStatus.Success; + } + } + + internal class NewFriendPatchHandler : ODataAPIHandler + { + Friend parent; + public NewFriendPatchHandler(Friend parent) + { + this.parent = parent; + } + + public override IODataAPIHandler GetNestedHandler(NewFriend parent, string navigationPropertyName) + { + throw new NotImplementedException(); + } + + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out NewFriend createdObject, out string errorMessage) + { + createdObject = new NewFriend(); + if(parent.NewFriends == null) + { + parent.NewFriends = new List(); + } + + parent.NewFriends.Add(createdObject); + errorMessage = string.Empty; + return ODataAPIResponseStatus.Success; + } + + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + int id = Int32.Parse(keyValues.First().Value.ToString()); + + parent.NewFriends.Remove(parent.NewFriends.First(x => x.Id == id)); + errorMessage = string.Empty; + + return ODataAPIResponseStatus.Success; + } + + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out NewFriend originalObject, out string errorMessage) + { + errorMessage = string.Empty; + originalObject = null; + + if(parent.NewFriends == null) + { + return ODataAPIResponseStatus.NotFound; + } + + int id = Int32.Parse(keyValues.First().Value.ToString()); + originalObject = parent.NewFriends.FirstOrDefault(x => x.Id == id); + errorMessage = string.Empty; + + return originalObject!=null? ODataAPIResponseStatus.Success : ODataAPIResponseStatus.NotFound; + } + } +} diff --git a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/EdmChangedObjectCollectionTest.cs b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/EdmChangedObjectCollectionTest.cs index 511934dfcb..df985801ab 100644 --- a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/EdmChangedObjectCollectionTest.cs +++ b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/EdmChangedObjectCollectionTest.cs @@ -5,13 +5,61 @@ // //------------------------------------------------------------------------------ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Microsoft.AspNet.OData.Builder; +using Microsoft.AspNet.OData.Test.Abstraction; using Microsoft.AspNet.OData.Test.Common; using Microsoft.OData.Edm; +using Microsoft.OData.UriParser; using Moq; using Xunit; namespace Microsoft.AspNet.OData.Test { + internal class TypelessAPIHandlerFactory : ODataEdmAPIHandlerFactory + { + IEdmEntityType entityType; + IEdmStructuredObject employee; + + protected TypelessAPIHandlerFactory(IEdmModel model): base(model) + { + + } + + public TypelessAPIHandlerFactory(IEdmEntityType entityType, IEdmModel model): base(model) + { + this.entityType = entityType; + } + + public TypelessAPIHandlerFactory(IEdmEntityType entityType, IEdmStructuredObject employee, IEdmModel model) : base(model) + { + this.entityType = entityType; + this.employee = employee; + } + + public override EdmODataAPIHandler GetHandler(ODataPath path) + { + if (path != null) + { + switch (path.LastSegment.Identifier) + { + case "UnTypedFriend": + case "Friend": + return new FriendTypelessPatchHandler(entityType); + + default: + return null; + } + } + + return null; + } + + } + public class EdmChangedObjectCollectionTest { [Fact] @@ -36,5 +84,465 @@ public void GetEdmType_Returns_EdmTypeInitializedByCtor() Assert.Same(_entityType, collectionTypeReference.ElementType().Definition); } - } + + public static List friends = new List(); + + internal void InitFriends() + { + friends = new List(); + EdmEntityType _entityType = new EdmEntityType("Microsoft.AspNet.OData.Test", "Friend"); + _entityType.AddKeys(_entityType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); + _entityType.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); + + EdmEntityType _entityType1 = new EdmEntityType("Microsoft.AspNet.OData.Test", "NewFriend"); + _entityType1.AddKeys(_entityType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); + _entityType1.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); + + var friend1 = new EdmEntityObject(_entityType); + friend1.TrySetPropertyValue("Id", 1); + friend1.TrySetPropertyValue("Name", "Test1"); + + var friend2 = new EdmEntityObject(_entityType); + friend2.TrySetPropertyValue("Id", 2); + friend2.TrySetPropertyValue("Name", "Test2"); + + var nfriend1 = new EdmEntityObject(_entityType1); + nfriend1.TrySetPropertyValue("Id", 1); + nfriend1.TrySetPropertyValue("Name", "Test1"); + + var nfriend2 = new EdmEntityObject(_entityType1); + nfriend2.TrySetPropertyValue("Id", 2); + nfriend2.TrySetPropertyValue("Name", "Test2"); + + var nfriends = new List(); + nfriends.Add(nfriend1); + nfriends.Add(nfriend2); + + friend1.TrySetPropertyValue("NewFriends", nfriends); + + friends.Add(friend1); + friends.Add(friend2); + } + + + [Fact] + public void EdmChangedObjectCollection_Patch() + { + //Assign + InitFriends(); + EdmEntityType _entityType = new EdmEntityType("Microsoft.AspNet.OData.Test", "Friend"); + _entityType.AddKeys(_entityType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); + _entityType.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); + + var lstId = new List(); + lstId.Add("Id"); + var deltaSet = new EdmChangedObjectCollection(_entityType); + + var edmChangedObj1 = new EdmDeltaEntityObject(_entityType); + edmChangedObj1.TrySetPropertyValue("Id", 1); + edmChangedObj1.TrySetPropertyValue("Name", "Friend1"); + + var edmChangedObj2 = new EdmDeltaEntityObject(_entityType); + edmChangedObj2.TrySetPropertyValue("Id", 2); + edmChangedObj2.TrySetPropertyValue("Name", "Friend2"); + + ODataConventionModelBuilder builder = ODataConventionModelBuilderFactory.Create(); + var friendsSet = builder.EntitySet("Friends"); + var model = builder.GetEdmModel(); + + var keys = new[] { new KeyValuePair("Id", 1) }; + var lst1 = new List(); + lst1.Add(new EntitySetSegment(model.EntityContainer.FindEntitySet("Friends")) { Identifier = "NewFriends" }); + lst1.Add(new KeySegment(keys, null, null)); + + var keys2 = new[] { new KeyValuePair("Id", 2) }; + var lst2 = new List(); + lst2.Add(new EntitySetSegment(model.EntityContainer.FindEntitySet("Friends")) { Identifier = "NewFriends" }); + lst2.Add(new KeySegment(keys2, null, null)); + + edmChangedObj1.ODataPath = new ODataPath(lst1); + edmChangedObj2.ODataPath = new ODataPath(lst2); + + deltaSet.Add(edmChangedObj1); + deltaSet.Add(edmChangedObj2); + + //Act + deltaSet.Patch(new FriendTypelessPatchHandler(_entityType), new TypelessAPIHandlerFactory(_entityType, null)); + + //Assert + Assert.Equal(2, friends.Count); + object obj; + friends[0].TryGetPropertyValue("Name", out obj); + Assert.Equal("Friend1", obj ); + friends[1].TryGetPropertyValue("Name", out obj); + Assert.Equal("Friend2", obj); + } + + [Fact] + public void EdmChangedObjectCollection_Patch_WithDeletes() + { + //Assign + InitFriends(); + EdmEntityType _entityType = new EdmEntityType("Microsoft.AspNet.OData.Test", "Friend"); + _entityType.AddKeys(_entityType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); + _entityType.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); + + var changedObjCollection = new EdmChangedObjectCollection(_entityType); + + var edmChangedObj1 = new EdmDeltaEntityObject(_entityType); + edmChangedObj1.TrySetPropertyValue("Id", 1); + edmChangedObj1.TrySetPropertyValue("Name", "Friend1"); + + var edmChangedObj2 = new EdmDeltaDeletedEntityObject(_entityType); + edmChangedObj2.TrySetPropertyValue("Id", 2); + edmChangedObj2.TrySetPropertyValue("Name", "Friend2"); + + ODataConventionModelBuilder builder = ODataConventionModelBuilderFactory.Create(); + var friendsSet = builder.EntitySet("Friends"); + var model = builder.GetEdmModel(); + + var keys = new[] { new KeyValuePair("Id", 1) }; + var lst1 = new List(); + lst1.Add(new EntitySetSegment(model.EntityContainer.FindEntitySet("Friends")) { Identifier = "NewFriends" }); + lst1.Add(new KeySegment(keys, null, null)); + + var keys2 = new[] { new KeyValuePair("Id", 2) }; + var lst2 = new List(); + lst2.Add(new EntitySetSegment(model.EntityContainer.FindEntitySet("Friends")) { Identifier = "NewFriends" }); + lst2.Add(new KeySegment(keys2, null, null)); + + edmChangedObj1.ODataPath = new ODataPath(lst1); + edmChangedObj2.ODataPath = new ODataPath(lst2); + + changedObjCollection.Add(edmChangedObj1); + changedObjCollection.Add(edmChangedObj2); + + //Act + changedObjCollection.Patch(new FriendTypelessPatchHandler(_entityType), new TypelessAPIHandlerFactory(_entityType, null)); + + //Assert + Assert.Single(friends); + object obj; + friends[0].TryGetPropertyValue("Name", out obj); + Assert.Equal("Friend1", obj); + + } + + [Fact] + public void EdmChangedObjectCollection_Patch_WithInstanceAnnotations() + { + //Assign + InitFriends(); + EdmEntityType _entityType = new EdmEntityType("Microsoft.AspNet.OData.Test", "Friend"); + _entityType.AddKeys(_entityType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); + _entityType.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); + + var changedObjCollection = new EdmChangedObjectCollection(_entityType); + + var edmChangedObj1 = new EdmDeltaEntityObject(_entityType); + edmChangedObj1.TrySetPropertyValue("Id", 1); + edmChangedObj1.TrySetPropertyValue("Name", "Friend1"); + edmChangedObj1.PersistentInstanceAnnotationsContainer = new ODataInstanceAnnotationContainer(); + edmChangedObj1.PersistentInstanceAnnotationsContainer.AddResourceAnnotation("NS.Test", 1); + + var edmChangedObj2 = new EdmDeltaEntityObject(_entityType); + edmChangedObj2.TrySetPropertyValue("Id", 2); + edmChangedObj2.TrySetPropertyValue("Name", "Friend2"); + + ODataConventionModelBuilder builder = ODataConventionModelBuilderFactory.Create(); + var friendsSet = builder.EntitySet("Friends"); + var model = builder.GetEdmModel(); + + var keys = new[] { new KeyValuePair("Id", 1) }; + var lst1 = new List(); + lst1.Add(new EntitySetSegment(model.EntityContainer.FindEntitySet("Friends")) { Identifier = "NewFriends" }); + lst1.Add(new KeySegment(keys, null, null)); + + var keys2 = new[] { new KeyValuePair("Id", 2) }; + var lst2 = new List(); + lst2.Add(new EntitySetSegment(model.EntityContainer.FindEntitySet("Friends")) { Identifier = "NewFriends" }); + lst2.Add(new KeySegment(keys2, null, null)); + + edmChangedObj1.ODataPath = new ODataPath(lst1); + edmChangedObj2.ODataPath = new ODataPath(lst2); + + changedObjCollection.Add(edmChangedObj1); + changedObjCollection.Add(edmChangedObj2); + + //Act + var coll= changedObjCollection.Patch(new FriendTypelessPatchHandler(_entityType), new TypelessAPIHandlerFactory(_entityType, null)); + + //Assert + Assert.Equal(2, friends.Count); + object obj; + friends[0].TryGetPropertyValue("Name", out obj); + Assert.Equal("Friend1", obj); + + var edmObj = coll[0] as EdmDeltaEntityObject; + + Assert.Equal("NS.Test", edmObj.PersistentInstanceAnnotationsContainer.GetResourceAnnotations().First().Key); + Assert.Equal(1, edmObj.PersistentInstanceAnnotationsContainer.GetResourceAnnotations().First().Value); + + friends[1].TryGetPropertyValue("Name", out obj); + Assert.Equal("Friend2", obj); + } + + } + + public class Friend + { + [Key] + public int Id { get; set; } + public string Name { get; set; } + public List NewFriends { get; set; } + + public IODataInstanceAnnotationContainer InstanceAnnotations { get; set; } + } + + public class NewFriend + { + public int Id { get; set; } + public string Name { get; set; } + } + + internal class FriendTypelessPatchHandler : EdmODataAPIHandler + { + IEdmEntityType entityType; + + public FriendTypelessPatchHandler(IEdmEntityType entityType) + { + this.entityType = entityType; + } + + public override ODataAPIResponseStatus TryCreate(IEdmChangedObject changedObject, out IEdmStructuredObject createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + createdObject = new EdmEntityObject(entityType); + + EdmChangedObjectCollectionTest.friends.Add(createdObject); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + var id = keyValues.First().Value.ToString(); + + foreach (var emp in EdmChangedObjectCollectionTest.friends) + { + object id1; + emp.TryGetPropertyValue("Id", out id1); + + if (id == id1.ToString()) + { + EdmChangedObjectCollectionTest.friends.Remove(emp); + + break; + } + } + + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out IEdmStructuredObject originalObject, out string errorMessage) + { + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; + errorMessage = string.Empty; + originalObject = null; + + try + { + var id = keyValues["Id"].ToString(); + + foreach (var friend in EdmChangedObjectCollectionTest.friends) + { + object id1; + friend.TryGetPropertyValue("Id", out id1); + + if (id == id1.ToString()) + { + originalObject = friend; + break; + } + } + + + if (originalObject == null) + { + status = ODataAPIResponseStatus.NotFound; + } + + } + catch (Exception ex) + { + status = ODataAPIResponseStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override EdmODataAPIHandler GetNestedHandler(IEdmStructuredObject parent, string navigationPropertyName) + { + switch (navigationPropertyName) + { + case "NewFriends": + return new NewFriendTypelessPatchHandler(parent, entityType.DeclaredNavigationProperties().First().Type.Definition.AsElementType() as IEdmEntityType); + default: + return null; + } + } + + } + + internal class NewFriendTypelessPatchHandler : EdmODataAPIHandler + { + IEdmEntityType entityType; + EdmStructuredObject friend; + + public NewFriendTypelessPatchHandler(IEdmStructuredObject friend, IEdmEntityType entityType) + { + this.entityType = entityType; + this.friend = friend as EdmStructuredObject; + } + + public override ODataAPIResponseStatus TryCreate(IEdmChangedObject changedObject, out IEdmStructuredObject createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + createdObject = new EdmEntityObject(entityType); + + object obj; + friend.TryGetPropertyValue("NewFriends", out obj); + + var nfriends = obj as List; + + nfriends.Add(createdObject); + + friend.TrySetPropertyValue("NewFriends", nfriends); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + object obj; + friend.TryGetPropertyValue("NewFriends", out obj); + + var nfriends = obj as List; + + var id = keyValues.First().Value.ToString(); + + foreach (var frnd in nfriends) + { + object id1; + frnd.TryGetPropertyValue("Id", out id1); + + if (id == id1.ToString()) + { + nfriends.Remove(frnd); + + break; + } + } + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out IEdmStructuredObject originalObject, out string errorMessage) + { + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; + errorMessage = string.Empty; + originalObject = null; + + try + { + object obj; + friend.TryGetPropertyValue("NewFriends", out obj); + + var nfriends = obj as List; + + var id = keyValues.First().Value.ToString(); + + foreach (var frnd in nfriends) + { + object id1; + frnd.TryGetPropertyValue("Id", out id1); + + if (id == id1.ToString()) + { + originalObject = frnd; + + break; + } + } + + + + if (originalObject == null) + { + status = ODataAPIResponseStatus.NotFound; + } + + } + catch (Exception ex) + { + status = ODataAPIResponseStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override EdmODataAPIHandler GetNestedHandler(IEdmStructuredObject parent, string navigationPropertyName) + { + return null; + } + + } + } diff --git a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/EnableNestedPathsTest.cs b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/EnableNestedPathsTest.cs index 0c7f86a5a4..c119273423 100644 --- a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/EnableNestedPathsTest.cs +++ b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/EnableNestedPathsTest.cs @@ -223,25 +223,6 @@ public async Task EnableNestedPaths_Returns404_WhenPathHasUnsupportedSegments(st Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } - [Fact] - public async Task EnableNestedPaths_AppliedBeforeEnableQuery() - { - // Arrange - string url = $"{_baseUrl}EnableNestedPathsCustomers(1)/Products?$orderby=Id desc"; - - // Act - HttpResponseMessage response = await _client.GetAsync(url); - - var stream = await response.Content.ReadAsStreamAsync(); - var readCustomer = ReadCollectionResponse(stream, _model); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal(_db.Customers.First().Products.OrderByDescending(p => p.Id).ToList(), - readCustomer, - new EnableNestedPathsProductComparer()); - } - private IEnumerable ReadCollectionResponse(Stream stream, IEdmModel model) { ODataMessageWrapper message = new ODataMessageWrapper(stream); diff --git a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Formatter/Deserialization/DeserializationHelpersTest.cs b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Formatter/Deserialization/DeserializationHelpersTest.cs index 1dfc32e621..87541c132f 100644 --- a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Formatter/Deserialization/DeserializationHelpersTest.cs +++ b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Formatter/Deserialization/DeserializationHelpersTest.cs @@ -329,6 +329,66 @@ public void ApplyProperty_DoesNotIgnoreKeyProperty_WithInstanceAnnotation() resource.Verify(); } + + + [Fact] + public void ReadResource_DeletedResource_WithTransientTypeAndAnnotations() + { + // Arrange + ODataConventionModelBuilder builder = ODataConventionModelBuilderFactory.Create(); + builder.EntityType(); + builder.EnumType(); + IEdmModel model = builder.GetEdmModel(); + + IEdmEntityTypeReference customerTypeReference = model.GetEdmTypeReference(typeof(SimpleOpenCustomer)).AsEntity(); + ODataDeserializerProvider _deserializerProvider = ODataDeserializerProviderFactory.Create(); + var deserializer = new ODataResourceSetDeserializer(_deserializerProvider); + + var instAnn = new List(); + instAnn.Add(new ODataInstanceAnnotation("NS.Test2", new ODataPrimitiveValue(345))); + instAnn.Add(new ODataInstanceAnnotation("Core.ContentID", new ODataPrimitiveValue(1))); + + ODataResourceBase odataResource = new ODataDeletedResource + { + Properties = new[] + { + // declared properties + new ODataProperty { Name = "CustomerId", Value = 991 }, + new ODataProperty { Name = "Name", Value = "Name #991" }, + }, + TypeName = typeof(SimpleOpenCustomer).FullName, + + InstanceAnnotations = instAnn + }; + + ODataDeserializerContext readContext = new ODataDeserializerContext() + { + Model = model + }; + + ODataResourceWrapper topLevelResourceWrapper = new ODataResourceWrapper(odataResource); + var deletedEntity = new DeltaDeletedEntityObject(); + + // Act + DeserializationHelpers.ApplyInstanceAnnotations(deletedEntity, customerTypeReference, odataResource, _deserializerProvider, readContext); + + // Assert + + //Verify Instance Annotations + object value; + deletedEntity.TryGetPropertyValue("InstanceAnnotations", out value); + var persistentAnnotations = (value as IODataInstanceAnnotationContainer).GetResourceAnnotations(); + var transientAnnotations = deletedEntity.TransientInstanceAnnotationContainer.GetResourceAnnotations(); + + Assert.Single(persistentAnnotations); + Assert.Single(transientAnnotations); + + Assert.Equal("NS.Test2", persistentAnnotations.First().Key); + Assert.Equal("Core.ContentID", transientAnnotations.First().Key); + Assert.Equal(345, persistentAnnotations.First().Value); + Assert.Equal(1, transientAnnotations.First().Value); + } + [Fact] public void ApplyProperty_FailsWithUsefulErrorMessageOnUnknownProperty() { diff --git a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Formatter/Deserialization/ODataResourceDeserializerTests.cs b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Formatter/Deserialization/ODataResourceDeserializerTests.cs index 365c361c93..c07a342983 100644 --- a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Formatter/Deserialization/ODataResourceDeserializerTests.cs +++ b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Formatter/Deserialization/ODataResourceDeserializerTests.cs @@ -158,7 +158,7 @@ public void ReadInline_Calls_ReadResource() ODataDeserializerContext readContext = new ODataDeserializerContext(); deserializer.CallBase = true; - deserializer.Setup(d => d.ReadResource(entry, _productEdmType, readContext)).Returns(42).Verifiable(); + deserializer.Setup(d => d.ReadResource(entry, _productEdmType, It.IsAny())).Returns(42).Verifiable(); // Act var result = deserializer.Object.ReadInline(entry, _productEdmType, readContext); @@ -421,9 +421,19 @@ public void ReadResource_CanReadDynamicPropertiesForOpenEntityType() TypeName = typeof(SimpleOpenCustomer).FullName }; + + IEdmEntityType entityType1 = customerTypeReference.EntityDefinition(); + EdmEntityContainer container = new EdmEntityContainer("NS", "Container"); + IEdmNavigationSource navigationSource = new EdmEntitySet(container, "EntitySet", entityType1); + + var keys = new[] { new KeyValuePair("CustomerId", 991) }; + ODataDeserializerContext readContext = new ODataDeserializerContext() { - Model = model + Model = model, + Path = new ODataPath(new ODataPathSegment[1] { + new KeySegment(keys, entityType1, navigationSource ) + }) }; ODataResourceWrapper topLevelResourceWrapper = new ODataResourceWrapper(odataResource); @@ -547,9 +557,19 @@ public void ReadResource_CanReadDynamicPropertiesForOpenEntityTypeAndAnnotations InstanceAnnotations = instAnn }; + + IEdmEntityType entityType1 = customerTypeReference.EntityDefinition(); + EdmEntityContainer container = new EdmEntityContainer("NS", "Container"); + IEdmNavigationSource navigationSource = new EdmEntitySet(container, "EntitySet", entityType1); + + var keys = new[] { new KeyValuePair("CustomerId", 991) }; + ODataDeserializerContext readContext = new ODataDeserializerContext() { - Model = model + Model = model, + Path = new ODataPath(new ODataPathSegment[1] { + new KeySegment(keys, entityType1, navigationSource ) + }) }; ODataResourceWrapper topLevelResourceWrapper = new ODataResourceWrapper(odataResource); @@ -612,6 +632,7 @@ public void ReadResource_CanReadDynamicPropertiesForOpenEntityTypeAndAnnotations IEdmModel model = builder.GetEdmModel(); IEdmEntityTypeReference customerTypeReference = model.GetEdmTypeReference(typeof(SimpleOpenCustomer)).AsEntity(); + var deserializer = new ODataResourceDeserializer(_deserializerProvider); @@ -696,9 +717,19 @@ public void ReadResource_CanReadDynamicPropertiesForOpenEntityTypeAndAnnotations InstanceAnnotations = instAnn }; + + IEdmEntityType entityType1 = customerTypeReference.EntityDefinition(); + EdmEntityContainer container = new EdmEntityContainer("NS", "Container"); + IEdmNavigationSource navigationSource = new EdmEntitySet(container, "EntitySet", entityType1); + + var keys = new[] { new KeyValuePair("CustomerId", 991) }; + ODataDeserializerContext readContext = new ODataDeserializerContext() { - Model = model + Model = model, + Path = new ODataPath(new ODataPathSegment[1] { + new KeySegment(keys, entityType1, navigationSource ) + }) }; ODataResourceWrapper topLevelResourceWrapper = new ODataResourceWrapper(odataResource); @@ -1037,7 +1068,7 @@ public void CreateResourceInstance_CreatesDeltaWith_ExpectedUpdatableProperties( Model = _readContext.Model, ResourceType = typeof(Delta) }; - var structuralProperties = _productEdmType.StructuralProperties().Select(p => p.Name); + var structuralProperties = _productEdmType.StructuralProperties().Select(p => p.Name).Union(_productEdmType.NavigationProperties().Select(p => p.Name)); // Act Delta resource = deserializer.CreateResourceInstance(_productEdmType, readContext) as Delta; @@ -1159,6 +1190,7 @@ public void ApplyNestedProperty_ThrowsArgumentNull_EntityResource() "resource"); } + [Fact] public void ApplyNestedProperty_ThrowsODataException_NavigationPropertyNotfound() { @@ -1172,35 +1204,6 @@ public void ApplyNestedProperty_ThrowsODataException_NavigationPropertyNotfound( "Cannot find nested property 'SomeProperty' on the resource type 'ODataDemo.Product'."); } - [Fact] - public void ApplyNestedProperty_ThrowsODataException_WhenPatchingNavigationProperty() - { - // Arrange - var deserializer = new ODataResourceDeserializer(_deserializerProvider); - ODataNestedResourceInfoWrapper resourceInfoWrapper = new ODataNestedResourceInfoWrapper(new ODataNestedResourceInfo { Name = "Supplier" }); - resourceInfoWrapper.NestedItems.Add(new ODataResourceWrapper(new ODataResource())); - _readContext.ResourceType = typeof(Delta); - - // Act & Assert - ExceptionAssert.Throws( - () => deserializer.ApplyNestedProperty(42, resourceInfoWrapper, _productEdmType, _readContext), - "Cannot apply PATCH to navigation property 'Supplier' on entity type 'ODataDemo.Product'."); - } - - [Fact] - public void ApplyNestedProperty_ThrowsODataException_WhenPatchingCollectionNavigationProperty() - { - // Arrange - var deserializer = new ODataResourceDeserializer(_deserializerProvider); - ODataNestedResourceInfoWrapper resourceInfoWrapper = new ODataNestedResourceInfoWrapper(new ODataNestedResourceInfo { Name = "Products" }); - resourceInfoWrapper.NestedItems.Add(new ODataResourceSetWrapper(new ODataResourceSet())); - _readContext.ResourceType = typeof(Delta); - - // Act & Assert - ExceptionAssert.Throws( - () => deserializer.ApplyNestedProperty(42, resourceInfoWrapper, _supplierEdmType, _readContext), - "Cannot apply PATCH to navigation property 'Products' on entity type 'ODataDemo.Supplier'."); - } [Fact] public void ApplyNestedProperty_UsesThePropertyAlias_ForResourceSet() @@ -1249,16 +1252,69 @@ public void ApplyNestedProperty_UsesThePropertyAlias_ForResourceWrapper() new ODataNestedResourceInfoWrapper(new ODataNestedResourceInfo { Name = "Customer" }); resourceInfoWrapper.NestedItems.Add(new ODataResourceWrapper(resource)); - ODataDeserializerContext context = new ODataDeserializerContext { Model = model.Model }; + IEdmEntityTypeReference customerTypeReference = model.Model.GetEdmTypeReference(typeof(Customer)).AsEntity(); + + IEdmEntityType entityType1 = customerTypeReference.EntityDefinition(); + EdmEntityContainer container = new EdmEntityContainer("NS", "Container"); + IEdmNavigationSource navigationSource = new EdmEntitySet(container, "EntitySet", entityType1); + + var keys = new[] { new KeyValuePair("ID", 42) }; + + ODataDeserializerContext readContext = new ODataDeserializerContext() + { + Model = model.Model + }; + // Act new ODataResourceDeserializer(_deserializerProvider) - .ApplyNestedProperty(order, resourceInfoWrapper, model.Order.AsReference(), context); + .ApplyNestedProperty(order, resourceInfoWrapper, model.Order.AsReference(), readContext); // Assert Assert.Equal(42, order.AliasedCustomer.ID); } + [Fact] + public void ApplyNestedProperty_UsesThePropertyAlias_ForResourceWrapper1() + { + // Arrange + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + model.Model.SetAnnotationValue(model.Customer, new ClrTypeAnnotation(typeof(Customer))); + model.Model.SetAnnotationValue(model.Order, new ClrTypeAnnotation(typeof(Order))); + model.Model.SetAnnotationValue( + model.Order.FindProperty("Customer"), + new ClrPropertyInfoAnnotation(typeof(Order).GetProperty("AliasedCustomer"))); + ODataResource resource = new ODataResource {Id= new Uri("http://works/"), TypeName= "NS.Order", Properties = new[] { new ODataProperty { Name = "ID", Value = 42 } } }; + + Order order = new Order(); + ODataNestedResourceInfoWrapper resourceInfoWrapper = + new ODataNestedResourceInfoWrapper(new ODataNestedResourceInfo { Name = "Customer1" }); + resourceInfoWrapper.NestedItems.Add(new ODataResourceWrapper(resource)); + + IEdmEntityTypeReference customerTypeReference = model.Model.GetEdmTypeReference(typeof(Customer)).AsEntity(); + + IEdmEntityType entityType1 = customerTypeReference.EntityDefinition(); + EdmEntityContainer container = new EdmEntityContainer("NS", "Container"); + IEdmNavigationSource navigationSource = new EdmEntitySet(container, "EntitySet", entityType1); + + var keys = new[] { new KeyValuePair("ID", 42) }; + + ODataDeserializerContext readContext = new ODataDeserializerContext() + { + Model = model.Model, + Path = new ODataPath(new ODataPathSegment[1] { + new KeySegment(keys, entityType1, navigationSource ) + }) + }; + + // Act + new ODataResourceDeserializer(_deserializerProvider) + .ApplyNestedProperty(order, resourceInfoWrapper, model.Order.AsReference(), readContext); + + // Assert + Assert.Equal(0, order.ID); + } + [Fact] public void ApplyNestedProperties_Preserves_ReadContextRequest() { @@ -1602,14 +1658,14 @@ public enum SupplierRating Bronze } - private class Customer + public class Customer { public int ID { get; set; } public Order[] AliasedOrders { get; set; } } - private class Order + public class Order { public int ID { get; set; } diff --git a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Formatter/Deserialization/ODataResourceSetDeserializerTest.cs b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Formatter/Deserialization/ODataResourceSetDeserializerTest.cs index f09a6d1e36..22756615fd 100644 --- a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Formatter/Deserialization/ODataResourceSetDeserializerTest.cs +++ b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Formatter/Deserialization/ODataResourceSetDeserializerTest.cs @@ -66,7 +66,7 @@ public void ReadInline_Throws_ArgumentMustBeOfType() ExceptionAssert.ThrowsArgument( () => deserializer.ReadInline(item: 42, edmType: _customersType, readContext: new ODataDeserializerContext()), "item", - "The argument must be of type 'ODataResourceSetWrapper'."); + "The argument must be of type 'ODataResourceSetWrapperBase'."); } [Fact] @@ -89,7 +89,7 @@ public void ReadInline_Calls_ReadFeed() deserializer.Verify(); Assert.Same(expectedResult, result); } - + [Fact] public void ReadFeed_Throws_TypeCannotBeDeserialized() { @@ -129,6 +129,59 @@ public void ReadFeed_Calls_ReadInlineForEachEntry() entityDeserializer.Verify(); } + [Fact] + public void ReadResourceSet_Calls_ReadInlineForDeltaFeeds() + { + // Arrange + Mock deserializerProvider = new Mock(); + Mock entityDeserializer = new Mock(ODataPayloadKind.Resource); + ODataResourceSetDeserializer deserializer = new ODataResourceSetDeserializer(deserializerProvider.Object); + ODataDeltaResourceSetWrapper resourceSetWrapper = new ODataDeltaResourceSetWrapper(new ODataDeltaResourceSet()); + resourceSetWrapper.Resources.Add(new ODataResourceWrapper(new ODataResource { Id = new Uri("http://a1/") })); + resourceSetWrapper.Resources.Add(new ODataResourceWrapper(new ODataResource { Id = new Uri("http://a2/") })); + ODataDeserializerContext readContext = new ODataDeserializerContext { Model = _model }; + + deserializerProvider.Setup(p => p.GetEdmTypeDeserializer(_customerType)).Returns(entityDeserializer.Object); + entityDeserializer.Setup(d => d.ReadInline(resourceSetWrapper.Resources[0], _customerType, readContext)).Returns("entry1").Verifiable(); + entityDeserializer.Setup(d => d.ReadInline(resourceSetWrapper.Resources[1], _customerType, readContext)).Returns("entry2").Verifiable(); + + + // Act + var result = deserializer.ReadResourceSet(resourceSetWrapper, _customerType, readContext).Cast().ToList(); + + // Assert + Assert.Equal(new[] { "entry1", "entry2" }, result.OfType()); + entityDeserializer.Verify(); + + } + + [Fact] + public void ReadResourceSet_Calls_ReadInlineForDeltaFeeds_WithDeletes() + { + // Arrange + Mock deserializerProvider = new Mock(); + Mock entityDeserializer = new Mock(ODataPayloadKind.Resource); + ODataResourceSetDeserializer deserializer = new ODataResourceSetDeserializer(deserializerProvider.Object); + ODataDeltaResourceSetWrapper resourceSetWrapper = new ODataDeltaResourceSetWrapper(new ODataDeltaResourceSet()); + resourceSetWrapper.Resources.Add(new ODataResourceWrapper(new ODataResource { Id = new Uri("http://a1/") })); + resourceSetWrapper.Resources.Add(new ODataResourceWrapper(new ODataResource { Id = new Uri("http://a2/") })); + resourceSetWrapper.Resources.Add(new ODataResourceWrapper(new ODataDeletedResource {TypeName=typeof(Customer).FullName, Reason= DeltaDeletedEntryReason.Deleted, Id = new Uri("http://a2/"), Properties = new List() })); + + ODataDeserializerContext readContext = new ODataDeserializerContext { Model = _model }; + + deserializerProvider.Setup(p => p.GetEdmTypeDeserializer(_customerType)).Returns(entityDeserializer.Object); + entityDeserializer.Setup(d => d.ReadInline(resourceSetWrapper.Resources[0], _customerType, readContext)).Returns("entry1").Verifiable(); + entityDeserializer.Setup(d => d.ReadInline(resourceSetWrapper.Resources[1], _customerType, readContext)).Returns("entry2").Verifiable(); + entityDeserializer.Setup(d => d.ReadInline(resourceSetWrapper.Resources[2], _customerType, readContext)).Returns("entry3").Verifiable(); + + // Act + var result = deserializer.ReadResourceSet(resourceSetWrapper, _customerType, readContext).Cast().ToList(); + + // Assert + Assert.Equal(new[] { "entry1", "entry2", "entry3" }, result.OfType()); + entityDeserializer.Verify(); + } + [Fact] public async Task Read_ReturnsEdmComplexObjectCollection_TypelessMode() { diff --git a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Formatter/InheritanceTests.cs b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Formatter/InheritanceTests.cs index 898700cf25..3cb78603db 100644 --- a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Formatter/InheritanceTests.cs +++ b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Formatter/InheritanceTests.cs @@ -774,7 +774,9 @@ public Motorcycle PatchMotorcycle_When_Expecting_Motorcycle_DerivedEngine(Delta< public Engine PatchMotorcycle_DerivedEngine(Delta patch) { - var engine = patch.Patch(motorcycle.MyEngine); + patch.Patch(motorcycle.MyEngine); + + var engine = motorcycle.MyEngine as V4; Assert.NotNull(engine); Assert.Equal(7000, engine.Hp); diff --git a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Formatter/ODataEntityReferenceLinkTests.cs b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Formatter/ODataEntityReferenceLinkTests.cs new file mode 100644 index 0000000000..ce8536d2cc --- /dev/null +++ b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Formatter/ODataEntityReferenceLinkTests.cs @@ -0,0 +1,185 @@ +//----------------------------------------------------------------------------- +// +// 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.ComponentModel.DataAnnotations; +using System.Linq; +using Microsoft.AspNet.OData.Builder; +using Microsoft.AspNet.OData.Formatter; +using Microsoft.AspNet.OData.Formatter.Deserialization; +using Microsoft.AspNet.OData.Test.Abstraction; +using Microsoft.OData; +using Microsoft.OData.Edm; +using Microsoft.OData.UriParser; +using Xunit; +using ODataPath = Microsoft.AspNet.OData.Routing.ODataPath; + +namespace Microsoft.AspNet.OData.Test.Formatter +{ + public class ODataEntityReferenceLinkTests + { + private readonly ODataDeserializerProvider _deserializerProvider; + public ODataEntityReferenceLinkTests() + { + _deserializerProvider = ODataDeserializerProviderFactory.Create(); + } + + /// + /// In OData v4.0 an ODataEntityReferenceLink will be converted + /// to a resource then deserialized as a resource. + /// + [Fact] + public void ReadResource_CanRead_AnEntityRefenceLink() + { + // Arrange + ODataConventionModelBuilder builder = ODataConventionModelBuilderFactory.Create(); + var books = builder.EntitySet("Books"); + builder.EntityType(); + builder.EntitySet("Authors"); + var author = + books.EntityType.HasOptional((e) => e.Author); + books.HasNavigationPropertyLink(author, (a, b) => new Uri("aa:b"), false); + books.HasOptionalBinding((e) => e.Author, "authorr"); + + + IEdmModel model = builder.GetEdmModel(); + IEdmEntityTypeReference bookTypeReference = model.GetEdmTypeReference(typeof(Book)).AsEntity(); + var deserializer = new ODataResourceDeserializer(_deserializerProvider); + ODataResource odataResource = new ODataResource + { + Properties = new[] + { + new ODataProperty { Name = "Id", Value = 1}, + new ODataProperty { Name = "Name", Value = "BookA"}, + }, + TypeName = "Microsoft.AspNet.OData.Test.Formatter.Book" + }; + + IEdmEntitySet entitySet = model.EntityContainer.FindEntitySet("Books"); + ODataPath path = new ODataPath(new EntitySetSegment(entitySet)); + var request = RequestFactory.CreateFromModel(model, path: path); + + ODataDeserializerContext readContext = new ODataDeserializerContext() + { + Model = model, + Request = request, + Path = path + }; + + ODataResourceWrapper topLevelResourceWrapper = new ODataResourceWrapper(odataResource); + ODataNestedResourceInfo resourceInfo = new ODataNestedResourceInfo + { + IsCollection = false, + Name = "Author" + }; + + ODataEntityReferenceLink refLink = new ODataEntityReferenceLink { Url = new Uri("http://localhost/Authors(2)") }; + ODataEntityReferenceLinkBase refLinkWrapper = new ODataEntityReferenceLinkBase(refLink); + + ODataNestedResourceInfoWrapper resourceInfoWrapper = new ODataNestedResourceInfoWrapper(resourceInfo); + resourceInfoWrapper.NestedItems.Add(refLinkWrapper); + topLevelResourceWrapper.NestedResourceInfos.Add(resourceInfoWrapper); + + // Act + Book book = deserializer.ReadResource(topLevelResourceWrapper, bookTypeReference, readContext) + as Book; + + // Assert + Assert.NotNull(book); + Assert.Equal(2, book.Author.Id); + Assert.NotNull(book.Author); + + } + + [Fact] + public void ReadResource_CanRead_ACollectionOfEntityRefenceLinks() + { + // Arrange + ODataConventionModelBuilder builder = ODataConventionModelBuilderFactory.Create(); + var books = builder.EntitySet("Books"); + builder.EntityType(); + builder.EntitySet("Authors"); + var author = + books.EntityType.HasOptional((e) => e.Author); + books.HasNavigationPropertyLink(author, (a, b) => new Uri("aa:b"), false); + books.HasOptionalBinding((e) => e.Author, "authorr"); + + + IEdmModel model = builder.GetEdmModel(); + IEdmEntityTypeReference bookTypeReference = model.GetEdmTypeReference(typeof(Book)).AsEntity(); + var deserializer = new ODataResourceDeserializer(_deserializerProvider); + ODataResource odataResource = new ODataResource + { + Properties = new[] + { + new ODataProperty { Name = "Id", Value = 1}, + new ODataProperty { Name = "Name", Value = "BookA"}, + }, + TypeName = "Microsoft.AspNet.OData.Test.Formatter.Book" + }; + + IEdmEntitySet entitySet = model.EntityContainer.FindEntitySet("Books"); + ODataPath path = new ODataPath(new EntitySetSegment(entitySet)); + var request = RequestFactory.CreateFromModel(model, path: path); + + ODataDeserializerContext readContext = new ODataDeserializerContext() + { + Model = model, + Request = request, + Path = path + }; + + ODataResourceWrapper topLevelResourceWrapper = new ODataResourceWrapper(odataResource); + ODataNestedResourceInfo resourceInfo = new ODataNestedResourceInfo + { + IsCollection = true, + Name = "AuthorList" + }; + + IList refLinks = new List() + { + new ODataEntityReferenceLinkBase(new ODataEntityReferenceLink{ Url = new Uri("http://localhost/Authors(2)") }), + new ODataEntityReferenceLinkBase(new ODataEntityReferenceLink{ Url = new Uri("http://localhost/Authors(3)")}) + }; + + + ODataNestedResourceInfoWrapper resourceInfoWrapper = new ODataNestedResourceInfoWrapper(resourceInfo); + + foreach (ODataEntityReferenceLinkBase refLinkWrapper in refLinks) + { + resourceInfoWrapper.NestedItems.Add(refLinkWrapper); + } + topLevelResourceWrapper.NestedResourceInfos.Add(resourceInfoWrapper); + + // Act + Book book = deserializer.ReadResource(topLevelResourceWrapper, bookTypeReference, readContext) + as Book; + + // Assert + Assert.NotNull(book); + Assert.NotNull(book.AuthorList); + Assert.Equal(2, book.AuthorList.Count()); + } + + public class Book + { + [Key] + public int Id { get; set; } + public string Name { get; set; } + public Author Author { get; set; } + public IList AuthorList { get; set; } + } + + public class Author + { + [Key] + public int Id { get; set; } + public string Name { get; set; } + } + } +} 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..3c4ff74d84 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; @@ -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.Shared/Microsoft.AspNet.OData.Test.Shared.projitems b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Microsoft.AspNet.OData.Test.Shared.projitems index c995567544..48f8deb8fb 100644 --- a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Microsoft.AspNet.OData.Test.Shared.projitems +++ b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Microsoft.AspNet.OData.Test.Shared.projitems @@ -149,6 +149,7 @@ + @@ -170,6 +171,7 @@ + @@ -226,6 +228,8 @@ + + @@ -332,6 +336,7 @@ + diff --git a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/ODataPathExtensionsTest.cs b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/ODataPathExtensionsTest.cs new file mode 100644 index 0000000000..09b722b044 --- /dev/null +++ b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/ODataPathExtensionsTest.cs @@ -0,0 +1,208 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + + +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.OData.Extensions; +using Microsoft.AspNet.OData.Test.Common.Models; +using Microsoft.OData.UriParser; +using Xunit; + +namespace Microsoft.AspNet.OData.Test +{ + public class ODataPathExtensionsTest + { + SampleEdmModel model = new SampleEdmModel(); + KeyValuePair[] customerKey = new[] { new KeyValuePair("Id", "1") }; + KeyValuePair[] friendsKey = new[] { new KeyValuePair("Id", "1001") }; + + [Fact] + public void GetKeys_PathWithTypeSegmentReturnsKeysFromLastKeySegment() + { + // From this path Customers(1) + // GetKeys() should return { "Id": "1" } + + // Arrange + ODataPath path = new ODataPath(new ODataPathSegment[] + { + new EntitySetSegment(model.customerSet), + new KeySegment(customerKey, model.customerType, model.customerSet), + new TypeSegment(model.vipCustomerType, model.customerType, null) + }); + + // Act + Dictionary keys = path.GetKeys(); + + // Assert + Assert.Single(keys); + Assert.Equal("Id", keys.First().Key); + Assert.Equal("1", keys.First().Value); + } + + [Fact] + public void GetKeys_PathWithNoSegmentReturnsEmptyCollection() + { + // Arrange + ODataPath path = new ODataPath(new ODataPathSegment[] + { + }); + + // Act + Dictionary keys = path.GetKeys(); + + // Assert + Assert.Empty(keys); + } + + [Fact] + public void GetKeys_PathWithNoKeySegmentReturnsEmptyCollection() + { + // Arrange + ODataPath path = new ODataPath(new ODataPathSegment[] + { + new EntitySetSegment(model.customerSet), + new TypeSegment(model.vipCustomerType, model.customerType, null) + }); + + // Act + Dictionary keys = path.GetKeys(); + + // Assert + Assert.Empty(keys); + } + + [Fact] + public void GetKeys_PathWithNavPropReturnsKeysFromLastKeySegment() + { + // From this path Customers(1)/Friends(1001) + // GetKeys() should return { "Id": "1001" } + + // Arrange + ODataPath path = new ODataPath(new ODataPathSegment[] + { + new EntitySetSegment(model.customerSet), + new KeySegment(customerKey, model.customerType, model.customerSet), + new NavigationPropertySegment(model.friendsProperty, model.customerSet), + new KeySegment(friendsKey, model.personType, null) + }); + + // Act + Dictionary keys = path.GetKeys(); + + // Assert + Assert.Single(keys); + Assert.Equal("Id", keys.First().Key); + Assert.Equal("1001", keys.First().Value); + } + + [Fact] + public void GetLastNonTypeNonKeySegment_TypeSegmentAsLastSegmentReturnsCorrectSegment() + { + // If the path is Customers(1)/Friends(1001)/Ns.UniquePerson where Ns.UniquePerson is a type segment + // and 1001 is a KeySegment, + // GetLastNonTypeNonKeySegment() should return Friends NavigationPropertySegment. + + // Arrange + ODataPath path = new ODataPath(new ODataPathSegment[] + { + new EntitySetSegment(model.customerSet), + new KeySegment(customerKey, model.customerType, model.customerSet), + new NavigationPropertySegment(model.friendsProperty, model.customerSet), + new KeySegment(friendsKey, model.personType, null), + new TypeSegment(model.uniquePersonType, model.personType, null) + }); + + // Act + ODataPathSegment segment = path.GetLastNonTypeNonKeySegment(); + + // Assert + Assert.Equal("Friends", segment.Identifier); + Assert.True(segment is NavigationPropertySegment); + } + + [Fact] + public void GetLastNonTypeNonKeySegment_KeySegmentAsLastSegmentReturnsCorrectSegment() + { + // If the path is Customers(1)/Friends(1001) where1001 is a KeySegment, + // GetLastNonTypeNonKeySegment() should return Friends NavigationPropertySegment. + + // Arrange + ODataPath path = new ODataPath(new ODataPathSegment[] + { + new EntitySetSegment(model.customerSet), + new KeySegment(customerKey, model.customerType, model.customerSet), + new NavigationPropertySegment(model.friendsProperty, model.customerSet), + new KeySegment(friendsKey, model.personType, null) + }); + + // Act + ODataPathSegment segment = path.GetLastNonTypeNonKeySegment(); + + // Assert + Assert.Equal("Friends", segment.Identifier); + Assert.True(segment is NavigationPropertySegment); + } + + [Fact] + public void GetLastNonTypeNonKeySegment_SingleSegmentPathReturnsCorrectSegment() + { + // If the path is /Customers, + // GetLastNonTypeNonKeySegment() should return Customers EntitySetSegment. + + // Arrange + ODataPath path = new ODataPath(new ODataPathSegment[] + { + new EntitySetSegment(model.customerSet) + }); + + // Act + ODataPathSegment segment = path.GetLastNonTypeNonKeySegment(); + + // Assert + Assert.True(segment is EntitySetSegment); + } + + [Fact] + public void GetLastNonTypeNonKeySegment_SingleKeySegmentPathReturnsNull() + { + // If the path is /1, + // GetLastNonTypeNonKeySegment() should return null since this is a KeySegment. + + // Arrange + ODataPath path = new ODataPath(new ODataPathSegment[] + { + new KeySegment(customerKey, model.customerType, model.customerSet) + }); + + // Act + ODataPathSegment segment = path.GetLastNonTypeNonKeySegment(); + + // Assert + Assert.Null(segment); + } + + [Fact] + public void GetLastNonTypeNonKeySegment_SingleTypeSegmentPathReturnsNull() + { + // If the path is /Ns.UniquePerson, + // GetLastNonTypeNonKeySegment() should return null since this is a TypeSegment. + + // Arrange + ODataPath path = new ODataPath(new ODataPathSegment[] + { + new TypeSegment(model.uniquePersonType, model.personType, null) + }); + + // Act + ODataPathSegment segment = path.GetLastNonTypeNonKeySegment(); + + // Assert + Assert.Null(segment); + } + } +} diff --git a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/ODataPathHelperTest.cs b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/ODataPathHelperTest.cs new file mode 100644 index 0000000000..d54e622cb4 --- /dev/null +++ b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/ODataPathHelperTest.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 System.Linq; +using Microsoft.AspNet.OData.Common; +using Microsoft.AspNet.OData.Test.Common; +using Microsoft.AspNet.OData.Test.Common.Models; +using Microsoft.OData.UriParser; +using Xunit; + +namespace Microsoft.AspNet.OData.Test +{ + public class ODataPathHelperTest + { + SampleEdmModel model = new SampleEdmModel(); + KeyValuePair[] customerKey = new[] { new KeyValuePair("Id", "1"), new KeyValuePair("AlternateId", "2") }; + KeyValuePair[] friendsKey = new[] { new KeyValuePair("Id", "1001") }; + + [Fact] + public void GetKeysFromKeySegment_ReturnsCorrectKeysDictionary() + { + // Arrange + KeySegment keySegment = new KeySegment(customerKey, model.customerType, model.customerSet); + + // Act + Dictionary keys = ODataPathHelper.KeySegmentAsDictionary(keySegment); + + // Assert + Assert.Equal(2, keys.Count); + Assert.Equal("Id", keys.First().Key); + Assert.Equal("1", keys.First().Value); + Assert.Equal("AlternateId", keys.Last().Key); + Assert.Equal("2", keys.Last().Value); + } + + [Fact] + public void GetKeysFromKeySegment_ThrowsExceptionForNullKeySegment() + { + KeySegment keySegment = null; + + ExceptionAssert.ThrowsArgumentNull( + () => ODataPathHelper.KeySegmentAsDictionary(keySegment), + nameof(keySegment)); + } + + [Fact] + public void GetNextKeySegmentPosition_ReturnsCorrectPosition() + { + // If the path is Customers(1)/Friends(1001)/Ns.UniqueFriend where Ns.UniqueFriend is a type segment + // and 1001 is a KeySegment, and the starting position is index 1, the next keysegment position is index 3. + + // Arrange + ODataPath path = new ODataPath(new ODataPathSegment[] + { + new EntitySetSegment(model.customerSet), + new KeySegment(customerKey, model.customerType, model.customerSet), + new NavigationPropertySegment(model.friendsProperty, model.customerSet), + new KeySegment(friendsKey, model.personType, null), + new TypeSegment(model.uniquePersonType, model.personType, null) + }); + + // Act + int position = ODataPathHelper.GetNextKeySegmentPosition(path.AsList(), 1); + + // Assert + Assert.Equal(3, position); + } + + [Fact] + public void GetNextKeySegmentPosition_ReturnsNegativeOneIfNoKeySegmentIsFound() + { + // If the path is Customers(1)/Friends(1001)/Ns.UniqueFriend where Ns.UniqueFriend is a type segment + // and 1001 is a KeySegment, and the starting position is index 1, the next keysegment position is index 3. + + // Arrange + ODataPath path = new ODataPath(new ODataPathSegment[] + { + new EntitySetSegment(model.customerSet), + new KeySegment(customerKey, model.customerType, model.customerSet), + new NavigationPropertySegment(model.friendsProperty, model.customerSet), + new TypeSegment(model.uniquePersonType, model.personType, null) + }); + + // Act + int position = ODataPathHelper.GetNextKeySegmentPosition(path.AsList(), 1); + + // Assert + Assert.Equal(-1, position); + } + + [Fact] + public void GetNextKeySegmentPosition_ReturnsNegativeOneIfInvalidPositionIsPassed() + { + // If the path is Customers(1)/Friends(1001)/Ns.UniqueFriend where Ns.UniqueFriend is a type segment + // and 1001 is a KeySegment, and the starting position is index 1, the next keysegment position is index 3. + + // Arrange + ODataPath path = new ODataPath(new ODataPathSegment[] + { + new EntitySetSegment(model.customerSet), + new KeySegment(customerKey, model.customerType, model.customerSet), + new NavigationPropertySegment(model.friendsProperty, model.customerSet), + new TypeSegment(model.uniquePersonType, model.personType, null) + }); + + // Act + int position = ODataPathHelper.GetNextKeySegmentPosition(path.AsList(), 10); + + // Assert + Assert.Equal(-1, position); + } + } +} diff --git a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Routing/AttributeRoutingTest.cs b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Routing/AttributeRoutingTest.cs index 676c627f3a..c5125d51bf 100644 --- a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Routing/AttributeRoutingTest.cs +++ b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Routing/AttributeRoutingTest.cs @@ -144,6 +144,13 @@ public string PostCustomerFromSpecialCustomer() return "PostCustomerFromSpecialCustomer"; } + [HttpPatch] + [ODataRoute("Customers/NS.SpecialCustomer")] + public string PatchCustomerFromSpecialCustomer() + { + return "PatchCustomerFromSpecialCustomer"; + } + [HttpPost] [ODataRoute("Customers")] public string CreateCustomer() diff --git a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Routing/Conventions/EntitySetRoutingConventionTest.cs b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Routing/Conventions/EntitySetRoutingConventionTest.cs index a96ce439c6..13f568fa66 100644 --- a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Routing/Conventions/EntitySetRoutingConventionTest.cs +++ b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Routing/Conventions/EntitySetRoutingConventionTest.cs @@ -37,6 +37,28 @@ public void SelectAction_ReturnsNull_IfActionIsMissing(string method) Assert.Empty(SelectActionHelper.GetRouteData(request).Values); } + [Theory] + [InlineData("POST", "PostCustomer")] + [InlineData("PATCH", "PatchCustomers")] + public void SelectAction_Returns_ExpectedActionName(string method, string expected) + { + // Arrange + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + + IEdmCollectionType collection = new EdmCollectionType(new EdmEntityTypeReference(model.SpecialCustomer, isNullable: false)); + + ODataPath odataPath = new ODataPath(new EntitySetSegment(model.Customers)); + + var request = RequestFactory.Create(new HttpMethod(method), "http://localhost/"); + var actionMap = SelectActionHelper.CreateActionMap(expected); + + // Act + string selectedAction = SelectActionHelper.SelectAction(new EntitySetRoutingConvention(), odataPath, request, actionMap); + + // Assert + Assert.Equal(expected, selectedAction); + } + [Theory] [InlineData("GET", "GetCustomersFromSpecialCustomer")] [InlineData("POST", "PostCustomerFromSpecialCustomer")] diff --git a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/TestCommon/Models/SampleEdmModel.cs b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/TestCommon/Models/SampleEdmModel.cs new file mode 100644 index 0000000000..89e7db509b --- /dev/null +++ b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/TestCommon/Models/SampleEdmModel.cs @@ -0,0 +1,60 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using Microsoft.OData.Edm; + +namespace Microsoft.AspNet.OData.Test.Common.Models +{ + public class SampleEdmModel + { + public EdmNavigationProperty friendsProperty; + public EdmEntityType customerType; + public EdmEntityType personType; + public EdmEntityType uniquePersonType; + public EdmEntityType vipCustomerType; + public IEdmEntitySet customerSet; + public EdmModel model; + + public SampleEdmModel() + { + model = new EdmModel(); + EdmEntityContainer container = new EdmEntityContainer("NS", "Container"); + + personType = new EdmEntityType("NS", "Person"); + personType.AddKeys(personType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32, isNullable: false)); + personType.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String, isNullable: false); + + customerType = new EdmEntityType("NS", "Customer"); + customerType.AddKeys(customerType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32, isNullable: false)); + customerType.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String, isNullable: false); + friendsProperty = customerType.AddUnidirectionalNavigation( + new EdmNavigationPropertyInfo + { + ContainsTarget = true, + Name = "Friends", + Target = personType, + TargetMultiplicity = EdmMultiplicity.Many + }); + + vipCustomerType = new EdmEntityType("NS", "VipCustomer", customerType); + vipCustomerType.AddKeys(vipCustomerType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32, isNullable: false)); + vipCustomerType.AddStructuralProperty("VipName", EdmPrimitiveTypeKind.String, isNullable: false); + + uniquePersonType = new EdmEntityType("NS", "UniquePerson", personType); + uniquePersonType.AddKeys(uniquePersonType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32, isNullable: false)); + uniquePersonType.AddStructuralProperty("UniqueName", EdmPrimitiveTypeKind.String, isNullable: false); + + model.AddElement(customerType); + model.AddElement(personType); + model.AddElement(uniquePersonType); + model.AddElement(vipCustomerType); + model.AddElement(container); + + customerSet = container.AddEntitySet("Customers", customerType); + } + } +} \ No newline at end of file diff --git a/test/UnitTest/Microsoft.AspNet.OData.Test/Microsoft.AspNet.OData.Test.csproj b/test/UnitTest/Microsoft.AspNet.OData.Test/Microsoft.AspNet.OData.Test.csproj index 81c539497a..7256c6ee86 100644 --- a/test/UnitTest/Microsoft.AspNet.OData.Test/Microsoft.AspNet.OData.Test.csproj +++ b/test/UnitTest/Microsoft.AspNet.OData.Test/Microsoft.AspNet.OData.Test.csproj @@ -56,7 +56,6 @@ - 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 c698552646..93419940b7 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 @@ -16,6 +16,12 @@ public enum Microsoft.AspNet.OData.EdmDeltaEntityKind : int { Unknown = 4 } +public enum Microsoft.AspNet.OData.ODataAPIResponseStatus : int { + Failure = 1 + NotFound = 2 + Success = 0 +} + public interface Microsoft.AspNet.OData.IDelta { void Clear () System.Collections.Generic.IEnumerable`1[[System.String]] GetChangedPropertyNames () @@ -25,6 +31,22 @@ public interface Microsoft.AspNet.OData.IDelta { bool TrySetPropertyValue (string name, object value) } +public interface Microsoft.AspNet.OData.IDeltaDeletedEntityObject { + System.Uri Id { public abstract get; public abstract set; } + Microsoft.OData.Edm.IEdmNavigationSource NavigationSource { public abstract get; public abstract set; } + System.Nullable`1[[Microsoft.OData.DeltaDeletedEntryReason]] Reason { public abstract get; public abstract set; } +} + +public interface Microsoft.AspNet.OData.IDeltaSet { +} + +public interface Microsoft.AspNet.OData.IDeltaSetItem { + EdmDeltaEntityKind DeltaKind { public abstract get; } + IODataIdContainer 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; } } @@ -63,6 +85,13 @@ public interface Microsoft.AspNet.OData.IEdmStructuredObject : IEdmObject { bool TryGetPropertyValue (string propertyName, out System.Object& value) } +public interface Microsoft.AspNet.OData.IODataAPIHandler { +} + +public interface Microsoft.AspNet.OData.IODataIdContainer { + string ODataId { public abstract get; public abstract set; } +} + public interface Microsoft.AspNet.OData.IPerRouteContainer { System.Func`1[[Microsoft.OData.IContainerBuilder]] BuilderFactory { public abstract get; public abstract set; } @@ -111,6 +140,25 @@ public abstract class Microsoft.AspNet.OData.EdmStructuredObject : Delta, IDynam public virtual bool TrySetPropertyValue (string name, object value) } +public abstract class Microsoft.AspNet.OData.ODataAPIHandler`1 : IODataAPIHandler { + protected ODataAPIHandler`1 () + + public abstract IODataAPIHandler GetNestedHandler (TStructuralType parent, string navigationPropertyName) + public abstract ODataAPIResponseStatus TryCreate (System.Collections.Generic.IDictionary`2[[System.String],[System.Object]] keyValues, out TStructuralType& createdObject, out System.String& errorMessage) + public abstract ODataAPIResponseStatus TryDelete (System.Collections.Generic.IDictionary`2[[System.String],[System.Object]] keyValues, out System.String& errorMessage) + public abstract ODataAPIResponseStatus TryGet (System.Collections.Generic.IDictionary`2[[System.String],[System.Object]] keyValues, out TStructuralType& originalObject, out System.String& errorMessage) + public virtual void UpdateLinkedObjects (TStructuralType resource, Microsoft.OData.Edm.IEdmModel model) +} + +public abstract class Microsoft.AspNet.OData.ODataAPIHandlerFactory { + protected ODataAPIHandlerFactory (Microsoft.OData.Edm.IEdmModel model) + + Microsoft.OData.Edm.IEdmModel Model { public get; } + + public abstract IODataAPIHandler GetHandler (Microsoft.OData.UriParser.ODataPath odataPath) + public IODataAPIHandler GetHandler (string path) +} + [ ODataFormattingAttribute(), ODataRoutingAttribute(), @@ -251,25 +299,30 @@ public class Microsoft.AspNet.OData.DefaultContainerBuilder : IContainerBuilder [ NonValidatingParameterBindingAttribute(), ] -public class Microsoft.AspNet.OData.Delta`1 : TypedDelta, IDynamicMetaObjectProvider, IDelta { +public class Microsoft.AspNet.OData.Delta`1 : TypedDelta, IDynamicMetaObjectProvider, IDelta, IDeltaSetItem { public Delta`1 () public Delta`1 (System.Type structuralType) public Delta`1 (System.Type structuralType, System.Collections.Generic.IEnumerable`1[[System.String]] updatableProperties) public Delta`1 (System.Type structuralType, System.Collections.Generic.IEnumerable`1[[System.String]] updatableProperties, System.Reflection.PropertyInfo dynamicDictionaryPropertyInfo) public Delta`1 (System.Type structuralType, System.Collections.Generic.IEnumerable`1[[System.String]] updatableProperties, System.Reflection.PropertyInfo dynamicDictionaryPropertyInfo, bool isComplexType) + public Delta`1 (System.Type structuralType, System.Collections.Generic.IEnumerable`1[[System.String]] updatableProperties, System.Reflection.PropertyInfo dynamicDictionaryPropertyInfo, bool isComplexType, System.Reflection.PropertyInfo instanceAnnotationsPropertyInfo) + EdmDeltaEntityKind DeltaKind { public virtual get; protected set; } System.Type ExpectedClrType { public virtual get; } bool IsComplexType { public get; } + IODataIdContainer ODataIdContainer { public virtual get; public virtual set; } + Microsoft.OData.UriParser.ODataPath ODataPath { public virtual get; public virtual set; } System.Type StructuredType { public virtual get; } + IODataInstanceAnnotationContainer TransientInstanceAnnotationContainer { public virtual get; public virtual set; } System.Collections.Generic.IList`1[[System.String]] UpdatableProperties { public get; } public virtual void Clear () - public TStructuralType CopyChangedValues (TStructuralType original) + public void CopyChangedValues (TStructuralType original) public void CopyUnchangedValues (TStructuralType original) public virtual System.Collections.Generic.IEnumerable`1[[System.String]] GetChangedPropertyNames () public TStructuralType GetInstance () public virtual System.Collections.Generic.IEnumerable`1[[System.String]] GetUnchangedPropertyNames () - public TStructuralType Patch (TStructuralType original) + public void Patch (TStructuralType original) public void Put (TStructuralType original) public bool TryGetNestedPropertyValue (string name, out System.Object& value) public virtual bool TryGetPropertyType (string name, out System.Type& type) @@ -277,6 +330,34 @@ public class Microsoft.AspNet.OData.Delta`1 : TypedDelta, IDynamicMetaObjectProv public virtual bool TrySetPropertyValue (string name, object value) } +[ +NonValidatingParameterBindingAttribute(), +] +public class Microsoft.AspNet.OData.DeltaDeletedEntityObject`1 : Delta`1, IDynamicMetaObjectProvider, IDelta, IDeltaDeletedEntityObject, IDeltaSetItem { + public DeltaDeletedEntityObject`1 () + public DeltaDeletedEntityObject`1 (System.Type structuralType) + public DeltaDeletedEntityObject`1 (System.Type structuralType, System.Collections.Generic.IEnumerable`1[[System.String]] updatableProperties) + public DeltaDeletedEntityObject`1 (System.Type structuralType, System.Reflection.PropertyInfo instanceAnnotationsPropertyInfo) + public DeltaDeletedEntityObject`1 (System.Type structuralType, System.Reflection.PropertyInfo dynamicDictionaryPropertyInfo, System.Reflection.PropertyInfo instanceAnnotationsPropertyInfo) + public DeltaDeletedEntityObject`1 (System.Type structuralType, System.Collections.Generic.IEnumerable`1[[System.String]] updatableProperties, System.Reflection.PropertyInfo dynamicDictionaryPropertyInfo, System.Reflection.PropertyInfo instanceAnnotationsPropertyInfo) + public DeltaDeletedEntityObject`1 (System.Type structuralType, System.Collections.Generic.IEnumerable`1[[System.String]] updatableProperties, System.Reflection.PropertyInfo dynamicDictionaryPropertyInfo, bool isComplexType, System.Reflection.PropertyInfo instanceAnnotationsPropertyInfo) + + System.Uri Id { public virtual get; public virtual set; } + Microsoft.OData.Edm.IEdmNavigationSource NavigationSource { public virtual get; public virtual set; } + 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) + public DeltaSet`1 Patch (ICollection`1 originalCollection) + public DeltaSet`1 Patch (ODataAPIHandler`1 apiHandlerOfT, ODataAPIHandlerFactory apiHandlerFactory) +} + [ NonValidatingParameterBindingAttribute(), ] @@ -284,6 +365,8 @@ public class Microsoft.AspNet.OData.EdmChangedObjectCollection : System.Collecti public EdmChangedObjectCollection (Microsoft.OData.Edm.IEdmEntityType entityType) public EdmChangedObjectCollection (Microsoft.OData.Edm.IEdmEntityType entityType, System.Collections.Generic.IList`1[[Microsoft.AspNet.OData.IEdmChangedObject]] changedObjectList) + Microsoft.OData.Edm.IEdmEntityType EntityType { public get; } + public virtual Microsoft.OData.Edm.IEdmTypeReference GetEdmType () } @@ -372,10 +455,17 @@ public class Microsoft.AspNet.OData.EdmDeltaLink : EdmEntityObject, IDynamicMeta [ NonValidatingParameterBindingAttribute(), ] -public class Microsoft.AspNet.OData.EdmEntityObject : EdmStructuredObject, IDynamicMetaObjectProvider, IDelta, IEdmEntityObject, IEdmObject, IEdmStructuredObject { +public class Microsoft.AspNet.OData.EdmEntityObject : EdmStructuredObject, IDynamicMetaObjectProvider, IDelta, IEdmChangedObject, IEdmEntityObject, IEdmObject, IEdmStructuredObject { public EdmEntityObject (Microsoft.OData.Edm.IEdmEntityType edmType) public EdmEntityObject (Microsoft.OData.Edm.IEdmEntityTypeReference edmType) public EdmEntityObject (Microsoft.OData.Edm.IEdmEntityType edmType, bool isNullable) + + EdmDeltaEntityKind DeltaKind { public virtual get; } + IODataIdContainer ODataIdContainer { public get; public set; } + IODataInstanceAnnotationContainer PersistentInstanceAnnotationsContainer { public get; public set; } + + public void AddDataException (Org.OData.Core.V1.DataModificationExceptionType dataModificationException) + public Org.OData.Core.V1.DataModificationExceptionType GetDataException () } [ @@ -489,6 +579,12 @@ public class Microsoft.AspNet.OData.ODataFormattingAttribute : System.Attribute, public virtual void Initialize (System.Web.Http.Controllers.HttpControllerSettings controllerSettings, System.Web.Http.Controllers.HttpControllerDescriptor controllerDescriptor) } +public class Microsoft.AspNet.OData.ODataIdContainer : IODataIdContainer { + public ODataIdContainer () + + string ODataId { public virtual get; public virtual set; } +} + public class Microsoft.AspNet.OData.ODataNullValueMessageHandler : System.Net.Http.DelegatingHandler, IDisposable { public ODataNullValueMessageHandler () @@ -3142,6 +3238,39 @@ public sealed class Microsoft.AspNet.OData.Routing.ODataRoutePrefixAttribute : S string Prefix { public get; } } +public enum Org.OData.Core.V1.DataModificationOperationKind : int { + Delete = 3 + Insert = 0 + Invoke = 4 + Link = 5 + Unlink = 6 + Update = 1 + Upsert = 2 +} + +public abstract class Org.OData.Core.V1.ExceptionType { + protected ExceptionType () + + Org.OData.Core.V1.MessageType MessageType { public get; public set; } +} + +public class Org.OData.Core.V1.DataModificationExceptionType : Org.OData.Core.V1.ExceptionType { + public DataModificationExceptionType (Org.OData.Core.V1.DataModificationOperationKind failedOperation) + + Org.OData.Core.V1.DataModificationOperationKind FailedOperation { public get; } + short ResponseCode { public get; public set; } +} + +public class Org.OData.Core.V1.MessageType { + public MessageType () + + string Code { public get; public set; } + string Details { public get; public set; } + string Message { public get; public set; } + string Severity { public get; public set; } + string Target { public get; public set; } +} + public abstract class Microsoft.AspNet.OData.Formatter.Deserialization.ODataDeserializer { protected ODataDeserializer (Microsoft.OData.ODataPayloadKind payloadKind) @@ -3176,6 +3305,13 @@ public abstract class Microsoft.AspNet.OData.Formatter.Deserialization.ODataItem Microsoft.OData.ODataItem Item { public get; } } +public abstract class Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceSetWrapperBase : ODataItemBase { + public ODataResourceSetWrapperBase (Microsoft.OData.ODataResourceSetBase item) + + System.Collections.Generic.IList`1[[Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceWrapper]] Resources { public get; } + Microsoft.OData.ODataResourceSetBase ResourceSetBase { public get; } +} + [ ExtensionAttribute(), ] @@ -3305,7 +3441,11 @@ public class Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceSetDe public virtual System.Threading.Tasks.Task`1[[System.Object]] ReadAsync (Microsoft.OData.ODataMessageReader messageReader, System.Type type, ODataDeserializerContext readContext) public virtual object ReadInline (object item, Microsoft.OData.Edm.IEdmTypeReference edmType, ODataDeserializerContext readContext) - public virtual System.Collections.IEnumerable ReadResourceSet (ODataResourceSetWrapper resourceSet, Microsoft.OData.Edm.IEdmStructuredTypeReference elementType, ODataDeserializerContext readContext) + public virtual System.Collections.IEnumerable ReadResourceSet (ODataResourceSetWrapperBase resourceSet, Microsoft.OData.Edm.IEdmStructuredTypeReference elementType, ODataDeserializerContext readContext) +} + +public sealed class Microsoft.AspNet.OData.Formatter.Deserialization.ODataDeltaResourceSetWrapper : ODataResourceSetWrapperBase { + public ODataDeltaResourceSetWrapper (Microsoft.OData.ODataDeltaResourceSet item) } public sealed class Microsoft.AspNet.OData.Formatter.Deserialization.ODataNestedResourceInfoWrapper : ODataItemBase { @@ -3315,18 +3455,22 @@ public sealed class Microsoft.AspNet.OData.Formatter.Deserialization.ODataNested Microsoft.OData.ODataNestedResourceInfo NestedResourceInfo { public get; } } -public sealed class Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceSetWrapper : ODataItemBase { +public sealed class Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceSetWrapper : ODataResourceSetWrapperBase { public ODataResourceSetWrapper (Microsoft.OData.ODataResourceSet item) - System.Collections.Generic.IList`1[[Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceWrapper]] Resources { public get; } Microsoft.OData.ODataResourceSet ResourceSet { public get; } } public sealed class Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceWrapper : ODataItemBase { - public ODataResourceWrapper (Microsoft.OData.ODataResource item) + public ODataResourceWrapper (Microsoft.OData.ODataResourceBase item) System.Collections.Generic.IList`1[[Microsoft.AspNet.OData.Formatter.Deserialization.ODataNestedResourceInfoWrapper]] NestedResourceInfos { public get; } + [ + ObsoleteAttribute(), + ] Microsoft.OData.ODataResource Resource { public get; } + + Microsoft.OData.ODataResourceBase ResourceBase { public get; } } public abstract class Microsoft.AspNet.OData.Formatter.Serialization.ODataEdmTypeSerializer : ODataSerializer { @@ -3487,9 +3631,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/Microsoft.AspNetCore.OData.Test.csproj b/test/UnitTest/Microsoft.AspNetCore.OData.Test/Microsoft.AspNetCore.OData.Test.csproj index 0b343ba0bb..799d0d0231 100644 --- a/test/UnitTest/Microsoft.AspNetCore.OData.Test/Microsoft.AspNetCore.OData.Test.csproj +++ b/test/UnitTest/Microsoft.AspNetCore.OData.Test/Microsoft.AspNetCore.OData.Test.csproj @@ -60,4 +60,8 @@ + + + + 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 dbb0c4e146..c113239bf7 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 @@ -25,6 +25,22 @@ public interface Microsoft.AspNet.OData.IDelta { bool TrySetPropertyValue (string name, object value) } +public interface Microsoft.AspNet.OData.IDeltaDeletedEntityObject { + System.Uri Id { public abstract get; public abstract set; } + Microsoft.OData.Edm.IEdmNavigationSource NavigationSource { public abstract get; public abstract set; } + System.Nullable`1[[Microsoft.OData.DeltaDeletedEntryReason]] Reason { public abstract get; public abstract set; } +} + +public interface Microsoft.AspNet.OData.IDeltaSet { +} + +public interface Microsoft.AspNet.OData.IDeltaSetItem { + EdmDeltaEntityKind DeltaKind { public abstract get; } + IODataIdContainer 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; } } @@ -63,6 +79,10 @@ public interface Microsoft.AspNet.OData.IEdmStructuredObject : IEdmObject { bool TryGetPropertyValue (string propertyName, out System.Object& value) } +public interface Microsoft.AspNet.OData.IODataIdContainer { + string ODataId { public abstract get; public abstract set; } +} + public interface Microsoft.AspNet.OData.IPerRouteContainer { System.Func`1[[Microsoft.OData.IContainerBuilder]] BuilderFactory { public abstract get; public abstract set; } @@ -266,16 +286,20 @@ public class Microsoft.AspNet.OData.DefaultContainerBuilder : IContainerBuilder [ NonValidatingParameterBindingAttribute(), ] -public class Microsoft.AspNet.OData.Delta`1 : TypedDelta, IDynamicMetaObjectProvider, IDelta { +public class Microsoft.AspNet.OData.Delta`1 : TypedDelta, IDynamicMetaObjectProvider, IDelta, IDeltaSetItem { public Delta`1 () public Delta`1 (System.Type structuralType) public Delta`1 (System.Type structuralType, System.Collections.Generic.IEnumerable`1[[System.String]] updatableProperties) public Delta`1 (System.Type structuralType, System.Collections.Generic.IEnumerable`1[[System.String]] updatableProperties, System.Reflection.PropertyInfo dynamicDictionaryPropertyInfo) public Delta`1 (System.Type structuralType, System.Collections.Generic.IEnumerable`1[[System.String]] updatableProperties, System.Reflection.PropertyInfo dynamicDictionaryPropertyInfo, bool isComplexType) - + public Delta`1 (System.Type structuralType, System.Collections.Generic.IEnumerable`1[[System.String]] updatableProperties, System.Reflection.PropertyInfo dynamicDictionaryPropertyInfo, bool isComplexType, System.Reflection.PropertyInfo instanceAnnotationsPropertyInfo) + EdmDeltaEntityKind DeltaKind { public virtual get; protected set; } System.Type ExpectedClrType { public virtual get; } bool IsComplexType { public get; } + IODataIdContainer ODataIdContainer { public virtual get; public virtual set; } + Microsoft.OData.UriParser.ODataPath ODataPath { public virtual get; public virtual set; } System.Type StructuredType { public virtual get; } + IODataInstanceAnnotationContainer TransientInstanceAnnotationContainer { public virtual get; public virtual set; } System.Collections.Generic.IList`1[[System.String]] UpdatableProperties { public get; } public virtual void Clear () @@ -284,7 +308,7 @@ public class Microsoft.AspNet.OData.Delta`1 : TypedDelta, IDynamicMetaObjectProv public virtual System.Collections.Generic.IEnumerable`1[[System.String]] GetChangedPropertyNames () public TStructuralType GetInstance () public virtual System.Collections.Generic.IEnumerable`1[[System.String]] GetUnchangedPropertyNames () - public TStructuralType Patch (TStructuralType original) + public void Patch (TStructuralType original) public void Put (TStructuralType original) public bool TryGetNestedPropertyValue (string name, out System.Object& value) public virtual bool TryGetPropertyType (string name, out System.Type& type) @@ -292,6 +316,32 @@ public class Microsoft.AspNet.OData.Delta`1 : TypedDelta, IDynamicMetaObjectProv public virtual bool TrySetPropertyValue (string name, object value) } +[ +NonValidatingParameterBindingAttribute(), +] +public class Microsoft.AspNet.OData.DeltaDeletedEntityObject`1 : Delta`1, IDynamicMetaObjectProvider, IDelta, IDeltaDeletedEntityObject, IDeltaSetItem { + public DeltaDeletedEntityObject`1 () + public DeltaDeletedEntityObject`1 (System.Type structuralType) + public DeltaDeletedEntityObject`1 (System.Type structuralType, System.Collections.Generic.IEnumerable`1[[System.String]] updatableProperties) + public DeltaDeletedEntityObject`1 (System.Type structuralType, System.Reflection.PropertyInfo instanceAnnotationsPropertyInfo) + public DeltaDeletedEntityObject`1 (System.Type structuralType, System.Reflection.PropertyInfo dynamicDictionaryPropertyInfo, System.Reflection.PropertyInfo instanceAnnotationsPropertyInfo) + public DeltaDeletedEntityObject`1 (System.Type structuralType, System.Collections.Generic.IEnumerable`1[[System.String]] updatableProperties, System.Reflection.PropertyInfo dynamicDictionaryPropertyInfo, System.Reflection.PropertyInfo instanceAnnotationsPropertyInfo) + public DeltaDeletedEntityObject`1 (System.Type structuralType, System.Collections.Generic.IEnumerable`1[[System.String]] updatableProperties, System.Reflection.PropertyInfo dynamicDictionaryPropertyInfo, bool isComplexType, System.Reflection.PropertyInfo instanceAnnotationsPropertyInfo) + + System.Uri Id { public virtual get; public virtual set; } + Microsoft.OData.Edm.IEdmNavigationSource NavigationSource { public virtual get; public virtual set; } + 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(), ] @@ -299,6 +349,8 @@ public class Microsoft.AspNet.OData.EdmChangedObjectCollection : System.Collecti public EdmChangedObjectCollection (Microsoft.OData.Edm.IEdmEntityType entityType) public EdmChangedObjectCollection (Microsoft.OData.Edm.IEdmEntityType entityType, System.Collections.Generic.IList`1[[Microsoft.AspNet.OData.IEdmChangedObject]] changedObjectList) + Microsoft.OData.Edm.IEdmEntityType EntityType { public get; } + public virtual Microsoft.OData.Edm.IEdmTypeReference GetEdmType () } @@ -387,10 +439,16 @@ public class Microsoft.AspNet.OData.EdmDeltaLink : EdmEntityObject, IDynamicMeta [ NonValidatingParameterBindingAttribute(), ] -public class Microsoft.AspNet.OData.EdmEntityObject : EdmStructuredObject, IDynamicMetaObjectProvider, IDelta, IEdmEntityObject, IEdmObject, IEdmStructuredObject { +public class Microsoft.AspNet.OData.EdmEntityObject : EdmStructuredObject, IDynamicMetaObjectProvider, IDelta, IEdmChangedObject, IEdmEntityObject, IEdmObject, IEdmStructuredObject { public EdmEntityObject (Microsoft.OData.Edm.IEdmEntityType edmType) public EdmEntityObject (Microsoft.OData.Edm.IEdmEntityTypeReference edmType) public EdmEntityObject (Microsoft.OData.Edm.IEdmEntityType edmType, bool isNullable) + EdmDeltaEntityKind DeltaKind { public virtual get; } + IODataIdContainer ODataIdContainer { public get; public set; } + IODataInstanceAnnotationContainer PersistentInstanceAnnotationsContainer { public get; public set; } + + public void AddDataException (Org.OData.Core.V1.DataModificationExceptionType dataModificationException) + public Org.OData.Core.V1.DataModificationExceptionType GetDataException () } [ @@ -482,6 +540,11 @@ public class Microsoft.AspNet.OData.MetadataController : ODataController { public Microsoft.OData.ODataServiceDocument GetServiceDocument () } +public class Microsoft.AspNet.OData.NavigationPath : System.Collections.Generic.List`1[[Microsoft.AspNet.OData.PathItem]], ICollection, IEnumerable, IList, ICollection`1, IEnumerable`1, IList`1, IReadOnlyCollection`1, IReadOnlyList`1 { + public NavigationPath (Microsoft.OData.UriParser.ODataPath path) + public NavigationPath (string path, Microsoft.OData.Edm.IEdmModel model) +} + public class Microsoft.AspNet.OData.NullEdmComplexObject : IEdmComplexObject, IEdmObject, IEdmStructuredObject { public NullEdmComplexObject (Microsoft.OData.Edm.IEdmComplexTypeReference edmType) @@ -542,6 +605,12 @@ public class Microsoft.AspNet.OData.ODataFormattingAttribute : System.Attribute public ODataFormattingAttribute () } +public class Microsoft.AspNet.OData.ODataIdContainer : IODataIdContainer { + public ODataIdContainer () + + string ODataId { public virtual get; public virtual set; } +} + public class Microsoft.AspNet.OData.ODataNullValueMessageHandler : IFilterMetadata, IResultFilter { public ODataNullValueMessageHandler () @@ -617,6 +686,15 @@ public class Microsoft.AspNet.OData.PageResult`1 : PageResult, IEnumerable`1, IE System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator () } +public class Microsoft.AspNet.OData.PathItem { + public PathItem () + + string CastTypeName { public get; } + bool IsCastType { public get; } + System.Collections.Generic.Dictionary`2[[System.String],[System.Object]] KeyProperties { public get; } + string Name { public get; } +} + public class Microsoft.AspNet.OData.PerRouteContainer : PerRouteContainerBase, IPerRouteContainer { public PerRouteContainer () @@ -3356,6 +3434,39 @@ public sealed class Microsoft.AspNet.OData.Routing.ODataRoutePrefixAttribute : S string Prefix { public get; } } +public enum Org.OData.Core.V1.DataModificationOperationKind : int { + Delete = 3 + Insert = 0 + Invoke = 4 + Link = 5 + Unlink = 6 + Update = 1 + Upsert = 2 +} + +public abstract class Org.OData.Core.V1.ExceptionType { + protected ExceptionType () + + Org.OData.Core.V1.MessageType MessageType { public get; public set; } +} + +public class Org.OData.Core.V1.DataModificationExceptionType : Org.OData.Core.V1.ExceptionType { + public DataModificationExceptionType (Org.OData.Core.V1.DataModificationOperationKind failedOperation) + + Org.OData.Core.V1.DataModificationOperationKind FailedOperation { public get; } + short ResponseCode { public get; public set; } +} + +public class Org.OData.Core.V1.MessageType { + public MessageType () + + string Code { public get; public set; } + string Details { public get; public set; } + string Message { public get; public set; } + string Severity { public get; public set; } + string Target { public get; public set; } +} + public abstract class Microsoft.AspNet.OData.Formatter.Deserialization.ODataDeserializer { protected ODataDeserializer (Microsoft.OData.ODataPayloadKind payloadKind) @@ -3390,6 +3501,13 @@ public abstract class Microsoft.AspNet.OData.Formatter.Deserialization.ODataItem Microsoft.OData.ODataItem Item { public get; } } +public abstract class Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceSetWrapperBase : ODataItemBase { + public ODataResourceSetWrapperBase (Microsoft.OData.ODataResourceSetBase item) + + System.Collections.Generic.IList`1[[Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceWrapper]] Resources { public get; } + Microsoft.OData.ODataResourceSetBase ResourceSetBase { public get; } +} + [ ExtensionAttribute(), ] @@ -3518,7 +3636,11 @@ public class Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceSetDe public virtual System.Threading.Tasks.Task`1[[System.Object]] ReadAsync (Microsoft.OData.ODataMessageReader messageReader, System.Type type, ODataDeserializerContext readContext) public virtual object ReadInline (object item, Microsoft.OData.Edm.IEdmTypeReference edmType, ODataDeserializerContext readContext) - public virtual System.Collections.IEnumerable ReadResourceSet (ODataResourceSetWrapper resourceSet, Microsoft.OData.Edm.IEdmStructuredTypeReference elementType, ODataDeserializerContext readContext) + public virtual System.Collections.IEnumerable ReadResourceSet (ODataResourceSetWrapperBase resourceSet, Microsoft.OData.Edm.IEdmStructuredTypeReference elementType, ODataDeserializerContext readContext) +} + +public sealed class Microsoft.AspNet.OData.Formatter.Deserialization.ODataDeltaResourceSetWrapper : ODataResourceSetWrapperBase { + public ODataDeltaResourceSetWrapper (Microsoft.OData.ODataDeltaResourceSet item) } public sealed class Microsoft.AspNet.OData.Formatter.Deserialization.ODataNestedResourceInfoWrapper : ODataItemBase { @@ -3528,18 +3650,22 @@ public sealed class Microsoft.AspNet.OData.Formatter.Deserialization.ODataNested Microsoft.OData.ODataNestedResourceInfo NestedResourceInfo { public get; } } -public sealed class Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceSetWrapper : ODataItemBase { +public sealed class Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceSetWrapper : ODataResourceSetWrapperBase { public ODataResourceSetWrapper (Microsoft.OData.ODataResourceSet item) - System.Collections.Generic.IList`1[[Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceWrapper]] Resources { public get; } Microsoft.OData.ODataResourceSet ResourceSet { public get; } } public sealed class Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceWrapper : ODataItemBase { - public ODataResourceWrapper (Microsoft.OData.ODataResource item) + public ODataResourceWrapper (Microsoft.OData.ODataResourceBase item) System.Collections.Generic.IList`1[[Microsoft.AspNet.OData.Formatter.Deserialization.ODataNestedResourceInfoWrapper]] NestedResourceInfos { public get; } + [ + ObsoleteAttribute(), + ] Microsoft.OData.ODataResource Resource { public get; } + + Microsoft.OData.ODataResourceBase ResourceBase { public get; } } public abstract class Microsoft.AspNet.OData.Formatter.Serialization.ODataEdmTypeSerializer : ODataSerializer { 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 760e126e98..22fc9e2ea5 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 @@ -16,6 +16,12 @@ public enum Microsoft.AspNet.OData.EdmDeltaEntityKind : int { Unknown = 4 } +public enum Microsoft.AspNet.OData.ODataAPIResponseStatus : int { + Failure = 1 + NotFound = 2 + Success = 0 +} + public interface Microsoft.AspNet.OData.IDelta { void Clear () System.Collections.Generic.IEnumerable`1[[System.String]] GetChangedPropertyNames () @@ -25,6 +31,22 @@ public interface Microsoft.AspNet.OData.IDelta { bool TrySetPropertyValue (string name, object value) } +public interface Microsoft.AspNet.OData.IDeltaDeletedEntityObject { + System.Uri Id { public abstract get; public abstract set; } + Microsoft.OData.Edm.IEdmNavigationSource NavigationSource { public abstract get; public abstract set; } + System.Nullable`1[[Microsoft.OData.DeltaDeletedEntryReason]] Reason { public abstract get; public abstract set; } +} + +public interface Microsoft.AspNet.OData.IDeltaSet { +} + +public interface Microsoft.AspNet.OData.IDeltaSetItem { + EdmDeltaEntityKind DeltaKind { public abstract get; } + IODataIdContainer 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; } } @@ -63,6 +85,13 @@ public interface Microsoft.AspNet.OData.IEdmStructuredObject : IEdmObject { bool TryGetPropertyValue (string propertyName, out System.Object& value) } +public interface Microsoft.AspNet.OData.IODataAPIHandler { +} + +public interface Microsoft.AspNet.OData.IODataIdContainer { + string ODataId { public abstract get; public abstract set; } +} + public interface Microsoft.AspNet.OData.IPerRouteContainer { System.Func`1[[Microsoft.OData.IContainerBuilder]] BuilderFactory { public abstract get; public abstract set; } @@ -111,6 +140,25 @@ public abstract class Microsoft.AspNet.OData.EdmStructuredObject : Delta, IDynam public virtual bool TrySetPropertyValue (string name, object value) } +public abstract class Microsoft.AspNet.OData.ODataAPIHandler`1 : IODataAPIHandler { + protected ODataAPIHandler`1 () + + public abstract IODataAPIHandler GetNestedHandler (TStructuralType parent, string navigationPropertyName) + public abstract ODataAPIResponseStatus TryCreate (System.Collections.Generic.IDictionary`2[[System.String],[System.Object]] keyValues, out TStructuralType& createdObject, out System.String& errorMessage) + public abstract ODataAPIResponseStatus TryDelete (System.Collections.Generic.IDictionary`2[[System.String],[System.Object]] keyValues, out System.String& errorMessage) + public abstract ODataAPIResponseStatus TryGet (System.Collections.Generic.IDictionary`2[[System.String],[System.Object]] keyValues, out TStructuralType& originalObject, out System.String& errorMessage) + public virtual void UpdateLinkedObjects (TStructuralType resource, Microsoft.OData.Edm.IEdmModel model) +} + +public abstract class Microsoft.AspNet.OData.ODataAPIHandlerFactory { + protected ODataAPIHandlerFactory (Microsoft.OData.Edm.IEdmModel model) + + Microsoft.OData.Edm.IEdmModel Model { public get; } + + public abstract IODataAPIHandler GetHandler (Microsoft.OData.UriParser.ODataPath odataPath) + public IODataAPIHandler GetHandler (string path) +} + [ ODataFormattingAttribute(), ODataRoutingAttribute(), @@ -270,25 +318,30 @@ public class Microsoft.AspNet.OData.DefaultContainerBuilder : IContainerBuilder [ NonValidatingParameterBindingAttribute(), ] -public class Microsoft.AspNet.OData.Delta`1 : TypedDelta, IDynamicMetaObjectProvider, IDelta { +public class Microsoft.AspNet.OData.Delta`1 : TypedDelta, IDynamicMetaObjectProvider, IDelta, IDeltaSetItem { public Delta`1 () public Delta`1 (System.Type structuralType) public Delta`1 (System.Type structuralType, System.Collections.Generic.IEnumerable`1[[System.String]] updatableProperties) public Delta`1 (System.Type structuralType, System.Collections.Generic.IEnumerable`1[[System.String]] updatableProperties, System.Reflection.PropertyInfo dynamicDictionaryPropertyInfo) public Delta`1 (System.Type structuralType, System.Collections.Generic.IEnumerable`1[[System.String]] updatableProperties, System.Reflection.PropertyInfo dynamicDictionaryPropertyInfo, bool isComplexType) + public Delta`1 (System.Type structuralType, System.Collections.Generic.IEnumerable`1[[System.String]] updatableProperties, System.Reflection.PropertyInfo dynamicDictionaryPropertyInfo, bool isComplexType, System.Reflection.PropertyInfo instanceAnnotationsPropertyInfo) + EdmDeltaEntityKind DeltaKind { public virtual get; protected set; } System.Type ExpectedClrType { public virtual get; } bool IsComplexType { public get; } + IODataIdContainer ODataIdContainer { public virtual get; public virtual set; } + Microsoft.OData.UriParser.ODataPath ODataPath { public virtual get; public virtual set; } System.Type StructuredType { public virtual get; } + IODataInstanceAnnotationContainer TransientInstanceAnnotationContainer { public virtual get; public virtual set; } System.Collections.Generic.IList`1[[System.String]] UpdatableProperties { public get; } public virtual void Clear () - public TStructuralType CopyChangedValues (TStructuralType original) + public void CopyChangedValues (TStructuralType original) public void CopyUnchangedValues (TStructuralType original) public virtual System.Collections.Generic.IEnumerable`1[[System.String]] GetChangedPropertyNames () public TStructuralType GetInstance () public virtual System.Collections.Generic.IEnumerable`1[[System.String]] GetUnchangedPropertyNames () - public TStructuralType Patch (TStructuralType original) + public void Patch (TStructuralType original) public void Put (TStructuralType original) public bool TryGetNestedPropertyValue (string name, out System.Object& value) public virtual bool TryGetPropertyType (string name, out System.Type& type) @@ -296,6 +349,34 @@ public class Microsoft.AspNet.OData.Delta`1 : TypedDelta, IDynamicMetaObjectProv public virtual bool TrySetPropertyValue (string name, object value) } +[ +NonValidatingParameterBindingAttribute(), +] +public class Microsoft.AspNet.OData.DeltaDeletedEntityObject`1 : Delta`1, IDynamicMetaObjectProvider, IDelta, IDeltaDeletedEntityObject, IDeltaSetItem { + public DeltaDeletedEntityObject`1 () + public DeltaDeletedEntityObject`1 (System.Type structuralType) + public DeltaDeletedEntityObject`1 (System.Type structuralType, System.Collections.Generic.IEnumerable`1[[System.String]] updatableProperties) + public DeltaDeletedEntityObject`1 (System.Type structuralType, System.Reflection.PropertyInfo instanceAnnotationsPropertyInfo) + public DeltaDeletedEntityObject`1 (System.Type structuralType, System.Reflection.PropertyInfo dynamicDictionaryPropertyInfo, System.Reflection.PropertyInfo instanceAnnotationsPropertyInfo) + public DeltaDeletedEntityObject`1 (System.Type structuralType, System.Collections.Generic.IEnumerable`1[[System.String]] updatableProperties, System.Reflection.PropertyInfo dynamicDictionaryPropertyInfo, System.Reflection.PropertyInfo instanceAnnotationsPropertyInfo) + public DeltaDeletedEntityObject`1 (System.Type structuralType, System.Collections.Generic.IEnumerable`1[[System.String]] updatableProperties, System.Reflection.PropertyInfo dynamicDictionaryPropertyInfo, bool isComplexType, System.Reflection.PropertyInfo instanceAnnotationsPropertyInfo) + + System.Uri Id { public virtual get; public virtual set; } + Microsoft.OData.Edm.IEdmNavigationSource NavigationSource { public virtual get; public virtual set; } + 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) + public DeltaSet`1 Patch (ICollection`1 originalCollection) + public DeltaSet`1 Patch (ODataAPIHandler`1 apiHandlerOfT, ODataAPIHandlerFactory apiHandlerFactory) +} + [ NonValidatingParameterBindingAttribute(), ] @@ -303,6 +384,8 @@ public class Microsoft.AspNet.OData.EdmChangedObjectCollection : System.Collecti public EdmChangedObjectCollection (Microsoft.OData.Edm.IEdmEntityType entityType) public EdmChangedObjectCollection (Microsoft.OData.Edm.IEdmEntityType entityType, System.Collections.Generic.IList`1[[Microsoft.AspNet.OData.IEdmChangedObject]] changedObjectList) + Microsoft.OData.Edm.IEdmEntityType EntityType { public get; } + public virtual Microsoft.OData.Edm.IEdmTypeReference GetEdmType () } @@ -391,10 +474,17 @@ public class Microsoft.AspNet.OData.EdmDeltaLink : EdmEntityObject, IDynamicMeta [ NonValidatingParameterBindingAttribute(), ] -public class Microsoft.AspNet.OData.EdmEntityObject : EdmStructuredObject, IDynamicMetaObjectProvider, IDelta, IEdmEntityObject, IEdmObject, IEdmStructuredObject { +public class Microsoft.AspNet.OData.EdmEntityObject : EdmStructuredObject, IDynamicMetaObjectProvider, IDelta, IEdmChangedObject, IEdmEntityObject, IEdmObject, IEdmStructuredObject { public EdmEntityObject (Microsoft.OData.Edm.IEdmEntityType edmType) public EdmEntityObject (Microsoft.OData.Edm.IEdmEntityTypeReference edmType) public EdmEntityObject (Microsoft.OData.Edm.IEdmEntityType edmType, bool isNullable) + + EdmDeltaEntityKind DeltaKind { public virtual get; } + IODataIdContainer ODataIdContainer { public get; public set; } + IODataInstanceAnnotationContainer PersistentInstanceAnnotationsContainer { public get; public set; } + + public void AddDataException (Org.OData.Core.V1.DataModificationExceptionType dataModificationException) + public Org.OData.Core.V1.DataModificationExceptionType GetDataException () } [ @@ -546,6 +636,12 @@ public class Microsoft.AspNet.OData.ODataFormattingAttribute : System.Attribute public ODataFormattingAttribute () } +public class Microsoft.AspNet.OData.ODataIdContainer : IODataIdContainer { + public ODataIdContainer () + + string ODataId { public virtual get; public virtual set; } +} + public class Microsoft.AspNet.OData.ODataNullValueMessageHandler : IFilterMetadata, IResultFilter { public ODataNullValueMessageHandler () @@ -3555,6 +3651,39 @@ public sealed class Microsoft.AspNet.OData.Routing.ODataRoutePrefixAttribute : S string Prefix { public get; } } +public enum Org.OData.Core.V1.DataModificationOperationKind : int { + Delete = 3 + Insert = 0 + Invoke = 4 + Link = 5 + Unlink = 6 + Update = 1 + Upsert = 2 +} + +public abstract class Org.OData.Core.V1.ExceptionType { + protected ExceptionType () + + Org.OData.Core.V1.MessageType MessageType { public get; public set; } +} + +public class Org.OData.Core.V1.DataModificationExceptionType : Org.OData.Core.V1.ExceptionType { + public DataModificationExceptionType (Org.OData.Core.V1.DataModificationOperationKind failedOperation) + + Org.OData.Core.V1.DataModificationOperationKind FailedOperation { public get; } + short ResponseCode { public get; public set; } +} + +public class Org.OData.Core.V1.MessageType { + public MessageType () + + string Code { public get; public set; } + string Details { public get; public set; } + string Message { public get; public set; } + string Severity { public get; public set; } + string Target { public get; public set; } +} + public abstract class Microsoft.AspNet.OData.Formatter.Deserialization.ODataDeserializer { protected ODataDeserializer (Microsoft.OData.ODataPayloadKind payloadKind) @@ -3589,6 +3718,13 @@ public abstract class Microsoft.AspNet.OData.Formatter.Deserialization.ODataItem Microsoft.OData.ODataItem Item { public get; } } +public abstract class Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceSetWrapperBase : ODataItemBase { + public ODataResourceSetWrapperBase (Microsoft.OData.ODataResourceSetBase item) + + System.Collections.Generic.IList`1[[Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceWrapper]] Resources { public get; } + Microsoft.OData.ODataResourceSetBase ResourceSetBase { public get; } +} + [ ExtensionAttribute(), ] @@ -3717,7 +3853,11 @@ public class Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceSetDe public virtual System.Threading.Tasks.Task`1[[System.Object]] ReadAsync (Microsoft.OData.ODataMessageReader messageReader, System.Type type, ODataDeserializerContext readContext) public virtual object ReadInline (object item, Microsoft.OData.Edm.IEdmTypeReference edmType, ODataDeserializerContext readContext) - public virtual System.Collections.IEnumerable ReadResourceSet (ODataResourceSetWrapper resourceSet, Microsoft.OData.Edm.IEdmStructuredTypeReference elementType, ODataDeserializerContext readContext) + public virtual System.Collections.IEnumerable ReadResourceSet (ODataResourceSetWrapperBase resourceSet, Microsoft.OData.Edm.IEdmStructuredTypeReference elementType, ODataDeserializerContext readContext) +} + +public sealed class Microsoft.AspNet.OData.Formatter.Deserialization.ODataDeltaResourceSetWrapper : ODataResourceSetWrapperBase { + public ODataDeltaResourceSetWrapper (Microsoft.OData.ODataDeltaResourceSet item) } public sealed class Microsoft.AspNet.OData.Formatter.Deserialization.ODataNestedResourceInfoWrapper : ODataItemBase { @@ -3727,18 +3867,22 @@ public sealed class Microsoft.AspNet.OData.Formatter.Deserialization.ODataNested Microsoft.OData.ODataNestedResourceInfo NestedResourceInfo { public get; } } -public sealed class Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceSetWrapper : ODataItemBase { +public sealed class Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceSetWrapper : ODataResourceSetWrapperBase { public ODataResourceSetWrapper (Microsoft.OData.ODataResourceSet item) - System.Collections.Generic.IList`1[[Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceWrapper]] Resources { public get; } Microsoft.OData.ODataResourceSet ResourceSet { public get; } } public sealed class Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceWrapper : ODataItemBase { - public ODataResourceWrapper (Microsoft.OData.ODataResource item) + public ODataResourceWrapper (Microsoft.OData.ODataResourceBase item) System.Collections.Generic.IList`1[[Microsoft.AspNet.OData.Formatter.Deserialization.ODataNestedResourceInfoWrapper]] NestedResourceInfos { public get; } + [ + ObsoleteAttribute(), + ] Microsoft.OData.ODataResource Resource { public get; } + + Microsoft.OData.ODataResourceBase ResourceBase { public get; } } public abstract class Microsoft.AspNet.OData.Formatter.Serialization.ODataEdmTypeSerializer : ODataSerializer { @@ -3899,9 +4043,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/tools/GetNugetPackageMetadata.proj b/tools/GetNugetPackageMetadata.proj index 2d9f3abfa2..723a88ffb3 100644 --- a/tools/GetNugetPackageMetadata.proj +++ b/tools/GetNugetPackageMetadata.proj @@ -1,5 +1,8 @@ + + $([System.DateTime]::Now.ToString("yyyyMMddHHmm")) + @@ -11,4 +14,5 @@ + \ No newline at end of file diff --git a/tools/WebStack.versions.settings.targets b/tools/WebStack.versions.settings.targets index af6c857e58..7dff19d854 100644 --- a/tools/WebStack.versions.settings.targets +++ b/tools/WebStack.versions.settings.targets @@ -4,8 +4,8 @@ 7 - 5 - 17 + 6 + 0 @@ -40,7 +40,7 @@ $(VersionFullSemantic) $(VersionFullSemantic)-$(VersionRelease) - +