EF Core 在 Null Check 完成时抛出 Null Reference Exception(Secondary Select with null check throws Null Reference Exception)

EF Core throws Null Reference Exception when Null Check is done (Secondary Select with null check throws Null Reference Exception)

我通过反射映射的泛型 类 对自动映射模型进行了多次复杂查询。

我在我的问题下面添加了实体配置,以备不时之需。

问题

关于问题,我有一个通过多次传递构建的查询,一次传递在基础存储库中,另一个传递在实体特定的存储库中。

但完成的生成查询将是这样的:

var x = await _uow.Knowledge.Query.Include(i => i.Translates)
            .ThenInclude(i => i.Language)
            .Select(s => new
            {
                Item = s,
                TranslateNative = s.Translates != null //.Any()
                    ? s.Translates.FirstOrDefault(w => w.Language.Iso6391 == FixedData.Language.Native.Iso6391)
                    : null,
                TranslateEnglish = s.Translates != null //.Any()
                    ? s.Translates.FirstOrDefault(w => w.Language.Iso6391 == FixedData.Language.English.Iso6391)
                    : null,
                TranslateSystem = s.Translates != null //.Any()
                    ? s.Translates.FirstOrDefault(w => w.LanguageId == SystemLanguageId)
                    : null,
                TranslateAnyNativePriority = s.Translates != null //.Any()
                    ? (
                        s.Translates.FirstOrDefault(w => w.Language.Iso6391 == FixedData.Language.Native.Iso6391)
                        ?? (SystemLanguageId.HasValue
                            ? s.Translates.FirstOrDefault(w => w.Language.Id == SystemLanguageId.Value)
                            : s.Translates.FirstOrDefault(w => w.Language.Iso6391 == FixedData.Language.English.Iso6391))
                        ?? s.Translates.FirstOrDefault()
                    )
                    : null,
                TranslateAnySystemPriority = s.Translates != null //.Any()
                    ? (
                        (SystemLanguageId.HasValue
                            ? s.Translates.FirstOrDefault(w => w.Language.Id == SystemLanguageId.Value)
                            : s.Translates.FirstOrDefault(w => w.Language.Iso6391 == FixedData.Language.Native.Iso6391))
                        ?? s.Translates.FirstOrDefault(w => w.Language.Iso6391 == FixedData.Language.English.Iso6391)
                        ?? s.Translates.FirstOrDefault()
                    )
                    : null

            })
            .Select(s=> new KnowledgeListVm
        {
            Id = s.Item.Id,
            Name = s.TranslateAnySystemPriority != null ? s.TranslateAnySystemPriority.Name : null,
            NameEn = s.TranslateEnglish != null ? s.TranslateEnglish.Name : null
        })
            .ToListAsync();

现在,当我添加一个只有本地翻译的新实体时,我收到以下异常,没有任何内部 (InnerException):

System.NullReferenceException: 'Object reference not set to an instance of an object.'

尽管我做了所有的空值检查。

由于我自己可以摆脱 Null Reference Exception 问题,我将在答案中讲述其余的故事。但是如果有人知道幕后发生了什么,请告诉我写一个更好的代码。因为我认为我只是在我的代码中作弊。

模型配置 TL;DR;

public class Knowledge : IIdentityIdEntity<int>, IHasTranslateEntity<Knowledge, KnowledgeTranslate, int>
{
    public Knowledge()
    {
        Translates = new HashSet<KnowledgeTranslate>();
        CreatorKnowledges = new HashSet<CreatorKnowledge>();
    }

    public int Id { get; set; }
    public ICollection<KnowledgeTranslate> Translates { get; set; }
    public ICollection<CreatorKnowledge> CreatorKnowledges { get; set; }
}

public class KnowledgeTranslate : IIdentityIdEntity<int>, IIsTranslateEntity<Knowledge, KnowledgeTranslate, int>
{
    public int Id { get; set; }

    public int OwnerId { get; set; }
    public Knowledge Owner { get; set; }
    public int LanguageId { get; set; }
    public Language Language { get; set; }
    
    public string Name { get; set; }
}

public class Language: IIdentityIdEntity<int>, IHasTranslateEntity<Language, LanguageTranslate, int>
{
    public Language()
    {
        Translates = new HashSet<LanguageTranslate>();
        //Languages = new HashSet<LanguageTranslate>();

        // we have so many joins for Languages ... with languageTranslate, with ProjectTranslate, with any kind of XTranslate but we don't need them
    }

    public int Id { get; set; }
    public string Iso6391 { get; set; }
    public string Iso6392T { get; set; }
    public string Iso6392B { get; set; }
    public string Iso6393 { get; set; }

    /// <summary>
    /// Translated information that one language have (Joined with LanguageId)
    /// </summary>
    public virtual ICollection<LanguageTranslate> Translates { get; set; }
    ///// <summary>
    ///// Language in which the translation is based on (Joined with OwnerId)
    ///// </summary>
    //public virtual HashSet<LanguageTranslate> Languages { get; set; }
    
    // we have so many joins for Languages ... with languageTranslate, with ProjectTranslate, with any kind of XTranslate but we don't need them
}

映射器像这样映射它们:

