微软.net表达式编译居然有bug?

微软.net表达式编译问题困扰本人很久了,
为此我整理了以下case给大家分享

1. 可行性调研

  • 用表达式把对象转化为另一个类型的对象
  • 当一个类含有多个同类型属性时,把相同类型转化提取为公共方法
  • LambdaExpression可以用来定义复用的公共方法
  • 一切看起来都很完美,但是居然翻车了!!!

2. 示例说明

2.1 Customer多个属性包含Address

对应CustomerDTO多个属性包含AddressDTO

public class Customer
{
    public string Name { get; set; }
    public Address Address { get; set; }
    public Address[] Addresses { get; set; }
    public List<Address> AddressList { get; set; }
}
public class Address
{
    public string City { get; set; }
}
public class CustomerDTO
{
    public string Name { get; set; }
    public AddressDTO Address { get; set; }
    public AddressDTO[] Addresses { get; set; }
    public List<AddressDTO> AddressList { get; set; }
}
public class AddressDTO
{
    public string City { get; set; }
}

2.2 定义公共方法把Address转化为AddressDTO

/// <summary>
/// 转化 Address -> AddressDTO
/// </summary>
/// <returns></returns>
public static Expression<Func<Address, AddressDTO>> CreateAddressDTO()
{
    var sourceType = typeof(Address);
    var destType = typeof(AddressDTO);
    // Address source;
    var source = Expression.Parameter(sourceType, "source");
    // AddressDTO dest;
    var dest = Expression.Parameter(destType, "dest");
    var body = Expression.Block(
        [dest],
        // dest = new AddressDTO();
        Expression.Assign(dest, Expression.New(destType)),
        // dest.City = source.City;
        Expression.Assign(Expression.Property(dest, "City"), Expression.Property(source, "City")),
        // return dest;
        dest
    );
    return Expression.Lambda<Func<Address, AddressDTO>>(body, source);
}

2.3 调用公共方法

