减少 EF Core 选择的列

Reduce columns selected by EF Core

我正在使用 EF Core 查询数据库。

减少使用 EF Core 检索的列数量的方法之一是使用 select 语句。例如,

using (SchoolContext context = new SchoolContext(connection, loggerFactory))
{
    foreach (Pupil pupil in context.Pupils.Select(pupil => new Pupil{ Id = pupil.Id, Name = pupil.Name }))
    {
        Console.WriteLine(pupil.Id);
        Console.WriteLine(pupil.Age);
        Console.WriteLine(pupil.Name);
    }
}

会减少

SELECT "p"."id", "p"."age", "p"."name"
FROM "pupil" AS "p"

SELECT "p"."id" AS "Id", "p"."name" AS "Name"
FROM "pupil" AS "p"

但是,我的问题是我需要一种可以动态执行此操作的方法,因为不同的列将通过用户选择 selected。所以我写了一个小的 class 可以动态创建这些 select 语句

public class Selector<T>
{
    private readonly List<PropertyInfo> _properties = new List<PropertyInfo>();

    public Selector<T> AddProperty<TPropertyType>(Expression<Func<T, TPropertyType>> property)
    {
        if (!(property.Body is MemberExpression member))
        {
            throw new ArgumentException($"Expression '{property}' refers to a method, not a property.");
        }

        if (!(member.Member is PropertyInfo propertyInfo))
        {
            throw new ArgumentException($"Expression '{property}' refers to a field, not a property.");
        }

        _properties.Add(propertyInfo);
        return this;
    }

    public Func<T, T> Select()
    {
        ParameterExpression parameter = Expression.Parameter(typeof(T), "x");
        NewExpression objToInitialise = Expression.New(typeof(T));
        IEnumerable<MemberAssignment> propertiesToInitialise = _properties.Select(property =>
            {
                MemberExpression originalValue = Expression.Property(parameter, property);
                return Expression.Bind(property, originalValue);
            }
        );
        MemberInitExpression initialisedMember = Expression.MemberInit(objToInitialise, propertiesToInitialise);
        return Expression.Lambda<Func<T, T>>(initialisedMember, parameter).Compile();
    }
}

并像

一样使用
using (SchoolContext context = new SchoolContext(connection, loggerFactory))
{
    Func<Pupil, Pupil> pupilIdsAndNames = new Selector<Pupil>()
        .AddProperty(x => x.Id)
        .AddProperty(x => x.Name)
        .Select();

    foreach (Pupil pupil in context.Pupils.Select(pupilIdsAndNames))
    {
        Console.WriteLine(pupil.Id);
        Console.WriteLine(pupil.Age);
        Console.WriteLine(pupil.Name);
    }
}

问题

问题是,我为动态生成表达式而编写的代码并没有减少查询返回的数据量。 EF Core 的查询 运行 是

SELECT "p"."id", "p"."age", "p"."name"
FROM "pupil" AS "p"

这可以在下面的 MCVE 中看到。

为什么会发生这种情况,如何解决?


MCVE

程序需要Microsoft.EntityFrameworkCore.Sqlite and Microsoft.Extensions.Logging.Console.

Install-Package Microsoft.EntityFrameworkCore.Sqlite -Version 3.1.1
Install-Package Microsoft.Extensions.Logging.Console -Version 3.1.1

