Write to DB after Controller has been disposed

瘦欲@ 提交于 2021-01-29 02:58:52

问题


Situation

We have a controller where users can submit any number of E-Mail addresses to invite other (potential) members as friends. If an address is not found in the database, we send an E-Mail message to that user. Since the user does not has to wait for this process to complete in order to continue working this is done asynchronously.

Sending E-Mails can take a long time if servers respond slowly, are down or overloaded. The E-Mail sender should update the database according to the status received from the E-Mail server, so for example setting the friend request into "Error" state, when a permanent failure occurs, for example if the address does not exists. For this purpose, the E-Mail component implements the function SendImmediateAsync(From,To,Subject,Content,Callback,UserArg). After the message has been delivered (or it failed), the Callback is called with certain arguments about the Delivery state.

When it eventually calls the delegate, the DbContext object has already been disposed (since the controller has been too) and I cannot manually create a new one using new ApplicationDbContext() because there is no constructor that accepts a connection string.

Question

How can I write to the database long after the controller has been disposed? I have not yet figured out how to manually create a DbContext object for myself. An object of type ApplicationDbContext is passed to the constructor of the Controller and I hoped I could instantiate one for myself, but the Constructor has no arguments I can supply (for example connection string). I want to avoid to manually create an SQL Connection and assemble INSERT statements manually and would prefer to work with the entity model we have already set up.

Code

The code shows the affected segment only without any error checking for readability.

[Authorize]
public class MembersController : Controller
{
    private ApplicationDbContext _context;

    public MembersController(ApplicationDbContext context)
    {
        _context = context;
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public IActionResult Friends()
    {
        MailHandler.SendImmediateAsync(FROM,TO,SUBJECT,CONTENT,
            delegate (Guid G, object any)
            {
                //THIS IS NOT WORKING BECAUSE _context IS DISPOSED
                var ctx = _context;

                Guid Result = (Guid)any; //user supplied argument

                if (G != Guid.Empty)
                {
                    ctx.MailConfirmation.Add(new MailConfirmation()
                    {
                        EntryId = Result,
                        For = EntryFor.FriendRequest,
                        Id = G
                    });

                    if (G == MailHandler.ErrorGuid)
                    {
                        var frq = _context.FriendRequest.SingleOrDefault(m => m.Id == Result);
                        frq.Status = FriendStatus.Error;
                        ctx.Update(frq);
                    }
                    ctx.SaveChanges();
                }
            }, req.Id);
        //rendering view
    }
}

回答1:


First of all, when you are using EF Core with ASP.NET Core's dependency injection, each DbContext instance is scoped per-request, unless you have specified otherwise in ".AddDbContext". This means you should not attempt to re-use an instance of DbContext after that HTTP request has completed. See https://docs.asp.net/en/latest/fundamentals/dependency-injection.html#service-lifetimes-and-registration-options

DbContextOptions, on the other hand, are singletons and can be re-used across requests.

If you need to close the HTTP request and perform an action afterwards, you'll need to create a new DbContext scope an manage it's lifetime.

Second of all, you can overload DbContext's base constructor and pass in DbContextOptions directly. See https://docs.efproject.net/en/latest/miscellaneous/configuring-dbcontext.html

Together, this is what a solution might look like.

public class MembersController : Controller
{
    private DbContextOptions<ApplicationDbContext> _options;

    public MembersController(DbContextOptions<ApplicationDbContext> options)
    {
        _options = options;
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public IActionResult Friends()
    {
        MailHandler.SendImmediateAsync(FROM,TO,SUBJECT,CONTENT, CreateDelegate(_options) req.Id);
    }

    private static Action<Guid, object> CreateDelegate(DbContextOptions<ApplicationDbContext> options)
    {
        return (G, any) => 
        {
            using (var context = new ApplicationDbContext(options))
            {
                //do work
                context.SaveChanges();
            }
        };
    }
}

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

    // the rest of your stuff
}

This assumes, of course, that your "MailHandler" class is properly using concurrency to run the delegate so it doesn't block the thread processing the HTTP request.




回答2:


Why not just pass the dbContext as the userArgs to your SendImmediateAsync? Then the dbContext will not get disposed and can be passed back when you do the callback. I'm pretty sure that should work.



来源:https://stackoverflow.com/questions/37678890/write-to-db-after-controller-has-been-disposed

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!