I have a legacy REST
API, where I want to use its repository
for my new Hot Chocolate
API.
To use best practices I followed the guide here to set up my repository with a pooled context.
When I do not use the async dispose, everything works fine, but when I add the Interface and Method, I get the error
System.ObjectDisposedException: 'Cannot access a disposed context instance. A common cause of this error is disposing a context instance that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling 'Dispose' on the context instance, or wrapping it in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances. ObjectDisposed_ObjectName_Name'
The exception call stack is:
[External Code]
[Exception] DataStore.dll!DataStore.DataBase.Repositories.ProductRepository.ProductExists(string name) Line 160 C#
[Exception] DataStore.dll!DataStore.DataBase.Repositories.ProductRepository.AddProduct(DataStore.Logic.Models.Data.ProductDto product) Line 30 C#
[Exception] HotChocolate_demo.dll!HotChocolate_demo.Mutations.ProductMutation.AddProduct(string name, double manufacturinCost, System.Collections.Generic.List<DataStore.Logic.Models.Transient.MaterialTransientRecord> materials) Line 43 C#
[External Code]
DataStore.dll!DataStore.DataBase.Repositories.ProductRepository.ProductExists(string name) Line 160 C#
DataStore.dll!DataStore.DataBase.Repositories.ProductRepository.AddProduct(DataStore.Logic.Models.Data.ProductDto product) Line 30 C#
HotChocolate_demo.dll!HotChocolate_demo.Mutations.ProductMutation.AddProduct(string name, double manufacturinCost, System.Collections.Generic.List<DataStore.Logic.Models.Transient.MaterialTransientRecord> materials) Line 43 C#
[External Code]
but this only happens after the second query. For one acces, I can chain multiple mutations for example, and all go through. I assume, that I have no async void
or things like that in my code.
I recreated the tutorial mentioned with a simple example project and everything seems to work. My guess is, that it has something to do with my repository implementation.
public class HotRepository : ProductRepository, IAsyncDisposable
{
public HotRepository([Service(ServiceKind.Synchronized)] IDbContextFactory<ProductDbContext> contextFactory, IMapper mapper)
: base(context: contextFactory.CreateDbContext(), mapper: mapper)
{
}
public ValueTask DisposeAsync()
{
return _context.DisposeAsync();
}
}
public class ProductRepository : IProductRepository
{
protected readonly ProductDbContext _context;
private readonly IMapper _mapper;
public ProductRepository(ProductDbContext context, IMapper mapper)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
}
public bool ProductExists(string name)
{
return _context.Products.Any(product=>product.Name==name);
}
}
public interface IProductRepository
{
public IEnumerable<TProduct> GetAllProducts<TProduct>() where TProduct : IProduct;
public Task<IEnumerable<TProduct>> GetAllProductsAsync<TProduct>() where TProduct : IProduct;
public IEnumerable<ProductMaterialsJoin> GetMaterialsOfProduct(string productName);
public ActionResult AddProduct(ProductDto product);
public bool ProductExists(string name);
public Task<bool> ProductExistsAsync(string name);
public bool DeleteProduct(string name);
}
public class ProductMutation
{
private IProductRepository _repository;
public ProductMutation(IProductRepository repository)
{
_repository = repository;
}
}
I registered everything like this:
var builder = WebApplication.CreateBuilder(args);
// graphQL ---
var graphQlServerBuilder = builder.Services.AddGraphQLServer();
builder.Services.AddTransient<IProductRepository, HotRepository>();
graphQlServerBuilder.RegisterService<IProductRepository>();
// automapper for the repository
builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
// context
builder.Services.AddPooledDbContextFactory<ProductDbContext>(
contextOptions => contextOptions.UseSqlServer(connectionstring)
);
graphQlServerBuilder.RegisterDbContext<ProductDbContext>(DbContextKind.Pooled);
// type
graphQlServerBuilder.AddType<ProductType>();
If I set breakpoints in my async methods, they do not get called. Also the constructor gets only called once, same with the dispose, so somehow the lifetime seems to be maybe scoped instead of transient!?
So the ProductRepository
should be treated like the FooService
in the guide.
Could the exception come from calling the base constructor?
If yes, why?
public class ProductMutation
{
private IProductRepository _repository;
public ProductMutation(IProductRepository repository)
{
_repository = repository;
}
}
This is where you're going wrong. Mutation and Query types are singletons in HotChocolate. You need to resolve scoped/transient services in Resolvers.
public class ProductMutation
{
public async Task<Result> SetProperty([Service] IProductRepository repo, Input input)
{
// Do things with your scoped service ...
}
}