user3337383
user3337383

Reputation: 223

Store computed property with Entity Framework Core

I'll try and illustrate my question with an oversimplified example: Imagine I have a domain entity like this:

public class Box
{
    public int Id { get; set; }
    public int Height { get; set; }
    public int Width { get; set; }
    public int Depth { get; set; }
    public int Volume => Height * Width * Depth;
}

Where I'm doing a calculation (volume) based on every other property. Now say I want to store this class using Entity Framework Core. Is there any way I can make EF core store the current value of Volume in its own column when i persist the entity?

That's my core question. I'm not allowed to share my actual code, but here's some more in-depth information about my real-world entity(let's call it "Box"):

Some ideas on how to solve this:

  1. Completely hydrate every Box, and calculate the volume for each and THEN project this to a summary. This includes joining all seven tables for each box with sub-entities, doing the calculation for each box and projecting to simplified view models. To me this just seems like a lot of overhead to get a number i knew at the time of persisting.
  2. Create a "persistence DTO" for Box and all sub-entities and then just map the result of Volume to a volume auto-property on the dto when storing. This also seems like a lot of overhead for just storing one number, and It also seems to be in complete discord with how EF is supposed to work. I just want to persist my entities.
  3. I could go proper OO on Box and create private fields and proper setters for every property, that updates a private volume field whenever a setter is called. This would also include writing methods on Box for manipulating all collections of sub-entities, and presenting these as read-only collections. This would lead to a lot of overhead private fields and code duplication on every setter, but does seem more "prudent" than the above alternatives.
  4. I could turn Volume in to a CalculateVolume() method and create a Volume-property using fluent API, and then populate that property in a SaveChanges() override on the context. But overriding SaveChanges is the kind of EF gung-ho I'm not comfortable doing.
  5. I Could to something like this:
public class Box
{
    public int Id { get; set; }
    public int Height { get; set; }
    public int Width { get; set; }
    public int Depth { get; set; }
    public int Volume {
        get => CalculateVolume(); 
        private set => _volume = value; }
    private int _volume;
    private int CalculateVolume() => Height * Width * Depth;
}

Which does seem to do what I want, but for some reason feels like cheating, and polluting my domain entity. Also I'm unsure this actually works in all cases, but this is my preferred solution at the time of writing.

I'd prefer to be able to just configure this using fluent API. I noticed the PropertyBuilder.ValueGeneratedOnAdd()-method description says "The value may be generated by a client-side value generator or may be generated by the database as part of saving the entity.", but I can't find any examples of client-side value-generation.

Any and all reasonable feedback welcome.

EDIT: Just to clarify: The actual calculation is pretty complex and uses values from 7 different tables. There's also a weighting of each property involved. The Box example at the start is over simplified and for explanation purposes only. Suffice to say, I need to keep the calculation in my code. I just want to store the result.

Upvotes: 22

Views: 19011

Answers (4)

falt86
falt86

Reputation: 133

Adding an answer, as this post is still the top result from google, and there is no answer to the original question. EF Core requires a setter to write a value to the db. Any workaround will just add complexity that you don't want. But you can add a discard setter, as to avoid needing a backing field.

public class Box
{
    public int Id { get; set; }
    public int Height { get; set; }
    public int Width { get; set; }
    public int Depth { get; set; }
    public int Volume { get => Height * Width * Depth; set => _ = value; }
}

Upvotes: 7

Arad
Arad

Reputation: 12871

For people who don't want to store computed values in the database and want to incorporate them into queries and also write them in C# (as opposed to raw SQL), you can use libraries like EntityFrameworkCore.Projectables to achieve that kind of thing:

Firs, you declare your computed properties marked with the [Projectable] attribute — let's use OP's example:

public class Box
{
    public int Id { get; set; }
    public int Height { get; set; }
    public int Width { get; set; }
    public int Depth { get; set; }
    [Projectable] public int Volume => Height * Width * Depth;
}

And then you'll be able to use those properties in any expression sent to EF Core (i.e. in your queries):

var result = db.Boxes
    .Select(b => new
    {
        b.Volume,
    })
    .ToList();

For more information, see the project's repository.

Upvotes: 4

McKabue
McKabue

Reputation: 2232

Below is the response I got from EF guys for the same problem:

Starting with EF Core 3.0, EF reads and writes directly to the backing field, where possible. EF can be configured to use the property instead, at which point the computed value will be read from the property and hence written to the database:

 protected override void OnModelCreating(ModelBuilder modelBuilder)
 {
     modelBuilder
         .Entity<Box>()
         .Property(e => e.Volume)
         .UsePropertyAccessMode(PropertyAccessMode.Property);
 }

or

modelBuilder.UsePropertyAccessMode(PropertyAccessMode.PreferFieldDuringConstruction);

Read more: https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-3.0/breaking-changes#backing-fields-are-used-by-default

Upvotes: 16

hawkstrider
hawkstrider

Reputation: 4341

You can use fluent api to compute it on sql server

class MyContext : DbContext
{
    public DbSet<Box> Box { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Box>()
            .Property(p => p.Volume)
            .HasComputedColumnSql("[Height] * [Width] * [Depth]");
    }
}

Upvotes: 7

Related Questions