0

I have a POCO like this

public class Foo {
   public string PartitionKey => $"foo-{Bar}";
   public string Bar { get; set; }
}

I'm storing that POCO as serialized JSON in a database (Azure Cosmos DB to be specific) and making it available to clients via a ASP.NET WebApi.

When serializing that document for Cosmos DB, I need the PartitionKey, so I cannot exclude it totally by using [JsonIgnore]. But I do not want it included in my API response. So what is the easiest way to achieve this? Ideally I don't want to write my own JsonSerializer, but maybe using some custom attribute?

Expected outcomes:

For the database:

{
  "partitionKey": "foo-alice",
  "bar": "alice"
}

For the api response:

{
  "bar": "alice"
}

Using .NET7 and System.Text.Json

silent
  • 14,494
  • 4
  • 46
  • 86
  • I have an approach in mind, but with a custom JsonConverter and that you are not looking for, right? – Vivek Nuna Dec 27 '22 at 11:54
  • 1
    Personally I would just create a DTO and map the entity to it. – Guru Stron Dec 27 '22 at 11:59
  • @GuruStron yeah I thought about it, too. But except for that one property they would look exactly the same (and there are plenty of more properties which I left out here) – silent Dec 27 '22 at 12:43
  • @viveknuna if possible I'd like to avoid that. But feel free to post it if nobody has a better idea – silent Dec 27 '22 at 12:44
  • Your question is not clear for me. Pls post json that you have now for API response, and json you want to send as API response. I don't understand what the difference it should be to cause such a problem – Serge Dec 27 '22 at 14:30
  • @Serge I think it was clear before, but I added examples of the outcomes now – silent Dec 28 '22 at 08:20

1 Answers1

3

You have a couple options to selectively exclude the PartitionKey property during serialization.

Firstly, since the property is read-only, you could set JsonSerializerOptions.IgnoreReadOnlyProperties = true:

var options = new JsonSerializerOptions
{
    IgnoreReadOnlyProperties = true,
    // Add other options as required
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase, 
    WriteIndented = true,           
};

var json = JsonSerializer.Serialize(foo, options);

This will exclude all read-only properties including PartitionKey.

Secondly, if you need to exclude only PartitionKey but include other read-only properties, or exclude modifiable properties, or otherwise need more control over what gets serialized, you can add a DefaultJsonTypeInfoResolver modifier to exclude the unwanted members.

To exclude the property by name, add the following extension method:

public static partial class JsonSerializerExtensions
{
    public static DefaultJsonTypeInfoResolver Exclude(this DefaultJsonTypeInfoResolver resolver, Type type, params string [] membersToExclude)
    {
        if (resolver == null || membersToExclude == null)
            throw new ArgumentNullException();
        var membersToExcludeSet = membersToExclude.ToHashSet();
        resolver.Modifiers.Add(typeInfo => 
                               {
                                   if (typeInfo.Kind == JsonTypeInfoKind.Object && type.IsAssignableFrom(typeInfo.Type)) // Or type == typeInfo.Type if you don't want to exclude from subtypes
                                       foreach (var property in typeInfo.Properties)
                                           if (property.GetMemberName() is {} name && membersToExcludeSet.Contains(name))
                                               property.ShouldSerialize = static (obj, value) => false;
                               });
        return resolver;
    }

    public static string? GetMemberName(this JsonPropertyInfo property) => (property.AttributeProvider as MemberInfo)?.Name;
}

And now you will be able to do:

var options = new JsonSerializerOptions
{
    TypeInfoResolver = new DefaultJsonTypeInfoResolver()
        .Exclude(typeof(Foo), nameof(Foo.PartitionKey)),
    // Add other options as required
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase, 
    WriteIndented = true,           
};

var json = JsonSerializer.Serialize(foo, options);

If you want to exclude by custom attribute rather than by name, introduce the following extension method:

public static partial class JsonSerializerExtensions
{
    public static DefaultJsonTypeInfoResolver ExcludeByAttribute<TAttribute>(this DefaultJsonTypeInfoResolver resolver) where TAttribute : System.Attribute
    {
        if (resolver == null)
            throw new ArgumentNullException();
        var attr = typeof(TAttribute);
        resolver.Modifiers.Add(typeInfo => 
                               {
                                   if (typeInfo.Kind == JsonTypeInfoKind.Object)
                                       foreach (var property in typeInfo.Properties)
                                           if (property.AttributeProvider?.IsDefined(attr, true) == true)
                                               property.ShouldSerialize = static (obj, value) => false;
                               });
        return resolver;
    }
}

Then modify Foo as follows:

[System.AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class JsonExludeFromResponseAttribute : System.Attribute { }

public class Foo {
    [JsonExludeFromResponseAttribute]
    public string PartitionKey => $"foo-{Bar}";
    public string Bar { get; set; }
}

And exclude PartitionKey as follows:

var options = new JsonSerializerOptions
{
    TypeInfoResolver = new DefaultJsonTypeInfoResolver()
        .ExcludeByAttribute<JsonExludeFromResponseAttribute>(),
    // Add other options as required
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase, 
    WriteIndented = true,           
};

var json = JsonSerializer.Serialize(foo, options);

Notes:

  • Contract customization is new in .NET 7.

  • If you have many different types with a PartitionKey property which you always want to exclude, you could pass in typeof(object) as the base class from which to exclude the property.

Demo fiddle here.

dbc
  • 104,963
  • 20
  • 228
  • 340