Mapping to Getter-only Properties with EF Core

One of the new features of Entity Framework Core 1.1 is that it allows mapping to fields. This is extremely useful when properties only have a get accessor. Previously, with properties get and set accessors were required. This article shows how you can implement mapping to fields using EF Core 1.1.

Golden Field

Creating a Model

Let’s start with a model. The type Book defines the properties BookId, Title, and Publisher. Because the book identifier and the publisher shouldn’t be changed after instantiating of the object, properties with only get accessors are defined. The properties are backed by private fields. Private members can only be accessed from within the class. Of course there’s an exception: EF Core needs to set these fields as well.

The Title property contains get and set accessors, there needs to be a way to change the title after the book is instantiated. I’m writing books about Microsoft technologies, and as it happens Microsoft changes product names after release candidates, so the book title changes as well 😉

public class Book
{
    private Book()
    {
    }
    public Book(string title, string publisher)
    {
        Title = title;
        _publisher = publisher;
    }
    private int _bookId = 0;
    public int BookId => _bookId;

    public string Title { get; set; }

    private string _publisher;
    public string Publisher => _publisher;
}

The entity type still needs a default constructor to be used with EF Core, but this constructor can be declared with the private accessor.

Creating the Context

With the context you need to map the properties with only get accessors to the field. This is done with the HasField method of the PropertyBuilder.

public class BooksContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer("server=(localdb)\mssqllocaldb;database=BooksSample;trusted_connection=true");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Book>().Property(b => b.BookId).HasField("_bookId");
        modelBuilder.Entity<Book>().Property(b => b.Publisher).HasField("_publisher");
    }
    public DbSet<Book> Books { get; set; }
}

Using the Context

With this in place, the EF Core context can be used as expected. Creating the Book object, the custom constructor can be used to fill in the title and publisher. Reading Book objects from the database, EF Core instantiates the Book object using the default constructor and directly sets the fields.

using (var context = new BooksContext())
{
    context.Database.EnsureCreated();
    context.Books.Add(new Book("Professional C# 6 and .NET Core 1.0", "Wrox Press"));
    context.SaveChanges();
}
using (var context = new BooksContext())
{
    foreach (var book in context.Books)
    {
        Console.WriteLine($"{book.BookId} {book.Title} {book.Publisher}");
    }
}

You might wonder if readonly fields can be used as well. EF Core is accessing private fields from the outside, what about readonly fields? This would also allow using the new C# 6 syntax for readonly properties. I’Ve checked it – and no, readonly fields are not allowed for mapping. Fields need to be writeable for the mapping.

Mapping Fields without Properties

With this new feature to map columns to fields, it is also possible to not use any properties, and just to map columns from a table to fields. To demonstrate this, I extended the Bookclass with a private field _internalState. There’s no property that allows accessing this field. The field is set within the constructor, and retrieved from the ToString method.

public class Book
{
    private Book()
        : this(string.Empty, string.Empty)
    { }

    private string _internalState = string.Empty;
    public Book(string title, string publisher)
    {
        Title = title;
        _publisher = publisher;
        _internalState = "initialized";
    }
    private int _bookId = 0;
    public int BookId => _bookId;

    public string Title { get; set; }

    private string _publisher;
    public string Publisher => _publisher;

    public override string ToString() =>
        $"{Title}, {Publisher}, internal state: {_internalState}";
}

The OnModelCreating method of the db context is extended calling the method Property on the EntityTypeBuilder using the string JustABackingField. A property with such a name does not exist within the Book type. The values are stored in the database and map to a column with the name JustABackingField. Using the HasField method, this column maps to the field _internalState.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Book>().Property(b => b.BookId).HasField("_bookId");
    modelBuilder.Entity<Book>().Property(b => b.Publisher).HasField("_publisher");

    modelBuilder.Entity<Book>().Property<string>("JustABackingField").HasField("_internalState");
}

The syntax to map fields without properties to columns is the same as for shadow properties that are possible with EF Core since the first version. Contrary to properties, now there’s a representation within the model.

Running the application, you can see a database table created with the column JustABackingField that has a value from the field _internalState.

Backing Field

To access the field that is mapped to a database column from outside of the model type, you can use the ChangeTracker. This is similar to using shadow properties.

Getting rid of the Field Names

Mapping to fields now allows using getter only properties, and its also possible to use fields without properties. What’s not so nice is that the name of private fields are used outside of the class. If the field name is changed, this leads to a breaking change. Because the field is private, the nameof expression from C# 6 cannot be used in this case. Changing the field name and not the name in the mapping of the model, the compiler compiles successfully, but during runtime an exception is thrown. However, there’s a way around this. EF Core offers even better features mapping columns to fields.

Instead of using the HasField method of the PropertyBuilder, the following code snippet uses the UsePropertyAccessMode method to specify to use a field during construction. Possible values of the PropertyAccessMode enumeration are Field to always use a field, both for read and write access, FieldDuringConstruction where a property is used for read access, and the field for write access, and Property to always use properties. The name of the field is retrieved from the property accessor.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Book>().Property(b => b.Publisher).UsePropertyAccessMode(PropertyAccessMode.FieldDuringConstruction);
    modelBuilder.Entity<Book>().Property(b => b.BookId).UsePropertyAccessMode(PropertyAccessMode.FieldDuringConstruction);
}

Summary

Entity Framework Core 1.1 gets some enhancements, and one of the really great enhancements are the mapping to fields. This allows creating a model with getter-only properties. Being afraid of making breaking changes too easily as the mapping might reference private field names, it is also possible to let EF Core find out the field name.

Sample Code

The sample code is available at GitHub. To build the sample code you need to have Preview 1 of .NET Core 1.1 installed.

Have fun with programming and learning!
Christian

More Information

More information about Entity Framework Core and C# is available in my new book and my C# workshops:

Professional C# 6 and .NET Core 1.0

Christian Nagel’s Workshops

Image from © Mykola Velychk | Dreamstime.com Golden field

6 thoughts on “Mapping to Getter-only Properties with EF Core

  1. Danke für die Ausführungen mit dem modelBuilder.Entity().Property(b => b.BookId).UsePropertyAccessMode(PropertyAccessMode.FieldDuringConstruction);

    Nachdem ich ein EF Video auf channel9 [https://channel9.msdn.com/Shows/Visual-Studio-Toolbox/Entity-Framework-Core] gesehen hatte, dachte ich das Field Mapping funktioniere out-of-the-box und nur bei speziellen Namen bräuchte man eine entsprechende Konfiguration.

    Like

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.