Skip to content

Commit

Permalink
Avoid using ActivatorServices for common DbContext constructor (#21254)
Browse files Browse the repository at this point in the history
Improvement to #18575

This change checks if the DbContext has the usual constructor taking just options:

```C#
MyContext(DbContextOptions<MyContext> options)
```

If it does, then we create a delegate (once in a singleton service) and use that to create context instances. This avoids using ActivatorServices and falling out of constructor injection (other than the delegate) for the common case.
  • Loading branch information
ajcvickers committed Jun 13, 2020
1 parent d814eba commit 63d84c9
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,8 @@ public static IServiceCollection AddDbContextFactory<TContext, TFactory>(
{
AddCoreServices<TContext>(serviceCollection, optionsAction, lifetime);

serviceCollection.AddSingleton<IDbContextFactorySource<TContext>, DbContextFactorySource<TContext>>();

serviceCollection.TryAdd(
new ServiceDescriptor(
typeof(IDbContextFactory<TContext>),
Expand Down
20 changes: 14 additions & 6 deletions src/EFCore/Internal/DbContextFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using System;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Utilities;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.EntityFrameworkCore.Internal
{
Expand All @@ -17,19 +16,28 @@ namespace Microsoft.EntityFrameworkCore.Internal
public class DbContextFactory<TContext> : IDbContextFactory<TContext>
where TContext : DbContext
{
private readonly IServiceProvider _provider;
private readonly IServiceProvider _serviceProvider;
private readonly DbContextOptions<TContext> _options;
private readonly Func<IServiceProvider, DbContextOptions<TContext>, TContext> _factory;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public DbContextFactory([NotNull] IServiceProvider provider)
public DbContextFactory(
[NotNull] IServiceProvider serviceProvider,
[NotNull] DbContextOptions<TContext> options,
[NotNull] IDbContextFactorySource<TContext> factorySource)
{
Check.NotNull(provider, nameof(provider));
Check.NotNull(serviceProvider, nameof(serviceProvider));
Check.NotNull(options, nameof(options));
Check.NotNull(factorySource, nameof(factorySource));

_provider = provider;
_serviceProvider = serviceProvider;
_options = options;
_factory = factorySource.Factory;
}

/// <summary>
Expand All @@ -39,6 +47,6 @@ public DbContextFactory([NotNull] IServiceProvider provider)
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual TContext CreateDbContext()
=> ActivatorUtilities.CreateInstance<TContext>(_provider);
=> _factory(_serviceProvider, _options);
}
}
73 changes: 73 additions & 0 deletions src/EFCore/Internal/DbContextFactorySource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// 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 System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.EntityFrameworkCore.Internal
{
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public class DbContextFactorySource<TContext> : IDbContextFactorySource<TContext>
where TContext : DbContext
{
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public DbContextFactorySource()
=> Factory = CreateActivator();

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual Func<IServiceProvider, DbContextOptions<TContext>, TContext> Factory { get; }

private static Func<IServiceProvider, DbContextOptions<TContext>, TContext> CreateActivator()
{
var constructors
= typeof(TContext).GetTypeInfo().DeclaredConstructors
.Where(c => !c.IsStatic && c.IsPublic)
.ToArray();

if (constructors.Length == 1)
{
var parameters = constructors[0].GetParameters();

if (parameters.Length == 1)
{
var isGeneric = parameters[0].ParameterType == typeof(DbContextOptions<TContext>);
if (isGeneric
|| parameters[0].ParameterType == typeof(DbContextOptions))
{
var optionsParam = Expression.Parameter(typeof(DbContextOptions<TContext>), "options");
var providerParam = Expression.Parameter(typeof(IServiceProvider), "provider");

return Expression.Lambda<Func<IServiceProvider, DbContextOptions<TContext>, TContext>>(
Expression.New(
constructors[0],
isGeneric
? optionsParam
: (Expression)Expression.Convert(optionsParam, typeof(DbContextOptions))),
providerParam, optionsParam)
.Compile();
}
}
}

return (p, _) => ActivatorUtilities.CreateInstance<TContext>(p);
}
}
}
25 changes: 25 additions & 0 deletions src/EFCore/Internal/IDbContextFactorySource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// 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 System;

namespace Microsoft.EntityFrameworkCore.Internal
{
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public interface IDbContextFactorySource<TContext>
where TContext : DbContext
{
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
Func<IServiceProvider, DbContextOptions<TContext>, TContext> Factory { get; }
}
}
18 changes: 7 additions & 11 deletions test/EFCore.Tests/DbContextFactoryTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ public WestwardHoContext(DbContextOptions options, Random random)
}

[ConditionalFact]
public void Can_register_factories_for_multiple_contexts()
public void Can_register_factories_for_multiple_contexts_even_with_non_generic_options()
{
var serviceProvider = (IServiceProvider)new ServiceCollection()
.AddDbContextFactory<CroydeContext>(
Expand All @@ -333,15 +333,11 @@ public void Can_register_factories_for_multiple_contexts()
b => b.UseInMemoryDatabase(nameof(ClovellyContext)))
.BuildServiceProvider(validateScopes: true);

Assert.Equal(
CoreStrings.NonGenericOptions(nameof(CroydeContext)),
Assert.Throws<InvalidOperationException>(
()
=>
{
using var context1 = serviceProvider.GetService<IDbContextFactory<CroydeContext>>().CreateDbContext();
using var context2 = serviceProvider.GetService<IDbContextFactory<ClovellyContext>>().CreateDbContext();
}).Message);
using var context1 = serviceProvider.GetService<IDbContextFactory<CroydeContext>>().CreateDbContext();
using var context2 = serviceProvider.GetService<IDbContextFactory<ClovellyContext>>().CreateDbContext();

Assert.Equal(nameof(CroydeContext), GetStoreName(context1));
Assert.Equal(nameof(ClovellyContext), GetStoreName(context2));
}

private class ClovellyContext : DbContext
Expand All @@ -353,7 +349,7 @@ public ClovellyContext(DbContextOptions options)
}

[ConditionalFact]
public void Throws_for_multiple_non_generic_builders()
public void Can_register_factories_for_multiple_contexts()
{
var serviceProvider = (IServiceProvider)new ServiceCollection()
.AddDbContextFactory<WidemouthBayContext>(
Expand Down

0 comments on commit 63d84c9

Please sign in to comment.