romiller.com

Program Manager on the .NET team at Microsoft

Skip to content
  • Home
Search

EF Core 1.1: Read Only Entities & Extending Metadata with Annotations

February 14, 2017 / romiller.com

EF Core has a well defined set of concepts that can be configured for entity types and properties. Examples include the primary key of an entity, whether a property is required or optional, etc.

Aside from these “baked in” concepts, there are metadata annotations that allow additional concepts to be added to the model. For example, the relational base provider introduces the idea of the table that an entity type is mapped to. These are stored as key/value annotations on the metadata (or model).

Annotations can then be read back from the model when required. In the above example, the relational provider can read the table annotation whenever it is generating SQL to send to the database.

We can use this same annotation functionality to add our own modelling concepts to EF Core.

Our Example: Read Only Entities

The following code defines an IsReadOnly extension method, which can be called when configuring an entity in OnModelCreating. This just adds an annotation to the entity type to flag that it is read only. We’re also defining an extension method to read the value back from metadata.

public static class Extensions
{
    private static readonly string READONLY_ANNOTATION = "custom:readonly";

    public static EntityTypeBuilder<TEntity> IsReadOnly<TEntity>(this EntityTypeBuilder<TEntity> builder)
        where TEntity : class
    {
        builder.HasAnnotation(READONLY_ANNOTATION, true);
        return builder;
    }

    public static bool IsReadOnly(this IEntityType entity)
    {
        var annotation = entity.FindAnnotation(READONLY_ANNOTATION);
        if(annotation != null)
        {
            return (bool)annotation.Value;
        }

        return false;
    }
}

Obviously EF Core doesn’t know anything about this annotation. So we’ll override SaveChanges on our context, to detect any changes to read only entities, and throw.

public override int SaveChanges()
{
    var errors = ChangeTracker
        .Entries()
        .Where(e => e.Metadata.IsReadOnly() && e.State != EntityState.Unchanged)
        .Select(e => e.Metadata.Name)
        .Distinct()
        .ToList();

    if (errors.Any())
    {
        throw new InvalidOperationException(
            $"Attempted to save read-only entities {string.Join(",", errors)}");
    }

    return base.SaveChanges();
}

Complete Code Listing

Here is the complete listing for a console application that shows everything working.

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Sample
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var db = new BloggingContext())
            {
                db.Database.EnsureCreated();

                db.Blogs.Add(new Blog { Url = "http://romiller.com" });
                var count = db.SaveChanges();
            }
        }
    }

    public class BloggingContext : DbContext
    {
        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Sample;Trusted_Connection=True;");
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Blog>().IsReadOnly();
        }

        public override int SaveChanges()
        {
            var errors = ChangeTracker
               .Entries()
               .Where(e => e.Metadata.IsReadOnly() && e.State != EntityState.Unchanged)
               .Select(e => e.Metadata.Name)
               .Distinct()
               .ToList();

            if (errors.Any())
            {
                throw new InvalidOperationException(
                    $"Attempted to save read-only entities {string.Join(",", errors)}");
            }

            return base.SaveChanges();
        }
    }

    public static class Extensions
    {
        private static readonly string READONLY_ANNOTATION = "custom:readonly";

        public static EntityTypeBuilder<TEntity> IsReadOnly<TEntity>(this EntityTypeBuilder<TEntity> builder)
            where TEntity : class
        {
            builder.HasAnnotation(READONLY_ANNOTATION, true);
            return builder;
        }

        public static bool IsReadOnly(this IEntityType entity)
        {
            var annotation = entity.FindAnnotation(READONLY_ANNOTATION);
            if(annotation != null)
            {
                return (bool)annotation.Value;
            }

            return false;
        }
    }

    public class Blog
    {
        public int BlogId { get; set; }
        public string Url { get; set; }

        public List<Post> Posts { get; set; }
    }

    public class Post
    {
        public int PostId { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }

        public int BlogId { get; set; }
        public Blog Blog { get; set; }
    }
}

Share this:

  • Twitter
  • Facebook

Like this:

Like Loading...

Related

Entity Framework
Annotations, EF, EF Core, Entity Framework, Metadata

Post navigation

← EF Core 1.1: Run Reverse Engineer from Code
I’m Leaving the EF Team, But Still Working on .NET →
Blog at WordPress.com.
  • Follow Following
    • romiller.com
    • Join 261 other followers
    • Already have a WordPress.com account? Log in now.
    • romiller.com
    • Customize
    • Follow Following
    • Sign up
    • Log in
    • Copy shortlink
    • Report this content
    • View post in Reader
    • Manage subscriptions
    • Collapse this bar
%d bloggers like this: