Skip to content
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

Open
5 of 47 tasks
rowanmiller opened this issue May 22, 2014 · 39 comments
Open
5 of 47 tasks

Flexible mapping to CLR types and members (Custom O/C Mapping) #240

rowanmiller opened this issue May 22, 2014 · 39 comments
Labels
area-o/c-mapping composite-issue A grouping of multiple related issues into one issue type-enhancement
Milestone

Comments

@rowanmiller
Copy link
Contributor

rowanmiller commented May 22, 2014

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


Although 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.

@rowanmiller
Copy link
Contributor Author

Here is a good example of this feature (good case to verify against once we implement it) - #1009

@divega
Copy link
Contributor

divega commented Sep 1, 2015

@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?

@rowanmiller
Copy link
Contributor Author

@divega yep that works

@divega
Copy link
Contributor

divega commented Sep 1, 2015

Otherwise I can merge it all here...

@divega divega changed the title Flexible mapping to CLR types/properties/methods/etc. (O/C Mapping) Flexible object mapping to CLR types and members (Custom O/C Mapping) Sep 1, 2015
@divega
Copy link
Contributor

divega commented Sep 1, 2015

Made some edits to the original text of the issue to add background and additional links to related issues.

@biqas
Copy link

biqas commented Mar 8, 2016

Hi,
the following example is for Illustration how maybe interface definition could be used to map EF structures.

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.

@bjorn-ali-goransson
Copy link

bjorn-ali-goransson commented Jul 30, 2016

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 IClrPropertyGetter/IClrPropertySetter infrastructure is already there.

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:

  1. Clr type (not needed in practice as PropertyInfo.SetValue takes object)
  2. Resulting primitive backing type (for DB provider)
  3. Getter Serializer (converts from Clr type to Resulting primitive type)
  4. Setter Parser (converts from Resulting primitive type to Clr type)

And the type check mentioned above needs to not happen if(Flexible).

Here's a (hopefully somewhat) functioning example of what I'd like to achieve:

public class Context : DbContext
{
    private static readonly IServiceProvider _serviceProvider = new ServiceCollection()
        .AddEntityFrameworkSqlServer()
        .AddSingleton<ICoreConventionSetBuilder, MyCoreConventionSetBuilder>()
        .BuildServiceProvider();

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder
            .UseInternalServiceProvider(_serviceProvider)
            .UseSqlServer(@"Data Source=.\SQLEXPRESS2014;Initial Catalog=EfCoreTest;Integrated Security=True;");
    }

    private class MyCoreConventionSetBuilder : CoreConventionSetBuilder
    {
        public override ConventionSet CreateConventionSet()
        {
            var value = base.CreateConventionSet();
            value.EntityTypeAddedConventions.Add(new MyPropertyDiscoveryConvention());
            return value;
        }
    }

    private class MyPropertyDiscoveryConvention : IEntityTypeConvention
    {
        public InternalEntityTypeBuilder Apply(InternalEntityTypeBuilder entityTypeBuilder)
        {
            entityTypeBuilder.Metadata.AddProperty(
                "Ancestors",
                propertyType: typeof(string),
                flexible: true,
                serialize: JsonConvert.Serialize,
                parse: value => JsonConvert.Parse<List<int>>((string)value)
            );

            return entityTypeBuilder;
        }
    }

    public DbSet<Page> Pages { get; set; }

    public class Page
    {
        public int Id { get; set; }
        public List<int> Ancestors { get; set; }
    }
}

@bjorn-ali-goransson
Copy link

@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 AddFlexibleProperty method rather than further overloading the AddProperty.

ajcvickers added a commit that referenced this issue Jan 3, 2018
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;
    }
}
```
ajcvickers added a commit that referenced this issue Jan 3, 2018
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;
    }
}
```
@smarts
Copy link

smarts commented Mar 8, 2019

Which one of the issues in this issue's description handles support for unsigned integer types?

@ajcvickers
Copy link
Contributor

@smarts Unsigned integer types are supported as of EF Core 2.1.

@smarts
Copy link

smarts commented Mar 10, 2019

@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 Int32 instead of UInt32. Do you know if this feature works for shadow properties too? Also, does it work for any DB integer type?
Our specific use-cases are SQL Server and PostgreSQL

@ajcvickers
Copy link
Contributor

@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.

@smarts
Copy link

smarts commented Mar 28, 2019

@ajcvickers sorry for the delay in responding.

In this case the value is stored in the database as a bigger signed type, by default.

Does that mean that using UInt32 for an entity's property will only work if the DB type for the corresponding column supports the full range of values for UInt32? I.e., we want to use INT in the DB and UInt32 on the entity.
While I understand that this conversion is generally bad, in the specific case of [generated] IDs the risk becomes negligible. Admittedly, this is based on my minimal experience with SQL DBs, so I apologize if this assumption is incorrect.
Is it possible to allow this generally-risky conversion for the negligible-risk scenario of IDs via some custom EF Convention?

@roji
Copy link
Member

roji commented Mar 29, 2019

@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).

@markusschaber
Copy link

markusschaber commented Mar 29, 2019

@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).
SQL Server, PostgreSQL and MySQL provide "int" as a 32 bit signed data type, so trying to store an uint32 there via C-Style cast looks feasible. (I didn't check other DBs).

@roji
Copy link
Member

roji commented Mar 29, 2019

@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.

@smarts
Copy link

smarts commented Mar 29, 2019

@markusschaber i'm not sure if you noticed, but i'm specifically talking about auto-generated IDs in the database. Using long for the entity's property type has the same problem (allowing negative values), which isn't what I want. Also, as far as large values… since the value only ever comes from the DB (i.e., not set by any C# code outside of EF Core reading DB values), it will never be larger than Int32.MaxValue (because INT is the DB type).

@ajcvickers
Copy link
Contributor

Just to close the loop on this. When deciding how to convert automatically, we look at:

  • Is is lossy?
  • Does it preserve semantics on the database--most notably ordering?

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

@smitpatel
Copy link
Contributor

@ajcvickers - What exactly needs-cleanup label is?

@voroninp
Copy link

Proposed for 6.0

Does it mean not earlier than November 2021?

@roji
Copy link
Member

roji commented Oct 15, 2020

@voroninp yes, 6.0 would mean November 2021. Previews would make this available much earlier, though.

@atrauzzi
Copy link

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:

Integration
IIntegratable
User : IIIntegratable
Organization : IIntegratable

Therefore, the table integration in the database gets a integratable_type and integratable_id column, then I could go:

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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-o/c-mapping composite-issue A grouping of multiple related issues into one issue type-enhancement
Projects
None yet
Development

No branches or pull requests