dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.63k stars 3.15k forks source link

DefiningEntityType not set on Owned entity #19873

Closed sylvaingirardbe closed 1 year ago

sylvaingirardbe commented 4 years ago

I'm using modelBuilder.Model.GetEntityTypes() to find types that are not owned and set their table name to the entity name to prevent pluralization, except for owned types to prevent table splitting. When checking modelBuilder.Model.GetEntityTypes(), Commication has its DefiningEntityType correctly set to Company while Address hasn't while both are defined as owned types of Company.

Steps to reproduce

I have an entity Address

    [Owned]
    public class Address
    {
        public string DepartmentName { get; set; }
        public virtual int? CountryId { get; set; }
        [StringLength(20)] 
        public string PostalCode { get; set; }
        [StringLength(120)] 
        public virtual string StreetName { get; set; }
        [StringLength(10)] 
        public virtual string StreetNumber { get; set; }
        public virtual Country Country { get; set; }
        public virtual string StateName { get; set; }
        public virtual string CityName { get; set; }
    }

and an entity Communication

    [Owned]
    public class Communication
    {
        public string WebsiteUrl { get; set; }
        public string EmailAddress { get; set; }
        public string FaxNumber { get; set; }
        public string MobileNumber { get; set; }
        public string Landline1 { get; set; }
        public string LandLine2 { get; set; }
        public string EmergencyNumber { get; set; }
        public string SkypeAddress { get; set; }
        public string LinkedInUrl { get; set; }
    }

both owned by an entity Company

    public class Company : DomainObjectWithId
    {
        public List<CompanyRoleType> CompanyRoles { get; set; }
        public string Code { get; set; }
        public virtual string Name { get; set; }
        public string Vat { get; set; }
        public Address Address { get; set; }
        public string ExternalReference { get; set; }
        [DefaultValue("1")] 
        public bool Enabled { get; set; }
        public Communication Communication { get; set; }
        public ICollection<Person> Persons { get; set; }
        public bool IsDeleted { get; set; }
    }

Company configuration

    public class CompanyConfiguration : IEntityTypeConfiguration<Company>
    {
        public void Configure(EntityTypeBuilder<Company> builder)
        {
            builder
                .Property(company => company.CompanyRoles)
                .HasJsonConversion();
            builder
                .OwnsOne(c => c.Communication);
            builder
                .OwnsOne(c => c.Address);
        }
    }

Debug view of the model:

