diff --git a/src/EFCore/DbContext.cs b/src/EFCore/DbContext.cs index a26e7b578c1..3a82898e844 100644 --- a/src/EFCore/DbContext.cs +++ b/src/EFCore/DbContext.cs @@ -66,6 +66,9 @@ public class DbContext : private bool _initializing; private bool _disposed; + private readonly Guid _contextId = Guid.NewGuid(); + private int _lease; + /// /// /// Initializes a new instance of the class. The @@ -137,6 +140,18 @@ public virtual IModel Model [DebuggerStepThrough] get => DbContextDependencies.Model; } + /// + /// + /// A unique identifier for the context instance and pool lease, if any. + /// + /// + /// This identifier is primarily intended as a correlation ID for logging and debugging such + /// that it is easy to identify that multiple events are using the same or different context instances. + /// + /// + public virtual DbContextId ContextId + => new DbContextId(_contextId, _lease); + /// /// 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 @@ -207,7 +222,8 @@ public virtual IModel Model /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [EntityFrameworkInternal] - IDiagnosticsLogger IDbContextDependencies.InfrastructureLogger => DbContextDependencies.InfrastructureLogger; + IDiagnosticsLogger IDbContextDependencies.InfrastructureLogger + => DbContextDependencies.InfrastructureLogger; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -478,7 +494,7 @@ private void TryDetectChanges(EntityEntry entry) { if (ChangeTracker.AutoDetectChangesEnabled) { - entry.DetectChanges(); + entry.DetectChanges(); } } @@ -584,6 +600,7 @@ public virtual async Task SaveChangesAsync( void IDbContextPoolable.SetPool(IDbContextPool contextPool) { _dbContextPool = contextPool; + _lease = 1; } /// @@ -610,6 +627,7 @@ DbContextPoolConfigurationSnapshot IDbContextPoolable.SnapshotConfiguration() void IDbContextPoolable.Resurrect(DbContextPoolConfigurationSnapshot configurationSnapshot) { _disposed = false; + ++_lease; if (configurationSnapshot.AutoDetectChangesEnabled != null) { @@ -904,7 +922,7 @@ public virtual async ValueTask> AddAsync( /// to anything other than the CLR default for the property type. /// /// - /// For entity types without generated keys, the state set is always . + /// For entity types without generated keys, the state set is always . /// /// /// Use to set the state of only a single entity. @@ -947,7 +965,7 @@ public virtual EntityEntry Attach([NotNull] TEntity entity) /// to anything other than the CLR default for the property type. /// /// - /// For entity types without generated keys, the state set is always . + /// For entity types without generated keys, the state set is always . /// /// /// Use to set the state of only a single entity. @@ -1108,7 +1126,7 @@ public virtual async ValueTask AddAsync( /// to anything other than the CLR default for the property type. /// /// - /// For entity types without generated keys, the state set is always . + /// For entity types without generated keys, the state set is always . /// /// /// Use to set the state of only a single entity. @@ -1149,7 +1167,7 @@ public virtual EntityEntry Attach([NotNull] object entity) /// to anything other than the CLR default for the property type. /// /// - /// For entity types without generated keys, the state set is always . + /// For entity types without generated keys, the state set is always . /// /// /// Use to set the state of only a single entity. @@ -1280,7 +1298,7 @@ public virtual Task AddRangeAsync([NotNull] params object[] entities) /// to anything other than the CLR default for the property type. /// /// - /// For entity types without generated keys, the state set is always . + /// For entity types without generated keys, the state set is always . /// /// /// Use to set the state of only a single entity. @@ -1317,7 +1335,7 @@ public virtual void AttachRange([NotNull] params object[] entities) /// to anything other than the CLR default for the property type. /// /// - /// For entity types without generated keys, the state set is always . + /// For entity types without generated keys, the state set is always . /// /// /// Use to set the state of only a single entity. @@ -1435,7 +1453,7 @@ await SetEntityStateAsync( /// to anything other than the CLR default for the property type. /// /// - /// For entity types without generated keys, the state set is always . + /// For entity types without generated keys, the state set is always . /// /// /// Use to set the state of only a single entity. @@ -1472,7 +1490,7 @@ public virtual void AttachRange([NotNull] IEnumerable entities) /// to anything other than the CLR default for the property type. /// /// - /// For entity types without generated keys, the state set is always . + /// For entity types without generated keys, the state set is always . /// /// /// Use to set the state of only a single entity. @@ -1574,7 +1592,8 @@ public virtual ValueTask FindAsync([NotNull] Type entityType, [CanBeNull /// The values of the primary key for the entity to be found. /// A to observe while waiting for the task to complete. /// The entity found, or null. - public virtual ValueTask FindAsync([NotNull] Type entityType, [CanBeNull] object[] keyValues, CancellationToken cancellationToken) + public virtual ValueTask FindAsync( + [NotNull] Type entityType, [CanBeNull] object[] keyValues, CancellationToken cancellationToken) { CheckDisposed(); diff --git a/src/EFCore/DbContextId.cs b/src/EFCore/DbContextId.cs new file mode 100644 index 00000000000..56320169078 --- /dev/null +++ b/src/EFCore/DbContextId.cs @@ -0,0 +1,100 @@ +// 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.Globalization; + +namespace Microsoft.EntityFrameworkCore +{ + /// + /// + /// A unique identifier for the context instance and pool lease, if any. + /// + /// + /// This identifier is primarily intended as a correlation ID for logging and debugging such + /// that it is easy to identify that multiple events are using the same or different context instances. + /// + /// + public readonly struct DbContextId + { + /// + /// Compares this ID to another ID to see if they represent the same leased context. + /// + /// The other ID. + /// True if they represent the same leased context; false otherwise. + public bool Equals(DbContextId other) + => InstanceId == other.InstanceId + && Lease == other.Lease; + + /// + /// Compares this ID to another ID to see if they represent the same leased context. + /// + /// The other ID. + /// True if they represent the same leased context; false otherwise. + public override bool Equals(object obj) + => obj is DbContextId other && Equals(other); + + /// + /// A hash code for this ID. + /// + /// The hash code. + public override int GetHashCode() + => HashCode.Combine(InstanceId, Lease); + + /// + /// Compares one ID to another ID to see if they represent the same leased context. + /// + /// The first ID. + /// The second ID. + /// True if they represent the same leased context; false otherwise. + public static bool operator ==(DbContextId left, DbContextId right) => left.Equals(right); + + /// + /// Compares one ID to another ID to see if they represent different leased contexts. + /// + /// The first ID. + /// The second ID. + /// True if they represent different leased contexts; false otherwise. + public static bool operator !=(DbContextId left, DbContextId right) => !left.Equals(right); + + /// + /// Creates a new with the given and lease number. + /// + /// A unique identifier for the being used. + /// A number indicating whether this is the first, second, third, etc. lease of this instance. + public DbContextId(Guid id, int lease) + { + InstanceId = id; + Lease = lease; + } + + /// + /// + /// A unique identifier for the being used. + /// + /// + /// When context pooling is being used, then this ID must be combined with + /// the in order to get a unique ID for the effective instance being used. + /// + /// + public Guid InstanceId { get; } + + /// + /// + /// A number that is incremented each time this particular instance is leased + /// from the context pool. + /// + /// + /// Will be zero if context pooling is not being used. + /// + /// + public int Lease { get; } + + /// Returns the fully qualified type name of this instance. + /// The fully qualified type name. + public override string ToString() + { + return InstanceId + ":" + Lease.ToString(CultureInfo.InvariantCulture); + } + } +} diff --git a/test/EFCore.Relational.Specification.Tests/CommandInterceptionTestBase.cs b/test/EFCore.Relational.Specification.Tests/CommandInterceptionTestBase.cs index f3d9cef3e16..13cef76d4cb 100644 --- a/test/EFCore.Relational.Specification.Tests/CommandInterceptionTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/CommandInterceptionTestBase.cs @@ -32,17 +32,22 @@ public virtual async Task Intercept_query_passively(bool async, bool inj var (context, interceptor) = CreateContext(inject); using (context) { - var results = async - ? await context.Set().ToListAsync() - : context.Set().ToList(); + using (var listener = Fixture.SubscribeToDiagnosticListener(context.ContextId)) + { + var results = async + ? await context.Set().ToListAsync() + : context.Set().ToList(); - Assert.Equal(2, results.Count); - Assert.Equal(77, results[0].Id); - Assert.Equal(88, results[1].Id); - Assert.Equal("Black Hole", results[0].Type); - Assert.Equal("Bing Bang", results[1].Type); + Assert.Equal(2, results.Count); + Assert.Equal(77, results[0].Id); + Assert.Equal(88, results[1].Id); + Assert.Equal("Black Hole", results[0].Type); + Assert.Equal("Bing Bang", results[1].Type); - AssertNormalOutcome(context, interceptor, async); + AssertNormalOutcome(context, interceptor, async); + + AssertExecutedEvents(listener); + } } return interceptor.CommandText; @@ -74,15 +79,20 @@ public virtual async Task Intercept_scalar_passively(bool async, bool inject) var commandParameterObject = new RelationalCommandParameterObject(connection, null, context, logger); - var result = async - ? await command.ExecuteScalarAsync(commandParameterObject) - : command.ExecuteScalar(commandParameterObject); + using (var listener = Fixture.SubscribeToDiagnosticListener(context.ContextId)) + { + var result = async + ? await command.ExecuteScalarAsync(commandParameterObject) + : command.ExecuteScalar(commandParameterObject); + + Assert.Equal(1, Convert.ToInt32(result)); - Assert.Equal(1, Convert.ToInt32(result)); + AssertNormalOutcome(context, interceptor, async); - AssertNormalOutcome(context, interceptor, async); + AssertSql(sql, interceptor.CommandText); - AssertSql(sql, interceptor.CommandText); + AssertExecutedEvents(listener); + } } } @@ -108,15 +118,20 @@ public virtual async Task Intercept_non_query_passively(bool async, bool inject) { const string nonQuery = "DELETE FROM Singularity WHERE Id = 77"; - var result = async - ? await context.Database.ExecuteSqlRawAsync(nonQuery) - : context.Database.ExecuteSqlRaw(nonQuery); + using (var listener = Fixture.SubscribeToDiagnosticListener(context.ContextId)) + { + var result = async + ? await context.Database.ExecuteSqlRawAsync(nonQuery) + : context.Database.ExecuteSqlRaw(nonQuery); - Assert.Equal(1, result); + Assert.Equal(1, result); - AssertNormalOutcome(context, interceptor, async); + AssertNormalOutcome(context, interceptor, async); - AssertSql(nonQuery, interceptor.CommandText); + AssertSql(nonQuery, interceptor.CommandText); + + AssertExecutedEvents(listener); + } } } } @@ -139,19 +154,24 @@ public virtual async Task Intercept_query_to_suppress_execution(bool asy var (context, interceptor) = CreateContext(inject); using (context) { - var results = async - ? await context.Set().ToListAsync() - : context.Set().ToList(); + using (var listener = Fixture.SubscribeToDiagnosticListener(context.ContextId)) + { + var results = async + ? await context.Set().ToListAsync() + : context.Set().ToList(); + + Assert.Equal(3, results.Count); + Assert.Equal(977, results[0].Id); + Assert.Equal(988, results[1].Id); + Assert.Equal(999, results[2].Id); + Assert.Equal("<977>", results[0].Type); + Assert.Equal("<988>", results[1].Type); + Assert.Equal("<999>", results[2].Type); - Assert.Equal(3, results.Count); - Assert.Equal(977, results[0].Id); - Assert.Equal(988, results[1].Id); - Assert.Equal(999, results[2].Id); - Assert.Equal("<977>", results[0].Type); - Assert.Equal("<988>", results[1].Type); - Assert.Equal("<999>", results[2].Type); + AssertNormalOutcome(context, interceptor, async); - AssertNormalOutcome(context, interceptor, async); + AssertExecutedEvents(listener); + } } return interceptor.CommandText; @@ -204,15 +224,20 @@ public virtual async Task Intercept_scalar_to_suppress_execution(bool async, boo var commandParameterObject = new RelationalCommandParameterObject(connection, null, context, logger); - var result = async - ? await command.ExecuteScalarAsync(commandParameterObject) - : command.ExecuteScalar(commandParameterObject); + using (var listener = Fixture.SubscribeToDiagnosticListener(context.ContextId)) + { + var result = async + ? await command.ExecuteScalarAsync(commandParameterObject) + : command.ExecuteScalar(commandParameterObject); - Assert.Equal(SuppressingScalarCommandInterceptor.InterceptedResult, result); + Assert.Equal(SuppressingScalarCommandInterceptor.InterceptedResult, result); - AssertNormalOutcome(context, interceptor, async); + AssertNormalOutcome(context, interceptor, async); - AssertSql(sql, interceptor.CommandText); + AssertSql(sql, interceptor.CommandText); + + AssertExecutedEvents(listener); + } } } @@ -262,15 +287,20 @@ public virtual async Task Intercept_non_query_to_suppress_execution(bool async, { const string nonQuery = "DELETE FROM Singularity WHERE Id = 77"; - var result = async - ? await context.Database.ExecuteSqlRawAsync(nonQuery) - : context.Database.ExecuteSqlRaw(nonQuery); + using (var listener = Fixture.SubscribeToDiagnosticListener(context.ContextId)) + { + var result = async + ? await context.Database.ExecuteSqlRawAsync(nonQuery) + : context.Database.ExecuteSqlRaw(nonQuery); - Assert.Equal(2, result); + Assert.Equal(2, result); - AssertNormalOutcome(context, interceptor, async); + AssertNormalOutcome(context, interceptor, async); + + AssertSql(nonQuery, interceptor.CommandText); - AssertSql(nonQuery, interceptor.CommandText); + AssertExecutedEvents(listener); + } } } } @@ -314,17 +344,22 @@ public virtual async Task Intercept_query_to_mutate_command(bool async, var (context, interceptor) = CreateContext(inject); using (context) { - var results = async - ? await context.Set().ToListAsync() - : context.Set().ToList(); + using (var listener = Fixture.SubscribeToDiagnosticListener(context.ContextId)) + { + var results = async + ? await context.Set().ToListAsync() + : context.Set().ToList(); - Assert.Equal(2, results.Count); - Assert.Equal(77, results[0].Id); - Assert.Equal(88, results[1].Id); - Assert.Equal("Black Hole?", results[0].Type); - Assert.Equal("Bing Bang?", results[1].Type); + Assert.Equal(2, results.Count); + Assert.Equal(77, results[0].Id); + Assert.Equal(88, results[1].Id); + Assert.Equal("Black Hole?", results[0].Type); + Assert.Equal("Bing Bang?", results[1].Type); - AssertNormalOutcome(context, interceptor, async); + AssertNormalOutcome(context, interceptor, async); + + AssertExecutedEvents(listener); + } } return interceptor.CommandText; @@ -380,15 +415,20 @@ public virtual async Task Intercept_scalar_to_mutate_command(bool async, bool in var commandParameterObject = new RelationalCommandParameterObject(connection, null, context, logger); - var result = async - ? await command.ExecuteScalarAsync(commandParameterObject) - : command.ExecuteScalar(commandParameterObject); + using (var listener = Fixture.SubscribeToDiagnosticListener(context.ContextId)) + { + var result = async + ? await command.ExecuteScalarAsync(commandParameterObject) + : command.ExecuteScalar(commandParameterObject); + + Assert.Equal(2, Convert.ToInt32(result)); - Assert.Equal(2, Convert.ToInt32(result)); + AssertNormalOutcome(context, interceptor, async); - AssertNormalOutcome(context, interceptor, async); + AssertSql(MutatingScalarCommandInterceptor.MutatedSql, interceptor.CommandText); - AssertSql(MutatingScalarCommandInterceptor.MutatedSql, interceptor.CommandText); + AssertExecutedEvents(listener); + } } } @@ -437,15 +477,20 @@ public virtual async Task Intercept_non_query_to_mutate_command(bool async, bool { const string nonQuery = "DELETE FROM Singularity WHERE Id = 77"; - var result = async - ? await context.Database.ExecuteSqlRawAsync(nonQuery) - : context.Database.ExecuteSqlRaw(nonQuery); + using (var listener = Fixture.SubscribeToDiagnosticListener(context.ContextId)) + { + var result = async + ? await context.Database.ExecuteSqlRawAsync(nonQuery) + : context.Database.ExecuteSqlRaw(nonQuery); - Assert.Equal(0, result); + Assert.Equal(0, result); - AssertNormalOutcome(context, interceptor, async); + AssertNormalOutcome(context, interceptor, async); + + AssertSql(MutatingNonQueryCommandInterceptor.MutatedSql, interceptor.CommandText); - AssertSql(MutatingNonQueryCommandInterceptor.MutatedSql, interceptor.CommandText); + AssertExecutedEvents(listener); + } } } } @@ -491,17 +536,22 @@ public virtual async Task Intercept_query_to_replace_execution(bool asyn var (context, interceptor) = CreateContext(inject); using (context) { - var results = async - ? await context.Set().ToListAsync() - : context.Set().ToList(); + using (var listener = Fixture.SubscribeToDiagnosticListener(context.ContextId)) + { + var results = async + ? await context.Set().ToListAsync() + : context.Set().ToList(); - Assert.Equal(2, results.Count); - Assert.Equal(77, results[0].Id); - Assert.Equal(88, results[1].Id); - Assert.Equal("Black Hole?", results[0].Type); - Assert.Equal("Bing Bang?", results[1].Type); + Assert.Equal(2, results.Count); + Assert.Equal(77, results[0].Id); + Assert.Equal(88, results[1].Id); + Assert.Equal("Black Hole?", results[0].Type); + Assert.Equal("Bing Bang?", results[1].Type); - AssertNormalOutcome(context, interceptor, async); + AssertNormalOutcome(context, interceptor, async); + + AssertExecutedEvents(listener); + } } return interceptor.CommandText; @@ -564,15 +614,20 @@ public virtual async Task Intercept_scalar_to_replace_execution(bool async, bool var commandParameterObject = new RelationalCommandParameterObject(connection, null, context, logger); - var result = async - ? await command.ExecuteScalarAsync(commandParameterObject) - : command.ExecuteScalar(commandParameterObject); + using (var listener = Fixture.SubscribeToDiagnosticListener(context.ContextId)) + { + var result = async + ? await command.ExecuteScalarAsync(commandParameterObject) + : command.ExecuteScalar(commandParameterObject); - Assert.Equal(2, Convert.ToInt32(result)); + Assert.Equal(2, Convert.ToInt32(result)); - AssertNormalOutcome(context, interceptor, async); + AssertNormalOutcome(context, interceptor, async); - AssertSql(sql, interceptor.CommandText); + AssertSql(sql, interceptor.CommandText); + + AssertExecutedEvents(listener); + } } } @@ -629,15 +684,20 @@ public virtual async Task Intercept_non_query_to_replace_execution(bool async, b { const string nonQuery = "DELETE FROM Singularity WHERE Id = 78"; - var result = async - ? await context.Database.ExecuteSqlRawAsync(nonQuery) - : context.Database.ExecuteSqlRaw(nonQuery); + using (var listener = Fixture.SubscribeToDiagnosticListener(context.ContextId)) + { + var result = async + ? await context.Database.ExecuteSqlRawAsync(nonQuery) + : context.Database.ExecuteSqlRaw(nonQuery); - Assert.Equal(1, result); + Assert.Equal(1, result); - AssertNormalOutcome(context, interceptor, async); + AssertNormalOutcome(context, interceptor, async); + + AssertSql(nonQuery, interceptor.CommandText); - AssertSql(nonQuery, interceptor.CommandText); + AssertExecutedEvents(listener); + } } } } @@ -692,23 +752,28 @@ public virtual async Task Intercept_query_to_replace_result(bool async, var (context, interceptor) = CreateContext(inject); using (context) { - var results = async - ? await context.Set().ToListAsync() - : context.Set().ToList(); + using (var listener = Fixture.SubscribeToDiagnosticListener(context.ContextId)) + { + var results = async + ? await context.Set().ToListAsync() + : context.Set().ToList(); + + Assert.Equal(5, results.Count); + Assert.Equal(77, results[0].Id); + Assert.Equal(88, results[1].Id); + Assert.Equal(977, results[2].Id); + Assert.Equal(988, results[3].Id); + Assert.Equal(999, results[4].Id); + Assert.Equal("Black Hole", results[0].Type); + Assert.Equal("Bing Bang", results[1].Type); + Assert.Equal("<977>", results[2].Type); + Assert.Equal("<988>", results[3].Type); + Assert.Equal("<999>", results[4].Type); - Assert.Equal(5, results.Count); - Assert.Equal(77, results[0].Id); - Assert.Equal(88, results[1].Id); - Assert.Equal(977, results[2].Id); - Assert.Equal(988, results[3].Id); - Assert.Equal(999, results[4].Id); - Assert.Equal("Black Hole", results[0].Type); - Assert.Equal("Bing Bang", results[1].Type); - Assert.Equal("<977>", results[2].Type); - Assert.Equal("<988>", results[3].Type); - Assert.Equal("<999>", results[4].Type); + AssertNormalOutcome(context, interceptor, async); - AssertNormalOutcome(context, interceptor, async); + AssertExecutedEvents(listener); + } } return interceptor.CommandText; @@ -814,15 +879,20 @@ public virtual async Task Intercept_scalar_to_replace_result(bool async, bool in var commandParameterObject = new RelationalCommandParameterObject(connection, null, context, logger); - var result = async - ? await command.ExecuteScalarAsync(commandParameterObject) - : command.ExecuteScalar(commandParameterObject); + using (var listener = Fixture.SubscribeToDiagnosticListener(context.ContextId)) + { + var result = async + ? await command.ExecuteScalarAsync(commandParameterObject) + : command.ExecuteScalar(commandParameterObject); + + Assert.Equal(ResultReplacingScalarCommandInterceptor.InterceptedResult, result); - Assert.Equal(ResultReplacingScalarCommandInterceptor.InterceptedResult, result); + AssertNormalOutcome(context, interceptor, async); - AssertNormalOutcome(context, interceptor, async); + AssertSql(sql, interceptor.CommandText); - AssertSql(sql, interceptor.CommandText); + AssertExecutedEvents(listener); + } } } @@ -862,7 +932,7 @@ public override Task ScalarExecutedAsync( [InlineData(true, false)] [InlineData(false, true)] [InlineData(true, true)] - public virtual async Task Intercept_non_query_to_replaceresult(bool async, bool inject) + public virtual async Task Intercept_non_query_to_replace_result(bool async, bool inject) { var (context, interceptor) = CreateContext(inject); using (context) @@ -871,15 +941,20 @@ public virtual async Task Intercept_non_query_to_replaceresult(bool async, bool { const string nonQuery = "DELETE FROM Singularity WHERE Id = 78"; - var result = async - ? await context.Database.ExecuteSqlRawAsync(nonQuery) - : context.Database.ExecuteSqlRaw(nonQuery); + using (var listener = Fixture.SubscribeToDiagnosticListener(context.ContextId)) + { + var result = async + ? await context.Database.ExecuteSqlRawAsync(nonQuery) + : context.Database.ExecuteSqlRaw(nonQuery); - Assert.Equal(7, result); + Assert.Equal(7, result); - AssertNormalOutcome(context, interceptor, async); + AssertNormalOutcome(context, interceptor, async); + + AssertSql(nonQuery, interceptor.CommandText); - AssertSql(nonQuery, interceptor.CommandText); + AssertExecutedEvents(listener); + } } } } @@ -1397,6 +1472,11 @@ private static void AssertErrorOutcome(DbContext context, CommandInterceptorBase Assert.Same(context, interceptor.Context); } + private static void AssertExecutedEvents(ITestDiagnosticListener listener) + => listener.AssertEventsInOrder( + RelationalEventId.CommandExecuting.Name, + RelationalEventId.CommandExecuted.Name); + protected abstract class CommandInterceptorBase : IDbCommandInterceptor { private readonly DbCommandMethod _commandMethod; diff --git a/test/EFCore.Relational.Specification.Tests/ConnectionInterceptionTestBase.cs b/test/EFCore.Relational.Specification.Tests/ConnectionInterceptionTestBase.cs index 94de4e52ede..88a32500952 100644 --- a/test/EFCore.Relational.Specification.Tests/ConnectionInterceptionTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/ConnectionInterceptionTestBase.cs @@ -34,29 +34,34 @@ public virtual async Task Intercept_connection_passively(bool async) connection.Close(); } - if (async) + using (var listener = Fixture.SubscribeToDiagnosticListener(context.ContextId)) { - await context.Database.OpenConnectionAsync(); - } - else - { - context.Database.OpenConnection(); - } + if (async) + { + await context.Database.OpenConnectionAsync(); + } + else + { + context.Database.OpenConnection(); + } - AssertNormalOpen(context, interceptor, async); + AssertNormalOpen(context, interceptor, async); - interceptor.Reset(); + interceptor.Reset(); - if (async) - { - await context.Database.CloseConnectionAsync(); - } - else - { - context.Database.CloseConnection(); - } + if (async) + { + await context.Database.CloseConnectionAsync(); + } + else + { + context.Database.CloseConnection(); + } - AssertNormalClose(context, interceptor, async); + AssertNormalClose(context, interceptor, async); + + AsertOpenCloseEvents(listener); + } if (startedOpen) { @@ -81,29 +86,34 @@ public virtual async Task Intercept_connection_to_override_opening(bool async) connection.Close(); } - if (async) + using (var listener = Fixture.SubscribeToDiagnosticListener(context.ContextId)) { - await context.Database.OpenConnectionAsync(); - } - else - { - context.Database.OpenConnection(); - } + if (async) + { + await context.Database.OpenConnectionAsync(); + } + else + { + context.Database.OpenConnection(); + } - AssertNormalOpen(context, interceptor, async); + AssertNormalOpen(context, interceptor, async); - interceptor.Reset(); + interceptor.Reset(); - if (async) - { - await context.Database.CloseConnectionAsync(); - } - else - { - context.Database.CloseConnection(); - } + if (async) + { + await context.Database.CloseConnectionAsync(); + } + else + { + context.Database.CloseConnection(); + } - AssertNormalClose(context, interceptor, async); + AssertNormalClose(context, interceptor, async); + + AsertOpenCloseEvents(listener); + } if (startedOpen) { @@ -139,38 +149,43 @@ public virtual async Task Intercept_connection_with_multiple_interceptors(bool a connection.Close(); } - if (async) + using (var listener = Fixture.SubscribeToDiagnosticListener(context.ContextId)) { - await context.Database.OpenConnectionAsync(); - } - else - { - context.Database.OpenConnection(); - } + if (async) + { + await context.Database.OpenConnectionAsync(); + } + else + { + context.Database.OpenConnection(); + } - AssertNormalOpen(context, interceptor1, async); - AssertNormalOpen(context, interceptor2, async); - AssertNormalOpen(context, interceptor3, async); - AssertNormalOpen(context, interceptor4, async); + AssertNormalOpen(context, interceptor1, async); + AssertNormalOpen(context, interceptor2, async); + AssertNormalOpen(context, interceptor3, async); + AssertNormalOpen(context, interceptor4, async); - interceptor1.Reset(); - interceptor2.Reset(); - interceptor3.Reset(); - interceptor4.Reset(); + interceptor1.Reset(); + interceptor2.Reset(); + interceptor3.Reset(); + interceptor4.Reset(); - if (async) - { - await context.Database.CloseConnectionAsync(); - } - else - { - context.Database.CloseConnection(); - } + if (async) + { + await context.Database.CloseConnectionAsync(); + } + else + { + context.Database.CloseConnection(); + } + + AssertNormalClose(context, interceptor1, async); + AssertNormalClose(context, interceptor2, async); + AssertNormalClose(context, interceptor3, async); + AssertNormalClose(context, interceptor4, async); - AssertNormalClose(context, interceptor1, async); - AssertNormalClose(context, interceptor2, async); - AssertNormalClose(context, interceptor3, async); - AssertNormalClose(context, interceptor4, async); + AsertOpenCloseEvents(listener); + } if (startedOpen) { @@ -262,6 +277,7 @@ protected class ConnectionInterceptor : IDbConnectionInterceptor { public DbContext Context { get; set; } public Exception Exception { get; set; } + public DbContextId ContextId { get; set; } public Guid ConnectionId { get; set; } public bool AsyncCalled { get; set; } public bool SyncCalled { get; set; } @@ -275,6 +291,7 @@ public void Reset() { Context = null; Exception = null; + ContextId = default; ConnectionId = default; AsyncCalled = false; SyncCalled = false; @@ -400,8 +417,10 @@ protected virtual void AssertOpening(ConnectionEventData eventData) { Assert.NotNull(eventData.Context); Assert.NotEqual(default, eventData.ConnectionId); + Assert.NotEqual(default, eventData.Context.ContextId); Context = eventData.Context; + ContextId = Context.ContextId; ConnectionId = eventData.ConnectionId; OpeningCalled = true; } @@ -410,6 +429,7 @@ protected virtual void AssertOpened(ConnectionEndEventData eventData) { Assert.Same(Context, eventData.Context); Assert.Equal(ConnectionId, eventData.ConnectionId); + Assert.Equal(ContextId, eventData.Context.ContextId); OpenedCalled = true; } @@ -418,8 +438,10 @@ protected virtual void AssertClosing(ConnectionEventData eventData) { Assert.NotNull(eventData.Context); Assert.NotEqual(default, eventData.ConnectionId); + Assert.NotEqual(default, eventData.Context.ContextId); Context = eventData.Context; + ContextId = Context.ContextId; ConnectionId = eventData.ConnectionId; ClosingCalled = true; } @@ -428,6 +450,7 @@ protected virtual void AssertClosed(ConnectionEndEventData eventData) { Assert.Same(Context, eventData.Context); Assert.Equal(ConnectionId, eventData.ConnectionId); + Assert.Equal(ContextId, eventData.Context.ContextId); ClosedCalled = true; } @@ -436,6 +459,7 @@ protected virtual void AssertFailed(ConnectionErrorEventData eventData) { Assert.Same(Context, eventData.Context); Assert.Equal(ConnectionId, eventData.ConnectionId); + Assert.Equal(ContextId, eventData.Context.ContextId); Assert.NotNull(eventData.Exception); Exception = eventData.Exception; @@ -481,5 +505,12 @@ private static void AssertErrorOnOpen(DbContext context, ConnectionInterceptor i Assert.True(interceptor.FailedCalled); Assert.Same(context, interceptor.Context); } + + private static void AsertOpenCloseEvents(ITestDiagnosticListener listener) + => listener.AssertEventsInOrder( + RelationalEventId.ConnectionOpening.Name, + RelationalEventId.ConnectionOpened.Name, + RelationalEventId.ConnectionClosing.Name, + RelationalEventId.ConnectionClosed.Name); } } diff --git a/test/EFCore.Relational.Specification.Tests/TransactionInterceptionTestBase.cs b/test/EFCore.Relational.Specification.Tests/TransactionInterceptionTestBase.cs index 1fc834b3f4d..199b6df0506 100644 --- a/test/EFCore.Relational.Specification.Tests/TransactionInterceptionTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/TransactionInterceptionTestBase.cs @@ -27,11 +27,16 @@ public virtual async Task BeginTransaction_without_interceptor(bool async) { using (var context = CreateContext(Enumerable.Empty())) { - using (var transaction = async - ? await context.Database.BeginTransactionAsync() - : context.Database.BeginTransaction()) + using (var listener = Fixture.SubscribeToDiagnosticListener(context.ContextId)) { - Assert.NotNull(transaction.GetDbTransaction()); + using (var transaction = async + ? await context.Database.BeginTransactionAsync() + : context.Database.BeginTransaction()) + { + Assert.NotNull(transaction.GetDbTransaction()); + } + + AssertBeginTransactionEvents(listener); } } } @@ -43,15 +48,20 @@ public virtual async Task UseTransaction_without_interceptor(bool async) { using (var context = CreateContext(Enumerable.Empty())) { - using (var transaction = context.Database.GetDbConnection().BeginTransaction()) + using (var listener = Fixture.SubscribeToDiagnosticListener(context.ContextId)) { - var contextTransaction = async - ? await context.Database.UseTransactionAsync(transaction) - : context.Database.UseTransaction(transaction); - + using (var transaction = context.Database.GetDbConnection().BeginTransaction()) { - Assert.NotNull(contextTransaction.GetDbTransaction()); - Assert.Same(transaction, contextTransaction.GetDbTransaction()); + var contextTransaction = async + ? await context.Database.UseTransactionAsync(transaction) + : context.Database.UseTransaction(transaction); + + { + Assert.NotNull(contextTransaction.GetDbTransaction()); + Assert.Same(transaction, contextTransaction.GetDbTransaction()); + } + + AssertUseTransactionEvents(listener); } } } @@ -65,11 +75,16 @@ public virtual async Task Intercept_BeginTransaction(bool async) var (context, interceptor) = CreateContext(); using (context) { - using (var _ = async - ? await context.Database.BeginTransactionAsync() - : context.Database.BeginTransaction()) + using (var listener = Fixture.SubscribeToDiagnosticListener(context.ContextId)) { - AssertBeginTransaction(context, interceptor, async); + using (var _ = async + ? await context.Database.BeginTransactionAsync() + : context.Database.BeginTransaction()) + { + AssertBeginTransaction(context, interceptor, async); + } + + AssertBeginTransactionEvents(listener); } } } @@ -82,11 +97,16 @@ public virtual async Task Intercept_BeginTransaction_with_isolation_level(bool a var (context, interceptor) = CreateContext(); using (context) { - using (var _ = async - ? await context.Database.BeginTransactionAsync(IsolationLevel.ReadUncommitted) - : context.Database.BeginTransaction(IsolationLevel.ReadUncommitted)) + using (var listener = Fixture.SubscribeToDiagnosticListener(context.ContextId)) { - AssertBeginTransaction(context, interceptor, async); + using (var _ = async + ? await context.Database.BeginTransactionAsync(IsolationLevel.ReadUncommitted) + : context.Database.BeginTransaction(IsolationLevel.ReadUncommitted)) + { + AssertBeginTransaction(context, interceptor, async); + } + + AssertBeginTransactionEvents(listener); } } } @@ -99,15 +119,20 @@ public virtual async Task Intercept_BeginTransaction_to_suppress(bool async) var (context, interceptor) = CreateContext(); using (context) { - using (var _ = async - ? await context.Database.BeginTransactionAsync() - : context.Database.BeginTransaction()) + using (var listener = Fixture.SubscribeToDiagnosticListener(context.ContextId)) { - AssertBeginTransaction(context, interceptor, async); - - // Throws if a real transaction has been created - using (context.Database.GetDbConnection().BeginTransaction()) + using (var _ = async + ? await context.Database.BeginTransactionAsync() + : context.Database.BeginTransaction()) { + AssertBeginTransaction(context, interceptor, async); + + // Throws if a real transaction has been created + using (context.Database.GetDbConnection().BeginTransaction()) + { + } + + AssertBeginTransactionEvents(listener); } } } @@ -145,13 +170,18 @@ public virtual async Task Intercept_BeginTransaction_to_wrap(bool async) var (context, interceptor) = CreateContext(); using (context) { - using (var transaction = async - ? await context.Database.BeginTransactionAsync() - : context.Database.BeginTransaction()) + using (var listener = Fixture.SubscribeToDiagnosticListener(context.ContextId)) { - AssertBeginTransaction(context, interceptor, async); + using (var transaction = async + ? await context.Database.BeginTransactionAsync() + : context.Database.BeginTransaction()) + { + AssertBeginTransaction(context, interceptor, async); + + Assert.IsType(transaction.GetDbTransaction()); - Assert.IsType(transaction.GetDbTransaction()); + AssertBeginTransactionEvents(listener); + } } } } @@ -209,16 +239,22 @@ public virtual async Task Intercept_UseTransaction(bool async) var (context, interceptor) = CreateContext(); using (context) { - using (var transaction = context.Database.GetDbConnection().BeginTransaction()) + using (var listener = Fixture.SubscribeToDiagnosticListener(context.ContextId)) { - var contextTransaction = async - ? await context.Database.UseTransactionAsync(transaction) - : context.Database.UseTransaction(transaction); + using (var transaction = context.Database.GetDbConnection().BeginTransaction()) + { + var contextTransaction = async + ? await context.Database.UseTransactionAsync(transaction) + : context.Database.UseTransaction(transaction); + + AssertUseTransaction(context, contextTransaction, interceptor, async); + } - AssertUseTransaction(context, contextTransaction, interceptor, async); + AssertUseTransactionEvents(listener); } } } + [ConditionalTheory] [InlineData(false)] [InlineData(true)] @@ -227,15 +263,20 @@ public virtual async Task Intercept_UseTransaction_to_wrap(bool async) var (context, interceptor) = CreateContext(); using (context) { - using (var transaction = context.Database.GetDbConnection().BeginTransaction()) + using (var listener = Fixture.SubscribeToDiagnosticListener(context.ContextId)) { - var contextTransaction = async - ? await context.Database.UseTransactionAsync(transaction) - : context.Database.UseTransaction(transaction); + using (var transaction = context.Database.GetDbConnection().BeginTransaction()) + { + var contextTransaction = async + ? await context.Database.UseTransactionAsync(transaction) + : context.Database.UseTransaction(transaction); + + Assert.IsType(contextTransaction.GetDbTransaction()); - Assert.IsType(contextTransaction.GetDbTransaction()); + AssertUseTransaction(context, contextTransaction, interceptor, async); - AssertUseTransaction(context, contextTransaction, interceptor, async); + AssertUseTransactionEvents(listener); + } } } } @@ -254,16 +295,21 @@ public virtual async Task Intercept_Commit(bool async) { interceptor.Reset(); - if (async) - { - await contextTransaction.CommitAsync(); - } - else + using (var listener = Fixture.SubscribeToDiagnosticListener(context.ContextId)) { - contextTransaction.Commit(); - } + if (async) + { + await contextTransaction.CommitAsync(); + } + else + { + contextTransaction.Commit(); + } + + AssertCommit(context, contextTransaction, interceptor, async); - AssertCommit(context, contextTransaction, interceptor, async); + AssertCommitEvents(listener); + } } } } @@ -282,19 +328,24 @@ public virtual async Task Intercept_Commit_to_suppress(bool async) { interceptor.Reset(); - if (async) - { - await contextTransaction.CommitAsync(); - } - else + using (var listener = Fixture.SubscribeToDiagnosticListener(context.ContextId)) { - contextTransaction.Commit(); - } + if (async) + { + await contextTransaction.CommitAsync(); + } + else + { + contextTransaction.Commit(); + } - // Will throw if Commit was already called - contextTransaction.GetDbTransaction().Commit(); + // Will throw if Commit was already called + contextTransaction.GetDbTransaction().Commit(); + + AssertCommit(context, contextTransaction, interceptor, async); - AssertCommit(context, contextTransaction, interceptor, async); + AssertCommitEvents(listener); + } } } } @@ -313,16 +364,21 @@ public virtual async Task Intercept_Rollback(bool async) { interceptor.Reset(); - if (async) - { - await contextTransaction.RollbackAsync(); - } - else + using (var listener = Fixture.SubscribeToDiagnosticListener(context.ContextId)) { - contextTransaction.Rollback(); - } + if (async) + { + await contextTransaction.RollbackAsync(); + } + else + { + contextTransaction.Rollback(); + } + + AssertRollBack(context, contextTransaction, interceptor, async); - AssertRollBack(context, contextTransaction, interceptor, async); + AssertRollBackEvents(listener); + } } } } @@ -341,19 +397,24 @@ public virtual async Task Intercept_Rollback_to_suppress(bool async) { interceptor.Reset(); - if (async) - { - await contextTransaction.RollbackAsync(); - } - else + using (var listener = Fixture.SubscribeToDiagnosticListener(context.ContextId)) { - contextTransaction.Rollback(); - } + if (async) + { + await contextTransaction.RollbackAsync(); + } + else + { + contextTransaction.Rollback(); + } - // Will throw if Commit was already called - contextTransaction.GetDbTransaction().Commit(); + // Will throw if Commit was already called + contextTransaction.GetDbTransaction().Commit(); + + AssertRollBack(context, contextTransaction, interceptor, async); - AssertRollBack(context, contextTransaction, interceptor, async); + AssertRollBackEvents(listener); + } } } } @@ -477,17 +538,22 @@ public virtual async Task Intercept_connection_with_multiple_interceptors(bool a interceptor3, interceptor4, new NoOpTransactionInterceptor() })) { - using (var contextTransaction = async - ? await context.Database.BeginTransactionAsync() - : context.Database.BeginTransaction()) + using (var listener = Fixture.SubscribeToDiagnosticListener(context.ContextId)) { + using (var contextTransaction = async + ? await context.Database.BeginTransactionAsync() + : context.Database.BeginTransaction()) + { + + Assert.IsType(contextTransaction.GetDbTransaction()); - Assert.IsType(contextTransaction.GetDbTransaction()); + AssertBeginTransaction(context, interceptor1, async); + AssertBeginTransaction(context, interceptor2, async); + AssertBeginTransaction(context, interceptor3, async); + AssertBeginTransaction(context, interceptor4, async); - AssertBeginTransaction(context, interceptor1, async); - AssertBeginTransaction(context, interceptor2, async); - AssertBeginTransaction(context, interceptor3, async); - AssertBeginTransaction(context, interceptor4, async); + AssertBeginTransactionEvents(listener); + } } } } @@ -626,6 +692,24 @@ private static void AssertError( Assert.Same(context, interceptor.Context); } + private static void AssertBeginTransactionEvents(ITestDiagnosticListener listener) + => listener.AssertEventsInOrder( + RelationalEventId.TransactionStarting.Name, + RelationalEventId.TransactionStarted.Name); + + private static void AssertUseTransactionEvents(ITestDiagnosticListener listener) + => listener.AssertEventsInOrder(RelationalEventId.TransactionUsed.Name); + + private static void AssertCommitEvents(ITestDiagnosticListener listener) + => listener.AssertEventsInOrder( + RelationalEventId.TransactionCommitting.Name, + RelationalEventId.TransactionCommitted.Name); + + private static void AssertRollBackEvents(ITestDiagnosticListener listener) + => listener.AssertEventsInOrder( + RelationalEventId.TransactionRollingBack.Name, + RelationalEventId.TransactionRolledBack.Name); + protected class TransactionInterceptor : IDbTransactionInterceptor { public DbContext Context { get; set; } diff --git a/test/EFCore.Specification.Tests/InterceptionTestBase.cs b/test/EFCore.Specification.Tests/InterceptionTestBase.cs index c65f02a87f9..3a63e74c56a 100644 --- a/test/EFCore.Specification.Tests/InterceptionTestBase.cs +++ b/test/EFCore.Specification.Tests/InterceptionTestBase.cs @@ -1,8 +1,10 @@ // 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.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; +using System.Diagnostics; using System.Linq; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.TestUtilities; @@ -107,8 +109,98 @@ public UniverseContext CreateContext( IEnumerable injectedInterceptors = null) => new UniverseContext(Fixture.CreateOptions(appInterceptors, injectedInterceptors ?? Enumerable.Empty())); + public interface ITestDiagnosticListener : IDisposable + { + void AssertEventsInOrder(params string[] eventNames); + } + + public class NullDiagnosticListener: ITestDiagnosticListener + { + public void AssertEventsInOrder(params string[] eventNames) + { + } + + public void Dispose() + { + } + } + + public class TestDiagnosticListener : ITestDiagnosticListener, IObserver, IObserver> + { + private readonly DbContextId _contextId; + private readonly IDisposable _subscription; + private readonly List _events = new List(); + + public TestDiagnosticListener(DbContextId contextId) + { + _contextId = contextId; + _subscription = DiagnosticListener.AllListeners.Subscribe(this); + } + + public void OnCompleted() + { + } + + public void OnError(Exception error) + { + } + + + public void AssertEventsInOrder(params string[] eventNames) + { + Assert.True(_events.Count >= eventNames.Length); + + var lastIndex = -1; + for (var i = 0; i < eventNames.Length; i++) + { + var indexFound = _events.IndexOf(eventNames[i]); + + if (indexFound < 0) + { + Assert.True(false, $"Event {eventNames[i]} not found."); + } + + if (indexFound < lastIndex) + { + Assert.True(false, $"Event {eventNames[i]} found before {eventNames[i - 1]}."); + } + + lastIndex = indexFound; + } + } + + public void OnNext(DiagnosticListener listener) + { + if (listener?.Name == DbLoggerCategory.Name) + { + listener.Subscribe(this); + } + } + + public void OnNext(KeyValuePair value) + { + var eventData = value.Value as DbContextEventData; + if (eventData?.Context?.ContextId == _contextId) + { + _events.Add(value.Key); + } + } + + public void Dispose() + { + _subscription.Dispose(); + } + } + public abstract class InterceptionFixtureBase : SharedStoreFixtureBase { + protected abstract bool ShouldSubscribeToDiagnosticListener { get; } + + public virtual ITestDiagnosticListener SubscribeToDiagnosticListener(DbContextId contextId) + => ShouldSubscribeToDiagnosticListener + ? (ITestDiagnosticListener)new TestDiagnosticListener(contextId) + : new NullDiagnosticListener(); + public virtual DbContextOptions CreateOptions( IEnumerable appInterceptors, IEnumerable injectedInterceptors) diff --git a/test/EFCore.SqlServer.FunctionalTests/CommandInterceptionSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/CommandInterceptionSqlServerTest.cs index b602ded7c12..80ded93c1c3 100644 --- a/test/EFCore.SqlServer.FunctionalTests/CommandInterceptionSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/CommandInterceptionSqlServerTest.cs @@ -10,10 +10,9 @@ namespace Microsoft.EntityFrameworkCore { - public class CommandInterceptionSqlServerTest - : CommandInterceptionTestBase, IClassFixture + public abstract class CommandInterceptionSqlServerTestBase : CommandInterceptionTestBase { - public CommandInterceptionSqlServerTest(InterceptionSqlServerFixture fixture) + protected CommandInterceptionSqlServerTestBase(InterceptionSqlServerFixtureBase fixture) : base(fixture) { } @@ -45,10 +44,9 @@ public override async Task Intercept_query_to_replace_execution(bool asy return null; } - public class InterceptionSqlServerFixture : InterceptionFixtureBase + public abstract class InterceptionSqlServerFixtureBase : InterceptionFixtureBase { protected override string StoreName => "CommandInterception"; - protected override ITestStoreFactory TestStoreFactory => SqlServerTestStoreFactory.Instance; protected override IServiceCollection InjectInterceptors( @@ -56,5 +54,33 @@ protected override IServiceCollection InjectInterceptors( IEnumerable injectedInterceptors) => base.InjectInterceptors(serviceCollection.AddEntityFrameworkSqlServer(), injectedInterceptors); } + + public class CommandInterceptionSqlServerTest + : CommandInterceptionSqlServerTestBase, IClassFixture + { + public CommandInterceptionSqlServerTest(InterceptionSqlServerFixture fixture) + : base(fixture) + { + } + + public class InterceptionSqlServerFixture : InterceptionSqlServerFixtureBase + { + protected override bool ShouldSubscribeToDiagnosticListener => false; + } + } + + public class CommandInterceptionWithDiagnosticsSqlServerTest + : CommandInterceptionSqlServerTestBase, IClassFixture + { + public CommandInterceptionWithDiagnosticsSqlServerTest(InterceptionSqlServerFixture fixture) + : base(fixture) + { + } + + public class InterceptionSqlServerFixture : InterceptionSqlServerFixtureBase + { + protected override bool ShouldSubscribeToDiagnosticListener => true; + } + } } } diff --git a/test/EFCore.SqlServer.FunctionalTests/ConnectionInterceptionSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/ConnectionInterceptionSqlServerTest.cs index 178a1e6e5ad..a4edebf2f80 100644 --- a/test/EFCore.SqlServer.FunctionalTests/ConnectionInterceptionSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/ConnectionInterceptionSqlServerTest.cs @@ -1,7 +1,10 @@ // 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.Collections.Generic; +using System.Data; +using System.Data.Common; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.TestUtilities; using Microsoft.Extensions.DependencyInjection; @@ -9,18 +12,16 @@ namespace Microsoft.EntityFrameworkCore { - public class ConnectionInterceptionSqlServerTest - : ConnectionInterceptionTestBase, IClassFixture + public abstract class ConnectionInterceptionSqlServerTestBase : ConnectionInterceptionTestBase { - public ConnectionInterceptionSqlServerTest(InterceptionSqlServerFixture fixture) + protected ConnectionInterceptionSqlServerTestBase(InterceptionSqlServerFixtureBase fixture) : base(fixture) { } - public class InterceptionSqlServerFixture : InterceptionFixtureBase + public abstract class InterceptionSqlServerFixtureBase : InterceptionFixtureBase { protected override string StoreName => "ConnectionInterception"; - protected override ITestStoreFactory TestStoreFactory => SqlServerTestStoreFactory.Instance; protected override IServiceCollection InjectInterceptors( @@ -30,6 +31,48 @@ protected override IServiceCollection InjectInterceptors( } protected override BadUniverseContext CreateBadUniverse(DbContextOptionsBuilder optionsBuilder) - => new BadUniverseContext(optionsBuilder.UseSqlServer("Database = IIzBad").Options); + => new BadUniverseContext(optionsBuilder.UseSqlServer(new FakeDbConnection()).Options); + + public class FakeDbConnection : DbConnection + { + public override string ConnectionString { get; set; } + public override string Database => "Database"; + public override string DataSource => "DataSource"; + public override string ServerVersion => throw new NotImplementedException(); + public override ConnectionState State => ConnectionState.Closed; + public override void ChangeDatabase(string databaseName) => throw new NotImplementedException(); + public override void Close() => throw new NotImplementedException(); + public override void Open() => throw new NotImplementedException(); + protected override DbTransaction BeginDbTransaction(System.Data.IsolationLevel isolationLevel) => throw new NotImplementedException(); + protected override DbCommand CreateDbCommand() => throw new NotImplementedException(); + } + + public class ConnectionInterceptionSqlServerTest + : ConnectionInterceptionSqlServerTestBase, IClassFixture + { + public ConnectionInterceptionSqlServerTest(InterceptionSqlServerFixture fixture) + : base(fixture) + { + } + + public class InterceptionSqlServerFixture : InterceptionSqlServerFixtureBase + { + protected override bool ShouldSubscribeToDiagnosticListener => false; + } + } + + public class ConnectionInterceptionWithDiagnosticsSqlServerTest + : ConnectionInterceptionSqlServerTestBase, IClassFixture + { + public ConnectionInterceptionWithDiagnosticsSqlServerTest(InterceptionSqlServerFixture fixture) + : base(fixture) + { + } + + public class InterceptionSqlServerFixture : InterceptionSqlServerFixtureBase + { + protected override bool ShouldSubscribeToDiagnosticListener => true; + } + } } } diff --git a/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs b/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs index 218de7c7194..07f1383d8ad 100644 --- a/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs @@ -65,7 +65,6 @@ private class DefaultOptionsPooledContext : DbContext public DefaultOptionsPooledContext(DbContextOptions options) : base(options) { - //ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; } } @@ -191,29 +190,116 @@ public void Can_pool_non_derived_context() var serviceProvider = BuildServiceProvider(); var serviceScope1 = serviceProvider.CreateScope(); - var context1 = serviceScope1.ServiceProvider.GetService(); var serviceScope2 = serviceProvider.CreateScope(); - var context2 = serviceScope2.ServiceProvider.GetService(); Assert.NotSame(context1, context2); + var id1 = context1.ContextId; + var id2 = context2.ContextId; + + Assert.NotEqual(default, id1.InstanceId); + Assert.NotEqual(id1, id2); + Assert.NotEqual(id1.InstanceId, id2.InstanceId); + Assert.Equal(1, id1.Lease); + Assert.Equal(1, id2.Lease); + serviceScope1.Dispose(); serviceScope2.Dispose(); - var serviceScope3 = serviceProvider.CreateScope(); + var id1d = context1.ContextId; + var id2d = context2.ContextId; + + Assert.Equal(id1, id1d); + Assert.Equal(id1.InstanceId, id1d.InstanceId); + Assert.Equal(1, id1d.Lease); + Assert.Equal(1, id2d.Lease); + var serviceScope3 = serviceProvider.CreateScope(); var context3 = serviceScope3.ServiceProvider.GetService(); + var id1r = context3.ContextId; + Assert.Same(context1, context3); + Assert.Equal(id1.InstanceId, id1r.InstanceId); + Assert.NotEqual(default, id1r.InstanceId); + Assert.NotEqual(id1, id1r); + Assert.Equal(2, id1r.Lease); var serviceScope4 = serviceProvider.CreateScope(); - var context4 = serviceScope4.ServiceProvider.GetService(); + var id2r = context4.ContextId; + Assert.Same(context2, context4); + Assert.Equal(id2.InstanceId, id2r.InstanceId); + Assert.NotEqual(default, id2r.InstanceId); + Assert.NotEqual(id2, id2r); + Assert.Equal(2, id2r.Lease); + } + + [ConditionalFact] + public void ContextIds_make_sense_when_not_pooling() + { + var serviceProvider = new ServiceCollection() + .AddDbContext( + ob + => ob.UseSqlServer(SqlServerNorthwindTestStoreFactory.NorthwindConnectionString) + .EnableServiceProviderCaching(false)) + .BuildServiceProvider(); + + var serviceScope1 = serviceProvider.CreateScope(); + var context1 = serviceScope1.ServiceProvider.GetService(); + + var serviceScope2 = serviceProvider.CreateScope(); + var context2 = serviceScope2.ServiceProvider.GetService(); + + Assert.NotSame(context1, context2); + + var id1 = context1.ContextId; + var id2 = context2.ContextId; + + Assert.NotEqual(default, id1.InstanceId); + Assert.NotEqual(default, id2.InstanceId); + + Assert.NotEqual(id1, id2); + Assert.Equal(0, id1.Lease); + Assert.Equal(0, id2.Lease); + + serviceScope1.Dispose(); + serviceScope2.Dispose(); + + var id1d = context1.ContextId; + var id2d = context2.ContextId; + + Assert.Equal(id1.InstanceId, id1d.InstanceId); + Assert.Equal(id2.InstanceId, id2d.InstanceId); + Assert.Equal(0, id1d.Lease); + Assert.Equal(0, id2d.Lease); + + var serviceScope3 = serviceProvider.CreateScope(); + var context3 = serviceScope3.ServiceProvider.GetService(); + + var id1r = context3.ContextId; + + Assert.NotSame(context1, context3); + Assert.NotEqual(default, id1r.InstanceId); + Assert.NotEqual(id1.InstanceId, id1r.InstanceId); + Assert.NotEqual(id1, id1r); + Assert.Equal(0, id1r.Lease); + + var serviceScope4 = serviceProvider.CreateScope(); + var context4 = serviceScope4.ServiceProvider.GetService(); + + var id2r = context4.ContextId; + + Assert.NotSame(context2, context4); + Assert.NotEqual(default, id2r.InstanceId); + Assert.NotEqual(id2.InstanceId, id2r.InstanceId); + Assert.NotEqual(id2, id2r); + Assert.Equal(0, id2r.Lease); } [ConditionalTheory] diff --git a/test/EFCore.SqlServer.FunctionalTests/TransactionInterceptionSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/TransactionInterceptionSqlServerTest.cs index 48df00cf086..a61c10021ba 100644 --- a/test/EFCore.SqlServer.FunctionalTests/TransactionInterceptionSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/TransactionInterceptionSqlServerTest.cs @@ -9,18 +9,16 @@ namespace Microsoft.EntityFrameworkCore { - public class TransactionInterceptionSqlServerTest - : TransactionInterceptionTestBase, IClassFixture + public abstract class TransactionInterceptionSqlServerTestBase : TransactionInterceptionTestBase { - public TransactionInterceptionSqlServerTest(InterceptionSqlServerFixture fixture) + protected TransactionInterceptionSqlServerTestBase(InterceptionSqlServerFixtureBase fixture) : base(fixture) { } - public class InterceptionSqlServerFixture : InterceptionFixtureBase + public abstract class InterceptionSqlServerFixtureBase : InterceptionFixtureBase { protected override string StoreName => "TransactionInterception"; - protected override ITestStoreFactory TestStoreFactory => SqlServerTestStoreFactory.Instance; protected override IServiceCollection InjectInterceptors( @@ -28,5 +26,33 @@ protected override IServiceCollection InjectInterceptors( IEnumerable injectedInterceptors) => base.InjectInterceptors(serviceCollection.AddEntityFrameworkSqlServer(), injectedInterceptors); } + + public class TransactionInterceptionSqlServerTest + : TransactionInterceptionSqlServerTestBase, IClassFixture + { + public TransactionInterceptionSqlServerTest(InterceptionSqlServerFixture fixture) + : base(fixture) + { + } + + public class InterceptionSqlServerFixture : InterceptionSqlServerFixtureBase + { + protected override bool ShouldSubscribeToDiagnosticListener => false; + } + } + + public class TransactionInterceptionWithDiagnosticsSqlServerTest + : TransactionInterceptionSqlServerTestBase, IClassFixture + { + public TransactionInterceptionWithDiagnosticsSqlServerTest(InterceptionSqlServerFixture fixture) + : base(fixture) + { + } + + public class InterceptionSqlServerFixture : InterceptionSqlServerFixtureBase + { + protected override bool ShouldSubscribeToDiagnosticListener => true; + } + } } } diff --git a/test/EFCore.Sqlite.FunctionalTests/CommandInterceptionSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/CommandInterceptionSqliteTest.cs index 78bf91975e7..981df5e087e 100644 --- a/test/EFCore.Sqlite.FunctionalTests/CommandInterceptionSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/CommandInterceptionSqliteTest.cs @@ -10,10 +10,9 @@ namespace Microsoft.EntityFrameworkCore { - public class CommandInterceptionSqliteTest - : CommandInterceptionTestBase, IClassFixture + public abstract class CommandInterceptionSqliteTestBase : CommandInterceptionTestBase { - public CommandInterceptionSqliteTest(InterceptionSqliteFixture fixture) + protected CommandInterceptionSqliteTestBase(InterceptionSqliteFixtureBase fixture) : base(fixture) { } @@ -45,10 +44,9 @@ public override async Task Intercept_query_to_replace_execution(bool asy return null; } - public class InterceptionSqliteFixture : InterceptionFixtureBase + public abstract class InterceptionSqliteFixtureBase : InterceptionFixtureBase { protected override string StoreName => "CommandInterception"; - protected override ITestStoreFactory TestStoreFactory => SqliteTestStoreFactory.Instance; protected override IServiceCollection InjectInterceptors( @@ -56,5 +54,33 @@ protected override IServiceCollection InjectInterceptors( IEnumerable injectedInterceptors) => base.InjectInterceptors(serviceCollection.AddEntityFrameworkSqlite(), injectedInterceptors); } + + public class CommandInterceptionSqliteTest + : CommandInterceptionSqliteTestBase, IClassFixture + { + public CommandInterceptionSqliteTest(InterceptionSqliteFixture fixture) + : base(fixture) + { + } + + public class InterceptionSqliteFixture : InterceptionSqliteFixtureBase + { + protected override bool ShouldSubscribeToDiagnosticListener => false; + } + } + + public class CommandInterceptionWithDiagnosticsSqliteTest + : CommandInterceptionSqliteTestBase, IClassFixture + { + public CommandInterceptionWithDiagnosticsSqliteTest(InterceptionSqliteFixture fixture) + : base(fixture) + { + } + + public class InterceptionSqliteFixture : InterceptionSqliteFixtureBase + { + protected override bool ShouldSubscribeToDiagnosticListener => true; + } + } } } diff --git a/test/EFCore.Sqlite.FunctionalTests/ConnectionInterceptionSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/ConnectionInterceptionSqliteTest.cs index 2086e714663..bee57ec3ef5 100644 --- a/test/EFCore.Sqlite.FunctionalTests/ConnectionInterceptionSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/ConnectionInterceptionSqliteTest.cs @@ -9,10 +9,9 @@ namespace Microsoft.EntityFrameworkCore { - public class ConnectionInterceptionSqliteTest - : ConnectionInterceptionTestBase, IClassFixture + public abstract class ConnectionInterceptionSqliteTestBase : ConnectionInterceptionTestBase { - public ConnectionInterceptionSqliteTest(InterceptionSqliteFixture fixture) + protected ConnectionInterceptionSqliteTestBase(InterceptionSqliteFixtureBase fixture) : base(fixture) { } @@ -20,10 +19,9 @@ public ConnectionInterceptionSqliteTest(InterceptionSqliteFixture fixture) protected override BadUniverseContext CreateBadUniverse(DbContextOptionsBuilder optionsBuilder) => new BadUniverseContext(optionsBuilder.UseSqlite("Data Source=file:data.db?mode=invalidmode").Options); - public class InterceptionSqliteFixture : InterceptionFixtureBase + public abstract class InterceptionSqliteFixtureBase : InterceptionFixtureBase { protected override string StoreName => "ConnectionInterception"; - protected override ITestStoreFactory TestStoreFactory => SqliteTestStoreFactory.Instance; protected override IServiceCollection InjectInterceptors( @@ -31,5 +29,33 @@ protected override IServiceCollection InjectInterceptors( IEnumerable injectedInterceptors) => base.InjectInterceptors(serviceCollection.AddEntityFrameworkSqlite(), injectedInterceptors); } + + public class ConnectionInterceptionSqliteTest + : ConnectionInterceptionSqliteTestBase, IClassFixture + { + public ConnectionInterceptionSqliteTest(InterceptionSqliteFixture fixture) + : base(fixture) + { + } + + public class InterceptionSqliteFixture : InterceptionSqliteFixtureBase + { + protected override bool ShouldSubscribeToDiagnosticListener => false; + } + } + + public class ConnectionInterceptionWithDiagnosticsSqliteTest + : ConnectionInterceptionSqliteTestBase, IClassFixture + { + public ConnectionInterceptionWithDiagnosticsSqliteTest(InterceptionSqliteFixture fixture) + : base(fixture) + { + } + + public class InterceptionSqliteFixture : InterceptionSqliteFixtureBase + { + protected override bool ShouldSubscribeToDiagnosticListener => true; + } + } } } diff --git a/test/EFCore.Sqlite.FunctionalTests/TransactionInterceptionSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/TransactionInterceptionSqliteTest.cs index f231967150a..4120b5ac862 100644 --- a/test/EFCore.Sqlite.FunctionalTests/TransactionInterceptionSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/TransactionInterceptionSqliteTest.cs @@ -9,18 +9,16 @@ namespace Microsoft.EntityFrameworkCore { - public class TransactionInterceptionSqliteTest - : TransactionInterceptionTestBase, IClassFixture + public abstract class TransactionInterceptionSqliteTestBase : TransactionInterceptionTestBase { - public TransactionInterceptionSqliteTest(InterceptionSqliteFixture fixture) + protected TransactionInterceptionSqliteTestBase(InterceptionSqliteFixtureBase fixture) : base(fixture) { } - public class InterceptionSqliteFixture : InterceptionFixtureBase + public abstract class InterceptionSqliteFixtureBase : InterceptionFixtureBase { protected override string StoreName => "TransactionInterception"; - protected override ITestStoreFactory TestStoreFactory => SqliteTestStoreFactory.Instance; protected override IServiceCollection InjectInterceptors( @@ -28,5 +26,33 @@ protected override IServiceCollection InjectInterceptors( IEnumerable injectedInterceptors) => base.InjectInterceptors(serviceCollection.AddEntityFrameworkSqlite(), injectedInterceptors); } + + public class TransactionInterceptionSqliteTest + : TransactionInterceptionSqliteTestBase, IClassFixture + { + public TransactionInterceptionSqliteTest(InterceptionSqliteFixture fixture) + : base(fixture) + { + } + + public class InterceptionSqliteFixture : InterceptionSqliteFixtureBase + { + protected override bool ShouldSubscribeToDiagnosticListener => false; + } + } + + public class TransactionInterceptionWithDiagnosticsSqliteTest + : TransactionInterceptionSqliteTestBase, IClassFixture + { + public TransactionInterceptionWithDiagnosticsSqliteTest(InterceptionSqliteFixture fixture) + : base(fixture) + { + } + + public class InterceptionSqliteFixture : InterceptionSqliteFixtureBase + { + protected override bool ShouldSubscribeToDiagnosticListener => true; + } + } } } diff --git a/test/EFCore.Tests/DbContextTest.cs b/test/EFCore.Tests/DbContextTest.cs index 6d2f22b4cf3..0b7c1fcf7e5 100644 --- a/test/EFCore.Tests/DbContextTest.cs +++ b/test/EFCore.Tests/DbContextTest.cs @@ -944,7 +944,7 @@ public async Task It_throws_object_disposed_exception(bool async) await Assert.ThrowsAsync(() => context.FindAsync(typeof(Random), 77).AsTask()); var methodCount = typeof(DbContext).GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly).Count(); - var expectedMethodCount = 42; + var expectedMethodCount = 43; Assert.True( methodCount == expectedMethodCount, userMessage: $"Expected {expectedMethodCount} methods on DbContext but found {methodCount}. " + @@ -957,6 +957,7 @@ public async Task It_throws_object_disposed_exception(bool async) var expectedProperties = new List { nameof(DbContext.ChangeTracker), + nameof(DbContext.ContextId), // By-design, does not throw for disposed context nameof(DbContext.Database), nameof(DbContext.Model) };