/// <summary>
/// 转化 Customer -> CustomerDTO
/// </summary>
/// <returns></returns>
public static Expression<Func<Customer, CustomerDTO>> CreateCustomerDTO()
{        
    var customerType = typeof(Customer);
    var dtoType = typeof(CustomerDTO);
    // Customer customer;
    var customer = Expression.Parameter(customerType, "customer");
    // CustomerDTO dto;
    var dto = Expression.Parameter(dtoType, "dto");
    var addressDTOConvertFunc = CreateAddressDTO();
    var body = Expression.Block(
        [dto],
        // dto = new AddressDTO();
        Expression.Assign(dto, Expression.New(dtoType)),
        // dto.Name = customer.Name;
        Expression.Assign(Expression.Property(dto, "Name"), Expression.Property(customer, "Name")),
        // dto.Address = addressDTOConvertFunc.Invoke(customer.Address);
        ConvertAddress(addressDTOConvertFunc, customer, dto),
        // dto.Addresses
        ConvertAddresses(addressDTOConvertFunc, customer, dto),
        // dto.AddressList
        ConvertAddressList(addressDTOConvertFunc, customer, dto),
        // return dto
        dto
    );
    return Expression.Lambda<Func<Customer, CustomerDTO>>(body, customer);
}
/// <summary>
/// 转化 customer.Address -> dto.Address
/// </summary>
/// <param name="addressDTOConvertFunc">共用方法</param>
/// <param name="customer"></param>
/// <param name="dto"></param>
/// <returns></returns>
public static Expression ConvertAddress(Expression<Func<Address, AddressDTO>> addressDTOConvertFunc, ParameterExpression customer, ParameterExpression dto)
{
    // dto.Address = addressDTOConvertFunc.Invoke(customer.Address);
    return Expression.Assign(Expression.Property(dto, "Address"), Expression.Invoke(addressDTOConvertFunc, Expression.Property(customer, "Address")));        
}
/// <summary>
/// 转化 customer.Address -> dto.Address
/// </summary>
/// <param name="addressDTOConvertFunc">共用方法</param>
/// <param name="customer"></param>
/// <param name="dto"></param>
/// <returns></returns>
public static BlockExpression ConvertAddresses(Expression<Func<Address, AddressDTO>> addressDTOConvertFunc, ParameterExpression customer, ParameterExpression dto)
{
    // Address[] addresses;
    var addresses = Expression.Parameter(typeof(Address[]), "addresses");
    // int length;
    var length = Expression.Variable(typeof(int), "length");
    // AddressDTO[] dtoList;
    var dtoList = Expression.Parameter(typeof(AddressDTO[]), "dtoList");
    //// Address item;
    //var item = Expression.Parameter(typeof(Address), "item");
    var forLabel = Expression.Label("forLabel");
    // int i;
    var i = Expression.Variable(typeof(int), "i");
    return Expression.Block(
        [addresses, dtoList, length, i],
        // addresses = customer.Addresses;
        Expression.Assign(addresses, Expression.Property(customer, "Addresses")),
        // length = addresses.Length;
        Expression.Assign(length, Expression.ArrayLength(addresses)),
        // dtoList = new AddressDTO[length];
        Expression.Assign(dtoList, Expression.NewArrayBounds(typeof(AddressDTO), length)),
        // dto.Addresses = dtoList;
        Expression.Assign(Expression.Property(dto, "Addresses"), dtoList),

        Expression.Loop(
            Expression.IfThenElse(
                // i < length
                Expression.LessThan(i, length),                    
                Expression.Block(
                    // dtoList[i] = addressDTOConvertFunc.Invoke(addressList[i]);
                    Expression.Assign(Expression.ArrayAccess(dtoList, i), Expression.Invoke(addressDTOConvertFunc, Expression.ArrayAccess(addresses, i))),
                    // i++;
                    Expression.PostIncrementAssign(i)
                ),
                Expression.Break(forLabel)
            ),
            forLabel)
    );
}
/// <summary>
/// 转化 customer.AddressList -> dto.AddressList
/// </summary>
/// <param name="addressDTOConvertFunc">共用方法</param>
/// <param name="customer"></param>
/// <param name="dto"></param>
/// <returns></returns>
public static BlockExpression ConvertAddressList(Expression<Func<Address, AddressDTO>> addressDTOConvertFunc, ParameterExpression customer, ParameterExpression dto)
{
    // List<Address> addressList;
    var addressList = Expression.Parameter(typeof(List<Address>), "addressList");

    // List<AddressDTO> dtoList;
    var dtoList = Expression.Parameter(typeof(List<AddressDTO>), "dtoList");
    // int count;
    var count = Expression.Variable(typeof(int), "count");
    //// Address item;
    //var item = Expression.Parameter(typeof(Address), "item");
    var forLabel = Expression.Label("forLabel");
    var i = Expression.Variable(typeof(int), "i");
    return Expression.Block(
        [addressList, dtoList, count, i],
        // addressList = customer.AddressList;
        Expression.Assign(addressList, Expression.Property(customer, "AddressList")),
        // dtoList = new List<AddressDTO>();
        Expression.Assign(dtoList, Expression.New(typeof(List<AddressDTO>))),
        // dto.AddressList = dtoList;
        Expression.Assign(Expression.Property(dto, "AddressList"), dtoList),
        // addressCount = addressList.Count;
        Expression.Assign(count, Expression.Property(addressList, "Count")),
        Expression.Loop(
            Expression.IfThenElse(
                // i < addressCount
                Expression.LessThan(i, count),
                // dtoList.Add(addressDTOConvertFunc.Invoke(addressList[i++]));
                Expression.Call(
                    dtoList, 
                    typeof(List<AddressDTO>).GetMethod("Add")!, 
                    Expression.Invoke(addressDTOConvertFunc, Expression.MakeIndex(addressList, typeof(List<Address>).GetProperty("Item"), [Expression.PostIncrementAssign(i)]))),
                Expression.Break(forLabel)
            ),
            forLabel)
    );
}

