Skip to content

IInterceptor.OnPrepareStatement results not used in insert/update commands #2278

Closed
@shiznit013

Description

@shiznit013

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;
	}
}

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions