Skip to content

Commit

Permalink
What's New: Interception
Browse files Browse the repository at this point in the history
  • Loading branch information
ajcvickers committed Sep 23, 2022
1 parent 2209fee commit 20394e6
Show file tree
Hide file tree
Showing 11 changed files with 1,268 additions and 11 deletions.
8 changes: 4 additions & 4 deletions entity-framework/core/querying/sql-queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ The following example uses a SQL query that selects from a Table-Valued Function
> [!NOTE]
> This feature was introduced in EF Core 7.0.
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:
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:

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

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

<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.

<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.
[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.

## Executing non-querying SQL

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>:
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):

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

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.
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.

> [!NOTE]
>
Expand Down
557 changes: 556 additions & 1 deletion entity-framework/core/what-is-new/ef-core-7.0/whatsnew.md

Large diffs are not rendered by default.

117 changes: 117 additions & 0 deletions samples/core/Miscellaneous/NewInEFCore7/InjectLoggerSample.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace NewInEfCore7;

public static class InjectLoggerSample
{
public static async Task Injecting_services_into_entities()
{
PrintSampleName();

var loggerFactory = LoggerFactory.Create(builder => { builder.AddConsole(); });

var serviceProvider = new ServiceCollection()
.AddDbContext<CustomerContext>(
b => b.UseLoggerFactory(loggerFactory)
.UseSqlite("Data Source = customers.db"))
.BuildServiceProvider();

using (var scope = serviceProvider.CreateScope())
{
var context = scope.ServiceProvider.GetRequiredService<CustomerContext>();

await context.Database.EnsureDeletedAsync();
await context.Database.EnsureCreatedAsync();

await context.AddRangeAsync(
new Customer { Name = "Alice", PhoneNumber = "+1 515 555 0123" },
new Customer { Name = "Mac", PhoneNumber = "+1 515 555 0124" });

await context.SaveChangesAsync();
}

using (var scope = serviceProvider.CreateScope())
{
var context = scope.ServiceProvider.GetRequiredService<CustomerContext>();

var customer = await context.Customers.SingleAsync(e => e.Name == "Alice");
customer.PhoneNumber = "+1 515 555 0125";
}
}

private static void PrintSampleName([CallerMemberName] string? methodName = null)
{
Console.WriteLine($">>>> Sample: {methodName}");
Console.WriteLine();
}

public class CustomerContext : DbContext
{
public CustomerContext(DbContextOptions<CustomerContext> options)
: base(options)
{
}

public DbSet<Customer> Customers
=> Set<Customer>();

#region OnConfiguring
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.AddInterceptors(new LoggerInjectionInterceptor());
#endregion
}

#region LoggerInjectionInterceptor
public class LoggerInjectionInterceptor : IMaterializationInterceptor
{
private ILogger? _logger;

public object InitializedInstance(MaterializationInterceptionData materializationData, object instance)
{
if (instance is IHasLogger hasLogger)
{
_logger ??= materializationData.Context.GetService<ILoggerFactory>().CreateLogger("CustomersLogger");
hasLogger.Logger = _logger;
}

return instance;
}
}
#endregion

#region IHasLogger
public interface IHasLogger
{
ILogger? Logger { get; set; }
}
#endregion

#region CustomerIHasLogger
public class Customer : IHasLogger
{
private string? _phoneNumber;

public int Id { get; set; }
public string Name { get; set; } = null!;

public string? PhoneNumber
{
get => _phoneNumber;
set
{
Logger?.LogInformation(1, $"Updating phone number for '{Name}' from '{_phoneNumber}' to '{value}'.");

_phoneNumber = value;
}
}

[NotMapped]
public ILogger? Logger { get; set; }
}
#endregion
}
120 changes: 120 additions & 0 deletions samples/core/Miscellaneous/NewInEFCore7/LazyConnectionStringSample.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Infrastructure;

namespace NewInEfCore7;