代码

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace ExplicitLoadTest
{
    internal static class Program
    {
        private static void Main()
        {
            SqliteConnection connection = new SqliteConnection("DataSource=:memory:");
            connection.Open();

            ILoggerFactory loggerFactory = LoggerFactory.Create(builder => {
                    builder.AddConsole();
                }
            );

            Pupil phil = MakePupilWithNameAndAge("Phil", 7);
            Pupil joe = MakePupilWithNameAndAge("Joe", 8);
            Pupil mac = MakePupilWithNameAndAge("Mac", 5);
            Pupil rose = MakePupilWithNameAndAge("Rose", 10);
            Pupil harry = MakePupilWithNameAndAge("Harry", 9);
            Pupil meg = MakePupilWithNameAndAge("Meg", 8);

            using (SchoolContext context = new SchoolContext(connection, loggerFactory))
            {
                context.Database.EnsureCreated();

                context.Pupils.Add(phil);
                context.Pupils.Add(joe);
                context.Pupils.Add(mac);
                context.Pupils.Add(rose);
                context.Pupils.Add(harry);
                context.Pupils.Add(meg);

                context.SaveChanges();
            }

            using (SchoolContext context = new SchoolContext(connection, loggerFactory))
            {
                foreach (Pupil pupil in context.Pupils.Select(pupil => new Pupil{ Id = pupil.Id, Name = pupil.Name }))
                {
                    Console.WriteLine(pupil.Id);
                    Console.WriteLine(pupil.Age);
                    Console.WriteLine(pupil.Name);
                }
            }

            using (SchoolContext context = new SchoolContext(connection, loggerFactory))
            {
                Func<Pupil, Pupil> pupilIdsAndNames = new Selector<Pupil>()
                    .AddProperty(x => x.Id)
                    .AddProperty(x => x.Name)
                    .Select();

                foreach (Pupil pupil in context.Pupils.Select(pupilIdsAndNames))
                {
                    Console.WriteLine(pupil.Id);
                    Console.WriteLine(pupil.Age);
                    Console.WriteLine(pupil.Name);
                }
            }

            connection.Close();
        }

        private static Pupil MakePupilWithNameAndAge(string name, int age) => new Pupil
        {
            Id = Guid.NewGuid(),
            Name = name,
            Age = age
        };
    }

    public class Selector<T>
    {
        private readonly List<PropertyInfo> _properties = new List<PropertyInfo>();

        public Selector<T> AddProperty<TPropertyType>(Expression<Func<T, TPropertyType>> property)
        {
            if (!(property.Body is MemberExpression member))
            {
                throw new ArgumentException($"Expression '{property}' refers to a method, not a property.");
            }

            if (!(member.Member is PropertyInfo propertyInfo))
            {
                throw new ArgumentException($"Expression '{property}' refers to a field, not a property.");
            }

            _properties.Add(propertyInfo);
            return this;
        }

        public Func<T, T> Select()
        {
            ParameterExpression parameter = Expression.Parameter(typeof(T), "x");
            NewExpression objToInitialise = Expression.New(typeof(T));
            IEnumerable<MemberAssignment> propertiesToInitialise = _properties.Select(property =>
                {
                    MemberExpression originalValue = Expression.Property(parameter, property);
                    return Expression.Bind(property, originalValue);
                }
            );
            MemberInitExpression initialisedMember = Expression.MemberInit(objToInitialise, propertiesToInitialise);
            return Expression.Lambda<Func<T, T>>(initialisedMember, parameter).Compile();
        }
    }

    public class Pupil
    {
        public Guid Id { get; set; }
        public string Name { get; set; }
        public int? Age { get; set; }
    }

    public class SchoolContext : DbContext
    {
        private readonly ILoggerFactory _loggerFactory;
        private readonly SqliteConnection _connection;

        public SchoolContext(SqliteConnection connection, ILoggerFactory loggerFactory)
        {
            _loggerFactory = loggerFactory;
            _connection = connection;
        }

        public DbSet<Pupil> Pupils { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            base.OnConfiguring(optionsBuilder);

            if (optionsBuilder == null)
            {
                throw new ArgumentNullException(nameof(optionsBuilder), "Options builder is required and cannot be null");
            }

            if (!optionsBuilder.IsConfigured)
            {
                optionsBuilder.UseSqlite(_connection)
                    .EnableSensitiveDataLogging()
                    .UseLoggerFactory(_loggerFactory);
            }
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            if (modelBuilder == null)
            {
                throw new ArgumentNullException(nameof(modelBuilder), "Model builder is required and cannot be null");
            }

            modelBuilder.Entity<Pupil>(entity =>
            {
                entity.ToTable("pupil");

                entity.HasIndex(e => e.Id)
                    .HasName("pupil_id_uindex")
                    .IsUnique();

                entity.Property(e => e.Id)
                    .IsRequired()
                    .HasColumnName("id")
                    .HasColumnType("char(36)");

                entity.Property(e => e.Name)
                    .HasColumnName("name")
                    .HasColumnType("varchar(45)");

                entity.Property(e => e.Age)
                    .HasColumnName("age")
                    .HasColumnType("int(3)");
            });
        }
    }
}

感谢Ivan Stoev


问题是我打电话给 Enumerable.Select 而不是 Queryable.Select

public Expression<Func<T, T>> Select()
{
    ParameterExpression parameter = Expression.Parameter(typeof(T), "x");
    NewExpression objToInitialise = Expression.New(typeof(T));
    IEnumerable<MemberAssignment> propertiesToInitialise = _properties.Select(property =>
        {
            MemberExpression originalValue = Expression.Property(parameter, property);
            return Expression.Bind(property, originalValue);
        }
    );
    MemberInitExpression initializedMember = Expression.MemberInit(objToInitialise, propertiesToInitialise);
    return Expression.Lambda<Func<T, T>>(initializedMember, parameter);
}

通过将 Selector.Select 方法签名更新为上面的方法,使用了正确的 select 语句并且它按预期工作。