Skip to content

Commit c02dcae

Browse files
ajcvickersroji
andauthored
What's New: Interception (#4052)
Co-authored-by: Shay Rojansky <roji@roji.org>
1 parent 2209fee commit c02dcae

File tree

11 files changed

+1276
-11
lines changed

11 files changed

+1276
-11
lines changed

entity-framework/core/querying/sql-queries.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ The following example uses a SQL query that selects from a Table-Valued Function
130130
> [!NOTE]
131131
> This feature was introduced in EF Core 7.0.
132132
133-
While <xref:Microsoft.EntityFrameworkCore.RelationalQueryableExtensions.FromSql%2A> is useful for querying entities defined in your model, <xref:Microsoft.EntityFrameworkCore.RelationalDatabaseFacadeExtensions.SqlQuery%2A> allows you to easily query for scalar, non-entity types via SQL, without needing to drop down to lower-level data access APIs. For example, the following query fetches all the IDs from the `Blogs` table:
133+
While <xref:Microsoft.EntityFrameworkCore.RelationalQueryableExtensions.FromSql%2A> is useful for querying entities defined in your model, [SqlQuery](https://github.com/dotnet/efcore/blob/2cfc7c3b9020daf9d2e28d404a78814e69941421/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs#L380) allows you to easily query for scalar, non-entity types via SQL, without needing to drop down to lower-level data access APIs. For example, the following query fetches all the IDs from the `Blogs` table:
134134

135135
[!code-csharp[Main](../../../samples/core/Querying/SqlQueries/Program.cs#SqlQuery)]
136136

@@ -140,15 +140,15 @@ You can also compose LINQ operators over your SQL query. However, since your SQL
140140

141141
<xref:Microsoft.EntityFrameworkCore.RelationalQueryableExtensions.FromSql%2A> can be used with any scalar type supported by your database provider. If you'd like to use a type not supported by your database provider, you can use [pre-convention configuration](xref:core/modeling/bulk-configuration#pre-convention-configuration) to define a value conversion for it.
142142

143-
<xref:Microsoft.EntityFrameworkCore.RelationalDatabaseFacadeExtensions.SqlQueryRaw%2A> allows for dynamic construction of SQL queries, just like <xref:Microsoft.EntityFrameworkCore.RelationalQueryableExtensions.FromSqlRaw%2A> does for entity types.
143+
[SqlQueryRaw](https://github.com/dotnet/efcore/blob/2cfc7c3b9020daf9d2e28d404a78814e69941421/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs#L334) allows for dynamic construction of SQL queries, just like <xref:Microsoft.EntityFrameworkCore.RelationalQueryableExtensions.FromSqlRaw%2A> does for entity types.
144144

145145
## Executing non-querying SQL
146146

147-
In some scenarios, it may be necessary to execute SQL which does not return any data, typically for modifying data in the database or calling a stored procedure which doesn't return any result sets. This can be done via <xref:Microsoft.EntityFrameworkCore.RelationalDatabaseFacadeExtensions.ExecuteSql%2A>:
147+
In some scenarios, it may be necessary to execute SQL which does not return any data, typically for modifying data in the database or calling a stored procedure which doesn't return any result sets. This can be done via [ExecuteSql](https://github.com/dotnet/efcore/blob/main/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs#L222):
148148

149149
[!code-csharp[Main](../../../samples/core/Querying/SqlQueries/Program.cs#ExecuteSql)]
150150

151-
This executes the provided SQL and returns the number of rows modified. <xref:Microsoft.EntityFrameworkCore.RelationalDatabaseFacadeExtensions.ExecuteSql%2A> protects against SQL injection by using safe parameterization, just like <xref:Microsoft.EntityFrameworkCore.RelationalQueryableExtensions.FromSql%2A>, and <xref:Microsoft.EntityFrameworkCore.RelationalDatabaseFacadeExtensions.ExecuteSqlRaw%2A> allows for dynamic construction of SQL queries, just like <xref:Microsoft.EntityFrameworkCore.RelationalQueryableExtensions.FromSqlRaw%2A> does for queries.
151+
This executes the provided SQL and returns the number of rows modified. [ExecuteSql](https://github.com/dotnet/efcore/blob/main/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs#L222) protects against SQL injection by using safe parameterization, just like <xref:Microsoft.EntityFrameworkCore.RelationalQueryableExtensions.FromSql%2A>, and <xref:Microsoft.EntityFrameworkCore.RelationalDatabaseFacadeExtensions.ExecuteSqlRaw%2A> allows for dynamic construction of SQL queries, just like <xref:Microsoft.EntityFrameworkCore.RelationalQueryableExtensions.FromSqlRaw%2A> does for queries.
152152

153153
> [!NOTE]
154154
>

entity-framework/core/what-is-new/ef-core-7.0/whatsnew.md

+564-1
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
using System.ComponentModel.DataAnnotations.Schema;
2+
using Microsoft.EntityFrameworkCore;
3+
using Microsoft.EntityFrameworkCore.Diagnostics;
4+
using Microsoft.EntityFrameworkCore.Infrastructure;
5+
using Microsoft.Extensions.DependencyInjection;
6+
using Microsoft.Extensions.Logging;
7+
8+
namespace NewInEfCore7;
9+
10+
public static class InjectLoggerSample
11+
{
12+
public static async Task Injecting_services_into_entities()
13+
{
14+
PrintSampleName();
15+
16+
var loggerFactory = LoggerFactory.Create(builder => { builder.AddConsole(); });
17+
18+
var serviceProvider = new ServiceCollection()
19+
.AddDbContext<CustomerContext>(
20+
b => b.UseLoggerFactory(loggerFactory)
21+
.UseSqlite("Data Source = customers.db"))
22+
.BuildServiceProvider();
23+
24+
using (var scope = serviceProvider.CreateScope())
25+
{
26+
var context = scope.ServiceProvider.GetRequiredService<CustomerContext>();
27+
28+
await context.Database.EnsureDeletedAsync();
29+
await context.Database.EnsureCreatedAsync();
30+
31+
await context.AddRangeAsync(
32+
new Customer { Name = "Alice", PhoneNumber = "+1 515 555 0123" },
33+
new Customer { Name = "Mac", PhoneNumber = "+1 515 555 0124" });
34+
35+
await context.SaveChangesAsync();
36+
}
37+
38+
using (var scope = serviceProvider.CreateScope())
39+
{
40+
var context = scope.ServiceProvider.GetRequiredService<CustomerContext>();
41+
42+
var customer = await context.Customers.SingleAsync(e => e.Name == "Alice");
43+
customer.PhoneNumber = "+1 515 555 0125";
44+
}
45+
}
46+
47+
private static void PrintSampleName([CallerMemberName] string? methodName = null)
48+
{
49+
Console.WriteLine($">>>> Sample: {methodName}");
50+
Console.WriteLine();
51+
}
52+
53+
public class CustomerContext : DbContext
54+
{
55+
public CustomerContext(DbContextOptions<CustomerContext> options)
56+
: base(options)
57+
{
58+
}
59+
60+
public DbSet<Customer> Customers
61+
=> Set<Customer>();
62+
63+
#region OnConfiguring
64+
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
65+
=> optionsBuilder.AddInterceptors(new LoggerInjectionInterceptor());
66+
#endregion
67+
}
68+
69+
#region LoggerInjectionInterceptor
70+
public class LoggerInjectionInterceptor : IMaterializationInterceptor
71+
{
72+
private ILogger? _logger;
73+
74+
public object InitializedInstance(MaterializationInterceptionData materializationData, object instance)
75+
{
76+
if (instance is IHasLogger hasLogger)
77+
{
78+
_logger ??= materializationData.Context.GetService<ILoggerFactory>().CreateLogger("CustomersLogger");
79+
hasLogger.Logger = _logger;
80+
}
81+
82+
return instance;
83+
}
84+
}
85+
#endregion
86+
87+
#region IHasLogger
88+
public interface IHasLogger
89+
{
90+
ILogger? Logger { get; set; }
91+
}
92+
#endregion
93+
94+
#region CustomerIHasLogger
95+
public class Customer : IHasLogger
96+
{
97+
private string? _phoneNumber;
98+
99+
public int Id { get; set; }
100+
public string Name { get; set; } = null!;
101+
102+
public string? PhoneNumber
103+
{
104+
get => _phoneNumber;
105+
set
106+
{
107+
Logger?.LogInformation(1, $"Updating phone number for '{Name}' from '{_phoneNumber}' to '{value}'.");
108+
109+
_phoneNumber = value;
110+
}
111+
}
112+
113+
[NotMapped]
114+
public ILogger? Logger { get; set; }
115+
}
116+
#endregion
117+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
using Microsoft.EntityFrameworkCore.Diagnostics;
2+
using Microsoft.EntityFrameworkCore.Infrastructure;
3+
4+
namespace NewInEfCore7;
5+
6+
public static class LazyConnectionStringSample
7+
{
8+
public static async Task Lazy_initialization_of_a_connection_string()
9+
{
10+
PrintSampleName();
11+
12+
var services = new ServiceCollection();
13+
14+
services.AddScoped<IClientConnectionStringFactory, TestClientConnectionStringFactory>();
15+
16+
services.AddDbContext<CustomerContext>(
17+
b => b.UseSqlServer()
18+
.LogTo(Console.WriteLine, LogLevel.Information)
19+
.EnableSensitiveDataLogging());
20+
21+
var serviceProvider = services.BuildServiceProvider();
22+
23+
using (var scope = serviceProvider.CreateScope())
24+
{
25+
var context = scope.ServiceProvider.GetRequiredService<CustomerContext>();
26+
27+
await context.Database.EnsureDeletedAsync();
28+
await context.Database.EnsureCreatedAsync();
29+
30+
await context.AddRangeAsync(
31+
new Customer { Name = "Alice" },
32+
new Customer { Name = "Mac" });
33+
34+
await context.SaveChangesAsync();
35+
36+
var customer = await context.Customers.SingleAsync(e => e.Name == "Alice");
37+
Console.WriteLine();
38+
Console.WriteLine($"Loaded {customer.Name}");
39+
Console.WriteLine();
40+
}
41+
}
42+
43+
private static void PrintSampleName([CallerMemberName] string? methodName = null)
44+
{
45+
Console.WriteLine($">>>> Sample: {methodName}");
46+
Console.WriteLine();
47+
}
48+
49+
public class CustomerContext : DbContext
50+
{
51+
private readonly IClientConnectionStringFactory _connectionStringFactory;
52+
53+
public CustomerContext(
54+
DbContextOptions<CustomerContext> options,
55+
IClientConnectionStringFactory connectionStringFactory)
56+
: base(options)
57+
{
58+
_connectionStringFactory = connectionStringFactory;
59+
}
60+
61+
public DbSet<Customer> Customers
62+
=> Set<Customer>();
63+
64+
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
65+
=> optionsBuilder.AddInterceptors(
66+
new ConnectionStringInitializationInterceptor(_connectionStringFactory));
67+
}
68+
69+
public interface IClientConnectionStringFactory
70+
{
71+
Task<string> GetConnectionStringAsync(CancellationToken cancellationToken);
72+
}
73+
74+
public class TestClientConnectionStringFactory : IClientConnectionStringFactory
75+
{
76+
public Task<string> GetConnectionStringAsync(CancellationToken cancellationToken)
77+
{
78+
Console.WriteLine();
79+
Console.WriteLine(">>> Getting connection string...");
80+
Console.WriteLine();
81+
return Task.FromResult(@"Server=(localdb)\mssqllocaldb;Database=LazyConnectionStringSample");
82+
}
83+
}
84+
85+
#region ConnectionStringInitializationInterceptor
86+
public class ConnectionStringInitializationInterceptor : DbConnectionInterceptor
87+
{
88+
private readonly IClientConnectionStringFactory _connectionStringFactory;
89+
90+
public ConnectionStringInitializationInterceptor(IClientConnectionStringFactory connectionStringFactory)
91+
{
92+
_connectionStringFactory = connectionStringFactory;
93+
}
94+
95+
public override InterceptionResult ConnectionOpening(
96+
DbConnection connection,
97+
ConnectionEventData eventData,
98+
InterceptionResult result)
99+
=> throw new NotSupportedException("Synchronous connections not supported.");
100+
101+
public override async ValueTask<InterceptionResult> ConnectionOpeningAsync(
102+
DbConnection connection, ConnectionEventData eventData, InterceptionResult result,
103+
CancellationToken cancellationToken = new())
104+
{
105+
if (string.IsNullOrEmpty(connection.ConnectionString))
106+
{
107+
connection.ConnectionString = (await _connectionStringFactory.GetConnectionStringAsync(cancellationToken));
108+
}
109+
110+
return result;
111+
}
112+
}
113+
#endregion
114+
115+
public class Customer
116+
{
117+
public int Id { get; set; }
118+
public string Name { get; set; } = null!;
119+
}
120+
}

samples/core/Miscellaneous/NewInEFCore7/NewInEFCore7.csproj

+6-5
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@
99
</PropertyGroup>
1010

1111
<ItemGroup>
12-
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.0-rc.2.22469.5" />
13-
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.0-rc.2.22469.5" />
14-
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.0-rc.2.22469.5" />
15-
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer.NetTopologySuite" Version="7.0.0-rc.2.22469.5" />
16-
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.0-rc.2.22469.5" />
12+
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.0-rc.2.22472.11" />
13+
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.0-rc.2.22472.11" />
14+
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.0-rc.2.22472.11" />
15+
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer.NetTopologySuite" Version="7.0.0-rc.2.22472.11" />
16+
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.0-rc.2.22472.11" />
17+
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" />
1718
</ItemGroup>
1819

1920
<ItemGroup>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
using Microsoft.EntityFrameworkCore;
2+
using Microsoft.EntityFrameworkCore.Diagnostics;
3+
using Microsoft.Extensions.Logging;
4+
5+
namespace NewInEfCore7;
6+
7+
public static class OptimisticConcurrencyInterceptionSample
8+
{
9+
public static async Task Optimistic_concurrency_interception()
10+
{
11+
PrintSampleName();
12+
13+
await using (var context = new CustomerContext())
14+
{
15+
await context.Database.EnsureDeletedAsync();
16+
await context.Database.EnsureCreatedAsync();
17+
18+
await context.AddRangeAsync(
19+
new Customer { Name = "Bill" },
20+
new Customer { Name = "Bob" });
21+
22+
await context.SaveChangesAsync();
23+
}
24+
25+
await using (var context1 = new CustomerContext())
26+
{
27+
var customer1 = await context1.Customers.SingleAsync(e => e.Name == "Bill");
28+
29+
await using (var context2 = new CustomerContext())
30+
{
31+
var customer2 = await context1.Customers.SingleAsync(e => e.Name == "Bill");
32+
context2.Entry(customer2).State = EntityState.Deleted;
33+
await context2.SaveChangesAsync();
34+
}
35+
36+
context1.Entry(customer1).State = EntityState.Deleted;
37+
await context1.SaveChangesAsync();
38+
}
39+
}
40+
41+
private static void PrintSampleName([CallerMemberName] string? methodName = null)
42+
{
43+
Console.WriteLine($">>>> Sample: {methodName}");
44+
Console.WriteLine();
45+
}
46+
47+
public class CustomerContext : DbContext
48+
{
49+
private static readonly SuppressDeleteConcurrencyInterceptor _concurrencyInterceptor = new();
50+
51+
public DbSet<Customer> Customers
52+
=> Set<Customer>();
53+
54+
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
55+
=> optionsBuilder
56+
.AddInterceptors(_concurrencyInterceptor)
57+
.UseSqlite("Data Source = customers.db")
58+
.LogTo(Console.WriteLine, LogLevel.Information);
59+
}
60+
61+
#region SuppressDeleteConcurrencyInterceptor
62+
public class SuppressDeleteConcurrencyInterceptor : ISaveChangesInterceptor
63+
{
64+
public InterceptionResult ThrowingConcurrencyException(
65+
ConcurrencyExceptionEventData eventData,
66+
InterceptionResult result)
67+
{
68+
if (eventData.Entries.All(e => e.State == EntityState.Deleted))
69+
{
70+
Console.WriteLine("Suppressing Concurrency violation for command:");
71+
Console.WriteLine(((RelationalConcurrencyExceptionEventData)eventData).Command.CommandText);
72+
73+
return InterceptionResult.Suppress();
74+
}
75+
76+
return result;
77+
}
78+
79+
public ValueTask<InterceptionResult> ThrowingConcurrencyExceptionAsync(
80+
ConcurrencyExceptionEventData eventData,
81+
InterceptionResult result,
82+
CancellationToken cancellationToken = default)
83+
=> new(ThrowingConcurrencyException(eventData, result));
84+
}
85+
#endregion
86+
87+
public class Customer
88+
{
89+
public int Id { get; set; }
90+
public string Name { get; set; } = null!;
91+
}
92+
}

samples/core/Miscellaneous/NewInEFCore7/Program.cs

+7
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,12 @@ public static async Task Main()
3535
await StoredProcedureMappingSample.Insert_Update_and_Delete_using_stored_procedures_with_TPH();
3636
await StoredProcedureMappingSample.Insert_Update_and_Delete_using_stored_procedures_with_TPT();
3737
await StoredProcedureMappingSample.Insert_Update_and_Delete_using_stored_procedures_with_TPC();
38+
39+
await SimpleMaterializationSample.Simple_actions_on_entity_creation();
40+
await QueryInterceptionSample.LINQ_expression_tree_interception();
41+
await OptimisticConcurrencyInterceptionSample.Optimistic_concurrency_interception();
42+
await InjectLoggerSample.Injecting_services_into_entities();
43+
await LazyConnectionStringSample.Lazy_initialization_of_a_connection_string();
44+
await QueryStatisticsLoggerSample.Executing_commands_after_consuming_a_result_set();
3845
}
3946
}

0 commit comments

Comments
 (0)