Imagine MyObject.DoSomething
has the following code and study the comments:
public void DoSomething()
{
// Session.Demand() == outerSession
var someEntity1 = new SomeEntity(); // someEntity.Session == outerSession
using (var session = Session.Open(domain, true)) {
// Session.Demand() == session
// session.Transaction == null
using (var tx = Transaction.Open()) {
// session.Transaction != null here
var someEntity2 = new SomeEntity(); // someEntity.Session == session
tx.Complete()
}
// session.Transaction == null
}
// Session.Demand() == outerSession
}
Hopefully, this explains everything. Note that "current" term is local to each thread.
One more important aspect related to session activation is "Session switching":
using (var sessionA = Session.Open(domain)) { // Open & activate
var personA = Query.All<Person>().First();
using (var sessionB = Session.Open(domain)) { // Open & activate
using(var tx = Transaction.Open()) {
AssertEx.Throws<InvalidOperationException>(() => {
var personAName = personA.Name; // Will fail because of Session switching attempt
});
}
}
}
The following happens here:
personA
belongs to SessionA
, i.e. personA.Session==sessionA
- transaction is running inside previously
- but
SessionB
is active inside innermost using
block
- when you read
personA.Name
, our [Transactional]
aspect, that is automatically applied to all
public members of all ISessionBound
implementors, tries to activate its own session (i.e.
sessionA
.
- but is does this using `Session.Activate(bool checkSwitching) method, that throws an exception,
if another session is already active, and transaction is running there.
That's so-called session switching check".
Note that if there is no transaction in sessionB
, no exception will be thrown:
using (var sessionA = Session.Open(domain)) { // Open & activate
var personA = Query.All<Person>().First();
using (var sessionB = Session.Open(domain)) { // Open & activate
var personAName = personA.Name; // Ok, since there is no running transaction in sessionB
}
}
So by default, automatic activation of one session inside another one with open transaction (i.e. when another session is active, and transaction is already running there) leads to exception. This is intentional: normally this indicates the same thread controls two sessions and transactions simultaneously, and thus there is a chance of getting application-level deadlock.
Application-level deadlock happens e.g. when the same thread sequentially:
- Accesses some resource in sessionA (so SQL Server acquires shared lock on it in that session)
- Tries to modify the resource in sessionB, so SQL Server tries to acquire X-lock on it for another
session, but to do this, it must wait for release of the first lock.
Thus it starts to wait for this, but this will never happen, because
the thread controlling both sessions is also waiting for completion
of current operation.
Note that such deadlocks can't be automatically detected by SQL Server, since it doesn't knows
two connections are actually controlled by the same thread. Thus finally it will cancel running
operation by timeout - i.e. two transactions will be running for ~ 30 seconds by default, and this
will negatively affect on other operations.
Alternatively, session switching might indicate unintentional usage of data fetched by
one session inside another - that's bad again, since normally all the data you access in
a single transaction must be acquired there (to ensure it's protected unexpected modifications
by other transactions - i.e. isolated).
But what if session switching is intentional?
There are actually two cases, when this can be intentional:
- When you deal with UI sesions with attached DisconnectedStates.
It's normally ok here, since you don't hit the DB.
- The case you 100% know this is intentional :)
And, correspondingly, there are two ways of blocking session switching check:
- Using
SessionOptions.AllowSwitching
option. If both Session
s participating in check
have this flag, session switching check doesn't happen.
- Using
Session.Deactivate()
static method. Any session can be activated inside
using
block with this method.
Example of way 1:
var cfg = new SessionConfiguration();
cfg.Options |= SessionOptions.AllowSwitching;
using (var sessionA = Session.Open(domain, cfg)) { // Open & activate
var personA = Query.All<Person>().First();
using (var sessionB = Session.Open(domain, cfg)) { // Open & activate
using(var tx = Transaction.Open()) {
var personAName = personA.Name; // Will not fail
}
}
}
Example of way 2:
using (var sessionA = Session.Open(domain, cfg)) { // Open & activate
var personA = Query.All<Person>().First();
using (var sessionB = Session.Open(domain, cfg)) { // Open & activate
using(var tx = Transaction.Open()) {
using (Session.Deactivate()) {
var personAName = personA.Name; // Will not fail
}
}
}
}