C# Serialize then Deserialize object produces different results than passing object directly

Viewed 33

I have a rules engine that uses Expression trees to run rules logic on a certain input and tells the user whether the object passes or fails the rules.

I have run into an issue where deserialzing an object to a custom type is producing different results than the same object passed directly without serialization.

var results = Rules.Run(myObject)
// results pass
MyType input = JsonConvert.DeserializeObject<MyType>(JsonConvert.SerializeObject(myObject));
var results = Rules.Run(input)
// results fail

The reason I noticed this is because I am building a WebApi. When I run a unit test on my function by passing myObject to the controller function, everything works fine, but when it comes in through a web request (hence running through the deserializer to convert from JSON) I get a failed result in my rules.

Stranger yet, the failure only occurs on one rule out out 5, the other rules all pass and all of them require reading the same values from the passed object.

I added JsonConvert.DeserializeObject(JsonConvert.SerializeObject(myObject)) to my unit test and it also fails after going through the deserialization.

Inspecting both the objects in the debugger shows them to have the exact same values.

What is going on here?

1 Answers

I discovered the issue. It seems some reflection type information was being lost during serialization that was being used by the rules engine. Specifying the data type explicitly in the rules engine instead of relying on reflection fixed the issue

edit: Here is a fake example that demonstrates what was occurring:

var engine = new RulesEngine();
AddAllRules();
var results = engine.Run(new FakeObject { SampleString = "valid" });

class FakeObject {
  public string SampleString {get; set;}
}
// this rule definition fails
private void AddAllRules()
{
  var rules = RulesBuilder<FakeObject>.Load(
  new List<IRulesCollectionDatabaseData<FakeObject>> {
    Rules = new List<IRuleDatabaseData>
    {
      Type = "ANY_EQUAL",
      Members =  new List<IRuleBuilderDatabaseData> 
      {
        new RuleBuilderDatabaseData {
          MemberPath = "SampleString",
          Values = new [] { "valid", "also_valid" } // either of these can match
        }
      }
    }
   }
  ).Build();

  engine.AddRules(rules);
}
// this rule definition works
private void AddAllRules()
{
  var rules = RulesBuilder<FakeObject>.Load(
  new List<IRulesCollectionDatabaseData<FakeObject>> {
    Rules = new List<IRuleDatabaseData>
    {
      Type = "ANY_EQUAL",
      Members =  new List<IRuleBuilderDatabaseData> 
      {
        new RuleBuilderDatabaseData {
          DataType = "System.String", // adding the type explictly works
          MemberPath = "SampleString",
          Values = new [] { "valid", "also_valid" }
        }
      }
    }
   }
  ).Build();

  engine.AddRules(rules);
}

This is because the rules engine was casting everything to object when no type was provided

// From the RulesBuilder

// this was defaulting to object because type wasn't specified
public Expression Equal(IBuilderRuleMeta meta) => Equal(meta, typeof(object), this._inputExpression); 

public Expression Equal(IBuilderRuleMeta meta, Type valueType, ParameterExpression parameterExpression)
        {

            return meta.Values.Skip(1).Aggregate(
                Expression.Equal(Expression.Convert(CreateNestedMemberExpression(parameterExpression, meta.MemberAccessCollection), valueType), Expression.Constant(meta.Values.First())) as Expression,
                (expression, value) => Expression.OR(
                    expression,
                    Expression.Equal(
                        Expression.Convert(CreateNestedMemberExpression(parameterExpression, meta.MemberAccessCollection), valueType),
                        Expression.Constant(value))));
        }

public static MemberExpression CreateNestedMemberExpression(ParameterExpression inputExpression, ICollection<string> memberAccessCollection)
        {
            if (inputExpression == null)
            {
                throw new ArgumentNullException(nameof(inputExpression));
            }

            return memberAccessCollection
                .Skip(1)
                .Aggregate(
                    Expression.PropertyOrField(inputExpression, memberAccessCollection.First()),
                    Expression.PropertyOrField)
                ;
        }


Related