Search code examples
c#sqliteasp.net-coreentity-framework-core

EntityFramework Core Computed Property Error: InvalidOperation No Backing field


I'm currently making a web app using .net core 2.2 and EntityFramework.

I have a model which has 2 computed properties "AgeYears" and "AgeMonths" these properties are calculated based on the property "Birthday" by a class library.

When I run the web app, and there is a fetch which includes this model, an error is thrown:

System.InvalidOperationException: No backing field could be found for property 'AgeMonths' of entity type 'HorseModel' and the property does not have a setter.

I've tried to figure out what's going on by reading through a couple of articles one of which is on the Microsoft website but just returns a simple concatenation of FirstName and LastName (FullName) and the other didn't seem to be up to date as there were attributes that simply were not available e.g. [computed]. Usually, I wouldn't bother with the properties, and simply do the calculation in the view or page model, but stupidly thought this would be a better/cleaner idea!?

Here's the part of the model causing problems:

    public DateTime Birthday { get; set; }
    [NotMapped]
    public int AgeYears
    {
        get
        {
            return DateTimeSpan.CompareDates(DateTime.Now, Birthday).Years;
        }
    }
    [NotMapped]
    public int AgeMonths
    {
        get
        {
            return DateTimeSpan.CompareDates(DateTime.Now, Birthday).Months;
        }
    }

I tried deleting the database and migrations, and upon attempting to run the initial migration again the error showed up, despite adding [NotMapped] to the properties.

What I was expecting to happen, is that when a record is fetched from the database, these properties are calculated and thought there would be no need to store them in the database. Clearly, I'm doing something wrong, and I assume its related to using my own functions for the calculation.

To make it work I've added a backing field as the error suggested I do, and when I look at the migration, I notice there are still columns being generated for the "NotMapped" values.

        migrationBuilder.CreateTable(
            name: "Horses",
            columns: table => new
            {
                Id = table.Column<int>(nullable: false)
                    .Annotation("Sqlite:Autoincrement", true),
                Name = table.Column<string>(nullable: true),
                Birthday = table.Column<DateTime>(nullable: false),
                AgeYears = table.Column<int>(nullable: false),
                AgeMonths = table.Column<int>(nullable: false),
                Breed = table.Column<string>(nullable: true),
                Height = table.Column<decimal>(nullable: false),
                About = table.Column<string>(nullable: true),
                ForSale = table.Column<bool>(nullable: false),
                Private = table.Column<bool>(nullable: false),
                OwnerId = table.Column<string>(nullable: true),
                Created = table.Column<DateTime>(nullable: false),
                LastEdited = table.Column<DateTime>(nullable: false),
                PurhaseDate = table.Column<DateTime>(nullable: false),
                Sold = table.Column<DateTime>(nullable: false),
                Slug = table.Column<string>(nullable: true),
                OwnershipStatus = table.Column<int>(nullable: false)
            },...

Now however AgeYears and AgeMonths don't get calculated when they are fetched!

The final model that I came up with is:

public class HorseModel
{
    private int _ageYears;
    private int _ageMonths;

    public int Id { get; set; }
    public string Name { get; set; }
    public DateTime Birthday { get; set; }
    [NotMapped]
    public int AgeYears
    {
        get
        {
            return _ageYears;
        }
        set
        {
            _ageYears = DateTimeSpan.CompareDates(DateTime.Now, Birthday).Years;
        }
    }
    [NotMapped]
    public int AgeMonths
    {
        get
        {
            return _ageMonths;
        }
        set
        {
            _ageMonths = DateTimeSpan.CompareDates(DateTime.Now, Birthday).Months;
        }
    }
    public string Breed { get; set; }
    public decimal Height { get; set; }
    public string About { get; set; }
    public bool ForSale { get; set; }
    public bool Private { get; set; }
    [ForeignKey("Owner")]
    public string OwnerId { get; set; }
    public ApplicationUser Owner { get; set; }
    public List<HorsePicture> Pictures { get; set; }
    public List<HorseYouTubeVideo> Videos { get; set; }
    public DateTime Created { get; set; }
    public DateTime LastEdited { get; set; }
    public DateTime PurhaseDate { get; set; }
    public DateTime Sold { get; set; }
    public HorseProfilePicture ProfilePicture { get; set; }
    public string Slug { get; set; }
    public OwnershipStatus OwnershipStatus { get; set; }
}

Any guidance would be very much appreciated.

Thanks :)


Solution

  • Decided to circle back to this as I eventually found the resolution to the very similar problem I was having - the problem of a field in an entity class requiring a conversion. Mine was slightly different in that my backing field was a different type (an enum), with the public getter and setter converting to and from string.

    TLDR - the the fix for the original problem should be to change AgeYears & AgeMonths from properties with getters, to simple methods e.g. public int GetAgeYears() & public int GetAgeMonths();

    Note, having conversions in field getters (or setters) is problematic as EF3 will change behaviour and by default write directly to backing fields, skipping any of the conversions. Oops!

    ...

    Here is where I was originally with the error thrown by EF, my first instinct was to just create an empty public setter. I know this sounds dumb but it actually worked - I was using the class with a query that including grouping, so I was calling the results from a stored procedure and all the data loaded as expected.

    set
    {
        // Required by EF
    }
    

    When I switched to LINQ to test local evaluation of the grouping the empty setter did it's job perfectly and EF returned objects with default values on that property!

    Take 2, I then just used a public setter which called the conversion

    private Status _status;
    
    public string Status
    {
        get { return _status; }
        private set {
            _status = ParseRag(value);
        }
    }
    

    I even had a little helper method in the class to do the conversion, hard-coded to that particular enun: oh man!

    public static RagStatus ParseRag(string value)
    {
        Status rag;
        Enum.TryParse(value, out rag);
        return rag;
    }
    

    Take 3, slightly better after realising how lame this was.

    My entity field is still an Enum.

    private Status _status;
    

    It now has just a straightforward public getter with no conversion.

    public Status Status
    {
        get { return _status; }
    }
    

    If I really need to read it as string I can call GetStatus()

    public string GetStatus()
    {
        return Status.ToString();
    }
    

    Have created a separate class as EnumHelper.cs in an ExtensionMethods folder, and the code is generic and not tied to a specific enum.

    public static class EnumHelper
    {
        public static T ConvertToEnum<T>(this string value)
        {
            if (Enum.IsDefined(typeof(T), value)) {
                return (T)Enum.Parse(typeof(T), value);
            }
            else {
                throw new ArgumentException($"{value} is not of the expected Enum");
            }
        }
    
        public static bool IsEnum<T>(this string value)
        {
            if (Enum.IsDefined(typeof(T), value)) {
                return true;
            }
            else {
                return false;
            }
        }
    }
    

    Finally in the dbcontext I specify the conversion as follows, and EF can find the backing field name by convention: read as string, and to write as an enum using the extension method.

    entity.Property(e => e.Status)
        .HasColumnName("Status")
        .HasMaxLength(255)
        .IsUnicode(false)
        .HasConversion(
            v => v.ToString(),
            v => v.ConvertToEnum<Status>());
    

    Naturally I need to keep in mind this dependency when I run unit tests for my dbcontext. EnumHelper is a static class so not suitable to define as a service - that's another can of worms!

    More info EF Core Backing fields - expose property as another type?

    And here https://learn.microsoft.com/en-us/ef/core/modeling/value-conversions

    And more about enum conversions here Cast int to enum in C#

    Hope this is some use to... somebody!