Skip to content

Commit

Permalink
Provide more information when exception thrown discovering/creating d…
Browse files Browse the repository at this point in the history
…esign-time DbContexts

Fixes #18715
  • Loading branch information
ajcvickers committed Jan 15, 2023
1 parent 5ba756e commit 99b7fa8
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 79 deletions.
147 changes: 81 additions & 66 deletions src/EFCore.Design/Design/Internal/DbContextOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -204,17 +204,26 @@ public virtual void Optimize(string? outputDir, string? modelNamespace, string?
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual DbContext CreateContext(string? contextType)
=> CreateContext(FindContextType(contextType).Value);

private DbContext CreateContext(Func<DbContext> factory)
{
var context = factory();
_reporter.WriteVerbose(DesignStrings.UseContext(context.GetType().ShortDisplayName()));
var factory = FindContextType(contextType).Value;
try
{
var context = factory();
_reporter.WriteVerbose(DesignStrings.UseContext(context.GetType().ShortDisplayName()));

var loggerFactory = context.GetService<ILoggerFactory>();
loggerFactory.AddProvider(new OperationLoggerProvider(_reporter));
var loggerFactory = context.GetService<ILoggerFactory>();
loggerFactory.AddProvider(new OperationLoggerProvider(_reporter));

return context;
return context;
}
catch (Exception ex)
{
if (ex is TargetInvocationException)
{
ex = ex.InnerException!;
}
throw new OperationException(DesignStrings.CannotCreateContextInstance(contextType, ex.Message), ex);
}
}

/// <summary>
Expand All @@ -241,73 +250,79 @@ private IDictionary<Type, Func<DbContext>> FindContextTypes()

var contexts = new Dictionary<Type, Func<DbContext>>();

// Look for IDesignTimeDbContextFactory implementations
_reporter.WriteVerbose(DesignStrings.FindingContextFactories);
var contextFactories = _startupAssembly.GetConstructibleTypes()
.Where(t => typeof(IDesignTimeDbContextFactory<DbContext>).IsAssignableFrom(t));
foreach (var factory in contextFactories)
try
{
_reporter.WriteVerbose(DesignStrings.FoundContextFactory(factory.ShortDisplayName()));
var manufacturedContexts =
from i in factory.ImplementedInterfaces
where i.IsGenericType
&& i.GetGenericTypeDefinition() == typeof(IDesignTimeDbContextFactory<>)
select i.GenericTypeArguments[0];
foreach (var context in manufacturedContexts)
// Look for IDesignTimeDbContextFactory implementations
_reporter.WriteVerbose(DesignStrings.FindingContextFactories);
var contextFactories = _startupAssembly.GetConstructibleTypes()
.Where(t => typeof(IDesignTimeDbContextFactory<DbContext>).IsAssignableFrom(t));
foreach (var factory in contextFactories)
{
_reporter.WriteVerbose(DesignStrings.FoundContextFactory(factory.ShortDisplayName()));
var manufacturedContexts =
from i in factory.ImplementedInterfaces
where i.IsGenericType
&& i.GetGenericTypeDefinition() == typeof(IDesignTimeDbContextFactory<>)
select i.GenericTypeArguments[0];
foreach (var context in manufacturedContexts)
{
_reporter.WriteVerbose(DesignStrings.FoundDbContext(context.ShortDisplayName()));
contexts.Add(
context,
() => CreateContextFromFactory(factory.AsType(), context));
}
}

// Look for DbContext classes registered in the service provider
var appServices = _appServicesFactory.Create(_args);
var registeredContexts = appServices.GetServices<DbContextOptions>()
.Select(o => o.ContextType);
foreach (var context in registeredContexts.Where(c => !contexts.ContainsKey(c)))
{
_reporter.WriteVerbose(DesignStrings.FoundDbContext(context.ShortDisplayName()));
contexts.Add(
context,
() => CreateContextFromFactory(factory.AsType(), context));
FindContextFactory(context)
?? FindContextFromRuntimeDbContextFactory(appServices, context)
?? (() => (DbContext)ActivatorUtilities.GetServiceOrCreateInstance(appServices, context)));
}
}

