Welcome to ShenZhenJia Knowledge Sharing Community for programmer and developer-Open, Learning and Share
menu search
person
Welcome To Ask or Share your Answers For Others

Categories

I am attempting to create a IContractResolver to simplify my security handling on a WebApi Project.

What I am attempting :

I want to serialize certain objects/properties based on a set of dynamic conditions ( for examples the Role of the user that called the endpoint).

So I implemented a custom attribute that is checked in the CreateProperty override of the Interface, and set the ShouldSerialize function to my own logic.

My question now is, is it possible to conditionally serialize full objects that are in a certain list ? Instead of filtering the lists in a preprocessing step (which is error prone, if I change my objects) I would like it to be handled recursively by the current ContractResolver.

In a way I was trying to get something like this:

override void CreateObject(JSONObject ob){
if ( ob.DeclaringType == MyType)
{
   ob.ShouldSerialize = instance => {[...] }; //Custom Logic
}
}

Am I missing a override, is this not possible at all? Is there a better way to actually do this, without me having to "pre-parse" all my values?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
thumb_up_alt 0 like thumb_down_alt 0 dislike
191 views
Welcome To Ask or Share your Answers For Others

1 Answer

This is not implemented out of the box. If you inspect the source for JsonSerializerInternalWriter.SerializeList() you will see there is no logic to skip collection entries based on some filter.

However, Json.NET does have robust exception handling. If an exception is thrown when beginning to serialize an object then caught and swallowed in an [OnError] callback:

  • If writing an array entry, the array entry is skipped (your desired behavior).
  • If writing the root object, the exception is not caught (possibly a bug?)
  • Otherwise null is written.

Thus one possibility to achieve your desired functionality would be to throw an exception from an artificial callback added to JsonContract.OnSerializingCallbacks by your custom contract resolver, then catch and swallow the exception with a handler added to JsonContract.OnErrorCallbacks. When combined with filtering on property values as you are already doing, this approach has the advantage of guaranteeing a secret object cannot be serialized even when it is the root object or when contained in a dictionary, a dynamic object, or a multidimensional array. This approach will not interfere with PreserveReferencesHandling.Arrays.

One contract resolver that does this is as follows:

sealed class JsonSkipObjectException : JsonException
{
}

public class ShouldSerializeContractResolver : DefaultContractResolver
{
    readonly Predicate<object> shouldSerialize;
    readonly SerializationCallback serializationCallback;
    readonly SerializationErrorCallback onErrorCallback;

    public ShouldSerializeContractResolver(Predicate<object> shouldSerialize)
        : base()
    {
        this.shouldSerialize = shouldSerialize;
        this.serializationCallback = (o, context) =>
            {
                if (shouldSerialize != null && !this.shouldSerialize(o))
                    throw new JsonSkipObjectException();
            };
        this.onErrorCallback = (o, context, errorContext) =>
            {
                if (errorContext.Error is JsonSkipObjectException)
                {
                    errorContext.Handled = true;
                }
            };
    }

    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        var property = base.CreateProperty(member, memberSerialization);

        if (shouldSerialize != null)
        {
            if (property.Readable)
            {
                var oldShouldSerialize = property.ShouldSerialize;
                property.ShouldSerialize = (o) =>
                    {
                        if (oldShouldSerialize != null && !oldShouldSerialize(o))
                            return false;
                        var value = property.ValueProvider.GetValue(o);
                        if (!this.shouldSerialize(value))
                            return false;
                        return true;
                    };
            }
        }
        return property;
    }

    protected override JsonContract CreateContract(Type objectType)
    {
        var contract = base.CreateContract(objectType);
        contract.OnSerializingCallbacks.Add(serializationCallback);
        contract.OnErrorCallbacks.Add(onErrorCallback);
        return contract;
    }
}

Then one possible use would be:

public interface IConditionalSerialization
{
    bool ShouldSerialize();
}

public class ConditionalSerializationObject : IConditionalSerialization
{
    public bool IsSecret { get; set; }

    public string SecretProperty { get { return "should not see me"; } }

    // Ensure "normal" conditional property serialization is not broken
    public bool ShouldSerializeSecretProperty()
    {
        return false;
    }

    #region IConditionalSerialization Members

    bool IConditionalSerialization.ShouldSerialize()
    {
        return !IsSecret;
    }

    #endregion
}

public class TestClass
{
    public static void Test()
    {
        Predicate<object> filter = (o) => 
            {
                var conditional = o as IConditionalSerialization;
                return conditional == null || conditional.ShouldSerialize();
            };
        var settings = new JsonSerializerSettings
        {
            ContractResolver = new ShouldSerializeContractResolver(filter),
        };

        var ok = new ConditionalSerializationObject { IsSecret = false };
        var notOk = new ConditionalSerializationObject { IsSecret = true };

        Test(ok, settings);
        Test(new { Public = ok, Private = notOk }, settings);
        Test(new [] { ok, notOk, ok, notOk }, settings);
        Test(new[,] {{ ok, notOk, ok, notOk }}, settings);
        Test(new { Array = new[,] { { ok, notOk, ok, notOk } } }, settings);
        try
        {
            Test(notOk, settings);
        }
        catch (Exception ex)
        {
            Console.WriteLine("Exception thrown and not caught serializing root object " + notOk.GetType());
            Console.WriteLine(ex);
        }
    }

    static void Test<T>(T value, JsonSerializerSettings settings)
    {
        Console.WriteLine("Unfiltered object: ");
        Console.WriteLine(JToken.FromObject(value));

        var serializer = JsonSerializer.CreateDefault(settings);
        var token = JToken.FromObject(value, serializer);
        Console.WriteLine("Filtered object: ");
        Console.WriteLine(token);
        if (!token.SelectTokens("..IsSecret").All(t => JToken.DeepEquals(t, (JValue)false)))
        {
            throw new InvalidOperationException("token.SelectTokens("..IsSecret").All(t => JToken.DeepEquals(t, (JValue)true))");
        }
        if (token.SelectTokens("..SecretProperty").Any())
        {
            throw new InvalidOperationException("token.SelectTokens("..SecretProperty").Any()");
        }
        Console.WriteLine("Secret objects and properties were successfully filtered.");
        Console.WriteLine("");
    }
}

Prototype fiddle.

Note that throwing and catching a large number of exceptions can have performance implications. See How expensive are exceptions in C#?. You will need to profile your web application to determine whether this is a problem. You also will need to decide whether your web service should return an exception when attempting to serialize a "secret" root object, or do something different.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
thumb_up_alt 0 like thumb_down_alt 0 dislike
Welcome to ShenZhenJia Knowledge Sharing Community for programmer and developer-Open, Learning and Share
...