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
- 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 Dec 31, 2017
1 parent a62607e commit 5fb1e32
Show file tree
Hide file tree
Showing 96 changed files with 5,023 additions and 707 deletions.
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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"); });
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -668,7 +668,8 @@ protected virtual void GenerateEntityTypeAnnotations(
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
Original file line number Diff line number Diff line change
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ private static IEnumerable<TEntity> EntityQuery<TEntity>(
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 @@ private static IEnumerable<TEntity> EntityQuery<TEntity>(
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
Original file line number Diff line number Diff line change
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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public BufferedEntityShaper(
[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
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public BufferedOffsetEntityShaper(
[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
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ protected EntityShaper(
[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 @@ protected EntityShaper(
/// 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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public MaterializerFactory(
/// 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 virtual Expression<Func<ValueBuffer, object>> CreateMaterializer(
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
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public UnbufferedEntityShaper(
[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
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public UnbufferedOffsetEntityShaper(
[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
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ private static IShaper<TEntity> CreateEntityShaper<TEntity>(
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
Original file line number Diff line number Diff line change
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
Loading

0 comments on commit 5fb1e32

Please sign in to comment.