Skip to content

Commit 6263921

Browse files
authored
Updated the Dependency Injection and Entity Framework documentation (#7604)
1 parent 497b717 commit 6263921

File tree

2 files changed

+266
-255
lines changed

2 files changed

+266
-255
lines changed

website/src/docs/hotchocolate/v14/integrations/entity-framework.md

Lines changed: 136 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -2,204 +2,176 @@
22
title: Entity Framework Core
33
---
44

5-
[Entity Framework Core](https://docs.microsoft.com/ef/core/) is a powerful object-relational mapping framework that has become a staple when working with SQL-based Databases in .NET Core applications.
5+
[Entity Framework Core](https://docs.microsoft.com/ef/core/) is a powerful object-relational mapping framework that has become a staple when working with SQL-based databases in .NET Core applications.
66

7-
When working with Entity Framework Core's [DbContext](https://docs.microsoft.com/dotnet/api/system.data.entity.dbcontext), it is most commonly registered as a scoped service.
8-
9-
```csharp
10-
var builder = WebApplication.CreateBuilder(args);
11-
12-
builder.Services.AddDbContext<ApplicationDbContext>(
13-
options => options.UseSqlServer("YOUR_CONNECTION_STRING"));
14-
```
7+
# Resolver injection of a DbContext
158

16-
If you have read our [guidance on dependency injection](/docs/hotchocolate/v14/server/dependency-injection#resolver-injection) you might be inclined to simply inject your `DbContext` using the `HotChocolate.ServiceAttribute`.
9+
When using the [default scope](/docs/hotchocolate/v14/server/dependency-injection#default-scope) for queries, each execution of a query that accepts a scoped DbContext will receive a **separate** instance, avoiding [threading issues](https://learn.microsoft.com/en-gb/ef/core/dbcontext-configuration/#avoiding-dbcontext-threading-issues).
1710

1811
```csharp
19-
public Foo GetFoo([Service] ApplicationDbContext dbContext)
20-
=> // Omitted code for brevity
21-
```
22-
23-
While this is usually the correct way to inject services and it may appear to work initially, it has a fatal flaw: Entity Framework Core doesn't support [multiple parallel operations being run on the same context instance](https://docs.microsoft.com/ef/core/miscellaneous/async).
24-
25-
Lets take a look at an example to understand why this can lead to issues. Both the `foo` and `bar` field in the below query are backed by a resolver that injects a scoped `DbContext` instance and performs a database query using it.
26-
27-
```graphql
28-
{
29-
foo
30-
bar
31-
}
12+
public static async Task<Book?> GetBookByIdAsync(
13+
ApplicationDbContext dbContext) => // ...
3214
```
3315

34-
Since Hot Chocolate parallelizes the execution of query fields, and both of the resolvers will receive the same scoped `DbContext` instance, two database queries are likely to be ran through this scoped `DbContext` instance in parallel. This will then lead to one of the following exceptions being thrown:
35-
36-
- `A second operation started on this context before a previous operation completed.`
37-
- `Cannot access a disposed object.`
38-
39-
# Resolver injection of a DbContext
40-
41-
In order to ensure that resolvers do not access the same scoped `DbContext` instance in parallel, you can inject it using the `ServiceKind.Synchronized`.
16+
When using the [default scope](/docs/hotchocolate/v14/server/dependency-injection#default-scope) for mutations, each execution of a mutation that accepts a scoped DbContext will receive the **same** request-scoped instance, as mutations are executed sequentially.
4217

4318
```csharp
44-
public Foo GetFoo(
45-
[Service(ServiceKind.Synchronized)] ApplicationDbContext dbContext)
46-
=> // Omitted code for brevity
19+
public static async Task<Book> AddBookAsync(
20+
AddBookInput input,
21+
AppDbContext dbContext) => // ...
4722
```
4823

49-
[Learn more about `ServiceKind.Synchronized`](/docs/hotchocolate/v14/server/dependency-injection#servicekindsynchronized)
24+
See the [Dependency Injection](/docs/hotchocolate/v14/server/dependency-injection) documentation for more details.
5025

51-
Since this is a lot of code to write, just to inject a `DbContext`, you can use [`RegisterDbContext<T>`](#registerdbcontext) to simplify the injection.
26+
> Warning: Changing the default scope for queries will likely result in the error "A second operation started on this context before a previous operation completed", since Entity Framework Core does not support multiple parallel operations being run on the same DbContext instance.
5227
53-
# RegisterDbContext
28+
# Using a DbContext factory
5429

55-
In order to simplify the injection of a `DbContext` we have introduced a method called `RegisterDbContext<T>`, similar to the [`RegisterService<T>`](/docs/hotchocolate/v14/server/dependency-injection#registerservice) method for regular services. This method is part of the `HotChocolate.Data.EntityFramework` package, which you'll have to install.
30+
In order to use a DbContext factory, you need to register your DbContext with Hot Chocolate. To do so, an additional package needs to be installed:
5631

5732
<PackageInstallation packageName="HotChocolate.Data.EntityFramework" />
5833

59-
Once installed you can simply call the `RegisterDbContext<T>` method on the `IRequestExecutorBuilder`. The Hot Chocolate Resolver Compiler will then take care of correctly injecting your scoped `DbContext` instance into your resolvers and also ensuring that the resolvers using it are never run in parallel.
34+
Once installed, you can simply call the `RegisterDbContextFactory<T>` method on the `IRequestExecutorBuilder`. The Hot Chocolate Resolver Compiler will then take care of correctly injecting your DbContext instance into your resolvers.
6035

6136
```csharp
6237
var builder = WebApplication.CreateBuilder(args);
6338

64-
builder.Services.AddDbContext<ApplicationDbContext>(
65-
options => options.UseSqlServer("YOUR_CONNECTION_STRING"));
66-
6739
builder.Services
68-
.AddGraphQLServer()
69-
.RegisterDbContext<ApplicationDbContext>()
70-
.AddQueryType<Query>();
40+
.AddDbContextFactory<ApplicationDbContext>(
41+
options => options.UseSqlServer("YOUR_CONNECTION_STRING"));
7142

72-
public class Query
73-
{
74-
public Foo GetFoo(ApplicationDbContext dbContext)
75-
=> // Omitted code for brevity
76-
}
77-
```
43+
// ... or AddPooledDbContextFactory.
7844
79-
> Warning: You still have to register your `DbContext` in the actual dependency injection container, by calling `services.AddDbContext<T>`. `RegisterDbContext<T>` on its own is not enough.
80-
81-
You can also specify a [DbContextKind](#dbcontextkind) as argument to the `RegisterDbContext<T>` method, to change how the `DbContext` should be injected.
82-
83-
```csharp
8445
builder.Services
8546
.AddGraphQLServer()
86-
.RegisterDbContext<ApplicationDbContext>(DbContextKind.Pooled)
47+
.RegisterDbContextFactory<ApplicationDbContext>() // ⬅️
48+
.AddTypes();
8749
```
8850

89-
# DbContextKind
90-
91-
When registering a `DbContext` you can specify a `DbContextKind` to instruct Hot Chocolate to use a certain strategy when injecting the `DbContext`. For the most part the `DbContextKind` is really similar to the [ServiceKind](/docs/hotchocolate/v14/server/dependency-injection#servicekind), with the exception of the [DbContextKind.Pooled](#dbcontextkindpooled).
92-
93-
## DbContextKind.Synchronized
94-
95-
This injection mechanism ensures that resolvers injecting the specified `DbContext` are never run in parallel. This allows you to use the same scoped `DbContext` instance throughout a request, without the risk of running into concurrency exceptions as mentioned above. It behaves in the same fashion as [ServiceKind.Synchronized](/docs/hotchocolate/v14/server/dependency-injection#servicekindsynchronized) does for regular services.
96-
97-
## DbContextKind.Resolver
98-
99-
This injection mechanism will resolve the scoped `DbContext` from a resolver-scoped [`IServiceScope`](https://docs.microsoft.com/dotnet/api/microsoft.extensions.dependencyinjection.iservicescope). It behaves in the same fashion as [ServiceKind.Resolver](/docs/hotchocolate/v14/server/dependency-injection#servicekindresolver) does for regular services. Since a different `DbContext` instance is resolved for each resolver invocation, Hot Chocolate can parallelize the execution of resolvers using this `DbContext`.
100-
101-
## DbContextKind.Pooled
102-
103-
This injection mechanism will require your `DbContext` to be registered as a [pooled](https://docs.microsoft.com/ef/core/performance/advanced-performance-topics?tabs=with-constant#dbcontext-pooling) `IDbContextFactory<T>`.
51+
<ExampleTabs>
52+
<Implementation>
10453

10554
```csharp
106-
var builder = WebApplication.CreateBuilder(args);
107-
108-
builder.Services.AddPooledDbContextFactory<ApplicationDbContext>(
109-
options => options.UseSqlServer("YOUR_CONNECTION_STRING"));
55+
[QueryType]
56+
public static class Query
57+
{
58+
public static async Task<Book?> GetBookByIdAsync(
59+
Guid id,
60+
ApplicationDbContext dbContext) // ⬅️
61+
{
62+
return await dbContext.Books.FindAsync(id);
63+
}
64+
}
65+
```
11066

111-
builder.Services
112-
.AddGraphQLServer()
113-
.RegisterDbContext<ApplicationDbContext>(DbContextKind.Pooled)
114-
.AddQueryType<Query>();
67+
</Implementation>
68+
<Code>
11569

116-
public class Query
70+
```csharp
71+
public sealed class QueryType : ObjectType
11772
{
118-
public Foo GetFoo(ApplicationDbContext dbContext)
119-
=> // Omitted code for brevity
73+
protected override void Configure(
74+
IObjectTypeDescriptor descriptor)
75+
{
76+
descriptor.Name(OperationTypeNames.Query);
77+
78+
descriptor
79+
.Field("bookById")
80+
.Argument("id", a => a.Type<NonNullType<UuidType>>())
81+
.Resolve(async ctx => await ctx
82+
.Service<IDbContextFactory<ApplicationDbContext>>()
83+
// ⬆️
84+
.CreateDbContext()
85+
.Books
86+
.FindAsync(ctx.ArgumentValue<Guid>("id")));
87+
}
12088
}
12189
```
12290

123-
When injecting a `DbContext` using the `DbContextKind.Pool`, Hot Chocolate will retrieve one `DbContext` instance from the pool for each invocation of a resolver. Once the resolver has finished executing, the instance will be returned to the pool.
91+
</Code>
92+
<Schema>
12493

125-
Since each resolver invocation is therefore working with a "transient" `DbContext` instance, Hot Chocolate can parallelize the execution of resolvers using this `DbContext`.
94+
Take a look at the implementation-first or code-first example.
12695

127-
# Working with a pooled DbContext
96+
</Schema>
97+
</ExampleTabs>
12898

129-
If you have registered your `DbContext` using [DbContextKind.Pooled](#dbcontextkindpooled) you are on your way to squeeze the most performance out of your GraphQL server, but unfortunately it also changes how you have to use the `DbContext`.
99+
> Warning: As shown above, you still need to add your `DbContextFactory` to the dependency injection container, by calling `AddDbContextFactory<T>` or `AddPooledDbContextFactory<T>`. `RegisterDbContextFactory<T>` on its own is not enough.
130100
131-
For example you need to move all of the configuration from the `OnConfiguring` method inside your `DbContext` into the configuration action on the `AddPooledDbContextFactory` call.
101+
# Working with a DbContext factory
132102

133-
You also need to access your `DbContext` differently. In the following chapters we will take a look at some of the changes you have to make.
103+
When you use a DbContext factory, you need to access your DbContext differently if it is not being directly injected into a resolver. In the following sections we will take a look at some of the changes that you need to make.
134104

135105
## DataLoaders
136106

137-
When creating DataLoaders that need access to your `DbContext`, you need to inject the `IDbContextFactory<T>` using the constructor.
107+
When creating DataLoaders that need access to your DbContext, you need to inject the `IDbContextFactory<T>` using the constructor.
138108

139-
The `DbContext` should only be created **and disposed** in the `LoadBatchAsync` method.
109+
The DbContext should only be created **and disposed** in the `LoadBatchAsync` method.
140110

141111
```csharp
142-
public class FooByIdDataLoader : BatchDataLoader<string, Foo>
112+
public sealed class BookByIdDataLoader : BatchDataLoader<Guid, Book>
143113
{
144-
private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;
114+
private readonly IDbContextFactory<AppDbContext>
115+
_dbContextFactory;
145116

146-
public FooByIdDataLoader(
147-
IDbContextFactory<ApplicationDbContext> dbContextFactory,
148-
IBatchScheduler batchScheduler, DataLoaderOptions options)
117+
public BookByIdDataLoader(
118+
IDbContextFactory<AppDbContext> dbContextFactory,
119+
IBatchScheduler batchScheduler,
120+
DataLoaderOptions options)
149121
: base(batchScheduler, options)
150122
{
151123
_dbContextFactory = dbContextFactory;
152124
}
153125

154-
protected override async Task<IReadOnlyDictionary<string, Foo>>
155-
LoadBatchAsync(IReadOnlyList<string> keys, CancellationToken ct)
126+
protected override async Task<IReadOnlyDictionary<Guid, Book>>
127+
LoadBatchAsync(
128+
IReadOnlyList<Guid> keys,
129+
CancellationToken cancellationToken)
156130
{
157-
await using ApplicationDbContext dbContext =
131+
using AppDbContext dbContext =
158132
_dbContextFactory.CreateDbContext();
159133

160-
return await dbContext.Foos
161-
.Where(s => keys.Contains(s.Id))
162-
.ToDictionaryAsync(t => t.Id, ct);
134+
return await dbContext.Books
135+
.Where(b => keys.Contains(b.Id))
136+
.ToDictionaryAsync(b => b.Id, cancellationToken);
163137
}
164138
}
165139
```
166140

167-
> Warning: It is important that you dispose the `DbContext` to return it to the pool. In the above example we are using `await using` to dispose the `DbContext` after it is no longer required.
141+
> Warning: It is important that you dispose the DbContext. In the example above we use the `using` statement to dispose the DbContext after it is no longer required.
168142
169143
## Services
170144

171-
When creating services, they now need to inject the `IDbContextFactory<T>` instead of the `DbContext` directly. Your services also need be of a transient lifetime. Otherwise you could be faced with the `DbContext` concurrency issue again, if the same `DbContext` instance is accessed by two resolvers through our service in parallel.
145+
When creating services, they now need to inject the `IDbContextFactory<T>` instead of the DbContext directly.
172146

173147
```csharp
174148
var builder = WebApplication.CreateBuilder(args);
175149

176-
builder.Services.AddPooledDbContextFactory<ApplicationDbContext>(
150+
builder.Services.AddDbContextFactory<ApplicationDbContext>(
177151
options => options.UseSqlServer("YOUR_CONNECTION_STRING"));
178152

179-
builder.Services.AddTransient<FooService>()
153+
builder.Services.AddScoped<BookService>()
180154

181155
builder.Services
182156
.AddGraphQLServer()
183-
.RegisterService<FooService>()
184-
.AddQueryType<Query>();
185-
186-
public class Query
187-
{
188-
public Foo GetFoo(FooService FooService)
189-
=> // Omitted code for brevity
190-
}
157+
.AddTypes();
158+
```
191159

192-
public class FooService : IAsyncDisposable
160+
```csharp
161+
public sealed class BookService : IAsyncDisposable
193162
{
194163
private readonly ApplicationDbContext _dbContext;
195164

196-
public FooService(IDbContextFactory<ApplicationDbContext> dbContextFactory)
165+
public BookService(
166+
IDbContextFactory<ApplicationDbContext> dbContextFactory)
197167
{
198168
_dbContext = dbContextFactory.CreateDbContext();
199169
}
200170

201-
public Foo GetFoo()
202-
=> _dbContext.Foos.FirstOrDefault();
171+
public async Task<Book?> GetBookAsync(Guid id)
172+
{
173+
return await _dbContext.Books.FindAsync(id);
174+
}
203175

204176
public ValueTask DisposeAsync()
205177
{
@@ -208,4 +180,49 @@ public class FooService : IAsyncDisposable
208180
}
209181
```
210182

211-
> Warning: It is important that you dispose the `DbContext` to return it to the pool, once your transient service is being disposed. In the above example we are implementing `IAsyncDisposable` and disposing the created `DbContext` in the `DisposeAsync` method. This method will be invoked by the dependency injection system.
183+
<ExampleTabs>
184+
<Implementation>
185+
186+
```csharp
187+
[QueryType]
188+
public static class Query
189+
{
190+
public static async Task<Book?> GetBookByIdAsync(
191+
Guid id,
192+
BookService bookService)
193+
{
194+
return await bookService.GetBookAsync(id);
195+
}
196+
}
197+
```
198+
199+
</Implementation>
200+
<Code>
201+
202+
```csharp
203+
public sealed class QueryType : ObjectType
204+
{
205+
protected override void Configure(
206+
IObjectTypeDescriptor descriptor)
207+
{
208+
descriptor.Name(OperationTypeNames.Query);
209+
210+
descriptor
211+
.Field("bookById")
212+
.Argument("id", a => a.Type<NonNullType<UuidType>>())
213+
.Resolve<Book?>(async ctx => await ctx
214+
.Service<BookService>()
215+
.GetBookAsync(ctx.ArgumentValue<Guid>("id")));
216+
}
217+
}
218+
```
219+
220+
</Code>
221+
<Schema>
222+
223+
Take a look at the implementation-first or code-first example.
224+
225+
</Schema>
226+
</ExampleTabs>
227+
228+
> Warning: It is important that you dispose the DbContext when your service is being disposed. In the example above we are implementing `IAsyncDisposable` and disposing the created DbContext in the `DisposeAsync` method. This method will be invoked by the dependency injection system.

0 commit comments

Comments
 (0)