public static class LazyConnectionStringSample
{
public static async Task Lazy_initialization_of_a_connection_string()
{
PrintSampleName();

var services = new ServiceCollection();

services.AddScoped<IClientConnectionStringFactory, TestClientConnectionStringFactory>();

services.AddDbContext<CustomerContext>(
b => b.UseSqlServer()
.LogTo(Console.WriteLine, LogLevel.Information)
.EnableSensitiveDataLogging());

var serviceProvider = services.BuildServiceProvider();

using (var scope = serviceProvider.CreateScope())
{
var context = scope.ServiceProvider.GetRequiredService<CustomerContext>();

await context.Database.EnsureDeletedAsync();
await context.Database.EnsureCreatedAsync();

await context.AddRangeAsync(
new Customer { Name = "Alice" },
new Customer { Name = "Mac" });

await context.SaveChangesAsync();

var customer = await context.Customers.SingleAsync(e => e.Name == "Alice");
Console.WriteLine();
Console.WriteLine($"Loaded {customer.Name}");
Console.WriteLine();
}
}

private static void PrintSampleName([CallerMemberName] string? methodName = null)
{
Console.WriteLine($">>>> Sample: {methodName}");
Console.WriteLine();
}

public class CustomerContext : DbContext
{
private readonly IClientConnectionStringFactory _connectionStringFactory;

public CustomerContext(
DbContextOptions<CustomerContext> options,
IClientConnectionStringFactory connectionStringFactory)
: base(options)
{
_connectionStringFactory = connectionStringFactory;
}

public DbSet<Customer> Customers
=> Set<Customer>();

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.AddInterceptors(
new ConnectionStringInitializationInterceptor(_connectionStringFactory));
}

public interface IClientConnectionStringFactory
{
Task<string> GetConnectionStringAsync(CancellationToken cancellationToken);
}

public class TestClientConnectionStringFactory : IClientConnectionStringFactory
{
public Task<string> GetConnectionStringAsync(CancellationToken cancellationToken)
{
Console.WriteLine();
Console.WriteLine(">>> Getting connection string...");
Console.WriteLine();
return Task.FromResult(@"Server=(localdb)\mssqllocaldb;Database=LazyConnectionStringSample");
}
}

#region ConnectionStringInitializationInterceptor
public class ConnectionStringInitializationInterceptor : DbConnectionInterceptor
{
private readonly IClientConnectionStringFactory _connectionStringFactory;

public ConnectionStringInitializationInterceptor(IClientConnectionStringFactory connectionStringFactory)
{
_connectionStringFactory = connectionStringFactory;
}

public override InterceptionResult ConnectionOpening(
DbConnection connection,
ConnectionEventData eventData,
InterceptionResult result)
=> throw new NotSupportedException("Synchronous connections not supported.");

public override async ValueTask<InterceptionResult> ConnectionOpeningAsync(
DbConnection connection, ConnectionEventData eventData, InterceptionResult result,
CancellationToken cancellationToken = new())
{
if (string.IsNullOrEmpty(connection.ConnectionString))
{
connection.ConnectionString = (await _connectionStringFactory.GetConnectionStringAsync(cancellationToken));
}

return result;
}
}
#endregion

public class Customer
{
public int Id { get; set; }
public string Name { get; set; } = null!;
}
}
11 changes: 6 additions & 5 deletions samples/core/Miscellaneous/NewInEFCore7/NewInEFCore7.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.0-rc.2.22469.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.0-rc.2.22469.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.0-rc.2.22469.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer.NetTopologySuite" Version="7.0.0-rc.2.22469.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.0-rc.2.22469.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.0-rc.2.22472.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.0-rc.2.22472.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.0-rc.2.22472.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer.NetTopologySuite" Version="7.0.0-rc.2.22472.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.0-rc.2.22472.11" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Logging;

namespace NewInEfCore7;

public static class OptimisticConcurrencyInterceptionSample
{
public static async Task Optimistic_concurrency_interception()
{
PrintSampleName();

await using (var context = new CustomerContext())
{
await context.Database.EnsureDeletedAsync();
await context.Database.EnsureCreatedAsync();

await context.AddRangeAsync(
new Customer { Name = "Bill" },
new Customer { Name = "Bob" });

await context.SaveChangesAsync();
}

await using (var context1 = new CustomerContext())
{
var customer1 = await context1.Customers.SingleAsync(e => e.Name == "Bill");

await using (var context2 = new CustomerContext())
{
var customer2 = await context1.Customers.SingleAsync(e => e.Name == "Bill");
context2.Entry(customer2).State = EntityState.Deleted;
await context2.SaveChangesAsync();
}

context1.Entry(customer1).State = EntityState.Deleted;
await context1.SaveChangesAsync();
}
}

private static void PrintSampleName([CallerMemberName] string? methodName = null)
{
Console.WriteLine($">>>> Sample: {methodName}");
Console.WriteLine();
}

public class CustomerContext : DbContext
{
private static readonly SuppressDeleteConcurrencyInterceptor _concurrencyInterceptor = new();

public DbSet<Customer> Customers
=> Set<Customer>();

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.AddInterceptors(_concurrencyInterceptor)
.UseSqlite("Data Source = customers.db")
.LogTo(Console.WriteLine, LogLevel.Information);
}

#region SuppressDeleteConcurrencyInterceptor
public class SuppressDeleteConcurrencyInterceptor : ISaveChangesInterceptor
{
public InterceptionResult ThrowingConcurrencyException(
ConcurrencyExceptionEventData eventData,
InterceptionResult result)
{
if (eventData.Entries.All(e => e.State == EntityState.Deleted))
{
Console.WriteLine("Suppressing Concurrency violation for command:");
Console.WriteLine(((RelationalConcurrencyExceptionEventData)eventData).Command.CommandText);

return InterceptionResult.Suppress();
}

return result;
}

public ValueTask<InterceptionResult> ThrowingConcurrencyExceptionAsync(
ConcurrencyExceptionEventData eventData,
InterceptionResult result,
CancellationToken cancellationToken = default)
=> new(ThrowingConcurrencyException(eventData, result));
}
#endregion

public class Customer
{
public int Id { get; set; }
public string Name { get; set; } = null!;
}
}
7 changes: 7 additions & 0 deletions samples/core/Miscellaneous/NewInEFCore7/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,12 @@ public static async Task Main()
await StoredProcedureMappingSample.Insert_Update_and_Delete_using_stored_procedures_with_TPH();
await StoredProcedureMappingSample.Insert_Update_and_Delete_using_stored_procedures_with_TPT();
await StoredProcedureMappingSample.Insert_Update_and_Delete_using_stored_procedures_with_TPC();

await SimpleMaterializationSample.Simple_actions_on_entity_creation();
await QueryInterceptionSample.LINQ_expression_tree_interception();
await OptimisticConcurrencyInterceptionSample.Optimistic_concurrency_interception();
await InjectLoggerSample.Injecting_services_into_entities();
await LazyConnectionStringSample.Lazy_initialization_of_a_connection_string();
await QueryStatisticsLoggerSample.Executing_commands_after_consuming_a_result_set();
}
}

0 comments on commit 20394e6

Please sign in to comment.