Skip to content

EF core 6 returns null value for owned entity #27516

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Closed
vsfeedback opened this issue Feb 28, 2022 · 5 comments
Closed

EF core 6 returns null value for owned entity #27516

vsfeedback opened this issue Feb 28, 2022 · 5 comments

Comments

@vsfeedback
Copy link

This issue has been moved from a ticket on Developer Community.


[severity:It's more difficult to complete my work] [regression] [worked-in:>Net Core 5, EF Core 5]
I recently upgraded my project to .Net Core 6, EF Core 6. I started getting this error message when I create a new migration or when I update-database:

Microsoft.EntityFrameworkCore.Model.Validation[20606]
The entity type 'Clinician.Name#PersonalName' is an optional dependent using table sharing without any required non shared property that could be used to identify whether the entity exists. If all nullable properties contain a null value in database then an object instance won't be created in the query. Add a required property to create instances with null values for other properties or mark the incoming navigation as required to always create an instance.

Ok, PersonalName is an owned entity consisting entirely of string properties. So I added a new property to the owned entity class:

public class PersonalName
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

// A non-nullable property to force owned entities to be created
    public bool NotUsed { get; set; }
}

That made the validation error go away, but now the property that should hold the owned entity is null, even though the SQL database has non-null values for many of the owned entity columns.

I have noticed a couple of (possibly) interesting things about this problem:

  1. If I remove the public bool NotUsed { get; set; } property then the query correctly returns the non-null owned entity.
  2. The new NotUsed Boolean column is nullable, despite the fact that it is a non-nullable data type and the parent entity does not inherit from a base entity class. Adding a [Required] attribute in the data model or fluent configuration in the DbContext has no effect on this.

Original Comments

Feedback Bot on 2/25/2022, 06:34 AM:

We have directed your feedback to the appropriate engineering team for further evaluation. The team will review the feedback and notify you about the next steps.


Original Solutions

(no solutions)

@ajcvickers
Copy link
Contributor

The new NotUsed Boolean column is nullable

How was the column created? EF Core Migrations, or some other way?

@IndigoHealth
Copy link

The new NotUsed Boolean column is nullable
How was the column created? EF Core Migrations, or some other way?

Hi @ajcvickers, not sure I understand the question. This is code-first. I added the NotUsed boolean property to several owned entity classes in my data model, then I ran add-migration to create new migration. The scaffolded migration code adds a new nullable column for every parent entity's reference to one of the owned entities. When I update-database, the database gets the new nullable property, which contains the default NULL value in the database. And, after updating the database, EF returns null rather than a populated "owned" entity for the various owned entity properties.

I'm being lazy... I haven't spent the time to try to create a simple repo of the problem. I'm crossing my fingers and hoping that someone at your end will look at it and say "oh, yea, that's a reproduceable bug".

@ajcvickers
Copy link
Contributor

@sbsw Thanks; I will discuss with the team.

@IndigoHealth
Copy link

IndigoHealth commented Feb 28, 2022

