-->

Dynamic LINQ: Specifying class name in new clause

2020-02-29 04:32发布

问题:

With Dynamic LINQ, what changes need to be done to have fields of the given class?

For example, how can the following C# query be reproduced in DLinq:

var carsPartial = cars.Select(c => new {c.year, c.name, make = new maker() {name = c.make.name} }).ToList();

I have applied the changes mentioned in this answer https://stackoverflow.com/a/1468357/288747 to allow the return type to be the calling type rather than an anonymous type.

With the class definition is as follows (if it helps):

class car
{
    public int vid;
    public int odo;
    public int year;

    public string name;

    public maker make;
}

class maker
{
    public string name;
    public int firstYear;
}

The following doesn't work (but I think is close, but still doesn't work as I don't have the changes necessary to the dynamic linq library, which is what I need):

var carsPartial = cars.Select("new(name, year, new maker() {name = make.name})").ToList();

But it fails at the new maker() { (as expected).

I'm sure I need to change the DynamicLibrary.cs to get this working and could do with some direction on how to alter it to achieve this.

回答1:

UPDATE: I have turned my answer into a little bit more extensive blog post.

I have not really ever used Dynamic Linq library, but I have taken a look at the DynamicLibrary.cs code and the change to support generating type classes provided in another stackoverflow question you provided link to in your question. Analyzing them all, it seems that the nested new-s should work out of the box in your configuration.

However, it seems your query is not the correct Dynamic Linq's language query. Note, that the query string for DLinq is not equivalent to C# and has its own grammar.

The query should read out, I believe, the following:

var carsPartial = cars.Select("new(name, year, new maker(make.name as name) as make)").ToList();

EDIT:

Rereading this stackoverflow question more carefully, I realizes, that it actually does not extend the Dynamic Linq's language with the possibility for creating new strong-typed classes. They just put the result to the class specified as a generic parameter of Select() instead of specifying it in the query string.

To obtain what you need you will need to revert their changes (get generic DLinq) and apply my changes, I have just verified to work:

Locate the ParseNew method of ExpressionParser class and change it to the following:

    Expression ParseNew() {
        NextToken();

        bool anonymous = true;
        Type class_type = null;

        if (token.id == TokenId.Identifier)
        {
            anonymous = false;
            StringBuilder full_type_name = new StringBuilder(GetIdentifier());

            NextToken();

            while (token.id == TokenId.Dot)
            {
                NextToken();
                ValidateToken(TokenId.Identifier, Res.IdentifierExpected);
                full_type_name.Append(".");
                full_type_name.Append(GetIdentifier());
                NextToken();
            }

            class_type = Type.GetType(full_type_name.ToString(), false);    
            if (class_type == null)
                throw ParseError(Res.TypeNotFound, full_type_name.ToString());
        }

        ValidateToken(TokenId.OpenParen, Res.OpenParenExpected);
        NextToken();
        List<DynamicProperty> properties = new List<DynamicProperty>();
        List<Expression> expressions = new List<Expression>();
        while (true) {
            int exprPos = token.pos;
            Expression expr = ParseExpression();
            string propName;
            if (TokenIdentifierIs("as")) {
                NextToken();
                propName = GetIdentifier();
                NextToken();
            }
            else {
                MemberExpression me = expr as MemberExpression;
                if (me == null) throw ParseError(exprPos, Res.MissingAsClause);
                propName = me.Member.Name;
            }
            expressions.Add(expr);
            properties.Add(new DynamicProperty(propName, expr.Type));
            if (token.id != TokenId.Comma) break;
            NextToken();
        }
        ValidateToken(TokenId.CloseParen, Res.CloseParenOrCommaExpected);
        NextToken();
        Type type = anonymous ? DynamicExpression.CreateClass(properties) : class_type; 
        MemberBinding[] bindings = new MemberBinding[properties.Count];
        for (int i = 0; i < bindings.Length; i++)
            bindings[i] = Expression.Bind(type.GetProperty(properties[i].Name), expressions[i]);
        return Expression.MemberInit(Expression.New(type), bindings);
    }

Then, find the class Res and add the following error message:

public const string TypeNotFound = "Type {0} not found";

Et voilà, you will be able to construct queries like:

var carsPartial = cars.Select("new(name, year, (new your_namespace.maker(make.name as name)) as make)").ToList();

Make sure, you include the full type name including the whole namespace+class path.

To explain my change, it just checks if there is some identifier between new and opening parenthesis (see the added "if" at the begging). If so we parse full dot-separated class name and try to get its Type through Type.GetType instead of constructing own class in case of anonymous news.



回答2:

If I've understood you correctly you want to make a plain anonymous class that contains fields from both class car and class maker. If it's the case you can just provide new names in that class, something like the following:

var carsPartial = cars.Select(c => new { year = c.year, name = c.name, make_name = c.make.name });

Or even provide names only to conflicting fields:

var carsPartial = cars.Select(c => new { c.year, c.name, make_name = c.make.name });