diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1a6a51b0..0ee36440 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,9 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
-## [1.6.0](https://github.com/Terradue/DotNetStac/compare/1.5.0...1.6.0)
+## [1.6.1](https://github.com/Terradue/DotNetStac/compare/1.6.0...1.6.1)
-add datacube extension support
+Added patching function for Stac Objects (RFC 7386)
+
+### Merged
+
+- Added patching function for Stac Objects (RFC 7386) [`#18`](https://github.com/Terradue/DotNetStac/pull/18)
+
+### Commits
+
+- liniting code [`9338969`](https://github.com/Terradue/DotNetStac/commit/9338969117132ad8fd8eb562f43da4e96a849a6a)
+- fix error IDE0038 [`01ff3ed`](https://github.com/Terradue/DotNetStac/commit/01ff3ede7d3c8726d8dc6005d80d52a6574edb69)
+- tests fixed [`1bd1dd8`](https://github.com/Terradue/DotNetStac/commit/1bd1dd8f62c71371c1f9f5962ecd88a800a4d790)
+
+## [1.6.0](https://github.com/Terradue/DotNetStac/compare/1.5.0...1.6.0) - 2022-10-27
### Merged
diff --git a/README.md b/README.md
index af0a3fa4..6082e3ab 100644
--- a/README.md
+++ b/README.md
@@ -12,9 +12,9 @@
-![Build Status](https://github.com/Terradue/DotNetStac/actions/workflows/build.yaml/badge.svg?branch=release/1.6.0)
+![Build Status](https://github.com/Terradue/DotNetStac/actions/workflows/build.yaml/badge.svg?branch=release/1.6.1)
[![NuGet](https://img.shields.io/nuget/vpre/DotNetStac)](https://www.nuget.org/packages/DotNetStac/)
-[![codecov](https://codecov.io/gh/Terradue/DotNetStac/branch/release/1.6.0/graph/badge.svg)](https://codecov.io/gh/Terradue/DotNetStac)
+[![codecov](https://codecov.io/gh/Terradue/DotNetStac/branch/release/1.6.1/graph/badge.svg)](https://codecov.io/gh/Terradue/DotNetStac)
[![Gitter](https://img.shields.io/gitter/room/SpatioTemporal-Asset-Catalog/Lobby?color=yellow)](https://gitter.im/SpatioTemporal-Asset-Catalog/Lobby)
[![License](https://img.shields.io/badge/license-AGPL3-blue.svg)](LICENSE)
[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/Terradue/DotNetStac/master?filepath=example.ipynb)
diff --git a/src/DotNetStac.Test/Common/PatchHelpersTests.cs b/src/DotNetStac.Test/Common/PatchHelpersTests.cs
new file mode 100644
index 00000000..8ca024d9
--- /dev/null
+++ b/src/DotNetStac.Test/Common/PatchHelpersTests.cs
@@ -0,0 +1,36 @@
+using System.Collections.Generic;
+using GeoJSON.Net.Geometry;
+using Newtonsoft.Json;
+using Stac.Collection;
+using Stac.Common;
+using Xunit;
+
+namespace Stac.Test.Item
+{
+ public class PatchHelpersTests : TestBase
+ {
+ [Fact]
+ public void PatchStacItemTest()
+ {
+ StacItem baseItem = StacConvert.Deserialize(GetJson("Common", "BaseItem"));
+
+ StacItem patchItem = StacConvert.Deserialize(GetJson("Common", "Patch"));
+
+ StacItem patchedItem = baseItem.Patch(patchItem);
+
+ JsonAssert.AreEqual(GetJson("Common", "PatchedItem"), StacConvert.Serialize(patchedItem));
+ }
+
+ [Fact]
+ public void PatchStacItemTest2()
+ {
+ StacItem baseItem = StacConvert.Deserialize(GetJson("Common", "BaseItem"));
+
+ Patch patchItem = JsonConvert.DeserializeObject(GetJson("Common", "Patch2"));
+
+ StacItem patchedItem = baseItem.Patch(patchItem);
+
+ JsonAssert.AreEqual(GetJson("Common", "PatchedItem2"), StacConvert.Serialize(patchedItem));
+ }
+ }
+}
diff --git a/src/DotNetStac.Test/Resources/Common/PatchHelpersTests_BaseItem.json b/src/DotNetStac.Test/Resources/Common/PatchHelpersTests_BaseItem.json
new file mode 100644
index 00000000..5ce267bb
--- /dev/null
+++ b/src/DotNetStac.Test/Resources/Common/PatchHelpersTests_BaseItem.json
@@ -0,0 +1,662 @@
+{
+ "type": "Feature",
+ "stac_version": "1.0.0",
+ "stac_extensions": [
+ "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
+ "https://stac-extensions.github.io/projection/v1.0.0/schema.json",
+ "https://stac-extensions.github.io/sat/v1.0.0/schema.json"
+ ],
+ "id": "S2A_30VWN_20200830_0_L2A",
+ "bbox": [
+ -3.000355032202007,
+ 60.33378967715628,
+ -0.9494552273687494,
+ 61.33443350563737
+ ],
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [
+ -1.0115813842949837,
+ 60.33378967715628
+ ],
+ [
+ -3.000344266875852,
+ 60.34865379793714
+ ],
+ [
+ -3.000355032202007,
+ 61.33443350563737
+ ],
+ [
+ -0.9494552273687494,
+ 61.31895930227699
+ ],
+ [
+ -1.0115813842949837,
+ 60.33378967715628
+ ]
+ ]
+ ]
+ },
+ "properties": {
+ "datetime": "2020-08-30T11:34:28Z",
+ "platform": "sentinel-2a",
+ "constellation": "sentinel-2",
+ "instruments": [
+ "msi"
+ ],
+ "gsd": 10,
+ "view:off_nadir": 0,
+ "proj:epsg": 32630,
+ "sat:relative_orbit": 80,
+ "sentinel:utm_zone": 30,
+ "sentinel:latitude_band": "V",
+ "sentinel:grid_square": "WN",
+ "sentinel:sequence": "0",
+ "sentinel:product_id": "S2A_MSIL2A_20200830T113321_N0214_R080_T30VWN_20200830T121958",
+ "sentinel:data_coverage": 100,
+ "eo:cloud_cover": 0,
+ "sentinel:valid_cloud_cover": false,
+ "created": "2020-08-31T09:57:42.772Z",
+ "updated": "2020-08-31T09:57:42.772Z"
+ },
+ "collection": "sentinel-s2-l2a-cogs",
+ "assets": {
+ "thumbnail": {
+ "title": "Thumbnail",
+ "type": "image/png",
+ "roles": [
+ "thumbnail"
+ ],
+ "href": "https://roda.sentinel-hub.com/sentinel-s2-l1c/tiles/30/V/WN/2020/8/30/0/preview.jpg"
+ },
+ "overview": {
+ "title": "True color image",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "overview"
+ ],
+ "gsd": 10,
+ "eo:bands": [
+ {
+ "name": "B04",
+ "common_name": "red",
+ "center_wavelength": 0.6645,
+ "full_width_half_max": 0.038
+ },
+ {
+ "name": "B03",
+ "common_name": "green",
+ "center_wavelength": 0.56,
+ "full_width_half_max": 0.045
+ },
+ {
+ "name": "B02",
+ "common_name": "blue",
+ "center_wavelength": 0.4966,
+ "full_width_half_max": 0.098
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/L2A_PVI.tif",
+ "proj:shape": [
+ 343,
+ 343
+ ],
+ "proj:transform": [
+ 320,
+ 0,
+ 499980,
+ 0,
+ -320,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "info": {
+ "title": "Original JSON metadata",
+ "type": "application/json",
+ "roles": [
+ "metadata"
+ ],
+ "href": "https://roda.sentinel-hub.com/sentinel-s2-l2a/tiles/30/V/WN/2020/8/30/0/tileInfo.json"
+ },
+ "metadata": {
+ "title": "Original XML metadata",
+ "type": "application/xml",
+ "roles": [
+ "metadata"
+ ],
+ "href": "https://roda.sentinel-hub.com/sentinel-s2-l2a/tiles/30/V/WN/2020/8/30/0/metadata.xml"
+ },
+ "visual": {
+ "title": "True color image",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "overview"
+ ],
+ "gsd": 10,
+ "eo:bands": [
+ {
+ "name": "B04",
+ "common_name": "red",
+ "center_wavelength": 0.6645,
+ "full_width_half_max": 0.038
+ },
+ {
+ "name": "B03",
+ "common_name": "green",
+ "center_wavelength": 0.56,
+ "full_width_half_max": 0.045
+ },
+ {
+ "name": "B02",
+ "common_name": "blue",
+ "center_wavelength": 0.4966,
+ "full_width_half_max": 0.098
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/TCI.tif",
+ "proj:shape": [
+ 10980,
+ 10980
+ ],
+ "proj:transform": [
+ 10,
+ 0,
+ 499980,
+ 0,
+ -10,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "B01": {
+ "title": "Band 1 (coastal)",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 60,
+ "eo:bands": [
+ {
+ "name": "B01",
+ "common_name": "coastal",
+ "center_wavelength": 0.4439,
+ "full_width_half_max": 0.027
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B01.tif",
+ "proj:shape": [
+ 1830,
+ 1830
+ ],
+ "proj:transform": [
+ 60,
+ 0,
+ 499980,
+ 0,
+ -60,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "B02": {
+ "title": "Band 2 (blue)",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 10,
+ "eo:bands": [
+ {
+ "name": "B02",
+ "common_name": "blue",
+ "center_wavelength": 0.4966,
+ "full_width_half_max": 0.098
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B02.tif",
+ "proj:shape": [
+ 10980,
+ 10980
+ ],
+ "proj:transform": [
+ 10,
+ 0,
+ 499980,
+ 0,
+ -10,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "B03": {
+ "title": "Band 3 (green)",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 10,
+ "eo:bands": [
+ {
+ "name": "B03",
+ "common_name": "green",
+ "center_wavelength": 0.56,
+ "full_width_half_max": 0.045
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B03.tif",
+ "proj:shape": [
+ 10980,
+ 10980
+ ],
+ "proj:transform": [
+ 10,
+ 0,
+ 499980,
+ 0,
+ -10,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "B04": {
+ "title": "Band 4 (red)",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 10,
+ "eo:bands": [
+ {
+ "name": "B04",
+ "common_name": "red",
+ "center_wavelength": 0.6645,
+ "full_width_half_max": 0.038
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B04.tif",
+ "proj:shape": [
+ 10980,
+ 10980
+ ],
+ "proj:transform": [
+ 10,
+ 0,
+ 499980,
+ 0,
+ -10,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "B05": {
+ "title": "Band 5",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 20,
+ "eo:bands": [
+ {
+ "name": "B05",
+ "center_wavelength": 0.7039,
+ "full_width_half_max": 0.019
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B05.tif",
+ "proj:shape": [
+ 5490,
+ 5490
+ ],
+ "proj:transform": [
+ 20,
+ 0,
+ 499980,
+ 0,
+ -20,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "B06": {
+ "title": "Band 6",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 20,
+ "eo:bands": [
+ {
+ "name": "B06",
+ "center_wavelength": 0.7402,
+ "full_width_half_max": 0.018
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B06.tif",
+ "proj:shape": [
+ 5490,
+ 5490
+ ],
+ "proj:transform": [
+ 20,
+ 0,
+ 499980,
+ 0,
+ -20,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "B07": {
+ "title": "Band 7",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 20,
+ "eo:bands": [
+ {
+ "name": "B07",
+ "center_wavelength": 0.7825,
+ "full_width_half_max": 0.028
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B07.tif",
+ "proj:shape": [
+ 5490,
+ 5490
+ ],
+ "proj:transform": [
+ 20,
+ 0,
+ 499980,
+ 0,
+ -20,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "B08": {
+ "title": "Band 8 (nir)",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 10,
+ "eo:bands": [
+ {
+ "name": "B08",
+ "common_name": "nir",
+ "center_wavelength": 0.8351,
+ "full_width_half_max": 0.145
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B08.tif",
+ "proj:shape": [
+ 10980,
+ 10980
+ ],
+ "proj:transform": [
+ 10,
+ 0,
+ 499980,
+ 0,
+ -10,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "B8A": {
+ "title": "Band 8A",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 20,
+ "eo:bands": [
+ {
+ "name": "B8A",
+ "center_wavelength": 0.8648,
+ "full_width_half_max": 0.033
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B8A.tif",
+ "proj:shape": [
+ 5490,
+ 5490
+ ],
+ "proj:transform": [
+ 20,
+ 0,
+ 499980,
+ 0,
+ -20,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "B09": {
+ "title": "Band 9",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 60,
+ "eo:bands": [
+ {
+ "name": "B09",
+ "center_wavelength": 0.945,
+ "full_width_half_max": 0.026
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B09.tif",
+ "proj:shape": [
+ 1830,
+ 1830
+ ],
+ "proj:transform": [
+ 60,
+ 0,
+ 499980,
+ 0,
+ -60,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "B11": {
+ "title": "Band 11 (swir16)",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 20,
+ "eo:bands": [
+ {
+ "name": "B11",
+ "common_name": "swir16",
+ "center_wavelength": 1.6137,
+ "full_width_half_max": 0.143
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B11.tif",
+ "proj:shape": [
+ 5490,
+ 5490
+ ],
+ "proj:transform": [
+ 20,
+ 0,
+ 499980,
+ 0,
+ -20,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "B12": {
+ "title": "Band 12 (swir22)",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 20,
+ "eo:bands": [
+ {
+ "name": "B12",
+ "common_name": "swir22",
+ "center_wavelength": 2.22024,
+ "full_width_half_max": 0.242
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B12.tif",
+ "proj:shape": [
+ 5490,
+ 5490
+ ],
+ "proj:transform": [
+ 20,
+ 0,
+ 499980,
+ 0,
+ -20,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "AOT": {
+ "title": "Aerosol Optical Thickness (AOT)",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/AOT.tif",
+ "proj:shape": [
+ 1830,
+ 1830
+ ],
+ "proj:transform": [
+ 60,
+ 0,
+ 499980,
+ 0,
+ -60,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "WVP": {
+ "title": "Water Vapour (WVP)",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/WVP.tif",
+ "proj:shape": [
+ 10980,
+ 10980
+ ],
+ "proj:transform": [
+ 10,
+ 0,
+ 499980,
+ 0,
+ -10,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "SCL": {
+ "title": "Scene Classification Map (SCL)",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/SCL.tif",
+ "proj:shape": [
+ 5490,
+ 5490
+ ],
+ "proj:transform": [
+ 20,
+ 0,
+ 499980,
+ 0,
+ -20,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ }
+ },
+ "links": [
+ {
+ "rel": "self",
+ "href": "https://earth-search.aws.element84.com/v0/collections/sentinel-s2-l2a-cogs/items/S2A_30VWN_20200830_0_L2A"
+ },
+ {
+ "rel": "canonical",
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/S2A_30VWN_20200830_0_L2A.json",
+ "type": "application/json"
+ },
+ {
+ "rel": "canonical",
+ "href": "https://cirrus-v0-data-1qm7gekzjucbq.s3.us-west-2.amazonaws.com/sentinel-s2-l2a/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/S2A_30VWN_20200830_0_L2A.json",
+ "type": "application/json"
+ },
+ {
+ "title": "Source STAC Item",
+ "rel": "derived_from",
+ "href": "https://cirrus-v0-data-1qm7gekzjucbq.s3.us-west-2.amazonaws.com/sentinel-s2-l2a/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/S2A_30VWN_20200830_0_L2A.json",
+ "type": "application/json"
+ },
+ {
+ "rel": "parent",
+ "href": "https://earth-search.aws.element84.com/v0/collections/sentinel-s2-l2a-cogs"
+ },
+ {
+ "rel": "collection",
+ "href": "https://earth-search.aws.element84.com/v0/collections/sentinel-s2-l2a-cogs"
+ },
+ {
+ "rel": "root",
+ "href": "https://earth-search.aws.element84.com/v0/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/DotNetStac.Test/Resources/Common/PatchHelpersTests_Patch.json b/src/DotNetStac.Test/Resources/Common/PatchHelpersTests_Patch.json
new file mode 100644
index 00000000..2139a73f
--- /dev/null
+++ b/src/DotNetStac.Test/Resources/Common/PatchHelpersTests_Patch.json
@@ -0,0 +1,88 @@
+{
+ "id": "S2A_30VWN_20200830_0_L2A",
+ "type": "Feature",
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [
+ -2.0,
+ 61.0
+ ],
+ [
+ -3.5,
+ 50.0
+ ],
+ [
+ -3.5,
+ 63
+ ],
+ [
+ -1.0,
+ 63.0
+ ],
+ [
+ -2.0,
+ 61.0
+ ]
+ ]
+ ]
+ },
+ "properties": {
+ "datetime": "2022-08-30T11:34:28Z",
+ "platform": "sentinel-2c",
+ },
+ "collection": "sentinel-s2-l3a-cogs",
+ "assets": {
+ "B01": {
+ "title": "Band 1 (coastal)",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 70,
+ "eo:bands": [
+ {
+ "name": "B01",
+ "common_name": "blue",
+ "center_wavelength": 0.5,
+ "full_width_half_max": 0.127
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B01.tiff",
+ "proj:shape": null,
+ },
+ "B2000": {
+ "title": "Band 2 (blue)",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 10,
+ "eo:bands": [
+ {
+ "name": "B02",
+ "common_name": "blue",
+ "center_wavelength": 0.4966,
+ "full_width_half_max": 0.098
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B02.tif",
+ "proj:shape": [
+ 10980,
+ 10980
+ ],
+ "proj:transform": [
+ 10,
+ 0,
+ 499980,
+ 0,
+ -10,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/DotNetStac.Test/Resources/Common/PatchHelpersTests_Patch2.json b/src/DotNetStac.Test/Resources/Common/PatchHelpersTests_Patch2.json
new file mode 100644
index 00000000..6a29b316
--- /dev/null
+++ b/src/DotNetStac.Test/Resources/Common/PatchHelpersTests_Patch2.json
@@ -0,0 +1,85 @@
+{
+ "geometry": {
+ "coordinates": [
+ [
+ [
+ -2.0,
+ 61.0
+ ],
+ [
+ -3.5,
+ 50.0
+ ],
+ [
+ -3.5,
+ 63
+ ],
+ [
+ -1.0,
+ 63.0
+ ],
+ [
+ -2.0,
+ 61.0
+ ]
+ ]
+ ]
+ },
+ "properties": {
+ "datetime": "2022-08-30T11:34:28Z",
+ "platform": "sentinel-2c",
+ },
+ "collection": null,
+ "assets": {
+ "B01": {
+ "title": "Band 1 (coastal)",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 70,
+ "eo:bands": [
+ {
+ "name": "B01",
+ "common_name": "blue",
+ "center_wavelength": 0.5,
+ "full_width_half_max": 0.127
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B01.tiff",
+ "proj:shape": null,
+ },
+ "B2000": {
+ "title": "Band 2 (blue)",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 10,
+ "eo:bands": [
+ {
+ "name": "B02",
+ "common_name": "blue",
+ "center_wavelength": 0.4966,
+ "full_width_half_max": 0.098
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B02.tif",
+ "proj:shape": [
+ 10980,
+ 10980
+ ],
+ "proj:transform": [
+ 10,
+ 0,
+ 499980,
+ 0,
+ -10,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/DotNetStac.Test/Resources/Common/PatchHelpersTests_PatchedItem.json b/src/DotNetStac.Test/Resources/Common/PatchHelpersTests_PatchedItem.json
new file mode 100644
index 00000000..55be4665
--- /dev/null
+++ b/src/DotNetStac.Test/Resources/Common/PatchHelpersTests_PatchedItem.json
@@ -0,0 +1,690 @@
+{
+ "type": "Feature",
+ "stac_version": "1.0.0",
+ "stac_extensions": [
+ "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
+ "https://stac-extensions.github.io/projection/v1.0.0/schema.json",
+ "https://stac-extensions.github.io/sat/v1.0.0/schema.json"
+ ],
+ "id": "S2A_30VWN_20200830_0_L2A",
+ "bbox": [
+ -3.5,
+ 50.0,
+ -1.0,
+ 63.0
+ ],
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [
+ -2.0,
+ 61.0
+ ],
+ [
+ -3.5,
+ 50.0
+ ],
+ [
+ -3.5,
+ 63.0
+ ],
+ [
+ -1.0,
+ 63.0
+ ],
+ [
+ -2.0,
+ 61.0
+ ]
+ ]
+ ]
+ },
+ "properties": {
+ "datetime": "2022-08-30T11:34:28Z",
+ "platform": "sentinel-2c",
+ "constellation": "sentinel-2",
+ "instruments": [
+ "msi"
+ ],
+ "gsd": 10,
+ "view:off_nadir": 0,
+ "proj:epsg": 32630,
+ "sat:relative_orbit": 80,
+ "sentinel:utm_zone": 30,
+ "sentinel:latitude_band": "V",
+ "sentinel:grid_square": "WN",
+ "sentinel:sequence": "0",
+ "sentinel:product_id": "S2A_MSIL2A_20200830T113321_N0214_R080_T30VWN_20200830T121958",
+ "sentinel:data_coverage": 100,
+ "eo:cloud_cover": 0,
+ "sentinel:valid_cloud_cover": false,
+ "created": "2020-08-31T09:57:42.772Z",
+ "updated": "2020-08-31T09:57:42.772Z"
+ },
+ "collection": "sentinel-s2-l3a-cogs",
+ "assets": {
+ "thumbnail": {
+ "title": "Thumbnail",
+ "type": "image/png",
+ "roles": [
+ "thumbnail"
+ ],
+ "href": "https://roda.sentinel-hub.com/sentinel-s2-l1c/tiles/30/V/WN/2020/8/30/0/preview.jpg"
+ },
+ "overview": {
+ "title": "True color image",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "overview"
+ ],
+ "gsd": 10,
+ "eo:bands": [
+ {
+ "name": "B04",
+ "common_name": "red",
+ "center_wavelength": 0.6645,
+ "full_width_half_max": 0.038
+ },
+ {
+ "name": "B03",
+ "common_name": "green",
+ "center_wavelength": 0.56,
+ "full_width_half_max": 0.045
+ },
+ {
+ "name": "B02",
+ "common_name": "blue",
+ "center_wavelength": 0.4966,
+ "full_width_half_max": 0.098
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/L2A_PVI.tif",
+ "proj:shape": [
+ 343,
+ 343
+ ],
+ "proj:transform": [
+ 320,
+ 0,
+ 499980,
+ 0,
+ -320,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "info": {
+ "title": "Original JSON metadata",
+ "type": "application/json",
+ "roles": [
+ "metadata"
+ ],
+ "href": "https://roda.sentinel-hub.com/sentinel-s2-l2a/tiles/30/V/WN/2020/8/30/0/tileInfo.json"
+ },
+ "metadata": {
+ "title": "Original XML metadata",
+ "type": "application/xml",
+ "roles": [
+ "metadata"
+ ],
+ "href": "https://roda.sentinel-hub.com/sentinel-s2-l2a/tiles/30/V/WN/2020/8/30/0/metadata.xml"
+ },
+ "visual": {
+ "title": "True color image",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "overview"
+ ],
+ "gsd": 10,
+ "eo:bands": [
+ {
+ "name": "B04",
+ "common_name": "red",
+ "center_wavelength": 0.6645,
+ "full_width_half_max": 0.038
+ },
+ {
+ "name": "B03",
+ "common_name": "green",
+ "center_wavelength": 0.56,
+ "full_width_half_max": 0.045
+ },
+ {
+ "name": "B02",
+ "common_name": "blue",
+ "center_wavelength": 0.4966,
+ "full_width_half_max": 0.098
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/TCI.tif",
+ "proj:shape": [
+ 10980,
+ 10980
+ ],
+ "proj:transform": [
+ 10,
+ 0,
+ 499980,
+ 0,
+ -10,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "B01": {
+ "title": "Band 1 (coastal)",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 70,
+ "eo:bands": [
+ {
+ "name": "B01",
+ "common_name": "blue",
+ "center_wavelength": 0.5,
+ "full_width_half_max": 0.127
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B01.tiff",
+ "proj:transform": [
+ 60,
+ 0,
+ 499980,
+ 0,
+ -60,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "B2000": {
+ "title": "Band 2 (blue)",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 10,
+ "eo:bands": [
+ {
+ "name": "B02",
+ "common_name": "blue",
+ "center_wavelength": 0.4966,
+ "full_width_half_max": 0.098
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B02.tif",
+ "proj:shape": [
+ 10980,
+ 10980
+ ],
+ "proj:transform": [
+ 10,
+ 0,
+ 499980,
+ 0,
+ -10,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "B02": {
+ "title": "Band 2 (blue)",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 10,
+ "eo:bands": [
+ {
+ "name": "B02",
+ "common_name": "blue",
+ "center_wavelength": 0.4966,
+ "full_width_half_max": 0.098
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B02.tif",
+ "proj:shape": [
+ 10980,
+ 10980
+ ],
+ "proj:transform": [
+ 10,
+ 0,
+ 499980,
+ 0,
+ -10,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "B03": {
+ "title": "Band 3 (green)",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 10,
+ "eo:bands": [
+ {
+ "name": "B03",
+ "common_name": "green",
+ "center_wavelength": 0.56,
+ "full_width_half_max": 0.045
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B03.tif",
+ "proj:shape": [
+ 10980,
+ 10980
+ ],
+ "proj:transform": [
+ 10,
+ 0,
+ 499980,
+ 0,
+ -10,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "B04": {
+ "title": "Band 4 (red)",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 10,
+ "eo:bands": [
+ {
+ "name": "B04",
+ "common_name": "red",
+ "center_wavelength": 0.6645,
+ "full_width_half_max": 0.038
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B04.tif",
+ "proj:shape": [
+ 10980,
+ 10980
+ ],
+ "proj:transform": [
+ 10,
+ 0,
+ 499980,
+ 0,
+ -10,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "B05": {
+ "title": "Band 5",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 20,
+ "eo:bands": [
+ {
+ "name": "B05",
+ "center_wavelength": 0.7039,
+ "full_width_half_max": 0.019
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B05.tif",
+ "proj:shape": [
+ 5490,
+ 5490
+ ],
+ "proj:transform": [
+ 20,
+ 0,
+ 499980,
+ 0,
+ -20,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "B06": {
+ "title": "Band 6",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 20,
+ "eo:bands": [
+ {
+ "name": "B06",
+ "center_wavelength": 0.7402,
+ "full_width_half_max": 0.018
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B06.tif",
+ "proj:shape": [
+ 5490,
+ 5490
+ ],
+ "proj:transform": [
+ 20,
+ 0,
+ 499980,
+ 0,
+ -20,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "B07": {
+ "title": "Band 7",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 20,
+ "eo:bands": [
+ {
+ "name": "B07",
+ "center_wavelength": 0.7825,
+ "full_width_half_max": 0.028
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B07.tif",
+ "proj:shape": [
+ 5490,
+ 5490
+ ],
+ "proj:transform": [
+ 20,
+ 0,
+ 499980,
+ 0,
+ -20,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "B08": {
+ "title": "Band 8 (nir)",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 10,
+ "eo:bands": [
+ {
+ "name": "B08",
+ "common_name": "nir",
+ "center_wavelength": 0.8351,
+ "full_width_half_max": 0.145
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B08.tif",
+ "proj:shape": [
+ 10980,
+ 10980
+ ],
+ "proj:transform": [
+ 10,
+ 0,
+ 499980,
+ 0,
+ -10,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "B8A": {
+ "title": "Band 8A",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 20,
+ "eo:bands": [
+ {
+ "name": "B8A",
+ "center_wavelength": 0.8648,
+ "full_width_half_max": 0.033
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B8A.tif",
+ "proj:shape": [
+ 5490,
+ 5490
+ ],
+ "proj:transform": [
+ 20,
+ 0,
+ 499980,
+ 0,
+ -20,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "B09": {
+ "title": "Band 9",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 60,
+ "eo:bands": [
+ {
+ "name": "B09",
+ "center_wavelength": 0.945,
+ "full_width_half_max": 0.026
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B09.tif",
+ "proj:shape": [
+ 1830,
+ 1830
+ ],
+ "proj:transform": [
+ 60,
+ 0,
+ 499980,
+ 0,
+ -60,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "B11": {
+ "title": "Band 11 (swir16)",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 20,
+ "eo:bands": [
+ {
+ "name": "B11",
+ "common_name": "swir16",
+ "center_wavelength": 1.6137,
+ "full_width_half_max": 0.143
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B11.tif",
+ "proj:shape": [
+ 5490,
+ 5490
+ ],
+ "proj:transform": [
+ 20,
+ 0,
+ 499980,
+ 0,
+ -20,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "B12": {
+ "title": "Band 12 (swir22)",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 20,
+ "eo:bands": [
+ {
+ "name": "B12",
+ "common_name": "swir22",
+ "center_wavelength": 2.22024,
+ "full_width_half_max": 0.242
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B12.tif",
+ "proj:shape": [
+ 5490,
+ 5490
+ ],
+ "proj:transform": [
+ 20,
+ 0,
+ 499980,
+ 0,
+ -20,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "AOT": {
+ "title": "Aerosol Optical Thickness (AOT)",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/AOT.tif",
+ "proj:shape": [
+ 1830,
+ 1830
+ ],
+ "proj:transform": [
+ 60,
+ 0,
+ 499980,
+ 0,
+ -60,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "WVP": {
+ "title": "Water Vapour (WVP)",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/WVP.tif",
+ "proj:shape": [
+ 10980,
+ 10980
+ ],
+ "proj:transform": [
+ 10,
+ 0,
+ 499980,
+ 0,
+ -10,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "SCL": {
+ "title": "Scene Classification Map (SCL)",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/SCL.tif",
+ "proj:shape": [
+ 5490,
+ 5490
+ ],
+ "proj:transform": [
+ 20,
+ 0,
+ 499980,
+ 0,
+ -20,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ }
+ },
+ "links": [
+ {
+ "rel": "self",
+ "href": "https://earth-search.aws.element84.com/v0/collections/sentinel-s2-l2a-cogs/items/S2A_30VWN_20200830_0_L2A"
+ },
+ {
+ "rel": "canonical",
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/S2A_30VWN_20200830_0_L2A.json",
+ "type": "application/json"
+ },
+ {
+ "rel": "canonical",
+ "href": "https://cirrus-v0-data-1qm7gekzjucbq.s3.us-west-2.amazonaws.com/sentinel-s2-l2a/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/S2A_30VWN_20200830_0_L2A.json",
+ "type": "application/json"
+ },
+ {
+ "title": "Source STAC Item",
+ "rel": "derived_from",
+ "href": "https://cirrus-v0-data-1qm7gekzjucbq.s3.us-west-2.amazonaws.com/sentinel-s2-l2a/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/S2A_30VWN_20200830_0_L2A.json",
+ "type": "application/json"
+ },
+ {
+ "rel": "parent",
+ "href": "https://earth-search.aws.element84.com/v0/collections/sentinel-s2-l2a-cogs"
+ },
+ {
+ "rel": "collection",
+ "href": "https://earth-search.aws.element84.com/v0/collections/sentinel-s2-l2a-cogs"
+ },
+ {
+ "rel": "root",
+ "href": "https://earth-search.aws.element84.com/v0/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/DotNetStac.Test/Resources/Common/PatchHelpersTests_PatchedItem2.json b/src/DotNetStac.Test/Resources/Common/PatchHelpersTests_PatchedItem2.json
new file mode 100644
index 00000000..8f3dec4f
--- /dev/null
+++ b/src/DotNetStac.Test/Resources/Common/PatchHelpersTests_PatchedItem2.json
@@ -0,0 +1,689 @@
+{
+ "type": "Feature",
+ "stac_version": "1.0.0",
+ "stac_extensions": [
+ "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
+ "https://stac-extensions.github.io/projection/v1.0.0/schema.json",
+ "https://stac-extensions.github.io/sat/v1.0.0/schema.json"
+ ],
+ "id": "S2A_30VWN_20200830_0_L2A",
+ "bbox": [
+ -3.000355032202007,
+ 60.33378967715628,
+ -0.9494552273687494,
+ 61.33443350563737
+ ],
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [
+ -2.0,
+ 61.0
+ ],
+ [
+ -3.5,
+ 50.0
+ ],
+ [
+ -3.5,
+ 63.0
+ ],
+ [
+ -1.0,
+ 63.0
+ ],
+ [
+ -2.0,
+ 61.0
+ ]
+ ]
+ ]
+ },
+ "properties": {
+ "datetime": "2022-08-30T11:34:28Z",
+ "platform": "sentinel-2c",
+ "constellation": "sentinel-2",
+ "instruments": [
+ "msi"
+ ],
+ "gsd": 10,
+ "view:off_nadir": 0,
+ "proj:epsg": 32630,
+ "sat:relative_orbit": 80,
+ "sentinel:utm_zone": 30,
+ "sentinel:latitude_band": "V",
+ "sentinel:grid_square": "WN",
+ "sentinel:sequence": "0",
+ "sentinel:product_id": "S2A_MSIL2A_20200830T113321_N0214_R080_T30VWN_20200830T121958",
+ "sentinel:data_coverage": 100,
+ "eo:cloud_cover": 0,
+ "sentinel:valid_cloud_cover": false,
+ "created": "2020-08-31T09:57:42.772Z",
+ "updated": "2020-08-31T09:57:42.772Z"
+ },
+ "assets": {
+ "thumbnail": {
+ "title": "Thumbnail",
+ "type": "image/png",
+ "roles": [
+ "thumbnail"
+ ],
+ "href": "https://roda.sentinel-hub.com/sentinel-s2-l1c/tiles/30/V/WN/2020/8/30/0/preview.jpg"
+ },
+ "overview": {
+ "title": "True color image",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "overview"
+ ],
+ "gsd": 10,
+ "eo:bands": [
+ {
+ "name": "B04",
+ "common_name": "red",
+ "center_wavelength": 0.6645,
+ "full_width_half_max": 0.038
+ },
+ {
+ "name": "B03",
+ "common_name": "green",
+ "center_wavelength": 0.56,
+ "full_width_half_max": 0.045
+ },
+ {
+ "name": "B02",
+ "common_name": "blue",
+ "center_wavelength": 0.4966,
+ "full_width_half_max": 0.098
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/L2A_PVI.tif",
+ "proj:shape": [
+ 343,
+ 343
+ ],
+ "proj:transform": [
+ 320,
+ 0,
+ 499980,
+ 0,
+ -320,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "info": {
+ "title": "Original JSON metadata",
+ "type": "application/json",
+ "roles": [
+ "metadata"
+ ],
+ "href": "https://roda.sentinel-hub.com/sentinel-s2-l2a/tiles/30/V/WN/2020/8/30/0/tileInfo.json"
+ },
+ "metadata": {
+ "title": "Original XML metadata",
+ "type": "application/xml",
+ "roles": [
+ "metadata"
+ ],
+ "href": "https://roda.sentinel-hub.com/sentinel-s2-l2a/tiles/30/V/WN/2020/8/30/0/metadata.xml"
+ },
+ "visual": {
+ "title": "True color image",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "overview"
+ ],
+ "gsd": 10,
+ "eo:bands": [
+ {
+ "name": "B04",
+ "common_name": "red",
+ "center_wavelength": 0.6645,
+ "full_width_half_max": 0.038
+ },
+ {
+ "name": "B03",
+ "common_name": "green",
+ "center_wavelength": 0.56,
+ "full_width_half_max": 0.045
+ },
+ {
+ "name": "B02",
+ "common_name": "blue",
+ "center_wavelength": 0.4966,
+ "full_width_half_max": 0.098
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/TCI.tif",
+ "proj:shape": [
+ 10980,
+ 10980
+ ],
+ "proj:transform": [
+ 10,
+ 0,
+ 499980,
+ 0,
+ -10,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "B01": {
+ "title": "Band 1 (coastal)",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 70,
+ "eo:bands": [
+ {
+ "name": "B01",
+ "common_name": "blue",
+ "center_wavelength": 0.5,
+ "full_width_half_max": 0.127
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B01.tiff",
+ "proj:transform": [
+ 60,
+ 0,
+ 499980,
+ 0,
+ -60,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "B2000": {
+ "title": "Band 2 (blue)",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 10,
+ "eo:bands": [
+ {
+ "name": "B02",
+ "common_name": "blue",
+ "center_wavelength": 0.4966,
+ "full_width_half_max": 0.098
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B02.tif",
+ "proj:shape": [
+ 10980,
+ 10980
+ ],
+ "proj:transform": [
+ 10,
+ 0,
+ 499980,
+ 0,
+ -10,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "B02": {
+ "title": "Band 2 (blue)",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 10,
+ "eo:bands": [
+ {
+ "name": "B02",
+ "common_name": "blue",
+ "center_wavelength": 0.4966,
+ "full_width_half_max": 0.098
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B02.tif",
+ "proj:shape": [
+ 10980,
+ 10980
+ ],
+ "proj:transform": [
+ 10,
+ 0,
+ 499980,
+ 0,
+ -10,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "B03": {
+ "title": "Band 3 (green)",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 10,
+ "eo:bands": [
+ {
+ "name": "B03",
+ "common_name": "green",
+ "center_wavelength": 0.56,
+ "full_width_half_max": 0.045
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B03.tif",
+ "proj:shape": [
+ 10980,
+ 10980
+ ],
+ "proj:transform": [
+ 10,
+ 0,
+ 499980,
+ 0,
+ -10,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "B04": {
+ "title": "Band 4 (red)",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 10,
+ "eo:bands": [
+ {
+ "name": "B04",
+ "common_name": "red",
+ "center_wavelength": 0.6645,
+ "full_width_half_max": 0.038
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B04.tif",
+ "proj:shape": [
+ 10980,
+ 10980
+ ],
+ "proj:transform": [
+ 10,
+ 0,
+ 499980,
+ 0,
+ -10,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "B05": {
+ "title": "Band 5",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 20,
+ "eo:bands": [
+ {
+ "name": "B05",
+ "center_wavelength": 0.7039,
+ "full_width_half_max": 0.019
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B05.tif",
+ "proj:shape": [
+ 5490,
+ 5490
+ ],
+ "proj:transform": [
+ 20,
+ 0,
+ 499980,
+ 0,
+ -20,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "B06": {
+ "title": "Band 6",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 20,
+ "eo:bands": [
+ {
+ "name": "B06",
+ "center_wavelength": 0.7402,
+ "full_width_half_max": 0.018
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B06.tif",
+ "proj:shape": [
+ 5490,
+ 5490
+ ],
+ "proj:transform": [
+ 20,
+ 0,
+ 499980,
+ 0,
+ -20,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "B07": {
+ "title": "Band 7",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 20,
+ "eo:bands": [
+ {
+ "name": "B07",
+ "center_wavelength": 0.7825,
+ "full_width_half_max": 0.028
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B07.tif",
+ "proj:shape": [
+ 5490,
+ 5490
+ ],
+ "proj:transform": [
+ 20,
+ 0,
+ 499980,
+ 0,
+ -20,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "B08": {
+ "title": "Band 8 (nir)",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 10,
+ "eo:bands": [
+ {
+ "name": "B08",
+ "common_name": "nir",
+ "center_wavelength": 0.8351,
+ "full_width_half_max": 0.145
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B08.tif",
+ "proj:shape": [
+ 10980,
+ 10980
+ ],
+ "proj:transform": [
+ 10,
+ 0,
+ 499980,
+ 0,
+ -10,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "B8A": {
+ "title": "Band 8A",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 20,
+ "eo:bands": [
+ {
+ "name": "B8A",
+ "center_wavelength": 0.8648,
+ "full_width_half_max": 0.033
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B8A.tif",
+ "proj:shape": [
+ 5490,
+ 5490
+ ],
+ "proj:transform": [
+ 20,
+ 0,
+ 499980,
+ 0,
+ -20,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "B09": {
+ "title": "Band 9",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 60,
+ "eo:bands": [
+ {
+ "name": "B09",
+ "center_wavelength": 0.945,
+ "full_width_half_max": 0.026
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B09.tif",
+ "proj:shape": [
+ 1830,
+ 1830
+ ],
+ "proj:transform": [
+ 60,
+ 0,
+ 499980,
+ 0,
+ -60,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "B11": {
+ "title": "Band 11 (swir16)",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 20,
+ "eo:bands": [
+ {
+ "name": "B11",
+ "common_name": "swir16",
+ "center_wavelength": 1.6137,
+ "full_width_half_max": 0.143
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B11.tif",
+ "proj:shape": [
+ 5490,
+ 5490
+ ],
+ "proj:transform": [
+ 20,
+ 0,
+ 499980,
+ 0,
+ -20,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "B12": {
+ "title": "Band 12 (swir22)",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "gsd": 20,
+ "eo:bands": [
+ {
+ "name": "B12",
+ "common_name": "swir22",
+ "center_wavelength": 2.22024,
+ "full_width_half_max": 0.242
+ }
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/B12.tif",
+ "proj:shape": [
+ 5490,
+ 5490
+ ],
+ "proj:transform": [
+ 20,
+ 0,
+ 499980,
+ 0,
+ -20,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "AOT": {
+ "title": "Aerosol Optical Thickness (AOT)",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/AOT.tif",
+ "proj:shape": [
+ 1830,
+ 1830
+ ],
+ "proj:transform": [
+ 60,
+ 0,
+ 499980,
+ 0,
+ -60,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "WVP": {
+ "title": "Water Vapour (WVP)",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/WVP.tif",
+ "proj:shape": [
+ 10980,
+ 10980
+ ],
+ "proj:transform": [
+ 10,
+ 0,
+ 499980,
+ 0,
+ -10,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ },
+ "SCL": {
+ "title": "Scene Classification Map (SCL)",
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
+ "roles": [
+ "data"
+ ],
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/SCL.tif",
+ "proj:shape": [
+ 5490,
+ 5490
+ ],
+ "proj:transform": [
+ 20,
+ 0,
+ 499980,
+ 0,
+ -20,
+ 6800040,
+ 0,
+ 0,
+ 1
+ ]
+ }
+ },
+ "links": [
+ {
+ "rel": "self",
+ "href": "https://earth-search.aws.element84.com/v0/collections/sentinel-s2-l2a-cogs/items/S2A_30VWN_20200830_0_L2A"
+ },
+ {
+ "rel": "canonical",
+ "href": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/S2A_30VWN_20200830_0_L2A.json",
+ "type": "application/json"
+ },
+ {
+ "rel": "canonical",
+ "href": "https://cirrus-v0-data-1qm7gekzjucbq.s3.us-west-2.amazonaws.com/sentinel-s2-l2a/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/S2A_30VWN_20200830_0_L2A.json",
+ "type": "application/json"
+ },
+ {
+ "title": "Source STAC Item",
+ "rel": "derived_from",
+ "href": "https://cirrus-v0-data-1qm7gekzjucbq.s3.us-west-2.amazonaws.com/sentinel-s2-l2a/30/V/WN/2020/8/S2A_30VWN_20200830_0_L2A/S2A_30VWN_20200830_0_L2A.json",
+ "type": "application/json"
+ },
+ {
+ "rel": "parent",
+ "href": "https://earth-search.aws.element84.com/v0/collections/sentinel-s2-l2a-cogs"
+ },
+ {
+ "rel": "collection",
+ "href": "https://earth-search.aws.element84.com/v0/collections/sentinel-s2-l2a-cogs"
+ },
+ {
+ "rel": "root",
+ "href": "https://earth-search.aws.element84.com/v0/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/DotNetStac/Common/JsonMergeUtils.cs b/src/DotNetStac/Common/JsonMergeUtils.cs
new file mode 100644
index 00000000..46501846
--- /dev/null
+++ b/src/DotNetStac/Common/JsonMergeUtils.cs
@@ -0,0 +1,246 @@
+using System;
+using System.Buffers;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Stac.Common
+{
+ public class JsonMergeUtils
+ {
+
+ ///
+ /// Return the result of merging the original JSON document with the JSON Merge patch document
+ /// according to https://tools.ietf.org/html/rfc7386
+ ///
+ ///
+ ///
+ /// Writer options used to write the merge result.
+ /// The document that represents the merge result.
+ public static string Merge(string original, string patch, JsonWriterOptions? writerOptions = null)
+ {
+ var memStream = new MemoryStream();
+
+ using (var originalDoc = JsonDocument.Parse(original))
+ using (var patchDoc = JsonDocument.Parse(patch))
+ using (var jsonWriter = new Utf8JsonWriter(memStream, writerOptions ?? new JsonWriterOptions { Indented = true }))
+ {
+
+ var originalKind = originalDoc.RootElement.ValueKind;
+ var patchKind = patchDoc.RootElement.ValueKind;
+
+ if (originalKind != JsonValueKind.Object)
+ {
+ throw new InvalidOperationException($"The original JSON document to merge new content into must be an object type. Instead it is {originalKind}.");
+ }
+
+ if (patchKind != JsonValueKind.Object)
+ {
+ throw new InvalidOperationException($"The patch JSON document must be an object type. Instead it is {originalKind}.");
+ }
+
+ if (originalKind != patchKind)
+ {
+ return original;
+ }
+
+ MergeObjects(jsonWriter, originalDoc.RootElement, patchDoc.RootElement);
+ }
+
+
+ return Encoding.UTF8.GetString(memStream.ToArray());
+ }
+
+ ///
+ /// Return the result of merging the original JSON document with the JSON Merge patch document
+ /// according to https://tools.ietf.org/html/rfc7386
+ ///
+ ///
+ ///
+ /// Writer options used to write the merge result.
+ public static async Task MergeAsync(string original, System.IO.Stream patch, CancellationToken token = default, JsonWriterOptions? writerOptions = null)
+ {
+ var outputBuffer = new MemoryStream();
+ var jsonDocumentOptions = new JsonDocumentOptions();
+ using (var originalDoc = JsonDocument.Parse(original, jsonDocumentOptions))
+ using (var patchDoc = await JsonDocument.ParseAsync(patch, jsonDocumentOptions, token))
+ using (var jsonWriter = new Utf8JsonWriter(outputBuffer, writerOptions ?? new JsonWriterOptions { Indented = true }))
+ {
+ var originalKind = originalDoc.RootElement.ValueKind;
+ var patchKind = patchDoc.RootElement.ValueKind;
+
+ if (originalKind != JsonValueKind.Object)
+ {
+ throw new InvalidOperationException($"The original JSON document to merge new content into must be an object type. Instead it is {originalKind}.");
+ }
+
+ if (patchKind != JsonValueKind.Object)
+ {
+ throw new InvalidOperationException($"The patch JSON document must be an object type. Instead it is {originalKind}.");
+ }
+
+ if (originalKind != patchKind)
+ {
+ originalDoc.WriteTo(jsonWriter);
+ }
+ else
+ {
+ MergeObjects(jsonWriter, originalDoc.RootElement, patchDoc.RootElement);
+ }
+ }
+
+ return Encoding.UTF8.GetString(outputBuffer.ToArray());
+ }
+
+ ///
+ /// Extract property names with a null value.
+ ///
+ /// Nested field names are returned joined by "."
+ /// Array items are ignored.
+ ///
+ ///
+ ///
+ /// Writer options used to write the merge result.
+ /// The list of null properties.
+ public static List ExtractNullProperties(string patch)
+ {
+ var patchDoc = JsonDocument.Parse(patch);
+ if (patchDoc.RootElement.ValueKind != JsonValueKind.Object)
+ {
+ throw new InvalidOperationException($"The patch JSON document must be an object type. Instead it is {patchDoc.RootElement.ValueKind}.");
+ }
+
+ return ExtractNullPropertiesFromObject(patchDoc.RootElement).ToList();
+ }
+
+ ///
+ /// Extract property names with a null value.
+ ///
+ /// Nested field names are returned joined by "."
+ /// Array items are ignored.
+ ///
+ ///
+ ///
+ /// Writer options used to write the merge result.
+ /// Cancellation token.
+ /// The list of null properties.
+ public static async Task> ExtractNullPropertiesAsync(System.IO.Stream patch, CancellationToken token = default)
+ {
+ var patchDoc = await JsonDocument.ParseAsync(patch, new JsonDocumentOptions(), token);
+ if (patchDoc.RootElement.ValueKind != JsonValueKind.Object)
+ {
+ throw new InvalidOperationException($"The patch JSON document must be an object type. Instead it is {patchDoc.RootElement.ValueKind}.");
+ }
+
+ return ExtractNullPropertiesFromObject(patchDoc.RootElement).ToList();
+ }
+
+ ///
+ /// Apply the result of a JSON merge patch to the given model, using System.Text.Json serializer
+ /// to serialize and deserialize the model.
+ ///
+ /// the model type
+ ///
+ ///
+ /// JSON serialization options
+ /// A new model representing the patched instance.
+ public static T MergeModel(T original, string patch, JsonSerializerOptions options = null)
+ {
+ var originalJson = JsonSerializer.Serialize(original, options);
+ return JsonSerializer.Deserialize(Merge(originalJson, patch), options);
+ }
+
+ ///
+ /// Apply the result of a JSON merge patch to the given model, using System.Text.Json serializer
+ /// to serialize and deserialize the model.
+ ///
+ ///
+ /// the model type
+ ///
+ ///
+ /// JSON serialization options
+ /// Cancellation token
+ /// A task that returns a new model representing the patched instance.
+ public static async Task MergeModelAsync(T original, System.IO.Stream patch, JsonSerializerOptions options = null, CancellationToken token = default)
+ {
+ var originalJson = JsonSerializer.Serialize(original, options);
+ return JsonSerializer.Deserialize(await MergeAsync(originalJson, patch, token), options);
+ }
+
+ private static IEnumerable ExtractNullPropertiesFromObject(JsonElement patch)
+ {
+ Debug.Assert(patch.ValueKind == JsonValueKind.Object);
+ foreach (var property in patch.EnumerateObject())
+ {
+ if (property.Value.ValueKind == JsonValueKind.Null)
+ {
+ yield return property.Name;
+ }
+ else if (property.Value.ValueKind == JsonValueKind.Object)
+ {
+ foreach (var field in ExtractNullPropertiesFromObject(property.Value))
+ {
+ yield return string.Join(".", property.Name, field);
+ }
+ }
+ }
+ }
+
+ private static void MergeObjects(Utf8JsonWriter jsonWriter, JsonElement original, JsonElement patch)
+ {
+ Debug.Assert(original.ValueKind == JsonValueKind.Object);
+ Debug.Assert(patch.ValueKind == JsonValueKind.Object);
+
+ jsonWriter.WriteStartObject();
+
+ // Write all the properties of the original document.
+ // If a property exists in both documents, either:
+ // * Merge them, if they are both objects
+ // * Completely override the value of the original with the one from the patch, if the value kind mismatches (e.g. one is object, while the other is an array or string)
+ // * Ignore the original property if the patch property value is null
+ foreach (var property in original.EnumerateObject())
+ {
+ if (patch.TryGetProperty(property.Name, out JsonElement patchPropValue))
+ {
+ if (patchPropValue.ValueKind == JsonValueKind.Null)
+ {
+ continue;
+ }
+
+ jsonWriter.WritePropertyName(property.Name);
+
+ var propValue = property.Value;
+
+ if (patchPropValue.ValueKind == JsonValueKind.Object && propValue.ValueKind == JsonValueKind.Object)
+ {
+ MergeObjects(jsonWriter, propValue, patchPropValue); // Recursive call
+ }
+ else
+ {
+ patchPropValue.WriteTo(jsonWriter);
+ }
+ }
+ else
+ {
+ property.WriteTo(jsonWriter);
+ }
+ }
+
+ // Write all the properties of the patch document that are unique to it (beside null values).
+ foreach (var property in patch.EnumerateObject())
+ {
+ if (!original.TryGetProperty(property.Name, out JsonElement patchPropValue) && patchPropValue.ValueKind != JsonValueKind.Null)
+ {
+ property.WriteTo(jsonWriter);
+ }
+ }
+
+ jsonWriter.WriteEndObject();
+ }
+ }
+}
diff --git a/src/DotNetStac/Common/PatchHelpers.cs b/src/DotNetStac/Common/PatchHelpers.cs
new file mode 100644
index 00000000..89ca9bd8
--- /dev/null
+++ b/src/DotNetStac/Common/PatchHelpers.cs
@@ -0,0 +1,31 @@
+using System.Collections.Generic;
+using System.Linq;
+using GeoJSON.Net.Geometry;
+using Newtonsoft.Json;
+
+namespace Stac.Common
+{
+ public static class PatchHelpers
+ {
+ public static T Patch(this T stacObject, IDictionary patch) where T : IStacObject
+ {
+ var itemJson = StacConvert.Serialize(stacObject);
+ var patchJson = JsonConvert.SerializeObject(patch);
+ var patchedJson = JsonMergeUtils.Merge(itemJson, patchJson, new System.Text.Json.JsonWriterOptions { Indented = false });
+ return StacConvert.Deserialize(patchedJson);
+ }
+
+ public static T Patch(this T stacObject, IStacObject patch) where T : IStacObject
+ {
+ var itemJson = StacConvert.Serialize(stacObject);
+ var patchJson = StacConvert.Serialize(patch);
+ IDictionary patchdic = JsonConvert.DeserializeObject>(patchJson);
+ if (patchdic.ContainsKey("links") && patchdic["links"] is IEnumerable