├── .gitignore ├── EFCore.Projections.sln ├── FeaturedImage.png ├── LICENSE ├── README.md ├── samples ├── InMemory │ ├── Program.cs │ └── Sample.InMemory.csproj ├── Serialization │ ├── EntityProjectionJsonConverter.cs │ ├── Program.cs │ └── Sample.Serialization.csproj └── SqlServer │ ├── Program.cs │ └── Sample.SqlServer.csproj └── src └── EFCore.Projections ├── EFCore.Projections.csproj ├── EntityProjection.cs ├── EntityTypeBuilderExtensions.cs ├── Internal ├── ModelExtensions.cs ├── ProjectionBase.cs ├── ProjectionTypeBuilder.cs ├── TypeBaseExtensions.cs └── TypeInfoExtensions.cs └── signkey.snk /.gitignore: -------------------------------------------------------------------------------- 1 | .vs/ 2 | 3 | # Build results 4 | [Dd]ebug/ 5 | [Dd]ebugPublic/ 6 | [Rr]elease/ 7 | [Rr]eleases/ 8 | x64/ 9 | x86/ 10 | bld/ 11 | [Bb]in/ 12 | [Oo]bj/ 13 | [Ll]og/ 14 | -------------------------------------------------------------------------------- /EFCore.Projections.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28307.136 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFCore.Projections", "src\EFCore.Projections\EFCore.Projections.csproj", "{73EE0160-ABB2-4AD8-970A-78607699C138}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.SqlServer", "samples\SqlServer\Sample.SqlServer.csproj", "{FAA7265F-4AB5-48D4-B064-A465D9718126}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{DC612A68-1125-451F-BAF8-446FB4322872}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{3DECFC96-6C33-443C-B4F8-1B2DFCDC6C68}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.InMemory", "samples\InMemory\Sample.InMemory.csproj", "{341DACF4-A9A4-4992-805F-154806796C8C}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.Serialization", "samples\Serialization\Sample.Serialization.csproj", "{286CBCFA-133D-4173-B8E8-149CD1E7F8CD}" 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Release|Any CPU = Release|Any CPU 22 | EndGlobalSection 23 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 24 | {73EE0160-ABB2-4AD8-970A-78607699C138}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {73EE0160-ABB2-4AD8-970A-78607699C138}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {73EE0160-ABB2-4AD8-970A-78607699C138}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {73EE0160-ABB2-4AD8-970A-78607699C138}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {FAA7265F-4AB5-48D4-B064-A465D9718126}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {FAA7265F-4AB5-48D4-B064-A465D9718126}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {FAA7265F-4AB5-48D4-B064-A465D9718126}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {FAA7265F-4AB5-48D4-B064-A465D9718126}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {341DACF4-A9A4-4992-805F-154806796C8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {341DACF4-A9A4-4992-805F-154806796C8C}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {341DACF4-A9A4-4992-805F-154806796C8C}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {341DACF4-A9A4-4992-805F-154806796C8C}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {286CBCFA-133D-4173-B8E8-149CD1E7F8CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {286CBCFA-133D-4173-B8E8-149CD1E7F8CD}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {286CBCFA-133D-4173-B8E8-149CD1E7F8CD}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {286CBCFA-133D-4173-B8E8-149CD1E7F8CD}.Release|Any CPU.Build.0 = Release|Any CPU 40 | EndGlobalSection 41 | GlobalSection(SolutionProperties) = preSolution 42 | HideSolutionNode = FALSE 43 | EndGlobalSection 44 | GlobalSection(NestedProjects) = preSolution 45 | {73EE0160-ABB2-4AD8-970A-78607699C138} = {3DECFC96-6C33-443C-B4F8-1B2DFCDC6C68} 46 | {FAA7265F-4AB5-48D4-B064-A465D9718126} = {DC612A68-1125-451F-BAF8-446FB4322872} 47 | {341DACF4-A9A4-4992-805F-154806796C8C} = {DC612A68-1125-451F-BAF8-446FB4322872} 48 | {286CBCFA-133D-4173-B8E8-149CD1E7F8CD} = {DC612A68-1125-451F-BAF8-446FB4322872} 49 | EndGlobalSection 50 | GlobalSection(ExtensibilityGlobals) = postSolution 51 | SolutionGuid = {04F1E34F-75B6-4A4E-A75C-D6F5EDB70741} 52 | EndGlobalSection 53 | EndGlobal 54 | -------------------------------------------------------------------------------- /FeaturedImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dasync/EntityFrameworkCore.Extensions.Projections/6f01ae93002ee42ef1e02a585a21434980ffd415/FeaturedImage.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 D-ASYNC LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > _A small weekend project with a higher future purpose_ 2 | 3 | ![Domain Entity Projections](FeaturedImage.png) 4 | 5 | This extension to the EntityFramework Core allows to decouple the read model from entities themselves. If you practice Domain-Driven Design, then you want to separate Domain Entity/Aggregate repositories from their querying mechanism. Query results must return Entity Projections/Views instead of Entities themselves (as they contain behavior), but building projection types by hand is tedious. 6 | 7 | The full description is published in the [Domain Entity Projections in EFCore](https://medium.com/@sergiis/domain-entity-projections-in-efcore-2dbd6a9116ff) post. 8 | 9 | ## Example 10 | 11 | This is a Domain Entity that contains behaviors and implements a projection interface (as a contract) defined below. 12 | 13 | ```csharp 14 | public class City : ICityProjection 15 | { 16 | public string Name { get; private set; } 17 | 18 | public string State { get; private set; } 19 | 20 | public long Population { get; private set; } 21 | 22 | public int TimeZone { get; private set; } 23 | 24 | public void SwitchToSummerTime() 25 | { 26 | TimeZone += 1; 27 | } 28 | } 29 | ``` 30 | A sample projection interface. An entity can have implement multiple projection interfaces. 31 | ```csharp 32 | public interface ICityProjection 33 | { 34 | string Name { get; } 35 | 36 | string State { get; } 37 | 38 | long Population { get; } 39 | } 40 | ``` 41 | 42 | Just use the `HasProjections` extension method on the desired entity(-ies). That will automatically find all interfaces with get-only properties. 43 | 44 | ```csharp 45 | using Dasync.EntityFrameworkCore.Extensions.Projections; 46 | 47 | public class SampleDbContext : DbContext 48 | { 49 | protected override void OnModelCreating(ModelBuilder modelBuilder) 50 | { 51 | modelBuilder.Entity(e => 52 | { 53 | // Declare that this entity has projection interfaces. 54 | e.HasProjections(); 55 | }); 56 | } 57 | } 58 | ``` 59 | 60 | Time to query entities using their projection. 61 | 62 | ```csharp 63 | var smallCities = await dbContext 64 | .Set() // Query directly on the projection interface 65 | .Where(c => c.Population < 1_000_000) 66 | .ToListAsync(); 67 | ``` 68 | 69 | Note that the result set does not contain instances of the original `City` entity type. Instead, this extension library generates types at runtime that implement given projection interfaces. 70 | 71 | Then you can safely serialize your result set as it represents projections but not entities with behavior. Useful for API methods. 72 | 73 | ```csharp 74 | var json = JsonConvert.SerializeObject(smallCities); 75 | ``` 76 | 77 | Deserialization back can be done without involving entities as well (visit the '[samples](samples)' folder to see how `EntityProjectionJsonConverter` is implemented). 78 | 79 | ```csharp 80 | var cityProjections = JsonConvert.DeserializeObject>( 81 | json, EntityProjectionJsonConverter.Instance); 82 | ``` 83 | 84 | ## How to start 85 | 86 | See the '[samples](samples)' folder, but in a nutshell: 87 | 88 | 1. Add [Dasync.EntityFrameworkCore.Extensions.Projections NuGet Package](https://www.nuget.org/packages/Dasync.EntityFrameworkCore.Extensions.Projections) to your app 89 | 1. Define projection interfaces and implement them on your entities 90 | 1. Add `using Dasync.EntityFrameworkCore.Extensions.Projections;` to your code 91 | 1. Use `HasProjections` extension method on entities while building your DbContext model 92 | 1. Query with projection interfaces as shown above 93 | 1. Serialize projections at the API layer 94 | 95 | -------------------------------------------------------------------------------- /samples/InMemory/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using Dasync.EntityFrameworkCore.Extensions.Projections; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Storage; 6 | using Microsoft.Extensions.Logging; 7 | using Microsoft.Extensions.Logging.Console; 8 | 9 | namespace Sample 10 | { 11 | public class Program 12 | { 13 | public static async Task Main(string[] args) 14 | { 15 | // The DB root is essential for the projections to work 16 | // due to implementation details of the InMemoryDatabase. 17 | var dbRoot = new InMemoryDatabaseRoot(); 18 | var dbContext = CreateDbContext(dbRoot); 19 | 20 | // Seed data. 21 | 22 | dbContext.AddRange( 23 | new City { Name = "Seattle", State = "WA", Population = 724_745, TimeZone = -8 }, 24 | new City { Name = "San Francisco", State = "CA", Population = 884_363, TimeZone = -8 }, 25 | new City { Name = "New York City", State = "NY", Population = 8_622_698, TimeZone = -5 }, 26 | new City { Name = "Los Angeles", State = "CA", Population = 3_999_759, TimeZone = -8 }, 27 | new City { Name = "Denver", State = "CO", Population = 704_621, TimeZone = -7 }); 28 | dbContext.SaveChanges(); 29 | 30 | // Query on the projection type. 31 | 32 | var smallCities = await dbContext 33 | .Set() 34 | .Where(c => c.Population < 1_000_000) 35 | .ToListAsync(); 36 | 37 | // The items in the result set do not derive from the City type, but instead 38 | // are dynamically generated types that implement the projection interface. 39 | } 40 | 41 | private static SampleDbContext CreateDbContext(InMemoryDatabaseRoot dbRoot) 42 | { 43 | var ctxOptionsBuilder = new DbContextOptionsBuilder() 44 | .UseInMemoryDatabase("sample", dbRoot) 45 | .UseLoggerFactory(new LoggerFactory(new[] { 46 | new ConsoleLoggerProvider(filter: (msg, level) => true, includeScopes: true) 47 | })); 48 | return new SampleDbContext(ctxOptionsBuilder.Options); 49 | } 50 | } 51 | 52 | public class SampleDbContext : DbContext 53 | { 54 | public SampleDbContext(DbContextOptions options) : base(options) { } 55 | 56 | protected override void OnModelCreating(ModelBuilder modelBuilder) 57 | { 58 | modelBuilder.Entity(e => 59 | { 60 | e.HasKey(nameof(City.Name), nameof(City.State)); 61 | 62 | // Declare that this entity has projection interfaces. 63 | e.HasProjections(); 64 | }); 65 | } 66 | } 67 | 68 | public class City : ICityProjection 69 | { 70 | public string Name { get; set; } 71 | 72 | public string State { get; set; } 73 | 74 | public long Population { get; set; } 75 | 76 | public int TimeZone { get; set; } 77 | 78 | public void SwitchToSummerTime() 79 | { 80 | TimeZone += 1; 81 | } 82 | } 83 | 84 | public interface ICityProjection 85 | { 86 | string Name { get; } 87 | 88 | string State { get; } 89 | 90 | long Population { get; } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /samples/InMemory/Sample.InMemory.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp2.1 6 | latest 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /samples/Serialization/EntityProjectionJsonConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Dasync.EntityFrameworkCore.Extensions.Projections; 3 | using Newtonsoft.Json; 4 | 5 | namespace Sample 6 | { 7 | public class EntityProjectionJsonConverter : JsonConverter 8 | { 9 | public static readonly JsonConverter Instance = new EntityProjectionJsonConverter(); 10 | 11 | public override bool CanRead => true; 12 | 13 | public override bool CanWrite => false; 14 | 15 | public override bool CanConvert(Type objectType) => EntityProjection.IsProjectionInterface(objectType); 16 | 17 | public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) 18 | => serializer.Deserialize(reader, EntityProjection.GetProjectionType(objectType)); 19 | 20 | public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) 21 | => throw new NotSupportedException(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /samples/Serialization/Program.cs: -------------------------------------------------------------------------------- 1 | using Dasync.EntityFrameworkCore.Extensions.Projections; 2 | using Newtonsoft.Json; 3 | 4 | namespace Sample 5 | { 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | // Initialize 11 | 12 | var projection = EntityProjection.CreateInstance(p => 13 | { 14 | p.Property(_ => _.Name).Set("Seattle"); 15 | p.Property(_ => _.State).Set("WA"); 16 | p.Property(_ => _.Population).Set(724_745); 17 | }); 18 | 19 | // Serialize 20 | 21 | var envelope = new ContentEnvelope { City = projection }; 22 | var json = JsonConvert.SerializeObject(envelope); 23 | 24 | // Deserialize (with special JSON converter) 25 | 26 | envelope = JsonConvert.DeserializeObject( 27 | json, EntityProjectionJsonConverter.Instance); 28 | 29 | // Identical projection instance 30 | 31 | projection = envelope.City; 32 | } 33 | } 34 | 35 | public interface ICityEntityProjection 36 | { 37 | string Name { get; } 38 | 39 | string State { get; } 40 | 41 | long Population { get; } 42 | } 43 | 44 | public class ContentEnvelope 45 | { 46 | public ICityEntityProjection City { get; set; } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /samples/Serialization/Sample.Serialization.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp2.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /samples/SqlServer/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Data.SqlClient; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Dasync.EntityFrameworkCore.Extensions.Projections; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.Extensions.Logging; 7 | using Microsoft.Extensions.Logging.Console; 8 | 9 | namespace Sample 10 | { 11 | public class Program 12 | { 13 | public static async Task Main(string[] args) 14 | { 15 | // DB settings are hard-coded in the method. 16 | var dbContext = CreateDbContext(); 17 | 18 | // Seed data. 19 | 20 | dbContext.AddRange( 21 | new City { Name = "Seattle", State = "WA", Population = 724_745, TimeZone = -8 }, 22 | new City { Name = "San Francisco", State = "CA", Population = 884_363, TimeZone = -8 }, 23 | new City { Name = "New York City", State = "NY", Population = 8_622_698, TimeZone = -5 }, 24 | new City { Name = "Los Angeles", State = "CA", Population = 3_999_759, TimeZone = -8 }, 25 | new City { Name = "Denver", State = "CO", Population = 704_621, TimeZone = -7 }); 26 | dbContext.SaveChanges(); 27 | 28 | // Query on the projection type. 29 | 30 | var smallCities = await dbContext 31 | .Set() 32 | .Where(c => c.Population < 1_000_000) 33 | .ToListAsync(); 34 | 35 | // The items in the result set do not derive from the City type, but instead 36 | // are dynamically generated types that implement the projection interface. 37 | } 38 | 39 | private static SampleDbContext CreateDbContext() 40 | { 41 | var connStrBuilder = new SqlConnectionStringBuilder 42 | { 43 | DataSource = @"LOCALHOST\SQLEXPRESS", 44 | InitialCatalog = "sample", 45 | IntegratedSecurity = true 46 | }; 47 | 48 | var ctxOptionsBuilder = new DbContextOptionsBuilder() 49 | .UseSqlServer(connStrBuilder.ConnectionString) 50 | .UseLoggerFactory(new LoggerFactory(new[] { 51 | new ConsoleLoggerProvider(filter: (msg, level) => true, includeScopes: true) 52 | })); 53 | 54 | return new SampleDbContext(ctxOptionsBuilder.Options); 55 | } 56 | } 57 | 58 | public class SampleDbContext : DbContext 59 | { 60 | public SampleDbContext(DbContextOptions options) : base(options) { } 61 | 62 | protected override void OnModelCreating(ModelBuilder modelBuilder) 63 | { 64 | modelBuilder.Entity(e => 65 | { 66 | e.HasKey(nameof(City.Name), nameof(City.State)); 67 | 68 | // Declare that this entity has projection interfaces. 69 | e.HasProjections(); 70 | }); 71 | } 72 | } 73 | 74 | public class City : ICityProjection 75 | { 76 | public string Name { get; set; } 77 | 78 | public string State { get; set; } 79 | 80 | public long Population { get; set; } 81 | 82 | public int TimeZone { get; set; } 83 | 84 | public void SwitchToSummerTime() 85 | { 86 | TimeZone += 1; 87 | } 88 | } 89 | 90 | public interface ICityProjection 91 | { 92 | string Name { get; } 93 | 94 | string State { get; } 95 | 96 | long Population { get; } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /samples/SqlServer/Sample.SqlServer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp2.1 6 | latest 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/EFCore.Projections/EFCore.Projections.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | Dasync.EntityFrameworkCore.Extensions.Projections 6 | Dasync.EntityFrameworkCore.Extensions.Projections 7 | latest 8 | true 9 | signkey.snk 10 | false 11 | true 12 | D-ASYNC LLC (c) 2018 13 | https://raw.githubusercontent.com/Dasync/EntityFrameworkCore.Extensions.Projections/master/LICENSE 14 | https://github.com/Dasync/EntityFrameworkCore.Extensions.Projections 15 | https://github.com/Dasync/EntityFrameworkCore.Extensions.Projections 16 | git 17 | EF Core EFCore Projection View EntityFramework EntityFrameworkCore entity-framework-core Extension 18 | D-ASYNC LLC 19 | Dasync sergiis 20 | Entity Projections for EntityFramework Core 21 | Entity Projections for EntityFramework Core 22 | Decouples the read model from entities themselves. The read model is automatically generated at runtime from given projection/view interfaces. Essentially works like views for tables in RDBMS. See the project page for examples. 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/EFCore.Projections/EntityProjection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Linq.Expressions; 4 | using System.Reflection; 5 | using Dasync.EntityFrameworkCore.Extensions.Projections.Internal; 6 | 7 | namespace Dasync.EntityFrameworkCore.Extensions.Projections 8 | { 9 | public static class EntityProjection 10 | { 11 | public static bool IsProjectionInterface(Type interfaceType) => interfaceType.IsProjectionInterface(); 12 | 13 | public static bool IsProjectionInterface() => typeof(TInterface).IsProjectionInterface(); 14 | 15 | public static Type GetProjectionType(Type projectionInterfaceType) 16 | => ProjectionTypeBuilder.GetProjectionType(projectionInterfaceType); 17 | 18 | public static Type GetProjectionType() 19 | => ProjectionTypeBuilder.GetProjectionType(typeof(TProjectionInterface)); 20 | 21 | public static object CreateInstance(Type projectionInterfaceType, object tag = null) 22 | { 23 | var instance = Activator.CreateInstance(ProjectionTypeBuilder.GetProjectionType(projectionInterfaceType)); 24 | SetTag(instance, tag); 25 | return instance; 26 | } 27 | 28 | public static TProjectionInterface CreateInstance(object tag = null) 29 | { 30 | var instance = (TProjectionInterface)Activator.CreateInstance( 31 | ProjectionTypeBuilder.GetProjectionType(typeof(TProjectionInterface))); 32 | SetTag(instance, tag); 33 | return instance; 34 | } 35 | 36 | public static TProjectionInterface CreateInstance( 37 | Action> initializeAction, object tag = null) 38 | { 39 | if (initializeAction == null) 40 | throw new ArgumentNullException(nameof(initializeAction)); 41 | 42 | var instance = (TProjectionInterface)Activator.CreateInstance( 43 | ProjectionTypeBuilder.GetProjectionType(typeof(TProjectionInterface))); 44 | 45 | var initializer = new Initializer(instance); 46 | initializeAction(initializer); 47 | 48 | SetTag(instance, tag); 49 | 50 | return instance; 51 | } 52 | 53 | public static TProjectionInterface SetValue( 54 | TProjectionInterface instance, 55 | Expression> memberAccessExpression, 56 | TValue value) 57 | { 58 | if (instance == null) 59 | throw new ArgumentNullException(nameof(instance)); 60 | if (memberAccessExpression == null) 61 | throw new ArgumentNullException(nameof(memberAccessExpression)); 62 | 63 | if (!(instance is ProjectionBase)) 64 | throw new ArgumentException($"The type '{instance.GetType()}' is not a dynamically generated entity projection."); 65 | 66 | if (!(memberAccessExpression.Body is MemberExpression memberExpression)) 67 | throw new ArgumentException("The expression must be a simple property access like: p => p.Id", nameof(memberAccessExpression)); 68 | 69 | var propertyName = memberExpression.Member.Name; 70 | var backingField = instance.GetType().GetTypeInfo().GetField( 71 | $"<{propertyName}>k__BackingField", 72 | BindingFlags.Instance | BindingFlags.NonPublic); 73 | 74 | backingField.SetValue(instance, value); 75 | 76 | return instance; 77 | } 78 | 79 | public static void SetTag(object instance, object tag) 80 | { 81 | if (!(instance is ProjectionBase projectionBase)) 82 | throw new ArgumentException($"The type '{instance.GetType()}' is not a dynamically generated entity projection."); 83 | projectionBase.SetTag(tag); 84 | } 85 | 86 | public static object GetTag(object instance) 87 | { 88 | if (!(instance is ProjectionBase projectionBase)) 89 | throw new ArgumentException($"The type '{instance.GetType()}' is not a dynamically generated entity projection."); 90 | return projectionBase.GetTag(); 91 | } 92 | 93 | [EditorBrowsable(EditorBrowsableState.Never)] 94 | public sealed class Initializer 95 | { 96 | private readonly TProjectionInterface _instance; 97 | 98 | public Initializer(TProjectionInterface instance) => _instance = instance; 99 | 100 | [EditorBrowsable(EditorBrowsableState.Never)] 101 | public sealed class PropertySetter 102 | { 103 | private readonly TProjectionInterface _instance; 104 | private readonly Expression> _memberAccessExpression; 105 | 106 | public PropertySetter( 107 | TProjectionInterface instance, 108 | Expression> memberAccessExpression) 109 | { 110 | _instance = instance; 111 | _memberAccessExpression = memberAccessExpression; 112 | } 113 | 114 | public void Set(TValue value) => SetValue(_instance, _memberAccessExpression, value); 115 | } 116 | 117 | public PropertySetter Property(Expression> memberAccessExpression) 118 | => new PropertySetter(_instance, memberAccessExpression); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/EFCore.Projections/EntityTypeBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Dasync.EntityFrameworkCore.Extensions.Projections.Internal; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 7 | using Microsoft.EntityFrameworkCore.Metadata.Internal; 8 | 9 | namespace Dasync.EntityFrameworkCore.Extensions.Projections 10 | { 11 | public static class EntityTypeBuilderExtensions 12 | { 13 | public static EntityTypeBuilder HasProjections(this EntityTypeBuilder builder) where TEntity : class 14 | { 15 | foreach (var interfaceType in builder.Metadata.ClrType.GetInterfaces()) 16 | { 17 | if (interfaceType.IsProjectionInterface()) 18 | builder.HasProjection(interfaceType); 19 | } 20 | 21 | return builder; 22 | } 23 | 24 | public static EntityTypeBuilder HasProjection(this EntityTypeBuilder builder, Type projectionInterfaceType) where TEntity : class 25 | { 26 | if (builder.Metadata.ClrType == null) 27 | throw new InvalidOperationException($"Cannot automatically discover projections for entity $'{builder.Metadata.Name}' as it does not have an associated CLR type."); 28 | 29 | if (!projectionInterfaceType.IsProjectionInterface()) 30 | throw new InvalidOperationException($"The type '{projectionInterfaceType}' cannot be used as a projection interface for entity '{builder.Metadata.Name}'."); 31 | 32 | var internalEntityBuilder = ((IInfrastructure)builder).Instance; 33 | var internalModelBuilder = internalEntityBuilder.ModelBuilder; 34 | 35 | // Generate new type for given projection interface. 36 | var projectionType = ProjectionTypeBuilder.GetProjectionType(projectionInterfaceType); 37 | 38 | // Register newly generated type as an entity. 39 | var projectionEntityTypeBuilder = new EntityTypeBuilder( 40 | internalModelBuilder.Entity(projectionType, ConfigurationSource.Explicit, throwOnQuery: true)); 41 | 42 | // Change the name of the new projection entity to the same name of the given entity. 43 | // This is needed to map both of them to the same underlying collection when in-memory 44 | // database is used. 45 | ((IInfrastructure)projectionEntityTypeBuilder) 46 | .Instance.Metadata.ChangeName(builder.Metadata.Name); 47 | 48 | // Copy all entity annotations which. 49 | foreach (var annotation in internalEntityBuilder.Metadata.GetAnnotations()) 50 | projectionEntityTypeBuilder.HasAnnotation(annotation.Name, annotation.Value); 51 | 52 | // Explicitly set the table name for the projection entity, otherwise EF may map it to a different one. 53 | projectionEntityTypeBuilder.HasAnnotation(RelationalAnnotationNames.TableName, 54 | new RelationalEntityTypeAnnotations(internalEntityBuilder.Metadata).TableName); 55 | 56 | // Copy all property definitions. 57 | foreach (var property in internalEntityBuilder.Metadata.GetProperties()) 58 | { 59 | #warning Optimization: exclude properties that are not on the interface 60 | 61 | var propertyBuilder = projectionEntityTypeBuilder.Property(property.ClrType, property.Name); 62 | 63 | foreach (var annotation in property.GetAnnotations()) 64 | propertyBuilder.HasAnnotation(annotation.Name, annotation.Value); 65 | 66 | // Need to explicitly set the column name, otherwise EF prepends the name of this new projection entity. 67 | if (property.FindAnnotation(RelationalAnnotationNames.ColumnName) == null) 68 | propertyBuilder.HasAnnotation( 69 | RelationalAnnotationNames.ColumnName, 70 | ConstraintNamer.GetDefaultName(property)); 71 | } 72 | 73 | // Setup relationship between the projection entity and the original 74 | // one as EF does not allow two entities referencing the same table. 75 | var primaryKey = internalEntityBuilder.Metadata.FindPrimaryKey(); 76 | if (primaryKey != null) 77 | { 78 | var pkProperties = primaryKey.Properties.Select(p => p.Name).ToArray(); 79 | projectionEntityTypeBuilder.HasKey(pkProperties); 80 | projectionEntityTypeBuilder 81 | .HasOne(builder.Metadata.ClrType).WithOne() 82 | .HasForeignKey(builder.Metadata.ClrType, pkProperties); 83 | } 84 | 85 | // Redirect projection interface types to actual projection entity type, 86 | // thus you can directly query an interface using LINQ. 87 | AddInterfaceProjectionAliases(internalModelBuilder.Metadata, projectionType, projectionInterfaceType); 88 | 89 | return builder; 90 | } 91 | 92 | private static void AddInterfaceProjectionAliases(IModel model, Type projectionType, Type projectionInterfaceType) 93 | { 94 | model.AddEntityTypeAlias(projectionType, projectionInterfaceType); 95 | 96 | foreach (var subProjectionInterface in projectionInterfaceType.GetInterfaces()) 97 | AddInterfaceProjectionAliases(model, projectionType, subProjectionInterface); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/EFCore.Projections/Internal/ModelExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Reflection; 4 | using Microsoft.EntityFrameworkCore.Metadata; 5 | using Microsoft.EntityFrameworkCore.Metadata.Internal; 6 | 7 | namespace Dasync.EntityFrameworkCore.Extensions.Projections.Internal 8 | { 9 | public static class ModelExtensions 10 | { 11 | private static readonly FieldInfo ClrTypeNameMapField = typeof(Model).GetField("_clrTypeNameMap", BindingFlags.Instance | BindingFlags.NonPublic); 12 | 13 | public static void AddEntityTypeAlias(this IModel model, Type entityType, Type aliasType) 14 | { 15 | var clrTypeNameMap = (ConcurrentDictionary)ClrTypeNameMapField.GetValue(model); 16 | 17 | if (!clrTypeNameMap.TryGetValue(entityType, out var entityName)) 18 | throw new ArgumentException($"Unknown entity type $'{entityType}'."); 19 | 20 | clrTypeNameMap.AddOrUpdate(aliasType, entityName, (t, n) => 21 | { 22 | if (n == entityName) 23 | return entityName; 24 | 25 | throw new InvalidOperationException($"The entity alias type '{aliasType}' is already assigned to the entity '{t}'."); 26 | }); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/EFCore.Projections/Internal/ProjectionBase.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace Dasync.EntityFrameworkCore.Extensions.Projections.Internal 4 | { 5 | public abstract class ProjectionBase 6 | { 7 | [DebuggerBrowsable(DebuggerBrowsableState.Never)] 8 | private object _tag; 9 | 10 | public void SetTag(object tag) => _tag = tag; 11 | 12 | public object GetTag() => _tag; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/EFCore.Projections/Internal/ProjectionTypeBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | using System.Reflection.Emit; 5 | using System.Runtime.CompilerServices; 6 | using System.Threading; 7 | 8 | namespace Dasync.EntityFrameworkCore.Extensions.Projections.Internal 9 | { 10 | public class ProjectionTypeBuilder 11 | { 12 | private static readonly Lazy _moduleBuilder; 13 | private static int _typeCounter; 14 | private static Dictionary _projectionTypes = new Dictionary(); 15 | 16 | private sealed class Context 17 | { 18 | public TypeBuilder Builder; 19 | public HashSet ImplementedInterfaces = new HashSet(); 20 | } 21 | 22 | static ProjectionTypeBuilder() 23 | { 24 | _moduleBuilder = new Lazy(() => 25 | { 26 | var thisAssembly = typeof(ProjectionTypeBuilder).GetTypeInfo().Assembly; 27 | var assemblyName = new AssemblyName(thisAssembly.GetName().Name + ".Dynamic"); 28 | var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); 29 | var moduleName = assemblyName.Name + ".dll"; 30 | var module = assemblyBuilder.DefineDynamicModule(moduleName); 31 | return module; 32 | }, 33 | LazyThreadSafetyMode.ExecutionAndPublication); 34 | } 35 | 36 | public static Type GetProjectionType(Type projectionInterfaceType) 37 | { 38 | lock (_projectionTypes) 39 | { 40 | if (_projectionTypes.TryGetValue(projectionInterfaceType, out var projectionType)) 41 | return projectionType; 42 | projectionType = BuildProjectionType(projectionInterfaceType); 43 | _projectionTypes.Add(projectionInterfaceType, projectionType); 44 | return projectionType; 45 | } 46 | } 47 | 48 | private static Type BuildProjectionType(Type projectionInterfaceType) 49 | { 50 | if (!projectionInterfaceType.IsProjectionInterface()) 51 | throw new InvalidOperationException($"The type '{projectionInterfaceType}' cannot be used as a projection interface."); 52 | 53 | var name = projectionInterfaceType.Name; 54 | if (name.StartsWith("I") && name.Length > 1) 55 | name = name.Substring(1); 56 | if (name.EndsWith("View") && name.Length > 4) 57 | name = name.Substring(0, name.Length - 4); 58 | else if (name.EndsWith("Projection") && name.Length > 10) 59 | name = name.Substring(0, name.Length - 10); 60 | name += $"-projection#{Interlocked.Increment(ref _typeCounter)}"; 61 | 62 | var typeBuilder = _moduleBuilder.Value.DefineType(name, 63 | TypeAttributes.Class | TypeAttributes.Public | TypeAttributes.Sealed, 64 | parent: typeof(ProjectionBase)); 65 | 66 | var context = new Context { Builder = typeBuilder }; 67 | ImplementInterface(context, projectionInterfaceType); 68 | 69 | var projectionType = typeBuilder.CreateTypeInfo().AsType(); 70 | return projectionType; 71 | } 72 | 73 | private static void ImplementInterface(Context context, Type interfaceType) 74 | { 75 | if (!context.ImplementedInterfaces.Add(interfaceType)) 76 | return; 77 | 78 | foreach (var subInterfaceType in interfaceType.GetInterfaces()) 79 | ImplementInterface(context, subInterfaceType); 80 | 81 | ImplementInterfaceProperties(context, interfaceType); 82 | } 83 | 84 | private static void ImplementInterfaceProperties(Context context, Type interfaceType) 85 | { 86 | context.Builder.AddInterfaceImplementation(interfaceType); 87 | 88 | foreach (var propertyInfo in interfaceType.GetProperties()) 89 | { 90 | var backingFieldName = string.Concat("<", propertyInfo.Name, ">k__BackingField"); 91 | var backingFieldBuilder = context.Builder.DefineField(backingFieldName, propertyInfo.PropertyType, FieldAttributes.Private); 92 | backingFieldBuilder.SetCustomAttribute(new CustomAttributeBuilder(typeof(CompilerGeneratedAttribute).GetConstructor(Type.EmptyTypes), new object[0])); 93 | 94 | var getterBuilder = context.Builder.DefineMethod("get_" + propertyInfo.Name, 95 | MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.Virtual, 96 | propertyInfo.PropertyType, Type.EmptyTypes); 97 | getterBuilder.SetCustomAttribute(new CustomAttributeBuilder(typeof(CompilerGeneratedAttribute).GetConstructor(Type.EmptyTypes), new object[0])); 98 | 99 | var getterIL = getterBuilder.GetILGenerator(); 100 | getterIL.Emit(OpCodes.Ldarg_0); 101 | getterIL.Emit(OpCodes.Ldfld, backingFieldBuilder); 102 | getterIL.Emit(OpCodes.Ret); 103 | 104 | var setterBuilder = context.Builder.DefineMethod("set_" + propertyInfo.Name, 105 | MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.Virtual, 106 | null, new Type[] { propertyInfo.PropertyType }); 107 | setterBuilder.SetCustomAttribute(new CustomAttributeBuilder(typeof(CompilerGeneratedAttribute).GetConstructor(Type.EmptyTypes), new object[0])); 108 | 109 | var setterIL = setterBuilder.GetILGenerator(); 110 | setterIL.Emit(OpCodes.Ldarg_0); 111 | setterIL.Emit(OpCodes.Ldarg_1); 112 | setterIL.Emit(OpCodes.Stfld, backingFieldBuilder); 113 | setterIL.Emit(OpCodes.Ret); 114 | 115 | var propertyBuilder = context.Builder.DefineProperty(propertyInfo.Name, PropertyAttributes.None, propertyInfo.PropertyType, Type.EmptyTypes); 116 | propertyBuilder.SetGetMethod(getterBuilder); 117 | propertyBuilder.SetSetMethod(setterBuilder); 118 | 119 | context.Builder.DefineMethodOverride(getterBuilder, propertyInfo.GetMethod); 120 | } 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/EFCore.Projections/Internal/TypeBaseExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Microsoft.EntityFrameworkCore.Metadata.Internal; 3 | 4 | namespace Dasync.EntityFrameworkCore.Extensions.Projections.Internal 5 | { 6 | public static class TypeBaseExtensions 7 | { 8 | private static readonly FieldInfo NameFieldInfo = typeof(TypeBase).GetField( 9 | "k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic); 10 | 11 | public static void ChangeName(this TypeBase typeBase, string newName) 12 | => NameFieldInfo.SetValue(typeBase, newName); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/EFCore.Projections/Internal/TypeInfoExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | 4 | namespace Dasync.EntityFrameworkCore.Extensions.Projections.Internal 5 | { 6 | public static class TypeInfoExtensions 7 | { 8 | public static bool IsProjectionInterface(this Type type) => IsProjectionInterface(type.GetTypeInfo()); 9 | 10 | public static bool IsProjectionInterface(this TypeInfo typeInfo) 11 | { 12 | if (typeInfo == null) 13 | throw new ArgumentNullException(nameof(typeInfo)); 14 | 15 | if (!typeInfo.IsInterface) 16 | return false; 17 | 18 | if (!typeInfo.IsPublic) 19 | return false; 20 | 21 | foreach (var interfaceType in typeInfo.GetInterfaces()) 22 | { 23 | if (!interfaceType.IsProjectionInterface()) 24 | return false; 25 | } 26 | 27 | foreach (var member in typeInfo.GetMembers()) 28 | { 29 | if (member.MemberType == MemberTypes.Method) 30 | { 31 | var methodInfo = (MethodInfo)member; 32 | if (!methodInfo.IsSpecialName) 33 | return false; 34 | 35 | continue; 36 | } 37 | 38 | if (member.MemberType != MemberTypes.Property) 39 | return false; 40 | 41 | var propertyInfo = (PropertyInfo)member; 42 | 43 | if (propertyInfo.SetMethod != null) 44 | return false; 45 | 46 | if (propertyInfo.GetMethod == null) 47 | return false; 48 | } 49 | 50 | return true; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/EFCore.Projections/signkey.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dasync/EntityFrameworkCore.Extensions.Projections/6f01ae93002ee42ef1e02a585a21434980ffd415/src/EFCore.Projections/signkey.snk --------------------------------------------------------------------------------