Skip to content

Commit 29b0f7d

Browse files
authored
Merge pull request #138 from ladeak/incremental-generator
Incremental source Generator
2 parents 9ef122f + 7ecabe6 commit 29b0f7d

File tree

65 files changed

+1198
-1532
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+1198
-1532
lines changed

.github/codecoverage.runsettings

Lines changed: 0 additions & 14 deletions
This file was deleted.

.github/workflows/cd.yaml

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
name: CD
2-
32
on:
43
release:
54
types: [created]
@@ -12,14 +11,12 @@ jobs:
1211
- name: Setup .NET
1312
uses: actions/setup-dotnet@v4
1413
with:
15-
dotnet-version: '8.0.x'
14+
dotnet-version: '9.0.x'
1615
include-prerelease: true
1716
- name: Set VERSION variable from tag
1817
run: echo "VERSION=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_ENV
19-
- name: Restore dependencies
20-
run: dotnet restore
2118
- name: Build
22-
run: dotnet build --configuration Release --no-restore -p:Version=${VERSION}
19+
run: dotnet build --configuration Release -p:Version=${VERSION}
2320
- name: Test
2421
run: dotnet test
2522
- name: Create package

.github/workflows/ci.yaml

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,15 @@ on: [push, workflow_dispatch]
44

55
jobs:
66
build:
7-
87
runs-on: ubuntu-latest
9-
108
steps:
119
- uses: actions/checkout@v4
1210
- name: Setup .NET
1311
uses: actions/setup-dotnet@v4
1412
with:
15-
dotnet-version: '8.0.x'
13+
dotnet-version: '9.0.x'
1614
include-prerelease: true
17-
- name: Install dependencies
18-
run: dotnet restore
1915
- name: Build
20-
run: dotnet build --configuration Release --no-restore
16+
run: dotnet build --configuration Release
2117
- name: Test
22-
run: dotnet test --collect:"XPlat Code Coverage" --settings ./.github/codecoverage.runsettings
23-
- name: Upload Test Coverage
24-
uses: codecov/codecov-action@v4
25-
with:
26-
flags: dotnet
18+
run: dotnet test

Readme.md

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22

33
JsonMergePatch library provides an implementation for json merge patch operations, detailed in RFC7396. This library uses C# source generators to generate the types required for serialization. The Http package provides extension methods for HTTP requests and responses, while the AspNetCore package provides an InputReader implementation.
44

