Yesterday, I created a new project to play around with Blazor and .NET 5. However, my fun was cut short when EF Core refused to fetch dependent child properties. This is despite using eager loading with Include
.
Clean projects, unit tests, nullable reference types, init
properties, the code was super simple with just 2 classes Market
and Instrument
and a one to many relationship. What could possibly go wrong?
1 2 3 4 5 6 7 8 9 10 11 |
public class Market { public int MarketId { get; init; } public string Name { get; init; } = string.Empty; public virtual List<Instrument> Instruments { get; init; } = new(); } public class Instrument { public int InstrumentId { get; init; } public int MarketId { get; init; } public virtual Market Market { get; init; } = new(); public string Symbol { get; init; } = string.Empty; } |
My Blazor editor was also super simple
1 2 3 4 5 6 7 |
protected override async Task OnParametersSetAsync() { ModelToEdit = await _dbContext.Markets.Include(x => x.Instruments) .AsSingleQuery() .Where(x => x.Ticker == Id) .FirstOrDefaultAsync(); await base.OnParametersSetAsync(); } |
However, to my dismay Market.Instruments
would always be empty. I've added some console logging to EF. After ensuring the data was indeed in the database and with the debugging turned on, there didn't appear to be anything wrong with the query generation itself.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
Generated query execution expression: 'queryContext => ShapedQueryCompilingExpressionVisitor.SingleOrDefaultAsync<Market>( asyncEnumerable: new SingleQueryingEnumerable<Market>( (RelationalQueryContext)queryContext, RelationalCommandCache.SelectExpression( Projection Mapping: SELECT t.MarketId, t.Name, m.InstrumentId, m.Symbol, m.MarketId FROM Projection Mapping: ( SELECT TOP(1) s.MarketId, s.Name, s.Ticker, s.Type FROM data.Markets AS s WHERE s.Ticker == N'FB' ) AS t LEFT JOIN data.Instruments AS m ON t.MarketId == m.MarketId ORDER BY t.MarketId ASC, m.InstrumentId ASC), Func<QueryContext, DbDataReader, ResultContext, SingleQueryResultCoordinator, Market>, QuantTrading.Data.Persistence.DataContext, False, False ), cancellationToken: queryContext.CancellationToken)' |
You'd also see the change tracker doing its job:
1 2 3 4 |
CoreEventId.StartedTracking[10806] (Microsoft.EntityFrameworkCore.ChangeTracking) Context 'DataContext' started tracking 'Market' entity with key '{MarketId: 4}'. CoreEventId.StartedTracking[10806] (Microsoft.EntityFrameworkCore.ChangeTracking) Context 'DataContext' started tracking 'Instrument' entity with key '{InstrumentId: 4}'. |
The next was to verify the EntityTypeConfiguration
were correct.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
public class MarketConfiguration : IEntityTypeConfiguration<Market> { public void Configure(EntityTypeBuilder<Market> builder) { if (builder is null) throw new ArgumentNullException(nameof(builder)); builder.HasKey(x => x.MarketId); builder.Property(x => x.Name).HasMaxLength(50); } } public class InstrumentConfiguration : IEntityTypeConfiguration<Instrument> { public void Configure(EntityTypeBuilder<Instrument> builder) { if (builder is null) throw new ArgumentNullException(nameof(builder)); builder.HasKey(x => x.InstrumentId); builder.HasOne(x => x.Market).WithMany(x => x.Instruments).HasForeignKey(x => x.MarketId); } } |
After pulling my hair out for 2.5 hours, I finally got to the bottom of the problem and it was this line here:
1 |
public virtual Market Market { get; init; } = new(); |
Instead of initializing a new Market
object inside the Instrument
class. You have to in fact make int a nullable reference type like so:
1 |
public virtual Market? Market { get; init; } |