From 94b6444a37ee13ae3ffbb99d22175f4f19ad0140 Mon Sep 17 00:00:00 2001 From: John Gathogo Date: Tue, 28 Sep 2021 11:48:42 +0300 Subject: [PATCH] Provide option for alternative form of delete link request Uri --- src/Microsoft.OData.Client/BaseSaveResult.cs | 24 ++- .../DataServiceContext.cs | 12 ++ .../DeleteLinkUriOption.cs | 26 +++ ...Microsoft.OData.Client.TDDUnitTests.csproj | 1 + .../Client.TDD.Tests/Tests/DeleteLinkTests.cs | 204 ++++++++++++++++++ 5 files changed, 264 insertions(+), 3 deletions(-) create mode 100644 src/Microsoft.OData.Client/DeleteLinkUriOption.cs create mode 100644 test/FunctionalTests/Tests/DataServices/UnitTests/Client.TDD.Tests/Tests/DeleteLinkTests.cs diff --git a/src/Microsoft.OData.Client/BaseSaveResult.cs b/src/Microsoft.OData.Client/BaseSaveResult.cs index 1891777a77..335a753107 100644 --- a/src/Microsoft.OData.Client/BaseSaveResult.cs +++ b/src/Microsoft.OData.Client/BaseSaveResult.cs @@ -1093,7 +1093,7 @@ private static string SerializeSupportedVersions() /// The binding. /// The target's entity descriptor. /// The original link uri or one with the target entity key appended. - private static Uri AppendTargetEntityKeyIfNeeded(Uri linkUri, LinkDescriptor binding, EntityDescriptor targetResource) + private Uri AppendTargetEntityKeyIfNeeded(Uri linkUri, LinkDescriptor binding, EntityDescriptor targetResource) { // To delete from a collection, we need to append the key. // For example: if the navigation property name is "Purchases" and the resource type is Order with key '1', then this method will generate 'baseuri/Purchases(1)' @@ -1104,8 +1104,26 @@ private static Uri AppendTargetEntityKeyIfNeeded(Uri linkUri, LinkDescriptor bin Debug.Assert(targetResource != null, "targetResource != null"); StringBuilder builder = new StringBuilder(); - builder.Append(UriUtil.UriToString(linkUri)); - builder.Append(UriHelper.QUESTIONMARK + XmlConstants.HttpQueryStringId + UriHelper.EQUALSSIGN + targetResource.Identity); + string uriString = UriUtil.UriToString(linkUri); + + if (this.RequestInfo.Context.DeleteLinkUriOption == DeleteLinkUriOption.RelatedKeyAsSegment) + { + // Related key segment should appear before /$ref + int indexOfLinkSegment = uriString.IndexOf(XmlConstants.UriLinkSegment, StringComparison.Ordinal); + builder.Append(indexOfLinkSegment > 0 ? uriString.Substring(0, indexOfLinkSegment - 1) : uriString); + this.RequestInfo.Context.UrlKeyDelimiter.AppendKeyExpression(targetResource.EdmValue, builder); + if (indexOfLinkSegment > 0) + { + builder.Append('/'); + builder.Append(XmlConstants.UriLinkSegment); + } + } + else + { + builder.Append(uriString); + builder.Append(UriHelper.QUESTIONMARK + XmlConstants.HttpQueryStringId + UriHelper.EQUALSSIGN + targetResource.Identity); + } + return UriUtil.CreateUri(builder.ToString(), UriKind.RelativeOrAbsolute); } diff --git a/src/Microsoft.OData.Client/DataServiceContext.cs b/src/Microsoft.OData.Client/DataServiceContext.cs index a7e755bdb4..8ae3c0dddf 100644 --- a/src/Microsoft.OData.Client/DataServiceContext.cs +++ b/src/Microsoft.OData.Client/DataServiceContext.cs @@ -173,6 +173,9 @@ public class DataServiceContext private ConcurrentDictionary resolveTypesCache = new ConcurrentDictionary(); + /// Used to specify the option for the form of Uri to be generated for a delete link request. + private DeleteLinkUriOption deleteLinkUriOption; + #region Test hooks for header and payload verification #pragma warning disable 0169, 0649 @@ -285,6 +288,7 @@ internal DataServiceContext(Uri serviceRoot, ODataProtocolVersion maxProtocolVer this.UsingDataServiceCollection = false; this.UsePostTunneling = false; this.keyComparisonGeneratesFilterQuery = false; + this.deleteLinkUriOption = DeleteLinkUriOption.DollarIdQueryParam; } #endregion @@ -689,6 +693,14 @@ public virtual bool KeyComparisonGeneratesFilterQuery get { return this.keyComparisonGeneratesFilterQuery; } set { this.keyComparisonGeneratesFilterQuery = value; } } + + /// Gets or sets the option for the form of Uri to be generated for a delete link request. + public virtual DeleteLinkUriOption DeleteLinkUriOption + { + get { return this.deleteLinkUriOption; } + set { this.deleteLinkUriOption = value; } + } + /// Gets or sets whether to support undeclared properties. /// UndeclaredPropertyBehavior. internal UndeclaredPropertyBehavior UndeclaredPropertyBehavior diff --git a/src/Microsoft.OData.Client/DeleteLinkUriOption.cs b/src/Microsoft.OData.Client/DeleteLinkUriOption.cs new file mode 100644 index 0000000000..f25b2e1bff --- /dev/null +++ b/src/Microsoft.OData.Client/DeleteLinkUriOption.cs @@ -0,0 +1,26 @@ +//--------------------------------------------------------------------- +// +// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. +// +//--------------------------------------------------------------------- + +namespace Microsoft.OData.Client +{ + /// + /// Used to specify the form of Uri to be used for a delete link request. + /// + public enum DeleteLinkUriOption + { + /// + /// Pass the id of the related entity as a query param, i.e., + /// {ServiceUri}/{EntitySet}/{Key}/{NavigationProperty}/$ref?$id={ServiceUri}/{RelatedEntitySet}/{RelatedKey} + /// + DollarIdQueryParam = 0, + + /// + /// Pass the id of the related entity as a key segment, i.e., + /// {ServiceUri}/{EntitySet}/{Key}/{NavigationProperty}/{RelatedKey}/$ref + /// + RelatedKeyAsSegment = 1, + } +} diff --git a/test/FunctionalTests/Tests/DataServices/UnitTests/Client.TDD.Tests/Microsoft.OData.Client.TDDUnitTests.csproj b/test/FunctionalTests/Tests/DataServices/UnitTests/Client.TDD.Tests/Microsoft.OData.Client.TDDUnitTests.csproj index 5ee458bd21..af8cfa9aa5 100644 --- a/test/FunctionalTests/Tests/DataServices/UnitTests/Client.TDD.Tests/Microsoft.OData.Client.TDDUnitTests.csproj +++ b/test/FunctionalTests/Tests/DataServices/UnitTests/Client.TDD.Tests/Microsoft.OData.Client.TDDUnitTests.csproj @@ -58,6 +58,7 @@ + diff --git a/test/FunctionalTests/Tests/DataServices/UnitTests/Client.TDD.Tests/Tests/DeleteLinkTests.cs b/test/FunctionalTests/Tests/DataServices/UnitTests/Client.TDD.Tests/Tests/DeleteLinkTests.cs new file mode 100644 index 0000000000..aa315471b7 --- /dev/null +++ b/test/FunctionalTests/Tests/DataServices/UnitTests/Client.TDD.Tests/Tests/DeleteLinkTests.cs @@ -0,0 +1,204 @@ +//--------------------------------------------------------------------- +// +// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. +// +//--------------------------------------------------------------------- + +namespace Microsoft.OData.Client.TDDUnitTests.Tests +{ + using System; + using System.ComponentModel; + using Microsoft.OData.Edm; + using Xunit; + + public class DeleteLinkTests + { + private const string NamespaceName = "Microsoft.OData.Client.TDDUnitTests.Tests"; + private const string ServiceUri = "http://tempuri.svc"; + private EdmModel model; + private TestDataServiceContext dataServiceContext; + + public DeleteLinkTests() + { + this.InitializeEdmModel(); + this.dataServiceContext = new TestDataServiceContext(new Uri(ServiceUri), this.model); + } + + [Theory] + [InlineData(DeleteLinkUriOption.DollarIdQueryParam, "http://tempuri.svc/Customers(1)/Orders/$ref?$id=http://tempuri.svc/Orders(1)")] + [InlineData(DeleteLinkUriOption.RelatedKeyAsSegment, "http://tempuri.svc/Customers(1)/Orders(1)/$ref")] + public void ExpectedDeleteLinkUriShouldBeGenerated(DeleteLinkUriOption deleteLinkUriOption, string expectedUri) + { + this.dataServiceContext.DeleteLinkUriOption = deleteLinkUriOption; + + var customer = new Customer { Id = 1 }; + var order = new Order { Id = 1 }; + + var customerCollection = new DataServiceCollection( + dataServiceContext, new[] { customer }, + TrackingMode.AutoChangeTracking, + "Customers", + null, + null); + var orderCollection = new DataServiceCollection( + dataServiceContext, + new[] { order }, + TrackingMode.AutoChangeTracking, + "Orders", + null, + null); + + this.dataServiceContext.DeleteLink(customer, "Orders", order); + var saveResult = new TestSaveResult(this.dataServiceContext, "SaveChanges", SaveChangesOptions.None, null, null); + + // The API does not offer an easy way to grap the created request and inspect the Uri so we ride on an extensibility hook + this.dataServiceContext.SendingRequest2 += (sender, args) => + { + Assert.Equal(expectedUri, args.RequestMessage.Url.AbsoluteUri); + }; + + // If SendingRequest2 event if not fired, an exception is thrown and the test will fail + saveResult.CreateRequestAndFireSendingEvent(); + } + + private void InitializeEdmModel() + { + model = new EdmModel(); + + var orderEntityType = new EdmEntityType(NamespaceName, "Order"); + orderEntityType.AddKeys(orderEntityType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); + model.AddElement(orderEntityType); + + var customerEntityType = new EdmEntityType(NamespaceName, "Customer"); + customerEntityType.AddKeys(customerEntityType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); + var ordersNavProperty = customerEntityType.AddUnidirectionalNavigation( + new EdmNavigationPropertyInfo + { + Name = "Orders", + Target = orderEntityType, + TargetMultiplicity = EdmMultiplicity.Many + }); + model.AddElement(customerEntityType); + + var entityContainer = new EdmEntityContainer(NamespaceName, "Container"); + model.AddElement(entityContainer); + + var orderEntitySet = entityContainer.AddEntitySet("Orders", orderEntityType); + var customerEntitySet = entityContainer.AddEntitySet("Customers", customerEntityType); + customerEntitySet.AddNavigationTarget(ordersNavProperty, orderEntitySet); + } + + [Key("Id")] + internal partial class Customer : BaseEntityType, INotifyPropertyChanged + { + public virtual int Id + { + get + { + return this._Id; + } + set + { + this.OnIdChanging(value); + this._Id = value; + this.OnIdChanged(); + this.OnPropertyChanged("Id"); + } + } + private int _Id; + partial void OnIdChanging(int value); + partial void OnIdChanged(); + + public virtual DataServiceCollection Orders + { + get + { + return this._Orders; + } + set + { + this.OnOrdersChanging(value); + this._Orders = value; + this.OnOrdersChanged(); + this.OnPropertyChanged("Orders"); + } + } + private DataServiceCollection _Orders = new DataServiceCollection(null, TrackingMode.None); + partial void OnOrdersChanging(DataServiceCollection value); + partial void OnOrdersChanged(); + + public event PropertyChangedEventHandler PropertyChanged; + protected virtual void OnPropertyChanged(string property) + { + if ((this.PropertyChanged != null)) + { + this.PropertyChanged(this, new PropertyChangedEventArgs(property)); + } + } + } + + [Key("Id")] + internal partial class Order : BaseEntityType, INotifyPropertyChanged + { + public virtual int Id + { + get + { + return this._Id; + } + set + { + this.OnIdChanging(value); + this._Id = value; + this.OnIdChanged(); + this.OnPropertyChanged("Id"); + } + } + private int _Id; + partial void OnIdChanging(int value); + partial void OnIdChanged(); + + public event PropertyChangedEventHandler PropertyChanged; + protected virtual void OnPropertyChanged(string property) + { + if ((this.PropertyChanged != null)) + { + this.PropertyChanged(this, new PropertyChangedEventArgs(property)); + } + } + } + + internal partial class TestDataServiceContext : DataServiceContext + { + public TestDataServiceContext(Uri serviceRoot, IEdmModel serviceModel) : + base(serviceRoot, ODataProtocolVersion.V4) + { + this.Format.UseJson(serviceModel); + } + } + + internal class TestSaveResult : SaveResult + { + public TestSaveResult(DataServiceContext context, string method, SaveChangesOptions options, AsyncCallback callback, object state) + : base(context, method, options, callback, state) + { + } + + internal void CreateRequestAndFireSendingEvent() + { + if (this.ChangedEntries.Count > 0) + { + if (this.ChangedEntries[0] is LinkDescriptor descriptor) + { + var requestMessageWrapper = this.CreateRequest(descriptor); + requestMessageWrapper.FireSendingEventHandlers(descriptor); + + return; + } + } + + throw new Exception(); // Throw exception to signal unexpected outcome + } + } + } +}