Home > Enterprise >  'Cannot access a disposed context instance.' error when calling a method from a repository
'Cannot access a disposed context instance.' error when calling a method from a repository

Time:04-26

I am trying to call a method from a repository from a service.

The context, the repository and the service all are defined as scoped services.

This is the method I am calling first:

  public async void ReceiveWebhook(HttpContext httpContext)
    {
        // some unimportant checks here

        var productPurchaseRequest = new ProductPurchaseRequest
        {
            Amount = Convert.ToInt32(result?.Quantity),
            Timestamp = DateTime.Now,
            ProductType = productType,
            PaymentProviderOrderId = Convert.ToInt32(result?.OrderId),
            PaymentProviderProductId = Convert.ToInt32(result?.ProductId),
            PaymentProviderTransactionId = result?.TransactionId!,
            PaymentModel = PaymentModel.Subscription,
            PhoneNumber = result?.Passthrough!
            //todo: change payment model
        };
        var bought = await _productProvisioningRepository.PurchaseProduct(productPurchaseRequest);

    }

This is the method: PurchaseProduct() in the repository:

    public async Task<bool> PurchaseProduct(ProductPurchaseRequest productPurchaseRequest)
    {
        await using var transactionScope = await _context.Database.BeginTransactionAsync();
        
        var query = from u in _context.signumid_user
            where u.PhoneNumber == productPurchaseRequest.PhoneNumber
            select u;
        
        var user = await query.FirstOrDefaultAsync();
        
        if (user == null)
        {
            return false;
            //todo: log user with phone number does not exist
        }
        
        try
        {
            var transaction = new Transaction
            {
                Timestamp = productPurchaseRequest.Timestamp,
                PaymentProviderOrderId = productPurchaseRequest.PaymentProviderOrderId,
                PaymentProviderProductId = productPurchaseRequest.PaymentProviderProductId,
                PaymentProviderTransactionId = productPurchaseRequest.PaymentProviderTransactionId
            };
            var transactionDb = await _context.signumid_transaction.AddAsync(transaction);
            await _context.SaveChangesAsync();

            var productEntry = new ProductEntry
            {
                Amount = productPurchaseRequest.Amount,
                ExpiryDate = productPurchaseRequest.Timestamp.AddMonths(1),
                PaymentModel = (int) PaymentModel.Subscription,
                ProductType = productPurchaseRequest.ProductType,
                UserId = 1
            };
            
            var productEntryDb = await _context.signumid_product_entry.AddAsync(productEntry);
            await _context.SaveChangesAsync();

            var transactionProductEntry = new Transaction_ProductEntry
            {
                TransactionId = transactionDb.Entity.Id,
                ProductEntryId = productEntryDb.Entity.Id
            };
            
            var transactionProductEntryDb = await _context.sisgnumid_transaction_product_entry.AddAsync(transactionProductEntry);
            await _context.SaveChangesAsync();

            //todo: check if everything is okay with database entries
            
            await transactionScope.CommitAsync();
            return true;
        }
        catch (Exception e)
        {
            // todo: add log
            Console.WriteLine(e);
            await transactionScope.RollbackAsync();
            return false;
        }
    }

This is the program.cs file:

using System.Diagnostics;
using System.Reflection;
using System.Text.Json.Serialization;
using FluentMigrator.Runner;
using Hangfire;
using Hangfire.PostgreSql;
using Hangfire.SQLite;
using Hangfire.SqlServer;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.AspNetCore.Http.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.DataEncryption;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Newtonsoft.Json;
using Serilog;
using Serilog.Exceptions;
using Serilog.Exceptions.Core;
using Signumid.ExceptionMiddleware;
using Signumid.Global;
using Signumid.MigratorRunner;
using Signumid.ProductProvisioning;
using Signumid.ProductProvisioning.Migrations;

var builder = WebApplication.CreateBuilder(args);
var configurationBuilder = new ConfigurationBuilder()
    .SetBasePath(builder.Environment.ContentRootPath)
    .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
    .AddEnvironmentVariables();
var configurationRoot = configurationBuilder.Build();
ConfigureLogging(configurationRoot);

var applicationSettings = new Signumid.ApplicationSettings.ApplicationSettings();

configurationRoot.Bind(applicationSettings);

SignumIdGlobal.InitialiseApplicationSettings(applicationSettings);

// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.Configure<JsonOptions>(options =>
{
    options.SerializerOptions.PropertyNameCaseInsensitive = false;
    options.SerializerOptions.PropertyNamingPolicy = null;
    options.SerializerOptions.WriteIndented = true;
    options.SerializerOptions.Converters.Add(new JsonStringEnumConverter());
});

MigratorRunner.MigrateDatabase(builder.Services,
    SignumIdGlobal.ApplicationSettings.ProductProvisioningApplicationSettings.ConnectionString,
    SignumIdGlobal.ApplicationSettings.ProductProvisioningApplicationSettings.DatabaseProvider,
    Assembly.GetAssembly(typeof(InitialMigration)));


builder.Services.AddScoped<ProductProvisioningContext>();
builder.Services.AddScoped<ProductProvisioningRepository>();
builder.Services.AddScoped<ProductProvisioningService>();


ConfigureHangfire(builder.Services);
ConfigureHealthCheck(builder.Services);

builder.Services.AddCors(options =>
    options.AddPolicy("CorsPolicy",
        corsPolicyBuilder =>
        {
            corsPolicyBuilder
                .AllowAnyMethod()
                .AllowAnyHeader()
                .AllowCredentials()
                .SetIsOriginAllowed(_ => true)
                .WithExposedHeaders("Content-Disposition", "Content-Length");
        }));

builder.Services.AddAuthorization();

var app = builder.Build();
// Configure hangfire to use the new JobActivator we defined.

// Use the previously configured CorsPolicy policy
app.UseCors("CorsPolicy");

app.ConfigureExceptionHandler();
// Configure the HTTP request pipeline.
app.UseSwagger();
app.UseSwaggerUI();

app.UseHttpsRedirection();

app.MapHealthChecks("/v/1/health/basic");
app.MapHealthChecks("/v/1/health/simplified",
    new HealthCheckOptions
    {
        ResponseWriter = async (context, report) =>
        {
            context.Response.ContentType = "application/json";
            await context.Response.WriteAsync(JsonConvert.SerializeObject(new
            {
                status = report.Status.ToString(),
                monitors = report.Entries.Select(e => new
                    {key = e.Key, value = Enum.GetName(typeof(HealthStatus), e.Value.Status)})
            }));
        }
    }
).RequireHost(SignumIdGlobal.ApplicationSettings.ProductProvisioningApplicationSettings.HealthCheckHosts);

app.MapGet("/v/1/signer/getAvailableSignatures",
    (string phoneNumber, int productType, ProductProvisioningService service) => service.RetrieveRemainingProductAmountForUser(phoneNumber, productType));

app.MapGet("/v/1/signer/generatePayLink",
    (string phoneNumber, int quantity, int productId, ProductProvisioningService service) => service.GeneratePayLink(phoneNumber, quantity, productId));

app.MapPost("/v/1/signer/receiveWebhook",  
    (HttpContext context, ProductProvisioningService service) =>  service.ReceiveWebhook(context));

app.UseAuthorization();

app.UseHangfireDashboard();
GlobalConfiguration.Configuration
    .UseActivator(new HangfireActivator(app.Services));


var runner = app.Services.GetRequiredService<IMigrationRunner>();
// Execute the migrations
runner.MigrateUp();

app.Run();

static void ConfigureLogging(IConfigurationRoot configuration)
{
    try
    {
        Serilog.Debugging.SelfLog.Enable(msg => Debug.WriteLine(msg));
        Serilog.Debugging.SelfLog.Enable(Console.Error);
        // Impossible to set with appsettings:
        // https://stackoverflow.com/questions/58587661/json-configuration-for-serilog-exception-destructurers/58622735#58622735
        // https://github.com/RehanSaeed/Serilog.Exceptions/issues/58
        var loggingConfiguration = new LoggerConfiguration()
            .Enrich
            .WithExceptionDetails(new DestructuringOptionsBuilder().WithDefaultDestructurers())
            .ReadFrom
            .Configuration(configuration);

        Log.Logger = loggingConfiguration
            .CreateLogger();
    }
    catch (Exception e)
    {
        Serilog.Debugging.SelfLog.Enable(msg => Debug.WriteLine(msg));
        Serilog.Debugging.SelfLog.Enable(Console.Error);
        Log.Logger = new LoggerConfiguration()
            .WriteTo.Console()
            .MinimumLevel.Debug() // set to minimal in serilog
            .CreateLogger();
        Log.Debug(e,
            "Unable to import serilog configuration from appsettings.json, logging only to console. Error: {@Ex}", e);
    }

    var currentDomain = AppDomain.CurrentDomain;
    currentDomain.UnhandledException  = UnhandledExceptionHandler;
}