Model: 
  EntityType: Address
    Properties: 
      CompanyId (no field, int) Shadow Required PK FK AfterSave:Throw ValueGenerated.OnAdd
      CityName (string)
      CountryId (Nullable<int>) FK Index
      DepartmentName (string)
      PostalCode (string) MaxLength20
        Annotations: 
          MaxLength: 20
      StateName (string)
      StreetName (string) MaxLength120
        Annotations: 
          MaxLength: 120
      StreetNumber (string) MaxLength10
        Annotations: 
          MaxLength: 10
    Navigations: 
      Country (Country) ToPrincipal Country
    Keys: 
      CompanyId PK
    Foreign keys: 
      Address {'CompanyId'} -> Company {'Id'} Unique Ownership ToDependent: Address
      Address {'CountryId'} -> Country {'Id'} ToPrincipal: Country
    Annotations: 
      RelationshipDiscoveryConvention:NavigationCandidates: System.Collections.Immutable.ImmutableSortedDictionary`2[System.Reflection.PropertyInfo,System.Type]
  EntityType: Company
    Properties: 
      Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      Code (string)
      CompanyRoles (List<CompanyRoleType>)
        Annotations: 
          Relational:ColumnType: nvarchar(MAX)
          ValueComparer: Microsoft.EntityFrameworkCore.ChangeTracking.ValueComparer`1[System.Collections.Generic.List`1[MyNamespace.Shared.Enum.CompanyRoleType]]
          ValueConverter: Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter`2[System.Collections.Generic.List`1[MyNamespace.Shared.Enum.CompanyRoleType],System.String]
      CreatedBy (string)
      CreatedOnUTC (no field, Nullable<DateTime>)
      Enabled (bool) Required
      ExternalReference (string)
      IsDeleted (bool) Required
      ModifiedBy (string)
      ModifiedOnUTC (no field, Nullable<DateTime>)
      Name (string)
      Vat (string)
    Navigations: 
      Address (Address) ToDependent Address
        Annotations: 
          EagerLoaded: True
      Communication (Communication) ToDependent Company.Communication#Communication
        Annotations: 
          EagerLoaded: True
      Persons (ICollection<Person>) Collection ToDependent Person Inverse: Company
    Keys: 
      Id PK
    Annotations: 
      RelationshipDiscoveryConvention:NavigationCandidates: System.Collections.Immutable.ImmutableSortedDictionary`2[System.Reflection.PropertyInfo,System.Type]
  EntityType: Country
    Properties: 
      Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      CanBeTransitCountry (bool) Required
      CreatedBy (string)
      CreatedOnUTC (no field, Nullable<DateTime>)
      Enabled (bool) Required
      Iso2Code (string) Required MaxLength2
        Annotations: 
          MaxLength: 2
      Iso3Code (string) MaxLength3
        Annotations: 
          MaxLength: 3
      ModifiedBy (string)
      ModifiedOnUTC (no field, Nullable<DateTime>)
      Name (string) Required MaxLength255
        Annotations: 
          MaxLength: 255
      TerminalLocation (bool) Required
    Navigations: 
      CountryLanguages (ICollection<CountryLanguage>) Collection ToDependent CountryLanguage Inverse: Country
    Keys: 
      Id PK
    Annotations: 
      RelationshipDiscoveryConvention:NavigationCandidates: System.Collections.Immutable.ImmutableSortedDictionary`2[System.Reflection.PropertyInfo,System.Type]
  EntityType: CountryLanguage
    Properties: 
      Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      CountryId (int) Required FK Index
      CreatedBy (string)
      CreatedOnUTC (no field, Nullable<DateTime>)
      LanguageId (int) Required FK Index
      ModifiedBy (string)
      ModifiedOnUTC (no field, Nullable<DateTime>)
    Navigations: 
      Country (Country) ToPrincipal Country Inverse: CountryLanguages
      Language (Language) ToPrincipal Language Inverse: CountryLanguages
    Keys: 
      Id PK
    Foreign keys: 
      CountryLanguage {'CountryId'} -> Country {'Id'} ToDependent: CountryLanguages ToPrincipal: Country
      CountryLanguage {'LanguageId'} -> Language {'Id'} ToDependent: CountryLanguages ToPrincipal: Language
    Annotations: 
      RelationshipDiscoveryConvention:NavigationCandidates: System.Collections.Immutable.ImmutableSortedDictionary`2[System.Reflection.PropertyInfo,System.Type]
  EntityType: Language
    Properties: 
      Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      CreatedBy (string)
      CreatedOnUTC (no field, Nullable<DateTime>)
      Enabled (bool) Required
      EnglishName (string) MaxLength50
        Annotations: 
          MaxLength: 50
      IsKioskLanguage (bool) Required
      Iso2Code (string) MaxLength2
        Annotations: 
          MaxLength: 2
      Iso3Code (string) MaxLength3
        Annotations: 
          MaxLength: 3
      LocalName (string) MaxLength50
        Annotations: 
          MaxLength: 50
      ModifiedBy (string)
      ModifiedOnUTC (no field, Nullable<DateTime>)
    Navigations: 
      CountryLanguages (ICollection<CountryLanguage>) Collection ToDependent CountryLanguage Inverse: Language
    Keys: 
      Id PK
    Annotations: 
      RelationshipDiscoveryConvention:NavigationCandidates: System.Collections.Immutable.ImmutableSortedDictionary`2[System.Reflection.PropertyInfo,System.Type]
  EntityType: Person
    Properties: 
      Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      Comments (string)
      CompanyId (Nullable<int>) FK Index
      CreatedBy (string)
      CreatedOnUTC (no field, Nullable<DateTime>)
      CurrentZoneId (Nullable<int>)
      DateOfBirth (Nullable<DateTime>)
      Enabled (bool) Required
      ErpReference (string) MaxLength50
        Annotations: 
          MaxLength: 50
      FirstName (string) Required MaxLength50
        Annotations: 
          MaxLength: 50
      IsDeleted (bool) Required
      LanguageId (Nullable<int>) FK Index
      LastName (string) Required MaxLength50
        Annotations: 
          MaxLength: 50
      Locked (bool) Required
      LockedReason (string) MaxLength512
        Annotations: 
          MaxLength: 512
      LockedSince (Nullable<DateTime>)
      MiddleName (string) MaxLength50
        Annotations: 
          MaxLength: 50
      ModifiedBy (string)
      ModifiedOnUTC (no field, Nullable<DateTime>)
      VAT (string) MaxLength30
        Annotations: 
          MaxLength: 30
    Navigations: 
      Communication (Communication) ToDependent Person.Communication#Communication
        Annotations: 
          EagerLoaded: True
      Company (Company) ToPrincipal Company Inverse: Persons
      Language (Language) ToPrincipal Language
      PersonRoles (ICollection<PersonRole>) Collection ToDependent PersonRole
    Keys: 
      Id PK
    Foreign keys: 
      Person {'CompanyId'} -> Company {'Id'} ToDependent: Persons ToPrincipal: Company
      Person {'LanguageId'} -> Language {'Id'} ToPrincipal: Language
    Annotations: 
      RelationshipDiscoveryConvention:NavigationCandidates: System.Collections.Immutable.ImmutableSortedDictionary`2[System.Reflection.PropertyInfo,System.Type]
  EntityType: PersonRole
    Properties: 
      PersonId (int) Required PK FK AfterSave:Throw
      RoleTypeId (PersonRoleTypes) Required PK AfterSave:Throw
      CreatedBy (string)
      CreatedOnUTC (no field, Nullable<DateTime>)
      EndDate (Nullable<DateTime>)
      ModifiedBy (string)
      ModifiedOnUTC (no field, Nullable<DateTime>)
      StartDate (Nullable<DateTime>)
    Keys: 
      PersonId, RoleTypeId PK
    Foreign keys: 
      PersonRole {'PersonId'} -> Person {'Id'} ToDependent: PersonRoles
    Annotations: 
      RelationshipDiscoveryConvention:NavigationCandidates: System.Collections.Immutable.ImmutableSortedDictionary`2[System.Reflection.PropertyInfo,System.Type]
  EntityType: Company.Communication#Communication
    Properties: 
      CompanyId (no field, int) Shadow Required PK FK AfterSave:Throw ValueGenerated.OnAdd
      EmailAddress (string)
      EmergencyNumber (string)
      FaxNumber (string)
      LandLine2 (string)
      Landline1 (string)
      LinkedInUrl (string)
      MobileNumber (string)
      SkypeAddress (string)
      WebsiteUrl (string)
    Keys: 
      CompanyId PK
    Foreign keys: 
      Company.Communication#Communication {'CompanyId'} -> Company {'Id'} Unique Ownership ToDependent: Communication
    Annotations: 
      RelationshipDiscoveryConvention:NavigationCandidates: System.Collections.Immutable.ImmutableSortedDictionary`2[System.Reflection.PropertyInfo,System.Type]
  EntityType: Person.Communication#Communication
    Properties: 
      PersonId (no field, int) Shadow Required PK FK AfterSave:Throw ValueGenerated.OnAdd
      EmailAddress (string)
      EmergencyNumber (string)
      FaxNumber (string)
      LandLine2 (string)
      Landline1 (string)
      LinkedInUrl (string)
      MobileNumber (string)
      SkypeAddress (string)
      WebsiteUrl (string)
    Keys: 
      PersonId PK
    Foreign keys: 
      Person.Communication#Communication {'PersonId'} -> Person {'Id'} Unique Ownership ToDependent: Communication
    Annotations: 
      RelationshipDiscoveryConvention:NavigationCandidates: System.Collections.Immutable.ImmutableSortedDictionary`2[System.Reflection.PropertyInfo,System.Type]
Annotations: 
  NonNullableConventionState: Microsoft.EntityFrameworkCore.Metadata.Conventions.NonNullableConventionBase+NonNullabilityConventionState
  OwnedTypes: System.Collections.Generic.Dictionary`2[System.String,Microsoft.EntityFrameworkCore.Metadata.ConfigurationSource]
  ProductVersion: 3.1.1
  Relational:DefaultSchema: AddressBook
  Relational:MaxIdentifierLength: 128
  SqlServer:ValueGenerationStrategy: IdentityColumn

Further technical details

EF Core version: 3.1.1 Database provider: Microsoft.EntityFrameworkCore.SqlServer Target framework: .NET Core 3.1 Operating system: Windows 10 IDE: Rider

smitpatel commented 4 years ago

Initial observation - Address is owned entity (only Company has Address). Communication is weak entity (Person.Communication & Company.Communication)

ajcvickers commented 4 years ago

@sylvaingirardbe To add a little more to what @smitpatel said it's probably worth saying a bit about when DefiningEntityType is needed. For a given CLR type, if that CLR type is only owned once by one owning entity type, then there is still a 1:1 mapping between CLR type and conceptual entity type. This is the case for Address--it is owned only once by Company. This is also the normal case for non-owned entity types.

However, if the CLR type is owned more than once, then there needs to be a different conceptual entity type in the model for each way the CLR type is used. DefiningEntityType is part of the mechanism that differentiates these different conceptual entity types from each other. This is the case for Communication--it is owned once by Person and once by Company.

sylvaingirardbe commented 4 years ago

So it’s correct to say that DefiningEntityType Is not a reliable way to determine if a type is owned? Is there a reliable way except for doing some reflection to get that attribute?

smitpatel commented 4 years ago

entityType.HasDefiningNavigation() || entityType.IsOwned()

AndriySvyryd commented 4 years ago

entityType.IsOwned() is enough