efcore icon indicating copy to clipboard operation
efcore copied to clipboard

How to store a conditional/calculated property in ef core 6

Open mdhthahmd opened this issue 2 years ago • 3 comments

Hello,

I have the following entities


public class Parent
{
    public int Id { get; set; }
    public string Name { get; set; }
    private readonly List<Child> _children;
    public IReadOnlyCollection<Child> Children => _children;
    public Status Status { get; private set; }
    private int _statusId;
}

public class Child
{
    private string _description;
    private bool _isValid;
    // other data fields
    public bool IsValid() 
    {
        return _isValid == true;
    }

     protected Child() { }
}


public class Status : Enumeration
{
   // Enumeration is from https://github.com/dotnet-architecture/eShopOnContainers/blob/dev/src/Services/Ordering/Ordering.Domain/SeedWork/Enumeration.cs

    public static Status Pending = new(1, nameof(Pending));
    public static Status InValid = new(2, nameof(InValid));
    public static Status Valid = new(3, nameof(Valid));

    public Status(int id, string name)
        : base(id, name)
    { }

      public static IEnumerable<Status> List() =>
        new[] { Pending, InValid, Valid };

}

My question is how to configure the Status property and the _statusId in Parent Entity such that,

When _children collection is empty Status needs to be set to Pending, or When all objects in _children collection returns true from IsValid() method, Status needs to be set to Valid, else Status needs to be set to InValid.

Basically the Status is set from the state of the _children collection

Regards,

mdhthahmd avatar Jan 24 '23 09:01 mdhthahmd

You could simply make Status a regular C# property which looks at Children and and calculates the return value from that. Configure that property as unmapped in order to make sure it isn't persisted to the database.

roji avatar Jan 24 '23 11:01 roji

You could simply make Status a regular C# property which looks at Children and and calculates the return value from that. Configure that property as unmapped in order to make sure it isn't persisted to the database.

Thanks for the quick reply,

I did consider this, but in the database i need to store the statuses, currently my entity config for parent looks like the following

public class ParentEntityTypeConfiguration : IEntityTypeConfiguration<Parent>
{
    public void Configure(EntityTypeBuilder<Parent> parentConfiguration)
    {
        parentConfiguration.ToTable("parents", AppDbContext.DEFAULT_SCHEMA);

        parentConfiguration.HasKey(t => t.Id);
        parentConfiguration.Property(t => t.Id)
            .HasColumnName("id")
            .UseHiLo("parentseq", AppDbContext.DEFAULT_SCHEMA);

        parentConfiguration.Property(t => t.Name)
            .HasColumnName("name")
            .HasMaxLength(64)
            .IsRequired();

        parentConfiguration.Property<int>("_statusId")
            .UsePropertyAccessMode(PropertyAccessMode.Field)
            .HasColumnName("status_id")
            .IsRequired();

        parentConfiguration.HasOne(o => o.Status)
            .WithMany()
            .HasForeignKey("_statusId");

        var navigation = parentConfiguration.Metadata.FindNavigation(nameof(Parent.Children));
        navigation?.SetPropertyAccessMode(PropertyAccessMode.Field);
    }
}

So is there a way to make the status logic work while also mapping the status in ef core?

I did try the following,

 public Status Status => _children.Count() == 0 
        ? Status.Pending
        : (_children.All(a => a.IsValid()) ? Status.Valid : Status.InValid);
 
 private int _statusId;

but then ef fails the migration with the error No backing field could be found for property 'Parent.Status' and the property does not have a setter.

mdhthahmd avatar Jan 24 '23 11:01 mdhthahmd

Duplicate of #13316. For now, use an empty setter as shown in that issue.

ajcvickers avatar Jan 25 '23 19:01 ajcvickers

thanks for the tip @ajcvickers, have the following getter and setter for the field,

public Status Status
{
    get
    {
        return _children.Count() == 0 
        ? Status.Pending
        : (_children.All(a => a.IsValid()) ? Status.Valid : Status.InValid);
    }
    private set { }
}
 
private int _statusId;

and it is working fine for the most part, that is, the first db row insert to parent table works, but any consequent inserts fails with the following exception Microsoft.Data.SqlClient.SqlException (0x80131904): Violation of PRIMARY KEY constraint 'PK_status'. Cannot insert duplicate key in object 'dbo.status'. The duplicate key value is (1) As far as i know, ef core is trying to insert the status for each parent save, but i am unsure how to prevent that.

mdhthahmd avatar Jan 28 '23 13:01 mdhthahmd

@mdhthahmd This issue is lacking enough information for us to be able to fully understand what is happening. Please attach a small, runnable project or post a small, runnable code listing that reproduces what you are seeing so that we can investigate.

ajcvickers avatar Jan 29 '23 19:01 ajcvickers

Hi, @ajcvickers i have created a repro as instructed, i hope this provides a full picture. Hope this clarifies the issue.

Regards

mdhthahmd avatar Feb 02 '23 20:02 mdhthahmd

@mdhthahmd The Status.Id is configured as .ValueGeneratedNever() and always has the value of 1 when inserting.

ajcvickers avatar Feb 14 '23 10:02 ajcvickers