// Look for DbContext classes registered in the service provider
var appServices = _appServicesFactory.Create(_args);
var registeredContexts = appServices.GetServices<DbContextOptions>()
.Select(o => o.ContextType);
foreach (var context in registeredContexts.Where(c => !contexts.ContainsKey(c)))
{
_reporter.WriteVerbose(DesignStrings.FoundDbContext(context.ShortDisplayName()));
contexts.Add(
context,
FindContextFactory(context)
?? FindContextFromRuntimeDbContextFactory(appServices, context)
?? (() => (DbContext)ActivatorUtilities.GetServiceOrCreateInstance(appServices, context)));
// Look for DbContext classes in assemblies
_reporter.WriteVerbose(DesignStrings.FindingReferencedContexts);
var types = _startupAssembly.GetConstructibleTypes()
.Concat(_assembly.GetConstructibleTypes())
.ToList();

var contextTypes = types.Where(t => typeof(DbContext).IsAssignableFrom(t)).Select(
t => t.AsType())
.Concat(
types.Where(t => typeof(Migration).IsAssignableFrom(t))
.Select(t => t.GetCustomAttribute<DbContextAttribute>()?.ContextType)
.Where(t => t != null)
.Cast<Type>())
.Distinct();

foreach (var context in contextTypes.Where(c => !contexts.ContainsKey(c)))
{
_reporter.WriteVerbose(DesignStrings.FoundDbContext(context.ShortDisplayName()));
contexts.Add(
context,
FindContextFactory(context)
?? (() => (DbContext)ActivatorUtilities.GetServiceOrCreateInstance(appServices, context)));
}
}

// Look for DbContext classes in assemblies
_reporter.WriteVerbose(DesignStrings.FindingReferencedContexts);
var types = _startupAssembly.GetConstructibleTypes()
.Concat(_assembly.GetConstructibleTypes())
.ToList();

var contextTypes = types.Where(t => typeof(DbContext).IsAssignableFrom(t)).Select(
t => t.AsType())
.Concat(
types.Where(t => typeof(Migration).IsAssignableFrom(t))
.Select(t => t.GetCustomAttribute<DbContextAttribute>()?.ContextType)
.Where(t => t != null)
.Cast<Type>())
.Distinct();

foreach (var context in contextTypes.Where(c => !contexts.ContainsKey(c)))
catch (Exception ex)
{
_reporter.WriteVerbose(DesignStrings.FoundDbContext(context.ShortDisplayName()));
contexts.Add(
context,
FindContextFactory(context)
?? (() =>
{
try
{
return (DbContext)ActivatorUtilities.GetServiceOrCreateInstance(appServices, context);
}
catch (Exception ex)
{
throw new OperationException(DesignStrings.NoParameterlessConstructor(context.Name), ex);
}
}));
if (ex is OperationException)
{
throw;
}

if (ex is TargetInvocationException)
{
ex = ex.InnerException!;
}
throw new OperationException(DesignStrings.CannotFindDbContextTypes(ex.Message), ex);
}

