Search code examples
c#entity-framework.net-coreasp.net-core-webapiasp.net-core-middleware

Cannot access a disposed object in Entity Framework


I am creating a middleware to store all incoming API request and response body along with some other data, I am using Entity Framework as my ORM.

It's working well, but in some request and response the response body is not getting stored.

While I log the error it shows:

Object name: 'TransactionDBContext'.line no 248
Cannot access a disposed object. A common cause of this error is disposing a context 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, or wrapping the context in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances.

I just every method using async returns task and await keyword using properly

Middleware.cs:

public class TransactionLoggingMiddleWare
{
    private readonly RequestDelegate _next;
    private TransactionLogService _transactionLogService;

    public TransactionLoggingMiddleWare(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context, TransactionLogService transactionLogService)
    {
        _transactionLogService = transactionLogService;
        Guid GUID = Guid.NewGuid();

        try
        {
            var request = context.Request;

            if (request.Path.ToString().Contains("Remittance/Post"))
            {
                var requestTime = DateTime.Now;
                var requestBodyContent = await ReadRequestBody(request);
                int bcode = requestBodyContent.IndexOf("content-disposition:");

                if (bcode > 0)
                {
                    string jung1 = requestBodyContent.Substring(0, bcode - 1);
                    requestBodyContent = requestBodyContent.Replace(jung1, "");
                }

                context.Items["Guid"] = GUID;

                requestBodyContent = requestBodyContent.Replace("content-disposition: form-data; name=", "");

                var form = await context.Request.ReadFormAsync();
                var transactionData = new RemittanceRQ();

                var properties = typeof(RemittanceRQ).GetProperties();
                int remIdValue = 0;
                long TxnRefNo = 0;
                string productcode = null;

                foreach (var property in properties)
                {
                    // Get the property name
                    var propertyName = property.Name;

                    // Check if the form data contains the key corresponding to the property name
                    if (form.ContainsKey(propertyName))
                    {
                        // Get the value from the form data
                        var value = form[propertyName];

                        // Convert and assign the value to the property
                        if (value != String.Empty)
                        {
                            // Handle different property types accordingly
                            if (property.PropertyType == typeof(int))
                            {
                                property.SetValue(transactionData, Convert.ToInt32(value));
                            }
                            else if (property.PropertyType == typeof(Int16))
                            {
                                property.SetValue(transactionData , Convert.ToInt16(value));
                            }
                            else if (property.PropertyType == typeof(long))
                            {
                                property.SetValue(transactionData, long.Parse(value));
                            }
                            else if (property.PropertyType == typeof(decimal))
                            {
                                property.SetValue(transactionData, decimal.Parse(value));
                            }
                            else if (property.PropertyType == typeof(short))
                            {
                                property.SetValue(transactionData, Convert.ToInt16(value));
                            }
                            else if (property.PropertyType == typeof(String))
                            {
                                property.SetValue(transactionData, value.ToString());
                            }

                            if (propertyName == "RemID")
                            {
                                remIdValue = Convert.ToInt32(value);
                            }

                            if (propertyName == "TxnRefNumber")
                            {
                                TxnRefNo = long.Parse(value);
                            }
                        }
                    }
                }

                // to store only request body
                await _transactionLogService.AddTransaction(new TransactionLogItem
                {
                    TransactionGuid = GUID.ToString(),
                    CreatedDate = requestTime,
                    APIRequest = JsonConvert.SerializeObject(transactionData),
                    RemId = remIdValue,
                    TxnNumber = TxnRefNo,
                    


                });

                // reading the response



                var orginalBodyStream = context.Response.Body;

                var originalBodyCode = context.Response.StatusCode;
           
                
                try
                {


                    using (var responseBody = new MemoryStream())
                    {
                        //context.Response.Body = responseBody;
                        var response = context.Response;
                        response.Body = responseBody;

                        await _next(context);

                        responseBody.Seek(0, SeekOrigin.Begin);
                        var responseBodyContent = await new StreamReader(responseBody).ReadToEndAsync();

                        var responseData = JsonConvert.DeserializeObject<ResponseData>(responseBodyContent);
                        
                        if (responseData != null)
                        {
                            var txnReferenceNo = responseData.Data?.TxnReferenceNo;
                                                        }
                            await _transactionLogService.UpdateTransactionLog(new TransactionLogItem
                            {

                                TransactionGuid = GUID.ToString(),
                                
                                APIResponse = responseBodyContent,
                                Status_Code = response.StatusCode,
                                TxnNumber = responseData.Data?.TxnReferenceNo,
                                TransactionStatus = responseData.IsSuccess,
                                XPIN = responseData.Data?.TTNUM,
                            });
                        
                        responseBody.Seek(0, SeekOrigin.Begin);
                        await responseBody.CopyToAsync(orginalBodyStream);
                    }
                   
                    // end of reading response body
                    //await _next(context);


                }
                catch (Exception ex)
                {
                    await _transactionLogService.UpdateTransactionLog(new TransactionLogItem
                    {
                        TransactionGuid = GUID.ToString(),
                        ExceptionMsg = ex.Message
                    });

                    File.AppendAllText("loggerMiddlewareException.txt",ex.Message.ToString()+"line no 231"+$"{GUID}"+"\n");
                }
            }
                

            else
            {
                await _next(context);
            }


        }
        catch (Exception ex)
        {
            bool translogGuid = context.Items.TryGetValue("Guid", out var requestIdObj);
            var request = context.Request;
            var requestTime = DateTime.Now;
            File.AppendAllText("loggerMiddlewareException.txt", ex.Message.ToString() + "line no 248" + "\n");
            var requestBodyContent = await ReadRequestBody(request);
            await _transactionLogService.UpdateTransactionLog(new TransactionLogItem
            {
                TransactionGuid = GUID.ToString(),
                ExceptionMsg = ex.Message,
            });
            await _next(context);

        }
    }

