efcore
efcore copied to clipboard
achieving join entity non-nullability semantics without startling log messages
this is
- a suggestion that the below log message is spuriously generated in some situations. if it's interesting enough i will try to duplicate outside the environment i'm observing this
- request for clarification on when the advice 'or define matching query filters for both entities in the navigation.' is emitted, and whether it should be construed as a model validation error message
Entity 'AccessControlEntry' has a global query filter defined and is the required end of a relationship with the entity 'GISFormatACL'. This may lead to unexpected results when the required entity is filtered out. Either configure the navigation as optional, or define matching query filters for both entities in the navigation. See https://go.microsoft.com/fwlink/?linkid=2131316 for more information.
this message refers to
- a join entity GISFormatACL and a many to many relationship with a one sided clr expressed navigation between AccessControlEntry (no navigation to GISFormat) and GISFormat (clr navigation to AccessControlEntry)
- that has a required relationship to another non-join entity
- that is applied to all other entities including itself in the dbcontext
- specifically for the purpose of causing the filtering described
more giving of the givens
- suspiciously similar evidence being presented in a join entity id detection issue https://github.com/dotnet/efcore/issues/33603
- dbcontext with dynamically rendered dbsets and global query filters in a multi-targeted class library and downstream consumer code targetting netcore6, 7 & 8
- global query filters applied dynamically that transitively apply to all discovered entities
abridged code included for the purpose of exposing 'something being done wrong'
public class GISEndpointOwner : IDeployableContentJoinEntity
{
public GISEndpointOwner() { }
public Guid GISEndpointId { get; set; }
public Guid OwnerId { get; set; }
public GISEndpoint GISEndpoint { get; set; } = null!;
public Principal Owner { get; set; } = null!;
}
public class GISEndpointACL : IDeployableContentJoinEntity
{
public GISEndpointACL() { }
public Guid GISEndpointId { get; set; }
public Guid AccessControlEntryId { get; set; }
public GISEndpoint GISEndpoint { get; set; } = null!;
public AccessControlEntry AccessControlEntry { get; set; } = null!;
}
[MultiTenant]
public class GISEndpoint : IDeployableContentEntity, IMetaDataModelEntity
{
public GISEndpoint() { }
//etc
public ICollection<AccessControlEntry>? AccessControlEntries { get; set; } = new HashSet<AccessControlEntry>(); // there is no reverse navigation for this on AccessControlEntry
public ICollection<Principal>? Owners { get; set; } = new HashSet<Principal>(); // there is no reverse navigation for this on Principal
[Key]
public Guid Id { get; set; }
// etc
/// <summary>
/// supports serializing leaflet/openlayers map configuration
/// </summary>
public ICollection<GISMapLayer>? GISMapLayers { get; set; } = new HashSet<GISMapLayer> { }; // there is reverse navigation here
// this code lets an entity developer (downstream and disconnected from the dbcontext development activity) participate in modelcreating
// thus benefiting from a row level rbac layer that requires one sided navigation references to the rbac entities
public static void OnModelCreating(ModelBuilder builder)
{
#if NET7_0 || NET8_0
builder.Entity<GISEndpoint>()
.HasMany(o => o.Owners)
.WithMany($"{nameof(GISEndpoint)}{nameof(IContentRowLevelSecured.AccessControlEntries)}")
.UsingEntity<GISEndpointOwner>(
x => x.HasOne(x => x.Owner).WithMany(),
x => x.HasOne(x => x.GISEndpoint).WithMany());
builder.Entity<GISEndpoint>()
.HasMany(o => o.AccessControlEntries)
.WithMany($"{nameof(GISEndpoint)}{nameof(IContentRowLevelSecured.AccessControlEntries)}")
.UsingEntity<GISEndpointACL>(
x => x.HasOne(x => x.AccessControlEntry).WithMany(),
x => x.HasOne(x => x.GISEndpoint).WithMany());
#endif
}