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; } } }