return contexts;
Expand Down
25 changes: 16 additions & 9 deletions src/EFCore.Design/Properties/DesignStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 7 additions & 4 deletions src/EFCore.Design/Properties/DesignStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@
<data name="BundleFullName" xml:space="preserve">
<value>Entity Framework Core Migrations Bundle</value>
</data>
<data name="CannotCreateContextInstance" xml:space="preserve">
Unable to create a 'DbContext' of type '{contextType}'. The exception '{rootException}' was thrown while attempting to create an instance. For the different patterns supported at design time, see https://go.microsoft.com/fwlink/?linkid=851728
</data>
<data name="CannotFindDbContextTypes" xml:space="preserve">
The exception '{rootException}' was thrown while attempting to find 'DbContext' types. For the different patterns supported at design time, see https://go.microsoft.com/fwlink/?linkid=851728
</data>
<data name="CannotFindDesignTimeProviderAssemblyAttribute" xml:space="preserve">
<value>Unable to find expected assembly attribute [DesignTimeProviderServices] in provider assembly '{runtimeProviderAssemblyName}'. This attribute is required to identify the class which acts as the design-time service provider factory for the provider.</value>
</data>
Expand Down Expand Up @@ -326,9 +332,6 @@ Change your target project to the migrations project by using the Package Manage
<data name="NonRelationalProvider" xml:space="preserve">
<value>The provider '{provider}' is not a Relational provider and therefore cannot be used with Migrations.</value>
</data>
<data name="NoParameterlessConstructor" xml:space="preserve">
<value>Unable to create an object of type '{contextType}'. For the different patterns supported at design time, see https://go.microsoft.com/fwlink/?linkid=851728</value>
</data>
<data name="NoReferencedServices" xml:space="preserve">
<value>No referenced design-time services were found.</value>
</data>
Expand Down Expand Up @@ -438,4 +441,4 @@ Change your target project to the migrations project by using the Package Manage
<data name="WritingSnapshot" xml:space="preserve">
<value>Writing model snapshot to '{file}'.</value>
</data>
</root>
</root>
43 changes: 43 additions & 0 deletions test/EFCore.Design.Tests/Design/DbContextActivatorTest.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.EntityFrameworkCore.Internal;

namespace Microsoft.EntityFrameworkCore.Design;

public class DbContextActivatorTest
Expand All @@ -25,4 +27,45 @@ protected override void OnConfiguring(DbContextOptionsBuilder options)
.EnableServiceProviderCaching(false)
.UseInMemoryDatabase(nameof(DbContextActivatorTest));
}

[ConditionalFact]
public void CreateInstance_throws_if_constructor_throws()
=> Assert.Equal(
DesignStrings.CannotCreateContextInstance(typeof(ThrowingTestContext).FullName, "Bang!"),
Assert.Throws<OperationException>(() => DbContextActivator.CreateInstance(typeof(ThrowingTestContext))).Message);

private class ThrowingTestContext : DbContext
{
public ThrowingTestContext()
{
throw new Exception("Bang!");
}

protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options
.EnableServiceProviderCaching(false)
.UseInMemoryDatabase(nameof(DbContextActivatorTest));
}

[ConditionalFact]
public void CreateInstance_throws_if_constructor_not_parameterless()
{
var message = Assert.Throws<OperationException>(
() => DbContextActivator.CreateInstance(typeof(ParameterTestContext))).Message;

Assert.StartsWith(DesignStrings.CannotCreateContextInstance(nameof(ParameterTestContext), "").Substring(0, 10), message);
Assert.Contains("Microsoft.EntityFrameworkCore.Design.DbContextActivatorTest+ParameterTestContext", message);
}

private class ParameterTestContext : DbContext
{
public ParameterTestContext(string foo)
{
}

protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options
.EnableServiceProviderCaching(false)
.UseInMemoryDatabase(nameof(DbContextActivatorTest));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,19 @@ public void GetContextInfo_does_not_throw_if_provider_not_relational()
Assert.Equal("Microsoft.EntityFrameworkCore.InMemory", info.ProviderName);
}

[ConditionalFact]
public void Useful_exception_if_finding_context_types_throws()
=> Assert.Equal(
DesignStrings.CannotFindDbContextTypes("Bang!"),
Assert.Throws<OperationException>(
() => CreateOperations(typeof(ThrowingTestProgram)).CreateContext(typeof(TestContext).FullName)).Message);

private static class ThrowingTestProgram
{
private static TestWebHost BuildWebHost(string[] args)
=> CreateWebHost(_ => throw new Exception("Bang!"));
}

private static class TestProgram
{
private static TestWebHost BuildWebHost(string[] args)
Expand Down

0 comments on commit 99b7fa8

Please sign in to comment.