diff --git a/MockData/ActionV4Tests.PatchMovieDetails.1.txt b/MockData/ActionV4Tests.PatchMovieDetails.1.txt new file mode 100644 index 000000000..e1bbe759d --- /dev/null +++ b/MockData/ActionV4Tests.PatchMovieDetails.1.txt @@ -0,0 +1 @@ +{"Content":"{\"@odata.type\":\"#WebApiOData.V4.Samples.Models.MovieDetails\",\"Synopsis\":\"Foo!\"}","ContentHeaders":[{"Key":"Content-Type","Value":["application\/json"]},{"Key":"Content-Length","Value":["48"]}],"Method":"PATCH","RequestHeaders":[{"Key":"Accept","Value":["application\/json","application\/xml","application\/text"]},{"Key":"Prefer","Value":["return=representation"]}],"RequestUri":"http:\/\/localhost\/actions\/Movies\/1/Details"} \ No newline at end of file diff --git a/MockData/ActionV4Tests.PatchMovieDetails.2.txt b/MockData/ActionV4Tests.PatchMovieDetails.2.txt new file mode 100644 index 000000000..e351190a1 --- /dev/null +++ b/MockData/ActionV4Tests.PatchMovieDetails.2.txt @@ -0,0 +1 @@ +{"Content":"{\"@odata.context\":\"http://localhost/actions/$metadata#Movies(1)/Details\",\"value\":[{\"Synopsis\":\"Foo!\"}]}","ContentHeaders":[{"Key":"Content-Type","Value":["application\/json; odata.metadata=minimal"]},{"Key":"Content-Length","Value":["148"]}],"RequestUri":"http:\/\/localhost\/actions\/Movies\/1/Details","ResponseHeaders":[{"Key":"OData-Version","Value":["4.0"]}],"StatusCode":200} \ No newline at end of file diff --git a/src/Simple.OData.Client.Core/Adapter/RequestWriterBase.cs b/src/Simple.OData.Client.Core/Adapter/RequestWriterBase.cs index ce4f60ccf..feeef2a98 100644 --- a/src/Simple.OData.Client.Core/Adapter/RequestWriterBase.cs +++ b/src/Simple.OData.Client.Core/Adapter/RequestWriterBase.cs @@ -99,7 +99,7 @@ public async Task CreateUpdateRequestAsync( var hasPropertiesToUpdate = entryDetails.Properties.Count > 0; var usePatch = _session.Settings.PreferredUpdateMethod == ODataUpdateMethod.Patch || !hasPropertiesToUpdate; - if (HasUpdatedKeyProperties(collection, entryKey, entryData)) + if (entryKey != null && HasUpdatedKeyProperties(collection, entryKey, entryData)) { usePatch = false; } diff --git a/src/Simple.OData.Client.Core/Fluent/RequestBuilder.cs b/src/Simple.OData.Client.Core/Fluent/RequestBuilder.cs index 41f9dcef1..04f550e60 100644 --- a/src/Simple.OData.Client.Core/Fluent/RequestBuilder.cs +++ b/src/Simple.OData.Client.Core/Fluent/RequestBuilder.cs @@ -81,8 +81,6 @@ public async Task UpdateRequestAsync( bool resultRequired, CancellationToken cancellationToken) { - AssertHasKey(_command); - await _session .ResolveAdapterAsync(cancellationToken) .ConfigureAwait(false); diff --git a/src/WebApiOData.V4.Samples.Tests/ActionV4Tests.cs b/src/WebApiOData.V4.Samples.Tests/ActionV4Tests.cs index dc6b56690..3c24aa3f0 100644 --- a/src/WebApiOData.V4.Samples.Tests/ActionV4Tests.cs +++ b/src/WebApiOData.V4.Samples.Tests/ActionV4Tests.cs @@ -151,4 +151,16 @@ public async Task CreateMovie_batch() Assert.True(result.ID > 0); } + + [Fact] + public async Task PatchMovieDetails() + { + var settings = CreateDefaultSettings().WithHttpMock(); + var client = new ODataClient(settings); + MovieDetails updated = await client.For().Key(1).NavigateTo(m => m.Details!).Set(new + { + Synopsis = "Foo!" + }).UpdateEntryAsync().ConfigureAwait(false); + Assert.Equal("Foo!", updated.Synopsis); + } } diff --git a/src/WebApiOData.V4.Samples.Tests/Resources/Metadata.xml b/src/WebApiOData.V4.Samples.Tests/Resources/Metadata.xml index 46d5f22f2..556a9db0b 100644 --- a/src/WebApiOData.V4.Samples.Tests/Resources/Metadata.xml +++ b/src/WebApiOData.V4.Samples.Tests/Resources/Metadata.xml @@ -10,6 +10,10 @@ + + + + diff --git a/src/WebApiOData.V4.Samples/Controllers/MoviesController.cs b/src/WebApiOData.V4.Samples/Controllers/MoviesController.cs index 9853c20fd..aace4b87c 100644 --- a/src/WebApiOData.V4.Samples/Controllers/MoviesController.cs +++ b/src/WebApiOData.V4.Samples/Controllers/MoviesController.cs @@ -17,6 +17,28 @@ public IHttpActionResult Get() return Ok(_db.Movies); } + public IHttpActionResult GetMovieDetails(int key) + { + var movie = _db.Movies.FirstOrDefault(m => m.ID == key); + if (movie?.Details is not null) + { + return Ok(movie.Details); + } + return NotFound(); + } + + [HttpPatch] + public IHttpActionResult PatchToMovieDetails(int key, Delta delta) + { + var movie = _db.Movies.FirstOrDefault(m => m.ID == key); + if (movie?.Details is not null) + { + delta?.Patch(movie.Details); + return Ok(movie.Details); + } + return NotFound(); + } + [HttpPost] public IHttpActionResult CheckOut(int key) { diff --git a/src/WebApiOData.V4.Samples/Models/Movie.cs b/src/WebApiOData.V4.Samples/Models/Movie.cs index 44fa214c4..dece5bb94 100644 --- a/src/WebApiOData.V4.Samples/Models/Movie.cs +++ b/src/WebApiOData.V4.Samples/Models/Movie.cs @@ -13,4 +13,11 @@ public class Movie public DateTimeOffset? DueDate { get; set; } public bool IsCheckedOut => DueDate.HasValue; + + public MovieDetails? Details { get; set; } } + +public class MovieDetails +{ + public string Synopsis { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/src/WebApiOData.V4.Samples/Models/MoviesContext.cs b/src/WebApiOData.V4.Samples/Models/MoviesContext.cs index 3eb8f41bc..7f9cf38dc 100644 --- a/src/WebApiOData.V4.Samples/Models/MoviesContext.cs +++ b/src/WebApiOData.V4.Samples/Models/MoviesContext.cs @@ -6,7 +6,7 @@ public class MoviesContext { public List Movies { get; set; } = new List() { - new Movie() { ID=1, Title = "Maximum Payback", Year = 1990 }, + new Movie() { ID=1, Title = "Maximum Payback", Year = 1990, Details = new() { Synopsis = "..." } }, new Movie() { ID=2, Title = "Inferno of Retribution", Year = 2005 }, new Movie() { ID=3, Title = "Fatal Vengeance 2", Year = 2012 }, new Movie() { ID=4, Title = "Sudden Danger", Year = 2012 }, diff --git a/src/WebApiOData.V4.Samples/Startups/ActionStartup.cs b/src/WebApiOData.V4.Samples/Startups/ActionStartup.cs index 3f6075c9c..09ec71509 100644 --- a/src/WebApiOData.V4.Samples/Startups/ActionStartup.cs +++ b/src/WebApiOData.V4.Samples/Startups/ActionStartup.cs @@ -29,6 +29,8 @@ private static IEdmModel GetEdmModel(HttpConfiguration config) var modelBuilder = new ODataConventionModelBuilder(config); _ = modelBuilder.EntitySet("Movies"); + modelBuilder.EntityType().HasOptional(m => m.Details!).Contained(); + // Now add actions. // CheckOut