static void UnhandledExceptionHandler(object sender, UnhandledExceptionEventArgs args)
{
    var e = (Exception) args.ExceptionObject;
    Log.Fatal(e, "Unhandled exception caught : {@error}", e);
    Log.Fatal("Runtime terminating: {0}", args.IsTerminating);
}

static void ConfigureHealthCheck(IServiceCollection services)
{
    if (SignumIdGlobal.ApplicationSettings.ProductProvisioningApplicationSettings.DatabaseProvider.Equals("SqlLite",
            StringComparison.InvariantCultureIgnoreCase))
    {
        services.AddHealthChecks()
            .AddSqlite(SignumIdGlobal.ApplicationSettings.ProductProvisioningApplicationSettings.ConnectionString);
    }
    else if (SignumIdGlobal.ApplicationSettings.ProductProvisioningApplicationSettings.DatabaseProvider.Equals(
                 "postgresql",
                 StringComparison.InvariantCultureIgnoreCase))
    {
        services.AddHealthChecks()
            .AddNpgSql(SignumIdGlobal.ApplicationSettings.ProductProvisioningApplicationSettings.ConnectionString);
    }
    else if (SignumIdGlobal.ApplicationSettings.ProductProvisioningApplicationSettings.DatabaseProvider.Equals(
                 "sqlserver",
                 StringComparison.InvariantCultureIgnoreCase))
    {
        services.AddHealthChecks()
            .AddSqlServer(SignumIdGlobal.ApplicationSettings.ProductProvisioningApplicationSettings.ConnectionString);
    }
    else
    {
        services.AddHealthChecks();
    }
}

static void ConfigureHangfire(IServiceCollection services)
{
    services.AddHangfire(configuration =>
    {
        configuration
            .SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
            .UseSimpleAssemblyNameTypeSerializer()
            .UseRecommendedSerializerSettings();
    });

    if (SignumIdGlobal.ApplicationSettings.ProductProvisioningApplicationSettings.DatabaseProvider.Equals("SqlLite",
            StringComparison.InvariantCultureIgnoreCase))
    {
        GlobalConfiguration.Configuration.UseSQLiteStorage(SignumIdGlobal.ApplicationSettings
            .ProductProvisioningApplicationSettings
            .ConnectionString, new SQLiteStorageOptions
        {
            SchemaName = "product_provisioning"
        });
    }
    else if (SignumIdGlobal.ApplicationSettings.ProductProvisioningApplicationSettings.DatabaseProvider.Equals(
                 "postgresql",
                 StringComparison.InvariantCultureIgnoreCase))
    {
        GlobalConfiguration.Configuration.UsePostgreSqlStorage(
            SignumIdGlobal.ApplicationSettings.ProductProvisioningApplicationSettings.ConnectionString,
            new PostgreSqlStorageOptions
            {
                SchemaName = "product_provisioning"
            });
    }
    else if (SignumIdGlobal.ApplicationSettings.ProductProvisioningApplicationSettings.DatabaseProvider.Equals(
                 "sqlserver",
                 StringComparison.InvariantCultureIgnoreCase))
    {
        GlobalConfiguration.Configuration.UseSqlServerStorage(
            SignumIdGlobal.ApplicationSettings.ProductProvisioningApplicationSettings.ConnectionString,
            new SqlServerStorageOptions
            {
                SchemaName = "product_provisioning"
            });
    }

    services.AddHangfireServer();
}

And i get an error in the first row of the method when i declare the transaction scope. The error is the following:

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. Object name: 'ProductProvisioningContext'.

screenshot of the error

CodePudding user response:

Your ReceiveWebhook is async void, change it to return Task:

public async Task ReceiveWebhook(HttpContext httpContext)

Otherwise ASP.NET Core can't wait till the end of the processing and will finish the request (including disposing created scope and disposable dependencies like database context) before the handler finishes.

Read more:

  1. async/await - when to return a Task vs void?
  2. Why exactly is void async bad?
  3. async Task vs async void
  4. Async/Await - Best Practices in Asynchronous Programming
  • Related