在现代Web应用程序中实现Entity Framework数据库事务的位置?

时间:2016-12-22 22:20:05

标签: angularjs entity-framework asp.net-web-api

假设您的应用程序中的主要组件是Angular客户端,它调用ASP.NET Web API,它使用Entity Framework对您的数据库执行CRUD操作。因此,例如,在您的API控制器中,Post(Add)方法将新实体添加到数据库上下文,然后通过调用Entity Framework SaveChanges方法将其提交到数据库。

当一次只需要将一条记录添加到数据库时,这样可以正常工作。

但是,例如,如果要在一个事务中向数据库添加多个不同实体类型的记录,该怎么办?你在哪里实现Database.BeginTransaction和Database.CommitTransaction / RollbackTransaction?如果您添加服务层来完成此任务,那么Angular客户端会调用什么?

请参阅下面的更多细节和问题。

我想提供有关解决此问题的当前方法的更多详细信息,并提出以下问题:

(1)这是一个好方法,还是有更好的方法?

(2)我的方法没有移植到.NET Core,因为.NET Core还不支持OData(参见https://github.com/OData/WebApi/issues/229)。有关于此的任何想法或想法吗?

我已经说过我遇到的问题以及我在下面选择的解决方案。我将使用一个简单的场景,其中客户正在为多个项目下订单 - 因此,有一个Order记录包含多个OrderDetail记录。必须在单个事务中将Order记录和关联的OrderDetail记录提交到数据库。

问题#1:从Angular客户端向ASP.NET Web API发送Order和OrderDetail记录的最佳方法是什么?

解决方案#1:我决定使用OData批处理,这样我就可以在一个POST中发送所有记录。我正在使用datajs库来执行批处理(https://www.nuget.org/packages/datajs)。

问题#2:如何围绕Order和OrderDetail记录包装单个事务?

解决方案#2:我在Web API中设置了一个OData批处理终结点,其中包含以下内容:

(1)在客户端中,配置批量请求路由。

// Configure the batch request route.
config.Routes.MapODataServiceRoute(
    routeName: "batch",
    routePrefix: "batch",
    model: builder.GetEdmModel(),
    pathHandler: new DefaultODataPathHandler(),
    routingConventions: conventions,
    batchHandler: new TransactionalBatchHandler(GlobalConfiguration.DefaultServer));
}

(2)在Web API中,实现一个自定义批处理程序,它围绕给定的OData批处理包装数据库事务。批处理程序启动事务,调用相应的ODataController来执行CRUD操作,然后根据结果提交/回滚事务。

/// <summary>
/// Custom batch handler specialized to execute batch changeset in OData $batch requests with transactions.
/// The requests will be executed in the order they arrive, that means that the client is responsible for
/// correctly ordering the operations to satisfy referential constraints.
/// </summary>
public class TransactionalBatchHandler : DefaultODataBatchHandler
{
    public TransactionalBatchHandler(HttpServer httpServer)
        : base(httpServer)
    {
    }

    /// <summary>
    /// Executes the batch request and wraps the execution of the whole changeset within a transaction.
    /// </summary>
    /// <param name="requests">The <see cref="ODataBatchRequestItem"/> instances of this batch request.</param>
    /// <param name="cancellation">The <see cref="CancellationToken"/> associated with the request.</param>
    /// <returns>The list of responses associated with the batch request.</returns>
    public async override Task<IList<ODataBatchResponseItem>> ExecuteRequestMessagesAsync(
        IEnumerable<ODataBatchRequestItem> requests,
        CancellationToken cancellation)
    {
        if (requests == null)
        {
            throw new ArgumentNullException("requests");
        }

        IList<ODataBatchResponseItem> responses = new List<ODataBatchResponseItem>();
        try
        {
            foreach (ODataBatchRequestItem request in requests)
            {
                OperationRequestItem operation = request as OperationRequestItem;
                if (operation != null)
                {
                    responses.Add(await request.SendRequestAsync(Invoker, cancellation));
                }
                else
                {
                    await ExecuteChangeSet((ChangeSetRequestItem)request, responses, cancellation);
                }
            }
        }
        catch
        {
            foreach (ODataBatchResponseItem response in responses)
            {
                if (response != null)
                {
                    response.Dispose();
                }
            }
            throw;
        }

        return responses;
    }

    private async Task ExecuteChangeSet(
        ChangeSetRequestItem changeSet,
        IList<ODataBatchResponseItem> responses,
        CancellationToken cancellation)
    {
        ChangeSetResponseItem changeSetResponse;

        // Since IUnitOfWorkAsync is a singleton (Unity PerRequestLifetimeManager) used by all our ODataControllers,
        // we simply need to get a reference to it and use it for managing transactions. The ODataControllers
        // will perform IUnitOfWorkAsync.SaveChanges(), but the changes won't get committed to the DB until the
        // IUnitOfWorkAsync.Commit() is performed (in the code directly below).

        var unitOfWorkAsync = GlobalConfiguration.Configuration.DependencyResolver.GetService(typeof(IUnitOfWorkAsync)) as IUnitOfWorkAsync;

        unitOfWorkAsync.BeginTransaction();

        // This sends each request in the changeSet to the appropriate ODataController.
        changeSetResponse = (ChangeSetResponseItem)await changeSet.SendRequestAsync(Invoker, cancellation);

        responses.Add(changeSetResponse);

        if (changeSetResponse.Responses.All(r => r.IsSuccessStatusCode))
        {
            unitOfWorkAsync.Commit();
        }
        else
        {
            unitOfWorkAsync.Rollback();
        }
    }

}

2 个答案:

答案 0 :(得分:1)

You do not need to implement Database.BeginTransaction and Database.CommitTransaction/RollbackTransaction if you are using Entity Framework. Entity Framework implements UnitOfWork. The only thing that you should care about is to work with a different instance of DbContext for every web request, but exaclty 1 instance for 1 request and call SaveChanges only 1 time when you made all the changes you need.

In case of any Exception during SaveChanges all the changes will be rolled back.

The angular client should not care about this, it only sends the data and checks if everything was fine.

This is very easy to do if you use an IoC framework, like Unity and let your DbContext injected in your Controller or Service.

In this case you should use the following settings (if you use Unity):

container.RegisterType<DbContext, YourDbContext>(new PerRequestLifetimeManager(), ...);

Then you can do this if you want to use it in a Controller:

public class YourController : Controller
{
    private YourDbContext _db;

    public YourController(DbContext context)
    {
        _db = context;
    }
...

答案 1 :(得分:0)

无需过度复杂化。将代码添加到WebApi项目。传递您的Transaction对象并重新使用它。有关示例,请参阅https://msdn.microsoft.com/en-us/library/dn456843(v=vs.113).aspx