2

I want to support partial updates with JSON Merge Patch. The domain model is based on the always valid concept and has no public setters. Therefore I can't just apply the changes to the class types. I need to translate the changes to the specific commands.

Since classes have nullable properties I need to be able to distinguish between properties set to null and not provided.

I'm aware of JSON Patch. I could use patch.JsonPatchDocument.Operations to go through the list of changes. JSON Patch is just verbose and more difficult for the client. JSON Patch requires to use Newtonsoft.Json (Microsoft states an option to change Startup.ConfigureServices to only use Newtonsoft.Json for JSON Patch (https://learn.microsoft.com/en-us/aspnet/core/web-api/jsonpatch?view=aspnetcore-6.0).

Newtonsoft supports IsSpecified-Properties that can be used as a solution for JSON Merge Patch in the DTO classes (How to make Json.NET set IsSpecified properties for properties with complex values?). This would solve the problem, but again requires Newtonsoft. System.Text.Json does not support this feature. There is an open issue for 2 years (https://github.com/dotnet/runtime/issues/40395), but nothing to expect.

There is a post that describes a solution with a custom JsonConverter for Web API (https://github.com/dotnet/runtime/issues/40395). Would this solution still be usable for NetCore?

I was wondering if there is an option to access the raw json or a json object inside the controller method after the DTO object was filled. Then I could manually check if a property was set. Web Api closes the stream, so I can't access the body anymore. It seems there are ways to change that behavior (https://gunnarpeipman.com/aspnet-core-request-body/#comments). It seems quite complicated and feels like a gun that is too big. I also don't understand what changes were made for NetCore 6.

I'm surpised that such a basic problem needs one to jump through so many loops. Is there an easy way to accomplish my goal with System.Text.Json and NetCore 6? Are there other options? Would using Newtonsoft have any other bad side effects?

M. Koch
  • 525
  • 4
  • 20
  • Optionals are your friend here. – jhmckimm Feb 07 '22 at 21:58
  • What do you mean with optionals? I don't understand. – M. Koch Feb 08 '22 at 10:38
  • There doesn't seem to be a standard implementation, but they're a generic type that allows you to tell the difference between a null value, and no value. – jhmckimm Feb 08 '22 at 18:33
  • Two common properties you'll see on an implementation are `HasValue` and `Value`. There are packages out there that implement this for you. – jhmckimm Feb 08 '22 at 18:34
  • [Here](https://github.com/dotnet/roslyn/blob/main/src/Compilers/Core/Portable/Optional.cs) is an implementation that Roslyn uses. – jhmckimm Feb 08 '22 at 18:39
  • Thanks for the hints. My first thought was a optional structure, but I didn't know how to make the serializer use a HasValue method. I found this post that describes a solution: https://stackoverflow.com/questions/63418549/custom-json-serializer-for-optional-property-with-system-text-json – M. Koch Feb 09 '22 at 21:04
  • The examples in the above link (see also https://github.com/dotnet/dotNext/tree/master/src/DotNext/Text/Json) produce nullable errors when enable and nullable is set. I don't understand enough of the code to make it work. Can someone provide an example for a ConverterFactory that will work with Microsoft.CodeAnalysis.Optional and NetCore Web Api controller? – M. Koch Feb 09 '22 at 23:28
  • I feel like that in the ASP .NET world people just send all the fields. This seems to be true for typescript stuff too. However most public REST APIs that I know support updating only provided fields even well known such as Salesforce, Shopify etc – GorillaApe Apr 13 '23 at 12:15

1 Answers1

2

With the helpful comments of jhmckimm I found Custom JSON serializer for optional property with System.Text.Json. DBC shows a fantastic solution using Text.Json and Optional<T>. This should be in the Microsoft docs!

In Startup I added:

services.AddControllers()
  .AddJsonOptions(o => o.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault)
  .AddJsonOptions(o => o.JsonSerializerOptions.Converters.Add(new OptionalConverter()));

Since we use <Nullable>enable</Nullable> and <WarningsAsErrors>nullable</WarningsAsErrors> I adapted the code for nullables.

public readonly struct Optional<T>
    {
        public Optional(T? value)
        {
            this.HasValue = true;
            this.Value = value;
        }

        public bool HasValue { get; }
        public T? Value { get; }
        public static implicit operator Optional<T>(T value) => new Optional<T>(value);
        public override string ToString() => this.HasValue ? (this.Value?.ToString() ?? "null") : "unspecified";
    }

public class OptionalConverter : JsonConverterFactory
    {
        public override bool CanConvert(Type typeToConvert)
        {
            if (!typeToConvert.IsGenericType) { return false; }
            if (typeToConvert.GetGenericTypeDefinition() != typeof(Optional<>)) { return false; }
            return true;
        }

        public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
        {
            Type valueType = typeToConvert.GetGenericArguments()[0];

            return (JsonConverter)Activator.CreateInstance(
                type: typeof(OptionalConverterInner<>).MakeGenericType(new Type[] { valueType }),
                bindingAttr: BindingFlags.Instance | BindingFlags.Public,
                binder: null,
                args: null,
                culture: null
            )!;
        }

        private class OptionalConverterInner<T> : JsonConverter<Optional<T>>
        {
            public override Optional<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
            {
                T? value = JsonSerializer.Deserialize<T>(ref reader, options);
                return new Optional<T>(value);
            }

            public override void Write(Utf8JsonWriter writer, Optional<T> value, JsonSerializerOptions options) =>
                JsonSerializer.Serialize(writer, value.Value, options);
        }
    }

My test DTO looks like this:

public class PatchGroupDTO
    {
        public Optional<Guid?> SalesGroupId { get; init; }

        public Optional<Guid?> AccountId { get; init; }

        public Optional<string?> Name { get; init; }

        public Optional<DateTime?> Start { get; init; }

        public Optional<DateTime?> End { get; init; }
    }

I can now access the fields and check with .HasValue if the value was set. It also works for writing and allows us to stripe fields based on permission.

M. Koch
  • 525
  • 4
  • 20
  • We are looking to do something similar. Some things to consider here are whether this works well with Automapper (mapping between DTOs and DAOs), and autogeneration of swagger definitions from DTOs. – redcalx Apr 13 '22 at 09:12
  • I haven't used AutoMapper, but I guess you should be able to configure AutoMapper, since Optional is just a class. I wrote my ToDTO() methods manually. I still have issues with autogenerating Swagger definitions. My post https://stackoverflow.com/questions/71265953/net-core-web-api-how-to-map-optionalt-for-swaggerdoc hasn't gotten any answers, yet. I also wrote an issue on github for swashbuckle: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/2359. No solution there either. If you find a solution, please post it here. – M. Koch Apr 17 '22 at 09:20