How to migrate SimpleMembership user data to ASPNET Core Identity - authentication

I have an application that I originally built when I was young and foolish and its authentication is set up using the SimpleMembership framework, with all user data contained in a webpages_Membership table. I am very interested in rebuilding my backend as an AspNetCore Web API with AspNetCore Identity via SQL Server, without losing user information.
I've had good luck with coming up with SQL scripts to move everything into an AspNetUsers table in preparation for working with Identity instead of SimpleMembership, but where I'm running into an issue is password hashing. I gather from articles like this and this that my best bet is to override PasswordHasher<IdentityUser> to duplicate the SimpleMembership crypto flow, and then rehash passwords as they come in to gradually migrate the database.
The trouble is I can't find out how to achieve this flow duplication in .NET Core. The latter article linked above states that the SimpleMembership flow is achieved via the System.Web.Helpers.Crypto package, which does not appear to exist in the .NET Core library, and I can't figure out if its implementation is documented anywhere. (Its MSDN documentation says that it is using RFC2898 hashing but I don't know enough about crypto to know if that's enough to go on by itself. This isn't my area of expertise. :( )
Any insight on how to approach this would be much appreciated. Thank you!

For anyone else who may be running into the same trouble -- I was able to find a copy of the System.Web.Helpers.Crypto code somewhere on GitHub, and more or less copied it into a custom password hasher class thus:
public class CustomPasswordHasher : PasswordHasher<IdentityUser>
{
public override PasswordVerificationResult VerifyHashedPassword(IdentityUser user, string hashedPassword,
string providedPassword)
{
var isValidPasswordWithLegacyHash = VerifyHashedPassword(hashedPassword, providedPassword);
return isValidPasswordWithLegacyHash
? PasswordVerificationResult.SuccessRehashNeeded
: base.VerifyHashedPassword(user, hashedPassword, providedPassword);
}
private const int _pbkdf2IterCount = 1000;
private const int _pbkdf2SubkeyLength = 256 / 8;
private const int _saltSize = 128 / 8;
public static bool VerifyHashedPassword(string hashedPassword, string password)
{
//Checks password using legacy hashing from System.Web.Helpers.Crypto
var hashedPasswordBytes = Convert.FromBase64String(hashedPassword);
if (hashedPasswordBytes.Length != (1 + _saltSize + _pbkdf2SubkeyLength) || hashedPasswordBytes[0] != 0x00)
{
return false;
}
var salt = new byte[_saltSize];
Buffer.BlockCopy(hashedPasswordBytes, 1, salt, 0, _saltSize);
var storedSubkey = new byte[_pbkdf2SubkeyLength];
Buffer.BlockCopy(hashedPasswordBytes, 1 + _saltSize, storedSubkey, 0, _pbkdf2SubkeyLength);
byte[] generatedSubkey;
using (var deriveBytes = new Rfc2898DeriveBytes(password, salt, _pbkdf2IterCount))
{
generatedSubkey = deriveBytes.GetBytes(_pbkdf2SubkeyLength);
}
return ByteArraysEqual(storedSubkey, generatedSubkey);
}
internal static string BinaryToHex(byte[] data)
{
var hex = new char[data.Length * 2];
for (var iter = 0; iter < data.Length; iter++)
{
var hexChar = (byte) (data[iter] >> 4);
hex[iter * 2] = (char) (hexChar > 9 ? hexChar + 0x37 : hexChar + 0x30);
hexChar = (byte) (data[iter] & 0xF);
hex[iter * 2 + 1] = (char) (hexChar > 9 ? hexChar + 0x37 : hexChar + 0x30);
}
return new string(hex);
}
[MethodImpl(MethodImplOptions.NoOptimization)]
private static bool ByteArraysEqual(byte[] a, byte[] b)
{
if (ReferenceEquals(a, b))
{
return true;
}
if (a == null || b == null || a.Length != b.Length)
{
return false;
}
var areSame = true;
for (var i = 0; i < a.Length; i++)
{
areSame &= (a[i] == b[i]);
}
return areSame;
}
}
This class overrides VerifyHashedPassword and checks whether the user's provided password works with the old Crypto hashing; if so, the method returns PasswordVerificationResult.SuccessRehashNeeded. Otherwise, it passes the password off to the base class's method and verifies it as normal with the .NET Core hashing behavior.
You can then instruct your UserManager to use this password hasher instead of the default by including it in your dependency injection configuration in Startup.cs:
public class Startup
{
...
public void ConfigureServices(IServiceCollection services)
{
...
services.AddScoped<IPasswordHasher<IdentityUser>, CustomPasswordHasher>();
}
...
}
My eventual intention is to have my controller trigger a rehash of the user's password when that SuccessRehashNeeded result is returned, allowing a gradual migration of all users to the correct hashing schema.

Related

Identity Two-Factor Authentication alternatives

One of my clients did not want to use any of the standard options (SMS or Email) for 2FA and I was wondering what others have implemented instead.
I felt that the site would be too vulnerable with just a username and password combination, even using max-attempts and timeouts.
A simple option that multiplies up the login uncertainty is by adding an additional security question as part of the login page.
My answer is posted below
Using the code-first approach in creating the user database, I added a set of security questions into my IdentityDbContext class.
public DbSet<SecurityQuestion> SecurityQuestions { get; set; }
This provides a simple list of questions such as "What is your favourite food". the questions should engender reasonably generic answers. The questions are added in the Configuration class Seed method
void AddSecurityQuestion(ApplicationDbContext db, string question)
{
db.SecurityQuestions.Add(new SecurityQuestion() { Question = question });
}
A simple table is sufficient
public class SecurityQuestion
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[StringLength(128)]
[DisplayName("Question")]
public string Question { get; set; }
}
A field as added to the Identity User class. This will contain either null or a hash of a security question and answer. For completeness, a property is added to check if the hash is present. The first time a user logs on, the hash is saved. On subsequent logons, the hash is checked
public string SecurityQuestion { get; protected set; }
[NotMapped]
public bool HasSecurityQuestion
{
get
{
return this.SecurityQuestion != null;
}
}
Hashing uses the same code as the internal Identity methods and stores the seed and hash in the same string
public static string HashSecurityQuestion(string question, string answer)
{
if (question == null)
{
throw new ArgumentNullException("Question is null");
}
if (answer == null)
{
throw new ArgumentNullException("Answer is null");
}
string questionAndAnswer = question + "_" + answer;
// random salt and hash in save result
byte[] salt;
byte[] buffer2;
using (Rfc2898DeriveBytes bytes = new Rfc2898DeriveBytes(questionAndAnswer, 0x10, 0x3e8))
{
salt = bytes.Salt;
buffer2 = bytes.GetBytes(0x20);
}
byte[] dst = new byte[0x31];
Buffer.BlockCopy(salt, 0, dst, 1, 0x10);
Buffer.BlockCopy(buffer2, 0, dst, 0x11, 0x20);
return Convert.ToBase64String(dst);
}
A verification method is required
public static bool VerifyHashedPassword(string hashedSecurityQuestion, string question, string answer)
{
if (hashedSecurityQuestion == null)
{
return false;
}
if (question == null)
{
throw new ArgumentNullException("Question is null");
}
if (answer == null)
{
throw new ArgumentNullException("Answer is null");
}
string questionAndAnswer = question + "_" + answer;
// has to retrieve salt
byte[] buffer4;
byte[] src = Convert.FromBase64String(hashedSecurityQuestion);
if ((src.Length != 0x31) || (src[0] != 0))
{
return false;
}
byte[] dst = new byte[0x10];
Buffer.BlockCopy(src, 1, dst, 0, 0x10);
byte[] buffer3 = new byte[0x20];
Buffer.BlockCopy(src, 0x11, buffer3, 0, 0x20);
using (Rfc2898DeriveBytes bytes = new Rfc2898DeriveBytes(questionAndAnswer, dst, 0x3e8))
{
buffer4 = bytes.GetBytes(0x20);
}
return buffer3.SequenceEqual(buffer4);
}
In the login process, there is one extra step to verify the security question and answer are checked. The MVC View displays a drop down of questions and a textbox for an answer, both values for which are in the view model
var result = await _signInManager.PasswordSignInAsync(model.UserName, model.Password, model.RememberMe, shouldLockout: true);
// if the user login is a success, check if a security question exists
if (result == SignInStatus.Success && user.HasSecurityQuestion)
{
// security question exists, so check it
if (!user.VerifySecrityQuestion(model.SecurityQuestion, model.SecurityQuestionAnswer))
{
result = SignInStatus.Failure;
}
}
Authy/Twilio Developer Evangelist here. There are a couple other options for additional security that you have:
Enforce & encourage strong passwords
This includes things like minimum length, showing a password "strength" indicator, and including easy ways for people to use a password manager.
I compiled more details about recommendations for strong passwords:
https://github.com/robinske/betterpasswords
One Time Passwords in the form of TOTP
This is what you'd see with apps like Authy or Google Authenticator. TOTP (time-based one time passwords) is a standard, you can read about that here.
Authy has APIs to implement OTPs here.
Push Authentication
This is another form of 2FA that allows your user to "approve" or "deny" a login request in the form of a push notification. This is the most secure form of 2FA with a seamless user experience, you can read more about how Authy does that here.
Authy has APIs to implement push authentication here.
========
Note: security questions are a lot like additional passwords that can be more easily Googled, so I'd encourage your client to think about using a true second factor.

My Akka.Net Demo is incredibly slow

I am trying to get a proof of concept running with akka.net. I am sure that I am doing something terribly wrong, but I can't figure out what it is.
I want my actors to form a graph of nodes. Later, this will be a complex graph of business objekts, but for now I want to try a simple linear structure like this:
I want to ask a node for a neighbour that is 9 steps away. I am trying to implement this in a recursive manner. I ask node #9 for a neighbour that is 9 steps away, then I ask node #8 for a neighbour that is 8 steps away and so on. Finally, this should return node #0 as an answer.
Well, my code works, but it takes more than 4 seconds to execute. Why is that?
This is my full code listing:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Akka;
using Akka.Actor;
namespace AkkaTest
{
class Program
{
public static Stopwatch stopwatch = new Stopwatch();
static void Main(string[] args)
{
var system = ActorSystem.Create("MySystem");
IActorRef[] current = new IActorRef[0];
Console.WriteLine("Initializing actors...");
for (int i = 0; i < 10; i++)
{
var current1 = current;
var props = Props.Create<Obj>(() => new Obj(current1, Guid.NewGuid()));
var actorRef = system.ActorOf(props, i.ToString());
current = new[] { actorRef };
}
Console.WriteLine("actors initialized.");
FindNeighboursRequest r = new FindNeighboursRequest(9);
stopwatch.Start();
var response = current[0].Ask(r);
FindNeighboursResponse result = (FindNeighboursResponse)response.Result;
stopwatch.Stop();
foreach (var d in result.FoundNeighbours)
{
Console.WriteLine(d);
}
Console.WriteLine("Search took " + stopwatch.ElapsedMilliseconds + "ms.");
Console.ReadLine();
}
}
public class FindNeighboursRequest
{
public FindNeighboursRequest(int distance)
{
this.Distance = distance;
}
public int Distance { get; private set; }
}
public class FindNeighboursResponse
{
private IActorRef[] foundNeighbours;
public FindNeighboursResponse(IEnumerable<IActorRef> descendants)
{
this.foundNeighbours = descendants.ToArray();
}
public IActorRef[] FoundNeighbours
{
get { return this.foundNeighbours; }
}
}
public class Obj : ReceiveActor
{
private Guid objGuid;
readonly List<IActorRef> neighbours = new List<IActorRef>();
public Obj(IEnumerable<IActorRef> otherObjs, Guid objGuid)
{
this.neighbours.AddRange(otherObjs);
this.objGuid = objGuid;
Receive<FindNeighboursRequest>(r => handleFindNeighbourRequest(r));
}
public Obj()
{
}
private async void handleFindNeighbourRequest (FindNeighboursRequest r)
{
if (r.Distance == 0)
{
FindNeighboursResponse response = new FindNeighboursResponse(new IActorRef[] { Self });
Sender.Tell(response, Self);
return;
}
List<FindNeighboursResponse> responses = new List<FindNeighboursResponse>();
foreach (var actorRef in neighbours)
{
FindNeighboursRequest req = new FindNeighboursRequest(r.Distance - 1);
var response2 = actorRef.Ask(req);
responses.Add((FindNeighboursResponse)response2.Result);
}
FindNeighboursResponse response3 = new FindNeighboursResponse(responses.SelectMany(rx => rx.FoundNeighbours));
Sender.Tell(response3, Self);
}
}
}
The reason of such slow behavior is the way you use Ask (an that you use it, but I'll cover this later). In your example, you're asking each neighbor in a loop, and then immediately executing response2.Result which is actively blocking current actor (and thread it resides on). So you're essentially making synchronous flow with blocking.
The easiest thing to fix that, is to collect all tasks returned from Ask and use Task.WhenAll to collect them all, without waiting for each one in a loop. Taking this example:
public class Obj : ReceiveActor
{
private readonly IActorRef[] _neighbours;
private readonly Guid _id;
public Obj(IActorRef[] neighbours, Guid id)
{
_neighbours = neighbours;
_id = id;
Receive<FindNeighboursRequest>(async r =>
{
if (r.Distance == 0) Sender.Tell(new FindNeighboursResponse(new[] {Self}));
else
{
var request = new FindNeighboursRequest(r.Distance - 1);
var replies = _neighbours.Select(neighbour => neighbour.Ask<FindNeighboursResponse>(request));
var ready = await Task.WhenAll(replies);
var responses = ready.SelectMany(x => x.FoundNeighbours);
Sender.Tell(new FindNeighboursResponse(responses.ToArray()));
}
});
}
}
This one is much faster.
NOTE: In general you shouldn't use Ask inside of an actor:
Each ask is allocating a listener inside current actor, so in general using Ask is A LOT heavier than passing messages with Tell.
When sending messages through chain of actors, cost of ask is additionally transporting message twice (one for request and one for reply) through each actor. One of the popular patterns is that, when you are sending request from A⇒B⇒C⇒D and respond from D back to A, you can reply directly D⇒A, without need of passing the message through whole chain back. Usually combination of Forward/Tell works better.
In general don't use async version of Receive if it's not necessary - at the moment, it's slower for an actor when compared to sync version.

How to simulate large number of clients using UCMA, for load testing?

I have created application using Lync client side SDK 2013 and UCMA 4.0 . Now I test my application with large number of users. How can I simulate large number of client using UCMA or Lync client side SDK?
It depends on what exactly what you want to "simulate".
If you just want call traffic there is sipp, but that is just simple sip calls and doesn't really reflect an actual Microsoft Lync Client.
As far as I know, Microsoft doesn't provide any load testing tools in Lync. You will have to generate them yourself base on what exactly you want to "simulate".
With a UCMA trusted application, you should be able to startup and use a large number of user endpoints to "simulate" common lync services (like randomly changing presence, making calls, send IM's, etc). You would have to create such an app yourself.
I created a tool in UCMA to do my stress test for all my applications than I have made.
It is simple to make, and it is composed of two parts.
This example is a stress tester for calls. Of course, you can easily make a different one by using this example.
We create our platform, follow our Set-CsTrustedApplication.
var platformSettings = new ProvisionedApplicationPlatformSettings("InnixiTester", "urn:application:innixitester");
var collabPlatform = new CollaborationPlatform(platformSettings);
collabPlatform.EndStartup(collabPlatform.BeginStartup(null, null));
Ok, I know what I am doing here is a wrong chaining together, the Begin and the End into one line of code. However, this is just a code exemple. I invite you to read the article of Tom Morgan, he explains why it is not good to do it like me.
We use here a Parallel loop to create all our users-endpoint. In that way, it goes faster.
/*
* Proprieties of the class
*/
private AutoResetEvent _waitForStressTestToFinish = new AutoResetEvent(false);
private List<UserEndpoint> _listUserEndpoints = new List<UserEndpoint>();
private int _maxUsers = 200;
private int _tickTotal;
private int _tickCount;
private int _nbrCallsByIntervall;
/*
* End
*/
_maxUsers = 200; // Nbr max of users
const var callsTotal = 200; // Nbr of total call
const var timeToTest = 30; // Total time to test
const var intervalOfCalls = 5; // We want to make our calls between specific intervals
Parallel.For(0, _maxUsers, i =>
{
CreateUserEndpoint(collabPlatform, i.ToString());
});
You simply create your UserEndpoint here. The scenario is that my users in the active directory are stressuser0 to stressuser200. With extension starting from +14250 to +1425200
private void CreateUserEndpoint(CollaborationPlatform cp, string iteration)
{
try
{
UserEndpointSettings settingsUser = new UserEndpointSettings($"sip:stressuser{iteration}#pferde.net", "pool2010.pferde.net", 5061);
settingsUser = InitializePublishAlwaysOnlineSettings(settingsUser);
var userEndpoint = new UserEndpoint(cp, settingsUser);
userEndpoint.EndEstablish(userEndpoint.BeginEstablish(null, null));
PublishOnline(userEndpoint);
_listUserEndpoints.Add(userEndpoint);
Console.WriteLine($"The User Endpoint owned by URI: {userEndpoint.OwnerUri} was created\n");
}
catch (Exception)
{
Console.WriteLine($"failed to create for --> sip:stressuser{iteration}#pferde.net");
throw;
}
}
private UserEndpointSettings InitializePublishAlwaysOnlineSettings(UserEndpointSettings settings)
{
settings.AutomaticPresencePublicationEnabled = true;
settings.Presence.PreferredServiceCapabilities.AudioSupport = CapabilitySupport.Supported;
return (settings);
}
Now time to place the calls! We are going to code a simple algorithm with a timer. Is going to calculate how many calls it needs to make for X time and for Y Calls and for Z intervals.
Console.WriteLine("Tape a key to place calls...");
Console.ReadKey();
PlaceCalls(callsTotal, timeToTest, intervalOfCalls);
_waitForStressTestToFinish.WaitOne();
}
catch (Exception ex)
{
Console.WriteLine($"Shutting down platform due to error {ex}");
ShutdownPlatform(collabPlatform);
}
ShutdownPlatform(collabPlatform);
}
private void PlaceCalls(int callsMax, int timeMax, int timeIntervall)
{
_tickTotal = timeMax / timeIntervall;
_nbrCallsByIntervall= callsMax / _tickTotal;
Console.WriteLine($"_nbrCallsByIntervall --> {_nbrCallsByIntervall}");
var timeIntervalTimespan = new TimeSpan(0, 0, 0, timeIntervall);
_timer = new Timer(timeIntervalTimespan.TotalMilliseconds);
_timer.Elapsed += new ElapsedEventHandler(_timer_Elapsed);
_timer.Enabled = true;
}
void _timer_Elapsed(object sender, ElapsedEventArgs e)
{
if (_tickCount < _tickTotal)
{
Console.WriteLine($"\n Pause Timer | On {_tickCount} to {_tickTotal}\n");
_timer.Enabled = false;
for (var i = 0; i <= _nbrCallsByIntervall - 1; ++i)
{
ConversationSettings convSettings = new ConversationSettings();
Conversation conversation = new Conversation(_listUserEndpoints[generateNumber(0, _listUserEndpoints.Count)], convSettings);
var audioVideoCall = new AudioVideoCall(conversation);
CallEstablishOptions options = new CallEstablishOptions();
var gNbr = generateNumber(0, _listUserEndpoints.Count);
try
{
// Here I'm calling a single phone number. You can use GenerateNumber to call stressusers each others. But you have to extend your code to accept the calls coming.
audioVideoCall.BeginEstablish($"3322", options, null, audioVideoCall);
}
catch (Exception)
{
Console.WriteLine("Fail to Call the remote user...");
throw;
}
Console.WriteLine($"Call--> +1425{gNbr}.Counter--> {_tickCount} Ticket--> {_tickTotal} and thread id {Thread.CurrentThread.ManagedThreadId}");
}
_tickCount++;
_timer.Enabled = true;
Console.WriteLine("\n reStart Timer \n");
}
else
{
Console.WriteLine("\n!!! END Stress test !!!\n");
_timer.Enabled = false;
_waitForCallToEstablish.Set();
}
}
private int generateNumber(int min, int max)
{
var r = new Random();
Thread.Sleep(200);
return (r.Next(min, max));
}

Localization With Database MVC

I am working on a multilingual ASP.NET MVC application (MVC4).
I want to make my resource file strings to be editable at runtime without recompiling the application and without app pool recycling And it doesn't look possible with .resx file, so I migrate to store string resources in Database.
I have to Get Each Label/String Resource From Database, so there will be more hits to database for each request. How to fix that?
I have googled around and someone suggests to load the resource in a dictionary and store it as application variable, at login/Sign In page and use that dictionary as resource instead of database hit.
I am confused, what will be effective approach.Can someone guide me in right direction to avoid more database hits?
Any help/suggestions will be highly appreciated.
Thanks
I ran into the same concerns using .resx files for localization. They just did not work well when the persons doing the translation were not programmers. Now, we have a translation page in our admin area. Works great.
One area which we still don't have a good solution for are the data annotations, which still use the .resx files. I have trimmed the source below to remove any references to our actual database structures or tables.
There is a fallback to using the underlying .resx file, if an entry does not exist in the database. If there is not an entry in the .resx file, I split the word whenever a capitol letter is found ( CamelSpace is a string extension method ).
Lastly, depending on your implementation, you may need to remove the context caching, especially if you are caching out of process.
A few examples of usage:
#LanguageDb.Translate("Enter your first name below","FirstNamePrompt")
#LanguageDb.Me.FirstName
#String
.Format(LanguageDb
.Translate(#"You can be insured for
{0} down and {1} payments of {2}"),
Model.Down,Model.NumPayments,
Model.InstallmentAmount)
public class LanguageDb : DynamicObject
{
public static dynamic Me = new LanguageDb();
private LanguageDb() { }
public static string Translate(string englishPhrase, string resourceCode = null)
{
return GetTranslation(englishPhrase, resourceCode) ?? englishPhrase;
}
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
result = GetTranslation(binder.Name);
return true;
}
private static string GetTranslation(string resourceName, string resourceCode = null)
{
resourceCode = resourceCode ?? resourceName;
if (resourceCode.Contains(" ") || resourceCode.Length > 50)
{
resourceCode = resourceName.GetHashCode().ToString(CultureInfo.InvariantCulture);
}
var lang = (string)HttpContext.Current.Request.RequestContext.RouteData.Values["lang"] ?? "en";
// cache entries for an hour
var result = Get(subagent + "_" + lang + "_" + resourceCode, 3600, () =>
{
// cache a datacontext object for 30 seconds.
var context = Get("_context", 30, () => new YourDataContext()) as YourDataContext;
var translation = context.Translations.FirstOrDefault(row => row.lang == lang && row.Code == resourceCode);
if (translation == null)
{
translation = new Lookup {
Code = resourceCode,
lang = lang,
Value = Language.ResourceManager.GetString(resourceName, Language.Culture)
?? resourceName.CamelSpace()
};
context.Translations.Add(translation);
context.SaveChanges();
}
return translation.Value;
});
return result;
}
public override bool TrySetMember(SetMemberBinder binder, object value)
{
// ignore this
return true;
}
public static T Get<T>(string cacheId, int secondsToCache, Func<T> getItemCallback) where T : class
{
T item = HttpRuntime.Cache.Get(cacheId) as T;
if (item == null)
{
item = getItemCallback();
if (secondsToCache > 0)
{
HttpRuntime.Cache.Insert(
cacheId, item, null, Cache.NoAbsoluteExpiration,
new TimeSpan(0, 0, secondsToCache), CacheItemPriority.Normal, null);
}
else
{
HttpRuntime.Cache.Insert(cacheId, item);
}
}
return item;
}
}

