Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Incremental source Generator #138

Merged
merged 4 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 0 additions & 14 deletions .github/codecoverage.runsettings

This file was deleted.

7 changes: 2 additions & 5 deletions .github/workflows/cd.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
name: CD

on:
release:
types: [created]
Expand All @@ -12,14 +11,12 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
dotnet-version: '9.0.x'
include-prerelease: true
- name: Set VERSION variable from tag
run: echo "VERSION=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_ENV
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore -p:Version=${VERSION}
run: dotnet build --configuration Release -p:Version=${VERSION}
- name: Test
run: dotnet test
- name: Create package
Expand Down
14 changes: 3 additions & 11 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,15 @@ on: [push, workflow_dispatch]

jobs:
build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
dotnet-version: '9.0.x'
include-prerelease: true
- name: Install dependencies
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore
run: dotnet build --configuration Release
- name: Test
run: dotnet test --collect:"XPlat Code Coverage" --settings ./.github/codecoverage.runsettings
- name: Upload Test Coverage
uses: codecov/codecov-action@v4
with:
flags: dotnet
run: dotnet test
14 changes: 5 additions & 9 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

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.

[![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/)
[![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/)

## Getting Started

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).

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.

Source Generations requires Visual Studio 16.9 or later.
Source Generations requires Visual Studio 17.12 or later.

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

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

1. Install AspNetCore package via NuGet
1. Add the required usings
1. Add a new controller with a parameter types ```Patch<T>``` where ```T``` is a custom target type chosen by the user
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.
1. Extend application startup

### Install AspNetCore packages via NuGet
Expand Down Expand Up @@ -58,7 +58,7 @@ public WeatherForecast PatchForecast(Patch<WeatherForecast> input)
}
```

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.
During build, the source generator scans types has `[Patchable]` attribute applied. When such a type is found a Wrapper type is generated for it.

### Extend application startup

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

### Patchable

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.
In thie case types may be attributed with `[Patchable]` attribute:

To generate wrapper types with the source generation add the `[Patchable]` attribute:
```csharp
[Patchable]
public class WeatherForecast
Expand All @@ -97,8 +95,6 @@ public class WeatherForecast
}
```

`[Patchable]` makes sure to generate wrapper types for source types not used in HTTP requests or method arguments of `Patch<T>`.

### Using it with System.Text.Json source generation

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:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<ImplicitUsings>true</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
Expand Down
4 changes: 1 addition & 3 deletions sample/AspNetCoreMinimal.Entities/Entities.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization;
using LaDeak.JsonMergePatch.Abstractions;

namespace AspNetCoreMinimal.Entities;
Expand Down
3 changes: 2 additions & 1 deletion sample/AspNetCoreMinimal/AspNetCoreMinimal.csproj
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<ImplicitUsings>true</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
Expand Down
8 changes: 2 additions & 6 deletions sample/AspNetCoreMinimal/Controllers/SampleController.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using System.Text.Json;
using AspNetCoreMinimal.Entities;
using LaDeak.JsonMergePatch.Abstractions;
using LaDeak.JsonMergePatch.Http;
Expand Down Expand Up @@ -43,7 +39,7 @@ public CitiesData PatchCities(Patch<CitiesData> input)
[HttpGet("ReadJsonPatchAsync")]
public async Task<WeatherForecast> GetReadJsonPatchAsync()
{
var target = new WeatherForecast() { Date = DateTime.UtcNow, Summary = "Sample weather forecast", TemperatureC = 24 };
var target = new WeatherForecast() { Date = DateTime.UtcNow, Summary = "Sample weather forecast", TemperatureC = 22 };
var httpClient = _clientFactory.CreateClient();
var response = await httpClient.GetAsync("https://localhost:5001/Sample/Weather", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
var responseData = await response.Content.ReadJsonPatchAsync<WeatherForecast>(new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }).ConfigureAwait(false);
Expand Down
12 changes: 1 addition & 11 deletions sample/AspNetCoreMinimal/Program.cs
Original file line number Diff line number Diff line change
@@ -1,32 +1,22 @@
using System.Text.Json.Serialization;
using LaDeak.JsonMergePatch.AspNetCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var builder = WebApplication.CreateBuilder(args);

var mvcBuilder = builder.Services.AddControllers().AddMvcOptions(options =>
{
LaDeak.JsonMergePatch.Abstractions.JsonMergePatchOptions.Repository = LaDeak.JsonMergePatch.Generated.SafeAspNetCoreMinimal.Entities.TypeRepository.Instance;
var jsonOptions = new Microsoft.AspNetCore.Http.Json.JsonOptions();
jsonOptions.SerializerOptions.AddContext<SampleJsonContext>();
jsonOptions.SerializerOptions.TypeInfoResolver = SampleJsonContext.Default;
options.InputFormatters.Insert(0, new JsonMergePatchInputReader(jsonOptions));
});
builder.Services.AddHttpClient();


var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();


[JsonSerializable(typeof(LaDeak.JsonMergePatch.Generated.SafeAspNetCoreMinimal.Entities.WeatherForecastWrapped))]
[JsonSerializable(typeof(LaDeak.JsonMergePatch.Generated.SafeAspNetCoreMinimal.Entities.CitiesDataWrapped))]
public partial class SampleJsonContext : JsonSerializerContext
Expand Down
18 changes: 0 additions & 18 deletions sample/AspNetCoreMinimal/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -1,31 +1,13 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:42816",
"sslPort": 44364
}
},
"profiles": {
"AspNetCoreMinimal": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
29 changes: 29 additions & 0 deletions sample/AspNetCoreMinimal/sample.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
###

GET https://localhost:5001/Sample/Weather

###

PATCH https://localhost:5001/Sample/PatchWeather
Content-Type: application/merge-patch+json

{
"temp":23,
"summary":null
}
###

PATCH https://localhost:5001/Sample/PatchCities
Content-Type: application/merge-patch+json

{
"cities":
{
"Dublin":"Ireland",
"London":"GB",
"New York":null
}
}
###

GET https://localhost:5001/Sample/ReadJsonPatchAsync
3 changes: 2 additions & 1 deletion sample/ConsoleApp/ConsoleApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<IsPackable>false</IsPackable>
<ImplicitUsings>true</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
Expand Down
53 changes: 23 additions & 30 deletions sample/ConsoleApp/Program.cs
Original file line number Diff line number Diff line change
@@ -1,38 +1,31 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using ConsoleAppLibrary;
using LaDeak.JsonMergePatch.Abstractions;
using LaDeak.JsonMergePatch.Abstractions;
using LaDeak.JsonMergePatch.Http;

namespace ReadJsonPatchAsync
namespace ReadJsonPatchAsync;

public class Program
{
public class Program
[Patchable]
public class WeatherForecast
{
public class WeatherForecast
{
public DateTime Date { get; set; }
public int Temp { get; set; }
public string Summary { get; set; }
}

public static async Task Main(string[] args)
{
LaDeak.JsonMergePatch.Abstractions.JsonMergePatchOptions.Repository = LaDeak.JsonMergePatch.Generated.SafeConsoleApp.TypeRepository.Instance.Extend(LaDeak.JsonMergePatch.Generated.SafeConsoleAppLibrary.TypeRepository.Instance);
await ReadAsJsonMergePatchAsync();
}
public DateTime Date { get; set; }
public int Temp { get; set; }
public string Summary { get; set; }
}

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

var client = new Client();
await client.ReadAsJsonMergePatchAsync();
}
public static async Task ReadAsJsonMergePatchAsync()
{
var httpClient = new HttpClient();
var response = await httpClient.GetAsync("https://localhost:5001/Sample/Weather", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
var responseData = await response.Content.ReadJsonPatchAsync<WeatherForecast>().ConfigureAwait(false);
var target = new WeatherForecast() { Date = DateTime.UtcNow, Summary = "Sample weather forecast", Temp = 22 };
var result = responseData.ApplyPatch(target);
Console.WriteLine($"Patched: Date={result.Date}, Summary={result.Summary}, Temp={result.Temp}");
}
}
3 changes: 2 additions & 1 deletion sample/ConsoleAppLibrary/ConsoleAppLibrary.csproj
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<IsPackable>false</IsPackable>
<ImplicitUsings>true</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
Expand Down
36 changes: 17 additions & 19 deletions sample/ConsoleAppLibrary/Sample.cs
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using LaDeak.JsonMergePatch.Abstractions;
using LaDeak.JsonMergePatch.Http;

namespace ConsoleAppLibrary
namespace ConsoleAppLibrary;

[Patchable]
public class DeviceData
{
public class DeviceData
{
public double Watts { get; set; }
public string Name { get; set; }
}
public double Watts { get; set; }
public string Name { get; set; }
}

public class Client
public class Client
{
public async Task ReadAsJsonMergePatchAsync()
{
public async Task ReadAsJsonMergePatchAsync()
{
var httpClient = new HttpClient();
var response = await httpClient.GetAsync("https://localhost:5001/Sample/DeviceData", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
var responseData = await response.Content.ReadJsonPatchAsync<DeviceData>().ConfigureAwait(false);
var target = new DeviceData() { Watts = 5, Name = "phone" };
var result = responseData.ApplyPatch(target);
Console.WriteLine($"Patched: Name={result.Name}, Watts={result.Watts}");
}
var httpClient = new HttpClient();
var response = await httpClient.GetAsync("https://localhost:5001/Sample/DeviceData", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
var responseData = await response.Content.ReadJsonPatchAsync<DeviceData>().ConfigureAwait(false);
var target = new DeviceData() { Watts = 5, Name = "phone" };
var result = responseData.ApplyPatch(target);
Console.WriteLine($"Patched: Name={result.Name}, Watts={result.Watts}");
}
}
Loading
Loading