Reputation: 3447
I am trying to create a custom serializer. It needs to be performant.
The idea is to construct and cache some Func<…>
for each type
In this simplified example, I successfully construct the Func
for STRING types, but I'm stuck on how to construct it for ARRAY types.
It could be helpful to imagine that I can now serialize the Meow
class, but that I would fail to serialize the Ruff
class.
class Program
{
class Meow
{
public string Rawr { get; set; } = "X";
}
class Ruff
{
public Meow[] Grr { get; set; } = new[] { new Meow(), new Meow() };
}
static class Serializer<T>
{
static int DoSomething(string value, Stream stream) => value.Length;
static int DoSomethingElse(T[] values, Stream stream) => values.Length;
public static Func<T, Stream, int> GetSerializer()
{
var firstProperty = typeof(T).GetProperties()[0].GetGetMethod();
var typeParam = Expression.Parameter(typeof(T));
var compiledGetter = Expression
.Lambda(
Expression.Call(typeParam, firstProperty),
typeParam
)
.Compile();
var returnType = firstProperty.ReturnType;
if (returnType == typeof(string))
{
var getString = (Func<T, string>)compiledGetter;
return (T item, Stream stream) => DoSomething(getString(item), stream);
}
if (returnType.IsArray)
{
// var getArray = (Func<T, returnType>)compiledGetter;
var elementType = returnType.GetElementType();
// return (T item, Stream stream) =>
// Serializer<elementType>.DoSomethingElse(getArray(item), stream))
}
return (T item, Stream stream) => 0;
}
}
static void Main(string[] args)
{
MemoryStream s = new MemoryStream();
Console.WriteLine(Serializer<Meow>.GetSerializer()(new Meow(), s));
Console.WriteLine(Serializer<Ruff>.GetSerializer()(new Ruff(), s));
Console.ReadKey();
// Should print "1", "2"
// Currently prints "1", "0"
}
}
Serializing Meow
is easy. The function will take the T
and Stream
, extract string
from T
, and pass them into DoSomething(string, Stream)
to return a bool.
But while serializing Ruff
, it encounters a property with return type Meow[]
. To serialize it, it needs to take T
and Stream
, extract an array of an unknown element type from T
, and pass them into Serializer<Meow>.DoSomethingElse(Meow[], Stream)
The commented-out lines show the gist of what I think needs to happen. But how can I create a compiled Expression
for it all and finally return a Func<T, Stream, bool>
?
EDIT: Test code is now included. When implemented, the Ruff
serializer should spit out 2
, the length of the array.
EDIT #2: SOLUTION! thanks to Jeff Mercado
Below is the working code (Just the GetSerializer method)
public static Func<T, Stream, int> GetSerializer()
{
var itemTypeExpression = Expression.Parameter(typeof(T));
var streamTypeExpression = Expression.Parameter(typeof(Stream));
var firstProperty = typeof(T).GetProperties().First();
var propType = firstProperty.PropertyType;
var getterExpression = Expression.Lambda(
Expression.Property(itemTypeExpression, firstProperty),
itemTypeExpression
);
Expression body = null;
if (propType == typeof(string))
{
body = Expression.Call(
typeof(Serializer<T>),
nameof(DoSomething),
Type.EmptyTypes,
Expression.Invoke(
getterExpression,
itemTypeExpression
),
streamTypeExpression
);
}
else if (propType.IsArray)
{
var elementType = propType.GetElementType();
var elementTypeExpression = Expression.Parameter(elementType);
var serializerType = typeof(Serializer<>).MakeGenericType(elementType);
var serializerTypeExpression = Expression.Parameter(serializerType);
body = Expression.Call(
serializerType,
nameof(DoSomethingElse),
Type.EmptyTypes,
Expression.Invoke(
getterExpression,
itemTypeExpression
),
streamTypeExpression
);
}
if (body != null)
return Expression.Lambda<Func<T, Stream, int>>(body, itemTypeExpression, streamTypeExpression).Compile();
return (T item, Stream stream) => 0;
}
Upvotes: 1
Views: 631
Reputation: 134811
You have a couple of problems here, with the way you're constructing your expression, your Ruff
type will not satisfy that expression.
You're effectively building up this expression:
(Rawr arg0, Stream arg1) =>
Serializer<Ruff>.DoSomethingElse(arg0.Grr, arg1);
Note that the type of arg0.Grr
is Meow[]
but the expected type is Ruff[]
. The DoSomethingElse()
method must be generic instead to be compatible.
static int DoSomethingElse<TValue>(TValue[] values, Stream stream) => values.Length;
On the other hand,it doesn't seem to really matter what the actual underlying type is, you just want the length of the array. So instead, you could just make it an Array
and it will still work.
static int DoSomethingElse(Array values, Stream stream) => values.Length;
Overall, I wouldn't mix up different types of expressions in that way (expression objects and lambdas), either build it all using lambdas or all using expressions. I'd write that method this way:
public static Func<T, Stream, int> GetSerializer()
{
var firstProperty = typeof(T).GetProperties().First();
var item = Expression.Parameter(typeof(T));
var stream = Expression.Parameter(typeof(Stream));
var propType = firstProperty.PropertyType;
if (typeof(string).IsAssignableFrom(propType))
{
var body = Expression.Call(
typeof(Serializer<T>),
"DoSomething",
Type.EmptyTypes,
Expression.Invoke(
MakeGetter(firstProperty),
item
),
stream
);
return Expression.Lambda<Func<T, Stream, int>>(body, item, stream).Compile();
}
if (typeof(Array).IsAssignableFrom(propType))
{
var body = Expression.Call(
typeof(Serializer<T>),
"DoSomethingElse",
Type.EmptyTypes,
Expression.Invoke(
MakeGetter(firstProperty),
item
),
stream
);
return Expression.Lambda<Func<T, Stream, int>>(body, item, stream).Compile();
}
return (T arg0, Stream arg1) => 0;
Expression MakeGetter(PropertyInfo prop)
{
var arg0 = Expression.Parameter(typeof(T));
return Expression.Lambda(
Expression.Property(arg0, prop),
arg0
);
}
}
Based on your comments, it makes more sense to me to make the methods generic, rather than the serializer. You'll just need to make the appropriate expressions to the generic calls.
static class Serializer
{
static int DoSomething(string value, Stream stream) => value.Length;
static int DoSomethingElse<T>(T[] values, Stream stream) => values.Length;
public static Func<T, Stream, int> GetSerializer<T>()
{
var firstProperty = typeof(T).GetProperties().First();
var item = Expression.Parameter(typeof(T));
var stream = Expression.Parameter(typeof(Stream));
var propType = firstProperty.PropertyType;
if (typeof(string).IsAssignableFrom(propType))
{
var body = Expression.Call(
typeof(Serializer),
"DoSomething",
Type.EmptyTypes,
Expression.Invoke(
MakeGetter(firstProperty),
item
),
stream
);
return Expression.Lambda<Func<T, Stream, int>>(body, item, stream).Compile();
}
if (typeof(Array).IsAssignableFrom(propType))
{
var body = Expression.Call(
typeof(Serializer),
"DoSomethingElse",
new[] { propType.GetElementType() },
Expression.Invoke(
MakeGetter(firstProperty),
item
),
stream
);
return Expression.Lambda<Func<T, Stream, int>>(body, item, stream).Compile();
}
return (T arg0, Stream arg1) => 0;
Expression MakeGetter(PropertyInfo prop)
{
var arg0 = Expression.Parameter(typeof(T));
return Expression.Lambda(
Expression.Property(arg0, prop),
arg0
);
}
}
}
Upvotes: 1