Search code examples
c#entity-framework-corenullreferenceexceptionef-core-3.1multiple-select-query

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


I had several complex query with auto mapped model through generics classes mapped by reflection.

I add the entity configuration below my issue, in case of need.

The Issue

As for Issue, I have a query which is built through multiple pass, one pass is in Base Repository, and the other is in entity specific Repository.

but the completed generated query will be like this:

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();

Now when i add a new entity with only native translation, I received following exception, without any inner (InnerException):

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

Even though I did all the null checks.

As I could get rid of the Null Reference Exception thing, myself, I will tell the rest of story in the answer. But if anyone knows what is going behind the scene please tell me to write a better code. as I think i just cheat in my code.

Model Configuration 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
}

and the mapper map them like this:

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

Note:

I already updated my packages several time, as I had little time, I happen to see so many versions of EfCore 3.1 packages, and now I'm on 3.1.10.


Solution

  • So every time i came across my project, I also visited this error, and since i had a little time, I end up checking all my checks, seeing all of them are right, and left the project without any change.

    Today, I had a little bit more time, I start recreating the query as you see above, step by step, and i Understand that the bigger one does not fails (TranslateAnySystemPriority) but the smaller one (TranslateEnglish)

    with following query parts:

    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
    

    Actually it's better if I say, the issue was with one that returned null in first Select. I run several test, and I found: If I null-check it once in first or second Select, or do not run any null-check on it, it will work as what I'm not expecting, it work fine.

    So I remove all the null checks.

    But if, I do the null check on both Select, It always throws NullReferenceException as long as the returning data from first select is empty. No matter how many null-check you do.

    So the Fixed code is either of this:

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

    or

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

    Or doing both.

    The final code for me is:

    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();