    private async Task<string> ReadRequestBody(HttpRequest request)
    {
        request.EnableBuffering();

        var buffer = new byte[Convert.ToInt32(request.ContentLength??0)];
        await request.Body.ReadAsync(buffer, 0, buffer.Length);
        var bodyAsText = Encoding.UTF8.GetString(buffer);
        request.Body.Seek(0, SeekOrigin.Begin);

        return bodyAsText;
    }
}

Service register:

 services.AddDbContext<TransactionDBContext>(options => options.UseSqlServer(connectionString));

DbContext

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

}
public DbSet<TransactionLogItem> tblt_TransactionLogs { get; set; }}

Serive.cs:

public class TransactionLogService{
private readonly TransactionDBContext _dbContext;
public int TransactionId { get; set; }
public TransactionLogService(TransactionDBContext transactionDBContext)
{
    _dbContext = transactionDBContext;
}
public async Task AddTransaction(TransactionLogItem transactionLogItem)
{
    _dbContext.tblt_TransactionLogs.Add(transactionLogItem);
    await _dbContext.SaveChangesAsync();
    //TransactionId = transactionLogItem.TransactionLogID;
}
public async Task UpdateTransactionLog(TransactionLogItem LogItem)
{
    var existingLog = await _dbContext.tblt_TransactionLogs.FirstOrDefaultAsync(t => t.TransactionGuid == LogItem.TransactionGuid);
    if (existingLog != null)
    {
        existingLog.EngineCode = LogItem.EngineCode = string.IsNullOrEmpty(LogItem.EngineCode) ? existingLog.EngineCode : LogItem.EngineCode; ;
        existingLog.APIResponse = LogItem.APIResponse=string.IsNullOrEmpty(LogItem.APIResponse)? existingLog.APIResponse: LogItem.APIResponse;
        existingLog.Status_Code = LogItem.Status_Code==0?existingLog.Status_Code:LogItem.Status_Code;
        existingLog.TxnNumber= LogItem.TxnNumber == null || LogItem.TxnNumber == 0 ? existingLog.TxnNumber:LogItem.TxnNumber;
        existingLog.XPIN = LogItem.XPIN=string.IsNullOrEmpty(LogItem.XPIN)?existingLog.XPIN:LogItem.XPIN;
        existingLog.MTORequest=LogItem.MTORequest=string.IsNullOrEmpty(LogItem.MTORequest)?existingLog.MTORequest:LogItem.MTORequest;
        existingLog.MTOResponse=LogItem.MTOResponse=string.IsNullOrEmpty(LogItem.MTOResponse)?existingLog.MTOResponse:LogItem.MTOResponse;
        existingLog.ExceptionMsg = LogItem.ExceptionMsg = string.IsNullOrEmpty(LogItem.ExceptionMsg) ? existingLog.ExceptionMsg : LogItem.ExceptionMsg;
        existingLog.TransactionStatus = LogItem.TransactionStatus;



    }
    await _dbContext.SaveChangesAsync();

}
}

It's working fine in almost 80% case successfully but in some case it fails to store only response.

How to fix it ? Issue in production


Solution

  • The issue you are describing seems to be related to lifespan of a service (probably the TransactionLogService). Note that a DbContext is always scoped (a new scope is created for each request) so if you have a singleton service (has a higher lifespan and stays the same between requests) that rely on it. It might happen that in some instances it is trying to access the dbcontext when it has been disposed already. A few possible solutions:

    1. check how it was added to the Dependency Injection container in Program.cs and study what is the appropriate lifespan of your service considering that the dbcontext will be scope.

    2. the more mechanical/sure way to make sure it doesn’t happen, instead of injecting the DbContext class directly, inject IServiceScopeFactory and in every method where the context is used, use the factory to create a new scope and get a fresh copy of DbContext from it. this give you total control on the lifetime of the dbcontext instance and those errors won’t happen ( with the only downside that the dbcontext data might get misaligned in between SaveChanges calls).

    I hope you find this helpful.

    Lemme know if u need sample code for the 2 solution, I will try and add them later on.