Фильтрация с наследованием EF Core 2.1 - PullRequest
0 голосов
/ 29 октября 2018

Я пытаюсь найти способ отфильтровать результаты в EF Core 2.1 при использовании унаследованных объектов.

У меня есть базовая модель и несколько унаследованных классов (но я только что включил один):

public class Like {
    public int Id { get; set; }
    public LikeType LikeType { get; set; }
}

public class DocumentLike : Like {
    [ForeignKey(nameof(Document))]
    public int DocumentId { get; set; }
    public virtual Document Document { get; set; }
}

LikeType - это перечисление, которое определяется как дискриминатор в dbcontext. Каждый Document имеет логическое свойство .IsCurrent.

Чтобы получить все элементы из базы данных, я использую запрос типа:

IQueryable<Like> query = _context.Set<Like>()
    .Include(x => x.Owner)
    .Include(x => (x as DocumentLike).Document.DocumentType)
    .Include(x => (x as ProductLike).Product)
    .Include(x => (x as TrainingLike).Training)

Это прекрасно работает и возвращает все объекты с включенными подобъектами без каких-либо ошибок. То, что я пытаюсь сделать, это получить все элементы из базы данных, для которой связанный документ имеет .IsCurrent == true. Я попытался добавить следующее в запрос выше, но оба приводят к исключению:

.Where(x => (x as DocumentLike).Document.IsCurrent == true)

И

.Where(x => x.LikeType == LikeType.Document ? (x as DocumentLike).Document.IsCurrent == true : true) 

Исключение, которое выдается при выполнении запроса:

NullReferenceException: Object reference not set to an instance of an object.
    lambda_method(Closure , TransparentIdentifier<TransparentIdentifier<TransparentIdentifier<TransparentIdentifier<TransparentIdentifier<TransparentIdentifier<TransparentIdentifier<TransparentIdentifier<TransparentIdentifier<TransparentIdentifier<TransparentIdentifier<TransparentIdentifier<Like, ApplicationUser>, Organisation>, Training>, Product>, Platform>, NewsItem>, Event>, Document>, DocumentType>, Course>, CourseType>, ApplicationUser> )
    System.Linq.Utilities+<>c__DisplayClass1_0<TSource>.<CombinePredicates>b__0(TSource x)
    System.Linq.Enumerable+WhereSelectEnumerableIterator<TSource, TResult>.MoveNext()
    Microsoft.EntityFrameworkCore.Query.Internal.LinqOperatorProvider._TrackEntities<TOut, TIn>(IEnumerable<TOut> results, QueryContext queryContext, IList<EntityTrackingInfo> entityTrackingInfos, IList<Func<TIn, object>> entityAccessors)+MoveNext()
    Microsoft.EntityFrameworkCore.Query.Internal.LinqOperatorProvider+ExceptionInterceptor<T>+EnumeratorExceptionInterceptor.MoveNext()
    System.Collections.Generic.List<T>.AddEnumerable(IEnumerable<T> enumerable)
    System.Linq.Enumerable.ToList<TSource>(IEnumerable<TSource> source)

Есть ли способ сделать это?

UPDATE: Чтобы уточнить: я ищу один запрос, который возвращает все Like -объекты из базы данных, независимо от их (под) типов. В случае, если подтип DocumentLike, я хочу только объекты, которые связаны с документом, который имеет .IsCurrent == true.

Ответы [ 3 ]

0 голосов
/ 29 октября 2018

Хитрость заключалась в том, чтобы немного отредактировать предикат, например:

.Where(x => !(x is DocumentLike) || ((DocumentLike)x).Document.IsCurrent == true)

Спасибо Панагиотису Канавосу за предложение.

0 голосов
/ 03 июня 2019

У меня была похожая проблема с многоуровневой иерархией классов, где использование .OfType<>() приводило к "преждевременному" (на мой взгляд) отключению базы данных для получения всех данных, поэтому может сделать фильтрацию в памяти, что нежелательно!

Это иллюстрирует мою иерархию:

public abstract class BaseSetting {}
public abstract class AccountSetting : BaseSetting {}
public abstract class UserSetting : BaseSetting {}

public class AccountSettingA : AccountSetting {}
public class AccountSettingB : AccountSetting {}
public class UserSettingA : UserSetting {}
public class UserSettingB : UserSetting {}

И это настройка для DbContext:

public class DataContext : DbContext
{
  public virtual DbSet<BaseSetting> Settings { get; set; }

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

    builder.Entity<BaseSetting>(e =>
    {
        e.ToTable("Settings");
        e.HasDiscriminator<string>("Type");
    });
  }
}

Тогда я бы попытался получить все настройки для одной учетной записи, например:

AccountSetting[] settings = context.Settings
    .OfType<AccountSetting>()
    .Where(s => s.Account.Id == accountId)
    .ToArray();

В результате SQL-запрос выглядит примерно так:

SELECT *
FROM [Settings] AS [s0]
WHERE [s0].[Type] IN (N'AccountSettingA',N'AccountSettingB',N'UserSettingA',N'UserSettingB')

как раз перед тем, как выбрасывает NullReferenceException в бит .Where(s => s.Account.Id == accountId) запроса, потому что Account является нулем. Вероятно, это можно было бы «исправить», добавив .Include(...) к запросу, чтобы тоже пропустить Account, но это только увеличит объем данных, которые мы получаем из базы данных. (Следует отметить, что если вы конфигурируете контекст для выдачи ошибок при попытке выполнить оценку на клиенте в соответствии с комментарием @ PanagiotisKanavos к исходному вопросу, то вместо этого вы получите QueryClientEvaluationWarning).

Решение (по крайней мере для меня) было добавить это к методу OnModelCreating в моем DbContext:

typeof(BaseSetting).Assembly.GetTypes()
  .Where(t => t != typeof(BaseSetting) && typeof(BaseSetting).IsAssignableFrom(t))
  .Each(s => builder.Entity(s).HasBaseType(s.BaseType));

Это пройдёт через все мои разные классы настроек (которые наследуются от BaseSetting) и сообщит Entity Framework, что их базовый тип - это их Type.BaseType. Я бы подумал, что EF может решить это самостоятельно, но после этого я получаю SQL вроде этого (и без QueryClientEvaluationWarning исключений!):

SELECT *
FROM [Settings] as [a]
INNER JOIN [Accounts] AS [a.Account] ON [a].[AccountId] = [a.Account].[Id]
WHERE ([a].[Type] IN (N'AccountSettingA',N'AccountSettingB',N'UserSettingA',N'UserSettingB')
AND ([a.Account].[Id] = @__accountId)

Который, очевидно, возвращает только настройки учетной записи для интересующей меня учетной записи, а не все настройки учетной записи и все пользовательские настройки как это было раньше.

0 голосов
/ 29 октября 2018

Вы можете использовать Enumerable.OfType для фильтрации типов. Для получения дополнительной информации вы можете взглянуть на https://docs.microsoft.com/de-de/dotnet/api/system.linq.enumerable.oftype?redirectedfrom=MSDN&view=netcore-2.1

А для вашего случая вы можете просто отфильтровать результат по

var documentLikes = query.OfType<DocumentLike>();
...