And, if it helps... here's the actual (full) code for the owned entity:

    #region [Owned] public class PersonalName

    //[Owned]
    public class PersonalName : IPersonalName
    {
        #region Constructors

        /// <summary> Default (parameterless) constructor </summary>
        public PersonalName()
        { }

        /// <summary> Initializes a new <see cref="PersonalName"/> with data from an <see cref="IPersonalName"/> instance. </summary>
        public PersonalName(IPersonalName name)
        {
            if (name != null) {
                PersonalTitle = name.PersonalTitle;
                FirstName = name.FirstName;
                MiddleName = name.MiddleName;
                LastName = name.LastName;
                LastNameSuffix = name.LastNameSuffix;
                PostNominalLetters = name.PostNominalLetters;
            }
        }

        #endregion

        #region IPersonalName interface

        [MaxLength(10)]
        public string PersonalTitle { get; set; }
        public string FirstName { get; set; }
        public string MiddleName { get; set; }
        public String LastName { get; set; }
        [MaxLength(10)]
        public String LastNameSuffix { get; set; }
        [MaxLength(20)]
        public string PostNominalLetters { get; set; }

        // Utility routines

        /// <summary> Returns the first name if it exists, otherwise returns the last name (e.g. "Bob"). </summary>
        public string FirstOrOnlyName => PersonalNameHelpers.GetFirstOrOnlyName(this);

        /// <summary> Returns the informal name (e.g. "John Smith"). </summary>
        public string InformalName => PersonalNameHelpers.GetInformalName(this);

        /// <summary> Returns the full name (e.g. "Dr. John Smith, MD"). </summary>
        public string FullName => PersonalNameHelpers.GetFullName(this);

        /// <summary> Returns the formal name (e.g. "John Smith, MD"). </summary>
        public string FormalName => PersonalNameHelpers.GetFormalName(this);

        /// <summary> Returns the professional name (e.g. "Dr. Bob Smith" or, if no personal title, "Bob Smith"). </summary>
        public string ProfessionalFullName => PersonalNameHelpers.GetProfessionalFullName(this);

        /// <summary> Returns the professional last name (e.g. "Dr. Smith" or, if no personal title, "Bob"). </summary>
        public string ProfessionalShortName => PersonalNameHelpers.GetProfessionalShortName(this);

        /// <summary> Returns true if the first and last names are both null. </summary>
        public bool IsNull() => PersonalNameHelpers.GetIsNull(this);


        #endregion
        #region Other properties

        public bool NotUsed { get; set; }   // A non-nullable property to force owned entities to be created

        #endregion
        #region IEquatable interface (required by IPersonalName)

        /// <summary> Returns true if this instance is "equal" to another instance. </summary>
        /// <param name="other"> The instance to compare. </param>
        public bool Equals(IPersonalName other)
        {
            return PersonalNameHelpers.GetEquals(this, other);
        }

        /// <summary> Must also override Object.Equals; see https://stackoverflow.com/a/2734941/1637105 </summary>
        public override bool Equals(object obj)
        {
            if (obj is not PersonalName other) return false;
            return this.Equals(other);
        }

        /// <summary> Must also override Object.GetHashCode; see See https://blogs.msdn.microsoft.com/jaredpar/2008/06/03/making-equality-easier </summary>
        public override int GetHashCode() => 1;

        #endregion
    }

