├── .gitignore ├── README.md ├── UserDefinedFieldsAndTables.sln └── UserDefinedFieldsAndTables ├── AdditionalEntity.cs ├── AdditionalField.cs ├── Database ├── DemoDbContext.cs └── Product.cs ├── Extensions └── MetamodelExtensions.cs ├── IMetamodelAccessor.cs ├── Metamodel.cs ├── MetamodelAwareCacheKeyFactory.cs ├── MetamodelCacheKey.cs ├── Program.cs └── UserDefinedFieldsAndTables.csproj /.gitignore: -------------------------------------------------------------------------------- 1 | **/.idea/** 2 | **/.vs/** 3 | 4 | **/obj/** 5 | **/bin/** 6 | 7 | **/*.sqlite* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Entity Framework Core: User-defined Fields and Tables 2 | -------------------------------------------------------------------------------- /UserDefinedFieldsAndTables.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "assets", "assets", "{D38BC328-24D4-47FA-BEEA-2B055FD0D920}" 4 | ProjectSection(SolutionItems) = preProject 5 | .gitignore = .gitignore 6 | README.md = README.md 7 | EndProjectSection 8 | EndProject 9 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UserDefinedFieldsAndTables", "UserDefinedFieldsAndTables\UserDefinedFieldsAndTables.csproj", "{89A5EA03-57E9-46A2-AFC2-34B54B425703}" 10 | EndProject 11 | Global 12 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 13 | Debug|Any CPU = Debug|Any CPU 14 | Release|Any CPU = Release|Any CPU 15 | EndGlobalSection 16 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 17 | {89A5EA03-57E9-46A2-AFC2-34B54B425703}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 18 | {89A5EA03-57E9-46A2-AFC2-34B54B425703}.Debug|Any CPU.Build.0 = Debug|Any CPU 19 | {89A5EA03-57E9-46A2-AFC2-34B54B425703}.Release|Any CPU.ActiveCfg = Release|Any CPU 20 | {89A5EA03-57E9-46A2-AFC2-34B54B425703}.Release|Any CPU.Build.0 = Release|Any CPU 21 | EndGlobalSection 22 | EndGlobal 23 | -------------------------------------------------------------------------------- /UserDefinedFieldsAndTables/AdditionalEntity.cs: -------------------------------------------------------------------------------- 1 | namespace UserDefinedFieldsAndTables; 2 | 3 | public class AdditionalEntity 4 | { 5 | public string EntityName { get; set; } 6 | public string TableName { get; set; } 7 | public string? TableSchema { get; set; } 8 | 9 | public List Key { get; } = new(); 10 | public List Fields { get; } = new(); 11 | } 12 | -------------------------------------------------------------------------------- /UserDefinedFieldsAndTables/AdditionalField.cs: -------------------------------------------------------------------------------- 1 | namespace UserDefinedFieldsAndTables; 2 | 3 | public class AdditionalField 4 | { 5 | public string EntityName { get; set; } 6 | public string PropertyName { get; set; } 7 | public Type PropertyType { get; set; } 8 | public bool IsRequired { get; set; } 9 | public int? MaxLength { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /UserDefinedFieldsAndTables/Database/DemoDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace UserDefinedFieldsAndTables.Database; 4 | 5 | public class DemoDbContext : DbContext, IMetamodelAccessor 6 | { 7 | public Metamodel Metamodel { get; } 8 | 9 | public DbSet Products { get; set; } 10 | 11 | public DemoDbContext( 12 | DbContextOptions options, 13 | Metamodel metamodel) 14 | : base(options) 15 | { 16 | Metamodel = metamodel; 17 | } 18 | 19 | protected override void OnModelCreating(ModelBuilder modelBuilder) 20 | { 21 | modelBuilder.Entity(builder => 22 | { 23 | builder.HasKey(p => p.Id); 24 | builder.Property(p => p.Name).HasMaxLength(100); 25 | }); 26 | 27 | Metamodel.ApplyChanges(modelBuilder); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /UserDefinedFieldsAndTables/Database/Product.cs: -------------------------------------------------------------------------------- 1 | namespace UserDefinedFieldsAndTables.Database; 2 | 3 | public class Product 4 | { 5 | public Guid Id { get; } 6 | public string Name { get; set; } 7 | 8 | public Product(Guid id, string name) 9 | { 10 | Id = id; 11 | Name = name; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /UserDefinedFieldsAndTables/Extensions/MetamodelExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 3 | 4 | // ReSharper disable once CheckNamespace 5 | namespace UserDefinedFieldsAndTables; 6 | 7 | public static class MetamodelExtensions 8 | { 9 | public static void ApplyChanges( 10 | this Metamodel metamodel, 11 | ModelBuilder modelBuilder) 12 | { 13 | foreach (var entity in metamodel.Entities) 14 | { 15 | modelBuilder.AddEntity(entity); 16 | } 17 | 18 | foreach (var fieldGroup in metamodel.Fields.GroupBy(f => f.EntityName)) 19 | { 20 | modelBuilder.Entity(fieldGroup.Key, 21 | builder => 22 | { 23 | foreach (var field in fieldGroup) 24 | { 25 | builder.AddField(field); 26 | } 27 | }); 28 | } 29 | } 30 | 31 | private static void AddEntity(this ModelBuilder modelBuilder, AdditionalEntity entity) 32 | { 33 | modelBuilder.Entity(entity.EntityName, 34 | builder => 35 | { 36 | builder.ToTable(entity.TableName, entity.TableSchema); 37 | 38 | foreach (var field in entity.Fields) 39 | { 40 | builder.AddField(field); 41 | } 42 | 43 | if (entity.Key.Count == 0) 44 | { 45 | builder.HasNoKey(); 46 | } 47 | else 48 | { 49 | builder.HasKey(entity.Key.Select(f => f.PropertyName).ToArray()); 50 | } 51 | }); 52 | } 53 | 54 | private static void AddField(this EntityTypeBuilder builder, AdditionalField field) 55 | { 56 | var propertyBuilder = builder.Property(field.PropertyType, field.PropertyName) 57 | .IsRequired(field.IsRequired); 58 | 59 | if (field.MaxLength.HasValue) 60 | propertyBuilder.HasMaxLength(field.MaxLength.Value); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /UserDefinedFieldsAndTables/IMetamodelAccessor.cs: -------------------------------------------------------------------------------- 1 | namespace UserDefinedFieldsAndTables; 2 | 3 | public interface IMetamodelAccessor 4 | { 5 | Metamodel Metamodel { get; } 6 | } 7 | -------------------------------------------------------------------------------- /UserDefinedFieldsAndTables/Metamodel.cs: -------------------------------------------------------------------------------- 1 | namespace UserDefinedFieldsAndTables; 2 | 3 | public class Metamodel 4 | { 5 | public int Version { get; set; } 6 | 7 | public List Fields { get; } = new(); 8 | public List Entities { get; } = new(); 9 | } 10 | -------------------------------------------------------------------------------- /UserDefinedFieldsAndTables/MetamodelAwareCacheKeyFactory.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Infrastructure; 3 | 4 | namespace UserDefinedFieldsAndTables; 5 | 6 | public class MetamodelAwareCacheKeyFactory : IModelCacheKeyFactory 7 | { 8 | public object Create(DbContext context, bool designTime) 9 | { 10 | return context is IMetamodelAccessor metamodelAccessor 11 | ? new MetamodelCacheKey(context, designTime, metamodelAccessor.Metamodel.Version) 12 | : new ModelCacheKey(context, designTime); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /UserDefinedFieldsAndTables/MetamodelCacheKey.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Infrastructure; 3 | 4 | namespace UserDefinedFieldsAndTables; 5 | 6 | public sealed class MetamodelCacheKey : ModelCacheKey 7 | { 8 | private readonly int _metamodelVersion; 9 | 10 | public MetamodelCacheKey(DbContext context, bool designTime, int metamodelVersion) 11 | : base(context, designTime) 12 | { 13 | _metamodelVersion = metamodelVersion; 14 | } 15 | 16 | protected override bool Equals(ModelCacheKey other) 17 | { 18 | return other is MetamodelCacheKey otherCacheKey 19 | && base.Equals(otherCacheKey) 20 | && otherCacheKey._metamodelVersion == _metamodelVersion; 21 | } 22 | 23 | public override int GetHashCode() 24 | { 25 | return HashCode.Combine(base.GetHashCode(), _metamodelVersion); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /UserDefinedFieldsAndTables/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using Microsoft.Data.Sqlite; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.Extensions.Caching.Memory; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using UserDefinedFieldsAndTables; 8 | using UserDefinedFieldsAndTables.Database; 9 | 10 | var connStringBuilder = new SqliteConnectionStringBuilder 11 | { 12 | DataSource = "UserDefinedFieldsAndTables.sqlite", 13 | ForeignKeys = true 14 | }; 15 | 16 | var serviceProvider = new ServiceCollection() 17 | .AddSingleton() 18 | .AddDbContext(builder => builder.UseSqlite(connStringBuilder.ConnectionString) 19 | .ReplaceService()) 20 | .BuildServiceProvider(); 21 | 22 | await ReCreateDatabaseAndFetchProductsAsync(serviceProvider); 23 | var oldCacheKey = await ChangeModelAsync(serviceProvider); 24 | await AccessDescriptionAsync(serviceProvider); 25 | await AccessProductTypeAsync(serviceProvider); 26 | 27 | // X hours later... 28 | await CleanUpEfCache(serviceProvider, oldCacheKey); 29 | 30 | static async Task CleanUpEfCache(ServiceProvider provider, object oldCacheKey) 31 | { 32 | await using var scope = provider.CreateAsyncScope(); 33 | 34 | var dbContext = scope.ServiceProvider.GetRequiredService(); 35 | 36 | var efCache = dbContext.GetService(); 37 | efCache.Remove(oldCacheKey); 38 | } 39 | 40 | static async Task AccessProductTypeAsync(ServiceProvider provider) 41 | { 42 | await using var scope = provider.CreateAsyncScope(); 43 | 44 | var dbContext = scope.ServiceProvider.GetRequiredService(); 45 | 46 | var names = await dbContext.Set>("ProductType") 47 | .Where(p => EF.Property(p, "Name") != String.Empty) 48 | .OrderBy(p => EF.Property(p, "Name")) 49 | .Select(p => EF.Property(p, "Name")) 50 | .ToListAsync(); 51 | 52 | // output: ["ProductType"] 53 | Console.WriteLine(JsonSerializer.Serialize(names)); 54 | 55 | var productTypes = await dbContext.Set>("ProductType") 56 | .ToListAsync(); 57 | 58 | // [{"Id":"5b3f23f9-9d97-42a2-99f2-1d19710e6690","Name":"ProductType"}] 59 | Console.WriteLine(JsonSerializer.Serialize(productTypes)); 60 | } 61 | 62 | static async Task AccessDescriptionAsync(ServiceProvider provider) 63 | { 64 | await using var scope = provider.CreateAsyncScope(); 65 | 66 | var dbContext = scope.ServiceProvider.GetRequiredService(); 67 | 68 | var descriptions = await dbContext.Products 69 | .Where(p => EF.Property(p, "Description") != null) 70 | .OrderBy(p => EF.Property(p, "Description")) 71 | .Select(p => EF.Property(p, "Description")) 72 | .ToListAsync(); 73 | 74 | // output: ["Product description"] 75 | Console.WriteLine(JsonSerializer.Serialize(descriptions)); 76 | 77 | var product = await dbContext.Products.SingleAsync(); 78 | 79 | // output: {"Id":"3cb4a79e-17df-4f3f-8a5f-62561153e789","Name":"Product"} 80 | Console.WriteLine(JsonSerializer.Serialize(product)); 81 | 82 | var description = dbContext.Entry(product).Property("Description").CurrentValue; 83 | 84 | // output: Product description 85 | Console.WriteLine(description); 86 | } 87 | 88 | static async Task ChangeModelAsync(ServiceProvider provider) 89 | { 90 | await using var scope = provider.CreateAsyncScope(); 91 | 92 | var dbContext = scope.ServiceProvider.GetRequiredService(); 93 | var metamodel = scope.ServiceProvider.GetRequiredService(); 94 | 95 | var cacheKey = dbContext.GetService().Create(dbContext, false); 96 | 97 | metamodel.Version++; 98 | 99 | // Add a new field to existing entity 100 | metamodel.Fields.Add(new AdditionalField 101 | { 102 | EntityName = "UserDefinedFieldsAndTables.Database.Product", 103 | PropertyName = "Description", 104 | PropertyType = typeof(string), 105 | MaxLength = 200 106 | }); 107 | 108 | // Add completely new entity 109 | var productTypeKey = new AdditionalField 110 | { 111 | PropertyName = "Id", 112 | PropertyType = typeof(Guid), 113 | IsRequired = true 114 | }; 115 | 116 | metamodel.Entities.Add(new AdditionalEntity 117 | { 118 | EntityName = "ProductType", 119 | TableName = "ProductTypes", 120 | Key = { productTypeKey }, 121 | Fields = 122 | { 123 | productTypeKey, 124 | new AdditionalField 125 | { 126 | PropertyName = "Name", 127 | PropertyType = typeof(string), 128 | MaxLength = 100 129 | } 130 | } 131 | }); 132 | 133 | dbContext.Database.ExecuteSqlRaw(@" 134 | ALTER TABLE Products ADD Description NVARCHAR(200); 135 | 136 | CREATE TABLE ProductTypes 137 | ( 138 | Id UNIQUEIDENTIFIER PRIMARY KEY, 139 | Name NVARCHAR(100) NOT NULL 140 | ); 141 | "); 142 | 143 | dbContext.Database.ExecuteSqlRaw(@" 144 | UPDATE Products 145 | SET Description = 'Product description'; 146 | 147 | INSERT INTO ProductTypes (Id, Name) 148 | VALUES ('5B3F23F9-9D97-42A2-99F2-1D19710E6690', 'ProductType'); 149 | "); 150 | 151 | return cacheKey; 152 | } 153 | 154 | static async Task ReCreateDatabaseAndFetchProductsAsync(ServiceProvider provider) 155 | { 156 | await using var scope = provider.CreateAsyncScope(); 157 | 158 | var dbContext = scope.ServiceProvider.GetRequiredService(); 159 | 160 | await dbContext.Database.EnsureDeletedAsync(); 161 | await dbContext.Database.EnsureCreatedAsync(); 162 | 163 | var id = new Guid("3CB4A79E-17DF-4F3F-8A5F-62561153E789"); 164 | dbContext.Products.Add(new Product(id, "Product")); 165 | 166 | await dbContext.SaveChangesAsync(); 167 | 168 | var products = await dbContext.Products.ToListAsync(); 169 | Console.WriteLine(JsonSerializer.Serialize(products)); 170 | } 171 | -------------------------------------------------------------------------------- /UserDefinedFieldsAndTables/UserDefinedFieldsAndTables.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | --------------------------------------------------------------------------------