-
Notifications
You must be signed in to change notification settings - Fork 3.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Flexible mapping to CLR types and members (Custom O/C Mapping) #240
Comments
Here is a good example of this feature (good case to verify against once we implement it) - #1009 |
@rowanmiller Looking at this it seems that the bug I filed yesterday is more specific about property access while this is the all-encompassing flexible mapping. Would you mind if I just add #2968 as a child, similar to how #857 is a child specific to collection patterns? |
@divega yep that works |
Otherwise I can merge it all here... |
Made some edits to the original text of the issue to add background and additional links to related issues. |
Hi, public class BloggingContext : DbContext
{
// Type mapping for materialization
public DbSet<IBlog, Blog> Blogs { get; set; }
// Materialization can create type on the fly because no specific type was given. (Generators or emitting)
public DbSet<IPost> Posts { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<IBlog>()
.Property(b => b.Url);
// Optional: provide type creation factory.
modelBuilder.Entity<IBlog>(() => new Blog());
// Optional: provide collection behavior if non is provided then use default or emitt or generate.
modelBuilder.Collection<IList<Any>>((IList<Any> collection, Any item) => collection.Add(item));
}
}
// Any represents all kind of types which can be used in a collection.
class Any { }
public class DbSet<T, TEntity> where TEntity : class, T { }
public interface IBlog
{
int BlogId { get; set; }
string Url { get; set; }
IList<IPost> Posts { get; set; }
}
public class Blog : IBlog
{
public int BlogId { get; set; }
public string Url { get; set; }
public IList<IPost> Posts { get; set; }
}
public interface IPost
{
int PostId { get; set; }
string Title { get; set; }
string Content { get; set; }
int BlogId { get; set; }
IBlog Blog { get; set; }
}
public class Post : IPost
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogId { get; set; }
public IBlog Blog { get; set; }
} I know there are lot of other scenarios, so if someone has specific questions how for example x,y and z would fit in this kind of mapping, please ask, I will try to provide examples and explanations. |
Actually, todays code base is quite close to being able to handle this. It just needs to be 'laxed in some parts. I mean the I got stuck at the type check in EntityType.AddProperty, I dunno if it would have worked otherwise (probably not...) We need to be able to specify, when adding a Property:
And the type check mentioned above needs to not happen Here's a (hopefully somewhat) functioning example of what I'd like to achieve:
|
@rowanmiller any thoughts or progress on this? I really think that just relaxing the checks a bit on this (and maybe generalizing the structure of Property a little) would get us 80% there. ( @divega ) Also, FWIW, I now think that we would be better off with a new |
Parts of issues #3342, #240, #10509, #3797 The main things here are: - Support for injecting values into parameterized entity constructors - Property values are injected if the parameter type and name matches - The current DbContext as DbContext or a derived DbContext type - A service from the internal or external service provider - A delegate to a method of a service - The IEntityType for the entity - Use of the above to inject lazy loading capabilities into entities For lazy loading, either the ILazyLoader service can be injected directly, or a delegate can be injected if the entity class cannot take a dependency on the EF assembly--see the examples below. Currently all constructor injection is done by convention. Remaining work includes: - API/attributes to configure the constructor binding - Allow factory to be used instead of using the constructor directly. (Functional already, but no API or convention to configure it.) - Allow property injection for services - Configuration of which entities/properties should be lazy loaded and which should not ### Examples In this example EF will use the private constructor passing in values from the database when creating entity instances. (Note that it is assumed that _blogId has been configured as the key.) ```C# public class Blog { private int _blogId; // This constructor used by EF Core private Blog( int blogId, string title, int? monthlyRevenue) { _blogId = blogId; Title = title; MonthlyRevenue = monthlyRevenue; } public Blog( string title, int? monthlyRevenue = null) : this(0, title, monthlyRevenue) { } public string Title { get; } public int? MonthlyRevenue { get; set; } } ``` In this example, EF will inject the ILazyLoader instance, which is then used to enable lazy-loading on navigation properties. Note that the navigation properties must have backing fields and all access by EF will go through the backing fields to prevent EF triggering lazy loading itself. ```C# public class LazyBlog { private readonly ILazyLoader _loader; private ICollection<LazyPost> _lazyPosts = new List<LazyPost>(); public LazyBlog() { } private LazyBlog(ILazyLoader loader) { _loader = loader; } public int Id { get; set; } public ICollection<LazyPost> LazyPosts => _loader.Load(this, ref _lazyPosts); } public class LazyPost { private readonly ILazyLoader _loader; private LazyBlog _lazyBlog; public LazyPost() { } private LazyPost(ILazyLoader loader) { _loader = loader; } public int Id { get; set; } public LazyBlog LazyBlog { get => _loader.Load(this, ref _lazyBlog); set => _lazyBlog = value; } } ``` This example is the same as the last example, except EF is matching the delegate type and parameter name and injecting a delegate for the ILazyLoader.Load method so that the entity class does not need to reference the EF assembly. A small extension method can be included in the entity assembly to make it a bit easier to use the delegate. ```C# public class LazyPocoBlog { private readonly Action<object, string> _loader; private ICollection<LazyPocoPost> _lazyPocoPosts = new List<LazyPocoPost>(); public LazyPocoBlog() { } private LazyPocoBlog(Action<object, string> lazyLoader) { _loader = lazyLoader; } public int Id { get; set; } public ICollection<LazyPocoPost> LazyPocoPosts => _loader.Load(this, ref _lazyPocoPosts); } public class LazyPocoPost { private readonly Action<object, string> _loader; private LazyPocoBlog _lazyPocoBlog; public LazyPocoPost() { } private LazyPocoPost(Action<object, string> lazyLoader) { _loader = lazyLoader; } public int Id { get; set; } public LazyPocoBlog LazyPocoBlog { get => _loader.Load(this, ref _lazyPocoBlog); set => _lazyPocoBlog = value; } } public static class TestPocoLoadingExtensions { public static TRelated Load<TRelated>( this Action<object, string> loader, object entity, ref TRelated navigationField, [CallerMemberName] string navigationName = null) where TRelated : class { loader?.Invoke(entity, navigationName); return navigationField; } } ```
Parts of issues #3342, #240, #10509, #3797 The main things here are: - Support for injecting values into parameterized entity constructors - Property values are injected if the parameter type and name matches - The current DbContext as DbContext or a derived DbContext type - A service from the internal or external service provider - A delegate to a method of a service - The IEntityType for the entity - Use of the above to inject lazy loading capabilities into entities For lazy loading, either the ILazyLoader service can be injected directly, or a delegate can be injected if the entity class cannot take a dependency on the EF assembly--see the examples below. Currently all constructor injection is done by convention. Remaining work includes: - API/attributes to configure the constructor binding - Allow factory to be used instead of using the constructor directly. (Functional already, but no API or convention to configure it.) - Allow property injection for services - Configuration of which entities/properties should be lazy loaded and which should not ### Examples In this example EF will use the private constructor passing in values from the database when creating entity instances. (Note that it is assumed that _blogId has been configured as the key.) ```C# public class Blog { private int _blogId; // This constructor used by EF Core private Blog( int blogId, string title, int? monthlyRevenue) { _blogId = blogId; Title = title; MonthlyRevenue = monthlyRevenue; } public Blog( string title, int? monthlyRevenue = null) : this(0, title, monthlyRevenue) { } public string Title { get; } public int? MonthlyRevenue { get; set; } } ``` In this example, EF will inject the ILazyLoader instance, which is then used to enable lazy-loading on navigation properties. Note that the navigation properties must have backing fields and all access by EF will go through the backing fields to prevent EF triggering lazy loading itself. ```C# public class LazyBlog { private readonly ILazyLoader _loader; private ICollection<LazyPost> _lazyPosts = new List<LazyPost>(); public LazyBlog() { } private LazyBlog(ILazyLoader loader) { _loader = loader; } public int Id { get; set; } public ICollection<LazyPost> LazyPosts => _loader.Load(this, ref _lazyPosts); } public class LazyPost { private readonly ILazyLoader _loader; private LazyBlog _lazyBlog; public LazyPost() { } private LazyPost(ILazyLoader loader) { _loader = loader; } public int Id { get; set; } public LazyBlog LazyBlog { get => _loader.Load(this, ref _lazyBlog); set => _lazyBlog = value; } } ``` This example is the same as the last example, except EF is matching the delegate type and parameter name and injecting a delegate for the ILazyLoader.Load method so that the entity class does not need to reference the EF assembly. A small extension method can be included in the entity assembly to make it a bit easier to use the delegate. ```C# public class LazyPocoBlog { private readonly Action<object, string> _loader; private ICollection<LazyPocoPost> _lazyPocoPosts = new List<LazyPocoPost>(); public LazyPocoBlog() { } private LazyPocoBlog(Action<object, string> lazyLoader) { _loader = lazyLoader; } public int Id { get; set; } public ICollection<LazyPocoPost> LazyPocoPosts => _loader.Load(this, ref _lazyPocoPosts); } public class LazyPocoPost { private readonly Action<object, string> _loader; private LazyPocoBlog _lazyPocoBlog; public LazyPocoPost() { } private LazyPocoPost(Action<object, string> lazyLoader) { _loader = lazyLoader; } public int Id { get; set; } public LazyPocoBlog LazyPocoBlog { get => _loader.Load(this, ref _lazyPocoBlog); set => _lazyPocoBlog = value; } } public static class TestPocoLoadingExtensions { public static TRelated Load<TRelated>( this Action<object, string> loader, object entity, ref TRelated navigationField, [CallerMemberName] string navigationName = null) where TRelated : class { loader?.Invoke(entity, navigationName); return navigationField; } } ```
Which one of the issues in this issue's description handles support for unsigned integer types? |
@smarts Unsigned integer types are supported as of EF Core 2.1. |
@ajcvickers is there documentation on how to use this feature? My team recently tried to use them and was getting an exception that EF Core was expecting |
@smarts Some databases natively support storing unsigned types, in which case they should behave like any other property. Some databases, like SQL Server, don't supported unsigned types, but EF should automatically perform a value conversion if you have such a type in your model. In this case the value is stored in the database as a bigger signed type, by default. If you have an entity type with unsigned properties and you are seeing errors, then please file a new issue with a small, runnable project/solution of complete code listing that demonstrates the behavior you are seeing. |
@ajcvickers sorry for the delay in responding.
Does that mean that using |
@smarts take a look at value converters - you can set up lossy value conversions, but EF will not that automatically for you (as it does with uint->long). However, lossy value conversions may get a bit tricky in some scenarios, and are best avoided if you have an easy lossless alternative (such as long). |
@roji As far as I can see, conversion between UInt32 and Int32 is lossless if you use "C-Style" casts. Negative int32 values are mapped to "high" unsigned values and back, one just should be careful when interpreting the converted values (e. G. ordering is different). |
@markusschaber you're technically right: if your value converter does a simple cast in C#, then uint values which are larger than int.MaxValue will show up as negatives. While this is technically lossless and would work, as you say there are some tricky caveats. Given that databases typically have a larger bigint type which can contain all uint values, it generally really makes sense to convert to that - trying to save 4 bytes isn't going to be justifiable in most cases. |
@markusschaber i'm not sure if you noticed, but i'm specifically talking about auto-generated IDs in the database. Using |
Just to close the loop on this. When deciding how to convert automatically, we look at:
The second point is why we convert uint to long, and not uint to int, since for the latter, if the uint does have a value that can't "fit" it will still be stored, but it will be interpreted as negative by the database and hence will have a different ordering. That being said, it's safe to convert uint to int if you know that the domain space for your uints can fit in an int, or if a specific ordering on the database is not needed. One way to do it is: modelBuilder.Entity<Foo>().Property(e => e.MyUInt).HasConversion<int>(); Finally, there are currently some limitations for conversion on keys: see #11597 |
@ajcvickers - What exactly needs-cleanup label is? |
Does it mean not earlier than November 2021? |
@voroninp yes, 6.0 would mean November 2021. Previews would make this available much earlier, though. |
I'm just here to say I've been waiting 6 years and counting for polymorphic relations using discriminators. I think it's more than made a case for itself from the digging I've been doing here. Something along the lines of:
Therefore, the table integration.Integratable...; // (returns a IIntegratable that can be passed through pattern matching to detect concrete type)
// and of course:
user.Integrations...; // (returns a list of `Integration` instances) |
This is a grouping of related issues. Feel free to vote (👍) for this issue to indicate that this is an area that you think we should spend time on, but consider also voting for individual issues for things you consider especially important.
This an "epic" issue for the theme of improvements to mapping of different CLR constructs to standard database concepts. Specific pieces of work will be tracked by linked issues.
Done in 7.0
Done in 8.0
Done in 9.0
Backlog
IImmutableList<T>
for collection navigational properties with backing fields? #21176Although we have had support for POCOs through a few major versions, using EF for persistence still places some undesired constraints on the design choices developers can make on their domain objects.
For example,
EF Core still requires that mapped CLR types can be instantiated through constructors without parameters(this is only true for navigation properties now), that they contain both property getters and setters for each mapped scalar and reference property, and that all mapped collection navigation properties are exposed as properties that are of a type that implements ICollection<T>.Note that POCO proxy functionality introduces additional constraints, e.g. lazy loading requires that property getters are virtual and change tracking proxies (not yet supported in EF Core) require that all properties are virtual at the same time that properties mapped to collections are effectively declared as ICollection<T>.
EF Core also requires each entity type in the model to be mapped to a distinct CLR type, which makes some dynamic model scenarios (#2282) harder.
Moreover, scalar properties on objects have to be of a small set of recognized types in order to be mapped (in EF Core the set of types supported nativly by the provider).
Richer constructs such as collections of scalars or collections of complex types, ordered collections, inheritance in complex types, collection manipulation methods, factory methods, and immutable objects are not supported.
This issue tracks the removal of those constrains as a whole and serves as a parent issue for some individual features that we will track independently:
An old hope
One of many interesting design strategies of EF as an O/RMs was that every EF model defined an abstract data model that is separate from the object model. When using EF in regular O/RM scenarios there are in fact three layered models involved: the storage/database model (often referred to as the S-space model), the conceptual/abstract data model (often referred to as the C-space) and the actual object model (also known as the O-space model). EF also makes use of two mapping specifications: the translation of data access operations between the C-space model and the S-space model is the most well-known and where most of the mapping capabilities of EF focus. The mapping between the C-space and the O-space model is less widely known and in fact since only trivial 1:1 mappings are supported.
There are many historical motivations for this underlying design, including the desire to provide a complete “weakly typed“ programming interface (often referred to as a “value layer”) that could be used in scenarios outside the traditional O/RM space, such as runtime metadata driven user interfaces and tools, database reporting, etc. EntityClient was intended to be such weakly typed API, but it had considerable restrictions (e.g. it was read-only) and some usability issues (e.g. it was designed after an ADO.NET provider without a public DbDataAdapter and with a quite complicated connection string format) on the first version of EF. Since its popularity never took off, the investments necessary to unleash the “value layer” were never completed. In the meanwhile, EF role in solving the traditional O/RM scenario of mapping strongly typed objects to database tables and functions became more and more relevant since EF4, so the lion share of the investments have gone into making EF easier to use in those scenarios, which in many cases meant burying the separation between the abstract data model and the object model even deeper under layers of new API.
In practice the object model can be considered for most intents and purposes a straight realization of what is in C-space. The most commonly used APIs on EF take advantage of this similarity and just conflates C-space and O-space seamlessly, abstracting away the fact that there is a layer of indirection. In other words, although this differentiation between C-space and O-space is real and very explicit in the implementation, it is purposely not evident in most of the EF APIs and usage patterns. A clear example of this is Code First. To a great degree, the usability of the Code First API relies on users being able to ignore the existence of an abstract data model.
While sharing the same meta-model for conceptual models has made it easier to expose EF models through things like OData services, the majority of EF users has never needed to know that there is a difference between the conceptual and object models of their applications.
Regardless of the past motivations for creating this extra layer of indirection, there is potential in the decoupling of the conceptual level (at which most of EF’s mapping, query and update pipeline operates) and the objects level. The basic idea is that by breaking the current status quo of 1:1 O-C mapping, it should be possible to extend EF to support much richer object mapping without having to touch at all the guts of the system.
The text was updated successfully, but these errors were encountered: