ServiceStack 在数据库事务中调用 ResolveService

ServiceStack calling ResolveService within a DB transaction

我最近将我们的 ServiceStack 包升级到 v4.0.46(从 v4.0.36),我们应用程序的某些区域使用 ResolveService 在数据库事务中调用另一个服务。以前一切正常,但升级到 v4.0.46 后我们收到此错误:

Connection must be valid and open

来电者看起来像这样:

public class DeleteItemService: CustomerServiceBase
{
    public object Post(DeleteItem request)
    {
        WriteDb(conn => {
            using (var service = ResolveService<DeleteDocumentsService>()) {
                service.Post(new DeleteDocumentsRequest {
                    Ids = ids.ToArray()
                });
            }
            conn.Delete<Item>(request.Id);
        });

        return RespondSuccess<ResponseBase>();
    }
}

DeleteDocumentsService 看起来有点像这样

public class DeleteDocumentsService: CustomerServiceBase
{
    public ILog Log { get; set; }

    public PrivateStorage PMStorage { get; set; }

    public ResponseBase Post(DeleteDocumentsRequest request)
    {
        WriteDb(conn => {
            var link = conn.Select<DocumentLink>(l => l.DocumentStorageId == item.Id).FirstOrDefault();
            conn.Delete<DocumentStorage>(item.Id);
        });

        return RespondSuccess<ResponseBase>();
    }

WriteDb 只是 DB 事务的包装器,看起来像这样:

public void WriteDb(Action<IWriteCustomerDbConnection> action)
{
    using (var connRef = ConnectionManager.Open()) {
        using (var transRef = ConnectionManager.BeginTrans()) {
            action(new CustomerDbConnection(Session, connRef.Conn));
            transRef.Commit();
        }
    }
}

我通读了 ServiceStack 的发行说明,但找不到任何表明 ResolveService 工作方式发生变化的内容。那么任何人都可以阐明可能发生的变化吗?

我知道这段代码不是最好的,但最好理解为什么它在升级到 v4.0.46 后现在才给我们错误。

ResolveService() 的行为没有改变,但从错误消息来看,数据库连接似乎已被释放。这可能是由于服务已处理后 eager disposing of transient IOC resources

但这是一个非常奇怪的使用模式,我不知道你为什么不使用更清晰的执行路径或者 ConnectionManager 的目的是什么以及为什么它不使用推荐的 API 到 use OpenTransaction() to create the transaction 关闭数据库连接。

但如果是由于急于处理服务依赖关系,您可以尝试在 using 范围内移动 Delete<T>,例如:

WriteDb(conn => {
    using (var service = ResolveService<DeleteDocumentsService>()) {
        service.Post(new DeleteDocumentsRequest {
            Ids = ids.ToArray()
        });
        conn.Delete<Item>(request.Id);
    }
});

尽管如果您使用的是事务,我会亲自重写它以传入并使用相同的数据库连接和接受连接实例的显式 API:

WriteDb(conn => {
    using (var service = ResolveService<DeleteDocumentsService>()) {
        service.DeleteDocuments(conn, ids);
        conn.Delete<Item>(request.Id);
    }
});

这不是真正的答案,但我需要展示这个简单的工作示例来演示嵌套连接和嵌套服务按预期工作。

然而,这与旧版本的服务堆栈的行为不同。 DbConnection 挂起并且必须手动处理的地方。现在是 "automatic".

如@mythz 所述,最新版本的服务堆栈清除了注入的依赖项。而且我猜 ormlite 现在与服务堆栈集成得更好,因此底层 DbConnection 被处理(变为空)。

连接管理器必须在内部挂起 DbConnection,这就是为什么在嵌套服务被释放后,连接管理器中的 DbConnection 实例也是如此。

又花了 5 分钟思考这个连接管理器,似乎这个 ConnMan 正试图进行某种 "distributed transaction",它试图抓住1 个 dbconn 实例并尝试将其泄漏到多个服务中,从而提供单个事务。正如@mythz 提到的那样,这是狡猾的!这是反模式!不要这样做!

using System;
using System.Data;
using ServiceStack;
using ServiceStack.Data;
using ServiceStack.OrmLite;

namespace NestedTransactionTest
{
    [Route("/test/ResolveViaIoC", Verbs = "GET")]
    public class Dto1 : IReturnVoid
    {

    }

    public class ResolveViaIoC : Service
    {
        readonly IDbConnectionFactory _factory;

        public ResolveViaIoC(IDbConnectionFactory factory)
        {
            _factory = factory;
        }

        public void Get(Dto1 request)
        {
            using (var conn = _factory.Open()) {
                using (var tran = conn.BeginTransaction()) {
                    var kv = new KeyValue {
                        Id = Guid.NewGuid().ToString(),
                        TypeName = "ResolveViaIoC",
                        Value = "empty",
                        ExpireAfter = DateTime.Now
                    };

                    using (var nested = ResolveService<ResolveViaIoCNested>()) {
                        nested.Get(new Dto1Nested());   
                    }

                    conn.Insert(kv);
                    tran.Commit();
                }
            }
        }
    }


    [Route("/test/ResolveViaIoC/nested", Verbs = "GET")]
    public class Dto1Nested : IReturnVoid
    {

    }

    public class ResolveViaIoCNested : Service
    {
        readonly IDbConnectionFactory _factory;

        public ResolveViaIoCNested(IDbConnectionFactory factory)
        {
            _factory = factory;
        }

        public void Get(Dto1Nested request)
        {
            using (var conn = _factory.Open()) {
                using (var tran = conn.BeginTransaction()) {
                    var kv = new KeyValue {
                        Id = Guid.NewGuid().ToString(),
                        TypeName = "ResolveViaIoCNested",
                        Value = "empty",
                        ExpireAfter = DateTime.Now
                    };

                    conn.Insert(kv);
                    tran.Commit();
                }
            }
        }
    }

    public class KeyValue
    {
        public string Id { get; set; }
        public string TypeName { get; set; }
        public string Value { get; set; }
        public DateTime ExpireAfter { get; set; }
    }
}