diff --git a/src/Microsoft.OData.Client/BaseSaveResult.cs b/src/Microsoft.OData.Client/BaseSaveResult.cs index 530d92de69..c707024008 100644 --- a/src/Microsoft.OData.Client/BaseSaveResult.cs +++ b/src/Microsoft.OData.Client/BaseSaveResult.cs @@ -1047,6 +1047,7 @@ private static void HandleResponsePost(LinkDescriptor linkDescriptor) Error.ThrowBatchUnexpectedContent(InternalError.LinkNotAddedState); } + linkDescriptor.DependsOnIds = null; linkDescriptor.State = EntityStates.Unchanged; } @@ -1226,6 +1227,7 @@ private void HandleResponsePost(EntityDescriptor entityDescriptor, string etag) { entityDescriptor.ETag = etag; entityDescriptor.State = EntityStates.Unchanged; + entityDescriptor.DependsOnIds = null; entityDescriptor.PropertiesToSerialize.Clear(); } @@ -1297,6 +1299,7 @@ private void HandleResponsePut(Descriptor descriptor, HeaderCollection responseH Debug.Assert(entityDescriptor.State == EntityStates.Modified, "descriptor.State == EntityStates.Modified"); entityDescriptor.ETag = etag; entityDescriptor.State = EntityStates.Unchanged; + entityDescriptor.DependsOnIds = null; entityDescriptor.PropertiesToSerialize.Clear(); } } @@ -1306,6 +1309,7 @@ private void HandleResponsePut(Descriptor descriptor, HeaderCollection responseH if ((EntityStates.Added == descriptor.State) || (EntityStates.Modified == descriptor.State)) { descriptor.State = EntityStates.Unchanged; + descriptor.DependsOnIds = null; } else if (EntityStates.Detached != descriptor.State) { // this link may have been previously detached by a detaching entity @@ -1317,6 +1321,7 @@ private void HandleResponsePut(Descriptor descriptor, HeaderCollection responseH Debug.Assert(descriptor.DescriptorKind == DescriptorKind.NamedStream, "it must be named stream"); Debug.Assert(descriptor.State == EntityStates.Modified, "named stream must only be in modified state"); descriptor.State = EntityStates.Unchanged; + descriptor.DependsOnIds = null; StreamDescriptor streamDescriptor = (StreamDescriptor)descriptor; diff --git a/src/Microsoft.OData.Client/DataServiceContext.cs b/src/Microsoft.OData.Client/DataServiceContext.cs index 663cc1a270..9509b1bc47 100644 --- a/src/Microsoft.OData.Client/DataServiceContext.cs +++ b/src/Microsoft.OData.Client/DataServiceContext.cs @@ -2491,6 +2491,11 @@ public virtual void SetLink(object source, string sourceProperty, object target) if (relation == null) { relation = new LinkDescriptor(source, sourceProperty, target, this.model); + EntityDescriptor sourceResource = this.entityTracker.GetEntityDescriptor(source); + if (sourceResource.State == EntityStates.Added) + { + relation.DependsOnIds = new List { sourceResource.ChangeOrder.ToString(CultureInfo.InvariantCulture) }; + } this.entityTracker.AddLink(relation); } @@ -2633,10 +2638,14 @@ public virtual void AddRelatedObject(object source, string sourceProperty, objec var targetResource = new EntityDescriptor(this.model) { Entity = target, - State = EntityStates.Added, - DependsOnIds = new List { sourceResource.ChangeOrder.ToString(CultureInfo.InvariantCulture) } + State = EntityStates.Added }; + if (sourceResource.State == EntityStates.Added) + { + targetResource.DependsOnIds = new List { sourceResource.ChangeOrder.ToString(CultureInfo.InvariantCulture) }; + } + targetResource.SetParentForInsert(sourceResource, sourceProperty); this.EntityTracker.AddEntityDescriptor(targetResource); @@ -2884,8 +2893,12 @@ public virtual void UpdateRelatedObject(object source, string sourceProperty, ob { Entity = target, State = EntityStates.Modified, - EditLink = sourceResource.GetNestedResourceInfo(this.baseUriResolver, property) + EditLink = sourceResource.GetNestedResourceInfo(this.baseUriResolver, property) }; + if (sourceResource.State == EntityStates.Added) + { + targetResource.DependsOnIds = new List { sourceResource.ChangeOrder.ToString(CultureInfo.InvariantCulture) }; + } targetResource.SetParentForUpdate(sourceResource, sourceProperty); this.EntityTracker.AddEntityDescriptor(targetResource); @@ -4313,6 +4326,7 @@ private void SetStateToUnchanged(object entity) } descriptor.State = EntityStates.Unchanged; + descriptor.DependsOnIds = null; } /// diff --git a/src/Microsoft.OData.Client/ObjectMaterializerLog.cs b/src/Microsoft.OData.Client/ObjectMaterializerLog.cs index 79e9f32d26..ad3fca69f4 100644 --- a/src/Microsoft.OData.Client/ObjectMaterializerLog.cs +++ b/src/Microsoft.OData.Client/ObjectMaterializerLog.cs @@ -206,6 +206,7 @@ internal void ApplyToContext() { // we should always reset descriptor's state to Unchanged (old v1 behavior) descriptor.State = EntityStates.Unchanged; + descriptor.DependsOnIds = null; descriptor.PropertiesToSerialize.Clear(); } } diff --git a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Batch/AsyncMethodTests.cs b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Batch/AsyncMethodTests.cs index ba986869af..83fc9a0fdb 100644 --- a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Batch/AsyncMethodTests.cs +++ b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Batch/AsyncMethodTests.cs @@ -27,7 +27,7 @@ public class TestsStartup : TestStartupBase { public override void ConfigureServices(IServiceCollection services) { - services.ConfigureControllers(typeof(BanksController), typeof(MetadataController)); + services.ConfigureControllers(typeof(BanksController), typeof(BankAccountsController), typeof(MetadataController)); services.AddControllers().AddOData(opt => opt.Count().Filter().Expand().Select().OrderBy().SetMaxTop(null) .AddRouteComponents("odata", CommonEndToEndEdmModel.GetEdmModel(), new DefaultODataBatchHandler())); } @@ -86,6 +86,98 @@ public async Task JsonBatchSequencingSingeChangeSetTest() Assert.Equal(201, bankResponse.StatusCode); Assert.Equal(201, bankAccountResponse.StatusCode); } + + [Fact] + // Validating regression reported here: https://github.com/OData/odata.net/issues/3150 + public async Task BatchSequencingSingleChangeSetWithRelatedAlone() + { + // Create new Bank object + var bank = new Bank + { + Id = 45, + Name = "Test Bank", + Location = "KE", + BankAccounts = new List() + }; + + // Add the Bank entity to the context + _context.AddObject("Banks", bank); + + // Save bank + var response = await _context.SaveChangesAsync(); + Assert.Equal(1, response.Count()); + + var bankResponse = response.First() as ChangeOperationResponse; + Assert.NotNull(bankResponse); + Assert.Equal(201, bankResponse.StatusCode); + + // Create new BankAccount object + var bankAccount = new BankAccount + { + Id = 890, + AccountNumber = "4567890", + BankId = bank.Id, + Bank = bank + }; + + // Establish the relationship between Bank and BankAccount + bank.BankAccounts.Add(bankAccount); + + // Add the related BankAccount entity + _context.AddRelatedObject(bank, "BankAccounts", bankAccount); + + // Save bankAccount in a single batch request. + response = await _context.SaveChangesAsync(SaveChangesOptions.BatchWithSingleChangeset); + Assert.Equal(1, response.Count()); + + var bankAccountResponse = response.Last() as ChangeOperationResponse; + Assert.NotNull(bankAccountResponse); + Assert.Equal(201, bankAccountResponse.StatusCode); + } + + [Fact] + public async Task JsonBatchSequencingSingeChangeSetTest_SetLink() + { + // Create new BankAccounts object + var bank = new Bank + { + Id = 45, + Name = "Test Bank", + Location = "KE", + BankAccounts = new List() + }; + + // Create new BankAccount object + var bankAccount = new BankAccount + { + Id = 890, + AccountNumber = "4567890", + BankId = bank.Id, + }; + + // Add the Bank and Account entities to the context + _context.AddObject("Banks", bank); + _context.AddObject("BankAccounts", bankAccount); + + // Set the link from BankAccount to Bank entity + _context.SetLink(bankAccount, "Bank", bank); + + // Save both entities in a single batch request using JSON + var response = await _context.SaveChangesAsync(SaveChangesOptions.BatchWithSingleChangeset | SaveChangesOptions.UseJsonBatch); + Assert.Equal(3, response.Count()); // We get 2 POST's and a PUT for the link + + var bankResponse = response.ElementAt(0) as ChangeOperationResponse; + var bankAccountResponse = response.ElementAt(1) as ChangeOperationResponse; + var bankAccountBankResponse = response.ElementAt(2) as ChangeOperationResponse; + + Assert.NotNull(bankResponse); + Assert.NotNull(bankAccountResponse); + Assert.NotNull(bankAccountBankResponse); + + Assert.Equal(201, bankResponse.StatusCode); + Assert.Equal(201, bankAccountResponse.StatusCode); + Assert.Equal(204, bankAccountBankResponse.StatusCode); + } } class Container : DataServiceContext diff --git a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Batch/Server/BankAccountsController.cs b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Batch/Server/BankAccountsController.cs new file mode 100644 index 0000000000..0e286e9cb3 --- /dev/null +++ b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Batch/Server/BankAccountsController.cs @@ -0,0 +1,44 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.OData.Formatter; +using Microsoft.AspNetCore.OData.Query; +using Microsoft.AspNetCore.OData.Routing.Controllers; +using Microsoft.OData.Client.E2E.Tests.Common.Server.EndToEnd; +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.OData.Client.E2E.Tests.Batch.Server +{ + internal class BankAccountsController : ODataController + { + // POST: odata/Banks + [EnableQuery] + [HttpPost("odata/BankAccounts")] + public IActionResult Post([FromBody] BankAccount bankAccount) + { + if (bankAccount == null) + { + return BadRequest(); + } + BanksController._dataSource.BankAccounts.Add(bankAccount); + return Created(bankAccount); + } + + // PUT: /odata/$1/Bank + [EnableQuery] + [HttpPut("odata/BankAccounts({id})/Bank/$ref")] + public IActionResult PutBankAccountBank([FromODataUri] int id, [FromBody] Bank bank) + { + if (bank == null) + { + return BadRequest(); + } + BankAccount bankAccount = BanksController._dataSource.BankAccounts.First(); + bankAccount.Bank = bank; + return Updated(bankAccount); + } + } +} diff --git a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Batch/Server/BanksController.cs b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Batch/Server/BanksController.cs index 62ef0350e2..65b4b5b1b4 100644 --- a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Batch/Server/BanksController.cs +++ b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Batch/Server/BanksController.cs @@ -8,7 +8,7 @@ namespace Microsoft.OData.Client.E2E.Tests.Batch.Server { public class BanksController : ODataController { - private static CommonEndToEndDataSource _dataSource = CommonEndToEndDataSource.CreateInstance(); + internal static CommonEndToEndDataSource _dataSource = CommonEndToEndDataSource.CreateInstance(); [EnableQuery] [HttpGet("odata/Banks")] diff --git a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Common/Server/EndToEnd/CommonEndToEndDataSource.cs b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Common/Server/EndToEnd/CommonEndToEndDataSource.cs index 2a69e04706..6e00aa6de0 100644 --- a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Common/Server/EndToEnd/CommonEndToEndDataSource.cs +++ b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/Common/Server/EndToEnd/CommonEndToEndDataSource.cs @@ -140,6 +140,7 @@ private void PopulateBank() } ]; + this.BankAccounts = new List(); } private void PopulateBankAccount()