And the interface:

    public interface IPersonalName : IEquatable<IPersonalName>
    {
        string PersonalTitle { get; set; }
        string FirstName { get; set; }
        string MiddleName { get; set; }
        string LastName { get; set; }
        string LastNameSuffix { get; set; }
        string PostNominalLetters { get; set; }

        string FirstOrOnlyName { get; }
        string InformalName { get; }
        string FullName { get; }
        string FormalName { get; }
        string ProfessionalFullName { get; }
        string ProfessionalShortName { get; }

        bool IsNull();
    }

    public static class PersonalNameHelpers
    {
        /// <summary> Returns the first name if it exists, otherwise returns the last name (e.g. "Bob"). </summary>
        public static string GetFirstOrOnlyName(IPersonalName self)
        {
            Contract.Requires(self != null);
            return (!string.IsNullOrWhiteSpace(self.FirstName)) ? self.FirstName : self.LastName;
        }

        /// <summary> Returns the informal name (e.g. "Bob Smith Jr."). </summary>
        public static string GetInformalName(IPersonalName self)
        {
            Contract.Requires(self != null);
            return $"{self.FirstName} {self.LastName} {self.LastNameSuffix}".Collapse();
        }

        /// <summary> Returns the full name (e.g. "Dr. Bob Smith, MD"). </summary>
        public static string GetFullName(IPersonalName self)
        {
            Contract.Requires(self != null);

            var t = $"{self.PersonalTitle} {self.FirstName} {self.MiddleName} {self.LastName} {self.LastNameSuffix}".Trim();
            return (t + _PostNominalLetters(self)).Collapse();
        }

        /// <summary> Returns the formal name (e.g. "Bob Smith, MD" or "Dr. Bob Smith"). </summary>
        public static string GetFormalName(IPersonalName self)
        {
            Contract.Requires(self != null);

            var pnl = _PostNominalLetters(self);
            if (string.IsNullOrEmpty(pnl) || string.IsNullOrWhiteSpace(self.PersonalTitle)) {
                // "Dr. Bob Smith" or "Bob Smith"
                return GetFullName(self);
            } else {
                // "Bob Smith, MD"
                var t = $"{self.FirstName} {self.LastName} {self.LastNameSuffix}".Trim();
                return (t + pnl).Collapse();
            }
        }

        public static string GetProfessionalFullName(IPersonalName self) {
            Contract.Requires(self != null);
            return $"{self.PersonalTitle} {self.FirstName} {self.LastName} {self.LastNameSuffix}".Collapse();
        }

        /// <summary> Returns the professional name (e.g. "Dr. Smith" or, if no personal title, "Bob"). </summary>
        public static string GetProfessionalShortName(IPersonalName self)
        {
            Contract.Requires(self != null);

            if (self.PersonalTitle == null) {
                // No personal title; return the first name
                return GetInformalName(self);
            } else {
                // Return the personal title + the last name
                return self.PersonalTitle + " " + (self.LastName ?? self.FirstName);
            }
        }

        /// <summary> Returns true if two IPersonalName instances are equal. </summary>
        /// <param name="self"> An IPersonalName to test. </param>
        /// <param name="other"> The other IPersonalName to test. </param>
        public static bool GetEquals(IPersonalName self, IPersonalName other)
        {
            // Argument validation
            Contract.Requires(self != null);
            Contract.Requires(other != null);

            // Equality means that all mutable properties are all equal
            return
                self.PersonalTitle.Equals(other.PersonalTitle, StringComparison.OrdinalIgnoreCase) &&
                self.FirstName.Equals(other.FirstName, StringComparison.OrdinalIgnoreCase) &&
                self.LastName.Equals(other.LastName, StringComparison.OrdinalIgnoreCase) &&
                self.LastNameSuffix.Equals(other.LastNameSuffix, StringComparison.OrdinalIgnoreCase) &&
                self.PostNominalLetters.Equals(other.PostNominalLetters, StringComparison.OrdinalIgnoreCase);
        }

        /// <summary> Returns true if the first and last names are both null. </summary>
        public static bool GetIsNull(IPersonalName self)
        {
            Contract.Requires(self != null);
            return self.FirstName == null && self.LastName == null;
        }

        /// <summary> Returns the person's initials. </summary>
        public static string GetInitials(IPersonalName self, CultureInfo culture = null)
        {
            Contract.Requires(self != null);

            // Get the CultureInfo
            if (culture == null) culture = CultureInfo.CurrentCulture;

            var sb = new StringBuilder();
            if (!string.IsNullOrWhiteSpace(self.FirstName)) sb.Append(self.FirstName.TrimStart()[..1].ToUpper(culture));
            if (!string.IsNullOrWhiteSpace(self.LastName)) sb.Append(self.LastName.TrimStart()[..1].ToUpper(culture));
            if (sb.Length == 0) sb.Append("xx");

            return sb.ToString();
        }

        /// <summary> Returns the post-nominal letters, including a leading comma. </summary>
        private static string _PostNominalLetters(IPersonalName self)
        {
            Contract.Requires(self != null);

            var letters = new StringBuilder();
            if (!string.IsNullOrWhiteSpace(self.PostNominalLetters)) letters.Append(", ").Append(self.PostNominalLetters.Trim());
            return letters.ToString();
        }
    }

And the fluent configuration

            // Clinician
            builder.Entity<Clinician>(e => {
                e.OwnsOne(c => c.Name, x => {  // Clinician.Name is a PersonalName
                    x.HasIndex(pn => pn.LastName);
                });

                e.OwnsOne(c => c.Address);
                e.OwnsOne(c => c.TimeZone);
                e.OwnsOne(c => c.Culture);

                e.Property(c => c.PatientOnboardingRatingScales).HasListConversion();
            });

@ajcvickers
Copy link
Contributor

Duplicate of #25359. See also dotnet/EntityFramework.Docs#3750.

@ajcvickers ajcvickers reopened this Oct 16, 2022
@ajcvickers ajcvickers closed this as not planned Won't fix, can't repro, duplicate, stale Oct 16, 2022
# for free to join this conversation on GitHub. Already have an account? # to comment
Projects
None yet
Development

No branches or pull requests

3 participants