In my database model I have a navigation property which cannot be null. However, since the variable may not have been loaded yet, it can be null during runtime.
Correct. EF Navigation properties will always need to be nullable, even if the underlying FK constraint (and NOT NULL
column constraints) require a valid reference to always be present in the DB, because of the simple fact that loading related data is never required.
In that situation, the entity class properties corresponding to the FK columns will (of course) be non-nullable types, but any reference navigation properties must always be ?
(i.e. nullable reference types). (Note: this does not apply to collection navigation properties, which are another story entirely).
According to Microsoft documentation, this problem can be solved by throwing an exception in the getter. But all Exceptions that are thrown in the Getters are not catched by a global Exception handler. The execution of the code immediately stopps, after the exception has been thrown.
I agree. I'm disappointed that EF's documentation is even suggesting that. This isn't anything new though: ever since they started seriously suggesting people do = null!
to initialize DbContext properties (which, honestly, is just really, really dumb)
Another method would be, that I make the property also nullable. But if I do so, I have to check in each single function, if the property is null, which seems not to be very DRY.
Yes, but only if you use the `class Car`` EF type itself as a DTO for passing data around in your application.
...but if you instead design a new, separate immutable DTO class
, with non-nullable properties, with a constructor that verifies these class-invariants, then that works great.
You can also use implicit
conversions between the DTO and the entity-type to reduce some frictions, such as passing a DTO into a method expecting an entity, or to even use the DTOs with Linq Queries and DbSet
and more.
So if this is your EF Entity class:
public class Car
{
[Key]
[DatabaseGenerated( Identity )]
public Int32 CarId { get; set; } // CarId int NOT NULL IDENTITY PRIMARY KEY
public Int32 MakeId { get; set; } // MakeId int NOT NULL CONSTRAINT FK_Car_Make FOREIGN KEY REFERENCES dbo.Makes ( MakeId )
public Brand? Make { get; set; } // Navigation property for FK_Car_Make
}
...and you want to represent a Car
with a loaded Make
propertty, then just add this to your project:
public class LoadedCarWithMake
{
public LoadedCarWithMake( Car car, Make make )
{
this.Car = car ?? throw new ArgumentNullException(nameof(car));
this.Make = make ?? throw new ArgumentNullException(nameof(make));
// Ensure `make` corresponds to `car.Make`:
if( !Object.ReferenceEquals( car.Make, make ) ) throw new ArgumentException( message: "Mismatched Car.Make", paramName: nameof(make) );
}
public Car Car { get; } // Immutable property, though `Car` is mutable.
public Make Make { get; } // Immutable property, though `Make` is mutable.
// Forward other members:
public Int32 CarId => this.Car.CarId;
public Int32 MakeId => this.Car.MakeId;
// Implicit conversion via reference-returns:
public static implicit operator Car( LoadedCarWithMake self ) => self.Car;
}
Now, even if that Car
entity has its Make
navigation-property re-set to null
or changed then that's okay because it won't affect consumers that use LoadedCarWithMake
because LoadedCarWithMake.Make
will never be null
.
You'd also want to add a loader method for this too, e.g.:
public static async Task<LoadedCarWithMake> LoadCarWithMakeAsync( this MyDbContext db, Int32 carId )
{
Car carWithMake = await db.Cars
.Include( c => c.Make )
.Where( c => c.CarId == carId )
.SingleAsync()
.ConfigureAwait(false);
return new LoadedCarWithMake( car, car.Make! );
}
If all this extra repetitive code looks tedious, don't worry: you shouldn't normally need to write this by-hand: it's straightforward to use tools like T4 - or Roslyn Code Generation - to automatically create these "Loaded...
" types for you - I just wish the EF team would include that in-box for the benefit of everyone.
You can improve this further by defining IReadOnly...
interfaces for each entity-type (so you'd have IReadOnlyCar
and IReadOnlyMake
, which do not contain any navigation-properties, only get
-only scalar/value properties), then LoadedCarWithMake
would also then get to implement IReadOnlyCar
.