if ((interfaceType = entityType.ClrType.GetInterfaces()
                    .FirstOrDefault(w => w.IsGenericType
                                         && w.GetGenericTypeDefinition() == typeof(IHasTranslateEntity<,,>))) != null)
            {
                if (interfaceType.GetGenericArguments().Length != 3)
                {
                    throw new NotImplementedException(@$"Cannot find implementation for ""{typeof(IHasTranslateEntity<,,>).Name}"" interface that take more than one argument in ""{nameof(ApplicationDbContext)}"" class.");
                }

                // var genericArgType = interfaceType.GenericTypeArguments[0]; // Here act same as ClrType
                var translateArgType = interfaceType.GenericTypeArguments[1];
                var idArgType = interfaceType.GenericTypeArguments[2];
                builder.SetTranslatesMapping(entityType.ClrType, translateArgType, idArgType);
            }

#region Translates
    public static void SetTranslatesMapping(this ModelBuilder modelBuilder, Type entityType, Type translateEntityType, Type tId)
    {
        SetTranslatesMappingMethod.MakeGenericMethod(entityType, translateEntityType, tId)
            .Invoke(null, new object[] { modelBuilder });
    }

    static readonly MethodInfo SetTranslatesMappingMethod = typeof(EFFilterExtensions)
        .GetMethods(BindingFlags.NonPublic | BindingFlags.Static)
        .Single(t => t.IsGenericMethod && t.Name == nameof(SetTranslatesMapping));

    private static void SetTranslatesMapping<TEntity, TTranslateEntity, TId>(this ModelBuilder modelBuilder)
        where TEntity : class, IHasTranslateEntity<TEntity, TTranslateEntity, TId>
        where TTranslateEntity : class, IIsTranslateEntity<TEntity, TTranslateEntity, TId>
    {
        // Is Duplicate
        // modelBuilder.Entity<TEntity>().Property(e => e.Id);

        // var translateType = modelBuilder.Entity<TEntity>().Property(p => p.Translates).Metadata.ClrType;

        modelBuilder
            .Entity<TEntity>()
            .HasMany(m => m.Translates)
            .WithOne(o => o.Owner)
            .HasForeignKey(fk => fk.OwnerId)
            .OnDelete(DeleteBehavior.Cascade);
    }

    #endregion Translates

注:

我已经更新了好几次我的包,因为我没有时间,我碰巧看到了这么多版本的 EfCore 3.1 包,现在我在 3.1.10。

所以每次遇到我的项目,我也访问这个错误,因为我有一点时间,我最终检查了我所有的检查,看到它们都是正确的,并且没有任何改变就离开了这个项目.

今天,我有更多的时间,我开始重新创建查询,如您在上面看到的那样,一步一步,我明白大的不会失败 (TranslateAnySystemPriority) 但小的不会失败(TranslateEnglish)

具有以下查询部分:

Select 1:

TranslateEnglish = s.Translates != null //.Any()
                    ? s.Translates.FirstOrDefault(w => w.Language.Iso6391 == FixedData.Language.English.Iso6391)
                    : null,

Select 2:

NameEn = s.TranslateEnglish != null ? s.TranslateEnglish.Name : null

实际上,如果我说更好,问题出在第一个 Select 中返回 null 的问题上。 我运行几次测试,我发现: 如果我在第一个或第二个 Select 中对它进行一次空检查,或者不对它进行 运行 任何空检查,它将像我不期望的那样工作,它工作正常。

所以我删除了所有空检查。

但是,如果我对两个 Select 都进行空检查,只要来自第一个 select 的返回数据为空,它总是会抛出 NullReferenceException。不管你做了多少空检查。

所以固定代码是这样的:

NameEn = s.TranslateEnglish //!= null ? s.TranslateEnglish.Name : null

TranslateEnglish = //s.Translates != null //.Any()
                    //? 
                s.Translates.FirstOrDefault(w => w.Language.Iso6391 == FixedData.Language.English.Iso6391)
                    //: null,
                    ,

或者两者都做。

我的最终代码是:

var x = await _uow.Knowledge.Query.Include(i => i.Translates)
    .ThenInclude(i => i.Language)
    .Select(s => new
    {
        Item = s,
        TranslateNative = s.Translates.FirstOrDefault(w => w.Language.Iso6391 == FixedData.Language.Native.Iso6391),
        TranslateEnglish = s.Translates.FirstOrDefault(w => w.Language.Iso6391 == FixedData.Language.English.Iso6391),
        TranslateSystem = s.Translates.FirstOrDefault(w => w.LanguageId == SystemLanguageId),
        TranslateAnyNativePriority = 
                s.Translates.FirstOrDefault(w => w.Language.Iso6391 == FixedData.Language.Native.Iso6391)
                ?? (SystemLanguageId.HasValue
                    ? s.Translates.FirstOrDefault(w => w.Language.Id == SystemLanguageId.Value)
                    : s.Translates.FirstOrDefault(w =>
                        w.Language.Iso6391 == FixedData.Language.English.Iso6391))
                ?? s.Translates.FirstOrDefault(),
        TranslateAnySystemPriority = (SystemLanguageId.HasValue
                    ? s.Translates.FirstOrDefault(w => w.Language.Id == SystemLanguageId.Value)
                    : s.Translates.FirstOrDefault(w =>
                        w.Language.Iso6391 == FixedData.Language.Native.Iso6391))
                ?? s.Translates.FirstOrDefault(
                    w => w.Language.Iso6391 == FixedData.Language.English.Iso6391)
                ?? s.Translates.FirstOrDefault()
    })
    .Select(s => new //KnowledgeListVm
    {
        Id = s.Item.Id,
        Name = s.TranslateAnySystemPriority.Name,
        NameEn = s.TranslateEnglish.Name
    })
    .ToListAsync();