diff --git a/src/EFCore.PG/ValueGeneration/Internal/NpgsqlValueGeneratorCache.cs b/src/EFCore.PG/ValueGeneration/Internal/NpgsqlValueGeneratorCache.cs index 23d09e006..4e662bfd5 100644 --- a/src/EFCore.PG/ValueGeneration/Internal/NpgsqlValueGeneratorCache.cs +++ b/src/EFCore.PG/ValueGeneration/Internal/NpgsqlValueGeneratorCache.cs @@ -5,7 +5,6 @@ using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.ValueGeneration; -using Npgsql.EntityFrameworkCore.PostgreSQL.Utilities; namespace Npgsql.EntityFrameworkCore.PostgreSQL.ValueGeneration.Internal { @@ -35,17 +34,24 @@ public virtual NpgsqlSequenceValueGeneratorState GetOrAddSequenceState( IProperty property, IRelationalConnection connection) { - //var sequence = property.GetNpgsql().FindHiLoSequence(); var sequence = property.FindHiLoSequence(); Debug.Assert(sequence != null); return _sequenceGeneratorCache.GetOrAdd( - GetSequenceName(sequence), + GetSequenceName(sequence, connection), sequenceName => new NpgsqlSequenceValueGeneratorState(sequence)); } - static string GetSequenceName(ISequence sequence) - => (sequence.Schema == null ? "" : sequence.Schema + ".") + sequence.Name; + static string GetSequenceName(ISequence sequence, IRelationalConnection connection) + { + var dbConnection = connection.DbConnection; + + return dbConnection.Database.ToUpperInvariant() + + "::" + + dbConnection.DataSource?.ToUpperInvariant() + + "::" + + (sequence.Schema == null ? "" : sequence.Schema + ".") + sequence.Name; + } } } diff --git a/test/EFCore.PG.FunctionalTests/SequenceEndToEndTest.cs b/test/EFCore.PG.FunctionalTests/SequenceEndToEndTest.cs new file mode 100644 index 000000000..508928a9a --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/SequenceEndToEndTest.cs @@ -0,0 +1,433 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Microsoft.EntityFrameworkCore.TestUtilities.Xunit; +using Microsoft.Extensions.DependencyInjection; +using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; +using Xunit; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL +{ + public class SequenceEndToEndTest : IDisposable + { + [ConditionalFact] + public void Can_use_sequence_end_to_end() + { + var serviceProvider = new ServiceCollection() + .AddEntityFrameworkNpgsql() + .BuildServiceProvider(); + + using (var context = new BronieContext(serviceProvider, TestStore.Name)) + { + context.Database.EnsureCreatedResiliently(); + } + + AddEntities(serviceProvider, TestStore.Name); + AddEntities(serviceProvider, TestStore.Name); + + // Use a different service provider so a different generator is used but with + // the same server sequence. + serviceProvider = new ServiceCollection() + .AddEntityFrameworkNpgsql() + .BuildServiceProvider(); + + AddEntities(serviceProvider, TestStore.Name); + + using (var context = new BronieContext(serviceProvider, TestStore.Name)) + { + var pegasuses = context.Pegasuses.ToList(); + + for (var i = 0; i < 10; i++) + { + Assert.Equal(3, pegasuses.Count(p => p.Name == "Rainbow Dash " + i)); + Assert.Equal(3, pegasuses.Count(p => p.Name == "Fluttershy " + i)); + } + } + } + + static void AddEntities(IServiceProvider serviceProvider, string name) + { + using var context = new BronieContext(serviceProvider, name); + for (var i = 0; i < 10; i++) + { + context.Add( + new Pegasus { Name = "Rainbow Dash " + i }); + context.Add( + new Pegasus { Name = "Fluttershy " + i }); + } + + context.SaveChanges(); + } + + [ConditionalFact] + public void Can_use_sequence_end_to_end_on_multiple_databases() + { + var serviceProvider = new ServiceCollection() + .AddEntityFrameworkNpgsql() + .BuildServiceProvider(); + + var dbOne = TestStore.Name + "1"; + var dbTwo = TestStore.Name + "2"; + + foreach (var dbName in new[] { dbOne, dbTwo }) + { + using var context = new BronieContext(serviceProvider, dbName); + context.Database.EnsureDeleted(); + Thread.Sleep(100); + context.Database.EnsureCreatedResiliently(); + } + + AddEntitiesToMultipleContexts(serviceProvider, dbOne, dbTwo); + AddEntitiesToMultipleContexts(serviceProvider, dbOne, dbTwo); + + // Use a different service provider so a different generator is used but with + // the same server sequence. + serviceProvider = new ServiceCollection() + .AddEntityFrameworkNpgsql() + .BuildServiceProvider(); + + AddEntitiesToMultipleContexts(serviceProvider, dbOne, dbTwo); + + foreach (var dbName in new[] { dbOne, dbTwo }) + { + using var context = new BronieContext(serviceProvider, dbName); + var pegasuses = context.Pegasuses.ToList(); + + for (var i = 0; i < 29; i++) + { + Assert.Equal( + dbName.EndsWith("1", StringComparison.Ordinal) ? 3 : 0, + pegasuses.Count(p => p.Name == "Rainbow Dash " + i)); + Assert.Equal(3, pegasuses.Count(p => p.Name == "Fluttershy " + i)); + } + } + } + + static void AddEntitiesToMultipleContexts( + IServiceProvider serviceProvider, + string dbName1, + string dbName2) + { + using var context1 = new BronieContext(serviceProvider, dbName1); + using var context2 = new BronieContext(serviceProvider, dbName2); + for (var i = 0; i < 29; i++) + { + context1.Add( + new Pegasus { Name = "Rainbow Dash " + i }); + + context2.Add( + new Pegasus { Name = "Fluttershy " + i }); + + context1.Add( + new Pegasus { Name = "Fluttershy " + i }); + } + + context1.SaveChanges(); + context2.SaveChanges(); + } + + [ConditionalFact] + public async Task Can_use_sequence_end_to_end_async() + { + var serviceProvider = new ServiceCollection() + .AddEntityFrameworkNpgsql() + .BuildServiceProvider(); + + using (var context = new BronieContext(serviceProvider, TestStore.Name)) + { + context.Database.EnsureCreatedResiliently(); + } + + await AddEntitiesAsync(serviceProvider, TestStore.Name); + await AddEntitiesAsync(serviceProvider, TestStore.Name); + + // Use a different service provider so a different generator is used but with + // the same server sequence. + serviceProvider = new ServiceCollection() + .AddEntityFrameworkNpgsql() + .BuildServiceProvider(); + + await AddEntitiesAsync(serviceProvider, TestStore.Name); + + using (var context = new BronieContext(serviceProvider, TestStore.Name)) + { + var pegasuses = await context.Pegasuses.ToListAsync(); + + for (var i = 0; i < 10; i++) + { + Assert.Equal(3, pegasuses.Count(p => p.Name == "Rainbow Dash " + i)); + Assert.Equal(3, pegasuses.Count(p => p.Name == "Fluttershy " + i)); + } + } + } + + static async Task AddEntitiesAsync(IServiceProvider serviceProvider, string databaseName) + { + using var context = new BronieContext(serviceProvider, databaseName); + for (var i = 0; i < 10; i++) + { + await context.AddAsync( + new Pegasus { Name = "Rainbow Dash " + i }); + await context.AddAsync( + new Pegasus { Name = "Fluttershy " + i }); + } + + await context.SaveChangesAsync(); + } + + [ConditionalFact] + public async Task Can_use_sequence_end_to_end_from_multiple_contexts_concurrently_async() + { + var serviceProvider = new ServiceCollection() + .AddEntityFrameworkNpgsql() + .BuildServiceProvider(); + + using (var context = new BronieContext(serviceProvider, TestStore.Name)) + { + context.Database.EnsureCreatedResiliently(); + } + + const int threadCount = 50; + + var tests = new Func[threadCount]; + for (var i = 0; i < threadCount; i++) + { + var closureProvider = serviceProvider; + tests[i] = () => AddEntitiesAsync(closureProvider, TestStore.Name); + } + + var tasks = tests.Select(Task.Run).ToArray(); + + foreach (var t in tasks) + { + await t; + } + + using (var context = new BronieContext(serviceProvider, TestStore.Name)) + { + var pegasuses = await context.Pegasuses.ToListAsync(); + + for (var i = 0; i < 10; i++) + { + Assert.Equal(threadCount, pegasuses.Count(p => p.Name == "Rainbow Dash " + i)); + Assert.Equal(threadCount, pegasuses.Count(p => p.Name == "Fluttershy " + i)); + } + } + } + + [ConditionalFact] + public void Can_use_explicit_values() + { + var serviceProvider = new ServiceCollection() + .AddEntityFrameworkNpgsql() + .BuildServiceProvider(); + + using (var context = new BronieContext(serviceProvider, TestStore.Name)) + { + context.Database.EnsureCreatedResiliently(); + } + + AddEntitiesWithIds(serviceProvider, 0, TestStore.Name); + AddEntitiesWithIds(serviceProvider, 2, TestStore.Name); + + // Use a different service provider so a different generator is used but with + // the same server sequence. + serviceProvider = new ServiceCollection() + .AddEntityFrameworkNpgsql() + .BuildServiceProvider(); + + AddEntitiesWithIds(serviceProvider, 4, TestStore.Name); + + using (var context = new BronieContext(serviceProvider, TestStore.Name)) + { + var pegasuses = context.Pegasuses.ToList(); + + for (var i = 1; i < 11; i++) + { + Assert.Equal(3, pegasuses.Count(p => p.Name == "Rainbow Dash " + i)); + Assert.Equal(3, pegasuses.Count(p => p.Name == "Fluttershy " + i)); + + for (var j = 0; j < 6; j++) + { + pegasuses.Single(p => p.Identifier == i * 100 + j); + } + } + } + } + + static void AddEntitiesWithIds(IServiceProvider serviceProvider, int idOffset, string name) + { + using var context = new BronieContext(serviceProvider, name); + for (var i = 1; i < 11; i++) + { + context.Add( + new Pegasus { Name = "Rainbow Dash " + i, Identifier = i * 100 + idOffset }); + context.Add( + new Pegasus { Name = "Fluttershy " + i, Identifier = i * 100 + idOffset + 1 }); + } + + context.SaveChanges(); + } + + class BronieContext : DbContext + { + readonly IServiceProvider _serviceProvider; + readonly string _databaseName; + + public BronieContext(IServiceProvider serviceProvider, string databaseName) + { + _serviceProvider = serviceProvider; + _databaseName = databaseName; + } + + public DbSet Pegasuses { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder + .UseInternalServiceProvider(_serviceProvider) + .UseNpgsql(NpgsqlTestStore.CreateConnectionString(_databaseName), b => b.ApplyConfiguration()); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity( + b => + { + b.HasKey(e => e.Identifier); + b.Property(e => e.Identifier).UseHiLo(); + }); + } + } + + class Pegasus + { + public int Identifier { get; set; } + public string Name { get; set; } + } + + [ConditionalFact] // Issue #478 + public void Can_use_sequence_with_nullable_key_end_to_end() + { + var serviceProvider = new ServiceCollection() + .AddEntityFrameworkNpgsql() + .BuildServiceProvider(); + + using (var context = new NullableBronieContext(serviceProvider, TestStore.Name, true)) + { + context.Database.EnsureCreatedResiliently(); + } + + AddEntitiesNullable(serviceProvider, TestStore.Name, true); + AddEntitiesNullable(serviceProvider, TestStore.Name, true); + AddEntitiesNullable(serviceProvider, TestStore.Name, true); + + using (var context = new NullableBronieContext(serviceProvider, TestStore.Name, true)) + { + var pegasuses = context.Unicons.ToList(); + + for (var i = 0; i < 10; i++) + { + Assert.Equal(3, pegasuses.Count(p => p.Name == "Twilight Sparkle " + i)); + Assert.Equal(3, pegasuses.Count(p => p.Name == "Rarity " + i)); + } + } + } + + [ConditionalFact] // Issue #478 + public void Can_use_identity_with_nullable_key_end_to_end() + { + var serviceProvider = new ServiceCollection() + .AddEntityFrameworkNpgsql() + .BuildServiceProvider(); + + using (var context = new NullableBronieContext(serviceProvider, TestStore.Name, false)) + { + context.Database.EnsureCreatedResiliently(); + } + + AddEntitiesNullable(serviceProvider, TestStore.Name, false); + AddEntitiesNullable(serviceProvider, TestStore.Name, false); + AddEntitiesNullable(serviceProvider, TestStore.Name, false); + + using (var context = new NullableBronieContext(serviceProvider, TestStore.Name, false)) + { + var pegasuses = context.Unicons.ToList(); + + for (var i = 0; i < 10; i++) + { + Assert.Equal(3, pegasuses.Count(p => p.Name == "Twilight Sparkle " + i)); + Assert.Equal(3, pegasuses.Count(p => p.Name == "Rarity " + i)); + } + } + } + + static void AddEntitiesNullable(IServiceProvider serviceProvider, string databaseName, bool useSequence) + { + using var context = new NullableBronieContext(serviceProvider, databaseName, useSequence); + for (var i = 0; i < 10; i++) + { + context.Add( + new Unicon { Name = "Twilight Sparkle " + i }); + context.Add( + new Unicon { Name = "Rarity " + i }); + } + + context.SaveChanges(); + } + + class NullableBronieContext : DbContext + { + readonly IServiceProvider _serviceProvider; + readonly string _databaseName; + readonly bool _useSequence; + + public NullableBronieContext(IServiceProvider serviceProvider, string databaseName, bool useSequence) + { + _serviceProvider = serviceProvider; + _databaseName = databaseName; + _useSequence = useSequence; + } + + public DbSet Unicons { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder + .UseInternalServiceProvider(_serviceProvider) + .UseNpgsql(NpgsqlTestStore.CreateConnectionString(_databaseName), b => b.ApplyConfiguration()); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity( + b => + { + b.HasKey(e => e.Identifier); + if (_useSequence) + { + b.Property(e => e.Identifier).UseHiLo(); + } + else + { + b.Property(e => e.Identifier).UseIdentityColumn(); + } + }); + } + } + + class Unicon + { + public int? Identifier { get; set; } + public string Name { get; set; } + } + + public SequenceEndToEndTest() + { + TestStore = NpgsqlTestStore.CreateInitialized("SequenceEndToEndTest"); + } + + protected NpgsqlTestStore TestStore { get; } + + public void Dispose() => TestStore.Dispose(); + } +}