From 0bce83dd667ac47ca53c25854dece155c1ddfd61 Mon Sep 17 00:00:00 2001 From: Sreejith Pazhampilly <37782322+Sreejithpin@users.noreply.github.com> Date: Thu, 3 Jun 2021 06:10:47 -0700 Subject: [PATCH 01/28] Bulk operations8 (#2502) * Bulk operations6 (#2) * BulkInsert1 draft * Bulk insert changes * deleted old test * updates * Update ODataResourceSetWrapper.cs * updates * Updated addressing comments * updates * updates updates * Updates * Updates * Minor updates * comments * updates * update publicapi for core * Address comments * Cleanup and additional tests Cleanup and additional tests * Updated code * BulkInsert1 draft * Bulk insert changes * deleted old test * updates * updates * Updated addressing comments * updates * updates updates * Updates * Updates * Minor updates * updates * Address comments * Cleanup and additional tests Cleanup and additional tests * DataAnnotationException updates * comments * small updates * updates * small update * updates * Updates * Update DeltaSetOfT.cs * Updates with Patch * updates * updates * Update WebHostTestFixture.cs * updates * Update DeltaOfTStructuralType.cs * Updates * Updates for serializer etc * Update WebHostTestFixture.cs * updates * updates * updates * Bulk Operations Updates * Review comments addressed * Updates * updated for deltaentity * Update Microsoft.AspNet.OData.Test.csproj * Update Microsoft.AspNet.OData.PublicApi.bsl * update public api * test fix * Update EdmStructuredObject.cs * Update EdmStructuredObject.cs * Update BulkInsertTest.cs * update * public api * review comments * Review comments updates * smalll update * updates * updates * updates --- .../AspNetODataSample.Web.csproj | 8 +- samples/AspNetODataSample.Web/Web.config | 164 ++-- .../Common/Error.cs | 1 + .../Common/SRResources.Designer.cs | 66 ++ .../Common/SRResources.resx | 18 + .../DataModificationExceptionType.cs | 115 +++ .../DefaultEdmPatchMethodHandler.cs | 154 ++++ .../DefaultPatchHandler.cs | 149 ++++ .../DeltaDeletedEntityObjectOfT.cs | 108 +++ .../DeltaOfTStructuralType.cs | 332 ++++++-- .../DeltaSetOfT.cs | 328 ++++++++ .../EdmChangedObjectCollection.cs | 281 ++++++- .../EdmDeltaDeletedEntityObject.cs | 7 +- .../EdmDeltaDeletedLink.cs | 7 +- .../EdmDeltaEntityObject.cs | 5 +- .../EdmDeltaLink.cs | 7 +- .../EdmEntityObject.cs | 47 +- .../EdmPatchMethodHandler.cs | 53 ++ .../EdmStructuredObject.cs | 10 +- .../EdmTypeExtensions.cs | 2 +- .../CollectionDeserializationHelpers.cs | 19 +- .../DefaultODataDeserializerProvider.cs | 2 - .../Deserialization/DeserializationHelpers.cs | 71 +- .../ODataDeltaResourceSetWrapper.cs | 26 + .../ODataDeserializerContext.cs | 63 +- .../Deserialization/ODataReaderExtensions.cs | 81 +- .../ODataResourceDeserializer.cs | 103 ++- .../ODataResourceSetDeserializer.cs | 93 ++- .../ODataResourceSetWrapper.cs | 22 +- .../ODataResourceSetWrapperBase.cs | 41 + .../Deserialization/ODataResourceWrapper.cs | 15 +- .../Deserialization/ResourceSetType.cs | 25 + .../Formatter/EdmLibHelpers.cs | 23 +- .../Formatter/ODataModelBinderConverter.cs | 6 +- .../Formatter/ODataOutputFormatterHelper.cs | 5 +- .../DefaultODataSerializerProvider.cs | 4 + .../Serialization/ODataDeltaFeedSerializer.cs | 148 +++- .../Serialization/ODataResourceSerializer.cs | 181 ++-- .../Serialization/ODataSerializerContext.cs | 45 +- .../Serialization/ODataSerializerHelper.cs | 111 +++ .../IDeltaDeletedEntityObject.cs | 31 + .../IDeltaSet.cs | 17 + .../IDeltaSetItem.cs | 27 + .../Microsoft.AspNet.OData.Shared.projitems | 15 + .../PatchMethodHandler.cs | 81 ++ .../ResourceContext.cs | 11 +- .../Conventions/EntitySetRoutingConvention.cs | 7 + .../NavigationRoutingConvention.cs | 4 +- .../TransientAnnotations.cs | 13 + .../TypedDelta.cs | 2 +- .../GlobalSuppressions.cs | 34 + .../Microsoft.Test.E2E.AspNet.OData.csproj | 15 + ...Microsoft.Test.E2E.AspNetCore.OData.csproj | 12 + .../BulkOperation/EFTests/BulkInsertTestEF.cs | 252 ++++++ .../EFTests/BulkOperationControllerEF.cs | 196 +++++ .../EFTests/BulkOperationPatchHandlersEF.cs | 397 +++++++++ ...crosoft.Test.E2E.AspNetCore3x.OData.csproj | 13 + .../BulkOperation/BulkInsertController.cs | 373 +++++++++ .../BulkOperation/BulkInsertDataModel.cs | 178 ++++ .../BulkOperation/BulkInsertEdmModel.cs | 101 +++ .../BulkOperation/BulkInsertTest.cs | 782 ++++++++++++++++++ .../BulkOperationPatchHandlers.cs | 632 ++++++++++++++ .../Common/Execution/WebHostTestBase.cs | 4 +- .../Common/Execution/WebHostTestFixture.cs | 4 +- .../ComplexTypeInheritanceTests.cs | 10 +- .../DeltaQueryTests/DeltaQueryTests.cs | 15 +- .../DollarLevels/DollarLevelsController.cs | 2 +- .../DollarLevels/DollarLevelsDataModel.cs | 9 + .../DollarLevels/DollarLevelsEdmModel.cs | 1 + .../Formatter/DeltaTests.cs | 36 +- .../Untyped/UntypedDeltaSerializationTests.cs | 4 +- .../DeltaSetOfTTest.cs | 309 +++++++ .../EdmChangedObjectCollectionTest.cs | 419 +++++++++- .../DeserializationHelpersTest.cs | 60 ++ .../ODataResourceDeserializerTests.cs | 32 +- .../ODataResourceSetDeserializerTest.cs | 57 +- .../ODataDeltaFeedSerializerTests.cs | 30 + ...crosoft.AspNet.OData.Test.Shared.projitems | 1 + .../Routing/AttributeRoutingTest.cs | 7 + .../EntitySetRoutingConventionTest.cs | 22 + .../Microsoft.AspNet.OData.Test.csproj | 5 +- .../Microsoft.AspNet.OData.PublicApi.bsl | 198 ++++- .../Microsoft.AspNet.OData.Test/app.config | 15 +- .../Microsoft.AspNetCore.OData.Test.csproj | 2 +- .../Microsoft.AspNetCore.OData.PublicApi.bsl | 157 +++- ...Microsoft.AspNetCore3x.OData.PublicApi.bsl | 191 ++++- 86 files changed, 7205 insertions(+), 524 deletions(-) create mode 100644 src/Microsoft.AspNet.OData.Shared/DataModificationExceptionType.cs create mode 100644 src/Microsoft.AspNet.OData.Shared/DefaultEdmPatchMethodHandler.cs create mode 100644 src/Microsoft.AspNet.OData.Shared/DefaultPatchHandler.cs create mode 100644 src/Microsoft.AspNet.OData.Shared/DeltaDeletedEntityObjectOfT.cs create mode 100644 src/Microsoft.AspNet.OData.Shared/DeltaSetOfT.cs create mode 100644 src/Microsoft.AspNet.OData.Shared/EdmPatchMethodHandler.cs create mode 100644 src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataDeltaResourceSetWrapper.cs create mode 100644 src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceSetWrapperBase.cs create mode 100644 src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ResourceSetType.cs create mode 100644 src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataSerializerHelper.cs create mode 100644 src/Microsoft.AspNet.OData.Shared/IDeltaDeletedEntityObject.cs create mode 100644 src/Microsoft.AspNet.OData.Shared/IDeltaSet.cs create mode 100644 src/Microsoft.AspNet.OData.Shared/IDeltaSetItem.cs create mode 100644 src/Microsoft.AspNet.OData.Shared/PatchMethodHandler.cs create mode 100644 src/Microsoft.AspNet.OData.Shared/TransientAnnotations.cs create mode 100644 test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/BulkOperation/EFTests/BulkInsertTestEF.cs create mode 100644 test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/BulkOperation/EFTests/BulkOperationControllerEF.cs create mode 100644 test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/BulkOperation/EFTests/BulkOperationPatchHandlersEF.cs create mode 100644 test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertController.cs create mode 100644 test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertDataModel.cs create mode 100644 test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertEdmModel.cs create mode 100644 test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertTest.cs create mode 100644 test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationPatchHandlers.cs create mode 100644 test/UnitTest/Microsoft.AspNet.OData.Test.Shared/DeltaSetOfTTest.cs diff --git a/samples/AspNetODataSample.Web/AspNetODataSample.Web.csproj b/samples/AspNetODataSample.Web/AspNetODataSample.Web.csproj index ec9a437faf..263303ec7d 100644 --- a/samples/AspNetODataSample.Web/AspNetODataSample.Web.csproj +++ b/samples/AspNetODataSample.Web/AspNetODataSample.Web.csproj @@ -96,7 +96,13 @@ ..\..\sln\packages\Microsoft.Spatial.7.9.0\lib\net45\Microsoft.Spatial.dll - True + ..\..\sln\packages\Microsoft.Spatial.7.8.2\lib\net45\Microsoft.Spatial.dll + + + ..\..\sln\packages\System.Buffers.4.5.0\lib\netstandard2.0\System.Buffers.dll + + + ..\..\sln\packages\System.Buffers.4.5.0\lib\netstandard2.0\System.Buffers.dll ..\..\sln\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll diff --git a/samples/AspNetODataSample.Web/Web.config b/samples/AspNetODataSample.Web/Web.config index e7cb3db212..989824878b 100644 --- a/samples/AspNetODataSample.Web/Web.config +++ b/samples/AspNetODataSample.Web/Web.config @@ -1,88 +1,88 @@ - + - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file 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/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..6401a9565b --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/DataModificationExceptionType.cs @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. 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 king 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..3cdd4d22ef --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/DefaultEdmPatchMethodHandler.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Linq; +using Microsoft.OData.Edm; + +namespace Microsoft.AspNet.OData +{ + internal class DefaultEdmPatchMethodHandler : EdmPatchMethodHandler + { + IEdmEntityType entityType; + ICollection originalList; + + public DefaultEdmPatchMethodHandler(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 PatchStatus TryGet(IDictionary keyValues, out IEdmStructuredObject originalObject, out string errorMessage) + { + PatchStatus status = PatchStatus.Success; + errorMessage = string.Empty; + originalObject = null; + + Contract.Assert(keyValues != null); + + try + { + originalObject = GetFilteredItem(keyValues); + + if (originalObject == null) + { + status = PatchStatus.NotFound; + } + } + catch (Exception ex) + { + status = PatchStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + public override PatchStatus TryCreate(IEdmChangedObject changedObject, out IEdmStructuredObject createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + createdObject = new EdmEntityObject(entityType); + originalList.Add(createdObject); + + return PatchStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return PatchStatus.Failure; + } + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + public override PatchStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + EdmStructuredObject originalObject = GetFilteredItem(keyValues); + + if (originalObject != null) + { + originalList.Remove(originalObject); + } + + return PatchStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return PatchStatus.Failure; + } + } + + public override EdmPatchMethodHandler GetNestedPatchHandler(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 DefaultEdmPatchMethodHandler(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/DefaultPatchHandler.cs b/src/Microsoft.AspNet.OData.Shared/DefaultPatchHandler.cs new file mode 100644 index 0000000000..ccd9bd080e --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/DefaultPatchHandler.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.OData +{ + internal class DefaultPatchHandler : PatchMethodHandler where TStructuralType :class + { + Type _clrType; + ICollection originalList; + + public DefaultPatchHandler(ICollection originalList) + { + this._clrType = typeof(TStructuralType); + this.originalList = originalList?? new List(); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + public override PatchStatus TryGet(IDictionary keyValues, out TStructuralType originalObject, out string errorMessage) + { + PatchStatus status = PatchStatus.Success; + errorMessage = string.Empty; + originalObject = default(TStructuralType); + + try + { + originalObject = GetFilteredItem(keyValues); + + if (originalObject == null) + { + status = PatchStatus.NotFound; + } + } + catch (Exception ex) + { + status = PatchStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + public override PatchStatus TryCreate(Delta patchObject, 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 PatchStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return PatchStatus.Failure; + } + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + public override PatchStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + TStructuralType originalObject = GetFilteredItem(keyValues); + originalList.Remove(originalObject); + + return PatchStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return PatchStatus.Failure; + } + } + + public override IPatchMethodHandler GetNestedPatchHandler(TStructuralType parent, string navigationPropertyName) + { + foreach (PropertyInfo property in _clrType.GetProperties()) + { + if (property.Name == navigationPropertyName) + { + Type type = typeof(DefaultPatchHandler<>).MakeGenericType(property.PropertyType.GetGenericArguments()[0]); + + return Activator.CreateInstance(type, property.GetValue(parent)) as IPatchMethodHandler; + } + } + + 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..6a249de90e --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/DeltaDeletedEntityObjectOfT.cs @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. 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) + : base(structuralType, updatableProperties, dynamicDictionaryPropertyInfo, 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 58d7f28cca..b80d3108a6 100644 --- a/src/Microsoft.AspNet.OData.Shared/DeltaOfTStructuralType.cs +++ b/src/Microsoft.AspNet.OData.Shared/DeltaOfTStructuralType.cs @@ -6,6 +6,7 @@ //------------------------------------------------------------------------------ using System; +using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; @@ -16,8 +17,11 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Serialization; +using Microsoft.AspNet.OData.Builder; +using Microsoft.AspNet.OData.Builder.Conventions.Attributes; using Microsoft.AspNet.OData.Common; using Microsoft.AspNet.OData.Formatter; +using Microsoft.OData.Edm; namespace Microsoft.AspNet.OData { @@ -26,7 +30,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,9 +48,11 @@ private static readonly ConcurrentDictionary _changedDynamicProperties; private IDictionary _dynamicDictionaryCache; - + private NavigationPath _navigationPath; + /// /// Initializes a new instance of . /// @@ -79,6 +85,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,12 +111,13 @@ 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) + { + } /// /// Initializes a new instance of . @@ -107,21 +130,29 @@ 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); + InitializeProperties(updatableProperties); + TransientInstanceAnnotationContainer = new ODataInstanceAnnotationContainer(); + _instanceAnnotationsPropertyInfo = instanceAnnotationsPropertyInfo; + _navigationPath = new NavigationPath(structuralType.Name, null); + DeltaKind = EdmDeltaEntityKind.Entry; IsComplexType = isComplexType; } - - /// public override Type StructuredType => _structuredType; + internal IDictionary DeltaNestedResources + { + get { return _deltaNestedResources; } + } + /// public override Type ExpectedClrType => typeof(TStructuralType); @@ -139,6 +170,20 @@ public IList 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; } + + /// + internal PropertyInfo InstanceAnnotationsPropertyInfo { get { return _instanceAnnotationsPropertyInfo; } } + /// public override void Clear() { @@ -153,6 +198,19 @@ public override bool TrySetPropertyValue(string name, object value) throw Error.ArgumentNull("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. @@ -171,7 +229,7 @@ public override bool TrySetPropertyValue(string name, object value) } } - if (value is IDelta) + if (value is IDelta || value is IDeltaSet) { return TrySetNestedResourceInternal(name, value); } @@ -189,6 +247,19 @@ public override bool TryGetPropertyValue(string name, out object value) throw Error.ArgumentNull("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) @@ -209,6 +280,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. @@ -311,6 +390,13 @@ public override IEnumerable GetUnchangedPropertyNames() /// /// The entity to be updated. public void CopyChangedValues(TStructuralType original) + { + CopyChangedValues(original, 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) { @@ -324,6 +410,12 @@ public void CopyChangedValues(TStructuralType original) throw Error.Argument("original", SRResources.DeltaTypeMismatch, _structuredType, original.GetType()); } + //To apply ODataId if its present + if (apiHandlerFactory != null && ODataIdContainer?.ODataIdNavigationPath != null) + { + ApplyODataId(original, apiHandlerFactory); + } + RuntimeHelpers.EnsureSufficientExecutionStack(); // For regular non-structural properties at current level. @@ -342,91 +434,53 @@ public void 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(original, nestedResourceName, originalNestedResource, newType, originalType, deltaNestedResource.ExpectedClrType); + throw Error.Argument(nestedResourceName, SRResources.DeltaNestedResourceNameNotFound, + nestedResourceName, original.GetType()); } - deltaNestedResource.CopyChangedValues(originalNestedResource); - } - } - } - - 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(); + if (originalNestedResource == null) + { + // When patching original target of null value, directly set nested resource. + dynamic deltaObject = _deltaNestedResources[nestedResourceName]; + dynamic instance = deltaObject.GetInstance(); - //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; - } + // Recursively patch up the instance with the nested resources. + deltaObject.CopyChangedValues(instance); - newBaseTypes.Add(declaredType); + _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."); - //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; + Type newType = deltaNestedResource.StructuredType; + Type originalType = originalNestedResource.GetType(); - 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); + if (deltaNestedResource.IsComplexType && newType != originalType) + { + originalNestedResource = ReAssignComplexDerivedType(original, nestedResourceName, originalNestedResource, newType, originalType, deltaNestedResource.ExpectedClrType); + } - //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); + deltaNestedResource.CopyChangedValues(originalNestedResource); } - - _structuredType.GetProperty(nestedPropertyName).SetValue(parent, (object)newOriginalNestedResource); - - break; } - - originalType = originalType.BaseType; + } - - return newOriginalNestedResource; } /// @@ -465,6 +519,51 @@ 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. + /// API Handler Factory + public void Patch(TStructuralType original, ODataAPIHandlerFactory apiHandlerFactory) + { + IODataAPIHandler apiHandler = apiHandlerFactory.GetHandler(_navigationPath); + + 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 ApplyODataId(TStructuralType original, ODataAPIHandlerFactory apiHandlerFactory) + { + IODataAPIHandler refapiHandler = apiHandlerFactory.GetHandler(ODataIdContainer.ODataIdNavigationPath); + + if (refapiHandler != null) + { + ODataAPIHandler refapiHandlerOfT = refapiHandler as ODataAPIHandler; + + Debug.Assert(refapiHandlerOfT != null); + + TStructuralType referencedObj; + string error; + + //Checking to get the referenced entity, get the properties and apply it on original object + if (refapiHandlerOfT.TryGet(ODataIdContainer.ODataIdNavigationPath.GetNavigationPathItems().Last().KeyProperties, out referencedObj, out error) == ODataAPIResponseStatus.Success) + { + foreach (string property in _updatableProperties) + { + PropertyInfo propertyInfo = _structuredType.GetProperty(property); + + object value = propertyInfo.GetValue(referencedObj); + propertyInfo.SetValue(original, value); + } + } + } + } + /// /// Overwrites the entity with the values stored in this Delta. /// The semantics of this operation are equivalent to a HTTP PUT operation, hence the name. @@ -476,6 +575,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) { @@ -591,10 +742,10 @@ private void InitializeProperties(IEnumerable updatableProperties) _structuredType, (backingType) => backingType .GetProperties(BindingFlags.Instance | BindingFlags.Public) - .Where(p => !IsIgnoredProperty(backingType.GetCustomAttributes(typeof(DataContractAttribute), inherit: true).Any(), p) && (p.GetSetMethod() != null || TypeHelper.IsCollection(p.PropertyType)) && p.GetGetMethod() != null) + .Where(p => !IsIgnoredProperty(backingType.GetCustomAttributes(typeof(DataContractAttribute), inherit: true).Any(), p) && (p.GetSetMethod() != null || TypeHelper.IsCollection(p.PropertyType)) && p.GetGetMethod() != null) .Select>(p => new FastPropertyAccessor(p)) .ToDictionary(p => p.Property.Name)); - + if (updatableProperties != null) { _updatableProperties = updatableProperties.Intersect(_allProperties.Keys).ToList(); @@ -629,7 +780,7 @@ private bool IsIgnoredProperty(bool isTypeDataContract, PropertyInfo propertyInf return !propertyInfo.GetCustomAttributes(typeof(DataMemberAttribute), inherit: true).Any(); } - return propertyInfo.GetCustomAttributes(typeof(IgnoreDataMemberAttribute), inherit: true).Any(); + return propertyInfo.GetCustomAttributes(typeof(IgnoreDataMemberAttribute), inherit: true).Any(); } // Copy changed dynamic properties and leave the unchanged dynamic properties @@ -779,11 +930,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 @@ -793,4 +949,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..dffc2bab88 --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/DeltaSetOfT.cs @@ -0,0 +1,328 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. 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.Contracts; +using System.Linq; +using System.Reflection; +using Microsoft.AspNet.OData.Builder; +using Microsoft.AspNet.OData.Common; +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(), typeof(TStructuralType)); + } + + 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) + { + PatchMethodHandler patchHandler = new DefaultPatchHandler(originalCollection); + + return CopyChangedValues(patchHandler); + } + + /// + /// Patch for DeltaSet, a collection for Delta + /// + /// DeltaSet response + public DeltaSet Patch(IPatchMethodHandler patchHandler) + { + return CopyChangedValues(patchHandler as PatchMethodHandler); + } + + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + internal DeltaSet CopyChangedValues(PatchMethodHandler patchHandler) + { + //Here we are getting the keys and using the keys to find the original object + //to patch from the list of collection + + 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 + { + PatchStatus patchStatus = patchHandler.TryGet(keyValues, out original, out getErrorMessage); + + DeltaDeletedEntityObject deletedObj = changedObj as DeltaDeletedEntityObject; + + if (patchStatus == PatchStatus.Failure || (deletedObj != null && patchStatus == PatchStatus.NotFound)) + { + IDeltaSetItem deltaSetItem = changedObj; + + DataModificationExceptionType dataModificationExceptionType = new DataModificationExceptionType(operation); + dataModificationExceptionType.MessageType = new MessageType { Message = getErrorMessage }; + + deltaSetItem.TransientInstanceAnnotationContainer.AddResourceAnnotation(SRResources.DataModificationException, dataModificationExceptionType); + + continue; + } + + if (deletedObj != null) + { + operation = DataModificationOperationKind.Delete; + + changedObj.Patch(original, patchHandler); + + if (patchHandler.TryDelete(keyValues, out errorMessage) != PatchStatus.Success) + { + //Handle Failed Operation - Delete + + if (patchStatus == PatchStatus.Success) + { + IDeltaSetItem changedObject = HandleFailedOperation(changedObj, operation, original, errorMessage); + deltaSet.Add(changedObject); + continue; + } + } + + deltaSet.Add(deletedObj); + } + else + { + if (patchStatus == PatchStatus.NotFound) + { + operation = DataModificationOperationKind.Insert; + + if (patchHandler.TryCreate(changedObj, out original, out errorMessage) != PatchStatus.Success) + { + //Handle failed Opreataion - create + IDeltaSetItem changedObject = HandleFailedOperation(changedObj, operation, original, errorMessage); + deltaSet.Add(changedObject); + continue; + } + } + else if (patchStatus == PatchStatus.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.Patch(original, patchHandler); + + 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, + 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..9c2e1882fe 100644 --- a/src/Microsoft.AspNet.OData.Shared/EdmChangedObjectCollection.cs +++ b/src/Microsoft.AspNet.OData.Shared/EdmChangedObjectCollection.cs @@ -5,11 +5,17 @@ // //------------------------------------------------------------------------------ +using System; +using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics.Contracts; using System.Linq; +using System.Net.Http.Headers; using Microsoft.AspNet.OData.Common; +using Microsoft.OData; using Microsoft.OData.Edm; +using Org.OData.Core.V1; namespace Microsoft.AspNet.OData { @@ -22,7 +28,7 @@ public class EdmChangedObjectCollection : Collection, IEdmObj private IEdmEntityType _entityType; private EdmDeltaCollectionType _edmType; private IEdmCollectionTypeReference _edmTypeReference; - + /// /// Initializes a new instance of the class. /// @@ -43,7 +49,13 @@ public EdmChangedObjectCollection(IEdmEntityType entityType, IList + /// Represents EntityType of the changedobject + /// + public IEdmEntityType EntityType { get { return _entityType; } } + /// public IEdmTypeReference GetEdmType() { @@ -61,5 +73,270 @@ 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) + { + EdmPatchMethodHandler patchHandler = new DefaultEdmPatchMethodHandler(originalCollection, _entityType); + + return CopyChangedValues(patchHandler); + } + + /// + /// Patch for EdmChangedObjectCollection, a collection for IEdmChangedObject + /// + /// ChangedObjectCollection response + public EdmChangedObjectCollection Patch(EdmPatchMethodHandler patchHandler) + { + return CopyChangedValues(patchHandler); + } + + + [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(EdmPatchMethodHandler patchHandler) + { + EdmChangedObjectCollection changedObjectCollection = new EdmChangedObjectCollection(_entityType); + IEdmStructuralProperty[] keys = _entityType.Key().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; + EdmDeltaDeletedEntityObject deletedObj = changedObj as EdmDeltaDeletedEntityObject; + + if (deletedObj != null) + { + operation = DataModificationOperationKind.Delete; + + if (patchHandler.TryDelete(keyValues, out errorMessage) != PatchStatus.Success) + { + //Handle Failed Operation - Delete + PatchStatus patchStatus = patchHandler.TryGet(keyValues, out original, out getErrorMessage); + if (patchStatus == PatchStatus.Success) + { + IEdmChangedObject changedObject = HandleFailedOperation(deletedObj, operation, original, keys, errorMessage, patchHandler); + changedObjectCollection.Add(changedObject); + continue; + } + } + + changedObjectCollection.Add(deletedObj); + } + else + { + EdmEntityObject deltaEntityObject = changedObj as EdmEntityObject; + + PatchStatus patchStatus = patchHandler.TryGet(keyValues, out original, out getErrorMessage); + + if (patchStatus == PatchStatus.NotFound) + { + operation = DataModificationOperationKind.Insert; + + if (patchHandler.TryCreate(changedObj, out original, out errorMessage) != PatchStatus.Success) + { + //Handle failed Opreataion - create + IEdmChangedObject changedObject = HandleFailedOperation(deltaEntityObject, operation, original, keys, errorMessage, patchHandler); + changedObjectCollection.Add(changedObject); + continue; + } + } + else if (patchStatus == PatchStatus.Success) + { + operation = DataModificationOperationKind.Update; + } + else + { + //Handle failed operation + IEdmChangedObject changedObject = HandleFailedOperation(deltaEntityObject, operation, null, keys, getErrorMessage, patchHandler); + changedObjectCollection.Add(changedObject); + continue; + } + + //Patch for addition/update. + PatchItem(deltaEntityObject, original as EdmStructuredObject, patchHandler); + + changedObjectCollection.Add(changedObj); + } + } + catch (Exception ex) + { + //Handle Failed Operation + IEdmChangedObject changedObject = HandleFailedOperation(changedObj as EdmEntityObject, operation, originalObj, keys, ex.Message, patchHandler); + + Contract.Assert(changedObject != null); + changedObjectCollection.Add(changedObject); + } + } + + return changedObjectCollection; + } + + private static IDictionary GetKeyValues(IEdmStructuralProperty [] keys, IEdmChangedObject changedObj) + { + IDictionary keyValues = new Dictionary(); + + foreach (IEdmStructuralProperty key in keys) + { + object value; + changedObj.TryGetPropertyValue(key.Name, out value); + + if (value != null) + { + keyValues.Add(key.Name, value); + } + } + + return keyValues; + } + + private void PatchItem(EdmStructuredObject changedObj, EdmStructuredObject originalObj, EdmPatchMethodHandler patchHandler) + { + foreach (string propertyName in changedObj.GetChangedPropertyNames()) + { + ApplyProperties(changedObj, originalObj, propertyName, patchHandler); + } + + foreach (string propertyName in changedObj.GetUnchangedPropertyNames()) + { + ApplyProperties(changedObj, originalObj, propertyName, patchHandler); + } + } + + private void ApplyProperties(EdmStructuredObject changedObj, EdmStructuredObject originalObj, string propertyName, EdmPatchMethodHandler patchHandler) + { + object value; + if (changedObj.TryGetPropertyValue(propertyName, out value)) + { + EdmChangedObjectCollection changedColl = value as EdmChangedObjectCollection; + if (changedColl != null) + { + EdmPatchMethodHandler patchHandlerNested = patchHandler.GetNestedPatchHandler(originalObj, propertyName); + if (patchHandlerNested != null) + { + changedColl.Patch(patchHandlerNested); + } + 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, patchHandler); + } + else + { + originalObj.TrySetPropertyValue(propertyName, value); + } + } + } + } + + private IEdmChangedObject HandleFailedOperation(EdmEntityObject changedObj, DataModificationOperationKind operation, IEdmStructuredObject originalObj, + IEdmStructuralProperty[] keys, string errorMessage, EdmPatchMethodHandler patchHandler) + { + 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, patchHandler); + + 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, patchHandler); + + 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(IEdmStructuralProperty[] keys, EdmDeltaDeletedEntityObject edmDeletedObject) + { + bool hasNullKeys = false; + for (int i = 0; i < keys.Length; i++) + { + object value; + if (edmDeletedObject.TryGetPropertyValue(keys[i].Name, 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..f548c7a44b 100644 --- a/src/Microsoft.AspNet.OData.Shared/EdmDeltaDeletedEntityObject.cs +++ b/src/Microsoft.AspNet.OData.Shared/EdmDeltaDeletedEntityObject.cs @@ -80,12 +80,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..bd12856bb3 100644 --- a/src/Microsoft.AspNet.OData.Shared/EdmDeltaDeletedLink.cs +++ b/src/Microsoft.AspNet.OData.Shared/EdmDeltaDeletedLink.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.DeletedLinkEntry; } } } diff --git a/src/Microsoft.AspNet.OData.Shared/EdmDeltaEntityObject.cs b/src/Microsoft.AspNet.OData.Shared/EdmDeltaEntityObject.cs index e90a72ffaf..dcd7603af9 100644 --- a/src/Microsoft.AspNet.OData.Shared/EdmDeltaEntityObject.cs +++ b/src/Microsoft.AspNet.OData.Shared/EdmDeltaEntityObject.cs @@ -51,12 +51,11 @@ public EdmDeltaEntityObject(IEdmEntityType entityType, bool isNullable) } /// - 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..02b47b8b7f 100644 --- a/src/Microsoft.AspNet.OData.Shared/EdmEntityObject.cs +++ b/src/Microsoft.AspNet.OData.Shared/EdmEntityObject.cs @@ -6,7 +6,12 @@ //------------------------------------------------------------------------------ using System; +using System.Diagnostics.Contracts; +using System.Reflection; +using Microsoft.AspNet.OData.Builder; +using Microsoft.AspNet.OData.Common; using Microsoft.OData.Edm; +using Org.OData.Core.V1; namespace Microsoft.AspNet.OData { @@ -14,7 +19,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 +46,47 @@ 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; } + + /// + /// 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/EdmPatchMethodHandler.cs b/src/Microsoft.AspNet.OData.Shared/EdmPatchMethodHandler.cs new file mode 100644 index 0000000000..39def05d90 --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/EdmPatchMethodHandler.cs @@ -0,0 +1,53 @@ +using Microsoft.OData.Edm; +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.OData +{ + + /// + /// Handler Class to handle users methods for create, delete and update + /// + public abstract class EdmPatchMethodHandler + { + /// + /// 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 PatchStatus 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 PatchStatus 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 PatchStatus TryDelete(IDictionary keyValues, out string errorMessage); + + /// + /// Get the PatchHandler 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 EdmPatchMethodHandler GetNestedPatchHandler(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/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..10a0e1dc72 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/DeserializationHelpers.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/DeserializationHelpers.cs @@ -87,18 +87,43 @@ 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) { - return; + 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; - IODataInstanceAnnotationContainer instanceAnnotationContainer = GetAnnotationContainer(propertyInfo, resource); + if (deltaItem != null) + { + transientAnnotationContainer = deltaItem.TransientInstanceAnnotationContainer; + } + } - SetInstanceAnnotations(oDataResource, instanceAnnotationContainer, deserializerProvider, readContext); + if (instanceAnnotationContainer == null && transientAnnotationContainer == null) + { + return; + } + + SetInstanceAnnotations(oDataResource, instanceAnnotationContainer, transientAnnotationContainer, deserializerProvider, readContext); } internal static void SetDynamicProperty(object resource, IEdmStructuredTypeReference resourceType, @@ -140,15 +165,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 +193,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 +304,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 +350,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 +363,7 @@ public static IODataInstanceAnnotationContainer GetAnnotationContainer(PropertyI else { value = propertyInfo.GetValue(resource); - } + } IODataInstanceAnnotationContainer instanceAnnotationContainer = value as IODataInstanceAnnotationContainer; @@ -346,8 +379,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..daccf58009 --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataDeltaResourceSetWrapper.cs @@ -0,0 +1,26 @@ +// 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 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/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..e4826282d5 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializer.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializer.cs @@ -24,6 +24,7 @@ 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 { /// @@ -123,8 +124,9 @@ 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 +136,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 +181,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 +222,11 @@ public virtual object CreateResourceInstance(IEdmStructuredTypeReference structu { if (structuredType.IsEntity()) { + if (readContext.IsDeltaDeletedEntity) + { + return new EdmDeltaDeletedEntityObject(structuredType.AsEntity()); + } + return new EdmEntityObject(structuredType.AsEntity()); } @@ -226,8 +243,11 @@ 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()) { @@ -235,12 +255,12 @@ public virtual object CreateResourceInstance(IEdmStructuredTypeReference structu 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 +269,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 . /// @@ -325,7 +359,7 @@ public virtual void ApplyNestedProperty(object resource, ODataNestedResourceInfo continue; } - ODataResourceSetWrapper resourceSetWrapper = childItem as ODataResourceSetWrapper; + ODataResourceSetWrapperBase resourceSetWrapper = childItem as ODataResourceSetWrapperBase; if (resourceSetWrapper != null) { if (edmProperty == null) @@ -373,7 +407,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 +428,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); } /// @@ -422,6 +456,7 @@ public virtual void ApplyStructuralProperty(object resource, ODataProperty struc private void ApplyResourceProperties(object resource, ODataResourceWrapper resourceWrapper, IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext) + { ApplyStructuralProperties(resource, resourceWrapper, structuredType, readContext); ApplyNestedProperties(resource, resourceWrapper, structuredType, readContext); @@ -435,19 +470,8 @@ 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 // or delta resource accordingly. string propertyName = EdmLibHelpers.GetClrPropertyName(nestedProperty, readContext.Model); @@ -464,7 +488,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); @@ -503,7 +527,7 @@ private object ReadNestedResourceInline(ODataResourceWrapper resourceWrapper, IE if (readContext.IsUntyped) { clrType = structuredType.IsEntity() - ? typeof(EdmEntityObject) + ? (readContext.IsDeltaEntity ? (readContext.IsDeltaDeletedEntity? typeof(EdmDeltaDeletedEntityObject) : typeof(EdmDeltaEntityObject) ) : typeof(EdmEntityObject)) : typeof(EdmComplexObject); } else @@ -524,43 +548,32 @@ private object ReadNestedResourceInline(ODataResourceWrapper resourceWrapper, IE } 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,7 +600,7 @@ private void ApplyDynamicResourceSetInNestedProperty(string propertyName, object result, collectionType, readContext.Model); } - private object ReadNestedResourceSetInline(ODataResourceSetWrapper resourceSetWrapper, IEdmTypeReference edmType, + private object ReadNestedResourceSetInline(ODataResourceSetWrapperBase resourceSetWrapper, IEdmTypeReference edmType, ODataDeserializerContext readContext) { Contract.Assert(resourceSetWrapper != null); @@ -612,7 +625,8 @@ private object ReadNestedResourceSetInline(ODataResourceSetWrapper resourceSetWr { if (structuredType.IsEntity()) { - nestedReadContext.ResourceType = typeof(EdmEntityObjectCollection); + nestedReadContext.ResourceType = (readContext.IsDeltaOfT && resourceSetWrapper.ResourceSetType == ResourceSetType.DeltaResourceSet)? + typeof(EdmChangedObjectCollection): typeof(EdmEntityObjectCollection); } else { @@ -629,7 +643,8 @@ private object ReadNestedResourceSetInline(ODataResourceSetWrapper resourceSetWr Error.Format(SRResources.MappingDoesNotContainResourceType, structuredType.FullName())); } - nestedReadContext.ResourceType = typeof(List<>).MakeGenericType(clrType); + nestedReadContext.ResourceType = (readContext.IsDeltaOfT && resourceSetWrapper.ResourceSetType == ResourceSetType.DeltaResourceSet) + ? typeof(DeltaSet<>).MakeGenericType(clrType) : typeof(List<>).MakeGenericType(clrType); } return deserializer.ReadInline(resourceSetWrapper, edmType, nestedReadContext); 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..6b08110768 --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceSetWrapperBase.cs @@ -0,0 +1,41 @@ +// 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 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..70799cd070 --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ResourceSetType.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text; + +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/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..198b5d61e7 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); @@ -207,6 +207,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 +252,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 722aa128d4..9783fd9052 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataResourceSerializer.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataResourceSerializer.cs @@ -238,7 +238,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 +253,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); @@ -688,6 +688,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 +708,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 +723,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 +790,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 +824,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 +847,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 +974,60 @@ public virtual void AppendDynamicProperties(ODataResource resource, SelectExpand /// /// The describing the resource, which is being annotated. /// The context for the resource instance, which is being annotated. - public virtual void AppendInstanceAnnotations(ODataResource resource, ResourceContext resourceContext) + public virtual void AppendInstanceAnnotations(ODataResourceBase resource, ResourceContext resourceContext) { IEdmStructuredType structuredType = resourceContext.StructuredType; IEdmStructuredObject structuredObject = resourceContext.EdmObject; + + //For appending transient and persistent instance annotations for both enity object and normal resources + PropertyInfo instanceAnnotationInfo = EdmLibHelpers.GetInstanceAnnotationsContainer(structuredType, resourceContext.EdmModel); - object value; - - if (instanceAnnotationInfo == null || structuredObject == null || - !structuredObject.TryGetPropertyValue(instanceAnnotationInfo.Name, out value) || value == null) - { - return; - } + EdmEntityObject edmEntityObject = null; + object instanceAnnotations = null; + IODataInstanceAnnotationContainer transientAnnotations = null; - IODataInstanceAnnotationContainer instanceAnnotationContainer = value as IODataInstanceAnnotationContainer; + IDelta delta = null; - if (instanceAnnotationContainer != null) + if (resourceContext.SerializerContext.IsDeltaOfT) { - IDictionary clrAnnotations = instanceAnnotationContainer.GetResourceAnnotations(); - - if (clrAnnotations != null) - { - foreach (KeyValuePair annotation in clrAnnotations) - { - AddODataAnnotations(resource.InstanceAnnotations, resourceContext, annotation); - } - } - - foreach(ODataProperty property in resource.Properties) - { - string propertyName = property.Name; - - if (property.InstanceAnnotations == null) - { - property.InstanceAnnotations = new List(); - } - - IDictionary propertyAnnotations = instanceAnnotationContainer.GetPropertyAnnotations(propertyName); - - if (propertyAnnotations != null) - { - foreach (KeyValuePair annotation in propertyAnnotations) - { - AddODataAnnotations(property.InstanceAnnotations, resourceContext, annotation); - } - } - } + delta = resourceContext.ResourceInstance as IDelta; } - } - private void AddODataAnnotations(ICollection InstanceAnnotations, ResourceContext resourceContext, KeyValuePair annotation) - { - ODataValue annotationValue = null; - - if (annotation.Value != null) + if (delta != null) { - IEdmTypeReference edmTypeReference = resourceContext.SerializerContext.GetEdmType(annotation.Value, - annotation.Value.GetType()); + if (instanceAnnotationInfo != null) + { + delta.TryGetPropertyValue(instanceAnnotationInfo.Name, out instanceAnnotations); + + } - ODataEdmTypeSerializer edmTypeSerializer = GetEdmTypeSerializer(edmTypeReference); + IDeltaSetItem deltaitem = resourceContext.ResourceInstance as IDeltaSetItem; - if (edmTypeSerializer != null) + if(deltaitem != null) { - annotationValue = edmTypeSerializer.CreateODataValue(annotation.Value, edmTypeReference, resourceContext.SerializerContext); + transientAnnotations = deltaitem.TransientInstanceAnnotationContainer; } } else { - annotationValue = new ODataNullValue(); - } - - if (annotationValue != null) - { - InstanceAnnotations.Add(new ODataInstanceAnnotation(annotation.Key, annotationValue)); - } - } + if (instanceAnnotationInfo == null || structuredObject == null || + !structuredObject.TryGetPropertyValue(instanceAnnotationInfo.Name, out instanceAnnotations) || instanceAnnotations == null) + { + edmEntityObject = structuredObject as EdmEntityObject; - private ODataEdmTypeSerializer GetEdmTypeSerializer(IEdmTypeReference edmTypeReference) - { - ODataEdmTypeSerializer edmTypeSerializer; - - if (edmTypeReference.IsCollection()) - { - edmTypeSerializer = new ODataCollectionSerializer(SerializerProvider, true); - } - else if (edmTypeReference.IsStructured()) - { - edmTypeSerializer = new ODataResourceValueSerializer(SerializerProvider); - } - else - { - edmTypeSerializer = SerializerProvider.GetEdmTypeSerializer(edmTypeReference); + if (edmEntityObject != null) + { + instanceAnnotations = edmEntityObject.PersistentInstanceAnnotationsContainer; + transientAnnotations = edmEntityObject.TransientInstanceAnnotationContainer; + } + } } - return edmTypeSerializer; + ODataSerializerHelper.AppendInstanceAnnotations(resource, resourceContext, instanceAnnotations, SerializerProvider); + + ODataSerializerHelper.AppendInstanceAnnotations(resource, resourceContext, transientAnnotations, SerializerProvider); } /// @@ -1617,6 +1618,8 @@ private IEnumerable CreateStructuralPropertyBag(SelectExpandNode 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()) @@ -1626,11 +1629,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; @@ -1948,7 +1953,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 @@ -1973,7 +1978,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 @@ -2047,7 +2052,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..914ff6054a 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); + } + + 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..0e831615e5 --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataSerializerHelper.cs @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using Microsoft.AspNet.OData.Builder; +using Microsoft.OData; +using Microsoft.OData.Edm; +using Org.OData.Core.V1; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.AspNet.OData.Formatter.Serialization +{ + /// + /// Helper class for OData Serialization + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1053:StaticHolderTypesShouldNotHaveConstructors")] + public 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..703ad0f145 --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/IDeltaDeletedEntityObject.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. 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..8813099e35 --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/IDeltaSet.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.AspNet.OData +{ + /// + /// Basic interface to reperesent a deltaset which is a collection of Deltas. + /// + 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..aff7f077ef --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/IDeltaSetItem.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using Microsoft.AspNet.OData.Builder; +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; } + } +} 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..e97e428b49 100644 --- a/src/Microsoft.AspNet.OData.Shared/Microsoft.AspNet.OData.Shared.projitems +++ b/src/Microsoft.AspNet.OData.Shared/Microsoft.AspNet.OData.Shared.projitems @@ -53,12 +53,18 @@ + + + + + + @@ -71,8 +77,17 @@ + + + + + + + + + diff --git a/src/Microsoft.AspNet.OData.Shared/PatchMethodHandler.cs b/src/Microsoft.AspNet.OData.Shared/PatchMethodHandler.cs new file mode 100644 index 0000000000..c1c34748f0 --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/PatchMethodHandler.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.OData +{ + /// + /// Base Interface for PatchMethodHandler + /// + public interface IPatchMethodHandler + { + + } + + /// + /// Handler Class to handle users methods for create, delete and update + /// + public abstract class PatchMethodHandler: IPatchMethodHandler where TStructuralType : class + { + /// + /// TryCreate method to create a new object. + /// + /// The Delta object to be patched over original object. Optional to patch + /// 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 PatchStatus TryCreate(Delta patchObject, 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 PatchStatus 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 PatchStatus TryDelete(IDictionary keyValues, out string errorMessage); + + /// + /// Get the PatchHandler for the nested type + /// + /// Parent instance. + /// The name of the navigation property for the handler + /// The type of Nested PatchMethodHandler + public abstract IPatchMethodHandler GetNestedPatchHandler(TStructuralType parent, string navigationPropertyName); + } + + + /// + /// Enum for Patch Status + /// + public enum PatchStatus + { + /// + /// Success Status + /// + Success, + /// + /// Failure Status + /// + Failure, + /// + /// Resource Not Found + /// + NotFound + } + +} diff --git a/src/Microsoft.AspNet.OData.Shared/ResourceContext.cs b/src/Microsoft.AspNet.OData.Shared/ResourceContext.cs index 2950e17097..529c8287e0 100644 --- a/src/Microsoft.AspNet.OData.Shared/ResourceContext.cs +++ b/src/Microsoft.AspNet.OData.Shared/ResourceContext.cs @@ -195,12 +195,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..7098b414fd 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 PatchCustomer 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/TransientAnnotations.cs b/src/Microsoft.AspNet.OData.Shared/TransientAnnotations.cs new file mode 100644 index 0000000000..1946f2f18b --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/TransientAnnotations.cs @@ -0,0 +1,13 @@ +// 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 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..9963fa07c0 100644 --- a/src/Microsoft.AspNet.OData/GlobalSuppressions.cs +++ b/src/Microsoft.AspNet.OData/GlobalSuppressions.cs @@ -66,3 +66,37 @@ [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", "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", "CA1007:UseGenericsWhereAppropriate", Scope = "member", Target = "Microsoft.AspNet.OData.PatchMethodHandler+TryCreate.#Invoke(System.Object&,System.String&)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1007:UseGenericsWhereAppropriate", 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 = "0#", Scope = "member", Target = "Microsoft.AspNet.OData.PatchMethodHandler`1.#TryCreate(!0&,System.String&)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.PatchMethodHandler`1.#TryCreate(!0&,System.String&)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Scope = "member", Target = "Microsoft.AspNet.OData.PatchMethodHandler`1.#TryGet(System.Collections.Generic.IDictionary`2,!0&,System.String&)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.PatchMethodHandler`1.#TryDelete(System.Collections.Generic.IDictionary`2,System.String&)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.PatchMethodHandler`1.#TryGet(System.Collections.Generic.IDictionary`2,!0&,System.String&)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1040:AvoidEmptyInterfaces", Scope = "type", Target = "Microsoft.AspNet.OData.IPatchMethodHandler")] +[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.EdmPatchMethodHandler.#TryCreate(Microsoft.AspNet.OData.IEdmChangedObject,Microsoft.AspNet.OData.EdmStructuredObject&,System.String&)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Scope = "member", Target = "Microsoft.AspNet.OData.EdmPatchMethodHandler.#TryCreate(Microsoft.AspNet.OData.IEdmChangedObject,Microsoft.AspNet.OData.EdmStructuredObject&,System.String&)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Scope = "member", Target = "Microsoft.AspNet.OData.EdmPatchMethodHandler.#TryGet(System.Collections.Generic.IDictionary`2,Microsoft.AspNet.OData.EdmStructuredObject&,System.String&)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.EdmPatchMethodHandler.#TryDelete(System.Collections.Generic.IDictionary`2,System.String&)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.EdmPatchMethodHandler.#TryGet(System.Collections.Generic.IDictionary`2,Microsoft.AspNet.OData.EdmStructuredObject&,System.String&)")] + 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 508461bf32..3852c30b13 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 @@ -151,6 +151,21 @@ Aggregation\AggregationTests.cs + + BulkOperation\BulkInsertDataModel.cs + + + BulkOperation\BulkInsertEdmModel.cs + + + BulkOperation\BulkInsertTest.cs + + + BulkOperation\BulkInsertController.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 77e3770158..8d5a4432d0 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,18 @@ OpenType\TypedTest.cs + + BulkOperation\BulkInsertDataModel.cs + + + BulkOperation\BulkInsertEdmModel.cs + + + BulkOperation\BulkInsertController.cs + + + BulkOperation\BulkOperationPatchHandlers.cs + ParameterAlias\ParameterAliasDataSource.cs diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/BulkOperation/EFTests/BulkInsertTestEF.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/BulkOperation/EFTests/BulkInsertTestEF.cs new file mode 100644 index 0000000000..38b9108cea --- /dev/null +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/BulkOperation/EFTests/BulkInsertTestEF.cs @@ -0,0 +1,252 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +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.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Test.E2E.AspNet.OData.BulkOperation; +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.BulkInsert +{ + public class BulkInsertTestEF : WebHostTestBase + { + public BulkInsertTestEF(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", BulkInsertEdmModel.GetConventionModel(configuration)); + configuration.MapODataServiceRoute("explicit", "explicit", BulkInsertEdmModel.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':'http://host/service/$metadata#Employees/$delta', + 'value':[{ '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkInsert.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.BulkInsert.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); + + 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("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/BulkOperation/EFTests/BulkOperationControllerEF.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/BulkOperation/EFTests/BulkOperationControllerEF.cs new file mode 100644 index 0000000000..12edc96900 --- /dev/null +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/BulkOperation/EFTests/BulkOperationControllerEF.cs @@ -0,0 +1,196 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. 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.Routing; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Test.E2E.AspNet.OData.BulkOperation; +using Microsoft.Test.E2E.AspNet.OData.Common.Controllers; +using Xunit; + +namespace Microsoft.Test.E2E.AspNet.OData.BulkInsert +{ + public class EmployeesControllerEF : TestODataController + { + public EmployeesControllerEF() + { + + } + + public static List employees; + public static List friends; + + public 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; + } + + public 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)); + + + 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)); + + } + 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..604e00774f --- /dev/null +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/BulkOperation/EFTests/BulkOperationPatchHandlersEF.cs @@ -0,0 +1,397 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. 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.Test.E2E.AspNet.OData.BulkInsert; + +namespace Microsoft.Test.E2E.AspNet.OData.BulkOperation +{ + public 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.InstanceAnnotations); + modelBuilder.Entity().Ignore(c => c.InstanceAnnotations); + + modelBuilder.Entity().HasKey(c => c.Id); + + } + + + } + + public class EmployeeEFPatchHandler : PatchMethodHandler + { + EmployeeDBContext dbContext = null; + + public EmployeeEFPatchHandler(EmployeeDBContext dbContext) + { + this.dbContext = dbContext; + } + + public override PatchStatus TryCreate(Delta deltaObject, out Employee createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + createdObject = new Employee(); + dbContext.Employees.Add(createdObject); + + return PatchStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return PatchStatus.Failure; + } + } + + public override PatchStatus 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 PatchStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return PatchStatus.Failure; + } + } + + public override PatchStatus TryGet(IDictionary keyValues, out Employee originalObject, out string errorMessage) + { + PatchStatus status = PatchStatus.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 = PatchStatus.NotFound; + } + + } + catch (Exception ex) + { + status = PatchStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override IPatchMethodHandler GetNestedPatchHandler(Employee parent, string navigationPropertyName) + { + switch (navigationPropertyName) + { + case "Friends": + return new FriendEFPatchHandler(parent); + case "NewFriends": + return new NewFriendEFPatchHandler(parent); + default: + return null; + } + + } + + } + + public class FriendEFPatchHandler : PatchMethodHandler + { + Employee employee; + public FriendEFPatchHandler(Employee employee) + { + this.employee = employee; + } + + public override PatchStatus TryCreate(Delta deltaObject, out Friend createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + createdObject = new Friend(); + employee.Friends.Add(createdObject); + + return PatchStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return PatchStatus.Failure; + } + } + + public override PatchStatus 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 PatchStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return PatchStatus.Failure; + } + } + + public override PatchStatus TryGet(IDictionary keyValues, out Friend originalObject, out string errorMessage) + { + PatchStatus status = PatchStatus.Success; + errorMessage = string.Empty; + originalObject = null; + + try + { + var id = keyValues["Id"].ToString(); + if (employee.Friends == null) + { + status = PatchStatus.NotFound; + } + else + { + originalObject = employee.Friends.FirstOrDefault(x => x.Id == Int32.Parse(id)); + } + + + if (originalObject == null) + { + status = PatchStatus.NotFound; + } + + } + catch (Exception ex) + { + status = PatchStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override IPatchMethodHandler GetNestedPatchHandler(Friend parent, string navigationPropertyName) + { + return new OrderEFPatchHandler(parent); + } + + } + + + public class NewFriendEFPatchHandler : PatchMethodHandler + { + Employee employee; + public NewFriendEFPatchHandler(Employee employee) + { + this.employee = employee; + } + + public override PatchStatus TryCreate(Delta deltaObject, out NewFriend createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + createdObject = new NewFriend(); + employee.NewFriends.Add(createdObject); + + return PatchStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return PatchStatus.Failure; + } + } + + public override PatchStatus 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 PatchStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return PatchStatus.Failure; + } + } + + public override PatchStatus TryGet(IDictionary keyValues, out NewFriend originalObject, out string errorMessage) + { + PatchStatus status = PatchStatus.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 = PatchStatus.NotFound; + } + + } + catch (Exception ex) + { + status = PatchStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override IPatchMethodHandler GetNestedPatchHandler(NewFriend parent, string navigationPropertyName) + { + return null; + } + + } + + + + public class OrderEFPatchHandler : PatchMethodHandler + { + Friend friend; + public OrderEFPatchHandler(Friend friend) + { + this.friend = friend; + } + + public override PatchStatus TryCreate(Delta deltaObject, out Order createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + createdObject = new Order(); + friend.Orders.Add(createdObject); + + return PatchStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return PatchStatus.Failure; + } + } + + public override PatchStatus 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 PatchStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return PatchStatus.Failure; + } + } + + public override PatchStatus TryGet(IDictionary keyValues, out Order originalObject, out string errorMessage) + { + PatchStatus status = PatchStatus.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 = PatchStatus.NotFound; + } + + } + catch (Exception ex) + { + status = PatchStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override IPatchMethodHandler GetNestedPatchHandler(Order parent, string navigationPropertyName) + { + throw new NotImplementedException(); + } + + } +} 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 4c7fadb656..eeaa5c7dee 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 @@ -1665,6 +1665,18 @@ Validation\DeltaOfTValidationTests.cs + + BulkOperation\BulkInsertDataModel.cs + + + BulkOperation\BulkInsertEdmModel.cs + + + BulkOperation\BulkInsertController.cs + + + BulkOperation\BulkOperationPatchHandlers.cs + @@ -1678,4 +1690,5 @@ + diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertController.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertController.cs new file mode 100644 index 0000000000..f5d0050712 --- /dev/null +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertController.cs @@ -0,0 +1,373 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using Microsoft.AspNet.OData; +using Microsoft.AspNet.OData.Extensions; +using Microsoft.AspNet.OData.Routing; +using Microsoft.OData.Edm; +using Microsoft.Test.E2E.AspNet.OData.BulkOperation; +using Microsoft.Test.E2E.AspNet.OData.Common.Controllers; +using Xunit; + +namespace Microsoft.Test.E2E.AspNet.OData.BulkInsert +{ + 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" }, 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, + + 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, + + 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("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 NewFriendPatchHandler(employee)); + + return changedObjColl; + } + public EdmChangedObjectCollection PatchWithUsersMethodTypeLess(int key, EdmChangedObjectCollection friendColl) + { + + var entity = Request.GetModel().FindDeclaredType("Microsoft.Test.E2E.AspNet.OData.BulkInsert.UnTypedEmployee") as IEdmEntityType; + InitTypeLessEmployees(entity); + + var entity1 = Request.GetModel().FindDeclaredType("Microsoft.Test.E2E.AspNet.OData.BulkInsert.UnTypedFriend") as IEdmEntityType; + + var changedObjColl = friendColl.Patch(new FriendTypelessPatchHandler(EmployeesTypeless[key-1], entity1)); + + return changedObjColl; + } + + public EdmChangedObjectCollection EmployeePatchMethodTypeLess(EdmChangedObjectCollection empColl) + { + var entity = Request.GetModel().FindDeclaredType("Microsoft.Test.E2E.AspNet.OData.BulkInsert.UnTypedEmployee") as IEdmEntityType; + InitTypeLessEmployees(entity); + + var changedObjColl = empColl.Patch(new EmployeeEdmPatchHandler(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); + + Assert.Equal("Friend1", obj1.ToString()); + + } + + [EnableQuery(PageSize = 10, MaxExpansionDepth = 5)] + public ITestActionResult Get() + { + return Ok(Employees.AsQueryable()); + } + + public ITestActionResult Get(int key) + { + var emp = Employees.SingleOrDefault(e => e.ID == key); + return Ok(emp); + } + + [ODataRoute("Employees({key})/Friends")] + public ITestActionResult GetFriends(int key) + { + var emp = Employees.SingleOrDefault(e => e.ID == key); + return Ok(emp.Friends); + } + + [ODataRoute("Employees({key})/UnTypedFriends")] + public ITestActionResult GetUnTypedFriends(int key) + { + var entity = Request.GetModel().FindDeclaredType("Microsoft.Test.E2E.AspNet.OData.BulkInsert.UnTypedEmployee") as IEdmEntityType; + InitTypeLessEmployees(entity); + + foreach (var emp in EmployeesTypeless) + { + object obj; + emp.TryGetPropertyValue("ID", out obj); + + if(Equals(key, obj)) + { + object friends ; + emp.TryGetPropertyValue("UntypedFriends", out friends); + return Ok(friends); + } + } + return Ok(); + } + + + [ODataRoute("Employees")] + [HttpPatch] + public ITestActionResult PatchEmployees([FromBody] DeltaSet coll) + { + InitEmployees(); + + Assert.NotNull(coll); + + var returncoll = coll.Patch(new EmployeePatchHandler()); + + return Ok(returncoll); + } + + + [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.BulkInsert.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})")] + 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 EmployeePatchHandler()); + return Created(employee); + } + + try + { + delta.Patch(employee, new EmployeePatchHandler()); + } + catch (ArgumentException ae) + { + return BadRequest(ae.Message); + } + + return Ok(employee); + } + + + } +} \ No newline at end of file diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertDataModel.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertDataModel.cs new file mode 100644 index 0000000000..11d203faad --- /dev/null +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertDataModel.cs @@ -0,0 +1,178 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. 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.Builder; + +namespace Microsoft.Test.E2E.AspNet.OData.BulkInsert +{ + 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; } + + [AutoExpand] + public List Friends { get; set; } + + public List NewFriends { get; set; } + + public List UnTypedFriends { 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 Friend + { + [Key] + public int Id { get; set; } + [Required] + public string Name { get; set; } + [Range(10, 20)] + 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; } + + } + + 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; } + + [AutoExpand] + 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/BulkInsertEdmModel.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertEdmModel.cs new file mode 100644 index 0000000000..8903e1a5b9 --- /dev/null +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertEdmModel.cs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. 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.BulkInsert +{ + internal class BulkInsertEdmModel + { + 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 unemployees = builder.EntitySet("UnTypedEmployees"); + EntityTypeConfiguration unemployee = unemployees.EntityType; + + // 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; + + 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/BulkInsertTest.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertTest.cs new file mode 100644 index 0000000000..2fe121d7b4 --- /dev/null +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertTest.cs @@ -0,0 +1,782 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +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.OData; +using Microsoft.OData.Edm; +using Microsoft.Test.E2E.AspNet.OData.Common.Execution; +using Microsoft.Test.E2E.AspNet.OData.Common.Extensions; +using Microsoft.Test.E2E.AspNet.OData.ModelBuilder; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace Microsoft.Test.E2E.AspNet.OData.BulkInsert +{ + public class BulkInsertTest : WebHostTestBase + { + public BulkInsertTest(WebHostTestFixture fixture) + :base(fixture) + { + } + + protected override void UpdateConfiguration(WebRouteConfiguration configuration) + { + var controllers = new[] { typeof(EmployeesController), typeof(MetadataController) }; + configuration.AddControllers(controllers); + + configuration.Routes.Clear(); + configuration.Count().Filter().OrderBy().Expand().MaxTop(null).Select(); + configuration.MapODataServiceRoute("convention", "convention", BulkInsertEdmModel.GetConventionModel(configuration)); + configuration.MapODataServiceRoute("explicit", "explicit", BulkInsertEdmModel.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 + 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.BulkInsert.Friend', + '@odata.context':'http://host/service/$metadata#Employees(1)/Friends/$delta', + 'value':[{ 'Id':1,'Name':'Friend1'}, { 'Id':2,'Name':'Friend2'}] + }"; + + 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 + 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':'http://host/service/$metadata#Employees(1)/Friends/$delta', + 'value':[{ '@odata.removed' : {'reason':'changed'}, 'Id':1},{ 'Id':2,'Name':'Friend2'}] + }"; + + 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.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':'http://host/service/$metadata#Employees(1)/Friends/$delta', '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkInsert.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); + + 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.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':'http://host/service/$metadata#Employees(1)/Friends/$delta', '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkInsert.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); + + 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); + } + + + 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"; + //{ '@odata.removed' : {'reason':'changed'}, 'Id':1},{ '@odata.removed' : {'reason':'deleted'}, 'Id':2}, + var content = @"{'@odata.context':'http://host/service/$metadata#Employees(1)/NewFriends/$delta', + 'value':[{ 'Id':3, 'Age':35, '@NS.Test':1}] + }"; + + + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + + requestForPost.Content = new StringContent(content); + + requestForPost.Content.Headers.ContentType= MediaTypeWithQualityHeaderValue.Parse("application/json"); + // 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_WithFailedAdds_Friends() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Employees(1)/NewFriends"; + //{ '@odata.removed' : {'reason':'changed'}, 'Id':1},{ '@odata.removed' : {'reason':'deleted'}, 'Id':2}, + var content = @"{'@odata.context':'http://host/service/$metadata#Employees(1)/NewFriends/$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() + { + //Arrange + string requestUri = this.BaseAddress + "/convention/Employees(2)/NewFriends"; + //{ '@odata.removed' : {'reason':'changed'}, 'Id':1},{ '@odata.removed' : {'reason':'deleted'}, 'Id':2}, + var content = @"{'@odata.context':'http://host/service/$metadata#Employees(1)/NewFriends/$delta', + 'value':[{ '@odata.removed' : {'reason':'changed'}, 'Id':2, '@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("$delta", json); + } + + } + + + [Fact] + public async Task PatchEmployee_WithFailedOperation_WithAnnotations() + { + //Arrange + string requestUri = this.BaseAddress + "/convention/Employees(2)/NewFriends"; + //{ '@odata.removed' : {'reason':'changed'}, 'Id':1},{ '@odata.removed' : {'reason':'deleted'}, 'Id':2}, + var content = @"{'@odata.context':'http://host/service/$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); + + 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); + } + + } + + + [Fact] + public async Task PatchUntypedEmployee_WithAdds_Friends_Untyped() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/UnTypedEmployees"; + //{ '@odata.removed' : {'reason':'changed'}, 'Id':1},{ '@odata.removed' : {'reason':'deleted'}, 'Id':2}, + var content = @"{'@odata.context':'http://host/service/$metadata#Employees(2)/UnTypedFriends/$delta', + 'value':[{ 'Id':3, 'Age':35,}] + }"; + + content = @"{'@odata.context':'http://host/service/$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); + + 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_WithAdds_Friends_WithNested_Untyped() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Employees(1)/UnTypedFriends"; + //{ '@odata.removed' : {'reason':'changed'}, 'Id':1},{ '@odata.removed' : {'reason':'deleted'}, 'Id':2}, + var content = @"{'@odata.context':'http://host/service/$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"; + //{ '@odata.removed' : {'reason':'changed'}, 'Id':1},{ '@odata.removed' : {'reason':'deleted'}, 'Id':2}, + var content = @"{'@odata.context':'http://host/service/$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"; + //{ '@odata.removed' : {'reason':'changed'}, 'Id':1},{ '@odata.removed' : {'reason':'deleted'}, 'Id':2}, + var content = @"{'@odata.context':'http://host/service/$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':'http://host/service/$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"; + //{ '@odata.removed' : {'reason':'changed'}, 'Id':1},{ '@odata.removed' : {'reason':'deleted'}, 'Id':2}, + var content = @"{'@odata.context':'http://host/service/$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_WithUpdates_Employees() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Employees"; + + var content = @"{'@odata.context':'http://host/service/$metadata#Employees/$delta', + 'value':[{ '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkInsert.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.BulkInsert.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); + + 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 = response.Content.ReadAsStringAsync().Result; + + Assert.Contains("Friend1", json.ToString()); + Assert.Contains("Friend2", json.ToString()); + } + + requestUri = this.BaseAddress + "/convention/Employees(2)/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; + + 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.Single(result); + Assert.DoesNotContain("Test0", result.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()); + } + } + + + #endregion + + } +} \ 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..e2bbfceea5 --- /dev/null +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationPatchHandlers.cs @@ -0,0 +1,632 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. 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.OData.Edm; +using Microsoft.Test.E2E.AspNet.OData.BulkInsert; + +namespace Microsoft.Test.E2E.AspNet.OData.BulkOperation +{ + public class EmployeePatchHandler : PatchMethodHandler + { + public override PatchStatus TryCreate(Delta patchObject, out Employee createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + createdObject = new Employee(); + EmployeesController.Employees.Add(createdObject); + + return PatchStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return PatchStatus.Failure; + } + } + + public override PatchStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + var id = keyValues.First().Value.ToString(); + var customer = EmployeesController.Employees.First(x => x.ID == Int32.Parse(id)); + + EmployeesController.Employees.Remove(customer); + + return PatchStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return PatchStatus.Failure; + } + } + + public override PatchStatus TryGet(IDictionary keyValues, out Employee originalObject, out string errorMessage) + { + PatchStatus status = PatchStatus.Success; + errorMessage = string.Empty; + originalObject = null; + + try + { + var id = keyValues["ID"].ToString(); + originalObject = EmployeesController.Employees.First(x => x.ID == Int32.Parse(id)); + + + if (originalObject == null) + { + status = PatchStatus.NotFound; + } + + } + catch (Exception ex) + { + status = PatchStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override IPatchMethodHandler GetNestedPatchHandler(Employee parent, string navigationPropertyName) + { + switch (navigationPropertyName) + { + case "Friends": + return new FriendPatchHandler(parent); + case "NewFriends": + return new NewFriendPatchHandler(parent); + default: + return null; + } + + } + } + + public class FriendPatchHandler : PatchMethodHandler + { + Employee employee; + public FriendPatchHandler(Employee employee) + { + this.employee = employee; + } + + public override PatchStatus TryCreate(Delta patchObject, out Friend createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + createdObject = new Friend(); + employee.Friends.Add(createdObject); + + return PatchStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return PatchStatus.Failure; + } + } + + public override PatchStatus 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 PatchStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return PatchStatus.Failure; + } + } + + public override PatchStatus TryGet(IDictionary keyValues, out Friend originalObject, out string errorMessage) + { + PatchStatus status = PatchStatus.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 = PatchStatus.NotFound; + } + + } + catch (Exception ex) + { + status = PatchStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override IPatchMethodHandler GetNestedPatchHandler(Friend parent, string navigationPropertyName) + { + switch (navigationPropertyName) + { + case "Orders": + return new OrderPatchHandler(parent); + default: + return null; + + } + } + + } + + + public class OrderPatchHandler : PatchMethodHandler + { + Friend friend; + public OrderPatchHandler(Friend friend) + { + this.friend = friend; + } + + public override PatchStatus TryCreate(Delta patchObject, 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 PatchStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return PatchStatus.Failure; + } + } + + public override PatchStatus 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 PatchStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return PatchStatus.Failure; + } + } + + public override PatchStatus TryGet(IDictionary keyValues, out Order originalObject, out string errorMessage) + { + PatchStatus status = PatchStatus.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 = PatchStatus.NotFound; + } + + } + catch (Exception ex) + { + status = PatchStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override IPatchMethodHandler GetNestedPatchHandler(Order parent, string navigationPropertyName) + { + throw new NotImplementedException(); + } + + } + + + + public class NewFriendPatchHandler : PatchMethodHandler + { + Employee employee; + public NewFriendPatchHandler(Employee employee) + { + this.employee = employee; + } + + public override PatchStatus TryCreate(Delta patchObject, 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 PatchStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return PatchStatus.Failure; + } + } + + public override PatchStatus 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 PatchStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return PatchStatus.Failure; + } + } + + public override PatchStatus TryGet(IDictionary keyValues, out NewFriend originalObject, out string errorMessage) + { + PatchStatus status = PatchStatus.Success; + errorMessage = string.Empty; + originalObject = null; + + try + { + var id = keyValues["Id"].ToString(); + + if(employee.NewFriends == null) + { + return PatchStatus.NotFound; + } + + originalObject = employee.NewFriends.FirstOrDefault(x => x.Id == Int32.Parse(id)); + + + if (originalObject == null) + { + status = PatchStatus.NotFound; + } + + } + catch (Exception ex) + { + status = PatchStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override IPatchMethodHandler GetNestedPatchHandler(NewFriend parent, string navigationPropertyName) + { + throw new NotImplementedException(); + } + + } + + + public class EmployeeEdmPatchHandler : EdmPatchMethodHandler + { + IEdmEntityType entityType; + public EmployeeEdmPatchHandler(IEdmEntityType entityType) + { + this.entityType = entityType; + } + + public override PatchStatus 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 PatchStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return PatchStatus.Failure; + } + } + + public override PatchStatus 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 PatchStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return PatchStatus.Failure; + } + } + + public override PatchStatus TryGet(IDictionary keyValues, out IEdmStructuredObject originalObject, out string errorMessage) + { + PatchStatus status = PatchStatus.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 = PatchStatus.NotFound; + } + + } + catch (Exception ex) + { + status = PatchStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override EdmPatchMethodHandler GetNestedPatchHandler(IEdmStructuredObject parent, string navigationPropertyName) + { + switch (navigationPropertyName) + { + case "UnTypedFriends": + return new FriendTypelessPatchHandler(parent, entityType.DeclaredNavigationProperties().First().Type.Definition.AsElementType() as IEdmEntityType); + + default: + return null; + } + + } + + } + + public class FriendTypelessPatchHandler : EdmPatchMethodHandler + { + IEdmEntityType entityType; + EdmStructuredObject employee; + + public FriendTypelessPatchHandler(IEdmStructuredObject employee, IEdmEntityType entityType) + { + this.employee = employee as EdmStructuredObject; + this.entityType = entityType; + } + + public override PatchStatus 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 PatchStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return PatchStatus.Failure; + } + } + + public override PatchStatus 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 PatchStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return PatchStatus.Failure; + } + } + + public override PatchStatus TryGet(IDictionary keyValues, out IEdmStructuredObject originalObject, out string errorMessage) + { + PatchStatus status = PatchStatus.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 PatchStatus.NotFound; + } + + foreach (var friend in friends) + { + object id1; + friend.TryGetPropertyValue("Id", out id1); + + if (id == id1.ToString()) + { + originalObject = friend; + break; + } + } + + + if (originalObject == null) + { + status = PatchStatus.NotFound; + } + + } + catch (Exception ex) + { + status = PatchStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override EdmPatchMethodHandler GetNestedPatchHandler(IEdmStructuredObject parent, string navigationPropertyName) + { + return null; + } + + } +} 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 28cf968508..5179c741d2 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 @@ -64,7 +64,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(); @@ -156,7 +156,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/ComplexTypeInheritanceTests.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/ComplexTypeInheritance/ComplexTypeInheritanceTests.cs index 03796b045a..e8635ebf7a 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..86374db467 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,12 @@ public class DLEmployee public DLEmployee Friend { get; set; } } + + + public class TestQueryOptions + { + [Key] + public string Id { get; set; } + public string name { get; set; } + } } diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/DollarLevels/DollarLevelsEdmModel.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/DollarLevels/DollarLevelsEdmModel.cs index c380647162..4c9a3bc0f3 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/DollarLevels/DollarLevelsEdmModel.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/DollarLevels/DollarLevelsEdmModel.cs @@ -18,6 +18,7 @@ public static IEdmModel GetConventionModel(WebRouteConfiguration configuration) ODataConventionModelBuilder builder = configuration.CreateConventionModelBuilder(); builder.EntitySet("DLManagers"); builder.EntitySet("DLEmployees"); + builder.EntitySet("Tests"); builder.Namespace = typeof(DLManager).Namespace; return builder.GetEdmModel(); 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 c2199a7b7c..79a2d91718 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,11 +424,11 @@ 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); } @@ -434,6 +453,7 @@ public async Task PatchShouldSupportComplexDerivedTypeTransform() dynamic query = await response.Content.ReadAsObject(); Assert.Equal("abc", query.MyAddress.Street.ToString()); } + } public class DeltaCustomersController : TestODataController @@ -480,6 +500,7 @@ public ITestActionResult Put([FromODataUri] int key, [FromBody] Delta c.Id == key).FirstOrDefault(); entity.Put(customer); return Ok(customer); + } [AcceptVerbs("PATCH", "MERGE")] @@ -509,6 +530,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/UnitTest/Microsoft.AspNet.OData.Test.Shared/DeltaSetOfTTest.cs b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/DeltaSetOfTTest.cs new file mode 100644 index 0000000000..e9b38d70af --- /dev/null +++ b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/DeltaSetOfTTest.cs @@ -0,0 +1,309 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. 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 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"); + + 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" }); + + + var edmChangedObj1 = new Delta(); + edmChangedObj1.TrySetPropertyValue("Id", 1); + edmChangedObj1.TrySetPropertyValue("Name", "Friend1"); + + var edmChangedObj2 = new DeltaDeletedEntityObject(); + edmChangedObj2.TrySetPropertyValue("Id", 2); + + + 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()); + + //Assert + Assert.Single(friends); + Assert.Equal("Friend1", friends[0].Name); + } + + [Fact] + public void DeltaSet_Patch_WithInstanceAnnotations() + { + //Arrange + + var deltaSet = new DeltaSet((new List() { "Id" })); + + + 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); + + 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()).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 + + var lstId = new List(); + lstId.Add("Id"); + + var deltaSet = new DeltaSet(lstId); + + var deltaSet1 = new DeltaSet(lstId); + + var edmNewObj1 = new Delta(); + edmNewObj1.TrySetPropertyValue("Id", 1); + edmNewObj1.TrySetPropertyValue("Name", "NewFriend1"); + + var edmNewObj2 = new Delta(); + edmNewObj2.TrySetPropertyValue("Id", 2); + edmNewObj2.TrySetPropertyValue("Name", "NewFriend2"); + + 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"); + + deltaSet2.Add(edmNewObj21); + deltaSet2.Add(edmNewObj22); + + 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); + + 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()); + + //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); + } + + } + + public class FriendPatchHandler : PatchMethodHandler + { + public override IPatchMethodHandler GetNestedPatchHandler(Friend parent, string navigationPropertyName) + { + return new NewFriendPatchHandler(parent); + } + + public override PatchStatus TryCreate(Delta deltaFriend, out Friend createdObject, out string errorMessage) + { + createdObject = new Friend(); + DeltaSetOfTTest.friends.Add(createdObject); + errorMessage = string.Empty; + return PatchStatus.Success; + } + + public override PatchStatus 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 PatchStatus.Success; + } + + public override PatchStatus 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 PatchStatus.Success; + } + } + + public class NewFriendPatchHandler : PatchMethodHandler + { + Friend parent; + public NewFriendPatchHandler(Friend parent) + { + this.parent = parent; + } + + public override IPatchMethodHandler GetNestedPatchHandler(NewFriend parent, string navigationPropertyName) + { + throw new NotImplementedException(); + } + + public override PatchStatus TryCreate(Delta deltaFriend, 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 PatchStatus.Success; + } + + public override PatchStatus 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 PatchStatus.Success; + } + + public override PatchStatus TryGet(IDictionary keyValues, out NewFriend originalObject, out string errorMessage) + { + errorMessage = string.Empty; + originalObject = null; + + if(parent.NewFriends == null) + { + return PatchStatus.NotFound; + } + + int id = Int32.Parse(keyValues.First().Value.ToString()); + originalObject = parent.NewFriends.FirstOrDefault(x => x.Id == id); + errorMessage = string.Empty; + + return originalObject!=null? PatchStatus.Success : PatchStatus.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..175ccb037c 100644 --- a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/EdmChangedObjectCollectionTest.cs +++ b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/EdmChangedObjectCollectionTest.cs @@ -5,6 +5,11 @@ // //------------------------------------------------------------------------------ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Microsoft.AspNet.OData.Builder; using Microsoft.AspNet.OData.Test.Common; using Microsoft.OData.Edm; using Moq; @@ -36,5 +41,417 @@ 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"); + + deltaSet.Add(edmChangedObj1); + deltaSet.Add(edmChangedObj2); + + //Act + deltaSet.Patch(new FriendTypelessPatchHandler(_entityType)); + + //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"); + + changedObjCollection.Add(edmChangedObj1); + changedObjCollection.Add(edmChangedObj2); + + //Act + changedObjCollection.Patch(new FriendTypelessPatchHandler(_entityType)); + + //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"); + + changedObjCollection.Add(edmChangedObj1); + changedObjCollection.Add(edmChangedObj2); + + //Act + var coll= changedObjCollection.Patch(new FriendTypelessPatchHandler(_entityType)); + + //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; } + } + + public class FriendTypelessPatchHandler : EdmPatchMethodHandler + { + IEdmEntityType entityType; + + public FriendTypelessPatchHandler(IEdmEntityType entityType) + { + this.entityType = entityType; + } + + public override PatchStatus TryCreate(IEdmChangedObject changedObject, out IEdmStructuredObject createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + createdObject = new EdmEntityObject(entityType); + + EdmChangedObjectCollectionTest.friends.Add(createdObject); + + return PatchStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return PatchStatus.Failure; + } + } + + public override PatchStatus 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 PatchStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return PatchStatus.Failure; + } + } + + public override PatchStatus TryGet(IDictionary keyValues, out IEdmStructuredObject originalObject, out string errorMessage) + { + PatchStatus status = PatchStatus.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 = PatchStatus.NotFound; + } + + } + catch (Exception ex) + { + status = PatchStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override EdmPatchMethodHandler GetNestedPatchHandler(IEdmStructuredObject parent, string navigationPropertyName) + { + switch (navigationPropertyName) + { + case "NewFriends": + return new NewFriendTypelessPatchHandler(parent, entityType.DeclaredNavigationProperties().First().Type.Definition.AsElementType() as IEdmEntityType); + default: + return null; + } + } + + } + + public class NewFriendTypelessPatchHandler : EdmPatchMethodHandler + { + IEdmEntityType entityType; + EdmStructuredObject friend; + + public NewFriendTypelessPatchHandler(IEdmStructuredObject friend, IEdmEntityType entityType) + { + this.entityType = entityType; + this.friend = friend as EdmStructuredObject; + } + + public override PatchStatus 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 PatchStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return PatchStatus.Failure; + } + } + + public override PatchStatus 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 PatchStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return PatchStatus.Failure; + } + } + + public override PatchStatus TryGet(IDictionary keyValues, out IEdmStructuredObject originalObject, out string errorMessage) + { + PatchStatus status = PatchStatus.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 = PatchStatus.NotFound; + } + + } + catch (Exception ex) + { + status = PatchStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override EdmPatchMethodHandler GetNestedPatchHandler(IEdmStructuredObject parent, string navigationPropertyName) + { + return null; + } + + } + } 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..e741e2f92a 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 @@ -1037,7 +1037,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 +1159,7 @@ public void ApplyNestedProperty_ThrowsArgumentNull_EntityResource() "resource"); } + [Fact] public void ApplyNestedProperty_ThrowsODataException_NavigationPropertyNotfound() { @@ -1172,35 +1173,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() 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/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 95e565927c..41db702ac0 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 @@ -147,6 +147,7 @@ + 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/Microsoft.AspNet.OData.Test.csproj b/test/UnitTest/Microsoft.AspNet.OData.Test/Microsoft.AspNet.OData.Test.csproj index ab259554dd..d77fd275b8 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 @@ -1,5 +1,5 @@  - + @@ -53,7 +53,6 @@ - @@ -155,5 +154,5 @@ - + \ No newline at end of file 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 8938404558..c216312381 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 @@ -15,6 +15,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 () @@ -24,6 +30,21 @@ 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; } + ODataIdContainer ODataIdContainer { 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; } } @@ -62,6 +83,9 @@ 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.IPerRouteContainer { System.Func`1[[Microsoft.OData.IContainerBuilder]] BuilderFactory { public abstract get; public abstract set; } @@ -88,6 +112,15 @@ public abstract class Microsoft.AspNet.OData.Delta : System.Dynamic.DynamicObjec public abstract bool TrySetPropertyValue (string name, object value) } +public abstract class Microsoft.AspNet.OData.EdmODataAPIHandler { + protected EdmODataAPIHandler () + + public abstract EdmODataAPIHandler GetNestedHandler (IEdmStructuredObject parent, string navigationPropertyName) + public abstract ODataAPIResponseStatus TryCreate (IEdmChangedObject changedObject, out IEdmStructuredObject& 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 IEdmStructuredObject& originalObject, out System.String& errorMessage) +} + [ NonValidatingParameterBindingAttribute(), ] @@ -110,6 +143,21 @@ 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 abstract class Microsoft.AspNet.OData.ODataAPIHandlerFactory { + protected ODataAPIHandlerFactory () + + public abstract IODataAPIHandler GetHandler (NavigationPath navigationPath) +} + [ ODataFormattingAttribute(), ODataRoutingAttribute(), @@ -123,6 +171,12 @@ public abstract class Microsoft.AspNet.OData.ODataController : System.Web.Http.A protected virtual UpdatedODataResult`1 Updated (TEntity entity) } +public abstract class Microsoft.AspNet.OData.ODataEdmAPIHandlerFactory { + protected ODataEdmAPIHandlerFactory () + + public abstract EdmODataAPIHandler GetHandler (NavigationPath navigationPath) +} + [ DataContractAttribute(), ] @@ -250,18 +304,29 @@ 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, System.Reflection.PropertyInfo instanceAnnotationsPropertyInfo) + 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; } + + ODataIdContainer ODataIdContainer { public virtual get; public virtual set; } + System.Type StructuredType { public virtual get; } + System.Collections.Generic.IList`1[[System.String]] UpdatableProperties { public get; } + IODataInstanceAnnotationContainer TransientInstanceAnnotationContainer { public virtual get; public virtual set; } + public virtual void Clear () public void CopyChangedValues (TStructuralType original) public void CopyUnchangedValues (TStructuralType original) @@ -269,12 +334,40 @@ public class Microsoft.AspNet.OData.Delta`1 : TypedDelta, IDynamicMetaObjectProv public TStructuralType GetInstance () public virtual System.Collections.Generic.IEnumerable`1[[System.String]] GetUnchangedPropertyNames () public void Patch (TStructuralType original) + public void Patch (TStructuralType original, params ODataAPIHandlerFactory apiHandlerFactory) public void Put (TStructuralType original) public virtual bool TryGetPropertyType (string name, out System.Type& type) public virtual bool TryGetPropertyValue (string name, out System.Object& value) 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) + + 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 (params ODataAPIHandlerFactory apiHandlerFactory) +} + [ NonValidatingParameterBindingAttribute(), ] @@ -282,7 +375,10 @@ 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 () + public EdmChangedObjectCollection Patch (EdmODataAPIHandler apiHandler, params ODataEdmAPIHandlerFactory apiHandlerFactory) } [ @@ -370,10 +466,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; } + ODataIdContainer 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 () } [ @@ -456,6 +559,13 @@ public class Microsoft.AspNet.OData.HttpRequestScope { System.Net.Http.HttpRequestMessage HttpRequest { public get; public set; } } +public class Microsoft.AspNet.OData.KeyProperty { + public KeyProperty () + + string Name { public get; } + object Value { public get; } +} + public class Microsoft.AspNet.OData.MetadataController : ODataController, IDisposable, IHttpController { public MetadataController () @@ -463,6 +573,14 @@ public class Microsoft.AspNet.OData.MetadataController : ODataController, IDispo public Microsoft.OData.ODataServiceDocument GetServiceDocument () } +public class Microsoft.AspNet.OData.NavigationPath { + public NavigationPath (string navigationPath, System.Collections.ObjectModel.ReadOnlyCollection`1[[Microsoft.OData.UriParser.ODataPathSegment]] pathSegments) + + string NavigationPathName { public get; } + + public PathItem[] GetNavigationPathItems () +} + public class Microsoft.AspNet.OData.NullEdmComplexObject : IEdmComplexObject, IEdmObject, IEdmStructuredObject { public NullEdmComplexObject (Microsoft.OData.Edm.IEdmComplexTypeReference edmType) @@ -555,6 +673,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 (System.Web.Http.HttpConfiguration configuration) @@ -643,6 +770,12 @@ public sealed class Microsoft.AspNet.OData.FromODataUriAttribute : System.Web.Ht public virtual System.Web.Http.Controllers.HttpParameterBinding GetBinding (System.Web.Http.Controllers.HttpParameterDescriptor parameter) } +public sealed class Microsoft.AspNet.OData.ODataIdContainer { + public ODataIdContainer () + + NavigationPath ODataIdNavigationPath { public get; public set; } +} + [ AttributeUsageAttribute(), ] @@ -3138,6 +3271,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) @@ -3172,6 +3338,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(), ] @@ -3301,7 +3474,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 { @@ -3311,18 +3488,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 { @@ -3483,8 +3664,9 @@ 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.ODataDeletedResource CreateDeletedResource (SelectExpandNode selectExpandNode, ResourceContext resourceContext) public virtual string CreateETag (ResourceContext resourceContext) public virtual Microsoft.OData.ODataNestedResourceInfo CreateNavigationLink (Microsoft.OData.Edm.IEdmNavigationProperty navigationProperty, ResourceContext resourceContext) public virtual Microsoft.OData.ODataAction CreateODataAction (Microsoft.OData.Edm.IEdmAction action, ResourceContext resourceContext) diff --git a/test/UnitTest/Microsoft.AspNet.OData.Test/app.config b/test/UnitTest/Microsoft.AspNet.OData.Test/app.config index 58c01219df..75b71f7c94 100644 --- a/test/UnitTest/Microsoft.AspNet.OData.Test/app.config +++ b/test/UnitTest/Microsoft.AspNet.OData.Test/app.config @@ -1,7 +1,18 @@  + + +
+ - - + + + + + + + \ No newline at end of file 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 20585573e8..21081d868b 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 @@ -59,5 +59,5 @@ - + 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 5a6dd5e227..8a1e0677e3 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 @@ -15,6 +15,12 @@ public enum Microsoft.AspNet.OData.EdmDeltaEntityKind : int { Unknown = 4 } +public enum Microsoft.AspNet.OData.PatchStatus : int { + Failure = 1 + NotFound = 2 + Success = 0 +} + public interface Microsoft.AspNet.OData.IDelta { void Clear () System.Collections.Generic.IEnumerable`1[[System.String]] GetChangedPropertyNames () @@ -24,6 +30,20 @@ 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; } + IODataInstanceAnnotationContainer TransientInstanceAnnotationContainer { public abstract get; public abstract set; } +} + public interface Microsoft.AspNet.OData.IEdmChangedObject : IEdmObject, IEdmStructuredObject { EdmDeltaEntityKind DeltaKind { public abstract get; } } @@ -62,6 +82,9 @@ public interface Microsoft.AspNet.OData.IEdmStructuredObject : IEdmObject { bool TryGetPropertyValue (string propertyName, out System.Object& value) } +public interface Microsoft.AspNet.OData.IPatchMethodHandler { +} + public interface Microsoft.AspNet.OData.IPerRouteContainer { System.Func`1[[Microsoft.OData.IContainerBuilder]] BuilderFactory { public abstract get; public abstract set; } @@ -88,6 +111,15 @@ public abstract class Microsoft.AspNet.OData.Delta : System.Dynamic.DynamicObjec public abstract bool TrySetPropertyValue (string name, object value) } +public abstract class Microsoft.AspNet.OData.EdmPatchMethodHandler { + protected EdmPatchMethodHandler () + + public abstract EdmPatchMethodHandler GetNestedPatchHandler (IEdmStructuredObject parent, string navigationPropertyName) + public abstract PatchStatus TryCreate (IEdmChangedObject changedObject, out IEdmStructuredObject& createdObject, out System.String& errorMessage) + public abstract PatchStatus TryDelete (System.Collections.Generic.IDictionary`2[[System.String],[System.Object]] keyValues, out System.String& errorMessage) + public abstract PatchStatus TryGet (System.Collections.Generic.IDictionary`2[[System.String],[System.Object]] keyValues, out IEdmStructuredObject& originalObject, out System.String& errorMessage) +} + [ NonValidatingParameterBindingAttribute(), ] @@ -147,6 +179,15 @@ public abstract class Microsoft.AspNet.OData.PageResult { System.Uri NextPageLink { public get; } } +public abstract class Microsoft.AspNet.OData.PatchMethodHandler`1 : IPatchMethodHandler { + protected PatchMethodHandler`1 () + + public abstract IPatchMethodHandler GetNestedPatchHandler (TStructuralType parent, string navigationPropertyName) + public abstract PatchStatus TryCreate (Delta`1 patchObject, out TStructuralType& createdObject, out System.String& errorMessage) + public abstract PatchStatus TryDelete (System.Collections.Generic.IDictionary`2[[System.String],[System.Object]] keyValues, out System.String& errorMessage) + public abstract PatchStatus TryGet (System.Collections.Generic.IDictionary`2[[System.String],[System.Object]] keyValues, out TStructuralType& originalObject, out System.String& errorMessage) +} + public abstract class Microsoft.AspNet.OData.PerRouteContainerBase : IPerRouteContainer { protected PerRouteContainerBase () @@ -265,18 +306,28 @@ 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, System.Reflection.PropertyInfo instanceAnnotationsPropertyInfo) + 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; } + + ODataIdContainer ODataIdContainer { public virtual get; public virtual set; } + System.Type StructuredType { public virtual get; } + System.Collections.Generic.IList`1[[System.String]] UpdatableProperties { public get; } + IODataInstanceAnnotationContainer TransientInstanceAnnotationContainer { public virtual get; public virtual set; } + public virtual void Clear () public void CopyChangedValues (TStructuralType original) public void CopyUnchangedValues (TStructuralType original) @@ -284,12 +335,40 @@ public class Microsoft.AspNet.OData.Delta`1 : TypedDelta, IDynamicMetaObjectProv public TStructuralType GetInstance () public virtual System.Collections.Generic.IEnumerable`1[[System.String]] GetUnchangedPropertyNames () public void Patch (TStructuralType original) + public void Patch (TStructuralType original, IPatchMethodHandler patchHandler) public void Put (TStructuralType original) public virtual bool TryGetPropertyType (string name, out System.Type& type) public virtual bool TryGetPropertyValue (string name, out System.Object& value) 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) + + 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 (IPatchMethodHandler patchHandler) +} + [ NonValidatingParameterBindingAttribute(), ] @@ -297,7 +376,10 @@ 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 () + public EdmChangedObjectCollection Patch (EdmPatchMethodHandler patchHandler) } [ @@ -385,10 +467,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; } + IODataInstanceAnnotationContainer PersistentInstanceAnnotationsContainer { public get; public set; } + + public void AddDataException (Org.OData.Core.V1.DataModificationExceptionType dataModificationException) + public Org.OData.Core.V1.DataModificationExceptionType GetDataException () } [ @@ -3352,6 +3440,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) @@ -3386,6 +3507,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(), ] @@ -3514,7 +3642,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 { @@ -3524,18 +3656,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 { @@ -3696,8 +3832,9 @@ 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.ODataDeletedResource CreateDeletedResource (SelectExpandNode selectExpandNode, ResourceContext resourceContext) public virtual string CreateETag (ResourceContext resourceContext) public virtual Microsoft.OData.ODataNestedResourceInfo CreateNavigationLink (Microsoft.OData.Edm.IEdmNavigationProperty navigationProperty, ResourceContext resourceContext) public virtual Microsoft.OData.ODataAction CreateODataAction (Microsoft.OData.Edm.IEdmAction action, ResourceContext resourceContext) @@ -3763,6 +3900,10 @@ public class Microsoft.AspNet.OData.Formatter.Serialization.ODataSerializerConte Microsoft.AspNetCore.Mvc.IUrlHelper Url { public get; public set; } } +public class Microsoft.AspNet.OData.Formatter.Serialization.ODataSerializerHelper { + public ODataSerializerHelper () +} + public class Microsoft.AspNet.OData.Formatter.Serialization.ODataServiceDocumentSerializer : ODataSerializer { public ODataServiceDocumentSerializer () 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 2f8970d071..b0a0b1f4f8 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 @@ -15,6 +15,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 () @@ -24,6 +30,21 @@ 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; } + ODataIdContainer ODataIdContainer { 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; } } @@ -62,6 +83,9 @@ 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.IPerRouteContainer { System.Func`1[[Microsoft.OData.IContainerBuilder]] BuilderFactory { public abstract get; public abstract set; } @@ -88,6 +112,15 @@ public abstract class Microsoft.AspNet.OData.Delta : System.Dynamic.DynamicObjec public abstract bool TrySetPropertyValue (string name, object value) } +public abstract class Microsoft.AspNet.OData.EdmODataAPIHandler { + protected EdmODataAPIHandler () + + public abstract EdmODataAPIHandler GetNestedHandler (IEdmStructuredObject parent, string navigationPropertyName) + public abstract ODataAPIResponseStatus TryCreate (IEdmChangedObject changedObject, out IEdmStructuredObject& 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 IEdmStructuredObject& originalObject, out System.String& errorMessage) +} + [ NonValidatingParameterBindingAttribute(), ] @@ -110,6 +143,21 @@ 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 abstract class Microsoft.AspNet.OData.ODataAPIHandlerFactory { + protected ODataAPIHandlerFactory () + + public abstract IODataAPIHandler GetHandler (NavigationPath navigationPath) +} + [ ODataFormattingAttribute(), ODataRoutingAttribute(), @@ -134,6 +182,12 @@ public abstract class Microsoft.AspNet.OData.ODataController : Microsoft.AspNetC protected virtual UpdatedODataResult`1 Updated (TEntity entity) } +public abstract class Microsoft.AspNet.OData.ODataEdmAPIHandlerFactory { + protected ODataEdmAPIHandlerFactory () + + public abstract EdmODataAPIHandler GetHandler (NavigationPath navigationPath) +} + [ DataContractAttribute(), ] @@ -269,18 +323,28 @@ 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, System.Reflection.PropertyInfo instanceAnnotationsPropertyInfo) + 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; } + + ODataIdContainer ODataIdContainer { public virtual get; public virtual set; } + System.Type StructuredType { public virtual get; } + System.Collections.Generic.IList`1[[System.String]] UpdatableProperties { public get; } + IODataInstanceAnnotationContainer TransientInstanceAnnotationContainer { public virtual get; public virtual set; } + public virtual void Clear () public void CopyChangedValues (TStructuralType original) public void CopyUnchangedValues (TStructuralType original) @@ -288,12 +352,40 @@ public class Microsoft.AspNet.OData.Delta`1 : TypedDelta, IDynamicMetaObjectProv public TStructuralType GetInstance () public virtual System.Collections.Generic.IEnumerable`1[[System.String]] GetUnchangedPropertyNames () public void Patch (TStructuralType original) + public void Patch (TStructuralType original, ODataAPIHandlerFactory apiHandlerFactory) public void Put (TStructuralType original) public virtual bool TryGetPropertyType (string name, out System.Type& type) public virtual bool TryGetPropertyValue (string name, out System.Object& value) 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) + + 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 (ODataAPIHandlerFactory apiHandlerFactory) +} + [ NonValidatingParameterBindingAttribute(), ] @@ -301,7 +393,10 @@ 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 () + public EdmChangedObjectCollection Patch (ODataEdmAPIHandlerFactory apiHandlerFactory) } [ @@ -389,10 +484,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; } + ODataIdContainer 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 () } [ @@ -484,6 +586,15 @@ public class Microsoft.AspNet.OData.MetadataController : ODataController { public Microsoft.OData.ODataServiceDocument GetServiceDocument () } +public class Microsoft.AspNet.OData.NavigationPath { + public NavigationPath () + public NavigationPath (string navigationPath, System.Collections.ObjectModel.ReadOnlyCollection`1[[Microsoft.OData.UriParser.ODataPathSegment]] pathSegments) + + string NavigationPathName { public get; } + + public PathItem[] GetNavigationPathItems () +} + public class Microsoft.AspNet.OData.NullEdmComplexObject : IEdmComplexObject, IEdmObject, IEdmStructuredObject { public NullEdmComplexObject (Microsoft.OData.Edm.IEdmComplexTypeReference edmType) @@ -619,6 +730,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 () @@ -712,6 +832,12 @@ public sealed class Microsoft.AspNet.OData.FromODataUriAttribute : Microsoft.Asp public FromODataUriAttribute () } +public sealed class Microsoft.AspNet.OData.ODataIdContainer { + public ODataIdContainer () + + NavigationPath ODataIdNavigationPath { public get; public set; } +} + [ AttributeUsageAttribute(), ] @@ -3551,6 +3677,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) @@ -3585,6 +3744,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(), ] @@ -3713,7 +3879,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 { @@ -3723,18 +3893,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 { @@ -3895,8 +4069,9 @@ 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.ODataDeletedResource CreateDeletedResource (SelectExpandNode selectExpandNode, ResourceContext resourceContext) public virtual string CreateETag (ResourceContext resourceContext) public virtual Microsoft.OData.ODataNestedResourceInfo CreateNavigationLink (Microsoft.OData.Edm.IEdmNavigationProperty navigationProperty, ResourceContext resourceContext) public virtual Microsoft.OData.ODataAction CreateODataAction (Microsoft.OData.Edm.IEdmAction action, ResourceContext resourceContext) From 7c3f4049f8bfe94740956ec59fafd8effc87083c Mon Sep 17 00:00:00 2001 From: Elizabeth Okerio Date: Fri, 11 Jun 2021 15:09:25 +0300 Subject: [PATCH 02/28] Feature/new add support for odata bind (#2506) * support for odata.bind * support for odata.bind --- .../ODataEntityReferenceLinkBase.cs | 10 +- .../ODataNestedResourceInfoWrapper.cs | 1 + .../ODataResourceDeserializer.cs | 185 +++++++++++- .../ODataEntityReferenceLinkE2ETests.cs | 276 ++++++++++++++++++ .../ODataEntityReferenceLinkTests.cs | 181 ++++++++++++ ...crosoft.AspNet.OData.Test.Shared.projitems | 1 + 6 files changed, 634 insertions(+), 20 deletions(-) create mode 100644 test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNet/Formatter/ODataEntityReferenceLinkE2ETests.cs create mode 100644 test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Formatter/ODataEntityReferenceLinkTests.cs 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/ODataResourceDeserializer.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializer.cs index e4826282d5..34883c7b1d 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializer.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializer.cs @@ -15,9 +15,12 @@ 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.AspNet.OData.Routing; using Microsoft.OData; using Microsoft.OData.Edm; +using Microsoft.OData.UriParser; +using ODataPath = Microsoft.AspNet.OData.Routing.ODataPath; namespace Microsoft.AspNet.OData.Formatter.Deserialization { @@ -101,6 +104,7 @@ public sealed override object ReadInline(object item, IEdmTypeReference edmType, // Recursion guard to avoid stack overflows RuntimeHelpers.EnsureSufficientExecutionStack(); + resourceWrapper = UpdateResourceWrapper(resourceWrapper, readContext); return ReadResource(resourceWrapper, edmType.AsStructured(), readContext); } @@ -335,7 +339,39 @@ 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; + } + + foreach (ODataItemBase childItem in nestedItems) { // it maybe null. if (childItem == null) @@ -351,14 +387,7 @@ public virtual void ApplyNestedProperty(object resource, ODataNestedResourceInfo ApplyResourceInNestedProperty(edmProperty, resource, null, readContext); } } - - ODataEntityReferenceLinkBase entityReferenceLink = childItem as ODataEntityReferenceLinkBase; - if (entityReferenceLink != null) - { - // ignore entity reference links. - continue; - } - + ODataResourceSetWrapperBase resourceSetWrapper = childItem as ODataResourceSetWrapperBase; if (resourceSetWrapper != null) { @@ -392,6 +421,137 @@ public virtual void ApplyNestedProperty(object resource, ODataNestedResourceInfo } } + + 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); + } + + /// + /// Update the resource wrapper if it has the "Id" value. + /// + /// The resource wrapper. + /// The read context. + /// The resource wrapper. + private ODataResourceWrapper UpdateResourceWrapper(ODataResourceWrapper resourceWrapper, ODataDeserializerContext readContext) + { + Contract.Assert(readContext != null); + + if (resourceWrapper?.Resource?.Id == null) + { + return resourceWrapper; + } + + IEnumerable keys = CreateKeyProperties(resourceWrapper.Resource.Id, readContext); + if (keys == null) + { + return resourceWrapper; + } + + if (resourceWrapper.Resource.Properties == null) + { + resourceWrapper.Resource.Properties = keys; + } + else + { + IDictionary newPropertiesDic = resourceWrapper.Resource.Properties.ToDictionary(p => p.Name, p => p); + foreach (ODataProperty key in keys) + { + if (!newPropertiesDic.ContainsKey(key.Name)) + { + newPropertiesDic[key.Name] = key; + } + } + + resourceWrapper.Resource.Properties = newPropertiesDic.Values; + } + + return resourceWrapper; + } + + /// + /// 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; + } + + IODataPathHandler pathHandler = readContext.InternalRequest.PathHandler; + IWebApiRequestMessage internalRequest = readContext.InternalRequest; + IWebApiUrlHelper urlHelper = readContext.InternalUrlHelper; + try + { + string serviceRoot = urlHelper.CreateODataLink( + internalRequest.Context.RouteName, + internalRequest.PathHandler, + new List()); + ODataPath odataPath = pathHandler.Parse(serviceRoot, id.OriginalString, internalRequest.RequestContainer); + KeySegment keySegment = odataPath.Segments.OfType().LastOrDefault(); + if (keySegment != null) + { + foreach (KeyValuePair key in keySegment.Keys) + { + properties.Add(new ODataProperty + { + Name = key.Key, + Value = key.Value + }); + } + } + + return properties; + } + catch (Exception) + { + return properties; + } + } + /// /// Deserializes the structural properties from into . /// @@ -520,7 +680,7 @@ private object ReadNestedResourceInline(ODataResourceWrapper resourceWrapper, IE { Path = readContext.Path, Model = readContext.Model, - Request = readContext.Request, + Request = readContext.Request }; Type clrType = null; @@ -546,7 +706,6 @@ private object ReadNestedResourceInline(ODataResourceWrapper resourceWrapper, IE : clrType; return deserializer.ReadInline(resourceWrapper, edmType, nestedReadContext); } - private void ApplyResourceSetInNestedProperty(IEdmProperty nestedProperty, object resource, ODataResourceSetWrapperBase resourceSetWrapper, ODataDeserializerContext readContext) { @@ -618,7 +777,7 @@ private object ReadNestedResourceSetInline(ODataResourceSetWrapperBase resourceS { Path = readContext.Path, Model = readContext.Model, - Request = readContext.Request, + Request = readContext.Request }; if (readContext.IsUntyped) 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/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..1d6ec7cf06 --- /dev/null +++ b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Formatter/ODataEntityReferenceLinkTests.cs @@ -0,0 +1,181 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. 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/Microsoft.AspNet.OData.Test.Shared.projitems b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Microsoft.AspNet.OData.Test.Shared.projitems index 41db702ac0..b28e672f9d 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 @@ -169,6 +169,7 @@ + From cebba7f9a1f34a1380b51f01ee7edd9c0fbff3e9 Mon Sep 17 00:00:00 2001 From: Sreejith Pazhampilly Date: Thu, 8 Jul 2021 11:20:12 -0700 Subject: [PATCH 03/28] updates --- .../ODataResourceDeserializer.cs | 12 +++---- .../GlobalSuppressions.cs | 33 +++++++------------ .../BulkOperation/BulkInsertController.cs | 5 +++ .../BulkOperation/BulkInsertTest.cs | 28 ++++++++++++++++ 4 files changed, 51 insertions(+), 27 deletions(-) diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializer.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializer.cs index 34883c7b1d..9ba76d8011 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializer.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializer.cs @@ -473,24 +473,24 @@ private ODataResourceWrapper UpdateResourceWrapper(ODataResourceWrapper resource { Contract.Assert(readContext != null); - if (resourceWrapper?.Resource?.Id == null) + if (resourceWrapper?.ResourceBase?.Id == null) { return resourceWrapper; } - IEnumerable keys = CreateKeyProperties(resourceWrapper.Resource.Id, readContext); + IEnumerable keys = CreateKeyProperties(resourceWrapper.ResourceBase.Id, readContext); if (keys == null) { return resourceWrapper; } - if (resourceWrapper.Resource.Properties == null) + if (resourceWrapper.ResourceBase.Properties == null) { - resourceWrapper.Resource.Properties = keys; + resourceWrapper.ResourceBase.Properties = keys; } else { - IDictionary newPropertiesDic = resourceWrapper.Resource.Properties.ToDictionary(p => p.Name, p => p); + IDictionary newPropertiesDic = resourceWrapper.ResourceBase.Properties.ToDictionary(p => p.Name, p => p); foreach (ODataProperty key in keys) { if (!newPropertiesDic.ContainsKey(key.Name)) @@ -499,7 +499,7 @@ private ODataResourceWrapper UpdateResourceWrapper(ODataResourceWrapper resource } } - resourceWrapper.Resource.Properties = newPropertiesDic.Values; + resourceWrapper.ResourceBase.Properties = newPropertiesDic.Values; } return resourceWrapper; diff --git a/src/Microsoft.AspNet.OData/GlobalSuppressions.cs b/src/Microsoft.AspNet.OData/GlobalSuppressions.cs index 9963fa07c0..6582c7bd5f 100644 --- a/src/Microsoft.AspNet.OData/GlobalSuppressions.cs +++ b/src/Microsoft.AspNet.OData/GlobalSuppressions.cs @@ -70,33 +70,24 @@ [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", "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", "CA1007:UseGenericsWhereAppropriate", Scope = "member", Target = "Microsoft.AspNet.OData.PatchMethodHandler+TryCreate.#Invoke(System.Object&,System.String&)")] -[assembly: SuppressMessage("Microsoft.Design", "CA1007:UseGenericsWhereAppropriate", 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 = "0#", Scope = "member", Target = "Microsoft.AspNet.OData.PatchMethodHandler`1.#TryCreate(!0&,System.String&)")] -[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.PatchMethodHandler`1.#TryCreate(!0&,System.String&)")] [assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Scope = "member", Target = "Microsoft.AspNet.OData.PatchMethodHandler`1.#TryGet(System.Collections.Generic.IDictionary`2,!0&,System.String&)")] [assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.PatchMethodHandler`1.#TryDelete(System.Collections.Generic.IDictionary`2,System.String&)")] [assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.PatchMethodHandler`1.#TryGet(System.Collections.Generic.IDictionary`2,!0&,System.String&)")] [assembly: SuppressMessage("Microsoft.Design", "CA1040:AvoidEmptyInterfaces", Scope = "type", Target = "Microsoft.AspNet.OData.IPatchMethodHandler")] [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.EdmPatchMethodHandler.#TryCreate(Microsoft.AspNet.OData.IEdmChangedObject,Microsoft.AspNet.OData.EdmStructuredObject&,System.String&)")] -[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Scope = "member", Target = "Microsoft.AspNet.OData.EdmPatchMethodHandler.#TryCreate(Microsoft.AspNet.OData.IEdmChangedObject,Microsoft.AspNet.OData.EdmStructuredObject&,System.String&)")] -[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Scope = "member", Target = "Microsoft.AspNet.OData.EdmPatchMethodHandler.#TryGet(System.Collections.Generic.IDictionary`2,Microsoft.AspNet.OData.EdmStructuredObject&,System.String&)")] [assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.EdmPatchMethodHandler.#TryDelete(System.Collections.Generic.IDictionary`2,System.String&)")] [assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.EdmPatchMethodHandler.#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.EdmPatchMethodHandler.#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.EdmPatchMethodHandler.#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.EdmPatchMethodHandler.#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.EdmPatchMethodHandler.#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)")] diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertController.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertController.cs index f5d0050712..2ce91d15e8 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertController.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertController.cs @@ -359,6 +359,11 @@ public ITestActionResult Patch(int key, [FromBody] Delta delta) try { delta.Patch(employee, new EmployeePatchHandler()); + + if(employee.Name == "Bind1") + { + Assert.NotNull(employee.Friends.Single(x => x.Id == 3)); + } } catch (ArgumentException ae) { diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertTest.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertTest.cs index 2fe121d7b4..fd29728e16 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertTest.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertTest.cs @@ -654,6 +654,34 @@ public async Task PatchEmployee_WithDelete() } } + [Fact] + public async Task PatchEmployee_WithODataBind() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Employees(1)"; + + 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; + + //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 PatchEmployee_WithAddUpdateAndDelete() From f9acd9820fd575b09634c8db9fe0b1e97b34dab4 Mon Sep 17 00:00:00 2001 From: Sreejith Pazhampilly Date: Fri, 9 Jul 2021 07:31:10 -0700 Subject: [PATCH 04/28] small update --- .../BulkOperation/BulkInsertDataModel.cs | 3 +-- .../BulkOperation/BulkInsertEdmModel.cs | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertDataModel.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertDataModel.cs index 11d203faad..a3ebc663d9 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertDataModel.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertDataModel.cs @@ -106,8 +106,7 @@ public class UnTypedFriend public string Name { get; set; } public int Age { get; set; } - - [AutoExpand] + public UnTypedAddress Address { get; set; } public IODataInstanceAnnotationContainer InstanceAnnotations { get; set; } diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertEdmModel.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertEdmModel.cs index 8903e1a5b9..d8f535e981 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertEdmModel.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertEdmModel.cs @@ -60,6 +60,7 @@ public static IEdmModel GetConventionModel(WebRouteConfiguration configuration) 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; From 627276da85111c8dc34ff05d52aab077f4b528ca Mon Sep 17 00:00:00 2001 From: Sreejith Pazhampilly Date: Tue, 27 Jul 2021 07:26:40 -0700 Subject: [PATCH 05/28] Code review comments --- .../DefaultEdmPatchMethodHandler.cs | 9 +++- .../DefaultPatchHandler.cs | 16 ++++--- .../DeltaSetOfT.cs | 8 +++- .../EdmPatchMethodHandler.cs | 3 +- .../Serialization/ODataSerializerHelper.cs | 7 +-- .../IDeltaSet.cs | 7 ++- .../IPatchMethodHandler.cs | 15 +++++++ .../Microsoft.AspNet.OData.Shared.projitems | 2 + .../PatchMethodHandler.cs | 43 +++---------------- .../PatchStatus.cs | 24 +++++++++++ .../Conventions/EntitySetRoutingConvention.cs | 2 +- .../BulkOperation/EFTests/BulkInsertTestEF.cs | 2 +- 12 files changed, 80 insertions(+), 58 deletions(-) create mode 100644 src/Microsoft.AspNet.OData.Shared/IPatchMethodHandler.cs create mode 100644 src/Microsoft.AspNet.OData.Shared/PatchStatus.cs diff --git a/src/Microsoft.AspNet.OData.Shared/DefaultEdmPatchMethodHandler.cs b/src/Microsoft.AspNet.OData.Shared/DefaultEdmPatchMethodHandler.cs index 3cdd4d22ef..17a98fcd08 100644 --- a/src/Microsoft.AspNet.OData.Shared/DefaultEdmPatchMethodHandler.cs +++ b/src/Microsoft.AspNet.OData.Shared/DefaultEdmPatchMethodHandler.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Linq; @@ -6,6 +9,10 @@ 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 DefaultEdmPatchMethodHandler : EdmPatchMethodHandler { IEdmEntityType entityType; diff --git a/src/Microsoft.AspNet.OData.Shared/DefaultPatchHandler.cs b/src/Microsoft.AspNet.OData.Shared/DefaultPatchHandler.cs index ccd9bd080e..fd9599e1c2 100644 --- a/src/Microsoft.AspNet.OData.Shared/DefaultPatchHandler.cs +++ b/src/Microsoft.AspNet.OData.Shared/DefaultPatchHandler.cs @@ -1,13 +1,17 @@ -using System; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; using System.Reflection; -using System.Text; -using System.Threading.Tasks; namespace Microsoft.AspNet.OData -{ +{ + /// + /// This is the default Patch Handler 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 DefaultPatchHandler : PatchMethodHandler where TStructuralType :class { Type _clrType; diff --git a/src/Microsoft.AspNet.OData.Shared/DeltaSetOfT.cs b/src/Microsoft.AspNet.OData.Shared/DeltaSetOfT.cs index dffc2bab88..f9b05e5e10 100644 --- a/src/Microsoft.AspNet.OData.Shared/DeltaSetOfT.cs +++ b/src/Microsoft.AspNet.OData.Shared/DeltaSetOfT.cs @@ -5,6 +5,7 @@ using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics; using System.Diagnostics.Contracts; using System.Linq; using System.Reflection; @@ -67,8 +68,11 @@ public DeltaSet Patch(ICollection originalColl /// /// DeltaSet response public DeltaSet Patch(IPatchMethodHandler patchHandler) - { - return CopyChangedValues(patchHandler as PatchMethodHandler); + { + PatchMethodHandler patchHandlerOfT = patchHandler as PatchMethodHandler; + Debug.Assert(patchHandlerOfT != null); + + return CopyChangedValues(patchHandlerOfT); } diff --git a/src/Microsoft.AspNet.OData.Shared/EdmPatchMethodHandler.cs b/src/Microsoft.AspNet.OData.Shared/EdmPatchMethodHandler.cs index 39def05d90..6aa7dc489e 100644 --- a/src/Microsoft.AspNet.OData.Shared/EdmPatchMethodHandler.cs +++ b/src/Microsoft.AspNet.OData.Shared/EdmPatchMethodHandler.cs @@ -12,7 +12,8 @@ namespace Microsoft.AspNet.OData { /// - /// Handler Class to handle users methods for create, delete and update + /// Handler Class to handle users methods for create, delete and update. + /// This is the handler for data modification where there is no CLR type. /// public abstract class EdmPatchMethodHandler { diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataSerializerHelper.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataSerializerHelper.cs index 0e831615e5..a130405301 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataSerializerHelper.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataSerializerHelper.cs @@ -1,13 +1,10 @@ // 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 Microsoft.AspNet.OData.Builder; using Microsoft.OData; using Microsoft.OData.Edm; -using Org.OData.Core.V1; -using System; -using System.Collections.Generic; -using System.Text; namespace Microsoft.AspNet.OData.Formatter.Serialization { @@ -15,7 +12,7 @@ namespace Microsoft.AspNet.OData.Formatter.Serialization /// Helper class for OData Serialization /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1053:StaticHolderTypesShouldNotHaveConstructors")] - public class ODataSerializerHelper + internal class ODataSerializerHelper { internal static void AppendInstanceAnnotations(ODataResourceBase resource, ResourceContext resourceContext, object value, ODataSerializerProvider SerializerProvider) { diff --git a/src/Microsoft.AspNet.OData.Shared/IDeltaSet.cs b/src/Microsoft.AspNet.OData.Shared/IDeltaSet.cs index 8813099e35..a09a4f127e 100644 --- a/src/Microsoft.AspNet.OData.Shared/IDeltaSet.cs +++ b/src/Microsoft.AspNet.OData.Shared/IDeltaSet.cs @@ -1,14 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. -using System; -using System.Collections.Generic; -using System.Text; - namespace Microsoft.AspNet.OData { /// /// Basic interface to reperesent a deltaset which is a collection of Deltas. + /// This is being implemented by Deltaset{TStructuralType}. Since its being implementd by a gemeric 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/IPatchMethodHandler.cs b/src/Microsoft.AspNet.OData.Shared/IPatchMethodHandler.cs new file mode 100644 index 0000000000..d87ce24543 --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/IPatchMethodHandler.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.OData +{ + /// + /// Base Interface for PatchMethodHandler. + /// This is being implemented by PatchMethodHandler{TStructuralType} which has a method returning nested patchhandler. + /// A generic empty interface is needed since the nestedpatch handler will be of different type. + /// + public interface IPatchMethodHandler + { + + } +} 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 e97e428b49..413e19fb1c 100644 --- a/src/Microsoft.AspNet.OData.Shared/Microsoft.AspNet.OData.Shared.projitems +++ b/src/Microsoft.AspNet.OData.Shared/Microsoft.AspNet.OData.Shared.projitems @@ -86,7 +86,9 @@ + + diff --git a/src/Microsoft.AspNet.OData.Shared/PatchMethodHandler.cs b/src/Microsoft.AspNet.OData.Shared/PatchMethodHandler.cs index c1c34748f0..56bfe39121 100644 --- a/src/Microsoft.AspNet.OData.Shared/PatchMethodHandler.cs +++ b/src/Microsoft.AspNet.OData.Shared/PatchMethodHandler.cs @@ -1,23 +1,13 @@ -using System; +// 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.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; namespace Microsoft.AspNet.OData -{ - /// - /// Base Interface for PatchMethodHandler - /// - public interface IPatchMethodHandler - { - - } - +{ /// - /// Handler Class to handle users methods for create, delete and update + /// 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 PatchMethodHandler: IPatchMethodHandler where TStructuralType : class { @@ -57,25 +47,4 @@ public abstract class PatchMethodHandler: IPatchMethodHandler w /// The type of Nested PatchMethodHandler public abstract IPatchMethodHandler GetNestedPatchHandler(TStructuralType parent, string navigationPropertyName); } - - - /// - /// Enum for Patch Status - /// - public enum PatchStatus - { - /// - /// Success Status - /// - Success, - /// - /// Failure Status - /// - Failure, - /// - /// Resource Not Found - /// - NotFound - } - } diff --git a/src/Microsoft.AspNet.OData.Shared/PatchStatus.cs b/src/Microsoft.AspNet.OData.Shared/PatchStatus.cs new file mode 100644 index 0000000000..16f9a48319 --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/PatchStatus.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.OData +{ + /// + /// Enum for Patch Status + /// + public enum PatchStatus + { + /// + /// Success Status + /// + Success, + /// + /// Failure Status + /// + Failure, + /// + /// Resource Not Found + /// + NotFound + } +} diff --git a/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/EntitySetRoutingConvention.cs b/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/EntitySetRoutingConvention.cs index 7098b414fd..38b775a6a4 100644 --- a/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/EntitySetRoutingConvention.cs +++ b/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/EntitySetRoutingConvention.cs @@ -48,7 +48,7 @@ internal static string SelectActionImpl(ODataPath odataPath, IWebApiControllerCo } else if (ODataRequestMethod.Patch == controllerContext.Request.Method) { - // e.g. Try PatchCustomer first, then fall back to Patch action name + // e.g. Try PatchCustomers first, then fall back to Patch action name return actionMap.FindMatchingAction( "Patch" + entitySet.Name, "Patch"); diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/BulkOperation/EFTests/BulkInsertTestEF.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/BulkOperation/EFTests/BulkInsertTestEF.cs index 38b9108cea..5919eb09f3 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/BulkOperation/EFTests/BulkInsertTestEF.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/BulkOperation/EFTests/BulkInsertTestEF.cs @@ -105,7 +105,7 @@ public async Task PatchEmployee_WithUpdates_Employees() string requestUri = this.BaseAddress + "/convention/Employees"; - var content = @"{'@odata.context':'http://host/service/$metadata#Employees/$delta', + var content = @"{'@odata.context':'http://localhost:11001/convention/$metadata#Employees/$delta', 'value':[{ '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkInsert.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'}] From 6789fb99dcb305b34f224a3564ed1ccbf3d6e758 Mon Sep 17 00:00:00 2001 From: Sreejith Pazhampilly Date: Tue, 24 Aug 2021 13:00:27 -0700 Subject: [PATCH 06/28] update for odataid _temp --- .../DefaultEdmPatchMethodHandler.cs | 28 +- ...chHandler.cs => DefaultODataAPIHandler.cs} | 32 +- .../DeltaSetOfT.cs | 41 +- .../EdmChangedObjectCollection.cs | 56 +- .../EdmEntityObject.cs | 5 + .../EdmPatchMethodHandler.cs | 18 +- .../ODataResourceDeserializer.cs | 63 ++- .../IDeltaSetItem.cs | 5 + ...chMethodHandler.cs => IODataAPIHandler.cs} | 6 +- .../KeyProperty.cs | 21 + .../Microsoft.AspNet.OData.Shared.projitems | 13 +- .../NavigationPath.cs | 84 +++ ...tchMethodHandler.cs => ODataAPIHandler.cs} | 22 +- .../ODataAPIHandlerFactory.cs | 18 + ...tchStatus.cs => ODataAPIResponseStatus.cs} | 2 +- .../ODataIdContainer.cs | 16 + src/Microsoft.AspNet.OData.Shared/PathItem.cs | 23 + .../GlobalSuppressions.cs | 20 +- .../EFTests/BulkOperationPatchHandlersEF.cs | 98 ++-- .../BulkOperation/BulkInsertController.cs | 187 +++++-- .../BulkOperation/BulkInsertDataModel.cs | 43 ++ .../BulkOperation/BulkInsertEdmModel.cs | 3 + .../BulkOperation/BulkInsertTest.cs | 68 ++- .../BulkOperationPatchHandlers.cs | 491 +++++++++++++++--- .../DeltaSetOfTTest.cs | 34 +- .../EdmChangedObjectCollectionTest.cs | 48 +- 26 files changed, 1120 insertions(+), 325 deletions(-) rename src/Microsoft.AspNet.OData.Shared/{DefaultPatchHandler.cs => DefaultODataAPIHandler.cs} (73%) rename src/Microsoft.AspNet.OData.Shared/{IPatchMethodHandler.cs => IODataAPIHandler.cs} (62%) create mode 100644 src/Microsoft.AspNet.OData.Shared/KeyProperty.cs create mode 100644 src/Microsoft.AspNet.OData.Shared/NavigationPath.cs rename src/Microsoft.AspNet.OData.Shared/{PatchMethodHandler.cs => ODataAPIHandler.cs} (66%) create mode 100644 src/Microsoft.AspNet.OData.Shared/ODataAPIHandlerFactory.cs rename src/Microsoft.AspNet.OData.Shared/{PatchStatus.cs => ODataAPIResponseStatus.cs} (93%) create mode 100644 src/Microsoft.AspNet.OData.Shared/ODataIdContainer.cs create mode 100644 src/Microsoft.AspNet.OData.Shared/PathItem.cs diff --git a/src/Microsoft.AspNet.OData.Shared/DefaultEdmPatchMethodHandler.cs b/src/Microsoft.AspNet.OData.Shared/DefaultEdmPatchMethodHandler.cs index 17a98fcd08..8e9390c54d 100644 --- a/src/Microsoft.AspNet.OData.Shared/DefaultEdmPatchMethodHandler.cs +++ b/src/Microsoft.AspNet.OData.Shared/DefaultEdmPatchMethodHandler.cs @@ -13,12 +13,12 @@ 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 DefaultEdmPatchMethodHandler : EdmPatchMethodHandler + internal class DefaultEdmODataAPIHandler : EdmODataAPIHandler { IEdmEntityType entityType; ICollection originalList; - public DefaultEdmPatchMethodHandler(ICollection originalList, IEdmEntityType entityType) + public DefaultEdmODataAPIHandler(ICollection originalList, IEdmEntityType entityType) { Contract.Assert(entityType != null); @@ -27,9 +27,9 @@ public DefaultEdmPatchMethodHandler(ICollection originalLi } [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] - public override PatchStatus TryGet(IDictionary keyValues, out IEdmStructuredObject originalObject, out string errorMessage) + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out IEdmStructuredObject originalObject, out string errorMessage) { - PatchStatus status = PatchStatus.Success; + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; errorMessage = string.Empty; originalObject = null; @@ -41,12 +41,12 @@ public override PatchStatus TryGet(IDictionary keyValues, out IE if (originalObject == null) { - status = PatchStatus.NotFound; + status = ODataAPIResponseStatus.NotFound; } } catch (Exception ex) { - status = PatchStatus.Failure; + status = ODataAPIResponseStatus.Failure; errorMessage = ex.Message; } @@ -54,7 +54,7 @@ public override PatchStatus TryGet(IDictionary keyValues, out IE } [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] - public override PatchStatus TryCreate(IEdmChangedObject changedObject, out IEdmStructuredObject createdObject, out string errorMessage) + public override ODataAPIResponseStatus TryCreate(IEdmChangedObject changedObject, out IEdmStructuredObject createdObject, out string errorMessage) { createdObject = null; errorMessage = string.Empty; @@ -64,18 +64,18 @@ public override PatchStatus TryCreate(IEdmChangedObject changedObject, out IEdmS createdObject = new EdmEntityObject(entityType); originalList.Add(createdObject); - return PatchStatus.Success; + return ODataAPIResponseStatus.Success; } catch (Exception ex) { errorMessage = ex.Message; - return PatchStatus.Failure; + return ODataAPIResponseStatus.Failure; } } [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] - public override PatchStatus TryDelete(IDictionary keyValues, out string errorMessage) + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) { errorMessage = string.Empty; @@ -88,17 +88,17 @@ public override PatchStatus TryDelete(IDictionary keyValues, out originalList.Remove(originalObject); } - return PatchStatus.Success; + return ODataAPIResponseStatus.Success; } catch (Exception ex) { errorMessage = ex.Message; - return PatchStatus.Failure; + return ODataAPIResponseStatus.Failure; } } - public override EdmPatchMethodHandler GetNestedPatchHandler(IEdmStructuredObject parent, string navigationPropertyName) + public override EdmODataAPIHandler GetNestedHandler(IEdmStructuredObject parent, string navigationPropertyName) { IEdmNavigationProperty navProperty = entityType.NavigationProperties().FirstOrDefault(navProp => navProp.Name == navigationPropertyName); @@ -114,7 +114,7 @@ public override EdmPatchMethodHandler GetNestedPatchHandler(IEdmStructuredObject { ICollection nestedList = obj as ICollection; - return new DefaultEdmPatchMethodHandler(nestedList, nestedEntityType); + return new DefaultEdmODataAPIHandler(nestedList, nestedEntityType); } return null; diff --git a/src/Microsoft.AspNet.OData.Shared/DefaultPatchHandler.cs b/src/Microsoft.AspNet.OData.Shared/DefaultODataAPIHandler.cs similarity index 73% rename from src/Microsoft.AspNet.OData.Shared/DefaultPatchHandler.cs rename to src/Microsoft.AspNet.OData.Shared/DefaultODataAPIHandler.cs index fd9599e1c2..5974ae2169 100644 --- a/src/Microsoft.AspNet.OData.Shared/DefaultPatchHandler.cs +++ b/src/Microsoft.AspNet.OData.Shared/DefaultODataAPIHandler.cs @@ -8,25 +8,25 @@ namespace Microsoft.AspNet.OData { /// - /// This is the default Patch Handler for CLR type. This calss has default Get, Create and Update + /// 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 DefaultPatchHandler : PatchMethodHandler where TStructuralType :class + internal class DefaultODataAPIHandler : ODataAPIHandler where TStructuralType :class { Type _clrType; ICollection originalList; - public DefaultPatchHandler(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 PatchStatus TryGet(IDictionary keyValues, out TStructuralType originalObject, out string errorMessage) + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out TStructuralType originalObject, out string errorMessage) { - PatchStatus status = PatchStatus.Success; + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; errorMessage = string.Empty; originalObject = default(TStructuralType); @@ -36,12 +36,12 @@ public override PatchStatus TryGet(IDictionary keyValues, out TS if (originalObject == null) { - status = PatchStatus.NotFound; + status = ODataAPIResponseStatus.NotFound; } } catch (Exception ex) { - status = PatchStatus.Failure; + status = ODataAPIResponseStatus.Failure; errorMessage = ex.Message; } @@ -49,7 +49,7 @@ public override PatchStatus TryGet(IDictionary keyValues, out TS } [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] - public override PatchStatus TryCreate(Delta patchObject, out TStructuralType createdObject, out string errorMessage) + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out TStructuralType createdObject, out string errorMessage) { createdObject = default(TStructuralType); errorMessage = string.Empty; @@ -64,18 +64,18 @@ public override PatchStatus TryCreate(Delta patchObject, out TS createdObject = Activator.CreateInstance(_clrType) as TStructuralType; originalList.Add(createdObject); - return PatchStatus.Success; + return ODataAPIResponseStatus.Success; } catch (Exception ex) { errorMessage = ex.Message; - return PatchStatus.Failure; + return ODataAPIResponseStatus.Failure; } } [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] - public override PatchStatus TryDelete(IDictionary keyValues, out string errorMessage) + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) { errorMessage = string.Empty; @@ -84,25 +84,25 @@ public override PatchStatus TryDelete(IDictionary keyValues, out TStructuralType originalObject = GetFilteredItem(keyValues); originalList.Remove(originalObject); - return PatchStatus.Success; + return ODataAPIResponseStatus.Success; } catch (Exception ex) { errorMessage = ex.Message; - return PatchStatus.Failure; + return ODataAPIResponseStatus.Failure; } } - public override IPatchMethodHandler GetNestedPatchHandler(TStructuralType parent, string navigationPropertyName) + public override IODataAPIHandler GetNestedHandler(TStructuralType parent, string navigationPropertyName) { foreach (PropertyInfo property in _clrType.GetProperties()) { if (property.Name == navigationPropertyName) { - Type type = typeof(DefaultPatchHandler<>).MakeGenericType(property.PropertyType.GetGenericArguments()[0]); + Type type = typeof(DefaultODataAPIHandler<>).MakeGenericType(property.PropertyType.GetGenericArguments()[0]); - return Activator.CreateInstance(type, property.GetValue(parent)) as IPatchMethodHandler; + return Activator.CreateInstance(type, property.GetValue(parent)) as IODataAPIHandler; } } diff --git a/src/Microsoft.AspNet.OData.Shared/DeltaSetOfT.cs b/src/Microsoft.AspNet.OData.Shared/DeltaSetOfT.cs index f9b05e5e10..a3921c3a0f 100644 --- a/src/Microsoft.AspNet.OData.Shared/DeltaSetOfT.cs +++ b/src/Microsoft.AspNet.OData.Shared/DeltaSetOfT.cs @@ -58,26 +58,27 @@ protected override void InsertItem(int index, IDeltaSetItem item) /// /// DeltaSet response public DeltaSet Patch(ICollection originalCollection) { - PatchMethodHandler patchHandler = new DefaultPatchHandler(originalCollection); + ODataAPIHandler apiHandler = new DefaultODataAPIHandler(originalCollection); - return CopyChangedValues(patchHandler); + return CopyChangedValues(apiHandler); } + /// /// Patch for DeltaSet, a collection for Delta /// /// DeltaSet response - public DeltaSet Patch(IPatchMethodHandler patchHandler) + public DeltaSet Patch(IODataAPIHandler apiHandler, ODataAPIHandlerFactory apiHandlerFactory = null) { - PatchMethodHandler patchHandlerOfT = patchHandler as PatchMethodHandler; - Debug.Assert(patchHandlerOfT != null); + ODataAPIHandler apiHandlerOfT = apiHandler as ODataAPIHandler; + Debug.Assert(apiHandlerOfT != null); - return CopyChangedValues(patchHandlerOfT); + return CopyChangedValues(apiHandlerOfT, apiHandlerFactory); } [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] - internal DeltaSet CopyChangedValues(PatchMethodHandler patchHandler) + internal DeltaSet CopyChangedValues(ODataAPIHandler 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 @@ -93,6 +94,14 @@ internal DeltaSet CopyChangedValues(PatchMethodHandler keyValues = new Dictionary(); foreach (string key in _keys) @@ -107,11 +116,11 @@ internal DeltaSet CopyChangedValues(PatchMethodHandler deletedObj = changedObj as DeltaDeletedEntityObject; - if (patchStatus == PatchStatus.Failure || (deletedObj != null && patchStatus == PatchStatus.NotFound)) + if (ODataAPIResponseStatus == ODataAPIResponseStatus.Failure || (deletedObj != null && ODataAPIResponseStatus == ODataAPIResponseStatus.NotFound)) { IDeltaSetItem deltaSetItem = changedObj; @@ -127,13 +136,13 @@ internal DeltaSet CopyChangedValues(PatchMethodHandler CopyChangedValues(PatchMethodHandler CopyChangedValues(PatchMethodHandler CopyChangedValues(PatchMethodHandler for each item in the collection // This will work in case we use delegates for using users method to create an object - changedObj.Patch(original, patchHandler); + changedObj.Patch(original, apiHandler, apiHandlerFactory); deltaSet.Add(changedObj); } diff --git a/src/Microsoft.AspNet.OData.Shared/EdmChangedObjectCollection.cs b/src/Microsoft.AspNet.OData.Shared/EdmChangedObjectCollection.cs index 9c2e1882fe..a103223d37 100644 --- a/src/Microsoft.AspNet.OData.Shared/EdmChangedObjectCollection.cs +++ b/src/Microsoft.AspNet.OData.Shared/EdmChangedObjectCollection.cs @@ -81,25 +81,25 @@ private void Initialize(IEdmEntityType entityType) /// ChangedObjectCollection response internal EdmChangedObjectCollection Patch(ICollection originalCollection) { - EdmPatchMethodHandler patchHandler = new DefaultEdmPatchMethodHandler(originalCollection, _entityType); + EdmODataAPIHandler apiHandler = new DefaultEdmODataAPIHandler(originalCollection, _entityType); - return CopyChangedValues(patchHandler); + return CopyChangedValues(apiHandler); } /// /// Patch for EdmChangedObjectCollection, a collection for IEdmChangedObject /// /// ChangedObjectCollection response - public EdmChangedObjectCollection Patch(EdmPatchMethodHandler patchHandler) + public EdmChangedObjectCollection Patch(EdmODataAPIHandler apiHandler) { - return CopyChangedValues(patchHandler); + return CopyChangedValues(apiHandler); } [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(EdmPatchMethodHandler patchHandler) + internal EdmChangedObjectCollection CopyChangedValues(EdmODataAPIHandler apiHandler) { EdmChangedObjectCollection changedObjectCollection = new EdmChangedObjectCollection(_entityType); IEdmStructuralProperty[] keys = _entityType.Key().ToArray(); @@ -121,13 +121,13 @@ internal EdmChangedObjectCollection CopyChangedValues(EdmPatchMethodHandler patc { operation = DataModificationOperationKind.Delete; - if (patchHandler.TryDelete(keyValues, out errorMessage) != PatchStatus.Success) + if (apiHandler.TryDelete(keyValues, out errorMessage) != ODataAPIResponseStatus.Success) { //Handle Failed Operation - Delete - PatchStatus patchStatus = patchHandler.TryGet(keyValues, out original, out getErrorMessage); - if (patchStatus == PatchStatus.Success) + ODataAPIResponseStatus ODataAPIResponseStatus = apiHandler.TryGet(keyValues, out original, out getErrorMessage); + if (ODataAPIResponseStatus == ODataAPIResponseStatus.Success) { - IEdmChangedObject changedObject = HandleFailedOperation(deletedObj, operation, original, keys, errorMessage, patchHandler); + IEdmChangedObject changedObject = HandleFailedOperation(deletedObj, operation, original, keys, errorMessage, apiHandler); changedObjectCollection.Add(changedObject); continue; } @@ -139,34 +139,34 @@ internal EdmChangedObjectCollection CopyChangedValues(EdmPatchMethodHandler patc { EdmEntityObject deltaEntityObject = changedObj as EdmEntityObject; - PatchStatus patchStatus = patchHandler.TryGet(keyValues, out original, out getErrorMessage); + ODataAPIResponseStatus ODataAPIResponseStatus = apiHandler.TryGet(keyValues, out original, out getErrorMessage); - if (patchStatus == PatchStatus.NotFound) + if (ODataAPIResponseStatus == ODataAPIResponseStatus.NotFound) { operation = DataModificationOperationKind.Insert; - if (patchHandler.TryCreate(changedObj, out original, out errorMessage) != PatchStatus.Success) + if (apiHandler.TryCreate(changedObj, out original, out errorMessage) != ODataAPIResponseStatus.Success) { //Handle failed Opreataion - create - IEdmChangedObject changedObject = HandleFailedOperation(deltaEntityObject, operation, original, keys, errorMessage, patchHandler); + IEdmChangedObject changedObject = HandleFailedOperation(deltaEntityObject, operation, original, keys, errorMessage, apiHandler); changedObjectCollection.Add(changedObject); continue; } } - else if (patchStatus == PatchStatus.Success) + else if (ODataAPIResponseStatus == ODataAPIResponseStatus.Success) { operation = DataModificationOperationKind.Update; } else { //Handle failed operation - IEdmChangedObject changedObject = HandleFailedOperation(deltaEntityObject, operation, null, keys, getErrorMessage, patchHandler); + IEdmChangedObject changedObject = HandleFailedOperation(deltaEntityObject, operation, null, keys, getErrorMessage, apiHandler); changedObjectCollection.Add(changedObject); continue; } //Patch for addition/update. - PatchItem(deltaEntityObject, original as EdmStructuredObject, patchHandler); + PatchItem(deltaEntityObject, original as EdmStructuredObject, apiHandler); changedObjectCollection.Add(changedObj); } @@ -174,7 +174,7 @@ internal EdmChangedObjectCollection CopyChangedValues(EdmPatchMethodHandler patc catch (Exception ex) { //Handle Failed Operation - IEdmChangedObject changedObject = HandleFailedOperation(changedObj as EdmEntityObject, operation, originalObj, keys, ex.Message, patchHandler); + IEdmChangedObject changedObject = HandleFailedOperation(changedObj as EdmEntityObject, operation, originalObj, keys, ex.Message, apiHandler); Contract.Assert(changedObject != null); changedObjectCollection.Add(changedObject); @@ -202,20 +202,20 @@ private static IDictionary GetKeyValues(IEdmStructuralProperty [ return keyValues; } - private void PatchItem(EdmStructuredObject changedObj, EdmStructuredObject originalObj, EdmPatchMethodHandler patchHandler) + private void PatchItem(EdmStructuredObject changedObj, EdmStructuredObject originalObj, EdmODataAPIHandler apiHandler) { foreach (string propertyName in changedObj.GetChangedPropertyNames()) { - ApplyProperties(changedObj, originalObj, propertyName, patchHandler); + ApplyProperties(changedObj, originalObj, propertyName, apiHandler); } foreach (string propertyName in changedObj.GetUnchangedPropertyNames()) { - ApplyProperties(changedObj, originalObj, propertyName, patchHandler); + ApplyProperties(changedObj, originalObj, propertyName, apiHandler); } } - private void ApplyProperties(EdmStructuredObject changedObj, EdmStructuredObject originalObj, string propertyName, EdmPatchMethodHandler patchHandler) + private void ApplyProperties(EdmStructuredObject changedObj, EdmStructuredObject originalObj, string propertyName, EdmODataAPIHandler apiHandler) { object value; if (changedObj.TryGetPropertyValue(propertyName, out value)) @@ -223,10 +223,10 @@ private void ApplyProperties(EdmStructuredObject changedObj, EdmStructuredObject EdmChangedObjectCollection changedColl = value as EdmChangedObjectCollection; if (changedColl != null) { - EdmPatchMethodHandler patchHandlerNested = patchHandler.GetNestedPatchHandler(originalObj, propertyName); - if (patchHandlerNested != null) + EdmODataAPIHandler apiHandlerNested = apiHandler.GetNestedHandler(originalObj, propertyName); + if (apiHandlerNested != null) { - changedColl.Patch(patchHandlerNested); + changedColl.Patch(apiHandlerNested); } else { @@ -264,7 +264,7 @@ private void ApplyProperties(EdmStructuredObject changedObj, EdmStructuredObject originalObj.TrySetPropertyValue(propertyName, origStructuredObj); } - PatchItem(structuredObj, origStructuredObj, patchHandler); + PatchItem(structuredObj, origStructuredObj, apiHandler); } else { @@ -275,7 +275,7 @@ private void ApplyProperties(EdmStructuredObject changedObj, EdmStructuredObject } private IEdmChangedObject HandleFailedOperation(EdmEntityObject changedObj, DataModificationOperationKind operation, IEdmStructuredObject originalObj, - IEdmStructuralProperty[] keys, string errorMessage, EdmPatchMethodHandler patchHandler) + IEdmStructuralProperty[] keys, string errorMessage, EdmODataAPIHandler apiHandler) { IEdmChangedObject edmChangedObject = null; DataModificationExceptionType dataModificationExceptionType = new DataModificationExceptionType(operation); @@ -291,7 +291,7 @@ private IEdmChangedObject HandleFailedOperation(EdmEntityObject changedObj, Data case DataModificationOperationKind.Insert: { EdmDeltaDeletedEntityObject edmDeletedObject = new EdmDeltaDeletedEntityObject(EntityType); - PatchItem(edmDeletedObject, changedObj, patchHandler); + PatchItem(edmDeletedObject, changedObj, apiHandler); ValidateForDeletedEntityId(keys, edmDeletedObject); @@ -305,7 +305,7 @@ private IEdmChangedObject HandleFailedOperation(EdmEntityObject changedObj, Data case DataModificationOperationKind.Delete: { EdmDeltaEntityObject edmEntityObject = new EdmDeltaEntityObject(EntityType); - PatchItem(originalObj as EdmStructuredObject, edmEntityObject, patchHandler); + PatchItem(originalObj as EdmStructuredObject, edmEntityObject, apiHandler); edmEntityObject.TransientInstanceAnnotationContainer = changedObj.TransientInstanceAnnotationContainer; edmEntityObject.PersistentInstanceAnnotationsContainer = changedObj.PersistentInstanceAnnotationsContainer; diff --git a/src/Microsoft.AspNet.OData.Shared/EdmEntityObject.cs b/src/Microsoft.AspNet.OData.Shared/EdmEntityObject.cs index 02b47b8b7f..fc092392cc 100644 --- a/src/Microsoft.AspNet.OData.Shared/EdmEntityObject.cs +++ b/src/Microsoft.AspNet.OData.Shared/EdmEntityObject.cs @@ -61,6 +61,11 @@ public EdmEntityObject(IEdmEntityType edmType, bool isNullable) /// public IODataInstanceAnnotationContainer PersistentInstanceAnnotationsContainer { get; set; } + /// + /// Container to hold ODataId + /// + public ODataIdContainer ODataIdContainer { get; set; } + /// /// DeltaKind as Entry /// diff --git a/src/Microsoft.AspNet.OData.Shared/EdmPatchMethodHandler.cs b/src/Microsoft.AspNet.OData.Shared/EdmPatchMethodHandler.cs index 6aa7dc489e..08273149fd 100644 --- a/src/Microsoft.AspNet.OData.Shared/EdmPatchMethodHandler.cs +++ b/src/Microsoft.AspNet.OData.Shared/EdmPatchMethodHandler.cs @@ -15,7 +15,7 @@ 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. /// - public abstract class EdmPatchMethodHandler + public abstract class EdmODataAPIHandler { /// /// TryCreate method to create a new object. @@ -23,8 +23,8 @@ public abstract class EdmPatchMethodHandler /// 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 PatchStatus TryCreate(IEdmChangedObject changedObject, out IEdmStructuredObject createdObject, out string errorMessage); + /// 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. @@ -32,23 +32,23 @@ public abstract class EdmPatchMethodHandler /// 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 PatchStatus TryGet(IDictionary keyValues, out IEdmStructuredObject originalObject, out string errorMessage); + /// 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 PatchStatus TryDelete(IDictionary keyValues, out string errorMessage); + /// The status of the TryDelete Method, statuses are + public abstract ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage); /// - /// Get the PatchHandler for the nested type + /// 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 EdmPatchMethodHandler GetNestedPatchHandler(IEdmStructuredObject parent, string navigationPropertyName); + public abstract EdmODataAPIHandler GetNestedHandler(IEdmStructuredObject parent, string navigationPropertyName); } } diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializer.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializer.cs index 9ba76d8011..3e500f0e56 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializer.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializer.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; @@ -520,18 +521,12 @@ private static IList CreateKeyProperties(Uri id, ODataDeserialize { return properties; } - - IODataPathHandler pathHandler = readContext.InternalRequest.PathHandler; - IWebApiRequestMessage internalRequest = readContext.InternalRequest; - IWebApiUrlHelper urlHelper = readContext.InternalUrlHelper; + try { - string serviceRoot = urlHelper.CreateODataLink( - internalRequest.Context.RouteName, - internalRequest.PathHandler, - new List()); - ODataPath odataPath = pathHandler.Parse(serviceRoot, id.OriginalString, internalRequest.RequestContainer); + ODataPath odataPath = GetODataPath(id.OriginalString, readContext); KeySegment keySegment = odataPath.Segments.OfType().LastOrDefault(); + if (keySegment != null) { foreach (KeyValuePair key in keySegment.Keys) @@ -552,6 +547,55 @@ private static IList CreateKeyProperties(Uri id, ODataDeserialize } } + private static ODataPath GetODataPath(string id, ODataDeserializerContext readContext) + { + 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); + + return odataPath; + } + + private static void ApplyODataIDContainer(object resource, ODataResourceWrapper resourceWrapper, + IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext) + { + //if id null check, add delta case as well c + if (resourceWrapper.ResourceBase?.Id != null) + { + string odataId = resourceWrapper.ResourceBase.Id.OriginalString; + + ODataPath odataPath = GetODataPath(odataId, readContext); + + PropertyInfo containerPropertyInfo = EdmLibHelpers.GetClrType(odataPath.EdmType, readContext.Model).GetProperties().Where(x => x.PropertyType == typeof(ODataIdContainer)).FirstOrDefault(); + if (containerPropertyInfo != null) + { + ODataIdContainer container = new ODataIdContainer(); + + NavigationPath navigationPath = new NavigationPath(odataId, odataPath.Segments); + container.ODataIdNavigationPath = navigationPath; + + if(resource is EdmEntityObject edmObject) + { + edmObject.ODataIdContainer = container; + } + else if(resource is IDeltaSetItem deltasetItem) + { + deltasetItem.ODataIdContainer = container; + } + else + { + containerPropertyInfo.SetValue(resource, container); + } + } + } + } + /// /// Deserializes the structural properties from into . /// @@ -621,6 +665,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, structuredType, readContext); } private void ApplyResourceInNestedProperty(IEdmProperty nestedProperty, object resource, diff --git a/src/Microsoft.AspNet.OData.Shared/IDeltaSetItem.cs b/src/Microsoft.AspNet.OData.Shared/IDeltaSetItem.cs index aff7f077ef..39ea15fb06 100644 --- a/src/Microsoft.AspNet.OData.Shared/IDeltaSetItem.cs +++ b/src/Microsoft.AspNet.OData.Shared/IDeltaSetItem.cs @@ -23,5 +23,10 @@ public interface IDeltaSetItem /// Annotation container to hold Transient Instance Annotations /// IODataInstanceAnnotationContainer TransientInstanceAnnotationContainer { get; set; } + + /// + /// Container to hold ODataId + /// + ODataIdContainer ODataIdContainer { get; set; } } } diff --git a/src/Microsoft.AspNet.OData.Shared/IPatchMethodHandler.cs b/src/Microsoft.AspNet.OData.Shared/IODataAPIHandler.cs similarity index 62% rename from src/Microsoft.AspNet.OData.Shared/IPatchMethodHandler.cs rename to src/Microsoft.AspNet.OData.Shared/IODataAPIHandler.cs index d87ce24543..dc63ad5370 100644 --- a/src/Microsoft.AspNet.OData.Shared/IPatchMethodHandler.cs +++ b/src/Microsoft.AspNet.OData.Shared/IODataAPIHandler.cs @@ -4,11 +4,11 @@ namespace Microsoft.AspNet.OData { /// - /// Base Interface for PatchMethodHandler. - /// This is being implemented by PatchMethodHandler{TStructuralType} which has a method returning nested patchhandler. + /// 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 IPatchMethodHandler + public interface IODataAPIHandler { } diff --git a/src/Microsoft.AspNet.OData.Shared/KeyProperty.cs b/src/Microsoft.AspNet.OData.Shared/KeyProperty.cs new file mode 100644 index 0000000000..24b83e0966 --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/KeyProperty.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.OData +{ + /// + /// Class to hold Name and value of a key property + /// + public class KeyProperty + { + /// + /// Name of the Key Property + /// + public string Name { private set; get; } + + /// + /// Value of the Key Property + /// + public object Value { private set; get; } + } +} 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 413e19fb1c..eba9b8da57 100644 --- a/src/Microsoft.AspNet.OData.Shared/Microsoft.AspNet.OData.Shared.projitems +++ b/src/Microsoft.AspNet.OData.Shared/Microsoft.AspNet.OData.Shared.projitems @@ -85,10 +85,15 @@ - - - - + + + + + + + + + diff --git a/src/Microsoft.AspNet.OData.Shared/NavigationPath.cs b/src/Microsoft.AspNet.OData.Shared/NavigationPath.cs new file mode 100644 index 0000000000..d2f7a76997 --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/NavigationPath.cs @@ -0,0 +1,84 @@ +// 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.Concurrent; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using Microsoft.OData.UriParser; + +namespace Microsoft.AspNet.OData +{ + /// + /// Navigation Path of an OData ID + /// + public class NavigationPath + { + private string navigationPathName; + private ReadOnlyCollection _pathSegments; + private ConcurrentDictionary _pathItemCache = new ConcurrentDictionary(); + + /// + /// Constructor which takes and odataId and creates PathItems + /// + /// ODataId in string format + /// Pathsegment collection + public NavigationPath(string navigationPath, ReadOnlyCollection pathSegments) + { + navigationPathName = navigationPath; + _pathSegments = pathSegments; + } + + + /// + /// NavigationPath/ODataId in string + /// + public string NavigationPathName { get { return navigationPathName; } } + + /// + /// To Get ODataId in Parsed format + /// + /// Array of PathItems + public PathItem[] GetNavigationPathItems() + { + PathItem[] pathItems; + if(!_pathItemCache.TryGetValue(navigationPathName, out pathItems)) + { + if (_pathSegments != null) + { + pathItems = ParseODataId(); + _pathItemCache.TryAdd(navigationPathName, pathItems); + } + } + + return pathItems; + } + + private PathItem[] ParseODataId() + { + List pathItems = new List(); + PathItem currentPathItem = null; + + foreach (ODataPathSegment segment in _pathSegments) + { + if(segment is KeySegment keySegment) + { + currentPathItem.KeyProperties = new Dictionary(); + + foreach(KeyValuePair key in keySegment.Keys) + { + currentPathItem.KeyProperties.Add(key.Key, key.Value); + } + } + else + { + pathItems.Add(new PathItem()); + currentPathItem = pathItems.Last(); + currentPathItem.Name = segment.Identifier; + } + } + + return pathItems.ToArray(); + } + } +} diff --git a/src/Microsoft.AspNet.OData.Shared/PatchMethodHandler.cs b/src/Microsoft.AspNet.OData.Shared/ODataAPIHandler.cs similarity index 66% rename from src/Microsoft.AspNet.OData.Shared/PatchMethodHandler.cs rename to src/Microsoft.AspNet.OData.Shared/ODataAPIHandler.cs index 56bfe39121..739c568969 100644 --- a/src/Microsoft.AspNet.OData.Shared/PatchMethodHandler.cs +++ b/src/Microsoft.AspNet.OData.Shared/ODataAPIHandler.cs @@ -9,18 +9,18 @@ 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 PatchMethodHandler: IPatchMethodHandler where TStructuralType : class + public abstract class ODataAPIHandler: IODataAPIHandler where TStructuralType : class { /// /// TryCreate method to create a new object. /// - /// The Delta object to be patched over original object. Optional to patch + /// 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 + /// 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 PatchStatus TryCreate(Delta patchObject, out TStructuralType createdObject, out string errorMessage); + 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. @@ -28,23 +28,23 @@ public abstract class PatchMethodHandler: IPatchMethodHandler w /// 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 PatchStatus TryGet(IDictionary keyValues, out TStructuralType originalObject, out string errorMessage); + /// 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 PatchStatus TryDelete(IDictionary keyValues, out string errorMessage); + /// The status of the TryGet method + public abstract ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage); /// - /// Get the PatchHandler for the nested type + /// Get the ODataAPIHandler for the nested type /// /// Parent instance. /// The name of the navigation property for the handler - /// The type of Nested PatchMethodHandler - public abstract IPatchMethodHandler GetNestedPatchHandler(TStructuralType parent, string navigationPropertyName); + /// The type of Nested ODataAPIHandler + public abstract IODataAPIHandler GetNestedHandler(TStructuralType parent, string navigationPropertyName); } } diff --git a/src/Microsoft.AspNet.OData.Shared/ODataAPIHandlerFactory.cs b/src/Microsoft.AspNet.OData.Shared/ODataAPIHandlerFactory.cs new file mode 100644 index 0000000000..507536ba34 --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/ODataAPIHandlerFactory.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.OData +{ + /// + /// Factory class for ODataAPIHandlers + /// + public abstract class ODataAPIHandlerFactory + { + /// + /// Get the handler depending on navigationpath + /// + /// Navigation path corresponding to an odataid + /// + public abstract IODataAPIHandler GetHandler(NavigationPath navigationPath); + } +} diff --git a/src/Microsoft.AspNet.OData.Shared/PatchStatus.cs b/src/Microsoft.AspNet.OData.Shared/ODataAPIResponseStatus.cs similarity index 93% rename from src/Microsoft.AspNet.OData.Shared/PatchStatus.cs rename to src/Microsoft.AspNet.OData.Shared/ODataAPIResponseStatus.cs index 16f9a48319..5b24274d38 100644 --- a/src/Microsoft.AspNet.OData.Shared/PatchStatus.cs +++ b/src/Microsoft.AspNet.OData.Shared/ODataAPIResponseStatus.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNet.OData /// /// Enum for Patch Status /// - public enum PatchStatus + public enum ODataAPIResponseStatus { /// /// Success Status diff --git a/src/Microsoft.AspNet.OData.Shared/ODataIdContainer.cs b/src/Microsoft.AspNet.OData.Shared/ODataIdContainer.cs new file mode 100644 index 0000000000..75f397cb9f --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/ODataIdContainer.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.OData +{ + /// + /// Sealed class to hold ODataID in parsed format, it will be used by POCO objects as well as Delta{TStructuralType} + /// + public sealed class ODataIdContainer + { + /// + /// The Navigation path corresponding to the ODataId + /// + public NavigationPath ODataIdNavigationPath { set; get; } + } +} diff --git a/src/Microsoft.AspNet.OData.Shared/PathItem.cs b/src/Microsoft.AspNet.OData.Shared/PathItem.cs new file mode 100644 index 0000000000..0e91fbf7e8 --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/PathItem.cs @@ -0,0 +1,23 @@ +// 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; + +namespace Microsoft.AspNet.OData +{ + /// + /// Class to hold the PathItem Name and its keyproperties + /// + public class PathItem + { + /// + /// Name of the Path Item (eg: entity name, entity set name) + /// + public string Name { internal set; get; } + + /// + /// List of Key properties of that entity + /// + public Dictionary KeyProperties { internal set; get; } + } +} diff --git a/src/Microsoft.AspNet.OData/GlobalSuppressions.cs b/src/Microsoft.AspNet.OData/GlobalSuppressions.cs index 6582c7bd5f..a44f75c687 100644 --- a/src/Microsoft.AspNet.OData/GlobalSuppressions.cs +++ b/src/Microsoft.AspNet.OData/GlobalSuppressions.cs @@ -71,18 +71,18 @@ [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.PatchMethodHandler`1.#TryGet(System.Collections.Generic.IDictionary`2,!0&,System.String&)")] -[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.PatchMethodHandler`1.#TryDelete(System.Collections.Generic.IDictionary`2,System.String&)")] -[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.PatchMethodHandler`1.#TryGet(System.Collections.Generic.IDictionary`2,!0&,System.String&)")] -[assembly: SuppressMessage("Microsoft.Design", "CA1040:AvoidEmptyInterfaces", Scope = "type", Target = "Microsoft.AspNet.OData.IPatchMethodHandler")] +[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.EdmPatchMethodHandler.#TryDelete(System.Collections.Generic.IDictionary`2,System.String&)")] -[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.EdmPatchMethodHandler.#TryGet(System.Collections.Generic.IDictionary`2,Microsoft.AspNet.OData.EdmStructuredObject&,System.String&)")] +[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.EdmPatchMethodHandler.#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.EdmPatchMethodHandler.#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.EdmPatchMethodHandler.#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.EdmPatchMethodHandler.#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.#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")] 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 index 604e00774f..068f95f331 100644 --- 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 @@ -43,7 +43,7 @@ protected override void OnModelCreating(Microsoft.EntityFrameworkCore.ModelBuild } - public class EmployeeEFPatchHandler : PatchMethodHandler + public class EmployeeEFPatchHandler : ODataAPIHandler { EmployeeDBContext dbContext = null; @@ -52,7 +52,7 @@ public EmployeeEFPatchHandler(EmployeeDBContext dbContext) this.dbContext = dbContext; } - public override PatchStatus TryCreate(Delta deltaObject, out Employee createdObject, out string errorMessage) + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out Employee createdObject, out string errorMessage) { createdObject = null; errorMessage = string.Empty; @@ -62,17 +62,17 @@ public override PatchStatus TryCreate(Delta deltaObject, out Employee createdObject = new Employee(); dbContext.Employees.Add(createdObject); - return PatchStatus.Success; + return ODataAPIResponseStatus.Success; } catch (Exception ex) { errorMessage = ex.Message; - return PatchStatus.Failure; + return ODataAPIResponseStatus.Failure; } } - public override PatchStatus TryDelete(IDictionary keyValues, out string errorMessage) + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) { errorMessage = string.Empty; @@ -83,19 +83,19 @@ public override PatchStatus TryDelete(IDictionary keyValues, out dbContext.Employees.Remove(customer); - return PatchStatus.Success; + return ODataAPIResponseStatus.Success; } catch (Exception ex) { errorMessage = ex.Message; - return PatchStatus.Failure; + return ODataAPIResponseStatus.Failure; } } - public override PatchStatus TryGet(IDictionary keyValues, out Employee originalObject, out string errorMessage) + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out Employee originalObject, out string errorMessage) { - PatchStatus status = PatchStatus.Success; + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; errorMessage = string.Empty; originalObject = null; @@ -107,20 +107,20 @@ public override PatchStatus TryGet(IDictionary keyValues, out Em if (originalObject == null) { - status = PatchStatus.NotFound; + status = ODataAPIResponseStatus.NotFound; } } catch (Exception ex) { - status = PatchStatus.Failure; + status = ODataAPIResponseStatus.Failure; errorMessage = ex.Message; } return status; } - public override IPatchMethodHandler GetNestedPatchHandler(Employee parent, string navigationPropertyName) + public override IODataAPIHandler GetNestedHandler(Employee parent, string navigationPropertyName) { switch (navigationPropertyName) { @@ -136,7 +136,7 @@ public override IPatchMethodHandler GetNestedPatchHandler(Employee parent, strin } - public class FriendEFPatchHandler : PatchMethodHandler + public class FriendEFPatchHandler : ODataAPIHandler { Employee employee; public FriendEFPatchHandler(Employee employee) @@ -144,7 +144,7 @@ public FriendEFPatchHandler(Employee employee) this.employee = employee; } - public override PatchStatus TryCreate(Delta deltaObject, out Friend createdObject, out string errorMessage) + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out Friend createdObject, out string errorMessage) { createdObject = null; errorMessage = string.Empty; @@ -154,17 +154,17 @@ public override PatchStatus TryCreate(Delta deltaObject, out Friend crea createdObject = new Friend(); employee.Friends.Add(createdObject); - return PatchStatus.Success; + return ODataAPIResponseStatus.Success; } catch (Exception ex) { errorMessage = ex.Message; - return PatchStatus.Failure; + return ODataAPIResponseStatus.Failure; } } - public override PatchStatus TryDelete(IDictionary keyValues, out string errorMessage) + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) { errorMessage = string.Empty; @@ -175,19 +175,19 @@ public override PatchStatus TryDelete(IDictionary keyValues, out employee.Friends.Remove(friend); - return PatchStatus.Success; + return ODataAPIResponseStatus.Success; } catch (Exception ex) { errorMessage = ex.Message; - return PatchStatus.Failure; + return ODataAPIResponseStatus.Failure; } } - public override PatchStatus TryGet(IDictionary keyValues, out Friend originalObject, out string errorMessage) + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out Friend originalObject, out string errorMessage) { - PatchStatus status = PatchStatus.Success; + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; errorMessage = string.Empty; originalObject = null; @@ -196,7 +196,7 @@ public override PatchStatus TryGet(IDictionary keyValues, out Fr var id = keyValues["Id"].ToString(); if (employee.Friends == null) { - status = PatchStatus.NotFound; + status = ODataAPIResponseStatus.NotFound; } else { @@ -206,20 +206,20 @@ public override PatchStatus TryGet(IDictionary keyValues, out Fr if (originalObject == null) { - status = PatchStatus.NotFound; + status = ODataAPIResponseStatus.NotFound; } } catch (Exception ex) { - status = PatchStatus.Failure; + status = ODataAPIResponseStatus.Failure; errorMessage = ex.Message; } return status; } - public override IPatchMethodHandler GetNestedPatchHandler(Friend parent, string navigationPropertyName) + public override IODataAPIHandler GetNestedHandler(Friend parent, string navigationPropertyName) { return new OrderEFPatchHandler(parent); } @@ -227,7 +227,7 @@ public override IPatchMethodHandler GetNestedPatchHandler(Friend parent, string } - public class NewFriendEFPatchHandler : PatchMethodHandler + public class NewFriendEFPatchHandler : ODataAPIHandler { Employee employee; public NewFriendEFPatchHandler(Employee employee) @@ -235,7 +235,7 @@ public NewFriendEFPatchHandler(Employee employee) this.employee = employee; } - public override PatchStatus TryCreate(Delta deltaObject, out NewFriend createdObject, out string errorMessage) + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out NewFriend createdObject, out string errorMessage) { createdObject = null; errorMessage = string.Empty; @@ -245,17 +245,17 @@ public override PatchStatus TryCreate(Delta deltaObject, out NewFrien createdObject = new NewFriend(); employee.NewFriends.Add(createdObject); - return PatchStatus.Success; + return ODataAPIResponseStatus.Success; } catch (Exception ex) { errorMessage = ex.Message; - return PatchStatus.Failure; + return ODataAPIResponseStatus.Failure; } } - public override PatchStatus TryDelete(IDictionary keyValues, out string errorMessage) + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) { errorMessage = string.Empty; @@ -266,19 +266,19 @@ public override PatchStatus TryDelete(IDictionary keyValues, out employee.NewFriends.Remove(friend); - return PatchStatus.Success; + return ODataAPIResponseStatus.Success; } catch (Exception ex) { errorMessage = ex.Message; - return PatchStatus.Failure; + return ODataAPIResponseStatus.Failure; } } - public override PatchStatus TryGet(IDictionary keyValues, out NewFriend originalObject, out string errorMessage) + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out NewFriend originalObject, out string errorMessage) { - PatchStatus status = PatchStatus.Success; + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; errorMessage = string.Empty; originalObject = null; @@ -290,20 +290,20 @@ public override PatchStatus TryGet(IDictionary keyValues, out Ne if (originalObject == null) { - status = PatchStatus.NotFound; + status = ODataAPIResponseStatus.NotFound; } } catch (Exception ex) { - status = PatchStatus.Failure; + status = ODataAPIResponseStatus.Failure; errorMessage = ex.Message; } return status; } - public override IPatchMethodHandler GetNestedPatchHandler(NewFriend parent, string navigationPropertyName) + public override IODataAPIHandler GetNestedHandler(NewFriend parent, string navigationPropertyName) { return null; } @@ -312,7 +312,7 @@ public override IPatchMethodHandler GetNestedPatchHandler(NewFriend parent, stri - public class OrderEFPatchHandler : PatchMethodHandler + public class OrderEFPatchHandler : ODataAPIHandler { Friend friend; public OrderEFPatchHandler(Friend friend) @@ -320,7 +320,7 @@ public OrderEFPatchHandler(Friend friend) this.friend = friend; } - public override PatchStatus TryCreate(Delta deltaObject, out Order createdObject, out string errorMessage) + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out Order createdObject, out string errorMessage) { createdObject = null; errorMessage = string.Empty; @@ -330,17 +330,17 @@ public override PatchStatus TryCreate(Delta deltaObject, out Order create createdObject = new Order(); friend.Orders.Add(createdObject); - return PatchStatus.Success; + return ODataAPIResponseStatus.Success; } catch (Exception ex) { errorMessage = ex.Message; - return PatchStatus.Failure; + return ODataAPIResponseStatus.Failure; } } - public override PatchStatus TryDelete(IDictionary keyValues, out string errorMessage) + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) { errorMessage = string.Empty; @@ -351,19 +351,19 @@ public override PatchStatus TryDelete(IDictionary keyValues, out friend.Orders.Remove(order); - return PatchStatus.Success; + return ODataAPIResponseStatus.Success; } catch (Exception ex) { errorMessage = ex.Message; - return PatchStatus.Failure; + return ODataAPIResponseStatus.Failure; } } - public override PatchStatus TryGet(IDictionary keyValues, out Order originalObject, out string errorMessage) + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out Order originalObject, out string errorMessage) { - PatchStatus status = PatchStatus.Success; + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; errorMessage = string.Empty; originalObject = null; @@ -375,20 +375,20 @@ public override PatchStatus TryGet(IDictionary keyValues, out Or if (originalObject == null) { - status = PatchStatus.NotFound; + status = ODataAPIResponseStatus.NotFound; } } catch (Exception ex) { - status = PatchStatus.Failure; + status = ODataAPIResponseStatus.Failure; errorMessage = ex.Message; } return status; } - public override IPatchMethodHandler GetNestedPatchHandler(Order parent, string navigationPropertyName) + public override IODataAPIHandler GetNestedHandler(Order parent, string navigationPropertyName) { throw new NotImplementedException(); } diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertController.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertController.cs index 2ce91d15e8..bc7ab6b5c1 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertController.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertController.cs @@ -32,12 +32,12 @@ public EmployeesController() public static IList EmployeesTypeless = null; - private List Friends = null; + private List Friends = null; private void InitEmployees() { - Friends = new List { new Friend { Id = 1, Name = "Test0" }, 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" } }; + Friends = new List { new Friend { Id = 1, Name = "Test0" }, 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 { @@ -48,7 +48,7 @@ private void InitEmployees() SkillSet=new List{Skill.CSharp,Skill.Sql}, Gender=Gender.Female, AccessLevel=AccessLevel.Execute, - + 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() @@ -57,7 +57,7 @@ private void InitEmployees() 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 } } } }, Friends = this.Friends.Where(x=>x.Id ==3 || x.Id==4).ToList() }, new Employee(){ @@ -65,7 +65,7 @@ private void InitEmployees() SkillSet=new List{Skill.Web,Skill.Sql}, Gender=Gender.Female, AccessLevel=AccessLevel.Read|AccessLevel.Write - + }, }; } @@ -131,29 +131,29 @@ private void InitTypeLessEmployees(IEdmEntityType entityType) public DeltaSet PatchWithUsersMethod(DeltaSet friendColl, Employee employee) { - var changedObjColl = friendColl.Patch(new NewFriendPatchHandler(employee)); + var changedObjColl = friendColl.Patch(new NewFriendAPIHandler(employee)); return changedObjColl; } public EdmChangedObjectCollection PatchWithUsersMethodTypeLess(int key, EdmChangedObjectCollection friendColl) { - - var entity = Request.GetModel().FindDeclaredType("Microsoft.Test.E2E.AspNet.OData.BulkInsert.UnTypedEmployee") as IEdmEntityType; + + var entity = Request.GetModel().FindDeclaredType("Microsoft.Test.E2E.AspNet.OData.BulkInsert.UnTypedEmployee") as IEdmEntityType; InitTypeLessEmployees(entity); var entity1 = Request.GetModel().FindDeclaredType("Microsoft.Test.E2E.AspNet.OData.BulkInsert.UnTypedFriend") as IEdmEntityType; - var changedObjColl = friendColl.Patch(new FriendTypelessPatchHandler(EmployeesTypeless[key-1], entity1)); + var changedObjColl = friendColl.Patch(new FriendTypelessAPIHandler(EmployeesTypeless[key - 1], entity1)); return changedObjColl; } public EdmChangedObjectCollection EmployeePatchMethodTypeLess(EdmChangedObjectCollection empColl) - { + { var entity = Request.GetModel().FindDeclaredType("Microsoft.Test.E2E.AspNet.OData.BulkInsert.UnTypedEmployee") as IEdmEntityType; InitTypeLessEmployees(entity); - var changedObjColl = empColl.Patch(new EmployeeEdmPatchHandler(entity)); + var changedObjColl = empColl.Patch(new EmployeeEdmAPIHandler(entity)); ValidateSuccessfulTypeless(); return changedObjColl; @@ -172,7 +172,7 @@ private void ValidateSuccessfulTypeless() friends.First().TryGetPropertyValue("Name", out obj1); Assert.Equal("Friend1", obj1.ToString()); - + } [EnableQuery(PageSize = 10, MaxExpansionDepth = 5)] @@ -205,9 +205,9 @@ public ITestActionResult GetUnTypedFriends(int key) object obj; emp.TryGetPropertyValue("ID", out obj); - if(Equals(key, obj)) + if (Equals(key, obj)) { - object friends ; + object friends; emp.TryGetPropertyValue("UntypedFriends", out friends); return Ok(friends); } @@ -223,13 +223,13 @@ public ITestActionResult PatchEmployees([FromBody] DeltaSet coll) InitEmployees(); Assert.NotNull(coll); - - var returncoll = coll.Patch(new EmployeePatchHandler()); + + var returncoll = coll.Patch(new EmployeeAPIHandler()); return Ok(returncoll); } - + [ODataRoute("Employees({key})/Friends")] [HttpPatch] public ITestActionResult PatchFriends(int key, [FromBody] DeltaSet friendColl) @@ -253,7 +253,7 @@ public ITestActionResult PatchNewFriends(int key, [FromBody] DeltaSet if (key == 1) { - var deltaSet = PatchWithUsersMethod(friendColl, Employees.First(x=>x.ID ==key)); + var deltaSet = PatchWithUsersMethod(friendColl, Employees.First(x => x.ID == key)); return Ok(deltaSet); } @@ -267,7 +267,7 @@ public ITestActionResult PatchNewFriends(int key, [FromBody] DeltaSet return Ok(changedObjColl); } - + } [ODataRoute("Employees({key})/UnTypedFriends")] @@ -286,7 +286,7 @@ public ITestActionResult PatchUnTypedFriends(int key, [FromBody] EdmChangedObjec if (lst != null && lst.Count > 1) { object obj1; - if(lst[1].TryGetPropertyValue("Name", out obj1) && Equals("Friend007", obj1)) + if (lst[1].TryGetPropertyValue("Name", out obj1) && Equals("Friend007", obj1)) { lst[1].TryGetPropertyValue("Address", out obj1); Assert.NotNull(obj1); @@ -294,13 +294,13 @@ public ITestActionResult PatchUnTypedFriends(int key, [FromBody] EdmChangedObjec (obj1 as EdmStructuredObject).TryGetPropertyValue("Street", out obj2); Assert.Equal("Abc 123", obj2); - + } } - + return Ok(changedObjColl); } - else if(key ==2) + else if (key == 2) { var entitytype = Request.GetModel().FindDeclaredType("Microsoft.Test.E2E.AspNet.OData.BulkInsert.UnTypedEmployee") as IEdmEntityType; var entity = new EdmEntityObject(friendColl[0].GetEdmType().AsEntity()); @@ -331,11 +331,11 @@ public ITestActionResult PatchUnTypedFriends(int key, [FromBody] EdmChangedObjec [HttpPatch] public ITestActionResult PatchUnTypedEmployees([FromBody] EdmChangedObjectCollection empColl) { - + var changedObjColl = EmployeePatchMethodTypeLess(empColl); return Ok(changedObjColl); - + } @@ -348,19 +348,19 @@ public ITestActionResult Patch(int key, [FromBody] Delta delta) 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 EmployeePatchHandler()); + delta.Patch(employee, new EmployeeAPIHandler()); return Created(employee); } try { - delta.Patch(employee, new EmployeePatchHandler()); + delta.Patch(employee, new EmployeeAPIHandler()); - if(employee.Name == "Bind1") + if (employee.Name == "Bind1") { Assert.NotNull(employee.Friends.Single(x => x.Id == 3)); } @@ -375,4 +375,133 @@ public ITestActionResult Patch(int key, [FromBody] Delta delta) } + + public class CompanyController : TestODataController + { + public static IList Companies = null; + public static IList OverdueOrders = null; + + public CompanyController() + { + if (null == Companies) + { + InitCompanies(); + } + } + + private void InitCompanies() + { + OverdueOrders = new List() { new NewOrder { Id = 1, Price = 10 }, new NewOrder { Id = 2, Price = 20 }, new NewOrder { Id = 3, Price = 30 }, new NewOrder { Id = 4, Price = 40 } }; + + Companies = new List() { new Company { Id = 1, Name = "Company1", OverdueOrders = OverdueOrders.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) + { + var empCntrl = new EmployeesController(); + InitCompanies(); + + Assert.NotNull(coll); + + var returncoll = coll.Patch(new CompanyAPIHandler(), new APIHandlerFactory()); + + ValidateOverdueOrders1(1,1); + + return Ok(returncoll); + } + + [ODataRoute("Companies")] + [HttpPost] + public ITestActionResult Post([FromBody] Company company) + { + + InitCompanies(); + InitEmployees(); + + MapOdataId(company); + + Companies.Add(company); + + ValidateOverdueOrders1(3,1); + + return Ok(company); + } + + private void MapOdataId(Company company) + { + for(int i =0; i< company.OverdueOrders.Count;i++) + { + var order = company.OverdueOrders[i]; + if(order.Container != null) + { + var pathItems = order.Container.ODataIdNavigationPath.GetNavigationPathItems(); + + int cnt = 0; + if(pathItems[cnt].Name== "Employees") + { + var emp = GetEmployee(pathItems[cnt].KeyProperties); + + if(emp != null) + { + if(pathItems[++cnt].Name == "NewFriends") + { + var frnd = GetNewFriendFromEmployee(emp, pathItems[cnt].KeyProperties); + + if(frnd!= null) + { + if (pathItems[++cnt].Name == "NewOrders") + { + company.OverdueOrders[i] = GetNewOrderFromNewFriend(frnd, pathItems[cnt].KeyProperties); + } + } + } + } + } + + } + } + } + + + private Employee GetEmployee(Dictionary keyValues) + { + var emp = EmployeesController.Employees.FirstOrDefault(x => x.ID == (int)keyValues["ID"]); + + return emp; + } + + private NewFriend GetNewFriendFromEmployee(Employee emp, Dictionary keyValues) + { + var frnd = emp.NewFriends.FirstOrDefault(x => x.Id == (int)keyValues["Id"]); + + return frnd; + } + + private NewOrder GetNewOrderFromNewFriend(NewFriend frnd, Dictionary keyValues) + { + var order = frnd.NewOrders.FirstOrDefault(x => x.Id == (int)keyValues["Id"]); + + return order; + } + + private void InitEmployees() + { + var cntrl = new EmployeesController(); + } + + private void ValidateOverdueOrders1(int companyId, int orderId) + { + 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(1, order.Id); + Assert.Equal(101, order.Price); + } + } } \ No newline at end of file diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertDataModel.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertDataModel.cs index a3ebc663d9..0fff205c23 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertDataModel.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertDataModel.cs @@ -5,6 +5,7 @@ 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.BulkInsert @@ -87,6 +88,48 @@ public class NewFriend 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 ODataIdContainer Container { get; set; } + } + + public class NewOrder + { + [Key] + public int Id { get; set; } + + public int Price { get; set; } + + public ODataIdContainer Container {get;set;} + } + + + public class Company + { + [Key] + public int Id { get; set; } + + public string Name { get; set; } + + public List OverdueOrders { get; set; } } public class UnTypedEmployee diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertEdmModel.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertEdmModel.cs index d8f535e981..0623d52eda 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertEdmModel.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertEdmModel.cs @@ -65,6 +65,9 @@ public static IEdmModel GetConventionModel(WebRouteConfiguration configuration) EntitySetConfiguration unemployees = builder.EntitySet("UnTypedEmployees"); EntityTypeConfiguration unemployee = unemployees.EntityType; + EntitySetConfiguration companies = builder.EntitySet("Companies"); + EntitySetConfiguration overdueorders = builder.EntitySet("OverdueOrders"); + // 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); diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertTest.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertTest.cs index fd29728e16..d3ae4ab2ad 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertTest.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertTest.cs @@ -31,7 +31,7 @@ public BulkInsertTest(WebHostTestFixture fixture) protected override void UpdateConfiguration(WebRouteConfiguration configuration) { - var controllers = new[] { typeof(EmployeesController), typeof(MetadataController) }; + var controllers = new[] { typeof(EmployeesController), typeof(CompanyController), typeof(MetadataController) }; configuration.AddControllers(controllers); configuration.Routes.Clear(); @@ -56,6 +56,11 @@ public async Task PatchEmployee_WithUpdates() 'Friends@odata.delta':[{'Id':1,'Name':'Test2'},{'Id':2,'Name':'Test3'}] }"; + content = @"{ + 'Name':'Sql' , + 'Friends@odata.delta':[{'@odata.id':'Employees(1)/Friends(1)'},{'Id':2,'Name':'Test3'}] + }"; + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); @@ -570,7 +575,7 @@ public async Task PatchEmployee_WithUpdates_Employees() string requestUri = this.BaseAddress + "/convention/Employees"; - var content = @"{'@odata.context':'http://host/service/$metadata#Employees/$delta', + var content = @"{'@odata.context':'"+ this.BaseAddress + @"/convention/$metadata#Employees/$delta', 'value':[{ '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkInsert.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'}] @@ -804,6 +809,65 @@ public async Task PatchEmployee_WithMultipleUpdatesinOrder2() } + [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.BulkInsert.Company', 'Id':1,'Name':'Company01', + 'OverdueOrders@odata.delta':[{'@odata.id':'Employees(1)/NewFriends(1)/NewOrders(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)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + + } + + + [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); + } + + } + + + #endregion } diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationPatchHandlers.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationPatchHandlers.cs index e2bbfceea5..8443808435 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationPatchHandlers.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationPatchHandlers.cs @@ -10,9 +10,242 @@ namespace Microsoft.Test.E2E.AspNet.OData.BulkOperation { - public class EmployeePatchHandler : PatchMethodHandler + public class APIHandlerFactory : ODataAPIHandlerFactory { - public override PatchStatus TryCreate(Delta patchObject, out Employee createdObject, out string errorMessage) + public override IODataAPIHandler GetHandler(NavigationPath navigationPath) + { + if(navigationPath != null) + { + var pathItems = navigationPath.GetNavigationPathItems(); + int cnt = 0; + + switch (pathItems[cnt].Name) + { + case "Employees": + { + Employee employee; + string msg; + if ((new EmployeeAPIHandler().TryGet(pathItems[cnt].KeyProperties, out employee, out msg)) == ODataAPIResponseStatus.Success) + { + return GetNestedHandlerForEmployee(pathItems, cnt, employee); + } + } + return null; + + default: + return null; + + } + + } + + return null; + } + + private static IODataAPIHandler GetNestedHandlerForEmployee(PathItem[] pathItems, int cnt, Employee employee) + { + switch (pathItems[++cnt].Name) + { + case "NewFriends": + NewFriend friend = employee.NewFriends.FirstOrDefault(x => x.Id == (int)pathItems[cnt].KeyProperties["Id"]); + + if (friend != null) + { + switch (pathItems[++cnt].Name) + { + case "NewOrders": + return new NewOrderAPIHandler(friend); + + default: + return null; + + } + } + return null; + + default: + return null; + + } + } + } + + public 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); + default: + return null; + } + + } + } + + public 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.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(NewOrder parent, string navigationPropertyName) + { + switch (navigationPropertyName) + { + + default: + return null; + } + + } + } + + + public class EmployeeAPIHandler : ODataAPIHandler + { + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out Employee createdObject, out string errorMessage) { createdObject = null; errorMessage = string.Empty; @@ -22,17 +255,17 @@ public override PatchStatus TryCreate(Delta patchObject, out Employee createdObject = new Employee(); EmployeesController.Employees.Add(createdObject); - return PatchStatus.Success; + return ODataAPIResponseStatus.Success; } catch (Exception ex) { errorMessage = ex.Message; - return PatchStatus.Failure; + return ODataAPIResponseStatus.Failure; } } - public override PatchStatus TryDelete(IDictionary keyValues, out string errorMessage) + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) { errorMessage = string.Empty; @@ -43,19 +276,19 @@ public override PatchStatus TryDelete(IDictionary keyValues, out EmployeesController.Employees.Remove(customer); - return PatchStatus.Success; + return ODataAPIResponseStatus.Success; } catch (Exception ex) { errorMessage = ex.Message; - return PatchStatus.Failure; + return ODataAPIResponseStatus.Failure; } } - public override PatchStatus TryGet(IDictionary keyValues, out Employee originalObject, out string errorMessage) + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out Employee originalObject, out string errorMessage) { - PatchStatus status = PatchStatus.Success; + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; errorMessage = string.Empty; originalObject = null; @@ -67,27 +300,27 @@ public override PatchStatus TryGet(IDictionary keyValues, out Em if (originalObject == null) { - status = PatchStatus.NotFound; + status = ODataAPIResponseStatus.NotFound; } } catch (Exception ex) { - status = PatchStatus.Failure; + status = ODataAPIResponseStatus.Failure; errorMessage = ex.Message; } return status; } - public override IPatchMethodHandler GetNestedPatchHandler(Employee parent, string navigationPropertyName) + public override IODataAPIHandler GetNestedHandler(Employee parent, string navigationPropertyName) { switch (navigationPropertyName) { case "Friends": - return new FriendPatchHandler(parent); + return new FriendAPIHandler(parent); case "NewFriends": - return new NewFriendPatchHandler(parent); + return new NewFriendAPIHandler(parent); default: return null; } @@ -95,15 +328,15 @@ public override IPatchMethodHandler GetNestedPatchHandler(Employee parent, strin } } - public class FriendPatchHandler : PatchMethodHandler + public class FriendAPIHandler : ODataAPIHandler { Employee employee; - public FriendPatchHandler(Employee employee) + public FriendAPIHandler(Employee employee) { this.employee = employee; } - public override PatchStatus TryCreate(Delta patchObject, out Friend createdObject, out string errorMessage) + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out Friend createdObject, out string errorMessage) { createdObject = null; errorMessage = string.Empty; @@ -113,17 +346,17 @@ public override PatchStatus TryCreate(Delta patchObject, out Friend crea createdObject = new Friend(); employee.Friends.Add(createdObject); - return PatchStatus.Success; + return ODataAPIResponseStatus.Success; } catch (Exception ex) { errorMessage = ex.Message; - return PatchStatus.Failure; + return ODataAPIResponseStatus.Failure; } } - public override PatchStatus TryDelete(IDictionary keyValues, out string errorMessage) + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) { errorMessage = string.Empty; @@ -134,19 +367,19 @@ public override PatchStatus TryDelete(IDictionary keyValues, out employee.Friends.Remove(friend); - return PatchStatus.Success; + return ODataAPIResponseStatus.Success; } catch (Exception ex) { errorMessage = ex.Message; - return PatchStatus.Failure; + return ODataAPIResponseStatus.Failure; } } - public override PatchStatus TryGet(IDictionary keyValues, out Friend originalObject, out string errorMessage) + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out Friend originalObject, out string errorMessage) { - PatchStatus status = PatchStatus.Success; + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; errorMessage = string.Empty; originalObject = null; @@ -158,25 +391,25 @@ public override PatchStatus TryGet(IDictionary keyValues, out Fr if (originalObject == null) { - status = PatchStatus.NotFound; + status = ODataAPIResponseStatus.NotFound; } } catch (Exception ex) { - status = PatchStatus.Failure; + status = ODataAPIResponseStatus.Failure; errorMessage = ex.Message; } return status; } - public override IPatchMethodHandler GetNestedPatchHandler(Friend parent, string navigationPropertyName) + public override IODataAPIHandler GetNestedHandler(Friend parent, string navigationPropertyName) { switch (navigationPropertyName) { case "Orders": - return new OrderPatchHandler(parent); + return new OrderAPIHandler(parent); default: return null; @@ -185,16 +418,108 @@ public override IPatchMethodHandler GetNestedPatchHandler(Friend parent, string } + public 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(); + } + + } + + - public class OrderPatchHandler : PatchMethodHandler + public class OrderAPIHandler : ODataAPIHandler { Friend friend; - public OrderPatchHandler(Friend friend) + public OrderAPIHandler(Friend friend) { this.friend = friend; } - public override PatchStatus TryCreate(Delta patchObject, out Order createdObject, out string errorMessage) + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out Order createdObject, out string errorMessage) { createdObject = null; errorMessage = string.Empty; @@ -210,17 +535,17 @@ public override PatchStatus TryCreate(Delta patchObject, out Order create friend.Orders.Add(createdObject); - return PatchStatus.Success; + return ODataAPIResponseStatus.Success; } catch (Exception ex) { errorMessage = ex.Message; - return PatchStatus.Failure; + return ODataAPIResponseStatus.Failure; } } - public override PatchStatus TryDelete(IDictionary keyValues, out string errorMessage) + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) { errorMessage = string.Empty; @@ -231,19 +556,19 @@ public override PatchStatus TryDelete(IDictionary keyValues, out this.friend.Orders.Remove(friend); - return PatchStatus.Success; + return ODataAPIResponseStatus.Success; } catch (Exception ex) { errorMessage = ex.Message; - return PatchStatus.Failure; + return ODataAPIResponseStatus.Failure; } } - public override PatchStatus TryGet(IDictionary keyValues, out Order originalObject, out string errorMessage) + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out Order originalObject, out string errorMessage) { - PatchStatus status = PatchStatus.Success; + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; errorMessage = string.Empty; originalObject = null; @@ -257,20 +582,20 @@ public override PatchStatus TryGet(IDictionary keyValues, out Or if (originalObject == null) { - status = PatchStatus.NotFound; + status = ODataAPIResponseStatus.NotFound; } } catch (Exception ex) { - status = PatchStatus.Failure; + status = ODataAPIResponseStatus.Failure; errorMessage = ex.Message; } return status; } - public override IPatchMethodHandler GetNestedPatchHandler(Order parent, string navigationPropertyName) + public override IODataAPIHandler GetNestedHandler(Order parent, string navigationPropertyName) { throw new NotImplementedException(); } @@ -279,15 +604,15 @@ public override IPatchMethodHandler GetNestedPatchHandler(Order parent, string n - public class NewFriendPatchHandler : PatchMethodHandler + public class NewFriendAPIHandler : ODataAPIHandler { Employee employee; - public NewFriendPatchHandler(Employee employee) + public NewFriendAPIHandler(Employee employee) { this.employee = employee; } - public override PatchStatus TryCreate(Delta patchObject, out NewFriend createdObject, out string errorMessage) + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out NewFriend createdObject, out string errorMessage) { createdObject = null; errorMessage = string.Empty; @@ -303,17 +628,17 @@ public override PatchStatus TryCreate(Delta patchObject, out NewFrien employee.NewFriends.Add(createdObject); - return PatchStatus.Success; + return ODataAPIResponseStatus.Success; } catch (Exception ex) { errorMessage = ex.Message; - return PatchStatus.Failure; + return ODataAPIResponseStatus.Failure; } } - public override PatchStatus TryDelete(IDictionary keyValues, out string errorMessage) + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) { errorMessage = string.Empty; @@ -324,19 +649,19 @@ public override PatchStatus TryDelete(IDictionary keyValues, out employee.NewFriends.Remove(friend); - return PatchStatus.Success; + return ODataAPIResponseStatus.Success; } catch (Exception ex) { errorMessage = ex.Message; - return PatchStatus.Failure; + return ODataAPIResponseStatus.Failure; } } - public override PatchStatus TryGet(IDictionary keyValues, out NewFriend originalObject, out string errorMessage) + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out NewFriend originalObject, out string errorMessage) { - PatchStatus status = PatchStatus.Success; + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; errorMessage = string.Empty; originalObject = null; @@ -346,7 +671,7 @@ public override PatchStatus TryGet(IDictionary keyValues, out Ne if(employee.NewFriends == null) { - return PatchStatus.NotFound; + return ODataAPIResponseStatus.NotFound; } originalObject = employee.NewFriends.FirstOrDefault(x => x.Id == Int32.Parse(id)); @@ -354,20 +679,20 @@ public override PatchStatus TryGet(IDictionary keyValues, out Ne if (originalObject == null) { - status = PatchStatus.NotFound; + status = ODataAPIResponseStatus.NotFound; } } catch (Exception ex) { - status = PatchStatus.Failure; + status = ODataAPIResponseStatus.Failure; errorMessage = ex.Message; } return status; } - public override IPatchMethodHandler GetNestedPatchHandler(NewFriend parent, string navigationPropertyName) + public override IODataAPIHandler GetNestedHandler(NewFriend parent, string navigationPropertyName) { throw new NotImplementedException(); } @@ -375,15 +700,15 @@ public override IPatchMethodHandler GetNestedPatchHandler(NewFriend parent, stri } - public class EmployeeEdmPatchHandler : EdmPatchMethodHandler + public class EmployeeEdmAPIHandler : EdmODataAPIHandler { IEdmEntityType entityType; - public EmployeeEdmPatchHandler(IEdmEntityType entityType) + public EmployeeEdmAPIHandler(IEdmEntityType entityType) { this.entityType = entityType; } - public override PatchStatus TryCreate(IEdmChangedObject changedObject, out IEdmStructuredObject createdObject, out string errorMessage) + public override ODataAPIResponseStatus TryCreate(IEdmChangedObject changedObject, out IEdmStructuredObject createdObject, out string errorMessage) { createdObject = null; errorMessage = string.Empty; @@ -393,17 +718,17 @@ public override PatchStatus TryCreate(IEdmChangedObject changedObject, out IEdmS createdObject = new EdmEntityObject(entityType); EmployeesController.EmployeesTypeless.Add(createdObject as EdmStructuredObject); - return PatchStatus.Success; + return ODataAPIResponseStatus.Success; } catch (Exception ex) { errorMessage = ex.Message; - return PatchStatus.Failure; + return ODataAPIResponseStatus.Failure; } } - public override PatchStatus TryDelete(IDictionary keyValues, out string errorMessage) + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) { errorMessage = string.Empty; @@ -423,19 +748,19 @@ public override PatchStatus TryDelete(IDictionary keyValues, out } - return PatchStatus.Success; + return ODataAPIResponseStatus.Success; } catch (Exception ex) { errorMessage = ex.Message; - return PatchStatus.Failure; + return ODataAPIResponseStatus.Failure; } } - public override PatchStatus TryGet(IDictionary keyValues, out IEdmStructuredObject originalObject, out string errorMessage) + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out IEdmStructuredObject originalObject, out string errorMessage) { - PatchStatus status = PatchStatus.Success; + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; errorMessage = string.Empty; originalObject = null; @@ -457,25 +782,25 @@ public override PatchStatus TryGet(IDictionary keyValues, out IE if (originalObject == null) { - status = PatchStatus.NotFound; + status = ODataAPIResponseStatus.NotFound; } } catch (Exception ex) { - status = PatchStatus.Failure; + status = ODataAPIResponseStatus.Failure; errorMessage = ex.Message; } return status; } - public override EdmPatchMethodHandler GetNestedPatchHandler(IEdmStructuredObject parent, string navigationPropertyName) + public override EdmODataAPIHandler GetNestedHandler(IEdmStructuredObject parent, string navigationPropertyName) { switch (navigationPropertyName) { case "UnTypedFriends": - return new FriendTypelessPatchHandler(parent, entityType.DeclaredNavigationProperties().First().Type.Definition.AsElementType() as IEdmEntityType); + return new FriendTypelessAPIHandler(parent, entityType.DeclaredNavigationProperties().First().Type.Definition.AsElementType() as IEdmEntityType); default: return null; @@ -485,18 +810,18 @@ public override EdmPatchMethodHandler GetNestedPatchHandler(IEdmStructuredObject } - public class FriendTypelessPatchHandler : EdmPatchMethodHandler + public class FriendTypelessAPIHandler : EdmODataAPIHandler { IEdmEntityType entityType; EdmStructuredObject employee; - public FriendTypelessPatchHandler(IEdmStructuredObject employee, IEdmEntityType entityType) + public FriendTypelessAPIHandler(IEdmStructuredObject employee, IEdmEntityType entityType) { this.employee = employee as EdmStructuredObject; this.entityType = entityType; } - public override PatchStatus TryCreate(IEdmChangedObject changedObject, out IEdmStructuredObject createdObject, out string errorMessage) + public override ODataAPIResponseStatus TryCreate(IEdmChangedObject changedObject, out IEdmStructuredObject createdObject, out string errorMessage) { createdObject = null; errorMessage = string.Empty; @@ -524,17 +849,17 @@ public override PatchStatus TryCreate(IEdmChangedObject changedObject, out IEdmS employee.TrySetPropertyValue("UnTypedFriends", friends); - return PatchStatus.Success; + return ODataAPIResponseStatus.Success; } catch (Exception ex) { errorMessage = ex.Message; - return PatchStatus.Failure; + return ODataAPIResponseStatus.Failure; } } - public override PatchStatus TryDelete(IDictionary keyValues, out string errorMessage) + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) { errorMessage = string.Empty; @@ -566,19 +891,19 @@ public override PatchStatus TryDelete(IDictionary keyValues, out } - return PatchStatus.Success; + return ODataAPIResponseStatus.Success; } catch (Exception ex) { errorMessage = ex.Message; - return PatchStatus.Failure; + return ODataAPIResponseStatus.Failure; } } - public override PatchStatus TryGet(IDictionary keyValues, out IEdmStructuredObject originalObject, out string errorMessage) + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out IEdmStructuredObject originalObject, out string errorMessage) { - PatchStatus status = PatchStatus.Success; + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; errorMessage = string.Empty; originalObject = null; @@ -592,7 +917,7 @@ public override PatchStatus TryGet(IDictionary keyValues, out IE if(friends == null) { - return PatchStatus.NotFound; + return ODataAPIResponseStatus.NotFound; } foreach (var friend in friends) @@ -610,20 +935,20 @@ public override PatchStatus TryGet(IDictionary keyValues, out IE if (originalObject == null) { - status = PatchStatus.NotFound; + status = ODataAPIResponseStatus.NotFound; } } catch (Exception ex) { - status = PatchStatus.Failure; + status = ODataAPIResponseStatus.Failure; errorMessage = ex.Message; } return status; } - public override EdmPatchMethodHandler GetNestedPatchHandler(IEdmStructuredObject parent, string navigationPropertyName) + public override EdmODataAPIHandler GetNestedHandler(IEdmStructuredObject parent, string navigationPropertyName) { return null; } diff --git a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/DeltaSetOfTTest.cs b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/DeltaSetOfTTest.cs index e9b38d70af..c113b542b8 100644 --- a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/DeltaSetOfTTest.cs +++ b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/DeltaSetOfTTest.cs @@ -218,42 +218,42 @@ public void DeltaSet_Patch_WithNestedDelta() } - public class FriendPatchHandler : PatchMethodHandler + public class FriendPatchHandler : ODataAPIHandler { - public override IPatchMethodHandler GetNestedPatchHandler(Friend parent, string navigationPropertyName) + public override IODataAPIHandler GetNestedHandler(Friend parent, string navigationPropertyName) { return new NewFriendPatchHandler(parent); } - public override PatchStatus TryCreate(Delta deltaFriend, out Friend createdObject, out string errorMessage) + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out Friend createdObject, out string errorMessage) { createdObject = new Friend(); DeltaSetOfTTest.friends.Add(createdObject); errorMessage = string.Empty; - return PatchStatus.Success; + return ODataAPIResponseStatus.Success; } - public override PatchStatus TryDelete(IDictionary keyValues, out string errorMessage) + 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 PatchStatus.Success; + return ODataAPIResponseStatus.Success; } - public override PatchStatus TryGet(IDictionary keyValues, out Friend originalObject, out string errorMessage) + 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 PatchStatus.Success; + return ODataAPIResponseStatus.Success; } } - public class NewFriendPatchHandler : PatchMethodHandler + public class NewFriendPatchHandler : ODataAPIHandler { Friend parent; public NewFriendPatchHandler(Friend parent) @@ -261,12 +261,12 @@ public NewFriendPatchHandler(Friend parent) this.parent = parent; } - public override IPatchMethodHandler GetNestedPatchHandler(NewFriend parent, string navigationPropertyName) + public override IODataAPIHandler GetNestedHandler(NewFriend parent, string navigationPropertyName) { throw new NotImplementedException(); } - public override PatchStatus TryCreate(Delta deltaFriend, out NewFriend createdObject, out string errorMessage) + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out NewFriend createdObject, out string errorMessage) { createdObject = new NewFriend(); if(parent.NewFriends == null) @@ -276,34 +276,34 @@ public override PatchStatus TryCreate(Delta deltaFriend, out NewFrien parent.NewFriends.Add(createdObject); errorMessage = string.Empty; - return PatchStatus.Success; + return ODataAPIResponseStatus.Success; } - public override PatchStatus TryDelete(IDictionary keyValues, out string errorMessage) + 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 PatchStatus.Success; + return ODataAPIResponseStatus.Success; } - public override PatchStatus TryGet(IDictionary keyValues, out NewFriend originalObject, out string errorMessage) + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out NewFriend originalObject, out string errorMessage) { errorMessage = string.Empty; originalObject = null; if(parent.NewFriends == null) { - return PatchStatus.NotFound; + 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? PatchStatus.Success : PatchStatus.NotFound; + 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 175ccb037c..f237414683 100644 --- a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/EdmChangedObjectCollectionTest.cs +++ b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/EdmChangedObjectCollectionTest.cs @@ -213,7 +213,7 @@ public class NewFriend public string Name { get; set; } } - public class FriendTypelessPatchHandler : EdmPatchMethodHandler + public class FriendTypelessPatchHandler : EdmODataAPIHandler { IEdmEntityType entityType; @@ -222,7 +222,7 @@ public FriendTypelessPatchHandler(IEdmEntityType entityType) this.entityType = entityType; } - public override PatchStatus TryCreate(IEdmChangedObject changedObject, out IEdmStructuredObject createdObject, out string errorMessage) + public override ODataAPIResponseStatus TryCreate(IEdmChangedObject changedObject, out IEdmStructuredObject createdObject, out string errorMessage) { createdObject = null; errorMessage = string.Empty; @@ -233,17 +233,17 @@ public override PatchStatus TryCreate(IEdmChangedObject changedObject, out IEdmS EdmChangedObjectCollectionTest.friends.Add(createdObject); - return PatchStatus.Success; + return ODataAPIResponseStatus.Success; } catch (Exception ex) { errorMessage = ex.Message; - return PatchStatus.Failure; + return ODataAPIResponseStatus.Failure; } } - public override PatchStatus TryDelete(IDictionary keyValues, out string errorMessage) + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) { errorMessage = string.Empty; @@ -265,19 +265,19 @@ public override PatchStatus TryDelete(IDictionary keyValues, out } - return PatchStatus.Success; + return ODataAPIResponseStatus.Success; } catch (Exception ex) { errorMessage = ex.Message; - return PatchStatus.Failure; + return ODataAPIResponseStatus.Failure; } } - public override PatchStatus TryGet(IDictionary keyValues, out IEdmStructuredObject originalObject, out string errorMessage) + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out IEdmStructuredObject originalObject, out string errorMessage) { - PatchStatus status = PatchStatus.Success; + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; errorMessage = string.Empty; originalObject = null; @@ -300,20 +300,20 @@ public override PatchStatus TryGet(IDictionary keyValues, out IE if (originalObject == null) { - status = PatchStatus.NotFound; + status = ODataAPIResponseStatus.NotFound; } } catch (Exception ex) { - status = PatchStatus.Failure; + status = ODataAPIResponseStatus.Failure; errorMessage = ex.Message; } return status; } - public override EdmPatchMethodHandler GetNestedPatchHandler(IEdmStructuredObject parent, string navigationPropertyName) + public override EdmODataAPIHandler GetNestedHandler(IEdmStructuredObject parent, string navigationPropertyName) { switch (navigationPropertyName) { @@ -326,7 +326,7 @@ public override EdmPatchMethodHandler GetNestedPatchHandler(IEdmStructuredObject } - public class NewFriendTypelessPatchHandler : EdmPatchMethodHandler + public class NewFriendTypelessPatchHandler : EdmODataAPIHandler { IEdmEntityType entityType; EdmStructuredObject friend; @@ -337,7 +337,7 @@ public NewFriendTypelessPatchHandler(IEdmStructuredObject friend, IEdmEntityType this.friend = friend as EdmStructuredObject; } - public override PatchStatus TryCreate(IEdmChangedObject changedObject, out IEdmStructuredObject createdObject, out string errorMessage) + public override ODataAPIResponseStatus TryCreate(IEdmChangedObject changedObject, out IEdmStructuredObject createdObject, out string errorMessage) { createdObject = null; errorMessage = string.Empty; @@ -355,17 +355,17 @@ public override PatchStatus TryCreate(IEdmChangedObject changedObject, out IEdmS friend.TrySetPropertyValue("NewFriends", nfriends); - return PatchStatus.Success; + return ODataAPIResponseStatus.Success; } catch (Exception ex) { errorMessage = ex.Message; - return PatchStatus.Failure; + return ODataAPIResponseStatus.Failure; } } - public override PatchStatus TryDelete(IDictionary keyValues, out string errorMessage) + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) { errorMessage = string.Empty; @@ -392,19 +392,19 @@ public override PatchStatus TryDelete(IDictionary keyValues, out } - return PatchStatus.Success; + return ODataAPIResponseStatus.Success; } catch (Exception ex) { errorMessage = ex.Message; - return PatchStatus.Failure; + return ODataAPIResponseStatus.Failure; } } - public override PatchStatus TryGet(IDictionary keyValues, out IEdmStructuredObject originalObject, out string errorMessage) + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out IEdmStructuredObject originalObject, out string errorMessage) { - PatchStatus status = PatchStatus.Success; + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; errorMessage = string.Empty; originalObject = null; @@ -434,20 +434,20 @@ public override PatchStatus TryGet(IDictionary keyValues, out IE if (originalObject == null) { - status = PatchStatus.NotFound; + status = ODataAPIResponseStatus.NotFound; } } catch (Exception ex) { - status = PatchStatus.Failure; + status = ODataAPIResponseStatus.Failure; errorMessage = ex.Message; } return status; } - public override EdmPatchMethodHandler GetNestedPatchHandler(IEdmStructuredObject parent, string navigationPropertyName) + public override EdmODataAPIHandler GetNestedHandler(IEdmStructuredObject parent, string navigationPropertyName) { return null; } From 9286d9845eeda69f6f92a8b884771981253d5659 Mon Sep 17 00:00:00 2001 From: Sreejith Pazhampilly Date: Thu, 26 Aug 2021 14:56:23 -0700 Subject: [PATCH 07/28] updates --- .../DeltaSetOfT.cs | 8 - .../EdmChangedObjectCollection.cs | 54 ++- .../Deserialization/DeserializationHelpers.cs | 39 +++ .../ODataResourceDeserializer.cs | 32 +- .../Microsoft.AspNet.OData.Shared.projitems | 5 +- .../NavigationPath.cs | 20 +- .../ODataEdmAPIHandlerFactory.cs | 18 + src/Microsoft.AspNet.OData.Shared/PathItem.cs | 10 + .../BulkOperation/BulkInsertController.cs | 58 +++- .../BulkOperation/BulkInsertDataModel.cs | 6 + .../BulkOperation/BulkInsertEdmModel.cs | 2 + .../BulkOperation/BulkInsertTest.cs | 67 +++- .../BulkOperationPatchHandlers.cs | 326 +++++++++++++++++- 13 files changed, 579 insertions(+), 66 deletions(-) create mode 100644 src/Microsoft.AspNet.OData.Shared/ODataEdmAPIHandlerFactory.cs diff --git a/src/Microsoft.AspNet.OData.Shared/DeltaSetOfT.cs b/src/Microsoft.AspNet.OData.Shared/DeltaSetOfT.cs index a3921c3a0f..a0e06278aa 100644 --- a/src/Microsoft.AspNet.OData.Shared/DeltaSetOfT.cs +++ b/src/Microsoft.AspNet.OData.Shared/DeltaSetOfT.cs @@ -94,14 +94,6 @@ internal DeltaSet CopyChangedValues(ODataAPIHandler keyValues = new Dictionary(); foreach (string key in _keys) diff --git a/src/Microsoft.AspNet.OData.Shared/EdmChangedObjectCollection.cs b/src/Microsoft.AspNet.OData.Shared/EdmChangedObjectCollection.cs index a103223d37..ae47208b2d 100644 --- a/src/Microsoft.AspNet.OData.Shared/EdmChangedObjectCollection.cs +++ b/src/Microsoft.AspNet.OData.Shared/EdmChangedObjectCollection.cs @@ -9,6 +9,7 @@ using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics; using System.Diagnostics.Contracts; using System.Linq; using System.Net.Http.Headers; @@ -90,16 +91,16 @@ internal EdmChangedObjectCollection Patch(ICollection orig /// Patch for EdmChangedObjectCollection, a collection for IEdmChangedObject /// /// ChangedObjectCollection response - public EdmChangedObjectCollection Patch(EdmODataAPIHandler apiHandler) + public EdmChangedObjectCollection Patch(EdmODataAPIHandler apiHandler, ODataEdmAPIHandlerFactory apiHandlerFactory = null) { - return CopyChangedValues(apiHandler); + 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) + internal EdmChangedObjectCollection CopyChangedValues(EdmODataAPIHandler apiHandler, ODataEdmAPIHandlerFactory apiHandlerFactory = null) { EdmChangedObjectCollection changedObjectCollection = new EdmChangedObjectCollection(_entityType); IEdmStructuralProperty[] keys = _entityType.Key().ToArray(); @@ -133,6 +134,8 @@ internal EdmChangedObjectCollection CopyChangedValues(EdmODataAPIHandler apiHand } } + PatchItem(deletedObj, original as EdmStructuredObject, apiHandler, apiHandlerFactory); + changedObjectCollection.Add(deletedObj); } else @@ -166,7 +169,7 @@ internal EdmChangedObjectCollection CopyChangedValues(EdmODataAPIHandler apiHand } //Patch for addition/update. - PatchItem(deltaEntityObject, original as EdmStructuredObject, apiHandler); + PatchItem(deltaEntityObject, original as EdmStructuredObject, apiHandler, apiHandlerFactory); changedObjectCollection.Add(changedObj); } @@ -202,20 +205,49 @@ private static IDictionary GetKeyValues(IEdmStructuralProperty [ return keyValues; } - private void PatchItem(EdmStructuredObject changedObj, EdmStructuredObject originalObj, EdmODataAPIHandler apiHandler) + private void PatchItem(EdmStructuredObject changedObj, EdmStructuredObject originalObj, EdmODataAPIHandler apiHandler, ODataEdmAPIHandlerFactory apiHandlerFactory = null) { + if(apiHandlerFactory!= null && changedObj is EdmEntityObject entityObject && entityObject.ODataIdContainer != null) + { + ApplyODataId(entityObject.ODataIdContainer, originalObj, apiHandlerFactory); + } + foreach (string propertyName in changedObj.GetChangedPropertyNames()) { - ApplyProperties(changedObj, originalObj, propertyName, apiHandler); + ApplyProperties(changedObj, originalObj, propertyName, apiHandler, apiHandlerFactory); + } + } + + private void ApplyODataId(ODataIdContainer container, EdmStructuredObject original, ODataEdmAPIHandlerFactory apiHandlerFactory) + { + EdmODataAPIHandler edmApiHandler = apiHandlerFactory.GetHandler(container.ODataIdNavigationPath); + + if(edmApiHandler == null) + { + return; } - foreach (string propertyName in changedObj.GetUnchangedPropertyNames()) + IEdmStructuredObject referencedObj; + string error; + + if (edmApiHandler.TryGet(container.ODataIdNavigationPath.GetNavigationPathItems().Last().KeyProperties, out referencedObj, out error) == ODataAPIResponseStatus.Success) { - ApplyProperties(changedObj, originalObj, propertyName, apiHandler); + EdmStructuredObject structuredObj = referencedObj as EdmStructuredObject; + + foreach (string propertyName in structuredObj.GetChangedPropertyNames()) + { + ApplyProperties(referencedObj as EdmStructuredObject, original, propertyName, edmApiHandler, apiHandlerFactory); + } + + foreach (string propertyName in structuredObj.GetUnchangedPropertyNames()) + { + ApplyProperties(referencedObj as EdmStructuredObject, original, propertyName, edmApiHandler, apiHandlerFactory); + } } } - private void ApplyProperties(EdmStructuredObject changedObj, EdmStructuredObject originalObj, string propertyName, EdmODataAPIHandler apiHandler) + + private void ApplyProperties(EdmStructuredObject changedObj, EdmStructuredObject originalObj, string propertyName, EdmODataAPIHandler apiHandler, ODataEdmAPIHandlerFactory apiHandlerFactory = null) { object value; if (changedObj.TryGetPropertyValue(propertyName, out value)) @@ -226,7 +258,7 @@ private void ApplyProperties(EdmStructuredObject changedObj, EdmStructuredObject EdmODataAPIHandler apiHandlerNested = apiHandler.GetNestedHandler(originalObj, propertyName); if (apiHandlerNested != null) { - changedColl.Patch(apiHandlerNested); + changedColl.Patch(apiHandlerNested, apiHandlerFactory); } else { @@ -264,7 +296,7 @@ private void ApplyProperties(EdmStructuredObject changedObj, EdmStructuredObject originalObj.TrySetPropertyValue(propertyName, origStructuredObj); } - PatchItem(structuredObj, origStructuredObj, apiHandler); + PatchItem(structuredObj, origStructuredObj, apiHandler, apiHandlerFactory); } else { diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/DeserializationHelpers.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/DeserializationHelpers.cs index 10a0e1dc72..cd6378a176 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/DeserializationHelpers.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/DeserializationHelpers.cs @@ -126,6 +126,45 @@ internal static void ApplyInstanceAnnotations(object resource, IEdmStructuredTyp 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, transientAnnotationContainer, deserializerProvider, readContext); + } + internal static void SetDynamicProperty(object resource, IEdmStructuredTypeReference resourceType, EdmTypeKind propertyKind, string propertyName, object propertyValue, IEdmTypeReference propertyType, IEdmModel model) diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializer.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializer.cs index 3e500f0e56..e8ce2c6055 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializer.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializer.cs @@ -572,26 +572,26 @@ private static void ApplyODataIDContainer(object resource, ODataResourceWrapper ODataPath odataPath = GetODataPath(odataId, readContext); - PropertyInfo containerPropertyInfo = EdmLibHelpers.GetClrType(odataPath.EdmType, readContext.Model).GetProperties().Where(x => x.PropertyType == typeof(ODataIdContainer)).FirstOrDefault(); - if (containerPropertyInfo != null) - { - ODataIdContainer container = new ODataIdContainer(); + ODataIdContainer container = new ODataIdContainer(); - NavigationPath navigationPath = new NavigationPath(odataId, odataPath.Segments); - container.ODataIdNavigationPath = navigationPath; + NavigationPath navigationPath = new NavigationPath(odataId, odataPath.Segments); + container.ODataIdNavigationPath = navigationPath; - if(resource is EdmEntityObject edmObject) - { - edmObject.ODataIdContainer = container; - } - else if(resource is IDeltaSetItem deltasetItem) - { - deltasetItem.ODataIdContainer = container; - } - else + if (resource is EdmEntityObject edmObject) + { + edmObject.ODataIdContainer = container; + } + else if (resource is IDeltaSetItem deltasetItem) + { + deltasetItem.ODataIdContainer = container; + } + else + { + PropertyInfo containerPropertyInfo = EdmLibHelpers.GetClrType(odataPath.EdmType, readContext.Model).GetProperties().Where(x => x.PropertyType == typeof(ODataIdContainer)).FirstOrDefault(); + if (containerPropertyInfo != null) { containerPropertyInfo.SetValue(resource, container); - } + } } } } 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 eba9b8da57..eb6fe93476 100644 --- a/src/Microsoft.AspNet.OData.Shared/Microsoft.AspNet.OData.Shared.projitems +++ b/src/Microsoft.AspNet.OData.Shared/Microsoft.AspNet.OData.Shared.projitems @@ -86,12 +86,13 @@ - + + - + diff --git a/src/Microsoft.AspNet.OData.Shared/NavigationPath.cs b/src/Microsoft.AspNet.OData.Shared/NavigationPath.cs index d2f7a76997..889a8e0bc9 100644 --- a/src/Microsoft.AspNet.OData.Shared/NavigationPath.cs +++ b/src/Microsoft.AspNet.OData.Shared/NavigationPath.cs @@ -61,7 +61,18 @@ private PathItem[] ParseODataId() foreach (ODataPathSegment segment in _pathSegments) { - if(segment is KeySegment keySegment) + if (segment is EntitySetSegment || segment is NavigationPropertySegment) + { + pathItems.Add(new PathItem()); + currentPathItem = pathItems.Last(); + currentPathItem.Name = segment.Identifier; + } + else if(segment is TypeSegment) + { + currentPathItem.IsCastType = true; + currentPathItem.CastTypeName = segment.Identifier; + } + else if (segment is KeySegment keySegment) { currentPathItem.KeyProperties = new Dictionary(); @@ -70,12 +81,7 @@ private PathItem[] ParseODataId() currentPathItem.KeyProperties.Add(key.Key, key.Value); } } - else - { - pathItems.Add(new PathItem()); - currentPathItem = pathItems.Last(); - currentPathItem.Name = segment.Identifier; - } + } return pathItems.ToArray(); diff --git a/src/Microsoft.AspNet.OData.Shared/ODataEdmAPIHandlerFactory.cs b/src/Microsoft.AspNet.OData.Shared/ODataEdmAPIHandlerFactory.cs new file mode 100644 index 0000000000..1a772d192a --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/ODataEdmAPIHandlerFactory.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.OData +{ + /// + /// Factory class for ODataAPIHandlers for typeless entities + /// + public abstract class ODataEdmAPIHandlerFactory + { + /// + /// Get the handler depending on navigationpath + /// + /// Navigation path corresponding to an odataid + /// + public abstract EdmODataAPIHandler GetHandler(NavigationPath navigationPath); + } +} diff --git a/src/Microsoft.AspNet.OData.Shared/PathItem.cs b/src/Microsoft.AspNet.OData.Shared/PathItem.cs index 0e91fbf7e8..aa47fcf535 100644 --- a/src/Microsoft.AspNet.OData.Shared/PathItem.cs +++ b/src/Microsoft.AspNet.OData.Shared/PathItem.cs @@ -15,6 +15,16 @@ public class PathItem /// public string Name { internal set; get; } + /// + /// Whether the PathItem is a cast segment + /// + public bool IsCastType { internal set; get; } + + /// + /// If the item is a cast segment, Name of the Path Item (eg: derived entity name, entity set name) + /// + public string CastTypeName { internal set; get; } + /// /// List of Key properties of that entity /// diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertController.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertController.cs index bc7ab6b5c1..f27d3a6b2f 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertController.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertController.cs @@ -37,7 +37,7 @@ public EmployeesController() private void InitEmployees() { - Friends = new List { new Friend { Id = 1, Name = "Test0" }, 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" } }; + 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 { @@ -57,7 +57,7 @@ private void InitEmployees() 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 } } } }, + 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(){ @@ -82,6 +82,7 @@ private void InitTypeLessEmployees(IEdmEntityType entityType) 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); @@ -153,7 +154,7 @@ public EdmChangedObjectCollection EmployeePatchMethodTypeLess(EdmChangedObjectCo var entity = Request.GetModel().FindDeclaredType("Microsoft.Test.E2E.AspNet.OData.BulkInsert.UnTypedEmployee") as IEdmEntityType; InitTypeLessEmployees(entity); - var changedObjColl = empColl.Patch(new EmployeeEdmAPIHandler(entity)); + var changedObjColl = empColl.Patch(new EmployeeEdmAPIHandler(entity), new TypelessAPIHandlerFactory(entity)); ValidateSuccessfulTypeless(); return changedObjColl; @@ -171,7 +172,20 @@ private void ValidateSuccessfulTypeless() friends.First().TryGetPropertyValue("Name", out obj1); - Assert.Equal("Friend1", obj1.ToString()); + 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()); + } } @@ -380,6 +394,7 @@ public class CompanyController : TestODataController { public static IList Companies = null; public static IList OverdueOrders = null; + public static IList MyOverdueOrders = null; public CompanyController() { @@ -391,9 +406,10 @@ public CompanyController() private void InitCompanies() { - OverdueOrders = new List() { new NewOrder { Id = 1, Price = 10 }, new NewOrder { Id = 2, Price = 20 }, new NewOrder { Id = 3, Price = 30 }, new NewOrder { Id = 4, Price = 40 } }; + 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() } , + 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() } }; } @@ -409,8 +425,21 @@ public ITestActionResult PatchCompanies([FromBody] DeltaSet coll) var returncoll = coll.Patch(new CompanyAPIHandler(), new APIHandlerFactory()); - ValidateOverdueOrders1(1,1); + 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); } @@ -493,7 +522,7 @@ private void InitEmployees() var cntrl = new EmployeesController(); } - private void ValidateOverdueOrders1(int companyId, int orderId) + private void ValidateOverdueOrders1(int companyId, int orderId, int quantity = 0) { var comp = Companies.FirstOrDefault(x => x.Id == companyId); Assert.NotNull(comp); @@ -502,6 +531,19 @@ private void ValidateOverdueOrders1(int companyId, int orderId) Assert.NotNull(order); Assert.Equal(1, order.Id); Assert.Equal(101, 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/BulkInsertDataModel.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertDataModel.cs index 0fff205c23..76cbcb9301 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertDataModel.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertDataModel.cs @@ -108,6 +108,8 @@ public class MyNewOrder public int Price { get; set; } + public int Quantity { get; set; } + public ODataIdContainer Container { get; set; } } @@ -118,6 +120,8 @@ public class NewOrder public int Price { get; set; } + public int Quantity { get; set; } + public ODataIdContainer Container {get;set;} } @@ -130,6 +134,8 @@ public class Company public string Name { get; set; } public List OverdueOrders { get; set; } + + public List MyOverdueOrders { get; set; } } public class UnTypedEmployee diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertEdmModel.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertEdmModel.cs index 0623d52eda..88fa414c05 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertEdmModel.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertEdmModel.cs @@ -67,6 +67,8 @@ public static IEdmModel GetConventionModel(WebRouteConfiguration configuration) 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. diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertTest.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertTest.cs index d3ae4ab2ad..15ad2d6e07 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertTest.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertTest.cs @@ -405,7 +405,7 @@ public async Task PatchUntypedEmployee_WithAdds_Friends_Untyped() 'value':[{ 'Id':3, 'Age':35,}] }"; - content = @"{'@odata.context':'http://host/service/$metadata#UnTypedEmployees/$delta', + 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'}] }, @@ -414,6 +414,7 @@ public async Task PatchUntypedEmployee_WithAdds_Friends_Untyped() }] }"; + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); @@ -428,6 +429,7 @@ public async Task PatchUntypedEmployee_WithAdds_Friends_Untyped() } } + [Fact] public async Task PatchEmployee_WithAdds_Friends_WithNested_Untyped() { @@ -818,11 +820,12 @@ public async Task PatchCompanies_WithUpdates_ODataId() var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Companies/$delta', 'value':[{ '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkInsert.Company', 'Id':1,'Name':'Company01', - 'OverdueOrders@odata.delta':[{'@odata.id':'Employees(1)/NewFriends(1)/NewOrders(1)'}] + 'OverdueOrders@odata.delta':[{'@odata.id':'Employees(1)/NewFriends(1)/NewOrders(1)', 'Quantity': 9}] }] }"; + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); @@ -838,6 +841,66 @@ public async Task PatchCompanies_WithUpdates_ODataId() } + [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.BulkInsert.Company', 'Id':1,'Name':'Company02', + 'MyOverdueOrders@odata.delta':[{'@odata.id':'Employees(2)/NewFriends(2)/Microsoft.Test.E2E.AspNet.OData.BulkInsert.MyNewFriend/MyNewOrders(2)', 'Quantity': 9}] + + }] + }"; + + + 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 = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + + } + + + [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); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPost.Content = stringContent; + Client.DefaultRequestHeaders.Add("Prefer", @"odata.include-annotations=""*"""); + + //Act & Assert + 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 PostCompany_WithODataId() diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationPatchHandlers.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationPatchHandlers.cs index 8443808435..d72b537595 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationPatchHandlers.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationPatchHandlers.cs @@ -22,10 +22,10 @@ public override IODataAPIHandler GetHandler(NavigationPath navigationPath) switch (pathItems[cnt].Name) { case "Employees": - { - Employee employee; - string msg; - if ((new EmployeeAPIHandler().TryGet(pathItems[cnt].KeyProperties, out employee, out msg)) == ODataAPIResponseStatus.Success) + { + Employee employee; + string msg; + if ((new EmployeeAPIHandler().TryGet(pathItems[cnt].KeyProperties, out employee, out msg)) == ODataAPIResponseStatus.Success) { return GetNestedHandlerForEmployee(pathItems, cnt, employee); } @@ -47,20 +47,140 @@ private static IODataAPIHandler GetNestedHandlerForEmployee(PathItem[] pathItems switch (pathItems[++cnt].Name) { case "NewFriends": - NewFriend friend = employee.NewFriends.FirstOrDefault(x => x.Id == (int)pathItems[cnt].KeyProperties["Id"]); + if (pathItems[cnt].IsCastType) + { + if (pathItems[cnt].CastTypeName == "Microsoft.Test.E2E.AspNet.OData.BulkInsert.MyNewFriend") + { + MyNewFriend friend = employee.NewFriends.FirstOrDefault(x => x.Id == (int)pathItems[cnt].KeyProperties["Id"]) as MyNewFriend; + + if (friend != null) + { + switch (pathItems[++cnt].Name) + { + case "MyNewOrders": + return new MyNewOrderAPIHandler(friend); + + default: + return null; + + } + } + } + } + else + { + NewFriend friend = employee.NewFriends.FirstOrDefault(x => x.Id == (int)pathItems[cnt].KeyProperties["Id"]); + + if (friend != null) + { + switch (pathItems[++cnt].Name) + { + case "NewOrders": + return new NewOrderAPIHandler(friend); + + default: + return null; + + } + } + } + + return null; + + default: + return null; + + } + } + } + + public class TypelessAPIHandlerFactory : ODataEdmAPIHandlerFactory + { + IEdmEntityType entityType; + public TypelessAPIHandlerFactory(IEdmEntityType entityType) + { + this.entityType = entityType; + } + + public override EdmODataAPIHandler GetHandler(NavigationPath navigationPath) + { + if (navigationPath != null) + { + var pathItems = navigationPath.GetNavigationPathItems(); + int cnt = 0; + + switch (pathItems[cnt].Name) + { + case "UnTypedEmployees": + { + IEdmStructuredObject employee; + string msg; + if ((new EmployeeEdmAPIHandler(entityType).TryGet(pathItems[cnt].KeyProperties, out employee, out msg)) == ODataAPIResponseStatus.Success) + { + cnt++; + + if (cnt x.Id == (int)pathItems[cnt].KeyProperties["Id"]) as MyNewFriend; - if (friend != null) + if (friend != null) + { + switch (pathItems[++cnt].Name) + { + case "MyNewOrders": + return new MyNewOrderAPIHandler(friend); + + default: + return null; + + } + } + } + } + else { - switch (pathItems[++cnt].Name) + NewFriend friend = employee.NewFriends.FirstOrDefault(x => x.Id == (int)pathItems[cnt].KeyProperties["Id"]); + + if (friend != null) { - case "NewOrders": - return new NewOrderAPIHandler(friend); + switch (pathItems[++cnt].Name) + { + case "NewOrders": + return new NewOrderAPIHandler(friend); - default: - return null; + default: + return null; + } } } + return null; default: @@ -70,6 +190,7 @@ private static IODataAPIHandler GetNestedHandlerForEmployee(PathItem[] pathItems } } + public class CompanyAPIHandler : ODataAPIHandler { public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out Company createdObject, out string errorMessage) @@ -146,6 +267,8 @@ public override IODataAPIHandler GetNestedHandler(Company parent, string navigat { case "OverdueOrders": return new OverdueOrderAPIHandler(parent); + case "MyOverdueOrders": + return new MyOverdueOrderAPIHandler(parent); default: return null; } @@ -212,7 +335,7 @@ public override ODataAPIResponseStatus TryGet(IDictionary keyVal try { var id = keyValues["Id"].ToString(); - originalObject = parent.OverdueOrders.First(x => x.Id == Int32.Parse(id)); + originalObject = parent.OverdueOrders.FirstOrDefault(x => x.Id == Int32.Parse(id)); if (originalObject == null) @@ -242,6 +365,95 @@ public override IODataAPIHandler GetNestedHandler(NewOrder parent, string naviga } } + public 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; + } + + } + } + public class EmployeeAPIHandler : ODataAPIHandler { @@ -509,6 +721,96 @@ public override IODataAPIHandler GetNestedHandler(NewOrder parent, string naviga } + public 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(); + } + + } public class OrderAPIHandler : ODataAPIHandler From 0f85f265c5afedc7fe803955909ae1c88107c52a Mon Sep 17 00:00:00 2001 From: Sreejith Pazhampilly Date: Fri, 27 Aug 2021 14:02:52 -0700 Subject: [PATCH 08/28] updates to use only factory --- .../DeltaSetOfT.cs | 27 ++- .../EdmChangedObjectCollection.cs | 26 ++- .../ODataResourceDeserializer.cs | 79 +++++---- .../EFTests/BulkOperationControllerEF.cs | 4 +- .../EFTests/BulkOperationPatchHandlersEF.cs | 155 +++++++++++------- .../BulkOperation/BulkInsertController.cs | 10 +- .../BulkOperation/BulkInsertTest.cs | 34 ++-- .../BulkOperationPatchHandlers.cs | 36 +++- .../DeltaSetOfTTest.cs | 30 +++- 9 files changed, 264 insertions(+), 137 deletions(-) diff --git a/src/Microsoft.AspNet.OData.Shared/DeltaSetOfT.cs b/src/Microsoft.AspNet.OData.Shared/DeltaSetOfT.cs index a0e06278aa..b0f913ae66 100644 --- a/src/Microsoft.AspNet.OData.Shared/DeltaSetOfT.cs +++ b/src/Microsoft.AspNet.OData.Shared/DeltaSetOfT.cs @@ -24,6 +24,7 @@ public class DeltaSet : Collection, IDeltaSet wh { private Type _clrType; IList _keys; + NavigationPath _navigationPath; /// /// Initializes a new instance of the class. @@ -32,7 +33,8 @@ public class DeltaSet : Collection, IDeltaSet wh public DeltaSet(IList keys) { _keys = keys; - _clrType = typeof(TStructuralType); + _clrType = typeof(TStructuralType); + _navigationPath = new NavigationPath(_clrType.Name, null); } @@ -44,7 +46,7 @@ protected override void InsertItem(int index, IDeltaSetItem item) //To ensure we dont insert null or a non related type to deltaset if (deltaItem == null) { - throw Error.Argument("item", SRResources.ChangedObjectTypeMismatch, item.GetType(), typeof(TStructuralType)); + throw Error.Argument("item", SRResources.ChangedObjectTypeMismatch, item.GetType(), _clrType); } base.InsertItem(index, item); @@ -68,8 +70,9 @@ public DeltaSet Patch(ICollection originalColl /// Patch for DeltaSet, a collection for Delta /// /// DeltaSet response - public DeltaSet Patch(IODataAPIHandler apiHandler, ODataAPIHandlerFactory apiHandlerFactory = null) + public DeltaSet Patch(ODataAPIHandlerFactory apiHandlerFactory = null) { + IODataAPIHandler apiHandler = apiHandlerFactory.GetHandler(_navigationPath); ODataAPIHandler apiHandlerOfT = apiHandler as ODataAPIHandler; Debug.Assert(apiHandlerOfT != null); @@ -78,11 +81,15 @@ public DeltaSet Patch(IODataAPIHandler apiHandler, ODataAPIHand [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] - internal DeltaSet CopyChangedValues(ODataAPIHandler apiHandler, ODataAPIHandlerFactory apiHandlerFactory = null) + 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) @@ -108,7 +115,7 @@ internal DeltaSet CopyChangedValues(ODataAPIHandler deletedObj = changedObj as DeltaDeletedEntityObject; @@ -121,6 +128,8 @@ internal DeltaSet CopyChangedValues(ODataAPIHandler CopyChangedValues(ODataAPIHandler CopyChangedValues(ODataAPIHandler CopyChangedValues(ODataAPIHandler for each item in the collection // This will work in case we use delegates for using users method to create an object - changedObj.Patch(original, apiHandler, apiHandlerFactory); + changedObj.CopyChangedValues(original, apiHandlerOfT, apiHandlerFactory); deltaSet.Add(changedObj); } diff --git a/src/Microsoft.AspNet.OData.Shared/EdmChangedObjectCollection.cs b/src/Microsoft.AspNet.OData.Shared/EdmChangedObjectCollection.cs index ae47208b2d..7b0a049c55 100644 --- a/src/Microsoft.AspNet.OData.Shared/EdmChangedObjectCollection.cs +++ b/src/Microsoft.AspNet.OData.Shared/EdmChangedObjectCollection.cs @@ -118,14 +118,29 @@ internal EdmChangedObjectCollection CopyChangedValues(EdmODataAPIHandler apiHand IEdmStructuredObject original = null; EdmDeltaDeletedEntityObject deletedObj = changedObj as EdmDeltaDeletedEntityObject; + ODataAPIResponseStatus ODataAPIResponseStatus = apiHandler.TryGet(keyValues, out original, out getErrorMessage); + + 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 - ODataAPIResponseStatus ODataAPIResponseStatus = apiHandler.TryGet(keyValues, out original, out getErrorMessage); + //Handle Failed Operation - Delete if (ODataAPIResponseStatus == ODataAPIResponseStatus.Success) { IEdmChangedObject changedObject = HandleFailedOperation(deletedObj, operation, original, keys, errorMessage, apiHandler); @@ -134,7 +149,7 @@ internal EdmChangedObjectCollection CopyChangedValues(EdmODataAPIHandler apiHand } } - PatchItem(deletedObj, original as EdmStructuredObject, apiHandler, apiHandlerFactory); + changedObjectCollection.Add(deletedObj); } @@ -142,8 +157,6 @@ internal EdmChangedObjectCollection CopyChangedValues(EdmODataAPIHandler apiHand { EdmEntityObject deltaEntityObject = changedObj as EdmEntityObject; - ODataAPIResponseStatus ODataAPIResponseStatus = apiHandler.TryGet(keyValues, out original, out getErrorMessage); - if (ODataAPIResponseStatus == ODataAPIResponseStatus.NotFound) { operation = DataModificationOperationKind.Insert; @@ -218,6 +231,9 @@ private void PatchItem(EdmStructuredObject changedObj, EdmStructuredObject origi } } + /// + /// This applies ODataId parsed Navigation paths, get the value identified by that and copy it on original object, for typeless entities + /// private void ApplyODataId(ODataIdContainer container, EdmStructuredObject original, ODataEdmAPIHandlerFactory apiHandlerFactory) { EdmODataAPIHandler edmApiHandler = apiHandlerFactory.GetHandler(container.ODataIdNavigationPath); diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializer.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializer.cs index e8ce2c6055..f379dc1654 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializer.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializer.cs @@ -521,10 +521,10 @@ private static IList CreateKeyProperties(Uri id, ODataDeserialize { return properties; } - - try + + ODataPath odataPath = GetODataPath(id.OriginalString, readContext); + if (odataPath?.Segments != null) { - ODataPath odataPath = GetODataPath(id.OriginalString, readContext); KeySegment keySegment = odataPath.Segments.OfType().LastOrDefault(); if (keySegment != null) @@ -538,28 +538,32 @@ private static IList CreateKeyProperties(Uri id, ODataDeserialize }); } } - - return properties; } - catch (Exception) - { - return properties; - } + + return properties; } private static ODataPath GetODataPath(string id, ODataDeserializerContext readContext) { - 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); - - return odataPath; + try + { + 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); + + + return odataPath; + } + catch (Exception) + { + return null; + } } private static void ApplyODataIDContainer(object resource, ODataResourceWrapper resourceWrapper, @@ -572,25 +576,28 @@ private static void ApplyODataIDContainer(object resource, ODataResourceWrapper ODataPath odataPath = GetODataPath(odataId, readContext); - ODataIdContainer container = new ODataIdContainer(); + if (odataPath != null) + { + ODataIdContainer container = new ODataIdContainer(); - NavigationPath navigationPath = new NavigationPath(odataId, odataPath.Segments); - container.ODataIdNavigationPath = navigationPath; + NavigationPath navigationPath = new NavigationPath(odataId, odataPath.Segments); + container.ODataIdNavigationPath = navigationPath; - if (resource is EdmEntityObject edmObject) - { - edmObject.ODataIdContainer = container; - } - else if (resource is IDeltaSetItem deltasetItem) - { - deltasetItem.ODataIdContainer = container; - } - else - { - PropertyInfo containerPropertyInfo = EdmLibHelpers.GetClrType(odataPath.EdmType, readContext.Model).GetProperties().Where(x => x.PropertyType == typeof(ODataIdContainer)).FirstOrDefault(); - if (containerPropertyInfo != null) + if (resource is EdmEntityObject edmObject) + { + edmObject.ODataIdContainer = container; + } + else if (resource is IDeltaSetItem deltasetItem) { - containerPropertyInfo.SetValue(resource, container); + deltasetItem.ODataIdContainer = container; + } + else + { + PropertyInfo containerPropertyInfo = EdmLibHelpers.GetClrType(odataPath.EdmType, readContext.Model).GetProperties().Where(x => x.PropertyType == typeof(ODataIdContainer)).FirstOrDefault(); + if (containerPropertyInfo != null) + { + containerPropertyInfo.SetValue(resource, container); + } } } } 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 index 12edc96900..00b5ea8ea0 100644 --- 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 @@ -79,7 +79,7 @@ public ITestActionResult PatchEmployees([FromBody] DeltaSet coll) Assert.NotNull(coll); - var returncoll = coll.Patch(new EmployeeEFPatchHandler(dbContext)); + var returncoll = coll.Patch(new APIHandlerFactoryEF(dbContext)); return Ok(returncoll); @@ -108,7 +108,7 @@ public ITestActionResult Patch(int key, [FromBody] Delta delta) try { - delta.Patch(employee, new EmployeeEFPatchHandler(dbContext)); + delta.Patch(employee, new APIHandlerFactoryEF(dbContext)); } catch (ArgumentException ae) 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 index 068f95f331..24f7a4aa6a 100644 --- 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 @@ -43,6 +43,46 @@ protected override void OnModelCreating(Microsoft.EntityFrameworkCore.ModelBuild } + public class APIHandlerFactoryEF: ODataAPIHandlerFactory + { + EmployeeDBContext dbContext; + + public APIHandlerFactoryEF() + { + + } + + public APIHandlerFactoryEF(EmployeeDBContext dbContext) + { + this.dbContext = dbContext; + } + + public override IODataAPIHandler GetHandler(NavigationPath navigationPath) + { + if (navigationPath != null) + { + var pathItems = navigationPath.GetNavigationPathItems(); + + if (pathItems == null) + { + switch (navigationPath.NavigationPathName) + { + case "Employees": + case "Employee": + return new EmployeeEFPatchHandler(dbContext); + + case "Company": + return new CompanyAPIHandler(); + default: + return null; + } + } + } + + return null; + + } + public class EmployeeEFPatchHandler : ODataAPIHandler { EmployeeDBContext dbContext = null; @@ -312,85 +352,86 @@ public override IODataAPIHandler GetNestedHandler(NewFriend parent, string navig - public class OrderEFPatchHandler : ODataAPIHandler - { - Friend friend; - public OrderEFPatchHandler(Friend friend) + public class OrderEFPatchHandler : ODataAPIHandler { - this.friend = friend; - } - - public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out Order createdObject, out string errorMessage) - { - createdObject = null; - errorMessage = string.Empty; - - try + Friend friend; + public OrderEFPatchHandler(Friend friend) { - createdObject = new Order(); - friend.Orders.Add(createdObject); - - return ODataAPIResponseStatus.Success; + this.friend = friend; } - catch (Exception ex) + + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out Order createdObject, out string errorMessage) { - errorMessage = ex.Message; + createdObject = null; + errorMessage = string.Empty; - return ODataAPIResponseStatus.Failure; - } - } + try + { + createdObject = new Order(); + friend.Orders.Add(createdObject); - public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) - { - errorMessage = string.Empty; + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; - try + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) { - var id = keyValues.First().Value.ToString(); - var order = friend.Orders.First(x => x.Id == Int32.Parse(id)); + errorMessage = string.Empty; - friend.Orders.Remove(order); + try + { + var id = keyValues.First().Value.ToString(); + var order = friend.Orders.First(x => x.Id == Int32.Parse(id)); - return ODataAPIResponseStatus.Success; - } - catch (Exception ex) - { - errorMessage = ex.Message; + friend.Orders.Remove(order); - return ODataAPIResponseStatus.Failure; - } - } + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; - public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out Order originalObject, out string errorMessage) - { - ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; - errorMessage = string.Empty; - originalObject = null; + return ODataAPIResponseStatus.Failure; + } + } - try + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out Order originalObject, out string errorMessage) { - var id = keyValues["Id"].ToString(); - originalObject = friend.Orders.First(x => x.Id == Int32.Parse(id)); + 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) + + if (originalObject == null) + { + status = ODataAPIResponseStatus.NotFound; + } + + } + catch (Exception ex) { - status = ODataAPIResponseStatus.NotFound; + status = ODataAPIResponseStatus.Failure; + errorMessage = ex.Message; } + return status; } - catch (Exception ex) + + public override IODataAPIHandler GetNestedHandler(Order parent, string navigationPropertyName) { - status = ODataAPIResponseStatus.Failure; - errorMessage = ex.Message; + throw new NotImplementedException(); } - - return status; - } - - public override IODataAPIHandler GetNestedHandler(Order parent, string navigationPropertyName) - { - throw new NotImplementedException(); } } diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertController.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertController.cs index f27d3a6b2f..7cbc49a9f7 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertController.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertController.cs @@ -132,7 +132,7 @@ private void InitTypeLessEmployees(IEdmEntityType entityType) public DeltaSet PatchWithUsersMethod(DeltaSet friendColl, Employee employee) { - var changedObjColl = friendColl.Patch(new NewFriendAPIHandler(employee)); + var changedObjColl = friendColl.Patch(new APIHandlerFactory(employee)); return changedObjColl; } @@ -238,7 +238,7 @@ public ITestActionResult PatchEmployees([FromBody] DeltaSet coll) Assert.NotNull(coll); - var returncoll = coll.Patch(new EmployeeAPIHandler()); + var returncoll = coll.Patch(new APIHandlerFactory()); return Ok(returncoll); } @@ -366,13 +366,13 @@ public ITestActionResult Patch(int key, [FromBody] Delta delta) if (employee == null) { employee = new Employee(); - delta.Patch(employee, new EmployeeAPIHandler()); + delta.Patch(employee, new APIHandlerFactory()); return Created(employee); } try { - delta.Patch(employee, new EmployeeAPIHandler()); + delta.Patch(employee, new APIHandlerFactory()); if (employee.Name == "Bind1") { @@ -423,7 +423,7 @@ public ITestActionResult PatchCompanies([FromBody] DeltaSet coll) Assert.NotNull(coll); - var returncoll = coll.Patch(new CompanyAPIHandler(), new APIHandlerFactory()); + var returncoll = coll.Patch( new APIHandlerFactory()); var comp = coll.First() as Delta; object val; diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertTest.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertTest.cs index 15ad2d6e07..ea33af60ae 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertTest.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkInsertTest.cs @@ -56,11 +56,7 @@ public async Task PatchEmployee_WithUpdates() 'Friends@odata.delta':[{'Id':1,'Name':'Test2'},{'Id':2,'Name':'Test3'}] }"; - content = @"{ - 'Name':'Sql' , - 'Friends@odata.delta':[{'@odata.id':'Employees(1)/Friends(1)'},{'Id':2,'Name':'Test3'}] - }"; - + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); @@ -137,7 +133,7 @@ public async Task PatchEmployee_WithUpdates_Friends() string requestUri = this.BaseAddress + "/convention/Employees(1)/Friends"; var content = @"{'@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkInsert.Friend', - '@odata.context':'http://host/service/$metadata#Employees(1)/Friends/$delta', + '@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(1)/Friends/$delta', 'value':[{ 'Id':1,'Name':'Friend1'}, { 'Id':2,'Name':'Friend2'}] }"; @@ -175,7 +171,7 @@ public async Task PatchEmployee_WithDeletes_Friends() string requestUri = this.BaseAddress + "/convention/Employees(1)/Friends"; - var content = @"{'@odata.context':'http://host/service/$metadata#Employees(1)/Friends/$delta', + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(1)/Friends/$delta', 'value':[{ '@odata.removed' : {'reason':'changed'}, 'Id':1},{ 'Id':2,'Name':'Friend2'}] }"; @@ -212,7 +208,7 @@ public async Task PatchEmployee_WithDeletes_Friends_WithNestedTypes() string requestUri = this.BaseAddress + "/convention/Employees(1)/Friends"; - var content = @"{'@odata.context':'http://host/service/$metadata#Employees(1)/Friends/$delta', '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkInsert.Friend', + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(1)/Friends/$delta', '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkInsert.Friend', 'value':[{ '@odata.removed' : {'reason':'changed'}, 'Id':1, 'Orders@odata.delta' :[{'Id':1,'Price': 10}, {'Id':2,'Price': 20} ] },{ 'Id':2,'Name':'Friend2'}] }"; @@ -250,7 +246,7 @@ public async Task PatchEmployee_WithDeletes_Friends_WithNestedDeletes() string requestUri = this.BaseAddress + "/convention/Employees(1)/Friends"; - var content = @"{'@odata.context':'http://host/service/$metadata#Employees(1)/Friends/$delta', '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkInsert.Friend', + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(1)/Friends/$delta', '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkInsert.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'}] }"; @@ -288,7 +284,7 @@ public async Task PatchEmployee_WithAdds_Friends_WithAnnotations() string requestUri = this.BaseAddress + "/convention/Employees(1)/NewFriends"; //{ '@odata.removed' : {'reason':'changed'}, 'Id':1},{ '@odata.removed' : {'reason':'deleted'}, 'Id':2}, - var content = @"{'@odata.context':'http://host/service/$metadata#Employees(1)/NewFriends/$delta', + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(1)/NewFriends/$delta', 'value':[{ 'Id':3, 'Age':35, '@NS.Test':1}] }"; @@ -318,7 +314,7 @@ public async Task PatchEmployee_WithFailedAdds_Friends() string requestUri = this.BaseAddress + "/convention/Employees(1)/NewFriends"; //{ '@odata.removed' : {'reason':'changed'}, 'Id':1},{ '@odata.removed' : {'reason':'deleted'}, 'Id':2}, - var content = @"{'@odata.context':'http://host/service/$metadata#Employees(1)/NewFriends/$delta', + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(1)/NewFriends/$delta', 'value':[{ 'Id':3, 'Age':3, '@NS.Test':1}] }"; @@ -343,7 +339,7 @@ public async Task PatchEmployee_WithFailedDeletes_Friends() //Arrange string requestUri = this.BaseAddress + "/convention/Employees(2)/NewFriends"; //{ '@odata.removed' : {'reason':'changed'}, 'Id':1},{ '@odata.removed' : {'reason':'deleted'}, 'Id':2}, - var content = @"{'@odata.context':'http://host/service/$metadata#Employees(1)/NewFriends/$delta', + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(1)/NewFriends/$delta', 'value':[{ '@odata.removed' : {'reason':'changed'}, 'Id':2, '@NS.Test':1}] }"; @@ -370,7 +366,7 @@ public async Task PatchEmployee_WithFailedOperation_WithAnnotations() //Arrange string requestUri = this.BaseAddress + "/convention/Employees(2)/NewFriends"; //{ '@odata.removed' : {'reason':'changed'}, 'Id':1},{ '@odata.removed' : {'reason':'deleted'}, 'Id':2}, - var content = @"{'@odata.context':'http://host/service/$metadata#Employees(2)/NewFriends/$delta', + 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'}] }"; @@ -401,7 +397,7 @@ public async Task PatchUntypedEmployee_WithAdds_Friends_Untyped() string requestUri = this.BaseAddress + "/convention/UnTypedEmployees"; //{ '@odata.removed' : {'reason':'changed'}, 'Id':1},{ '@odata.removed' : {'reason':'deleted'}, 'Id':2}, - var content = @"{'@odata.context':'http://host/service/$metadata#Employees(2)/UnTypedFriends/$delta', + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(2)/UnTypedFriends/$delta', 'value':[{ 'Id':3, 'Age':35,}] }"; @@ -437,7 +433,7 @@ public async Task PatchEmployee_WithAdds_Friends_WithNested_Untyped() string requestUri = this.BaseAddress + "/convention/Employees(1)/UnTypedFriends"; //{ '@odata.removed' : {'reason':'changed'}, 'Id':1},{ '@odata.removed' : {'reason':'deleted'}, 'Id':2}, - var content = @"{'@odata.context':'http://host/service/$metadata#Employees(1)/UnTypedFriends/$delta', + 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}] }"; @@ -466,7 +462,7 @@ public async Task PatchEmployee_WithAdds_Friends_WithAnnotations_Untyped() string requestUri = this.BaseAddress + "/convention/Employees(2)/UnTypedFriends"; //{ '@odata.removed' : {'reason':'changed'}, 'Id':1},{ '@odata.removed' : {'reason':'deleted'}, 'Id':2}, - var content = @"{'@odata.context':'http://host/service/$metadata#Employees(2)/UnTypedFriends/$delta', + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(2)/UnTypedFriends/$delta', 'value':[{ 'Id':2, 'Age':35, '@NS.Test':1}] }"; @@ -493,7 +489,7 @@ public async Task PatchEmployee_WithFailedAdds_Friends_Untyped() string requestUri = this.BaseAddress + "/convention/Employees(3)/UnTypedFriends"; //{ '@odata.removed' : {'reason':'changed'}, 'Id':1},{ '@odata.removed' : {'reason':'deleted'}, 'Id':2}, - var content = @"{'@odata.context':'http://host/service/$metadata#Employees(3)/UnTypedFriends/$delta', + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(3)/UnTypedFriends/$delta', 'value':[{ 'Id':3, 'Age':3, '@NS.Test':1}] }"; @@ -518,7 +514,7 @@ 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':'http://host/service/$metadata#Employees(3)/UnTypedFriends/$delta', + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(3)/UnTypedFriends/$delta', 'value':[{ '@odata.removed' : {'reason':'changed'}, 'Id':5, '@NS.Test':1}] }"; @@ -546,7 +542,7 @@ public async Task PatchEmployee_WithFailedOperation_WithAnnotations_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':'http://host/service/$metadata#Employees(3)/UnTypedFriends/$delta', + 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'}] }"; diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationPatchHandlers.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationPatchHandlers.cs index d72b537595..03fe289b55 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationPatchHandlers.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationPatchHandlers.cs @@ -12,11 +12,39 @@ namespace Microsoft.Test.E2E.AspNet.OData.BulkOperation { public class APIHandlerFactory : ODataAPIHandlerFactory { + Employee employee; + public APIHandlerFactory() + { + + } + + public APIHandlerFactory(Employee employee) + { + this.employee = employee; + } + public override IODataAPIHandler GetHandler(NavigationPath navigationPath) { if(navigationPath != null) { var pathItems = navigationPath.GetNavigationPathItems(); + + if(pathItems == null) + { + switch (navigationPath.NavigationPathName) + { + case "Employees": + case "Employee": + return new EmployeeAPIHandler(); + case "NewFriend": + return new NewFriendAPIHandler(employee); + case "Company": + return new CompanyAPIHandler(); + default: + return null; + } + } + int cnt = 0; switch (pathItems[cnt].Name) @@ -44,7 +72,13 @@ public override IODataAPIHandler GetHandler(NavigationPath navigationPath) private static IODataAPIHandler GetNestedHandlerForEmployee(PathItem[] pathItems, int cnt, Employee employee) { - switch (pathItems[++cnt].Name) + cnt++; + if(pathItems.Length <= cnt) + { + return null; + } + + switch (pathItems[cnt].Name) { case "NewFriends": if (pathItems[cnt].IsCastType) diff --git a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/DeltaSetOfTTest.cs b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/DeltaSetOfTTest.cs index c113b542b8..8ad086696d 100644 --- a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/DeltaSetOfTTest.cs +++ b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/DeltaSetOfTTest.cs @@ -87,7 +87,7 @@ public void DeltaSet_Patch_WithDeletes() friends.Add(new Friend { Id = 2, Name = "Test2" }); //Act - deltaSet.Patch(new FriendPatchHandler()); + deltaSet.Patch(new APIHandlerFactory()); //Assert Assert.Single(friends); @@ -124,7 +124,7 @@ public void DeltaSet_Patch_WithInstanceAnnotations() friends.Add(new Friend { Id = 2, Name = "Test2" }); //Act - var coll = deltaSet.Patch(new FriendPatchHandler()).ToArray(); + var coll = deltaSet.Patch(new APIHandlerFactory()).ToArray(); //Assert Assert.Single(friends); @@ -200,7 +200,7 @@ public void DeltaSet_Patch_WithNestedDelta() 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()); + deltaSet.Patch(new APIHandlerFactory()); //Assert Assert.Equal(2, friends.Count); @@ -218,6 +218,30 @@ public void DeltaSet_Patch_WithNestedDelta() } + public class APIHandlerFactory : ODataAPIHandlerFactory + { + public override IODataAPIHandler GetHandler(NavigationPath navigationPath) + { + if (navigationPath != null) + { + var pathItems = navigationPath.GetNavigationPathItems(); + + if (pathItems == null) + { + switch (navigationPath.NavigationPathName) + { + case "Friend": + return new FriendPatchHandler(); + + default: + return null; + } + } + } + + return null; + } + } public class FriendPatchHandler : ODataAPIHandler { public override IODataAPIHandler GetNestedHandler(Friend parent, string navigationPropertyName) From 82823d0c476d66a22c61c66b183ae5525a211d90 Mon Sep 17 00:00:00 2001 From: Sreejith Pazhampilly Date: Mon, 30 Aug 2021 11:43:44 -0700 Subject: [PATCH 09/28] updates --- .../AspNetODataSample.Web.csproj | 13 +-- samples/AspNetODataSample.Web/Web.config | 6 +- samples/AspNetODataSample.Web/packages.config | 6 +- .../DeltaSetOfT.cs | 2 +- .../EdmChangedObjectCollection.cs | 14 ++-- .../ODataResourceDeserializer.cs | 4 +- .../KeyProperty.cs | 21 ----- .../Microsoft.AspNet.OData.Shared.projitems | 1 - .../NavigationPath.cs | 8 ++ .../GlobalSuppressions.cs | 9 +- .../Microsoft.AspNet.OData.csproj | 12 +-- src/Microsoft.AspNet.OData/packages.config | 6 +- .../Microsoft.AspNetCore.OData.csproj | 6 +- .../Microsoft.Test.E2E.AspNet.OData.csproj | 22 ++--- .../Build.AspNet/packages.config | 8 +- ...Microsoft.Test.E2E.AspNetCore.OData.csproj | 40 ++++----- .../Build.AspNetCore/packages.config | 8 +- .../EFTests/BulkOperationControllerEF.cs | 2 +- .../EFTests/BulkOperationPatchHandlersEF.cs | 4 +- ...crosoft.Test.E2E.AspNetCore3x.OData.csproj | 2 +- .../BulkOperation/BulkInsertController.cs | 4 +- .../BulkOperationPatchHandlers.cs | 27 +++++- .../EdmChangedObjectCollectionTest.cs | 48 ++++++++++- .../Microsoft.AspNet.OData.Test.csproj | 16 ++-- .../Microsoft.AspNet.OData.PublicApi.bsl | 14 +--- .../packages.config | 6 +- .../Microsoft.AspNetCore.OData.Test.csproj | 12 +-- .../Microsoft.AspNetCore.OData.PublicApi.bsl | 82 +++++++++++++------ 28 files changed, 250 insertions(+), 153 deletions(-) delete mode 100644 src/Microsoft.AspNet.OData.Shared/KeyProperty.cs diff --git a/samples/AspNetODataSample.Web/AspNetODataSample.Web.csproj b/samples/AspNetODataSample.Web/AspNetODataSample.Web.csproj index 263303ec7d..028797ecc0 100644 --- a/samples/AspNetODataSample.Web/AspNetODataSample.Web.csproj +++ b/samples/AspNetODataSample.Web/AspNetODataSample.Web.csproj @@ -88,16 +88,17 @@ True - - ..\..\sln\packages\Microsoft.OData.Core.7.9.0\lib\net45\Microsoft.OData.Core.dll + + ..\..\sln\packages\Microsoft.OData.Core.7.9.1\lib\net45\Microsoft.OData.Core.dll - - ..\..\sln\packages\Microsoft.OData.Edm.7.9.0\lib\net45\Microsoft.OData.Edm.dll + + ..\..\sln\packages\Microsoft.OData.Edm.7.9.1\lib\net45\Microsoft.OData.Edm.dll ..\..\sln\packages\Microsoft.Spatial.7.9.0\lib\net45\Microsoft.Spatial.dll - ..\..\sln\packages\Microsoft.Spatial.7.8.2\lib\net45\Microsoft.Spatial.dll - + ..\..\sln\packages\Microsoft.Spatial.7.9.1\lib\net45\Microsoft.Spatial.dll + True + ..\..\sln\packages\System.Buffers.4.5.0\lib\netstandard2.0\System.Buffers.dll diff --git a/samples/AspNetODataSample.Web/Web.config b/samples/AspNetODataSample.Web/Web.config index 989824878b..2a98e0bd9a 100644 --- a/samples/AspNetODataSample.Web/Web.config +++ b/samples/AspNetODataSample.Web/Web.config @@ -1,4 +1,4 @@ - + 7 - 5 - 12 - + 6 + 0 + beta @@ -40,7 +40,7 @@ $(VersionFullSemantic) $(VersionFullSemantic)-$(VersionRelease) - +