From ba85d80e84c951101e46b21e1aed8082cd8dbaf6 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/31] 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 | 174 +++- .../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 | 101 ++- .../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 | 37 +- .../Untyped/UntypedDeltaSerializationTests.cs | 4 +- .../DeltaSetOfTTest.cs | 309 +++++++ .../EdmChangedObjectCollectionTest.cs | 419 +++++++++- .../DeserializationHelpersTest.cs | 61 ++ .../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 | 150 +++- .../Microsoft.AspNet.OData.Test/app.config | 15 +- .../Microsoft.AspNetCore.OData.Test.csproj | 2 +- .../Microsoft.AspNetCore.OData.PublicApi.bsl | 150 +++- ...Microsoft.AspNetCore3x.OData.PublicApi.bsl | 150 +++- 86 files changed, 7009 insertions(+), 466 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 004c0178ed..12ee3ee603 100644 --- a/src/Microsoft.AspNet.OData.Shared/Common/Error.cs +++ b/src/Microsoft.AspNet.OData.Shared/Common/Error.cs @@ -99,6 +99,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 3c53dad32a..89716cb753 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 cfc417253e..270aa879ab 100644 --- a/src/Microsoft.AspNet.OData.Shared/Common/SRResources.resx +++ b/src/Microsoft.AspNet.OData.Shared/Common/SRResources.resx @@ -985,4 +985,22 @@ InstanceAnnotation Property name cannot be null or empty. + + 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 64c5830fbd..b6e2f61c25 100644 --- a/src/Microsoft.AspNet.OData.Shared/DeltaOfTStructuralType.cs +++ b/src/Microsoft.AspNet.OData.Shared/DeltaOfTStructuralType.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. See License.txt in the project root for license information. using System; +using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; @@ -10,9 +11,11 @@ using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; +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 { @@ -21,7 +24,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 ConcurrentDictionary>> _propertyCache @@ -39,9 +42,10 @@ private static ConcurrentDictionary _changedDynamicProperties; private IDictionary _dynamicDictionaryCache; - + /// /// Initializes a new instance of . /// @@ -70,8 +74,24 @@ public Delta(Type structuralType) /// The set of properties that can be updated or reset. Unknown property /// names, including those of dynamic properties, are ignored. public Delta(Type structuralType, IEnumerable updatableProperties) - : this(structuralType, updatableProperties: updatableProperties, dynamicDictionaryPropertyInfo: null) + : this(structuralType, updatableProperties: 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 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, instanceAnnotationsPropertyInfo: null) { + } /// @@ -83,13 +103,17 @@ public Delta(Type structuralType, IEnumerable updatableProperties) /// 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. + /// properties. null means this entity type is not open. + /// The property info that is used as container for Instance Annotations public Delta(Type structuralType, IEnumerable updatableProperties, - PropertyInfo dynamicDictionaryPropertyInfo) + PropertyInfo dynamicDictionaryPropertyInfo, PropertyInfo instanceAnnotationsPropertyInfo) { _dynamicDictionaryPropertyinfo = dynamicDictionaryPropertyInfo; Reset(structuralType); - InitializeProperties(updatableProperties); + InitializeProperties(updatableProperties); + TransientInstanceAnnotationContainer = new ODataInstanceAnnotationContainer(); + _instanceAnnotationsPropertyInfo = instanceAnnotationsPropertyInfo; + DeltaKind = EdmDeltaEntityKind.Entry; } /// @@ -101,12 +125,29 @@ public override Type StructuredType } } + internal IDictionary DeltaNestedResources + { + get { return _deltaNestedResources; } + } + /// public override Type ExpectedClrType { get { return typeof(TStructuralType); } } + /// + /// Gets the enum type of . + /// + public EdmDeltaEntityKind DeltaKind { get; protected set; } + + /// + public IODataInstanceAnnotationContainer TransientInstanceAnnotationContainer { get; set; } + + /// + internal PropertyInfo InstanceAnnotationsPropertyInfo { get { return _instanceAnnotationsPropertyInfo; } } + + /// public override void Clear() { @@ -121,6 +162,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. @@ -139,7 +193,7 @@ public override bool TrySetPropertyValue(string name, object value) } } - if (value is IDelta) + if (value is IDelta || value is IDeltaSet) { return TrySetNestedResourceInternal(name, value); } @@ -157,6 +211,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) @@ -177,6 +244,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. @@ -278,6 +353,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, PatchMethodHandler patchHandler = null) { if (original == null) { @@ -309,31 +391,45 @@ 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(); + IPatchMethodHandler patchHandlerNested = patchHandler.GetNestedPatchHandler(original, nestedResourceName); - // Recursively patch up the instance with the nested resources. - deltaObject.CopyChangedValues(instance); - - _allProperties[nestedResourceName].SetValue(original, instance); + if (patchHandlerNested != null) + { + deltaNestedResource.Patch(patchHandlerNested); + } } else { - // Recursively patch the subtree. - bool isDeltaType = TypedDelta.IsDeltaOfT(deltaNestedResource.GetType()); - Contract.Assert(isDeltaType, nestedResourceName + "'s corresponding value should be Delta type but is not."); + if (!TryGetPropertyRef(original, nestedResourceName, out originalNestedResource)) + { + throw Error.Argument(nestedResourceName, SRResources.DeltaNestedResourceNameNotFound, + nestedResourceName, original.GetType()); + } + + if (originalNestedResource == null) + { + // 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); + + _allProperties[nestedResourceName].SetValue(original, instance); + } + else + { + //Recursively patch the subtree. + bool isDeltaType = TypedDelta.IsDeltaOfT(deltaNestedResource.GetType()); + Contract.Assert(isDeltaType, nestedResourceName + "'s corresponding value should be Delta type but is not."); - deltaNestedResource.CopyChangedValues(originalNestedResource); + deltaNestedResource.CopyChangedValues(originalNestedResource); + } } + } } @@ -373,6 +469,17 @@ 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. + /// Patch Handler + public void Patch(TStructuralType original, IPatchMethodHandler patchHandler) + { + CopyChangedValues(original, patchHandler as PatchMethodHandler); + } + /// /// 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. @@ -502,7 +609,7 @@ private void InitializeProperties(IEnumerable updatableProperties) .Where(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 = new HashSet(updatableProperties); @@ -666,11 +773,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 @@ -680,4 +792,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 1af4915c28..cec26285bd 100644 --- a/src/Microsoft.AspNet.OData.Shared/EdmChangedObjectCollection.cs +++ b/src/Microsoft.AspNet.OData.Shared/EdmChangedObjectCollection.cs @@ -1,11 +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; 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 { @@ -18,7 +24,7 @@ public class EdmChangedObjectCollection : Collection, IEdmObj private IEdmEntityType _entityType; private EdmDeltaCollectionType _edmType; private IEdmCollectionTypeReference _edmTypeReference; - + /// /// Initializes a new instance of the class. /// @@ -39,7 +45,13 @@ public EdmChangedObjectCollection(IEdmEntityType entityType, IList + /// Represents EntityType of the changedobject + /// + public IEdmEntityType EntityType { get { return _entityType; } } + /// public IEdmTypeReference GetEdmType() { @@ -57,5 +69,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; + } + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.OData.Shared/EdmDeltaDeletedEntityObject.cs b/src/Microsoft.AspNet.OData.Shared/EdmDeltaDeletedEntityObject.cs index 25494d74ca..761dbf5bb1 100644 --- a/src/Microsoft.AspNet.OData.Shared/EdmDeltaDeletedEntityObject.cs +++ b/src/Microsoft.AspNet.OData.Shared/EdmDeltaDeletedEntityObject.cs @@ -76,12 +76,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 ba64398e0d..6640dbf83f 100644 --- a/src/Microsoft.AspNet.OData.Shared/EdmDeltaDeletedLink.cs +++ b/src/Microsoft.AspNet.OData.Shared/EdmDeltaDeletedLink.cs @@ -88,12 +88,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 8c0d0bdd40..562928f061 100644 --- a/src/Microsoft.AspNet.OData.Shared/EdmDeltaEntityObject.cs +++ b/src/Microsoft.AspNet.OData.Shared/EdmDeltaEntityObject.cs @@ -47,12 +47,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 75d750c7e9..2d3a6881a2 100644 --- a/src/Microsoft.AspNet.OData.Shared/EdmDeltaLink.cs +++ b/src/Microsoft.AspNet.OData.Shared/EdmDeltaLink.cs @@ -88,12 +88,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 bd5cd4ccd0..3f095360dc 100644 --- a/src/Microsoft.AspNet.OData.Shared/EdmEntityObject.cs +++ b/src/Microsoft.AspNet.OData.Shared/EdmEntityObject.cs @@ -2,7 +2,12 @@ // Licensed under the MIT License. See License.txt in the project root for license information. 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 { @@ -10,7 +15,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. @@ -37,7 +42,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 b902532bf2..a875f012f4 100644 --- a/src/Microsoft.AspNet.OData.Shared/EdmStructuredObject.cs +++ b/src/Microsoft.AspNet.OData.Shared/EdmStructuredObject.cs @@ -5,6 +5,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; @@ -220,8 +221,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 cd4cb7c314..02c95e31c0 100644 --- a/src/Microsoft.AspNet.OData.Shared/EdmTypeExtensions.cs +++ b/src/Microsoft.AspNet.OData.Shared/EdmTypeExtensions.cs @@ -22,7 +22,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 59bbccfc02..389da29f35 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/CollectionDeserializationHelpers.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/CollectionDeserializationHelpers.cs @@ -4,6 +4,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics.Contracts; using System.Linq; using System.Reflection; @@ -115,10 +116,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 cab4a6f838..8e7d9f5722 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/DefaultODataDeserializerProvider.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/DefaultODataDeserializerProvider.cs @@ -88,8 +88,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 a50c4938fe..a08063f1aa 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/DeserializationHelpers.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/DeserializationHelpers.cs @@ -82,18 +82,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, @@ -135,15 +160,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) { @@ -163,7 +188,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); @@ -274,14 +299,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); + } } } @@ -313,11 +345,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); @@ -325,7 +358,7 @@ public static IODataInstanceAnnotationContainer GetAnnotationContainer(PropertyI else { value = propertyInfo.GetValue(resource); - } + } IODataInstanceAnnotationContainer instanceAnnotationContainer = value as IODataInstanceAnnotationContainer; @@ -341,8 +374,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 494bfdfe6c..8430362d17 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataDeserializerContext.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataDeserializerContext.cs @@ -14,7 +14,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. @@ -58,20 +62,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 e86b4bcf3a..2f17761056 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataReaderExtensions.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataReaderExtensions.cs @@ -85,30 +85,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; @@ -135,12 +136,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 { @@ -151,6 +150,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; @@ -167,11 +167,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 290b21771b..370991c9af 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializer.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializer.cs @@ -20,6 +20,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 { /// @@ -119,8 +120,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; @@ -130,20 +132,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); @@ -174,10 +177,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. /// @@ -206,6 +218,11 @@ public virtual object CreateResourceInstance(IEdmStructuredTypeReference structu { if (structuredType.IsEntity()) { + if (readContext.IsDeltaDeletedEntity) + { + return new EdmDeltaDeletedEntityObject(structuredType.AsEntity()); + } + return new EdmEntityObject(structuredType.AsEntity()); } @@ -222,8 +239,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()) { @@ -231,11 +251,11 @@ public virtual object CreateResourceInstance(IEdmStructuredTypeReference structu structuredType.StructuredDefinition(), model); return Activator.CreateInstance(readContext.ResourceType, clrType, structuralProperties, - dynamicDictionaryPropertyInfo); + dynamicDictionaryPropertyInfo, instanceAnnotationProperty); } else { - return Activator.CreateInstance(readContext.ResourceType, clrType, structuralProperties); + return Activator.CreateInstance(readContext.ResourceType, clrType, structuralProperties, null, instanceAnnotationProperty); } } else @@ -245,6 +265,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 . /// @@ -321,7 +355,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) @@ -369,7 +403,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); } @@ -390,7 +424,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); } /// @@ -418,6 +452,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); @@ -431,19 +466,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); @@ -460,7 +484,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); @@ -499,7 +523,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 @@ -520,43 +544,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); @@ -583,7 +596,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); @@ -608,7 +621,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 { @@ -625,7 +639,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 b0d180f590..3bd2eeb205 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceSetDeserializer.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceSetDeserializer.cs @@ -3,6 +3,8 @@ using System; using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics.Contracts; using System.Linq; using System.Reflection; @@ -48,7 +50,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); } @@ -70,12 +72,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) @@ -93,20 +96,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()); @@ -124,13 +153,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 . /// @@ -138,7 +183,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) @@ -147,10 +192,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 7c7e4b80b0..5efd84d2f8 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceSetWrapper.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceSetWrapper.cs @@ -1,7 +1,9 @@ // 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 Microsoft.AspNet.OData.Common; using Microsoft.OData; namespace Microsoft.AspNet.OData.Formatter.Deserialization @@ -9,32 +11,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 f78be998a6..f70be5c4d9 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceWrapper.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceWrapper.cs @@ -1,7 +1,9 @@ // 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 Microsoft.AspNet.OData.Common; using Microsoft.OData; namespace Microsoft.AspNet.OData.Formatter.Deserialization @@ -10,20 +12,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 7a907d1887..e57f32afb8 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/EdmLibHelpers.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/EdmLibHelpers.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. See License.txt in the project root for license information. 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; @@ -130,13 +131,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; @@ -901,14 +912,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()) @@ -1041,6 +1055,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 7ac3f06f75..3e7708f40e 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/ODataModelBinderConverter.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/ODataModelBinderConverter.cs @@ -331,7 +331,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; @@ -344,14 +344,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 8b12a249c2..b05ff06f30 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/ODataOutputFormatterHelper.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/ODataOutputFormatterHelper.cs @@ -91,7 +91,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); @@ -203,6 +203,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) @@ -247,7 +248,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 3a863c676b..1fa8a5a46d 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/DefaultODataSerializerProvider.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/DefaultODataSerializerProvider.cs @@ -108,6 +108,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 617b4bbff1..25ee9d81a5 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataDeltaFeedSerializer.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataDeltaFeedSerializer.cs @@ -2,8 +2,11 @@ // Licensed under the MIT License. See License.txt in the project root for license information. 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; @@ -21,6 +24,7 @@ namespace Microsoft.AspNet.OData.Formatter.Serialization public class ODataDeltaFeedSerializer : ODataEdmTypeSerializer { private const string DeltaFeed = "deltafeed"; + IEdmStructuredTypeReference _elementType; /// /// Initializes a new instance of . @@ -56,6 +60,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(); @@ -89,6 +94,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(); @@ -182,6 +188,7 @@ private void WriteFeed(IEnumerable enumerable, IEdmTypeReference feedType, OData Contract.Assert(feedType != null); IEdmStructuredTypeReference elementType = GetResourceType(feedType); + _elementType = elementType; if (elementType.IsComplex()) { @@ -230,13 +237,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); @@ -250,6 +275,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( @@ -285,6 +311,7 @@ private async Task WriteFeedAsync(IEnumerable enumerable, IEdmTypeReference feed Contract.Assert(feedType != null); IEdmStructuredTypeReference elementType = GetResourceType(feedType); + _elementType = elementType; if (elementType.IsComplex()) { @@ -333,13 +360,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); @@ -434,15 +480,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(); + } } } @@ -452,14 +506,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(); + } } } @@ -469,7 +531,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); @@ -485,7 +547,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); @@ -501,7 +563,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); @@ -517,7 +579,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); @@ -527,26 +589,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 02ffd1b8c8..1fec6d3217 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataResourceSerializer.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataResourceSerializer.cs @@ -234,7 +234,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); @@ -249,7 +249,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); @@ -684,6 +684,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"); @@ -696,6 +704,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) @@ -703,11 +719,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) { @@ -756,7 +786,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)) { @@ -790,6 +820,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 @@ -800,7 +843,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); @@ -927,102 +970,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); } /// @@ -1613,6 +1614,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()) @@ -1622,11 +1625,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; @@ -1944,7 +1949,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 @@ -1969,7 +1974,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 @@ -2043,7 +2048,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 f98cfce09c..48ee73d246 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataSerializerContext.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataSerializerContext.cs @@ -23,6 +23,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. @@ -159,6 +161,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 /// @@ -296,6 +326,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); @@ -308,7 +343,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 f424f40b16..2ef9a94c62 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 bf587f6ab2..153a7c86c4 100644 --- a/src/Microsoft.AspNet.OData.Shared/ResourceContext.cs +++ b/src/Microsoft.AspNet.OData.Shared/ResourceContext.cs @@ -191,12 +191,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 c287c506c5..56c7c95967 100644 --- a/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/EntitySetRoutingConvention.cs +++ b/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/EntitySetRoutingConvention.cs @@ -42,6 +42,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 6b984ec86c..570853f6e8 100644 --- a/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/NavigationRoutingConvention.cs +++ b/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/NavigationRoutingConvention.cs @@ -46,9 +46,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 c0704162ac..9d8944ae50 100644 --- a/src/Microsoft.AspNet.OData.Shared/TypedDelta.cs +++ b/src/Microsoft.AspNet.OData.Shared/TypedDelta.cs @@ -28,7 +28,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 bebd2a6b5b..2746837e09 100644 --- a/src/Microsoft.AspNet.OData/GlobalSuppressions.cs +++ b/src/Microsoft.AspNet.OData/GlobalSuppressions.cs @@ -61,3 +61,37 @@ [assembly: SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Scope = "member", Target = "Microsoft.AspNet.OData.Builder.IODataInstanceAnnotationContainer.#GetAllResourceAnnotations()")] [assembly: SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Scope = "member", Target = "Microsoft.AspNet.OData.Builder.IODataInstanceAnnotationContainer.#GetResourceAnnotations()")] [assembly: SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Scope = "member", Target = "Microsoft.AspNet.OData.Common.Error.#PropertyNullOrWhiteSpace()")] +[assembly: SuppressMessage("Microsoft.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 6b439e284b..b0f2a61260 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 90cd827f5f..cb1fcd66f3 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 @@ -1399,6 +1399,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 8baade2685..82625bd304 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 @@ -1662,6 +1662,18 @@ Validation\DeltaOfTValidationTests.cs + + BulkOperation\BulkInsertDataModel.cs + + + BulkOperation\BulkInsertEdmModel.cs + + + BulkOperation\BulkInsertController.cs + + + BulkOperation\BulkOperationPatchHandlers.cs + @@ -1675,4 +1687,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 18226d8780..4e8dbe3cb1 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 @@ -39,10 +39,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 0028d515f6..158d9b51b0 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 @@ -60,7 +60,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(); @@ -152,7 +152,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 4a88640e6c..3beacf8bf0 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/ComplexTypeInheritance/ComplexTypeInheritanceTests.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/ComplexTypeInheritance/ComplexTypeInheritanceTests.cs @@ -298,7 +298,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; @@ -320,7 +326,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 744e7391f1..21aae58bea 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/DeltaQueryTests/DeltaQueryTests.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/DeltaQueryTests/DeltaQueryTests.cs @@ -47,6 +47,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(); @@ -55,7 +56,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."); @@ -94,22 +95,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 5c82bed89e..eceb29fb46 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/DollarLevels/DollarLevelsController.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/DollarLevels/DollarLevelsController.cs @@ -140,7 +140,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 e5d640713e..d405fb99d6 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/DollarLevels/DollarLevelsDataModel.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/DollarLevels/DollarLevelsDataModel.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. See License.txt in the project root for license information. using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; namespace Microsoft.Test.E2E.AspNet.OData.DollarLevels { @@ -22,4 +23,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 1e8c19eaa2..418d5890b1 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/DollarLevels/DollarLevelsEdmModel.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/DollarLevels/DollarLevelsEdmModel.cs @@ -14,6 +14,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 9542d4aea8..95248c63b9 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Formatter/DeltaTests.cs +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Formatter/DeltaTests.cs @@ -41,6 +41,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; @@ -351,7 +352,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(); @@ -362,12 +364,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) { @@ -385,15 +388,32 @@ 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("MERGE"), 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"); @@ -401,13 +421,14 @@ 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); } + } public class DeltaCustomersController : TestODataController @@ -453,6 +474,7 @@ public ITestActionResult Put([FromODataUri] int key, [FromBody] Delta c.Id == key).FirstOrDefault(); entity.Put(customer); return Ok(customer); + } [AcceptVerbs("PATCH", "MERGE")] @@ -482,6 +504,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 d500345999..55cd93dfc3 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 @@ -51,6 +51,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); @@ -67,8 +68,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 895a0a76a2..299069937f 100644 --- a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/EdmChangedObjectCollectionTest.cs +++ b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/EdmChangedObjectCollectionTest.cs @@ -1,6 +1,11 @@ // 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.Test.Common; using Microsoft.OData.Edm; using Moq; @@ -32,5 +37,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 d349cb6aeb..995db1d1bf 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 @@ -24,6 +24,7 @@ using Microsoft.OData.Edm; using Moq; using Xunit; +using Microsoft.AspNet.OData.Builder; namespace Microsoft.AspNet.OData.Test.Formatter.Deserialization { @@ -324,6 +325,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 d32077c33b..5b5d21db77 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 @@ -1033,7 +1033,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; @@ -1155,6 +1155,7 @@ public void ApplyNestedProperty_ThrowsArgumentNull_EntityResource() "resource"); } + [Fact] public void ApplyNestedProperty_ThrowsODataException_NavigationPropertyNotfound() { @@ -1168,35 +1169,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 05de466e51..9d7863b412 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 @@ -62,7 +62,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] @@ -85,7 +85,7 @@ public void ReadInline_Calls_ReadFeed() deserializer.Verify(); Assert.Same(expectedResult, result); } - + [Fact] public void ReadFeed_Throws_TypeCannotBeDeserialized() { @@ -125,6 +125,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 4dc60f1ae7..3c64617fa0 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 @@ -3,6 +3,7 @@ using System; using System.Collections; +using System.Collections.Generic; using System.IO; using System.Runtime.Serialization; using Microsoft.AspNet.OData.Formatter; @@ -271,6 +272,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); @@ -288,6 +290,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); @@ -350,6 +353,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 1d970dc204..38ba2ee985 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 @@ -146,6 +146,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 6232b0e7fa..e2b1eb9294 100644 --- a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Routing/AttributeRoutingTest.cs +++ b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Routing/AttributeRoutingTest.cs @@ -140,6 +140,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 f88378e0aa..44fe05d99d 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 @@ -33,6 +33,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 24aa0e43ef..01a68e3135 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.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(), ] @@ -140,6 +172,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 () @@ -250,14 +291,17 @@ 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, System.Reflection.PropertyInfo instanceAnnotationsPropertyInfo) + EdmDeltaEntityKind DeltaKind { public virtual get; protected set; } System.Type ExpectedClrType { public virtual get; } System.Type StructuredType { public virtual get; } + IODataInstanceAnnotationContainer TransientInstanceAnnotationContainer { public virtual get; public virtual set; } public virtual void Clear () public void CopyChangedValues (TStructuralType original) @@ -266,12 +310,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(), ] @@ -279,7 +351,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) } [ @@ -367,10 +442,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 () } [ @@ -3134,6 +3215,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) @@ -3168,6 +3282,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(), ] @@ -3297,7 +3418,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 { @@ -3307,18 +3432,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 { @@ -3479,8 +3608,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) @@ -3547,6 +3677,10 @@ public class Microsoft.AspNet.OData.Formatter.Serialization.ODataSerializerConte System.Web.Http.Routing.UrlHelper 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.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 700c7801da..4f3e265654 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,14 +306,17 @@ 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, System.Reflection.PropertyInfo instanceAnnotationsPropertyInfo) + EdmDeltaEntityKind DeltaKind { public virtual get; protected set; } System.Type ExpectedClrType { public virtual get; } System.Type StructuredType { public virtual get; } + IODataInstanceAnnotationContainer TransientInstanceAnnotationContainer { public virtual get; public virtual set; } public virtual void Clear () public void CopyChangedValues (TStructuralType original) @@ -281,12 +325,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(), ] @@ -294,7 +366,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) } [ @@ -382,10 +457,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 () } [ @@ -3339,6 +3420,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) @@ -3373,6 +3487,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(), ] @@ -3501,7 +3622,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 { @@ -3511,18 +3636,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 { @@ -3683,8 +3812,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) @@ -3750,6 +3880,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 fb3aedfe11..5ce329adb7 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.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(), ] @@ -151,6 +183,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 () @@ -269,14 +310,17 @@ 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, System.Reflection.PropertyInfo instanceAnnotationsPropertyInfo) + EdmDeltaEntityKind DeltaKind { public virtual get; protected set; } System.Type ExpectedClrType { public virtual get; } System.Type StructuredType { public virtual get; } + IODataInstanceAnnotationContainer TransientInstanceAnnotationContainer { public virtual get; public virtual set; } public virtual void Clear () public void CopyChangedValues (TStructuralType original) @@ -285,12 +329,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(), ] @@ -298,7 +370,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) } [ @@ -386,10 +461,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 () } [ @@ -3538,6 +3619,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) @@ -3572,6 +3686,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(), ] @@ -3700,7 +3821,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 { @@ -3710,18 +3835,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 { @@ -3882,8 +4011,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) @@ -3949,6 +4079,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 () From d9ce354d2ae8f7b4490d118e725eefd82111f34b Mon Sep 17 00:00:00 2001 From: Elizabeth Okerio Date: Fri, 11 Jun 2021 15:09:25 +0300 Subject: [PATCH 02/31] 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 eff07d138a..dc0e9a8649 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataEntityReferenceLinkBase.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataEntityReferenceLinkBase.cs @@ -17,17 +17,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 5dc286af5a..86808653b9 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataNestedResourceInfoWrapper.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataNestedResourceInfoWrapper.cs @@ -40,5 +40,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 370991c9af..893f61db7f 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializer.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializer.cs @@ -11,9 +11,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 { @@ -97,6 +100,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); } @@ -331,7 +335,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) @@ -347,14 +383,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) { @@ -388,6 +417,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 . /// @@ -516,7 +676,7 @@ private object ReadNestedResourceInline(ODataResourceWrapper resourceWrapper, IE { Path = readContext.Path, Model = readContext.Model, - Request = readContext.Request, + Request = readContext.Request }; Type clrType = null; @@ -542,7 +702,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) { @@ -614,7 +773,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 38ba2ee985..07a6db0ad3 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 @@ -168,6 +168,7 @@ + From fa714f16334e5c86aa05889468912132ac33ed6b Mon Sep 17 00:00:00 2001 From: Sreejith Pazhampilly Date: Thu, 8 Jul 2021 11:20:12 -0700 Subject: [PATCH 03/31] 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 893f61db7f..c5c128cb6b 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializer.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializer.cs @@ -469,24 +469,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)) @@ -495,7 +495,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 2746837e09..72558f284a 100644 --- a/src/Microsoft.AspNet.OData/GlobalSuppressions.cs +++ b/src/Microsoft.AspNet.OData/GlobalSuppressions.cs @@ -65,33 +65,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 a52aed53dc51aa4e840ffb8100d6efe933aa1f46 Mon Sep 17 00:00:00 2001 From: Sreejith Pazhampilly Date: Fri, 9 Jul 2021 07:31:10 -0700 Subject: [PATCH 04/31] 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 9378ff254e15bf5d659ca4bd9258599a3dc244ed Mon Sep 17 00:00:00 2001 From: Sreejith Pazhampilly Date: Tue, 27 Jul 2021 07:26:40 -0700 Subject: [PATCH 05/31] 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 2ef9a94c62..f84a850853 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 56c7c95967..771c7f8019 100644 --- a/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/EntitySetRoutingConvention.cs +++ b/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/EntitySetRoutingConvention.cs @@ -44,7 +44,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 b65e4033f31348562d6e0db9af655290c78cec07 Mon Sep 17 00:00:00 2001 From: Sreejith Pazhampilly Date: Tue, 24 Aug 2021 13:00:27 -0700 Subject: [PATCH 06/31] update for odataid _temp --- .../DefaultEdmPatchMethodHandler.cs | 28 +- ...chHandler.cs => DefaultODataAPIHandler.cs} | 32 +- .../DeltaOfTStructuralType.cs | 61 ++- .../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 +- 27 files changed, 1173 insertions(+), 333 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/DeltaOfTStructuralType.cs b/src/Microsoft.AspNet.OData.Shared/DeltaOfTStructuralType.cs index b6e2f61c25..d52a13dadc 100644 --- a/src/Microsoft.AspNet.OData.Shared/DeltaOfTStructuralType.cs +++ b/src/Microsoft.AspNet.OData.Shared/DeltaOfTStructuralType.cs @@ -45,6 +45,7 @@ private static ConcurrentDictionary _changedDynamicProperties; private IDictionary _dynamicDictionaryCache; + private NavigationPath _navigationPath; /// /// Initializes a new instance of . @@ -113,6 +114,7 @@ public Delta(Type structuralType, IEnumerable updatableProperties, InitializeProperties(updatableProperties); TransientInstanceAnnotationContainer = new ODataInstanceAnnotationContainer(); _instanceAnnotationsPropertyInfo = instanceAnnotationsPropertyInfo; + _navigationPath = new NavigationPath(structuralType.Name, null); DeltaKind = EdmDeltaEntityKind.Entry; } @@ -144,6 +146,9 @@ public override Type ExpectedClrType /// public IODataInstanceAnnotationContainer TransientInstanceAnnotationContainer { get; set; } + /// + public ODataIdContainer ODataIdContainer { get; set; } + /// internal PropertyInfo InstanceAnnotationsPropertyInfo { get { return _instanceAnnotationsPropertyInfo; } } @@ -359,7 +364,7 @@ public void CopyChangedValues(TStructuralType original) [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")] [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily")] - internal void CopyChangedValues(TStructuralType original, PatchMethodHandler patchHandler = null) + internal void CopyChangedValues(TStructuralType original, ODataAPIHandler apiHandler = null, ODataAPIHandlerFactory apiHandlerFactory = null) { if (original == null) { @@ -394,11 +399,11 @@ internal void CopyChangedValues(TStructuralType original, PatchMethodHandlerThe semantics of this operation are equivalent to a HTTP PATCH operation, hence the name. /// /// The entity to be updated. - /// Patch Handler - public void Patch(TStructuralType original, IPatchMethodHandler patchHandler) - { - CopyChangedValues(original, patchHandler as PatchMethodHandler); + /// API Handler + /// API Handler Factory + public void Patch(TStructuralType original, IODataAPIHandler apiHandler, ODataAPIHandlerFactory apiHandlerFactory = null) + { + if (apiHandlerFactory != null && ODataIdContainer?.ODataIdNavigationPath != null) + { + IODataAPIHandler refapiHandler = apiHandlerFactory.GetHandler(ODataIdContainer.ODataIdNavigationPath); + + ODataAPIHandler refapiHandlerOfT = refapiHandler as ODataAPIHandler; + + Debug.Assert(refapiHandlerOfT != null); + + TStructuralType referencedObj; + string error; + + if (refapiHandlerOfT.TryGet(ODataIdContainer.ODataIdNavigationPath.GetNavigationPathItems().Last().KeyProperties, out referencedObj, out error) == ODataAPIResponseStatus.Success) + { + if (apiHandler != null) + { + ODataAPIHandler apiHandlerOfT = apiHandler as ODataAPIHandler; + + TStructuralType createdObj; + string errMsg; + if (apiHandlerOfT.TryCreate(null, out createdObj, out errMsg) == ODataAPIResponseStatus.Success) + { + foreach(string property in _updatableProperties) + { + PropertyInfo propertyInfo = _structuredType.GetProperty(property); + + object value = propertyInfo.GetValue(referencedObj); + propertyInfo.SetValue(createdObj, value); + } + } + } + else + { + original = referencedObj; + } + } + } + else + { + CopyChangedValues(original, apiHandler as ODataAPIHandler, apiHandlerFactory); + } } /// 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 cec26285bd..4a3c66735c 100644 --- a/src/Microsoft.AspNet.OData.Shared/EdmChangedObjectCollection.cs +++ b/src/Microsoft.AspNet.OData.Shared/EdmChangedObjectCollection.cs @@ -77,25 +77,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(); @@ -117,13 +117,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; } @@ -135,34 +135,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); } @@ -170,7 +170,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); @@ -198,20 +198,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)) @@ -219,10 +219,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 { @@ -260,7 +260,7 @@ private void ApplyProperties(EdmStructuredObject changedObj, EdmStructuredObject originalObj.TrySetPropertyValue(propertyName, origStructuredObj); } - PatchItem(structuredObj, origStructuredObj, patchHandler); + PatchItem(structuredObj, origStructuredObj, apiHandler); } else { @@ -271,7 +271,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); @@ -287,7 +287,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); @@ -301,7 +301,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 3f095360dc..e8416575d9 100644 --- a/src/Microsoft.AspNet.OData.Shared/EdmEntityObject.cs +++ b/src/Microsoft.AspNet.OData.Shared/EdmEntityObject.cs @@ -57,6 +57,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 c5c128cb6b..90a8022b11 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializer.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializer.cs @@ -4,6 +4,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics.Contracts; using System.Linq; using System.Reflection; @@ -516,18 +517,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) @@ -548,6 +543,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 . /// @@ -617,6 +661,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 f84a850853..1383c12960 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 72558f284a..8ff6c86353 100644 --- a/src/Microsoft.AspNet.OData/GlobalSuppressions.cs +++ b/src/Microsoft.AspNet.OData/GlobalSuppressions.cs @@ -66,18 +66,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 299069937f..d8645ba3a0 100644 --- a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/EdmChangedObjectCollectionTest.cs +++ b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/EdmChangedObjectCollectionTest.cs @@ -209,7 +209,7 @@ public class NewFriend public string Name { get; set; } } - public class FriendTypelessPatchHandler : EdmPatchMethodHandler + public class FriendTypelessPatchHandler : EdmODataAPIHandler { IEdmEntityType entityType; @@ -218,7 +218,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; @@ -229,17 +229,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; @@ -261,19 +261,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; @@ -296,20 +296,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) { @@ -322,7 +322,7 @@ public override EdmPatchMethodHandler GetNestedPatchHandler(IEdmStructuredObject } - public class NewFriendTypelessPatchHandler : EdmPatchMethodHandler + public class NewFriendTypelessPatchHandler : EdmODataAPIHandler { IEdmEntityType entityType; EdmStructuredObject friend; @@ -333,7 +333,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; @@ -351,17 +351,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; @@ -388,19 +388,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; @@ -430,20 +430,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 d9a562d8b5733e8d82efd039579552422c94af12 Mon Sep 17 00:00:00 2001 From: Sreejith Pazhampilly Date: Thu, 26 Aug 2021 14:56:23 -0700 Subject: [PATCH 07/31] updates --- .../DeltaOfTStructuralType.cs | 53 ++- .../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 +++++++++++++++++- 14 files changed, 600 insertions(+), 98 deletions(-) create mode 100644 src/Microsoft.AspNet.OData.Shared/ODataEdmAPIHandlerFactory.cs diff --git a/src/Microsoft.AspNet.OData.Shared/DeltaOfTStructuralType.cs b/src/Microsoft.AspNet.OData.Shared/DeltaOfTStructuralType.cs index d52a13dadc..7553bd79ac 100644 --- a/src/Microsoft.AspNet.OData.Shared/DeltaOfTStructuralType.cs +++ b/src/Microsoft.AspNet.OData.Shared/DeltaOfTStructuralType.cs @@ -485,43 +485,32 @@ public void Patch(TStructuralType original, IODataAPIHandler apiHandler, ODataAP { if (apiHandlerFactory != null && ODataIdContainer?.ODataIdNavigationPath != null) { - IODataAPIHandler refapiHandler = apiHandlerFactory.GetHandler(ODataIdContainer.ODataIdNavigationPath); + ApplyODataId(original, apiHandlerFactory); + } + + CopyChangedValues(original, apiHandler as ODataAPIHandler, apiHandlerFactory); + } + + private void ApplyODataId(TStructuralType original, ODataAPIHandlerFactory apiHandlerFactory) + { + IODataAPIHandler refapiHandler = apiHandlerFactory.GetHandler(ODataIdContainer.ODataIdNavigationPath); - ODataAPIHandler refapiHandlerOfT = refapiHandler as ODataAPIHandler; + ODataAPIHandler refapiHandlerOfT = refapiHandler as ODataAPIHandler; - Debug.Assert(refapiHandlerOfT != null); + Debug.Assert(refapiHandlerOfT != null); - TStructuralType referencedObj; - string error; + TStructuralType referencedObj; + string error; - if (refapiHandlerOfT.TryGet(ODataIdContainer.ODataIdNavigationPath.GetNavigationPathItems().Last().KeyProperties, out referencedObj, out error) == ODataAPIResponseStatus.Success) + if (refapiHandlerOfT.TryGet(ODataIdContainer.ODataIdNavigationPath.GetNavigationPathItems().Last().KeyProperties, out referencedObj, out error) == ODataAPIResponseStatus.Success) + { + foreach (string property in _updatableProperties) { - if (apiHandler != null) - { - ODataAPIHandler apiHandlerOfT = apiHandler as ODataAPIHandler; - - TStructuralType createdObj; - string errMsg; - if (apiHandlerOfT.TryCreate(null, out createdObj, out errMsg) == ODataAPIResponseStatus.Success) - { - foreach(string property in _updatableProperties) - { - PropertyInfo propertyInfo = _structuredType.GetProperty(property); - - object value = propertyInfo.GetValue(referencedObj); - propertyInfo.SetValue(createdObj, value); - } - } - } - else - { - original = referencedObj; - } - } - } - else - { - CopyChangedValues(original, apiHandler as ODataAPIHandler, apiHandlerFactory); + PropertyInfo propertyInfo = _structuredType.GetProperty(property); + + object value = propertyInfo.GetValue(referencedObj); + propertyInfo.SetValue(original, value); + } } } 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 4a3c66735c..9fe466e83d 100644 --- a/src/Microsoft.AspNet.OData.Shared/EdmChangedObjectCollection.cs +++ b/src/Microsoft.AspNet.OData.Shared/EdmChangedObjectCollection.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.Net.Http.Headers; @@ -86,16 +87,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(); @@ -129,6 +130,8 @@ internal EdmChangedObjectCollection CopyChangedValues(EdmODataAPIHandler apiHand } } + PatchItem(deletedObj, original as EdmStructuredObject, apiHandler, apiHandlerFactory); + changedObjectCollection.Add(deletedObj); } else @@ -162,7 +165,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); } @@ -198,20 +201,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)) @@ -222,7 +254,7 @@ private void ApplyProperties(EdmStructuredObject changedObj, EdmStructuredObject EdmODataAPIHandler apiHandlerNested = apiHandler.GetNestedHandler(originalObj, propertyName); if (apiHandlerNested != null) { - changedColl.Patch(apiHandlerNested); + changedColl.Patch(apiHandlerNested, apiHandlerFactory); } else { @@ -260,7 +292,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 a08063f1aa..e5740d6dcb 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/DeserializationHelpers.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/DeserializationHelpers.cs @@ -121,6 +121,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 90a8022b11..71df749e2c 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializer.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializer.cs @@ -568,26 +568,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 1383c12960..add262c36d 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 2a6b2bc8482f79208c8c0a126a7bed20866dd28e Mon Sep 17 00:00:00 2001 From: Sreejith Pazhampilly Date: Fri, 27 Aug 2021 14:02:52 -0700 Subject: [PATCH 08/31] updates to use only factory --- .../DeltaOfTStructuralType.cs | 49 +++--- .../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 +++- .../Microsoft.AspNet.OData.PublicApi.bsl | 89 +++++++--- 11 files changed, 359 insertions(+), 180 deletions(-) diff --git a/src/Microsoft.AspNet.OData.Shared/DeltaOfTStructuralType.cs b/src/Microsoft.AspNet.OData.Shared/DeltaOfTStructuralType.cs index 7553bd79ac..07955f2376 100644 --- a/src/Microsoft.AspNet.OData.Shared/DeltaOfTStructuralType.cs +++ b/src/Microsoft.AspNet.OData.Shared/DeltaOfTStructuralType.cs @@ -378,6 +378,12 @@ internal void CopyChangedValues(TStructuralType original, ODataAPIHandler 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 + /// The entity to be updated. /// API Handler Factory - public void Patch(TStructuralType original, IODataAPIHandler apiHandler, ODataAPIHandlerFactory apiHandlerFactory = null) + public void Patch(TStructuralType original, ODataAPIHandlerFactory apiHandlerFactory = null) { - if (apiHandlerFactory != null && ODataIdContainer?.ODataIdNavigationPath != null) - { - ApplyODataId(original, apiHandlerFactory); - } + IODataAPIHandler apiHandler = apiHandlerFactory.GetHandler(_navigationPath); + + Debug.Assert(apiHandler != null); CopyChangedValues(original, apiHandler as ODataAPIHandler, apiHandlerFactory); } + /// + /// 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); - ODataAPIHandler refapiHandlerOfT = refapiHandler as ODataAPIHandler; + if (refapiHandler != null) + { + ODataAPIHandler refapiHandlerOfT = refapiHandler as ODataAPIHandler; - Debug.Assert(refapiHandlerOfT != null); + Debug.Assert(refapiHandlerOfT != null); - TStructuralType referencedObj; - string error; + TStructuralType referencedObj; + string error; - if (refapiHandlerOfT.TryGet(ODataIdContainer.ODataIdNavigationPath.GetNavigationPathItems().Last().KeyProperties, out referencedObj, out error) == ODataAPIResponseStatus.Success) - { - foreach (string property in _updatableProperties) + //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) { - PropertyInfo propertyInfo = _structuredType.GetProperty(property); + foreach (string property in _updatableProperties) + { + PropertyInfo propertyInfo = _structuredType.GetProperty(property); - object value = propertyInfo.GetValue(referencedObj); - propertyInfo.SetValue(original, value); - } + object value = propertyInfo.GetValue(referencedObj); + propertyInfo.SetValue(original, value); + } + } } } 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 9fe466e83d..e211e272ed 100644 --- a/src/Microsoft.AspNet.OData.Shared/EdmChangedObjectCollection.cs +++ b/src/Microsoft.AspNet.OData.Shared/EdmChangedObjectCollection.cs @@ -114,14 +114,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); @@ -130,7 +145,7 @@ internal EdmChangedObjectCollection CopyChangedValues(EdmODataAPIHandler apiHand } } - PatchItem(deletedObj, original as EdmStructuredObject, apiHandler, apiHandlerFactory); + changedObjectCollection.Add(deletedObj); } @@ -138,8 +153,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; @@ -214,6 +227,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 71df749e2c..fc38cb3802 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializer.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializer.cs @@ -517,10 +517,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) @@ -534,28 +534,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, @@ -568,25 +572,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) 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 01a68e3135..36526ba8da 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,7 +15,7 @@ public enum Microsoft.AspNet.OData.EdmDeltaEntityKind : int { Unknown = 4 } -public enum Microsoft.AspNet.OData.PatchStatus : int { +public enum Microsoft.AspNet.OData.ODataAPIResponseStatus : int { Failure = 1 NotFound = 2 Success = 0 @@ -41,6 +41,7 @@ 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; } } @@ -82,7 +83,7 @@ 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.IODataAPIHandler { } public interface Microsoft.AspNet.OData.IPerRouteContainer { @@ -111,13 +112,13 @@ 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 class Microsoft.AspNet.OData.EdmODataAPIHandler { + protected EdmODataAPIHandler () - 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) + 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) } [ @@ -142,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(), @@ -155,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(), ] @@ -172,15 +194,6 @@ 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 () @@ -300,6 +313,7 @@ public class Microsoft.AspNet.OData.Delta`1 : TypedDelta, IDynamicMetaObjectProv EdmDeltaEntityKind DeltaKind { public virtual get; protected set; } System.Type ExpectedClrType { public virtual get; } + ODataIdContainer ODataIdContainer { public virtual get; public virtual set; } System.Type StructuredType { public virtual get; } IODataInstanceAnnotationContainer TransientInstanceAnnotationContainer { public virtual get; public virtual set; } @@ -310,7 +324,7 @@ 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 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) @@ -341,7 +355,7 @@ public class Microsoft.AspNet.OData.DeltaSet`1 : System.Collections.ObjectModel. protected virtual void InsertItem (int index, IDeltaSetItem item) public DeltaSet`1 Patch (ICollection`1 originalCollection) - public DeltaSet`1 Patch (IPatchMethodHandler patchHandler) + public DeltaSet`1 Patch (params ODataAPIHandlerFactory apiHandlerFactory) } [ @@ -354,7 +368,7 @@ public class Microsoft.AspNet.OData.EdmChangedObjectCollection : System.Collecti Microsoft.OData.Edm.IEdmEntityType EntityType { public get; } public virtual Microsoft.OData.Edm.IEdmTypeReference GetEdmType () - public EdmChangedObjectCollection Patch (EdmPatchMethodHandler patchHandler) + public EdmChangedObjectCollection Patch (EdmODataAPIHandler apiHandler, params ODataEdmAPIHandlerFactory apiHandlerFactory) } [ @@ -448,6 +462,7 @@ public class Microsoft.AspNet.OData.EdmEntityObject : EdmStructuredObject, IDyna 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) @@ -534,6 +549,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 () @@ -541,6 +563,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) @@ -633,6 +663,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) @@ -721,6 +760,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(), ] @@ -3677,10 +3722,6 @@ public class Microsoft.AspNet.OData.Formatter.Serialization.ODataSerializerConte System.Web.Http.Routing.UrlHelper 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 () From 1e138f3c4e455e9515fe84c0e9d652c84f6bd8e3 Mon Sep 17 00:00:00 2001 From: Sreejith Pazhampilly Date: Mon, 30 Aug 2021 11:43:44 -0700 Subject: [PATCH 09/31] updates --- .../AspNetODataSample.Web.csproj | 13 +-- samples/AspNetODataSample.Web/Web.config | 6 +- samples/AspNetODataSample.Web/packages.config | 6 +- .../DeltaOfTStructuralType.cs | 2 +- .../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 | 83 +++++++++++++------ ...Microsoft.AspNetCore3x.OData.PublicApi.bsl | 83 +++++++++++++------ 30 files changed, 311 insertions(+), 178 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 @@ - + - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 f034a86b8f..44270bb897 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 cfc417253e..270aa879ab 100644 --- a/src/Microsoft.AspNet.OData.Shared/Common/SRResources.resx +++ b/src/Microsoft.AspNet.OData.Shared/Common/SRResources.resx @@ -985,4 +985,22 @@ InstanceAnnotation Property name cannot be null or empty. + + 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 7b01ee6412..b46c0564fd 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.Diagnostics.CodeAnalysis; @@ -13,8 +14,11 @@ using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; +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 { @@ -23,7 +27,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 @@ -41,9 +45,10 @@ private static readonly ConcurrentDictionary _changedDynamicProperties; private IDictionary _dynamicDictionaryCache; - + /// /// Initializes a new instance of . /// @@ -72,8 +77,24 @@ public Delta(Type structuralType) /// The set of properties that can be updated or reset. Unknown property /// names, including those of dynamic properties, are ignored. public Delta(Type structuralType, IEnumerable updatableProperties) - : this(structuralType, updatableProperties: updatableProperties, dynamicDictionaryPropertyInfo: null) + : this(structuralType, updatableProperties: 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 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, instanceAnnotationsPropertyInfo: null) { + } /// @@ -85,19 +106,28 @@ public Delta(Type structuralType, IEnumerable updatableProperties) /// 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. + /// properties. null means this entity type is not open. + /// The property info that is used as container for Instance Annotations public Delta(Type structuralType, IEnumerable updatableProperties, - PropertyInfo dynamicDictionaryPropertyInfo) + PropertyInfo dynamicDictionaryPropertyInfo, PropertyInfo instanceAnnotationsPropertyInfo) { _dynamicDictionaryPropertyinfo = dynamicDictionaryPropertyInfo; Reset(structuralType); - InitializeProperties(updatableProperties); + InitializeProperties(updatableProperties); + TransientInstanceAnnotationContainer = new ODataInstanceAnnotationContainer(); + _instanceAnnotationsPropertyInfo = instanceAnnotationsPropertyInfo; + DeltaKind = EdmDeltaEntityKind.Entry; } /// public override Type StructuredType => _structuredType; + internal IDictionary DeltaNestedResources + { + get { return _deltaNestedResources; } + } + /// public override Type ExpectedClrType => typeof(TStructuralType); @@ -110,6 +140,18 @@ public override Type ExpectedClrType public IList UpdatableProperties => _updatableProperties; + /// + /// Gets the enum type of . + /// + public EdmDeltaEntityKind DeltaKind { get; protected set; } + + /// + public IODataInstanceAnnotationContainer TransientInstanceAnnotationContainer { get; set; } + + /// + internal PropertyInfo InstanceAnnotationsPropertyInfo { get { return _instanceAnnotationsPropertyInfo; } } + + /// public override void Clear() { @@ -124,6 +166,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. @@ -142,7 +197,7 @@ public override bool TrySetPropertyValue(string name, object value) } } - if (value is IDelta) + if (value is IDelta || value is IDeltaSet) { return TrySetNestedResourceInternal(name, value); } @@ -160,6 +215,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) @@ -180,6 +248,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. @@ -282,6 +358,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, PatchMethodHandler patchHandler = null) { if (original == null) { @@ -313,31 +396,45 @@ 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(); + IPatchMethodHandler patchHandlerNested = patchHandler.GetNestedPatchHandler(original, nestedResourceName); - // Recursively patch up the instance with the nested resources. - deltaObject.CopyChangedValues(instance); - - _allProperties[nestedResourceName].SetValue(original, instance); + if (patchHandlerNested != null) + { + deltaNestedResource.Patch(patchHandlerNested); + } } else { - // Recursively patch the subtree. - bool isDeltaType = TypedDelta.IsDeltaOfT(deltaNestedResource.GetType()); - Contract.Assert(isDeltaType, nestedResourceName + "'s corresponding value should be Delta type but is not."); + if (!TryGetPropertyRef(original, nestedResourceName, out originalNestedResource)) + { + throw Error.Argument(nestedResourceName, SRResources.DeltaNestedResourceNameNotFound, + nestedResourceName, original.GetType()); + } + + if (originalNestedResource == null) + { + // 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); + + _allProperties[nestedResourceName].SetValue(original, instance); + } + else + { + //Recursively patch the subtree. + bool isDeltaType = TypedDelta.IsDeltaOfT(deltaNestedResource.GetType()); + Contract.Assert(isDeltaType, nestedResourceName + "'s corresponding value should be Delta type but is not."); - deltaNestedResource.CopyChangedValues(originalNestedResource); + deltaNestedResource.CopyChangedValues(originalNestedResource); + } } + } } @@ -377,6 +474,17 @@ 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. + /// Patch Handler + public void Patch(TStructuralType original, IPatchMethodHandler patchHandler) + { + CopyChangedValues(original, patchHandler as PatchMethodHandler); + } + /// /// 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. @@ -506,7 +614,7 @@ private void InitializeProperties(IEnumerable updatableProperties) .Where(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(); @@ -669,11 +777,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 @@ -683,4 +796,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 e0a5912738..53d14a38e8 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/DeserializationHelpers.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/DeserializationHelpers.cs @@ -86,18 +86,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, @@ -139,15 +164,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) { @@ -167,7 +192,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); @@ -278,14 +303,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); + } } } @@ -317,11 +349,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); @@ -329,7 +362,7 @@ public static IODataInstanceAnnotationContainer GetAnnotationContainer(PropertyI else { value = propertyInfo.GetValue(resource); - } + } IODataInstanceAnnotationContainer instanceAnnotationContainer = value as IODataInstanceAnnotationContainer; @@ -345,8 +378,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 612681fa44..859c05b720 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,11 +255,11 @@ public virtual object CreateResourceInstance(IEdmStructuredTypeReference structu structuredType.StructuredDefinition(), model); return Activator.CreateInstance(readContext.ResourceType, clrType, structuralProperties, - dynamicDictionaryPropertyInfo); + dynamicDictionaryPropertyInfo, instanceAnnotationProperty); } else { - return Activator.CreateInstance(readContext.ResourceType, clrType, structuralProperties); + return Activator.CreateInstance(readContext.ResourceType, clrType, structuralProperties, null, 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 3abb9b19a3..659c5bc2e6 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; @@ -905,14 +916,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()) @@ -1045,6 +1059,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 15cd2d212f..8a9ace2dbf 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 dbceffe86f..c5f6b319ce 100644 --- a/src/Microsoft.AspNet.OData/GlobalSuppressions.cs +++ b/src/Microsoft.AspNet.OData/GlobalSuppressions.cs @@ -65,3 +65,37 @@ [assembly: SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Scope = "member", Target = "Microsoft.AspNet.OData.Builder.IODataInstanceAnnotationContainer.#GetAllResourceAnnotations()")] [assembly: SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Scope = "member", Target = "Microsoft.AspNet.OData.Builder.IODataInstanceAnnotationContainer.#GetResourceAnnotations()")] [assembly: SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Scope = "member", Target = "Microsoft.AspNet.OData.Common.Error.#PropertyNullOrWhiteSpace()")] +[assembly: SuppressMessage("Microsoft.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 6b439e284b..b0f2a61260 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 90cd827f5f..cb1fcd66f3 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 @@ -1399,6 +1399,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 8baade2685..82625bd304 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 @@ -1662,6 +1662,18 @@ Validation\DeltaOfTValidationTests.cs + + BulkOperation\BulkInsertDataModel.cs + + + BulkOperation\BulkInsertEdmModel.cs + + + BulkOperation\BulkInsertController.cs + + + BulkOperation\BulkOperationPatchHandlers.cs + @@ -1675,4 +1687,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 0baf7ca3cb..5f3c88d82f 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 1d86167a98..3221c66fdf 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,32 @@ 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("MERGE"), 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,13 +425,14 @@ 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); } + } public class DeltaCustomersController : TestODataController @@ -457,6 +478,7 @@ public ITestActionResult Put([FromODataUri] int key, [FromBody] Delta c.Id == key).FirstOrDefault(); entity.Put(customer); return Ok(customer); + } [AcceptVerbs("PATCH", "MERGE")] @@ -486,6 +508,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 78baf32dcc..25d0ce1d6c 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 @@ -28,6 +28,7 @@ using Microsoft.OData.Edm; using Moq; using Xunit; +using Microsoft.AspNet.OData.Builder; namespace Microsoft.AspNet.OData.Test.Formatter.Deserialization { @@ -328,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 06849bc2fb..ea9e57a197 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 a03b52d19f..41650fe364 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.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(), ] @@ -140,6 +172,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 () @@ -250,16 +291,21 @@ 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, System.Reflection.PropertyInfo instanceAnnotationsPropertyInfo) + EdmDeltaEntityKind DeltaKind { public virtual get; protected set; } System.Type ExpectedClrType { public virtual get; } 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) @@ -267,12 +313,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(), ] @@ -280,7 +354,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) } [ @@ -368,10 +445,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 () } [ @@ -3135,6 +3218,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) @@ -3169,6 +3285,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(), ] @@ -3298,7 +3421,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 { @@ -3308,18 +3435,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 { @@ -3480,8 +3611,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) @@ -3548,6 +3680,10 @@ public class Microsoft.AspNet.OData.Formatter.Serialization.ODataSerializerConte System.Web.Http.Routing.UrlHelper 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.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 794be95048..219db70056 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,16 +306,21 @@ 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, System.Reflection.PropertyInfo instanceAnnotationsPropertyInfo) + EdmDeltaEntityKind DeltaKind { public virtual get; protected set; } System.Type ExpectedClrType { public virtual get; } 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) @@ -282,12 +328,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(), ] @@ -295,7 +369,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) } [ @@ -383,10 +460,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 () } [ @@ -3349,6 +3432,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) @@ -3383,6 +3499,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(), ] @@ -3511,7 +3634,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 { @@ -3521,18 +3648,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 { @@ -3693,8 +3824,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) @@ -3760,6 +3892,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 b2cad9a7c2..6d39bb9e80 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.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(), ] @@ -151,6 +183,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 () @@ -269,16 +310,21 @@ 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, System.Reflection.PropertyInfo instanceAnnotationsPropertyInfo) + EdmDeltaEntityKind DeltaKind { public virtual get; protected set; } System.Type ExpectedClrType { public virtual get; } 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) @@ -286,12 +332,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(), ] @@ -299,7 +373,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) } [ @@ -387,10 +464,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 () } [ @@ -3548,6 +3631,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) @@ -3582,6 +3698,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(), ] @@ -3710,7 +3833,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 { @@ -3720,18 +3847,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 { @@ -3892,8 +4023,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) @@ -3959,6 +4091,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 () From 934c726eff9cfd60056ca8c0e9863717fb6a6db9 Mon Sep 17 00:00:00 2001 From: Elizabeth Okerio Date: Fri, 11 Jun 2021 15:09:25 +0300 Subject: [PATCH 11/31] 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 859c05b720..c16b86a184 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 ea9e57a197..466cff1d0a 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 13ec941816a6e1f8cd25cd84aea94079d61df9d1 Mon Sep 17 00:00:00 2001 From: Sreejith Pazhampilly Date: Thu, 8 Jul 2021 11:20:12 -0700 Subject: [PATCH 12/31] 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 c16b86a184..ae448eeadd 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 c5f6b319ce..81d96d4e98 100644 --- a/src/Microsoft.AspNet.OData/GlobalSuppressions.cs +++ b/src/Microsoft.AspNet.OData/GlobalSuppressions.cs @@ -69,33 +69,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 46a911407985120571bb6301009610119bc4fb4f Mon Sep 17 00:00:00 2001 From: Sreejith Pazhampilly Date: Fri, 9 Jul 2021 07:31:10 -0700 Subject: [PATCH 13/31] 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 54b70eea92b6d162920d0fc52079963f81fc4ca8 Mon Sep 17 00:00:00 2001 From: Sreejith Pazhampilly Date: Tue, 27 Jul 2021 07:26:40 -0700 Subject: [PATCH 14/31] 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 8a9ace2dbf..a0a28deebc 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 ae9291748591eb06c2d8aa3d48fa4e9a7c05d8ba Mon Sep 17 00:00:00 2001 From: Sreejith Pazhampilly Date: Tue, 24 Aug 2021 13:00:27 -0700 Subject: [PATCH 15/31] update for odataid _temp --- .../DefaultEdmPatchMethodHandler.cs | 28 +- ...chHandler.cs => DefaultODataAPIHandler.cs} | 32 +- .../DeltaOfTStructuralType.cs | 61 ++- .../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 +- 27 files changed, 1173 insertions(+), 333 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/DeltaOfTStructuralType.cs b/src/Microsoft.AspNet.OData.Shared/DeltaOfTStructuralType.cs index b46c0564fd..7dbc18264c 100644 --- a/src/Microsoft.AspNet.OData.Shared/DeltaOfTStructuralType.cs +++ b/src/Microsoft.AspNet.OData.Shared/DeltaOfTStructuralType.cs @@ -48,6 +48,7 @@ private static readonly ConcurrentDictionary _changedDynamicProperties; private IDictionary _dynamicDictionaryCache; + private NavigationPath _navigationPath; /// /// Initializes a new instance of . @@ -116,6 +117,7 @@ public Delta(Type structuralType, IEnumerable updatableProperties, InitializeProperties(updatableProperties); TransientInstanceAnnotationContainer = new ODataInstanceAnnotationContainer(); _instanceAnnotationsPropertyInfo = instanceAnnotationsPropertyInfo; + _navigationPath = new NavigationPath(structuralType.Name, null); DeltaKind = EdmDeltaEntityKind.Entry; } @@ -148,6 +150,9 @@ public IList UpdatableProperties /// public IODataInstanceAnnotationContainer TransientInstanceAnnotationContainer { get; set; } + /// + public ODataIdContainer ODataIdContainer { get; set; } + /// internal PropertyInfo InstanceAnnotationsPropertyInfo { get { return _instanceAnnotationsPropertyInfo; } } @@ -364,7 +369,7 @@ public void CopyChangedValues(TStructuralType original) [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")] [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily")] - internal void CopyChangedValues(TStructuralType original, PatchMethodHandler patchHandler = null) + internal void CopyChangedValues(TStructuralType original, ODataAPIHandler apiHandler = null, ODataAPIHandlerFactory apiHandlerFactory = null) { if (original == null) { @@ -399,11 +404,11 @@ internal void CopyChangedValues(TStructuralType original, PatchMethodHandlerThe semantics of this operation are equivalent to a HTTP PATCH operation, hence the name. /// /// The entity to be updated. - /// Patch Handler - public void Patch(TStructuralType original, IPatchMethodHandler patchHandler) - { - CopyChangedValues(original, patchHandler as PatchMethodHandler); + /// API Handler + /// API Handler Factory + public void Patch(TStructuralType original, IODataAPIHandler apiHandler, ODataAPIHandlerFactory apiHandlerFactory = null) + { + if (apiHandlerFactory != null && ODataIdContainer?.ODataIdNavigationPath != null) + { + IODataAPIHandler refapiHandler = apiHandlerFactory.GetHandler(ODataIdContainer.ODataIdNavigationPath); + + ODataAPIHandler refapiHandlerOfT = refapiHandler as ODataAPIHandler; + + Debug.Assert(refapiHandlerOfT != null); + + TStructuralType referencedObj; + string error; + + if (refapiHandlerOfT.TryGet(ODataIdContainer.ODataIdNavigationPath.GetNavigationPathItems().Last().KeyProperties, out referencedObj, out error) == ODataAPIResponseStatus.Success) + { + if (apiHandler != null) + { + ODataAPIHandler apiHandlerOfT = apiHandler as ODataAPIHandler; + + TStructuralType createdObj; + string errMsg; + if (apiHandlerOfT.TryCreate(null, out createdObj, out errMsg) == ODataAPIResponseStatus.Success) + { + foreach(string property in _updatableProperties) + { + PropertyInfo propertyInfo = _structuredType.GetProperty(property); + + object value = propertyInfo.GetValue(referencedObj); + propertyInfo.SetValue(createdObj, value); + } + } + } + else + { + original = referencedObj; + } + } + } + else + { + CopyChangedValues(original, apiHandler as ODataAPIHandler, apiHandlerFactory); + } } /// 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 ae448eeadd..a27c035284 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 a0a28deebc..afe741c19a 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 81d96d4e98..3769dfce8c 100644 --- a/src/Microsoft.AspNet.OData/GlobalSuppressions.cs +++ b/src/Microsoft.AspNet.OData/GlobalSuppressions.cs @@ -70,18 +70,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 072da71f38e5b4e907666012a02ea405c694e0a2 Mon Sep 17 00:00:00 2001 From: Sreejith Pazhampilly Date: Thu, 26 Aug 2021 14:56:23 -0700 Subject: [PATCH 16/31] updates --- .../DeltaOfTStructuralType.cs | 53 ++- .../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 +++++++++++++++++- 14 files changed, 600 insertions(+), 98 deletions(-) create mode 100644 src/Microsoft.AspNet.OData.Shared/ODataEdmAPIHandlerFactory.cs diff --git a/src/Microsoft.AspNet.OData.Shared/DeltaOfTStructuralType.cs b/src/Microsoft.AspNet.OData.Shared/DeltaOfTStructuralType.cs index 7dbc18264c..92097b9759 100644 --- a/src/Microsoft.AspNet.OData.Shared/DeltaOfTStructuralType.cs +++ b/src/Microsoft.AspNet.OData.Shared/DeltaOfTStructuralType.cs @@ -490,43 +490,32 @@ public void Patch(TStructuralType original, IODataAPIHandler apiHandler, ODataAP { if (apiHandlerFactory != null && ODataIdContainer?.ODataIdNavigationPath != null) { - IODataAPIHandler refapiHandler = apiHandlerFactory.GetHandler(ODataIdContainer.ODataIdNavigationPath); + ApplyODataId(original, apiHandlerFactory); + } + + CopyChangedValues(original, apiHandler as ODataAPIHandler, apiHandlerFactory); + } + + private void ApplyODataId(TStructuralType original, ODataAPIHandlerFactory apiHandlerFactory) + { + IODataAPIHandler refapiHandler = apiHandlerFactory.GetHandler(ODataIdContainer.ODataIdNavigationPath); - ODataAPIHandler refapiHandlerOfT = refapiHandler as ODataAPIHandler; + ODataAPIHandler refapiHandlerOfT = refapiHandler as ODataAPIHandler; - Debug.Assert(refapiHandlerOfT != null); + Debug.Assert(refapiHandlerOfT != null); - TStructuralType referencedObj; - string error; + TStructuralType referencedObj; + string error; - if (refapiHandlerOfT.TryGet(ODataIdContainer.ODataIdNavigationPath.GetNavigationPathItems().Last().KeyProperties, out referencedObj, out error) == ODataAPIResponseStatus.Success) + if (refapiHandlerOfT.TryGet(ODataIdContainer.ODataIdNavigationPath.GetNavigationPathItems().Last().KeyProperties, out referencedObj, out error) == ODataAPIResponseStatus.Success) + { + foreach (string property in _updatableProperties) { - if (apiHandler != null) - { - ODataAPIHandler apiHandlerOfT = apiHandler as ODataAPIHandler; - - TStructuralType createdObj; - string errMsg; - if (apiHandlerOfT.TryCreate(null, out createdObj, out errMsg) == ODataAPIResponseStatus.Success) - { - foreach(string property in _updatableProperties) - { - PropertyInfo propertyInfo = _structuredType.GetProperty(property); - - object value = propertyInfo.GetValue(referencedObj); - propertyInfo.SetValue(createdObj, value); - } - } - } - else - { - original = referencedObj; - } - } - } - else - { - CopyChangedValues(original, apiHandler as ODataAPIHandler, apiHandlerFactory); + PropertyInfo propertyInfo = _structuredType.GetProperty(property); + + object value = propertyInfo.GetValue(referencedObj); + propertyInfo.SetValue(original, value); + } } } 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 53d14a38e8..d0bb84036f 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/DeserializationHelpers.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/DeserializationHelpers.cs @@ -125,6 +125,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 a27c035284..eb10d0a5f5 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 afe741c19a..faf185759b 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 1e890276c7db3cbc5792b0b86de031ee042ce6e1 Mon Sep 17 00:00:00 2001 From: Sreejith Pazhampilly Date: Fri, 27 Aug 2021 14:02:52 -0700 Subject: [PATCH 17/31] updates to use only factory --- .../DeltaOfTStructuralType.cs | 49 +++--- .../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 +++- .../Microsoft.AspNet.OData.PublicApi.bsl | 89 +++++++--- 11 files changed, 359 insertions(+), 180 deletions(-) diff --git a/src/Microsoft.AspNet.OData.Shared/DeltaOfTStructuralType.cs b/src/Microsoft.AspNet.OData.Shared/DeltaOfTStructuralType.cs index 92097b9759..ebedff68f8 100644 --- a/src/Microsoft.AspNet.OData.Shared/DeltaOfTStructuralType.cs +++ b/src/Microsoft.AspNet.OData.Shared/DeltaOfTStructuralType.cs @@ -383,6 +383,12 @@ internal void CopyChangedValues(TStructuralType original, ODataAPIHandler 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 + /// The entity to be updated. /// API Handler Factory - public void Patch(TStructuralType original, IODataAPIHandler apiHandler, ODataAPIHandlerFactory apiHandlerFactory = null) + public void Patch(TStructuralType original, ODataAPIHandlerFactory apiHandlerFactory = null) { - if (apiHandlerFactory != null && ODataIdContainer?.ODataIdNavigationPath != null) - { - ApplyODataId(original, apiHandlerFactory); - } + IODataAPIHandler apiHandler = apiHandlerFactory.GetHandler(_navigationPath); + + Debug.Assert(apiHandler != null); CopyChangedValues(original, apiHandler as ODataAPIHandler, apiHandlerFactory); } + /// + /// 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); - ODataAPIHandler refapiHandlerOfT = refapiHandler as ODataAPIHandler; + if (refapiHandler != null) + { + ODataAPIHandler refapiHandlerOfT = refapiHandler as ODataAPIHandler; - Debug.Assert(refapiHandlerOfT != null); + Debug.Assert(refapiHandlerOfT != null); - TStructuralType referencedObj; - string error; + TStructuralType referencedObj; + string error; - if (refapiHandlerOfT.TryGet(ODataIdContainer.ODataIdNavigationPath.GetNavigationPathItems().Last().KeyProperties, out referencedObj, out error) == ODataAPIResponseStatus.Success) - { - foreach (string property in _updatableProperties) + //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) { - PropertyInfo propertyInfo = _structuredType.GetProperty(property); + foreach (string property in _updatableProperties) + { + PropertyInfo propertyInfo = _structuredType.GetProperty(property); - object value = propertyInfo.GetValue(referencedObj); - propertyInfo.SetValue(original, value); - } + object value = propertyInfo.GetValue(referencedObj); + propertyInfo.SetValue(original, value); + } + } } } 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 eb10d0a5f5..9c3bc48a5d 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) 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 41650fe364..abda06f614 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,7 +15,7 @@ public enum Microsoft.AspNet.OData.EdmDeltaEntityKind : int { Unknown = 4 } -public enum Microsoft.AspNet.OData.PatchStatus : int { +public enum Microsoft.AspNet.OData.ODataAPIResponseStatus : int { Failure = 1 NotFound = 2 Success = 0 @@ -41,6 +41,7 @@ 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; } } @@ -82,7 +83,7 @@ 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.IODataAPIHandler { } public interface Microsoft.AspNet.OData.IPerRouteContainer { @@ -111,13 +112,13 @@ 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 class Microsoft.AspNet.OData.EdmODataAPIHandler { + protected EdmODataAPIHandler () - 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) + 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) } [ @@ -142,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(), @@ -155,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(), ] @@ -172,15 +194,6 @@ 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 () @@ -300,6 +313,7 @@ public class Microsoft.AspNet.OData.Delta`1 : TypedDelta, IDynamicMetaObjectProv EdmDeltaEntityKind DeltaKind { public virtual get; protected set; } System.Type ExpectedClrType { public virtual 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; } @@ -313,7 +327,7 @@ 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 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) @@ -344,7 +358,7 @@ public class Microsoft.AspNet.OData.DeltaSet`1 : System.Collections.ObjectModel. protected virtual void InsertItem (int index, IDeltaSetItem item) public DeltaSet`1 Patch (ICollection`1 originalCollection) - public DeltaSet`1 Patch (IPatchMethodHandler patchHandler) + public DeltaSet`1 Patch (params ODataAPIHandlerFactory apiHandlerFactory) } [ @@ -357,7 +371,7 @@ public class Microsoft.AspNet.OData.EdmChangedObjectCollection : System.Collecti Microsoft.OData.Edm.IEdmEntityType EntityType { public get; } public virtual Microsoft.OData.Edm.IEdmTypeReference GetEdmType () - public EdmChangedObjectCollection Patch (EdmPatchMethodHandler patchHandler) + public EdmChangedObjectCollection Patch (EdmODataAPIHandler apiHandler, params ODataEdmAPIHandlerFactory apiHandlerFactory) } [ @@ -451,6 +465,7 @@ public class Microsoft.AspNet.OData.EdmEntityObject : EdmStructuredObject, IDyna 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) @@ -537,6 +552,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 () @@ -544,6 +566,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) @@ -636,6 +666,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) @@ -724,6 +763,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(), ] @@ -3680,10 +3725,6 @@ public class Microsoft.AspNet.OData.Formatter.Serialization.ODataSerializerConte System.Web.Http.Routing.UrlHelper 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 () From f08f13e3a08ae887abab6389ed3c9d96d5e8b479 Mon Sep 17 00:00:00 2001 From: Sreejith Pazhampilly Date: Mon, 30 Aug 2021 11:43:44 -0700 Subject: [PATCH 18/31] updates --- .../AspNetODataSample.Web.csproj | 13 +-- samples/AspNetODataSample.Web/Web.config | 6 +- samples/AspNetODataSample.Web/packages.config | 6 +- .../DeltaOfTStructuralType.cs | 2 +- .../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 | 83 +++++++++++++------ ...Microsoft.AspNetCore3x.OData.PublicApi.bsl | 83 +++++++++++++------ 30 files changed, 311 insertions(+), 178 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 - 10 - + 6 + 0 + beta From 1a3b3a34aaeab620ad93424ccb16bae8858be6f9 Mon Sep 17 00:00:00 2001 From: Sreejith Pazhampilly Date: Wed, 1 Sep 2021 22:00:42 -0700 Subject: [PATCH 27/31] Update WebStack.versions.settings.targets --- tools/WebStack.versions.settings.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/WebStack.versions.settings.targets b/tools/WebStack.versions.settings.targets index da99da57b4..bdd9999546 100644 --- a/tools/WebStack.versions.settings.targets +++ b/tools/WebStack.versions.settings.targets @@ -6,7 +6,7 @@ 7 6 0 - beta + From 3a3e2cb7384707c66d61fa300cb2732e2dcf8d1c Mon Sep 17 00:00:00 2001 From: Sreejith Pazhampilly Date: Thu, 2 Sep 2021 18:50:59 -0700 Subject: [PATCH 28/31] Update GetNugetPackageMetadata.proj --- tools/GetNugetPackageMetadata.proj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tools/GetNugetPackageMetadata.proj b/tools/GetNugetPackageMetadata.proj index 2d9f3abfa2..723a88ffb3 100644 --- a/tools/GetNugetPackageMetadata.proj +++ b/tools/GetNugetPackageMetadata.proj @@ -1,5 +1,8 @@ + + $([System.DateTime]::Now.ToString("yyyyMMddHHmm")) + @@ -11,4 +14,5 @@ + \ No newline at end of file From 4a6bf158f67f72196903c2c8866a83171a03d496 Mon Sep 17 00:00:00 2001 From: Sreejith Pazhampilly Date: Thu, 2 Sep 2021 23:10:54 -0700 Subject: [PATCH 29/31] Update WebStack.versions.settings.targets --- tools/WebStack.versions.settings.targets | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tools/WebStack.versions.settings.targets b/tools/WebStack.versions.settings.targets index bdd9999546..bf5619baf5 100644 --- a/tools/WebStack.versions.settings.targets +++ b/tools/WebStack.versions.settings.targets @@ -41,6 +41,10 @@ $(VersionFullSemantic)-$(VersionRelease) + + $([System.DateTime]::Now.ToString("yyyyMMddHHmm")) + +