diff --git a/src/EFCore.Design/Scaffolding/Internal/CSharpRuntimeModelCodeGenerator.cs b/src/EFCore.Design/Scaffolding/Internal/CSharpRuntimeModelCodeGenerator.cs index 14a7e6651f7..8c11c285844 100644 --- a/src/EFCore.Design/Scaffolding/Internal/CSharpRuntimeModelCodeGenerator.cs +++ b/src/EFCore.Design/Scaffolding/Internal/CSharpRuntimeModelCodeGenerator.cs @@ -164,24 +164,18 @@ private string CreateModel( using (mainBuilder.Indent()) { mainBuilder - .Append("private static ").Append(className).AppendLine(nullable ? "? _instance;" : " _instance;") - .Append("public static IModel Instance") - .AppendLines(@" + .Append("static ").Append(className).Append("()") + .AppendLines( + @" { - get - { - if (_instance == null) - { - _instance = new " + className + @"(); - _instance.Initialize(); - _instance.Customize(); - } - - return _instance; - } -}"); - - mainBuilder + var model = new " + className + @"(); + model.Initialize(); + model.Customize(); + _instance = model; +}") + .AppendLine() + .Append("private static ").Append(className).AppendLine(nullable ? "? _instance;" : " _instance;") + .AppendLine("public static IModel Instance => _instance;") .AppendLine() .AppendLine("partial void Initialize();") .AppendLine() diff --git a/src/EFCore.Relational/Infrastructure/RelationalModelRuntimeInitializer.cs b/src/EFCore.Relational/Infrastructure/RelationalModelRuntimeInitializer.cs index 377dbcc2795..e350afda1ce 100644 --- a/src/EFCore.Relational/Infrastructure/RelationalModelRuntimeInitializer.cs +++ b/src/EFCore.Relational/Infrastructure/RelationalModelRuntimeInitializer.cs @@ -57,7 +57,7 @@ protected override void InitializeModel(IModel model, bool designTime, bool prev { if (prevalidation) { - model.AddRuntimeAnnotation(RelationalAnnotationNames.ModelDependencies, RelationalDependencies.RelationalModelDependencies); + model.SetRuntimeAnnotation(RelationalAnnotationNames.ModelDependencies, RelationalDependencies.RelationalModelDependencies); } else { diff --git a/src/EFCore/Infrastructure/ModelRuntimeInitializer.cs b/src/EFCore/Infrastructure/ModelRuntimeInitializer.cs index 1f31ae74769..15b6091c6ee 100644 --- a/src/EFCore/Infrastructure/ModelRuntimeInitializer.cs +++ b/src/EFCore/Infrastructure/ModelRuntimeInitializer.cs @@ -25,6 +25,8 @@ namespace Microsoft.EntityFrameworkCore.Infrastructure /// public class ModelRuntimeInitializer : IModelRuntimeInitializer { + private static readonly object _syncObject = new(); + /// /// Creates a new instance. /// @@ -56,59 +58,55 @@ public virtual IModel Initialize( if (model is Model mutableModel && !mutableModel.IsReadOnly) { - model = mutableModel.FinalizeModel(); + lock (_syncObject) + { + if (!mutableModel.IsReadOnly) + { + model = mutableModel.FinalizeModel(); + } + } } if (model.ModelDependencies == null) { - model = model.GetOrAddRuntimeAnnotationValue( - CoreAnnotationNames.ReadOnlyModel, - static args => + // Make sure InitializeModel really only gets called once, since it may not be thread safe or idempotent. + lock (_syncObject) + { + if (model.ModelDependencies == null) { - var (initializer, model, designTime, validationLogger) = args; + model.ModelDependencies = Dependencies.ModelDependencies; - model.ModelDependencies = initializer.Dependencies.ModelDependencies; - - initializer.InitializeModel(model, designTime, prevalidation: true); + InitializeModel(model, designTime, prevalidation: true); if (validationLogger != null && model is IConventionModel) { - initializer.Dependencies.ModelValidator.Validate(model, validationLogger); - } - - initializer.InitializeModel(model, designTime, prevalidation: false); - - if (!designTime - && model is Model mutableModel) - { - model = mutableModel.OnModelFinalized(); + Dependencies.ModelValidator.Validate(model, validationLogger); } - return model; - }, - (this, model, designTime, validationLogger)); - - if (designTime) - { - model.RemoveRuntimeAnnotation(CoreAnnotationNames.ReadOnlyModel); + InitializeModel(model, designTime, prevalidation: false); + } } } - else if (!designTime) + + if (designTime) { - model = model.GetOrAddRuntimeAnnotationValue( - CoreAnnotationNames.ReadOnlyModel, - static model => + return model; + } + + model = model.GetOrAddRuntimeAnnotationValue( + CoreAnnotationNames.ReadOnlyModel, + static model => + { + if (model is Model mutableModel) { - if (model is Model mutableModel) - { - model = mutableModel.OnModelFinalized(); - } + // This assumes OnModelFinalized is thread-safe + model = mutableModel.OnModelFinalized(); + } - return model!; - }, - model); - } + return model!; + }, + model); return model; } diff --git a/src/EFCore/Metadata/Conventions/IModelFinalizedConvention.cs b/src/EFCore/Metadata/Conventions/IModelFinalizedConvention.cs index fa5e4652e1a..70f524a9d94 100644 --- a/src/EFCore/Metadata/Conventions/IModelFinalizedConvention.cs +++ b/src/EFCore/Metadata/Conventions/IModelFinalizedConvention.cs @@ -9,7 +9,8 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Conventions public interface IModelFinalizedConvention : IConvention { /// - /// Called after a model is finalized and can no longer be mutated. + /// Called after a model is finalized and can no longer be mutated. + /// The implementation must be thread-safe. /// /// The model. IModel ProcessModelFinalized(IModel model); diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs index 01d1e8c1456..7f436fa4768 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs @@ -55,22 +55,17 @@ namespace TestNamespace [DbContext(typeof(CSharpRuntimeModelCodeGeneratorTest.EmptyContext))] partial class EmptyContextModel : RuntimeModel { - private static EmptyContextModel _instance; - public static IModel Instance + static EmptyContextModel() { - get - { - if (_instance == null) - { - _instance = new EmptyContextModel(); - _instance.Initialize(); - _instance.Customize(); - } - - return _instance; - } + var model = new EmptyContextModel(); + model.Initialize(); + model.Customize(); + _instance = model; } + private static EmptyContextModel _instance; + public static IModel Instance => _instance; + partial void Initialize(); partial void Customize(); @@ -476,22 +471,17 @@ namespace TestNamespace [DbContext(typeof(CSharpRuntimeModelCodeGeneratorTest.BigContext))] partial class BigContextModel : RuntimeModel { - private static BigContextModel? _instance; - public static IModel Instance + static BigContextModel() { - get - { - if (_instance == null) - { - _instance = new BigContextModel(); - _instance.Initialize(); - _instance.Customize(); - } - - return _instance; - } + var model = new BigContextModel(); + model.Initialize(); + model.Customize(); + _instance = model; } + private static BigContextModel? _instance; + public static IModel Instance => _instance; + partial void Initialize(); partial void Customize(); @@ -1853,22 +1843,17 @@ namespace TestNamespace [DbContext(typeof(CSharpRuntimeModelCodeGeneratorTest.DbFunctionContext))] partial class DbFunctionContextModel : RuntimeModel { - private static DbFunctionContextModel _instance; - public static IModel Instance + static DbFunctionContextModel() { - get - { - if (_instance == null) - { - _instance = new DbFunctionContextModel(); - _instance.Initialize(); - _instance.Customize(); - } - - return _instance; - } + var model = new DbFunctionContextModel(); + model.Initialize(); + model.Customize(); + _instance = model; } + private static DbFunctionContextModel _instance; + public static IModel Instance => _instance; + partial void Initialize(); partial void Customize(); @@ -2343,22 +2328,17 @@ namespace TestNamespace [DbContext(typeof(CSharpRuntimeModelCodeGeneratorTest.SequencesContext))] partial class SequencesContextModel : RuntimeModel { - private static SequencesContextModel _instance; - public static IModel Instance + static SequencesContextModel() { - get - { - if (_instance == null) - { - _instance = new SequencesContextModel(); - _instance.Initialize(); - _instance.Customize(); - } - - return _instance; - } + var model = new SequencesContextModel(); + model.Initialize(); + model.Customize(); + _instance = model; } + private static SequencesContextModel _instance; + public static IModel Instance => _instance; + partial void Initialize(); partial void Customize(); @@ -2551,22 +2531,17 @@ namespace TestNamespace [DbContext(typeof(CSharpRuntimeModelCodeGeneratorTest.ConstraintsContext))] partial class ConstraintsContextModel : RuntimeModel { - private static ConstraintsContextModel _instance; - public static IModel Instance + static ConstraintsContextModel() { - get - { - if (_instance == null) - { - _instance = new ConstraintsContextModel(); - _instance.Initialize(); - _instance.Customize(); - } - - return _instance; - } + var model = new ConstraintsContextModel(); + model.Initialize(); + model.Customize(); + _instance = model; } + private static ConstraintsContextModel _instance; + public static IModel Instance => _instance; + partial void Initialize(); partial void Customize(); @@ -2707,22 +2682,17 @@ namespace Microsoft.EntityFrameworkCore.Metadata [DbContext(typeof(CSharpRuntimeModelCodeGeneratorTest.SqliteContext))] partial class SqliteContextModel : RuntimeModel { - private static SqliteContextModel _instance; - public static IModel Instance + static SqliteContextModel() { - get - { - if (_instance == null) - { - _instance = new SqliteContextModel(); - _instance.Initialize(); - _instance.Customize(); - } - - return _instance; - } + var model = new SqliteContextModel(); + model.Initialize(); + model.Customize(); + _instance = model; } + private static SqliteContextModel _instance; + public static IModel Instance => _instance; + partial void Initialize(); partial void Customize(); @@ -2887,22 +2857,17 @@ namespace TestNamespace [DbContext(typeof(CSharpRuntimeModelCodeGeneratorTest.CosmosContext))] partial class CosmosContextModel : RuntimeModel { - private static CosmosContextModel _instance; - public static IModel Instance + static CosmosContextModel() { - get - { - if (_instance == null) - { - _instance = new CosmosContextModel(); - _instance.Initialize(); - _instance.Customize(); - } - - return _instance; - } + var model = new CosmosContextModel(); + model.Initialize(); + model.Customize(); + _instance = model; } + private static CosmosContextModel _instance; + public static IModel Instance => _instance; + partial void Initialize(); partial void Customize(); diff --git a/test/EFCore.SqlServer.Tests/SqlServerOptionsExtensionTest.cs b/test/EFCore.SqlServer.Tests/SqlServerOptionsExtensionTest.cs index a0a122fe984..59e991c60fc 100644 --- a/test/EFCore.SqlServer.Tests/SqlServerOptionsExtensionTest.cs +++ b/test/EFCore.SqlServer.Tests/SqlServerOptionsExtensionTest.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; using Microsoft.Extensions.DependencyInjection; @@ -11,6 +13,54 @@ namespace Microsoft.EntityFrameworkCore { public class SqlServerOptionsExtensionTest { + [ConditionalFact] + public void Compiled_model_is_thread_safe() + { + var tasks = new Task[Environment.ProcessorCount]; + for (var i = 0; i < tasks.Length; i++) + { + tasks[i] = Task.Factory.StartNew(() => + { + using (var ctx = new EmptyContext()) + { + Assert.NotNull(ctx.Model.GetRelationalDependencies()); + } + }); + } + + Task.WaitAll(tasks); + } + + private class EmptyContext : DbContext + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + if (!optionsBuilder.IsConfigured) + { + optionsBuilder.UseSqlServer().UseModel(EmptyContextModel.Instance); + } + } + } + + [DbContext(typeof(EmptyContext))] + partial class EmptyContextModel : RuntimeModel + { + static EmptyContextModel() + { + var model = new EmptyContextModel(); + model.Initialize(); + model.Customize(); + _instance = model; + } + + private static readonly EmptyContextModel _instance; + public static IModel Instance => _instance; + + partial void Initialize(); + + partial void Customize(); + } + [ConditionalFact] public void ApplyServices_adds_SQL_server_services() {