Creating an nhibernate session per web request with Castle.Facility.AutoTx and Castle.Facility.NHibernate

I am using Castle Windors and it's AutoTx and the NHibernate Facility by haf. Ultimately I want the benefits of ease of use of the Transaction attribute provided by AutoTx. (ASP.NET MVC 4 project).
I am using Castle.Facilities.NHibernate.ISessionManager to manage my PerWebRequest sessions. I have setup the Windsor Installer as such:
public void Install(Castle.Windsor.IWindsorContainer container, Castle.MicroKernel.SubSystems.Configuration.IConfigurationStore store)
{
container.AddFacility<AutoTxFacility>();
container.Register(Component.For<INHibernateInstaller>().ImplementedBy<NHibernateInstaller>().LifeStyle.Singleton);
container.AddFacility<NHibernateFacility>(f => f.DefaultLifeStyle = DefaultSessionLifeStyleOption.SessionPerWebRequest);
container.Install(FromAssembly.Containing<PersonRepository>());
}
I'm using the DefaultLifeStyle of SessionPerWebRequest, which I expect would do exactly that, provide me with a session that lasts the entire web request, such that all calls to OpenSession on SessionManager within same request use the same session. I'm testing that with the following code:
public class HomeController : Controller
{
private readonly ISessionManager _sessionManager;
public HomeController(ISessionManager sessionManager)
{
_sessionManager = sessionManager;
}
public ActionResult Index()
{
using (var session1 = _sessionManager.OpenSession())
{
var person = session1.Get<Person>(1);
using (var session2 = _sessionManager.OpenSession())
{
var person2 = session2.Get<Person>(1);
}
}
return View();
}
}
and checking the log to see the id of each created session. The id is always different. eg
05/01/2013 11:27:39.109 DEBUG 9 NHibernate.Impl.SessionImpl - [session-id=c1ba248a-14ba-4468-a20c-d6114b7dac61] opened session at timestamp: 634929820591, for session factory: [/ea869bb12b4d4e51b9f431a4f9c9d9fa]
05/01/2013 11:30:36.383 DEBUG 9 NHibernate.Impl.SessionImpl - [session-id=72481180-625d-4085-98e9-929e3fd93e8a] opened session at timestamp: 634929822363, for session factory: [/ea869bb12b4d4e51b9f431a4f9c9d9fa]
It's worth noting that I haven't added anything to the web.config in the way of Handlers. Do I need to? ( I didn't see any documentation suggesting this in the NHib Facility Wiki) Are my expectations that the same Session will always be returned incorrect.
I've had a look through the source code for the facility and do not understand how a session per web request is being instantiated and how multiple calls to OpenSession would result in the same session in the same web request.
The following is how SessionManager is registered with Windsor:
Component.For<ISessionManager>().Instance(new SessionManager(() =>
{
var factory = Kernel.Resolve<ISessionFactory>(x.Instance.SessionFactoryKey);
var s = x.Instance.Interceptor.Do(y => factory.OpenSession(y)).OrDefault(factory.OpenSession());
s.FlushMode = flushMode;
return s;
}))
.Named(x.Instance.SessionFactoryKey + SessionManagerSuffix)
.LifeStyle.Singleton
ISession is registered with Windsor using the following
private IRegistration RegisterSession(Data x, uint index)
{
Contract.Requires(index < 3,
"there are only three supported lifestyles; per transaction, per web request and transient");
Contract.Requires(x != null);
Contract.Ensures(Contract.Result<IRegistration>() != null);
return GetLifeStyle(
Component.For<ISession>()
.UsingFactoryMethod((k, c) =>
{
var factory = k.Resolve<ISessionFactory>(x.Instance.SessionFactoryKey);
var s = x.Instance.Interceptor.Do(y => factory.OpenSession(y)).OrDefault(factory.OpenSession());
s.FlushMode = flushMode;
logger.DebugFormat("resolved session component named '{0}'", c.Handler.ComponentModel.Name);
return s;
}), index, x.Instance.SessionFactoryKey);
}
private ComponentRegistration<T> GetLifeStyle<T>(ComponentRegistration<T> registration, uint index, string baseName)
where T : class
{
Contract.Requires(index < 3,
"there are only three supported lifestyles; per transaction, per web request and transient");
Contract.Ensures(Contract.Result<ComponentRegistration<T>>() != null);
switch (defaultLifeStyle)
{
case DefaultSessionLifeStyleOption.SessionPerTransaction:
if (index == 0)
return registration.Named(baseName + SessionPerTxSuffix).LifeStyle.PerTopTransaction();
if (index == 1)
return registration.Named(baseName + SessionPWRSuffix).LifeStyle.PerWebRequest;
if (index == 2)
return registration.Named(baseName + SessionTransientSuffix).LifeStyle.Transient;
goto default;
case DefaultSessionLifeStyleOption.SessionPerWebRequest:
if (index == 0)
return registration.Named(baseName + SessionPWRSuffix).LifeStyle.PerWebRequest;
if (index == 1)
return registration.Named(baseName + SessionPerTxSuffix).LifeStyle.PerTopTransaction();
if (index == 2)
return registration.Named(baseName + SessionTransientSuffix).LifeStyle.Transient;
goto default;
case DefaultSessionLifeStyleOption.SessionTransient:
if (index == 0)
return registration.Named(baseName + SessionTransientSuffix).LifeStyle.Transient;
if (index == 1)
return registration.Named(baseName + SessionPerTxSuffix).LifeStyle.PerTopTransaction();
if (index == 2)
return registration.Named(baseName + SessionPWRSuffix).LifeStyle.PerWebRequest;
goto default;
default:
throw new FacilityException("invalid index passed to GetLifeStyle<T> - please file a bug report");
}
}
which does register an ISession as PerWebRequest, but I can't see anywhere in the code where that named registration is extracted when a session is required?
Any help on what I need to do get Session per web request working is appreciated.
UPDATE I decided to just replace the code function being passed into SessionManager constructor with code that grabs the ISession from the container, rather than uses the factory. Works perfectly for what I want it to do, including being wrapped in transactions and only opening one session per web request, or transient etc.
Component.For<ISessionManager>().Instance(new SessionManager(() =>
{
var s = Kernel.Resolve<ISession>();
s.FlushMode = flushMode;
return s;
}))
//Component.For<ISessionManager>().Instance(new SessionManager(() =>
//{
// var factory = Kernel.Resolve<ISessionFactory>(x.Instance.SessionFactoryKey);
// var s = x.Instance.Interceptor.Do(y => factory.OpenSession(y)).OrDefault(factory.OpenSession());
// s.FlushMode = flushMode;
// return s;
//}))
.Named(x.Instance.SessionFactoryKey + SessionManagerSuffix)
.LifeStyle.Singleton
Kernel.Resolve() I expect will grab the first registered service in the container. This will be whatever I set the Lifestyle to.