5-
[![CI](https://github.com/ladeak/JsonMergePatch/workflows/CI/badge.svg)](https://github.com/ladeak/JsonMergePatch/actions) [![CodeCoverage](https://codecov.io/gh/ladeak/JsonMergePatch/branch/master/graph/badge.svg)](https://app.codecov.io/gh/ladeak/JsonMergePatch) [![NuGet](https://img.shields.io/nuget/v/LaDeak.JsonMergePatch.AspNetCore.svg)](https://www.nuget.org/packages/LaDeak.JsonMergePatch.AspNetCore/)
5+
[![CI](https://github.com/ladeak/JsonMergePatch/workflows/CI/badge.svg)](https://github.com/ladeak/JsonMergePatch/actions) [![NuGet](https://img.shields.io/nuget/v/LaDeak.JsonMergePatch.AspNetCore.svg)](https://www.nuget.org/packages/LaDeak.JsonMergePatch.AspNetCore/)
66

77
## Getting Started
88

99
JsonMergePatch library helps to deserialize http requests' and responses' json body content for merge patch operation. Merge patch operation is detailed by [RFC7396](https://tools.ietf.org/html/rfc7396). If the merge patch request contains members that appear as null on the target object, those members are added. If the target object contains the member, the value is replaced. Members with null values in the merge patch requests, are removed from the target object (set to null or default).
1010

1111
JsonMergePatch library is based on C# source generators. For the http body content to be deserialized into a type, the SourceGenerator library generates helper classes. Helper classes are called Wrappers, capturing all the features of the type intended to be used for the deserialization. Once the request is deserialized into a Wrapper object, the object can be used to apply the patch on the user defined target object. The JsonMergePatch library is designed to be used with POCO classes and record types.
1212

13-
Source Generations requires Visual Studio 16.9 or later.
13+
Source Generations requires Visual Studio 17.12 or later.
1414

1515
Based on the given application type different packages may be installed from NuGet by running one or more of the following commands:
1616

@@ -24,7 +24,7 @@ dotnet add package LaDeak.JsonMergePatch.AspNetCore
2424

2525
1. Install AspNetCore package via NuGet
2626
1. Add the required usings
27-
1. Add a new controller with a parameter types ```Patch<T>``` where ```T``` is a custom target type chosen by the user
27+
1. Add a new controller with a parameter types ```Patch<T>``` where ```T``` is a custom target type chosen by the user. Make sure that `[Patchable]` is applied on the `T` target type.
2828
1. Extend application startup
2929

3030
### Install AspNetCore packages via NuGet
@@ -58,7 +58,7 @@ public WeatherForecast PatchForecast(Patch<WeatherForecast> input)
5858
}
5959
```
6060

61-
During build, the source generator scans for methods with type parameters of ```Patch<T>```. When such a parameter is found a Wrapper type is generated for ```T```. The base class of the generated type provides the necessary operations to work with the type.
61+
During build, the source generator scans types has `[Patchable]` attribute applied. When such a type is found a Wrapper type is generated for it.
6262

6363
### Extend application startup
6464

@@ -86,9 +86,7 @@ The AspNetCore input reader supports requests with ```application/merge-patch+js
8686

8787
### Patchable
8888

89-
Certain use-cases require to generate wrapper types with the source generation for assemblies that do not directly use `Patch<T>` (where T is the wrapped source type). This could be a reason for having separate assemblies for entity types, or because of the need of stacking multiple source generators on top of each other.
90-
In thie case types may be attributed with `[Patchable]` attribute:
91-
89+
To generate wrapper types with the source generation add the `[Patchable]` attribute:
9290
```csharp
9391
[Patchable]
9492
public class WeatherForecast
@@ -97,8 +95,6 @@ public class WeatherForecast
9795
}
9896
```
9997

100-
`[Patchable]` makes sure to generate wrapper types for source types not used in HTTP requests or method arguments of `Patch<T>`.
101-
10298
### Using it with System.Text.Json source generation
10399

104100
In order to use multiple source generators, we need to *stack* them. Today the only way to do it is by enforcing a build order between two projects, while adding the first source generator to the first project built, and the second one to the second project built. To make sure JsonMergePatch source generator works with System.Text.Json's source generator create two projects:

sample/AspNetCoreMinimal.Entities/AspNetCoreMinimal.Entities.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFramework>net8.0</TargetFramework>
4+
<TargetFramework>net9.0</TargetFramework>
55
<Nullable>enable</Nullable>
66
<IsPackable>false</IsPackable>
7+
<ImplicitUsings>true</ImplicitUsings>
78
</PropertyGroup>
89

910
<ItemGroup>

sample/AspNetCoreMinimal.Entities/Entities.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
using System;
2-
using System.Collections.Generic;
3-
using System.Text.Json.Serialization;
1+
using System.Text.Json.Serialization;
42
using LaDeak.JsonMergePatch.Abstractions;
53

64
namespace AspNetCoreMinimal.Entities;

sample/AspNetCoreMinimal/AspNetCoreMinimal.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
<Project Sdk="Microsoft.NET.Sdk.Web">
22

33
<PropertyGroup>
4-
<TargetFramework>net8.0</TargetFramework>
4+
<TargetFramework>net9.0</TargetFramework>
55
<Nullable>enable</Nullable>
66
<IsPackable>false</IsPackable>
7+
<ImplicitUsings>true</ImplicitUsings>
78
</PropertyGroup>
89

910
<ItemGroup>

sample/AspNetCoreMinimal/Controllers/SampleController.cs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
using System;
2-
using System.Collections.Generic;
3-
using System.Net.Http;
4-
using System.Text.Json;
5-
using System.Threading.Tasks;
1+
using System.Text.Json;
62
using AspNetCoreMinimal.Entities;
73
using LaDeak.JsonMergePatch.Abstractions;
84
using LaDeak.JsonMergePatch.Http;
@@ -43,7 +39,7 @@ public CitiesData PatchCities(Patch<CitiesData> input)
4339
[HttpGet("ReadJsonPatchAsync")]
4440
public async Task<WeatherForecast> GetReadJsonPatchAsync()
4541
{
46-
var target = new WeatherForecast() { Date = DateTime.UtcNow, Summary = "Sample weather forecast", TemperatureC = 24 };
42+
var target = new WeatherForecast() { Date = DateTime.UtcNow, Summary = "Sample weather forecast", TemperatureC = 22 };
4743
var httpClient = _clientFactory.CreateClient();
4844
var response = await httpClient.GetAsync("https://localhost:5001/Sample/Weather", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
4945
var responseData = await response.Content.ReadJsonPatchAsync<WeatherForecast>(new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }).ConfigureAwait(false);

sample/AspNetCoreMinimal/Program.cs

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,22 @@
11
using System.Text.Json.Serialization;
22
using LaDeak.JsonMergePatch.AspNetCore;
3-
using Microsoft.AspNetCore.Builder;
4-
using Microsoft.Extensions.DependencyInjection;
5-
using Microsoft.Extensions.Hosting;
63

74
var builder = WebApplication.CreateBuilder(args);
85

96
var mvcBuilder = builder.Services.AddControllers().AddMvcOptions(options =>
107
{
118
LaDeak.JsonMergePatch.Abstractions.JsonMergePatchOptions.Repository = LaDeak.JsonMergePatch.Generated.SafeAspNetCoreMinimal.Entities.TypeRepository.Instance;
129
var jsonOptions = new Microsoft.AspNetCore.Http.Json.JsonOptions();
13-
jsonOptions.SerializerOptions.AddContext<SampleJsonContext>();
10+
jsonOptions.SerializerOptions.TypeInfoResolver = SampleJsonContext.Default;
1411
options.InputFormatters.Insert(0, new JsonMergePatchInputReader(jsonOptions));
1512
});
1613
builder.Services.AddHttpClient();
17-
18-
1914
var app = builder.Build();
20-
2115
app.UseHttpsRedirection();
22-
2316
app.UseAuthorization();
24-
2517
app.MapControllers();
26-
2718
app.Run();
2819

29-
3020
[JsonSerializable(typeof(LaDeak.JsonMergePatch.Generated.SafeAspNetCoreMinimal.Entities.WeatherForecastWrapped))]
3121
[JsonSerializable(typeof(LaDeak.JsonMergePatch.Generated.SafeAspNetCoreMinimal.Entities.CitiesDataWrapped))]
3222
public partial class SampleJsonContext : JsonSerializerContext
Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,13 @@
11
{
22
"$schema": "https://json.schemastore.org/launchsettings.json",
3-
"iisSettings": {
4-
"windowsAuthentication": false,
5-
"anonymousAuthentication": true,
6-
"iisExpress": {
7-
"applicationUrl": "http://localhost:42816",
8-
"sslPort": 44364
9-
}
10-
},
113
"profiles": {
124
"AspNetCoreMinimal": {
135
"commandName": "Project",
146
"dotnetRunMessages": true,
15-
"launchBrowser": true,
16-
"launchUrl": "swagger",
177
"applicationUrl": "https://localhost:5001;http://localhost:5000",
188
"environmentVariables": {
199
"ASPNETCORE_ENVIRONMENT": "Development"
2010
}
21-
},
22-
"IIS Express": {
23-
"commandName": "IISExpress",
24-
"launchBrowser": true,
25-
"launchUrl": "swagger",
26-
"environmentVariables": {
27-
"ASPNETCORE_ENVIRONMENT": "Development"
28-
}
2911
}
3012
}
3113
}

sample/AspNetCoreMinimal/sample.http

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
###
2+
3+
GET https://localhost:5001/Sample/Weather
4+
5+
###
6+
7+
PATCH https://localhost:5001/Sample/PatchWeather
8+
Content-Type: application/merge-patch+json
9+
10+
{
11+
"temp":23,
12+
"summary":null
13+
}
14+
###
15+
16+
PATCH https://localhost:5001/Sample/PatchCities
17+
Content-Type: application/merge-patch+json
18+
19+
{
20+
"cities":
21+
{
22+
"Dublin":"Ireland",
23+
"London":"GB",
24+
"New York":null
25+
}
26+
}
27+
###
28+
29+
GET https://localhost:5001/Sample/ReadJsonPatchAsync

sample/ConsoleApp/ConsoleApp.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
5-
<TargetFramework>net8.0</TargetFramework>
5+
<TargetFramework>net9.0</TargetFramework>
66
<IsPackable>false</IsPackable>
7+
<ImplicitUsings>true</ImplicitUsings>
78
</PropertyGroup>
89

910
<ItemGroup>

sample/ConsoleApp/Program.cs

Lines changed: 23 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,31 @@
1-
using System;
2-
using System.Net.Http;
3-
using System.Threading.Tasks;
4-
using ConsoleAppLibrary;
5-
using LaDeak.JsonMergePatch.Abstractions;
1+
using LaDeak.JsonMergePatch.Abstractions;
62
using LaDeak.JsonMergePatch.Http;
73

8-
namespace ReadJsonPatchAsync
4+
namespace ReadJsonPatchAsync;
5+
6+
public class Program
97
{
10-
public class Program
8+
[Patchable]
9+
public class WeatherForecast
1110
{
12-
public class WeatherForecast
13-
{
14-
public DateTime Date { get; set; }
15-
public int Temp { get; set; }
16-
public string Summary { get; set; }
17-
}
18-
19-
public static async Task Main(string[] args)
20-
{
21-
LaDeak.JsonMergePatch.Abstractions.JsonMergePatchOptions.Repository = LaDeak.JsonMergePatch.Generated.SafeConsoleApp.TypeRepository.Instance.Extend(LaDeak.JsonMergePatch.Generated.SafeConsoleAppLibrary.TypeRepository.Instance);
22-
await ReadAsJsonMergePatchAsync();
23-
}
11+
public DateTime Date { get; set; }
12+
public int Temp { get; set; }
13+
public string Summary { get; set; }
14+
}
2415

25-
public static async Task ReadAsJsonMergePatchAsync()
26-
{
27-
var httpClient = new HttpClient();
28-
var response = await httpClient.GetAsync("https://localhost:5001/Sample/Weather", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
29-
var responseData = await response.Content.ReadJsonPatchAsync<WeatherForecast>().ConfigureAwait(false);
30-
var target = new WeatherForecast() { Date = DateTime.UtcNow, Summary = "Sample weather forecast", Temp = 24 };
31-
var result = responseData.ApplyPatch(target);
32-
Console.WriteLine($"Patched: Date={result.Date}, Summary={result.Summary}, Temp={result.Temp}");
16+
public static async Task Main(string[] args)
17+
{
18+
LaDeak.JsonMergePatch.Abstractions.JsonMergePatchOptions.Repository = LaDeak.JsonMergePatch.Generated.SafeConsoleApp.TypeRepository.Instance.Extend(LaDeak.JsonMergePatch.Generated.SafeConsoleAppLibrary.TypeRepository.Instance);
19+
await ReadAsJsonMergePatchAsync();
20+
}
3321

34-
var client = new Client();
35-
await client.ReadAsJsonMergePatchAsync();
36-
}
22+
public static async Task ReadAsJsonMergePatchAsync()
23+
{
24+
var httpClient = new HttpClient();
25+
var response = await httpClient.GetAsync("https://localhost:5001/Sample/Weather", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
26+
var responseData = await response.Content.ReadJsonPatchAsync<WeatherForecast>().ConfigureAwait(false);
27+
var target = new WeatherForecast() { Date = DateTime.UtcNow, Summary = "Sample weather forecast", Temp = 22 };
28+
var result = responseData.ApplyPatch(target);
29+
Console.WriteLine($"Patched: Date={result.Date}, Summary={result.Summary}, Temp={result.Temp}");
3730
}
3831
}

sample/ConsoleAppLibrary/ConsoleAppLibrary.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFramework>net8.0</TargetFramework>
4+
<TargetFramework>net9.0</TargetFramework>
55
<IsPackable>false</IsPackable>
6+
<ImplicitUsings>true</ImplicitUsings>
67
</PropertyGroup>
78

89
<ItemGroup>

sample/ConsoleAppLibrary/Sample.cs

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,24 @@
1-
using System;
2-
using System.Net.Http;
3-
using System.Threading.Tasks;
1+
using LaDeak.JsonMergePatch.Abstractions;
42
using LaDeak.JsonMergePatch.Http;
53

6-
namespace ConsoleAppLibrary
4+
namespace ConsoleAppLibrary;
5+
6+
[Patchable]
7+
public class DeviceData
78
{
8-
public class DeviceData
9-
{
10-
public double Watts { get; set; }
11-
public string Name { get; set; }
12-
}
9+
public double Watts { get; set; }
10+
public string Name { get; set; }
11+
}
1312

14-
public class Client
13+
public class Client
14+
{
15+
public async Task ReadAsJsonMergePatchAsync()
1516
{
16-
public async Task ReadAsJsonMergePatchAsync()
17-
{
18-
var httpClient = new HttpClient();
19-
var response = await httpClient.GetAsync("https://localhost:5001/Sample/DeviceData", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
20-
var responseData = await response.Content.ReadJsonPatchAsync<DeviceData>().ConfigureAwait(false);
21-
var target = new DeviceData() { Watts = 5, Name = "phone" };
22-
var result = responseData.ApplyPatch(target);
23-
Console.WriteLine($"Patched: Name={result.Name}, Watts={result.Watts}");
24-
}
17+
var httpClient = new HttpClient();
18+
var response = await httpClient.GetAsync("https://localhost:5001/Sample/DeviceData", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
19+
var responseData = await response.Content.ReadJsonPatchAsync<DeviceData>().ConfigureAwait(false);
20+
var target = new DeviceData() { Watts = 5, Name = "phone" };
21+
var result = responseData.ApplyPatch(target);
22+
Console.WriteLine($"Patched: Name={result.Name}, Watts={result.Watts}");
2523
}
2624
}

0 commit comments

Comments
 (0)