From ab55cde31b7f468e750bc20793d94bac78adf344 Mon Sep 17 00:00:00 2001 From: "Dailey, Alaric" Date: Thu, 29 Oct 2026 09:10:12 -0500 Subject: [PATCH] Register the MySQL update SQL generator as a singleton IUpdateSqlGenerator is a singleton service in EF Core, and the provider registers it as a forward to IMySQLUpdateSqlGenerator, which was scoped. A singleton depending on a scoped service is a captive dependency: with scope validation on (ASP.NET Core in development, EF's own test fixtures), resolving it throws "Cannot resolve scoped service 'MySql.EntityFrameworkCore.Update.IMySQLUpdateSqlGenerator' from root provider", so EnsureCreated and SaveChanges fail on a context that uses the application's service provider. MySQLUpdateSqlGenerator holds no per-scope state and depends only on the singleton UpdateSqlGeneratorDependencies, so register it as a singleton, as EF Core's SQL Server provider registers ISqlServerUpdateSqlGenerator. Co-Authored-By: Claude Opus 5.5 --- .../MySQLServiceCollectionExtensions.cs | 2 +- .../ServiceLifetimeTests.cs | 104 ++++++++++++++++++ 2 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 EFCore/tests/MySql.EFCore.Basic.Tests/ServiceLifetimeTests.cs diff --git a/EFCore/src/Extensions/MySQLServiceCollectionExtensions.cs b/EFCore/src/Extensions/MySQLServiceCollectionExtensions.cs index 91e6f6fd6..fe6d3ccf2 100644 --- a/EFCore/src/Extensions/MySQLServiceCollectionExtensions.cs +++ b/EFCore/src/Extensions/MySQLServiceCollectionExtensions.cs @@ -161,7 +161,7 @@ public static IServiceCollection AddEntityFrameworkMySQL(this IServiceCollection .TryAddProviderSpecificServices(m => m .TryAddSingleton() .TryAddScoped() - .TryAddScoped()); + .TryAddSingleton()); builder.TryAddCoreServices(); diff --git a/EFCore/tests/MySql.EFCore.Basic.Tests/ServiceLifetimeTests.cs b/EFCore/tests/MySql.EFCore.Basic.Tests/ServiceLifetimeTests.cs new file mode 100644 index 000000000..6adb741d5 --- /dev/null +++ b/EFCore/tests/MySql.EFCore.Basic.Tests/ServiceLifetimeTests.cs @@ -0,0 +1,104 @@ +// Copyright © 2026, Oracle and/or its affiliates. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0, as +// published by the Free Software Foundation. +// +// This program is designed to work with certain software (including +// but not limited to OpenSSL) that is licensed under separate terms, as +// designated in a particular file or component or in included license +// documentation. The authors of MySQL hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have either included with +// the program or referenced in the documentation. +// +// Without limiting anything contained in the foregoing, this file, +// which is part of MySQL Connector/NET, is also subject to the +// Universal FOSS Exception, version 1.0, a copy of which can be found at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Update; +using Microsoft.Extensions.DependencyInjection; +using MySql.EntityFrameworkCore.Basic.Tests.DbContextClasses; +using MySql.EntityFrameworkCore.Basic.Tests.Utils; +using MySql.EntityFrameworkCore.Extensions; +using MySql.EntityFrameworkCore.Update; +using NUnit.Framework; +using System.Linq; + +namespace MySql.EntityFrameworkCore.Basic.Tests +{ + public class ServiceLifetimeTests + { + private class ServiceLifetimeContext : DbContext + { + public ServiceLifetimeContext(DbContextOptions options) + : base(options) + { + } + + public DbSet People => Set(); + } + + // ASP.NET Core validates scopes in development, as do EF Core's own test fixtures. + private static ServiceProvider BuildValidatingProvider(IServiceCollection services) + => services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true }); + + [Test] + public void UpdateSqlGeneratorResolvesFromAScopeValidatingProvider() + { + using var serviceProvider = BuildValidatingProvider(new ServiceCollection().AddEntityFrameworkMySQL()); + using var scope = serviceProvider.CreateScope(); + + var generator = scope.ServiceProvider.GetRequiredService(); + + Assert.That(generator, Is.InstanceOf()); + Assert.That(serviceProvider.GetRequiredService(), Is.SameAs(generator)); + } + + [Test] + public void ProviderSpecificUpdateSqlGeneratorIsASingletonLikeIUpdateSqlGenerator() + { + var services = new ServiceCollection().AddEntityFrameworkMySQL(); + + Assert.That(services.Single(d => d.ServiceType == typeof(IUpdateSqlGenerator)).Lifetime, Is.EqualTo(ServiceLifetime.Singleton)); + Assert.That(services.Single(d => d.ServiceType == typeof(IMySQLUpdateSqlGenerator)).Lifetime, Is.EqualTo(ServiceLifetime.Singleton)); + } + + [Test] + public void SaveChangesWorksWithAScopeValidatingInternalServiceProvider() + { + var services = new ServiceCollection().AddEntityFrameworkMySQL(); + services.AddDbContext((sp, options) => options + .UseMySQL(MySQLTestStore.GetContextConnectionString()) + .UseInternalServiceProvider(sp)); + using var serviceProvider = BuildValidatingProvider(services); + + using (var scope = serviceProvider.CreateScope()) + { + var context = scope.ServiceProvider.GetRequiredService(); + context.Database.EnsureDeleted(); + context.Database.EnsureCreated(); + context.People.Add(new Person { Name = "John Doe", PersonNumber = "1", PhoneNumber = "2" }); + context.SaveChanges(); + } + + using (var scope = serviceProvider.CreateScope()) + { + var context = scope.ServiceProvider.GetRequiredService(); + Assert.That(context.People.Single().Name, Is.EqualTo("John Doe")); + context.Database.EnsureDeleted(); + } + } + } +}