Skip to content

Commit

Permalink
Initial implementation of lazy-loading and entities with constructors
Browse files Browse the repository at this point in the history
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;
    }
}
```
  • Loading branch information
ajcvickers committed Jan 2, 2018
1 parent af8ea32 commit 953980c
Show file tree
Hide file tree
Showing 100 changed files with 5,210 additions and 721 deletions.
Expand Up @@ -3,6 +3,7 @@

using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using Microsoft.EntityFrameworkCore.Utilities;
Expand Down Expand Up @@ -51,7 +52,9 @@ public static ConventionSet Build()
new RelationalConventionSetBuilderDependencies(oracleTypeMapper, currentContext: null, setFinder: null))
.AddConventions(
new CoreConventionSetBuilder(
new CoreConventionSetBuilderDependencies(oracleTypeMapper))
new CoreConventionSetBuilderDependencies(
oracleTypeMapper,
new ConstructorBindingFactory()))
.CreateConventionSet());
}
}
Expand Down
@@ -0,0 +1,40 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.TestUtilities;

namespace Microsoft.EntityFrameworkCore
{
public class WithConstructorsOracleTest : WithConstructorsTestBase<WithConstructorsOracleTest.WithConstructorsOracleFixture>
{
public WithConstructorsOracleTest(WithConstructorsOracleFixture fixture)
: base(fixture)
{
}

protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction)
=> facade.UseTransaction(transaction.GetDbTransaction());

public class WithConstructorsOracleFixture : WithConstructorsFixtureBase
{
protected override ITestStoreFactory TestStoreFactory => OracleTestStoreFactory.Instance;

protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context)
{
base.OnModelCreating(modelBuilder, context);

modelBuilder.Entity<HasContext<DbContext>>().ToTable("HasContext_DbContext");
modelBuilder.Entity<HasContext<WithConstructorsContext>>().ToTable("HasContext_WithConstructorsContext");
modelBuilder.Entity<HasContext<OtherContext>>().ToTable("HasContext_OtherContext");

modelBuilder.Entity<Blog>(
b => { b.Property("_blogId").HasColumnName("BlogId"); });

modelBuilder.Entity<Post>(
b => { b.Property("_id").HasColumnName("Id"); });
}
}
}
}
Expand Up @@ -668,7 +668,8 @@ protected virtual void GeneratePropertyAnnotations([NotNull] IProperty property,
annotations,
RelationshipDiscoveryConvention.NavigationCandidatesAnnotationName,
RelationshipDiscoveryConvention.AmbiguousNavigationsAnnotationName,
InversePropertyAttributeConvention.InverseNavigationsAnnotationName);
InversePropertyAttributeConvention.InverseNavigationsAnnotationName,
CoreAnnotationNames.ConstructorBinding);

if (annotations.Any())
{
Expand Down
2 changes: 1 addition & 1 deletion src/EFCore.InMemory/Query/Internal/IMaterializerFactory.cs
Expand Up @@ -19,6 +19,6 @@ public interface IMaterializerFactory
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
Expression<Func<IEntityType, ValueBuffer, object>> CreateMaterializer([NotNull] IEntityType entityType);
Expression<Func<IEntityType, ValueBuffer, DbContext, object>> CreateMaterializer([NotNull] IEntityType entityType);
}
}
Expand Up @@ -49,7 +49,7 @@ public class InMemoryQueryModelVisitor : EntityQueryModelVisitor
QueryContext queryContext,
IEntityType entityType,
IKey key,
Func<IEntityType, ValueBuffer, object> materializer,
Func<IEntityType, ValueBuffer, DbContext, object> materializer,
bool queryStateManager)
where TEntity : class
=> ((InMemoryQueryContext)queryContext).Store
Expand All @@ -67,7 +67,8 @@ public class InMemoryQueryModelVisitor : EntityQueryModelVisitor
key,
new EntityLoadInfo(
valueBuffer,
vr => materializer(t.EntityType, vr)),
queryContext.Context,
(vr, c) => materializer(t.EntityType, vr, c)),
queryStateManager,
throwOnNullKey: false);
}));
Expand Down
21 changes: 13 additions & 8 deletions src/EFCore.InMemory/Query/Internal/MaterializerFactory.cs
Expand Up @@ -35,7 +35,7 @@ public MaterializerFactory([NotNull] IEntityMaterializerSource entityMaterialize
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual Expression<Func<IEntityType, ValueBuffer, object>> CreateMaterializer(IEntityType entityType)
public virtual Expression<Func<IEntityType, ValueBuffer, DbContext, object>> CreateMaterializer(IEntityType entityType)
{
Check.NotNull(entityType, nameof(entityType));

Expand All @@ -45,17 +45,21 @@ var entityTypeParameter
var valueBufferParameter
= Expression.Parameter(typeof(ValueBuffer), "valueBuffer");

var contextParameter
= Expression.Parameter(typeof(DbContext), "context");

var concreteEntityTypes
= entityType.GetConcreteTypesInHierarchy().ToList();

if (concreteEntityTypes.Count == 1)
{
return Expression.Lambda<Func<IEntityType, ValueBuffer, object>>(
return Expression.Lambda<Func<IEntityType, ValueBuffer, DbContext, object>>(
_entityMaterializerSource
.CreateMaterializeExpression(
concreteEntityTypes[0], valueBufferParameter),
concreteEntityTypes[0], valueBufferParameter, contextParameter),
entityTypeParameter,
valueBufferParameter);
valueBufferParameter,
contextParameter);
}

var returnLabelTarget = Expression.Label(typeof(object));
Expand All @@ -71,7 +75,7 @@ var blockExpressions
returnLabelTarget,
_entityMaterializerSource
.CreateMaterializeExpression(
concreteEntityTypes[0], valueBufferParameter))),
concreteEntityTypes[0], valueBufferParameter, contextParameter))),
Expression.Label(
returnLabelTarget,
Expression.Default(returnLabelTarget.Type))
Expand All @@ -87,14 +91,15 @@ var blockExpressions
Expression.Return(
returnLabelTarget,
_entityMaterializerSource
.CreateMaterializeExpression(concreteEntityType, valueBufferParameter)),
.CreateMaterializeExpression(concreteEntityType, valueBufferParameter, contextParameter)),
blockExpressions[0]);
}

return Expression.Lambda<Func<IEntityType, ValueBuffer, object>>(
return Expression.Lambda<Func<IEntityType, ValueBuffer, DbContext, object>>(
Expression.Block(blockExpressions),
entityTypeParameter,
valueBufferParameter);
valueBufferParameter,
contextParameter);
}
}
}
Expand Up @@ -28,7 +28,7 @@ public class BufferedEntityShaper<TEntity> : EntityShaper, IShaper<TEntity>
[NotNull] IQuerySource querySource,
bool trackingQuery,
[NotNull] IKey key,
[NotNull] Func<ValueBuffer, object> materializer,
[NotNull] Func<ValueBuffer, DbContext, object> materializer,
[CanBeNull] Dictionary<Type, int[]> typeIndexMap)
: base(querySource, trackingQuery, key, materializer)
{
Expand All @@ -53,7 +53,7 @@ var entity
= (TEntity)queryContext.QueryBuffer
.GetEntity(
Key,
new EntityLoadInfo(valueBuffer, Materializer, _typeIndexMap),
new EntityLoadInfo(valueBuffer, queryContext.Context, Materializer, _typeIndexMap),
queryStateManager: IsTrackingQuery,
throwOnNullKey: !AllowNullResult);

Expand Down
Expand Up @@ -25,7 +25,7 @@ public class BufferedOffsetEntityShaper<TEntity> : BufferedEntityShaper<TEntity>
[NotNull] IQuerySource querySource,
bool trackingQuery,
[NotNull] IKey key,
[NotNull] Func<ValueBuffer, object> materializer,
[NotNull] Func<ValueBuffer, DbContext, object> materializer,
[CanBeNull] Dictionary<Type, int[]> typeIndexMap)
: base(querySource, trackingQuery, key, materializer, typeIndexMap)
{
Expand Down
Expand Up @@ -23,7 +23,7 @@ public abstract class EntityShaper : Shaper
[NotNull] IQuerySource querySource,
bool trackingQuery,
[NotNull] IKey key,
[NotNull] Func<ValueBuffer, object> materializer)
[NotNull] Func<ValueBuffer, DbContext, object> materializer)
: base(querySource)
{
IsTrackingQuery = trackingQuery;
Expand All @@ -47,7 +47,7 @@ public abstract class EntityShaper : Shaper
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
protected virtual Func<ValueBuffer, object> Materializer { get; }
protected virtual Func<ValueBuffer, DbContext, object> Materializer { get; }

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
Expand Down
Expand Up @@ -22,7 +22,7 @@ public interface IMaterializerFactory
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
Expression<Func<ValueBuffer, object>> CreateMaterializer(
Expression<Func<ValueBuffer, DbContext, object>> CreateMaterializer(
[NotNull] IEntityType entityType,
[NotNull] SelectExpression selectExpression,
[NotNull] Func<IProperty, SelectExpression, int> projectionAdder,
Expand Down
Expand Up @@ -41,7 +41,7 @@ public class MaterializerFactory : IMaterializerFactory
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual Expression<Func<ValueBuffer, object>> CreateMaterializer(
public virtual Expression<Func<ValueBuffer, DbContext, object>> CreateMaterializer(
IEntityType entityType,
SelectExpression selectExpression,
Func<IProperty, SelectExpression, int> projectionAdder,
Expand All @@ -57,6 +57,9 @@ public class MaterializerFactory : IMaterializerFactory
var valueBufferParameter
= Expression.Parameter(typeof(ValueBuffer), "valueBuffer");

var contextParameter
= Expression.Parameter(typeof(DbContext), "context");

var concreteEntityTypes
= entityType.GetConcreteTypesInHierarchy().ToList();

Expand All @@ -72,12 +75,13 @@ var concreteEntityTypes
var materializer
= _entityMaterializerSource
.CreateMaterializeExpression(
concreteEntityTypes[0], valueBufferParameter, indexMap);
concreteEntityTypes[0], valueBufferParameter, contextParameter, indexMap);

if (concreteEntityTypes.Count == 1
&& concreteEntityTypes[0].RootType() == concreteEntityTypes[0])
{
return Expression.Lambda<Func<ValueBuffer, object>>(materializer, valueBufferParameter);
return Expression.Lambda<Func<ValueBuffer, DbContext, object>>(
materializer, valueBufferParameter, contextParameter);
}

var discriminatorProperty = concreteEntityTypes[0].Relational().DiscriminatorProperty;
Expand All @@ -98,7 +102,8 @@ var discriminatorPredicate
selectExpression.Predicate
= new DiscriminatorPredicateExpression(discriminatorPredicate, querySource);

return Expression.Lambda<Func<ValueBuffer, object>>(materializer, valueBufferParameter);
return Expression.Lambda<Func<ValueBuffer, DbContext, object>>(
materializer, valueBufferParameter, contextParameter);
}

var discriminatorValueVariable
Expand Down Expand Up @@ -160,7 +165,8 @@ var discriminatorValue

materializer
= _entityMaterializerSource
.CreateMaterializeExpression(concreteEntityType, valueBufferParameter, indexMap);
.CreateMaterializeExpression(
concreteEntityType, valueBufferParameter, contextParameter, indexMap);

blockExpressions[1]
= Expression.IfThenElse(
Expand All @@ -177,9 +183,10 @@ var discriminatorValue
selectExpression.Predicate
= new DiscriminatorPredicateExpression(discriminatorPredicate, querySource);

return Expression.Lambda<Func<ValueBuffer, object>>(
return Expression.Lambda<Func<ValueBuffer, DbContext, object>>(
Expression.Block(new[] { discriminatorValueVariable }, blockExpressions),
valueBufferParameter);
valueBufferParameter,
contextParameter);
}

private static readonly MethodInfo _createUnableToDiscriminateException
Expand Down
Expand Up @@ -24,7 +24,7 @@ public class UnbufferedEntityShaper<TEntity> : EntityShaper, IShaper<TEntity>
[NotNull] IQuerySource querySource,
bool trackingQuery,
[NotNull] IKey key,
[NotNull] Func<ValueBuffer, object> materializer)
[NotNull] Func<ValueBuffer, DbContext, object> materializer)
: base(querySource, trackingQuery, key, materializer)
{
}
Expand All @@ -51,7 +51,7 @@ public virtual TEntity Shape(QueryContext queryContext, ValueBuffer valueBuffer)
}
}

return (TEntity)Materializer(valueBuffer);
return (TEntity)Materializer(valueBuffer, queryContext.Context);
}

/// <summary>
Expand Down
Expand Up @@ -24,7 +24,7 @@ public class UnbufferedOffsetEntityShaper<TEntity> : UnbufferedEntityShaper<TEnt
[NotNull] IQuerySource querySource,
bool trackingQuery,
[NotNull] IKey key,
[NotNull] Func<ValueBuffer, object> materializer)
[NotNull] Func<ValueBuffer, DbContext, object> materializer)
: base(querySource, trackingQuery, key, materializer)
{
}
Expand Down
Expand Up @@ -340,7 +340,7 @@ var discriminatorPredicate
IQuerySource querySource,
bool trackingQuery,
IKey key,
Func<ValueBuffer, object> materializer,
Func<ValueBuffer, DbContext, object> materializer,
Dictionary<Type, int[]> typeIndexMap,
bool useQueryBuffer)
where TEntity : class
Expand Down
6 changes: 3 additions & 3 deletions src/EFCore.Specification.Tests/DatabindingTestBase.cs
Expand Up @@ -862,7 +862,7 @@ public virtual void Entity_added_to_context_is_added_to_navigation_property_bind
{
using (var context = CreateF1Context())
{
var ferrari = context.Teams.Include(t => t.Drivers).Single(t => t.Id == Team.Ferrari);
var ferrari = context.Teams.Single(t => t.Id == Team.Ferrari);
var navBindingList = ((IListSource)ferrari.Drivers).GetList();

var larry = new Driver
Expand All @@ -882,7 +882,7 @@ public virtual void Entity_added_to_navigation_property_binding_list_is_added_to
{
using (var context = CreateF1Context())
{
var ferrari = context.Teams.Include(t => t.Drivers).Single(t => t.Id == Team.Ferrari);
var ferrari = context.Teams.Single(t => t.Id == Team.Ferrari);
var navBindingList = ((IListSource)ferrari.Drivers).GetList();
var localDrivers = context.Drivers.Local;

Expand All @@ -909,7 +909,7 @@ public virtual void Entity_removed_from_navigation_property_binding_list_is_remo
{
using (var context = CreateF1Context())
{
var ferrari = context.Teams.Include(t => t.Drivers).Single(t => t.Id == Team.Ferrari);
var ferrari = context.Teams.Single(t => t.Id == Team.Ferrari);
var navBindingList = ((IListSource)ferrari.Drivers).GetList();
var localDrivers = context.Drivers.Local;

Expand Down

0 comments on commit 953980c

Please sign in to comment.