Checking for null values for parent properties before calling ThenInclude
File a bug
Hi, I'm having an issue with EF Core, and I think it's a bug, because I can't find a solution on the internet and documentation and don't know how to solve it other than reporting this to the developers.
So the problem is when I do a Query that relies heavily on the Relationships in the model content.
var query = context.CatalogProduct
.Include( x => x.Pictures )
.Include( x => x.Attributes )
.ThenInclude(x => x.Schema )
.Include( x => x.Category )
.AsSplitQuery();
From the code above, I do Include 2x for a collection of Pictures and also Attributes, but the Attributes model itself is actually just a place to store the original value of the attribute itself, there is no property that explains the details of what the attribute is like, and the attribute is explained by Schema.
public class CatalogAttribute : BaseEntity
{
public string? Value { get; set; }
public Guid SchemaId { get; set; }
public virtual CatalogSchema? Schema { get; set; }
public Guid ProductId { get; set; }
public virtual CatalogProduct? Product { get; set; }
}
So that means when I want to do a complex query for Products with all the possibilities while Attributes only store the value but not the type, I also have to include Schema.
public class CatalogSchema : BaseEntity
{
public bool Filterable { get; set; }
public bool Required { get; set; }
public bool Public { get; set; }
public CatalogSchemaType Type { get; set; }
public string? Key { get; set; }
public string? Options { get; set; }
public Guid CategoryId { get; set; }
public virtual CatalogCategory? Category { get; set; }
public virtual ICollection<CatalogAttribute>? Attributes { get; set; }
}
Well, so the main problem is that when I need to include Schema, I have to call/Include Attributes first, and then ThenInclude on Attributes->Schema. But since Attributes is a collection whose value can be null, this will cause problems.
Severity Code Description Project File Line Suppression State Details
Warning (active) CS8620 Argument of type 'IIncludableQueryable<CatalogProduct, ICollection<CatalogAttribute>?>' cannot be used for parameter 'source' of type 'IIncludableQueryable<CatalogProduct, IEnumerable<CatalogAttribute>>' in 'IIncludableQueryable<CatalogProduct, CatalogSchema?> EntityFrameworkQueryableExtensions.ThenInclude<CatalogProduct, CatalogAttribute, CatalogSchema?>(IIncludableQueryable<CatalogProduct, IEnumerable<CatalogAttribute>> source, Expression<Func<CatalogAttribute, CatalogSchema?>> navigationPropertyPath)' due to differences in the nullability of reference types. Codeblue C:\Project\Codeblue\Codeblue.Catalog\Application\RequestCommandHandlers\CatalogQueryRequestCommandHandler.cs 18
While I don't know how to do a null check so that the function remains safe, this warning can be prevented by using '!'
.Include( x => x.Attributes! )
But I don't like to use '!' because when I declare a property as nullable, it means it can be null, while '!' seems to ensure that the value always exists, in real cases, such an error can cause a crash that can stop the application.
I feel that Include should also be done at the beginning, and it is impossible to do a manual query, the reason is that I query the product based on the attribute value, that's why if I don't do Include and ThenInclude, it means that there will be a massive query to do the query I want.
public async Task<QueryResponse<CatalogProduct>> Handle( CatalogQueryRequestCommand @event, CancellationToken cancellationToken = default )
{
var query = context.CatalogProduct
.Include( x => x.Pictures )
.Include( x => x.Attributes )
.ThenInclude(x => x.Schema )
.Include( x => x.Category )
.AsSplitQuery();
var queryTerms = @event.Request;
if( !string.IsNullOrWhiteSpace( queryTerms.Category ) )
{
query = query.Where( x => x.Category != null && x.Category.Name == queryTerms.Category );
}
if( !string.IsNullOrWhiteSpace( queryTerms.User ) )
{
query = query.Where( x => !string.IsNullOrWhiteSpace( x.User ) && x.User.ToLower().Equals( queryTerms.User.ToLower() ) );
}
if( !string.IsNullOrWhiteSpace( queryTerms.Keyword ) )
{
query = query.Where( x => !string.IsNullOrWhiteSpace( x.Name ) && x.Name.ToLower().Contains( queryTerms.Keyword.ToLower() ) );
}
if( queryTerms.Status != null && queryTerms.Status.Count != 0 )
{
query = query.Where( x => queryTerms.Status.Contains( x.Status ) );
}
if( queryTerms.MinPrice != null )
{
query = query.Where( x => queryTerms.MinPrice <= x.Price );
}
if( queryTerms.MaxPrice != null )
{
query = query.Where( x => queryTerms.MaxPrice >= x.Price );
}
if( queryTerms.Attributes != null )
{
foreach( KeyValuePair<string, string> attribute in queryTerms.Attributes )
{
if( !string.IsNullOrEmpty( attribute.Key ) && !string.IsNullOrEmpty( attribute.Value ) )
{
query = query.Where( p => p.Attributes != null && p.Attributes.Any( a => a.Schema != null && a.Schema.Key == attribute.Key && a.Value == attribute.Value ) );
}
}
}
query = queryTerms.SortBy switch
{
CatalogQuerySorting.Created => queryTerms.OrderAscending ? query.OrderBy( x => x.Created ) : query.OrderByDescending( x => x.Created ),
CatalogQuerySorting.Name => queryTerms.OrderAscending ? query.OrderBy( x => x.Name ) : query.OrderByDescending( x => x.Name ),
CatalogQuerySorting.Price => queryTerms.OrderAscending ? query.OrderBy( x => x.Price ) : query.OrderByDescending( x => x.Price ),
CatalogQuerySorting.Popularity => queryTerms.OrderAscending ? query.OrderBy( x => x.Created ) : query.OrderByDescending( x => x.Created ),
CatalogQuerySorting.Featured => queryTerms.OrderAscending ? query.OrderBy( x => x.Created ) : query.OrderByDescending( x => x.Created ),
_ => queryTerms.OrderAscending ? query.OrderBy( x => x.Created ) : query.OrderByDescending( x => x.Created )
};
var count = await query.CountAsync(cancellationToken);
var products = await query
.Skip((queryTerms.Page - 1) * queryTerms.Take)
.Take(queryTerms.Take)
.ToListAsync(cancellationToken);
foreach( var product in products )
{
product.Pictures = product.Pictures?.OrderBy( p => p.Created ).ToList();
}
return new QueryResponse<CatalogProduct>()
{
Page = queryTerms.Page,
PageSize = queryTerms.Take,
TotalCount = count,
Results = products
};
}
@MmMapIoSpace I'm not following everything you are trying to do--you'll need to attach a small, runnable project or post a small, runnable code listing that reproduces what you are seeing for that. However, with regard to this:
But I don't like to use '!' because when I declare a property as nullable, it means it can be null, while '!' seems to ensure that the value always exists, in real cases, such an error can cause a crash that can stop the application.
It is important to remember that this "code" is never executed. It is merely providing information to EF to build a query. EF does a lot of internal compensation for nulls when generating queries, so usually it is not necessary or correct to add a null check, and the correct thing to do is tell the compiler that actually this is okay--it's just an expression three. In this case, it means using the ! operator is the correct thing to do.