Closed
Description
I have a project where the database names are dynamic. The tables in each database are the same. We use a custom interceptor where the ID of the database is replaced with a placeholder. This code worked until we switched to the .NET Core version of NHibernate.
Exception
could not execute batch command.[SQL: SQL not available]
at NHibernate.AdoNet.GenericBatchingBatcher.DoExecuteBatchAsync(DbCommand ps, CancellationToken cancellationToken)
at NHibernate.AdoNet.AbstractBatcher.ExecuteBatchWithTimingAsync(DbCommand ps, CancellationToken cancellationToken)
at NHibernate.AdoNet.AbstractBatcher.ExecuteBatchAsync(CancellationToken cancellationToken)
at NHibernate.Engine.ActionQueue.ExecuteActionsAsync(IList list, CancellationToken cancellationToken)
at NHibernate.Engine.ActionQueue.ExecuteActionsAsync(CancellationToken cancellationToken)
at NHibernate.Engine.ActionQueue.ExecuteActionsAsync(CancellationToken cancellationToken)
at NHibernate.Event.Default.AbstractFlushingEventListener.PerformExecutionsAsync(IEventSource session, CancellationToken cancellationToken)
at NHibernate.Event.Default.DefaultFlushEventListener.OnFlushAsync(FlushEvent event, CancellationToken cancellationToken)
at NHibernate.Impl.SessionImpl.FlushAsync(CancellationToken cancellationToken)
at NHibernate.Impl.SessionImpl.BeforeTransactionCompletionAsync(ITransaction tx, CancellationToken cancellationToken)
at NHibernate.Transaction.AdoTransaction.CommitAsync(CancellationToken cancellationToken)
at UserQuery.Main(), line 15
at System.Threading.Tasks.Task.<>c.<ThrowAsync>b__139_1(Object state)
at System.Threading.QueueUserWorkItemCallback.<>c.<.cctor>b__6_0(QueueUserWorkItemCallback quwi)
at System.Threading.ExecutionContext.RunForThreadPoolUnsafe[TState](ExecutionContext executionContext, Action`1 callback, TState& state)
at System.Threading.QueueUserWorkItemCallback.Execute()
at System.Threading.ThreadPoolWorkQueue.Dispatch()
at System.Threading._ThreadPoolWaitCallback.PerformWaitCallback()
Inner Exception
Invalid object name 'DbPrefix_{database}.dbo.MyClass'.
at System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
at System.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
at System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock, Boolean asyncClose)
at System.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean& dataReady)
at System.Data.SqlClient.SqlCommand.FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, String resetOptionsString)
at System.Data.SqlClient.SqlCommand.CompleteAsyncExecuteReader()
at System.Data.SqlClient.SqlCommand.EndExecuteNonQueryInternal(IAsyncResult asyncResult)
at System.Data.SqlClient.SqlCommand.EndExecuteNonQuery(IAsyncResult asyncResult)
at System.Threading.Tasks.TaskFactory`1.FromAsyncCoreLogic(IAsyncResult iar, Func`2 endFunction, Action`1 endAction, Task`1 promise, Boolean requiresSynchronization)
--- End of stack trace from previous location where exception was thrown ---
at NHibernate.AdoNet.GenericBatchingBatcher.BatchingCommandSet.ExecuteNonQueryAsync(CancellationToken cancellationToken)
at NHibernate.AdoNet.GenericBatchingBatcher.DoExecuteBatchAsync(DbCommand ps, CancellationToken cancellationToken)
I can see that OnPrepareStatement
is called, and the replacements are properly made. However, when flushing or committing, the unprepared statement is issued. I have created a minimal LINQPad reproduction of the error (see below). When it is run in LINQPad 6 (which uses .NET Core), you will see the error. When you run it in LINQPad 5 or below (which uses .NET Framework), it will work as expected.
private ISessionFactory sessionFactory;
async void Main()
{
BuildSessionFactory();
using (var session = OpenSession())
using (var tx = session.BeginTransaction())
using (session.UseDatabase("F1DA0F05-7F86-4F00-8AFB-AA630140F93A"))
{
var myClass = await session.GetAsync<MyClass>(new Guid("0dc49796-2b1e-4499-9a35-aae000cf4191"));
myClass.SomeDate = DateTime.Now;
await tx.CommitAsync();
}
}
public ISession OpenSession()
{
return sessionFactory
.WithOptions()
.Interceptor(new UseDatabaseInterceptor())
.OpenSession();
}
public void BuildSessionFactory()
{
sessionFactory = Fluently.Configure()
.Database(() => MsSqlConfiguration.MsSql2012
.ConnectionString("<insert_connection_string>")
.DefaultSchema("dbo")
.Dialect<MsSql2012Dialect>()
.ShowSql())
.Mappings(m => m.FluentMappings.Add<MyClassMap>())
.BuildSessionFactory();
}
public class MyClass : Entity
{
public virtual DateTime SomeDate { get; set; }
}
public class MyClassMap : ClassMap<MyClass>
{
public MyClassMap() : base()
{
this.Schema("[DbPrefix_{database}].dbo");
this.Table("MyClass");
this.Id(x => x.Id).GeneratedBy.GuidComb();
this.Map(x => x.SomeDate);
}
}
public class Entity : IEquatable<Entity>
{
// Properties
public virtual Guid Id { get; protected set; }
// Methods
private Type GetUnproxiedType() => this.GetType();
public override bool Equals(object obj) => this.Equals(obj as Entity);
public override int GetHashCode() => IsTransient(this) ? base.GetHashCode() : this.Id.GetHashCode();
public override string ToString()=> $"{this.GetUnproxiedType()}: {this.Id}";
public static bool IsTransient(Entity entity) => Equals(entity.Id, default(Guid));
public virtual bool Equals(Entity other)
{
if (other == null) return false;
if (ReferenceEquals(this, other)) return true;
if (!IsTransient(this) && !IsTransient(other) && Equals(this.Id, other.Id))
{
var otherType = other.GetUnproxiedType();
var thisType = this.GetUnproxiedType();
return thisType.IsAssignableFrom(otherType) || otherType.IsAssignableFrom(thisType);
}
return false;
}
}
public class UseDatabaseInterceptor : EmptyInterceptor
{
// Fields
private ISession _session;
private readonly Stack<string> _databases = new Stack<string>();
// Methods
public IDisposable UseDatabase(string database)
{
_databases.Push(database);
return new Disposable(this);
}
public override SqlString OnPrepareStatement(SqlString sql)
{
var database = _databases.Any() ? _databases.Peek() : null;
if (database == null) return sql;
return sql.Replace("{database}", database).Dump();
}
public override void SetSession(ISession session)
{
_session = session;
}
// Inner Classes
public class Disposable : IDisposable
{
// Fields
private bool _disposed;
private readonly UseDatabaseInterceptor _interceptor;
// Constructors
public Disposable(UseDatabaseInterceptor interceptor)
{
_interceptor = interceptor;
}
// Methods
public void Dispose()
{
if (_disposed) return;
if (_interceptor._session.Transaction.IsActive)
{
try
{
_interceptor._session.Flush();
}
catch (Exception ex)
{
ex.Dump();
}
}
if (_interceptor._databases.Count > 0)
{
_interceptor._databases.Pop();
}
_disposed = true;
}
}
}
public static class SessionExtensions
{
public static IDisposable UseDatabase(this ISession session, string database)
{
if (session.GetSessionImplementation().Interceptor is UseDatabaseInterceptor interceptor)
{
return interceptor.UseDatabase(database);
}
return null;
}
}