2.3.1 代码解读

  • CreateCustomerDTO转化Customer为CustomerDTO
  • ConvertAddress转化Customer.Address为CustomerDTO.Address调用了CreateAddressDTO
  • ConvertAddresses转化Customer.Addresses为CustomerDTO.Addresses调用了CreateAddressDTO
  • ConvertAddressList转化Customer.AddressList为CustomerDTO.AddressList调用了CreateAddressDTO
  • 以上看上去是不是很完美!!!
  • 但是马上就要翻车了...

2.4 测试一下

var expression = CreateCustomerDTO();
var func = expression.Compile();
Customer _customer = new()
{
    Name = "jxj",
    Address = new() { City = "gz" },
    AddressList = [new() { City = "bj" }],
    Addresses = [new() { City = "sh" }]
};
var dto = func(_customer);
// {"Name":"jxj","Address":{"City":"gz"},"Addresses":[{"City":"sh"}],"AddressList":[]}

2.4.1 请大家围观翻车现场

  • Address和Addresses转化成功了,但是AddressList转化失败了
  • 如果说LambdaExpression不能复用,为什么Address和Addresses共用LambdaExpression能成功
  • 而且如果删掉Addresses属性AddressList就能转化成功

2.5 交换ConvertAddresses和ConvertAddressList前后顺序再测试

public static Expression<Func<Customer, CustomerDTO>> CreateCustomerDTO()
{
    var customerType = typeof(Customer);
    var dtoType = typeof(CustomerDTO);
    // Customer customer;
    var customer = Expression.Parameter(customerType, "customer");
    // CustomerDTO dto;
    var dto = Expression.Parameter(dtoType, "dto");
    var addressDTOConvertFunc = CreateAddressDTO();
    var body = Expression.Block(
        [dto],
        // dto = new AddressDTO();
        Expression.Assign(dto, Expression.New(dtoType)),
        // dto.Name = customer.Name;
        Expression.Assign(Expression.Property(dto, "Name"), Expression.Property(customer, "Name")),
        // dto.Address = addressDTOConvertFunc.Invoke(customer.Address);
        ConvertAddress(addressDTOConvertFunc, customer, dto),
        // dto.AddressList
        ConvertAddressList(addressDTOConvertFunc, customer, dto),
        // dto.Addresses
        ConvertAddresses(addressDTOConvertFunc, customer, dto),
        // return dto
        dto
    );
    return Expression.Lambda<Func<Customer, CustomerDTO>>(body, customer);
}

2.5.1 得到以下结果

{"Name":"jxj","Address":{"City":"gz"},"Addresses":[null],"AddressList":[{"City":"bj"}]}
  • 无论列表还是数组,谁在前成功!!!
  • 是不是有点无语了

2.6 换成FastExpressionCompiler再测试一下

var expression = CreateCustomerDTO();
var func = FastExpressionCompiler.ExpressionCompiler.CompileFast<Func<Customer, CustomerDTO>>(expression);
Customer _customer = new()
{
    Name = "jxj",
    Address = new() { City = "gz" },
    AddressList = [new() { City = "bj" }],
    Addresses = [new() { City = "sh" }]
};
var dto = func(_customer);
// {"Name":"jxj","Address":{"City":"gz"},"Addresses":[{"City":"sh"}],"AddressList":[{"City":"bj"}]}

换成FastExpressionCompiler全部成功,这是不是实锤是微软的bug

3. 附两个note对比示例

  • expression_sys.dib是微软转化失败示例
  • expression_fast.dib是FastExpressionCompiler转化成功示例
  • 大家可以下载本地执行
  • 用vscode打开就能执行(需要Jupyter Notebook插件)

现在很纠结是不是要换方案了,还是要依赖第三方FastExpressionCompiler ...

源码托管地址: https://github.com/donetsoftwork/MyEmit ,欢迎大家直接查看源码。
gitee同步更新:https://gitee.com/donetsoftwork/MyEmit

如果大家喜欢请动动您发财的小手手帮忙点一下Star。

posted on 2025-09-06 18:11  xiangji  阅读(946)  评论(8)    收藏  举报

导航