Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

What's New: Interception #4052

Merged
merged 7 commits into from
Sep 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
565 changes: 564 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();
}
}