DataContext Translate<> for anonymous types

痴心易碎 提交于 2019-12-11 07:36:12

问题


This is just selection of some columns from Cars:

var qs = myDataContext.Cars
    .Select(c => new { c.id, c.color })
    .ToList();

What I need is function, that would do the same, but via SqlCommand, so I can alter the process. Its (simplified) code is here

public static IEnumerable<P> ProjectionFunction<T, P>(
    this System.Data.Linq.Table<T> input,
    Func<T, P> projection
    ) where T : class
{
    System.Data.Linq.DataContext dc = input.Context;

    string paramList = string.Join(
        ",",
        typeof(P).GetProperties().Select(s => s.Name)
        );

    System.Data.SqlClient.SqlCommand cmd = new System.Data.SqlClient.SqlCommand(
       "SELECT " + paramList + " FROM " + typeof(T).Name,
       (System.Data.SqlClient.SqlConnection)dc.Connection
       );

    cmd.CommandType = CommandType.Text;

    if (cmd.Connection.State == ConnectionState.Closed)
    {
        cmd.Connection.Open();
    }

    return dc.Translate<P>(cmd.ExecuteReader()); // <-- the problem is here
}

The function works well for ordinary classes like this

private class X
{
    public int? id { get; set; }
    public string color { get; set; }
}

var qx = myDataContext.Cars
    .ProjectionFunction(c => new X () { id = c.id, color = c.color })
    .ToList();

But It fails on anonymous types

var qa = myDataContext.Cars
    .ProjectionFunction(c => new { c.id, c.color })
    .ToList();

I get runtime error

The type <>f__AnonymousType20`2[System.Nullable`1[System.Int32],System.String] must declare a default (parameterless) constructor in order to be constructed during mapping.

for Translate<> function. The ExecuteQuery<> I tried too does the same. It is hard to believe that DataContext does not know how to build anonymous type, it is what he does all the time. What am I missing? How can I make him do that for me?

The problem with separate one use class is, that its properties have to be explicitly synchronized with types and names of properties of the original class, which makes this approach somewhat impractical.


回答1:


I would still like to know if there is a way to make DataContext translate to anonymous types, but it well may be, that it does not expose this behaviour. In that case, another materializer has to be used. In the end, it was not that difficult to write one, so I shared it in case someone got interested. The only way to initialize anonymous type is via new operator. Fortunately expression can be built to do that in runtime.

private static Func<object[], P> getMaterializer<P>(
    System.Reflection.PropertyInfo[] props, IEnumerable<string> propertyNames)
{
    Type[] propertyTypes = props.Select(p => p.PropertyType).ToArray();
    ParameterExpression arrExpr = Expression.Parameter(typeof(object[]));
    var constructor = typeof(P).GetConstructor(propertyTypes);

    if (constructor == null || !constructor
        .GetParameters()
        .Select(p => p.Name)
        .SequenceEqual(propertyNames))
    {
        return null;
    }

    Expression[] paramExprList = propertyTypes.Select((type, i) =>
    {
        Expression ei = Expression.ArrayIndex(arrExpr, Expression.Constant(i));

        if (type.IsGenericType || type == typeof(string))
        {
            return (Expression)Expression.Condition(
                Expression.Equal(ei, Expression.Constant(DBNull.Value)),
                Expression.Convert(Expression.Constant(null), type),
                Expression.Convert(ei, type)
                );
        }
        else
        {
            return Expression.Convert(ei, type);
        }
    }).ToArray();

    return Expression.Lambda<Func<object[], P>>(
        Expression.New(constructor, paramExprList),
        arrExpr
        ).Compile();
}


private static System.Collections.Concurrent.ConcurrentDictionary<Type, Tuple<string, string, object>> cachedProjections =
    new System.Collections.Concurrent.ConcurrentDictionary<Type, Tuple<string, string, object>>();

private static Tuple<string, string, object> getProjection<T, P>()
{
    Type typeP = typeof(P);
    Tuple<string, string, object> projection;

    if (!cachedProjections.TryGetValue(typeP, out projection))
    {
        Type typeT = typeof(T);
        System.Reflection.PropertyInfo[] props = typeP.GetProperties();
        List<string> propertyNames = props.Select(p => p.Name).ToList();

        projection = new Tuple<string, string, object>(
            string.Join(",", propertyNames),
            typeT.Name,
            typeT == typeP ? null : getMaterializer<P>(props, propertyNames)
            );

        cachedProjections.TryAdd(typeP, projection);
    }

    return projection;
}

private static IEnumerable<P> Materialize<P>(SqlCommand cmd,
    Func<object[], P> materializer)
{
    using (var reader = cmd.ExecuteReader(CommandBehavior.CloseConnection))
    {
        object[] obj = new object[reader.VisibleFieldCount];

        while (reader.Read())
        {
            reader.GetValues(obj);
            yield return materializer(obj);
        }
    }
}

public static IEnumerable<P> ProjectionFunction<T, P>(
    this System.Data.Linq.Table<T> input,
    Func<T, P> projectionFunction
 ) where T : class
{
    var projection = getProjection<T, P>();

    using (SqlCommand cmd = new SqlCommand(
       "SELECT " + projection.Item1
       + " FROM " + projection.Item2,
       new SqlConnection(input.Context.Connection.ConnectionString)
       ))
    {
        cmd.CommandType = CommandType.Text;
        cmd.Connection.Open();

        var materializer = (Func<object[], P>)projection.Item3;
        if (materializer == null)
        {
            return input.Context.Translate<P>(cmd.ExecuteReader(CommandBehavior.CloseConnection));
        }
        else
        {
            return Materialize(cmd, materializer);
        }
    }
}


来源:https://stackoverflow.com/questions/42406032/datacontext-translate-for-anonymous-types

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!