Reputation: 701
I recently upgraded our ServiceStack package to v4.0.46 (from v4.0.36) and there are areas of our app which uses ResolveService to call another service within a DB transaction. Previously this all worked fine, but after upgrading to v4.0.46 we are getting this error:
Connection must be valid and open
The caller looks something like this:
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>();
}
}
The DeleteDocumentsService looks a bit like this
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 is just a wrapper for the DB transaction which looks something like this:
public void WriteDb(Action<IWriteCustomerDbConnection> action)
{
using (var connRef = ConnectionManager.Open()) {
using (var transRef = ConnectionManager.BeginTrans()) {
action(new CustomerDbConnection(Session, connRef.Conn));
transRef.Commit();
}
}
}
I read through the release notes for ServiceStack and couldn't find anything that would suggest there was a change in how ResolveService works. So could anyone shed any light on what could have changed?
I realise this bit of code is not the best, but it would be good to understand why it's only giving us the error now after upgrading to v4.0.46.
Upvotes: 1
Views: 280
Reputation: 13199
This is not really answer but I needed to show this simple working sample to demonstrate nested connection and nested service work as expected.
This however behaves differently with older versions of service stack. Where the DbConnection hangs around and have to be disposed manually. Now it's "automatic".
Like @mythz mentioned, latest version of service stack clears injected dependencies. And I guess ormlite now integrates with service stack much better, hence the underlying DbConnection gets disposed (becomes null).
The connection manager must hangs on to the DbConnection internally, that's why after the nested service gets disposed, so is the DbConnection instance inside connection manager.
Having spent another 5 mins thinking about this connection manager, it seems that this ConnMan is trying to conduct some kind of "distributed transaction", it tries to get hold onto 1 instance of dbconn and try leak it into multiple services and hence provide a single transaction. This as @mythz mentioned is dodgy! It's anti pattern! Don't do it!
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; }
}
}
Upvotes: 2
Reputation: 143319
Behavior of ResolveService()
hasn't changed but from the error message it looks like the DB connection was disposed. Maybe this is due to the eager disposing of transient IOC resources after a Service Has been Disposed.
But this is a really strange usage pattern and I don't know why you're not using a cleaner execution path or what the purpose of ConnectionManager
is and why its not using the recommended API to use OpenTransaction() to create the transaction off the db connection.
But if it's due to eager disposing of service dependencies you can try move Delete<T>
within the using scope, e.g:
WriteDb(conn => {
using (var service = ResolveService<DeleteDocumentsService>()) {
service.Post(new DeleteDocumentsRequest {
Ids = ids.ToArray()
});
conn.Delete<Item>(request.Id);
}
});
Although if you're using a transaction I'd personally rewrite it to pass in and use the same db connection with an explicit API that accepts the connection instance:
WriteDb(conn => {
using (var service = ResolveService<DeleteDocumentsService>()) {
service.DeleteDocuments(conn, ids);
conn.Delete<Item>(request.Id);
}
});
Upvotes: 5