From 1b0ed9ce57923b994b8309057f2465372fd6b38a Mon Sep 17 00:00:00 2001 From: Embo1 Date: Mon, 7 Sep 2015 16:51:14 +0200 Subject: [PATCH] Updated to version 2.0.1 of the MongoDB C# drivers --- MongoSessionStateStore.sln | 42 +- .../MongoSessionStateStore.cs | 1275 +++++++++-------- .../MongoSessionStateStore.csproj | 136 +- .../Properties/AssemblyInfo.cs | 72 +- MongoSessionStateStore/packages.config | 6 + 5 files changed, 797 insertions(+), 734 deletions(-) create mode 100644 MongoSessionStateStore/packages.config diff --git a/MongoSessionStateStore.sln b/MongoSessionStateStore.sln index e7edc38..0ecc68a 100644 --- a/MongoSessionStateStore.sln +++ b/MongoSessionStateStore.sln @@ -1,20 +1,22 @@ - -Microsoft Visual Studio Solution File, Format Version 11.00 -# Visual Studio 2010 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MongoSessionStateStore", "MongoSessionStateStore\MongoSessionStateStore.csproj", "{2F0DD54F-6EF0-4DDD-9737-6D174B9E4E9D}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {2F0DD54F-6EF0-4DDD-9737-6D174B9E4E9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2F0DD54F-6EF0-4DDD-9737-6D174B9E4E9D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2F0DD54F-6EF0-4DDD-9737-6D174B9E4E9D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2F0DD54F-6EF0-4DDD-9737-6D174B9E4E9D}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.23107.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MongoSessionStateStore", "MongoSessionStateStore\MongoSessionStateStore.csproj", "{2F0DD54F-6EF0-4DDD-9737-6D174B9E4E9D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2F0DD54F-6EF0-4DDD-9737-6D174B9E4E9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2F0DD54F-6EF0-4DDD-9737-6D174B9E4E9D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2F0DD54F-6EF0-4DDD-9737-6D174B9E4E9D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2F0DD54F-6EF0-4DDD-9737-6D174B9E4E9D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/MongoSessionStateStore/MongoSessionStateStore.cs b/MongoSessionStateStore/MongoSessionStateStore.cs index 00afad9..0bacbef 100644 --- a/MongoSessionStateStore/MongoSessionStateStore.cs +++ b/MongoSessionStateStore/MongoSessionStateStore.cs @@ -1,615 +1,660 @@ -using System; -using System.Globalization; -using System.Web.SessionState; -using System.Configuration; -using System.Configuration.Provider; -using System.Web.Configuration; -using MongoDB.Driver; -using MongoDB.Bson; -using System.Diagnostics; -using System.IO; -using MongoDB.Driver.Builders; -using System.Web; - -namespace MongoSessionStateStore -{ - /// - /// Custom ASP.NET Session State Provider using MongoDB as the state store. - /// For reference on this implementation see MSDN ref: - /// - http://msdn.microsoft.com/en-us/library/ms178587.aspx - /// - http://msdn.microsoft.com/en-us/library/ms178588.aspx - this sample provider was used as the basis for this - /// provider, with MongoDB-specific implementation swapped in, plus cosmetic changes like naming conventions. - /// - /// Session state is stored in a "Sessions" collection within a "SessionState" database. Example session document: - /// { - /// "_id" : "bh54lskss4ycwpreet21dr1h", - /// "ApplicationName" : "/", - /// "Created" : ISODate("2011-04-29T21:41:41.953Z"), - /// "Expires" : ISODate("2011-04-29T22:01:41.953Z"), - /// "LockDate" : ISODate("2011-04-29T21:42:02.016Z"), - /// "LockId" : 1, - /// "Timeout" : 20, - /// "Locked" : true, - /// "SessionItems" : "AQAAAP////8EVGVzdAgAAAABBkFkcmlhbg==", - /// "Flags" : 0 - /// } - /// - /// Inline with the above MSDN reference: - /// If the provider encounters an exception when working with the data source, it writes the details of the exception - /// to the Application Event Log instead of returning the exception to the ASP.NET application. This is done as a security - /// measure to avoid private information about the data source from being exposed in the ASP.NET application. - /// The sample provider specifies an event Source property value of "MongoSessionStateStore." Before your ASP.NET - /// application will be able to write to the Application Event Log successfully, you will need to create the following registry key: - /// HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Eventlog\Application\MongoSessionStateStore - /// If you do not want the sample provider to write exceptions to the event log, then you can set the custom writeExceptionsToEventLog - /// attribute to false in the Web.config file. - /// - /// The session-state store provider does not provide support for the Session_OnEnd event, it does not automatically clean up expired session-item data. - /// You should have a job to periodically delete expired session information from the data store where Expires date is in the past, i.e.: - /// db.Sessions.remove({"Expires" : {$lt : new Date() }}) - /// - /// Example web.config settings: - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// ... - /// - /// replicasToWrite setting is interpreted as the number of replicas to write to, in addition to the primary (in a replicaset environment). - /// i.e. replicasToWrite = 0, will wait for the response from writing to the primary node. > 0 will wait for the response having written to - /// ({replicasToWrite} + 1) nodes - /// - public sealed class MongoSessionStateStore : SessionStateStoreProviderBase - { - private SessionStateSection _config; - private ConnectionStringSettings _connectionStringSettings; - private string _applicationName; - private string _connectionString; - private bool _writeExceptionsToEventLog; - private const string ExceptionMessage = "An exception occurred. Please contact your administrator."; - private const string EventSource = "MongoSessionStateStore"; - private const string EventLog = "Application"; - private WriteConcern _writeConcern; - - /// - /// The ApplicationName property is used to differentiate sessions - /// in the data source by application. - /// - public string ApplicationName - { - get { return _applicationName; } - } - - /// - /// If false, exceptions are thrown to the caller. If true, - /// exceptions are written to the event log. - /// - public bool WriteExceptionsToEventLog - { - get { return _writeExceptionsToEventLog; } - set { _writeExceptionsToEventLog = value; } - } - - /// - /// Returns a reference to the collection in MongoDB that holds the Session state - /// data. - /// - /// MongoDB server connection - /// MongoCollection - private MongoCollection GetSessionCollection(MongoServer conn) - { - return conn.GetDatabase("SessionState").GetCollection("Sessions"); - } - - /// - /// Returns a connection to the MongoDB server holding the session state data. - /// - /// MongoServer - private MongoServer GetConnection() - { - var client = new MongoClient(_connectionString); - return client.GetServer(); - } - - /// - /// Initialise the session state store. - /// - /// session state store name. Defaults to "MongoSessionStateStore" if not supplied - /// configuration settings - public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config) - { - // Initialize values from web.config. - if (config == null) - throw new ArgumentNullException("config"); - - if (name.Length == 0) - name = "MongoSessionStateStore"; - - if (String.IsNullOrEmpty(config["description"])) - { - config.Remove("description"); - config.Add("description", "MongoDB Session State Store provider"); - } - - // Initialize the abstract base class. - base.Initialize(name, config); - - // Initialize the ApplicationName property. - _applicationName = System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath; - - // Get configuration element. - Configuration cfg = WebConfigurationManager.OpenWebConfiguration(ApplicationName); - _config = (SessionStateSection)cfg.GetSection("system.web/sessionState"); - - // Initialize connection string. - _connectionStringSettings = ConfigurationManager.ConnectionStrings[config["connectionStringName"]]; - - if (_connectionStringSettings == null || _connectionStringSettings.ConnectionString.Trim() == "") - { - throw new ProviderException("Connection string cannot be blank."); - } - - _connectionString = _connectionStringSettings.ConnectionString; - - // Initialize WriteExceptionsToEventLog - _writeExceptionsToEventLog = false; - - if (config["writeExceptionsToEventLog"] != null) - { - if (config["writeExceptionsToEventLog"].ToUpper() == "TRUE") - _writeExceptionsToEventLog = true; - } - - // Initialise WriteConcern options. - // Defaults to fsynch=false, w=1 (Provides acknowledgment of write operations on a standalone mongod or the primary in a replica set.) - // replicasToWrite config item comes into use when > 0. This translates to the WriteConcern wValue, by adding 1 to it. - // e.g. replicasToWrite = 1 is taken as meaning "I want to wait for write operations to be acknowledged at the primary + {replicasToWrite} replicas" - // MongoDB C# Driver references on WriteConcern : http://docs.mongodb.org/manual/core/write-operations/#write-concern - bool fsync = false; - if (config["fsync"] != null) - { - if (config["fsync"].ToUpper() == "TRUE") - fsync = true; - } - - int replicasToWrite = 1; - if (config["replicasToWrite"] != null) - { - if (!int.TryParse(config["replicasToWrite"], out replicasToWrite)) - throw new ProviderException("Replicas To Write must be a valid integer"); - } - - string wValue = "1"; - if (replicasToWrite > 0) - wValue = (1 + replicasToWrite).ToString(CultureInfo.InvariantCulture); - - _writeConcern = new WriteConcern - { - FSync = fsync, - W = WriteConcern.WValue.Parse(wValue) - }; - } - - public override SessionStateStoreData CreateNewStoreData(HttpContext context, int timeout) - { - return new SessionStateStoreData(new SessionStateItemCollection(), - SessionStateUtility.GetSessionStaticObjects(context), - timeout); - } - - /// - /// SessionStateProviderBase.SetItemExpireCallback - /// - public override bool SetItemExpireCallback(SessionStateItemExpireCallback expireCallback) - { - return false; - } - - /// - /// Serialize is called by the SetAndReleaseItemExclusive method to - /// convert the SessionStateItemCollection into a Base64 string to - /// be stored in MongoDB. - /// - private string Serialize(SessionStateItemCollection items) - { - using(var ms = new MemoryStream()) - using (var writer = new BinaryWriter(ms)) - { - if (items != null) - items.Serialize(writer); - - writer.Close(); - - return Convert.ToBase64String(ms.ToArray()); - } - } - - /// - /// SessionStateProviderBase.SetAndReleaseItemExclusive - /// - public override void SetAndReleaseItemExclusive(HttpContext context, - string id, - SessionStateStoreData item, - object lockId, - bool newItem) - { - // Serialize the SessionStateItemCollection as a string. - string sessItems = Serialize((SessionStateItemCollection)item.Items); - - MongoServer conn = GetConnection(); - MongoCollection sessionCollection = GetSessionCollection(conn); - - try - { - if (newItem) - { - var insertDoc = new BsonDocument - { - {"_id", id}, - {"ApplicationName", ApplicationName}, - {"Created", DateTime.Now.ToUniversalTime()}, - {"Expires", DateTime.Now.AddMinutes(item.Timeout).ToUniversalTime()}, - {"LockDate", DateTime.Now.ToUniversalTime()}, - {"LockId", 0}, - {"Timeout", item.Timeout}, - {"Locked", false}, - {"SessionItems", sessItems}, - {"Flags", 0} - }; - - var query = Query.And(Query.EQ("_id", id), Query.EQ("ApplicationName", ApplicationName), Query.LT("Expires", DateTime.Now.ToUniversalTime())); - sessionCollection.Remove(query, _writeConcern); - sessionCollection.Insert(insertDoc, _writeConcern); - } - else - { - var query = Query.And(Query.EQ("_id", id), Query.EQ("ApplicationName", ApplicationName), Query.EQ("LockId", (Int32)lockId)); - var update = Update.Set("Expires", DateTime.Now.AddMinutes(item.Timeout).ToUniversalTime()); - update.Set("SessionItems", sessItems); - update.Set("Locked", false); - sessionCollection.Update(query, update, _writeConcern); - } - } - catch (Exception e) - { - if (WriteExceptionsToEventLog) - { - WriteToEventLog(e, "SetAndReleaseItemExclusive"); - throw new ProviderException(ExceptionMessage); - } - throw; - } - } - - /// - /// SessionStateProviderBase.GetItem - /// - public override SessionStateStoreData GetItem(HttpContext context, - string id, - out bool locked, - out TimeSpan lockAge, - out object lockId, - out SessionStateActions actionFlags) - { - return GetSessionStoreItem(false, context, id, out locked, - out lockAge, out lockId, out actionFlags); - } - - /// - /// SessionStateProviderBase.GetItemExclusive - /// - public override SessionStateStoreData GetItemExclusive(HttpContext context, - string id, - out bool locked, - out TimeSpan lockAge, - out object lockId, - out SessionStateActions actionFlags) - { - return GetSessionStoreItem(true, context, id, out locked, - out lockAge, out lockId, out actionFlags); - } - - /// - /// GetSessionStoreItem is called by both the GetItem and - /// GetItemExclusive methods. GetSessionStoreItem retrieves the - /// session data from the data source. If the lockRecord parameter - /// is true (in the case of GetItemExclusive), then GetSessionStoreItem - /// locks the record and sets a new LockId and LockDate. - /// - private SessionStateStoreData GetSessionStoreItem(bool lockRecord, - HttpContext context, - string id, - out bool locked, - out TimeSpan lockAge, - out object lockId, - out SessionStateActions actionFlags) - { - // Initial values for return value and out parameters. - SessionStateStoreData item = null; - lockAge = TimeSpan.Zero; - lockId = null; - locked = false; - actionFlags = 0; - - MongoServer conn = GetConnection(); - MongoCollection sessionCollection = GetSessionCollection(conn); - - // DateTime to check if current session item is expired. - // String to hold serialized SessionStateItemCollection. - string serializedItems = ""; - // True if a record is found in the database. - bool foundRecord = false; - // True if the returned session item is expired and needs to be deleted. - bool deleteData = false; - // Timeout value from the data store. - int timeout = 0; - - try - { - // lockRecord is true when called from GetItemExclusive and - // false when called from GetItem. - // Obtain a lock if possible. Ignore the record if it is expired. - IMongoQuery query; - if (lockRecord) - { - query = Query.And(Query.EQ("_id", id), Query.EQ("ApplicationName", ApplicationName), Query.EQ("Locked", false), Query.GT("Expires", DateTime.Now.ToUniversalTime())); - var update = Update.Set("Locked", true); - update.Set("LockDate", DateTime.Now.ToUniversalTime()); - var result = sessionCollection.Update(query, update, _writeConcern); - - locked = result.DocumentsAffected == 0; // DocumentsAffected == 0 == No record was updated because the record was locked or not found. - } - - // Retrieve the current session item information. - query = Query.And(Query.EQ("_id", id), Query.EQ("ApplicationName",ApplicationName)); - var results = sessionCollection.FindOneAs(query); - - if (results != null) - { - DateTime expires = results["Expires"].AsDateTime; - - if (expires < DateTime.Now.ToUniversalTime()) - { - // The record was expired. Mark it as not locked. - locked = false; - // The session was expired. Mark the data for deletion. - deleteData = true; - } - else - foundRecord = true; - - serializedItems = results["SessionItems"].AsString; - lockId = results["LockId"].AsInt32; - lockAge = DateTime.Now.ToUniversalTime().Subtract(results["LockDate"].AsDateTime); - actionFlags = (SessionStateActions)results["Flags"].AsInt32; - timeout = results["Timeout"].AsInt32; - } - - // If the returned session item is expired, - // delete the record from the data source. - if (deleteData) - { - query = Query.And(Query.EQ("_id", id), Query.EQ("ApplicationName", ApplicationName)); - sessionCollection.Remove(query, _writeConcern); - } - - // The record was not found. Ensure that locked is false. - if (!foundRecord) - locked = false; - - // If the record was found and you obtained a lock, then set - // the lockId, clear the actionFlags, - // and create the SessionStateStoreItem to return. - if (foundRecord && !locked) - { - lockId = (int)lockId + 1; - - query = Query.And(Query.EQ("_id", id), Query.EQ("ApplicationName", ApplicationName)); - var update = Update.Set("LockId", (int)lockId); - update.Set("Flags", 0); - sessionCollection.Update(query, update, _writeConcern); - - // If the actionFlags parameter is not InitializeItem, - // deserialize the stored SessionStateItemCollection. - item = actionFlags == SessionStateActions.InitializeItem - ? CreateNewStoreData(context, (int)_config.Timeout.TotalMinutes) - : Deserialize(context, serializedItems, timeout); - } - } - catch (Exception e) - { - if (WriteExceptionsToEventLog) - { - WriteToEventLog(e, "GetSessionStoreItem"); - throw new ProviderException(ExceptionMessage); - } - - throw; - } - - return item; - } - - private SessionStateStoreData Deserialize(HttpContext context, - string serializedItems, int timeout) - { - using (var ms = - new MemoryStream(Convert.FromBase64String(serializedItems))) - { - - var sessionItems = - new SessionStateItemCollection(); - - if (ms.Length > 0) - { - using (var reader = new BinaryReader(ms)) - { - sessionItems = SessionStateItemCollection.Deserialize(reader); - } - } - - return new SessionStateStoreData(sessionItems, - SessionStateUtility.GetSessionStaticObjects(context), - timeout); - } - } - - public override void CreateUninitializedItem(HttpContext context, string id, int timeout) - { - MongoServer conn = GetConnection(); - MongoCollection sessionCollection = GetSessionCollection(conn); - var doc = new BsonDocument - { - {"_id", id}, - {"ApplicationName", ApplicationName}, - {"Created", DateTime.Now.ToUniversalTime()}, - {"Expires", DateTime.Now.AddMinutes(timeout).ToUniversalTime()}, - {"LockDate", DateTime.Now.ToUniversalTime()}, - {"LockId", 0}, - {"Timeout", timeout}, - {"Locked", false}, - {"SessionItems", ""}, - {"Flags", 1} - }; - - try - { - var result = sessionCollection.Insert(doc, _writeConcern); - if (!result.Ok) - { - throw new Exception(result.ErrorMessage); - } - } - catch (Exception e) - { - if (WriteExceptionsToEventLog) - { - WriteToEventLog(e, "CreateUninitializedItem"); - throw new ProviderException(ExceptionMessage); - } - - throw; - } - } - - /// - /// This is a helper function that writes exception detail to the - /// event log. Exceptions are written to the event log as a security - /// measure to ensure private database details are not returned to - /// browser. If a method does not return a status or Boolean - /// indicating the action succeeded or failed, the caller also - /// throws a generic exception. - /// - private void WriteToEventLog(Exception e, string action) - { - using (var log = new EventLog()) - { - log.Source = EventSource; - log.Log = EventLog; - - string message = - String.Format("An exception occurred communicating with the data source.\n\nAction: {0}\n\nException: {1}", - action, e); - - log.WriteEntry(message); - } - } - - public override void Dispose() - { - } - - public override void EndRequest(HttpContext context) - { - - } - - public override void InitializeRequest(HttpContext context) - { - - } - - public override void ReleaseItemExclusive(HttpContext context, string id, object lockId) - { - MongoServer conn = GetConnection(); - MongoCollection sessionCollection = GetSessionCollection(conn); - - var query = Query.And(Query.EQ("_id", id), Query.EQ("ApplicationName", ApplicationName), Query.EQ("LockId", (Int32)lockId)); - var update = Update.Set("Locked", false); - update.Set("Expires", DateTime.Now.AddMinutes(_config.Timeout.TotalMinutes).ToUniversalTime()); - - try - { - sessionCollection.Update(query, update, _writeConcern); - } - catch (Exception e) - { - if (WriteExceptionsToEventLog) - { - WriteToEventLog(e, "ReleaseItemExclusive"); - throw new ProviderException(ExceptionMessage); - } - throw; - } - } - - public override void RemoveItem(HttpContext context, string id, object lockId, SessionStateStoreData item) - { - MongoServer conn = GetConnection(); - MongoCollection sessionCollection = GetSessionCollection(conn); - - var query = Query.And(Query.EQ("_id", id), Query.EQ("ApplicationName", ApplicationName), Query.EQ("LockId", (Int32)lockId)); - - try - { - sessionCollection.Remove(query, _writeConcern); - } - catch (Exception e) - { - if (WriteExceptionsToEventLog) - { - WriteToEventLog(e, "RemoveItem"); - throw new ProviderException(ExceptionMessage); - } - - throw; - } - } - - public override void ResetItemTimeout(HttpContext context, string id) - { - MongoServer conn = GetConnection(); - MongoCollection sessionCollection = GetSessionCollection(conn); - var query = Query.And(Query.EQ("_id", id), Query.EQ("ApplicationName", ApplicationName)); - var update = Update.Set("Expires", DateTime.Now.AddMinutes(_config.Timeout.TotalMinutes).ToUniversalTime()); - - try - { - sessionCollection.Update(query, update, _writeConcern); - } - catch (Exception e) - { - if (WriteExceptionsToEventLog) - { - WriteToEventLog(e, "ResetItemTimeout"); - throw new ProviderException(ExceptionMessage); - } - throw; - } - } - } -} +using System; +using System.Collections.Specialized; +using System.Configuration; +using System.Configuration.Provider; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Threading.Tasks; +using System.Web; +using System.Web.Configuration; +using System.Web.Hosting; +using System.Web.SessionState; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace MongoSessionStateStore +{ + /// + /// Custom ASP.NET Session State Provider using MongoDB as the state store. + /// For reference on this implementation see MSDN ref: + /// - http://msdn.microsoft.com/en-us/library/ms178587.aspx + /// - http://msdn.microsoft.com/en-us/library/ms178588.aspx - this sample provider was used as the basis for this + /// provider, with MongoDB-specific implementation swapped in, plus cosmetic changes like naming conventions. + /// Session state is stored in a "Sessions" collection within a "SessionState" database. Example session document: + /// { + /// "_id" : "bh54lskss4ycwpreet21dr1h", + /// "ApplicationName" : "/", + /// "Created" : ISODate("2011-04-29T21:41:41.953Z"), + /// "Expires" : ISODate("2011-04-29T22:01:41.953Z"), + /// "LockDate" : ISODate("2011-04-29T21:42:02.016Z"), + /// "LockId" : 1, + /// "Timeout" : 20, + /// "Locked" : true, + /// "SessionItems" : "AQAAAP////8EVGVzdAgAAAABBkFkcmlhbg==", + /// "Flags" : 0 + /// } + /// Inline with the above MSDN reference: + /// If the provider encounters an exception when working with the data source, it writes the details of the exception + /// to the Application Event Log instead of returning the exception to the ASP.NET application. This is done as a + /// security + /// measure to avoid private information about the data source from being exposed in the ASP.NET application. + /// The sample provider specifies an event Source property value of "MongoSessionStateStore." Before your ASP.NET + /// application will be able to write to the Application Event Log successfully, you will need to create the following + /// registry key: + /// HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Eventlog\Application\MongoSessionStateStore + /// If you do not want the sample provider to write exceptions to the event log, then you can set the custom + /// writeExceptionsToEventLog + /// attribute to false in the Web.config file. + /// The session-state store provider does not provide support for the Session_OnEnd event, it does not automatically + /// clean up expired session-item data. + /// You should have a job to periodically delete expired session information from the data store where Expires date is + /// in the past, i.e.: + /// db.Sessions.remove({"Expires" : {$lt : new Date() }}) + /// Example web.config settings: + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// ... + /// + /// replicasToWrite setting is interpreted as the number of replicas to write to, in addition to the primary (in a + /// replicaset environment). + /// i.e. replicasToWrite = 0, will wait for the response from writing to the primary node. > 0 will wait for the + /// response having written to + /// ({replicasToWrite} + 1) nodes + /// + public sealed class MongoSessionStateStore : SessionStateStoreProviderBase + { + private const string ExceptionMessage = "An exception occurred. Please contact your administrator."; + private const string EventSource = "MongoSessionStateStore"; + private const string EventLog = "Application"; + private SessionStateSection _config; + private ConnectionStringSettings _connectionStringSettings; + private IMongoDatabase _mongoDb; + private WriteConcern _writeConcern; + private string _connectionString; + + /// + /// The ApplicationName property is used to differentiate sessions + /// in the data source by application. + /// + public string ApplicationName { get; private set; } + + /// + /// If false, exceptions are thrown to the caller. If true, + /// exceptions are written to the event log. + /// + public bool WriteExceptionsToEventLog { get; set; } + + /// + /// Returns a reference to the collection in MongoDB that holds the Session state + /// data. + /// + /// MongoCollection + private IMongoCollection GetSessionCollection() + { + return GetDatabase().GetCollection("Sessions").WithWriteConcern(_writeConcern); + } + + /// + /// Returns a connection to the MongoDB server holding the session state data. + /// + /// MongoServer + private IMongoDatabase GetDatabase() + { + if (_mongoDb == null) + { + var mc = new MongoClient(_connectionString); + _mongoDb = mc.GetDatabase("SessionState"); + } + return _mongoDb; + } + + /// + /// Initialise the session state store. + /// + /// session state store name. Defaults to "MongoSessionStateStore" if not supplied + /// configuration settings + public override void Initialize(string name, NameValueCollection config) + { + // Initialize values from web.config. + if (config == null) + { + throw new ArgumentNullException("config"); + } + + if (name.Length == 0) + { + name = "MongoSessionStateStore"; + } + + if (string.IsNullOrEmpty(config["description"])) + { + config.Remove("description"); + config.Add("description", "MongoDB Session State Store provider"); + } + + // Initialize the abstract base class. + base.Initialize(name, config); + + // Initialize the ApplicationName property. + ApplicationName = HostingEnvironment.ApplicationVirtualPath; + + // Get configuration element. + var cfg = WebConfigurationManager.OpenWebConfiguration(ApplicationName); + _config = (SessionStateSection) cfg.GetSection("system.web/sessionState"); + + // Initialize connection string. + _connectionStringSettings = ConfigurationManager.ConnectionStrings[config["connectionStringName"]]; + + if (_connectionStringSettings == null || _connectionStringSettings.ConnectionString.Trim() == "") + { + throw new ProviderException("Connection string cannot be blank."); + } + + _connectionString = _connectionStringSettings.ConnectionString; + + // Initialize WriteExceptionsToEventLog + WriteExceptionsToEventLog = false; + + if (config["writeExceptionsToEventLog"] != null) + { + if (config["writeExceptionsToEventLog"].ToUpper() == "TRUE") + { + WriteExceptionsToEventLog = true; + } + } + + // Initialise WriteConcern options. + // Defaults to fsynch=false, w=1 (Provides acknowledgment of write operations on a standalone mongod or the primary in a replica set.) + // replicasToWrite config item comes into use when > 0. This translates to the WriteConcern wValue, by adding 1 to it. + // e.g. replicasToWrite = 1 is taken as meaning "I want to wait for write operations to be acknowledged at the primary + {replicasToWrite} replicas" + // MongoDB C# Driver references on WriteConcern : http://docs.mongodb.org/manual/core/write-operations/#write-concern + var fsync = false; + if (config["fsync"] != null) + { + if (config["fsync"].ToUpper() == "TRUE") + { + fsync = true; + } + } + + var replicasToWrite = 1; + if (config["replicasToWrite"] != null) + { + if (!int.TryParse(config["replicasToWrite"], out replicasToWrite)) + { + throw new ProviderException("Replicas To Write must be a valid integer"); + } + } + + var wValue = "1"; + if (replicasToWrite > 0) + { + wValue = (1 + replicasToWrite).ToString(CultureInfo.InvariantCulture); + } + + _writeConcern = new WriteConcern(WriteConcern.WValue.Parse(wValue), null, fsync, null); + } + + public override SessionStateStoreData CreateNewStoreData(HttpContext context, int timeout) + { + return new SessionStateStoreData(new SessionStateItemCollection(), + SessionStateUtility.GetSessionStaticObjects(context), + timeout); + } + + /// + /// SessionStateProviderBase.SetItemExpireCallback + /// + public override bool SetItemExpireCallback(SessionStateItemExpireCallback expireCallback) + { + return false; + } + + /// + /// Serialize is called by the SetAndReleaseItemExclusive method to + /// convert the SessionStateItemCollection into a Base64 string to + /// be stored in MongoDB. + /// + private string Serialize(SessionStateItemCollection items) + { + using (var ms = new MemoryStream()) + { + using (var writer = new BinaryWriter(ms)) + { + if (items != null) + { + items.Serialize(writer); + } + + writer.Close(); + + return Convert.ToBase64String(ms.ToArray()); + } + } + } + + /// + /// SessionStateProviderBase.SetAndReleaseItemExclusive + /// + public override void SetAndReleaseItemExclusive(HttpContext context, + string id, + SessionStateStoreData item, + object lockId, + bool newItem) + { + // Serialize the SessionStateItemCollection as a string. + var items = Serialize((SessionStateItemCollection) item.Items); + + var sessionCollection = GetSessionCollection(); + + try + { + if (newItem) + { + var insertDoc = new BsonDocument + { + {"_id", id}, + {"ApplicationName", ApplicationName}, + {"Created", DateTime.Now.ToUniversalTime()}, + {"Expires", DateTime.Now.AddMinutes(item.Timeout).ToUniversalTime()}, + {"LockDate", DateTime.Now.ToUniversalTime()}, + {"LockId", 0}, + {"Timeout", item.Timeout}, + {"Locked", false}, + {"SessionItems", items}, + {"Flags", 0} + }; + + + var query = Builders.Filter.And(Builders.Filter.Eq("_id", id), + Builders.Filter.Eq("ApplicationName", ApplicationName), + Builders.Filter.Lt("Expires", DateTime.Now.ToUniversalTime())); + sessionCollection.DeleteOneAsync(query); + sessionCollection.InsertOneAsync(insertDoc); + } + else + { + var query = Builders.Filter.And(Builders.Filter.Eq("_id", id), + Builders.Filter.Eq("ApplicationName", ApplicationName), + Builders.Filter.Eq("LockId", (int) lockId)); + + var update = Builders.Update + .Set("Expires", DateTime.Now.AddMinutes(item.Timeout).ToUniversalTime()) + .Set("SessionItems", items) + .Set("Locked", false); + + sessionCollection.UpdateOneAsync(query, update); + } + } + catch (Exception e) + { + if (WriteExceptionsToEventLog) + { + WriteToEventLog(e, "SetAndReleaseItemExclusive"); + throw new ProviderException(ExceptionMessage); + } + throw; + } + } + + /// + /// SessionStateProviderBase.GetItem + /// + public override SessionStateStoreData GetItem(HttpContext context, + string id, + out bool locked, + out TimeSpan lockAge, + out object lockId, + out SessionStateActions actionFlags) + { + return GetSessionStoreItem(false, context, id, out locked, + out lockAge, out lockId, out actionFlags); + } + + /// + /// SessionStateProviderBase.GetItemExclusive + /// + public override SessionStateStoreData GetItemExclusive(HttpContext context, + string id, + out bool locked, + out TimeSpan lockAge, + out object lockId, + out SessionStateActions actionFlags) + { + return GetSessionStoreItem(true, context, id, out locked, + out lockAge, out lockId, out actionFlags); + } + + /// + /// GetSessionStoreItem is called by both the GetItem and + /// GetItemExclusive methods. GetSessionStoreItem retrieves the + /// session data from the data source. If the lockRecord parameter + /// is true (in the case of GetItemExclusive), then GetSessionStoreItem + /// locks the record and sets a new LockId and LockDate. + /// + private SessionStateStoreData GetSessionStoreItem(bool lockRecord, + HttpContext context, + string id, + out bool locked, + out TimeSpan lockAge, + out object lockId, + out SessionStateActions actionFlags) + { + // Initial values for return value and out parameters. + SessionStateStoreData item = null; + lockAge = TimeSpan.Zero; + lockId = null; + locked = false; + actionFlags = 0; + var sessionCollection = GetSessionCollection(); + + // DateTime to check if current session item is expired. + // String to hold serialized SessionStateItemCollection. + var serializedItems = ""; + // True if a record is found in the database. + var foundRecord = false; + // True if the returned session item is expired and needs to be deleted. + var deleteData = false; + // Timeout value from the data store. + var timeout = 0; + + try + { + // lockRecord is true when called from GetItemExclusive and + // false when called from GetItem. + // Obtain a lock if possible. Ignore the record if it is expired. + FilterDefinition query; + if (lockRecord) + { + query = Builders.Filter.And(Builders.Filter.Eq("_id", id), + Builders.Filter.Eq("ApplicationName", ApplicationName), + Builders.Filter.Eq("Locked", false), + Builders.Filter.Gt("Expires", DateTime.Now.ToUniversalTime())); + + + var update = Builders.Update + .Set("LockDate", DateTime.Now.ToUniversalTime()) + .Set("Locked", true); + + + var result = sessionCollection.UpdateOneAsync(query, update).Result; + + locked = result.ModifiedCount == 0; + // DocumentsAffected == 0 == No record was updated because the record was locked or not found. + } + + // Retrieve the current session item information. + query = Builders.Filter.And(Builders.Filter.Eq("_id", id), + Builders.Filter.Eq("ApplicationName", ApplicationName)); + var results = sessionCollection.Find(query).FirstOrDefaultAsync().Result; + + if (results != null) + { + var expires = (DateTime) results["Expires"]; + + if (expires < DateTime.Now.ToUniversalTime()) + { + // The record was expired. Mark it as not locked. + locked = false; + // The session was expired. Mark the data for deletion. + deleteData = true; + } + else + { + foundRecord = true; + } + + serializedItems = results["SessionItems"].ToString(); + lockId = results["LockId"].AsInt32; + lockAge = DateTime.Now.ToUniversalTime().Subtract(results["LockDate"].ToUniversalTime()); + actionFlags = (SessionStateActions) results["Flags"].AsInt32; + timeout = results["Timeout"].AsInt32; + } + + // If the returned session item is expired, + // delete the record from the data source. + if (deleteData) + { + query = Builders.Filter.And(Builders.Filter.Eq("_id", id), + Builders.Filter.Eq("ApplicationName", ApplicationName)); + sessionCollection.DeleteOneAsync(query); + } + + // The record was not found. Ensure that locked is false. + if (!foundRecord) + { + locked = false; + } + + // If the record was found and you obtained a lock, then set + // the lockId, clear the actionFlags, + // and create the SessionStateStoreItem to return. + if (foundRecord && !locked) + { + lockId = (int) lockId + 1; + + query = Builders.Filter.And(Builders.Filter.Eq("_id", id), + Builders.Filter.Eq("ApplicationName", ApplicationName)); + + var update = Builders.Update + .Set("LockId", (int) lockId) + .Set("Flags", 0); + + sessionCollection.UpdateOneAsync(query, update); + + // If the actionFlags parameter is not InitializeItem, + // deserialize the stored SessionStateItemCollection. + if (actionFlags == SessionStateActions.InitializeItem) + { + item = CreateNewStoreData(context, (int) _config.Timeout.TotalMinutes); + } + else + { + item = Deserialize(context, serializedItems, timeout); + } + } + } + catch (Exception e) + { + if (WriteExceptionsToEventLog) + { + WriteToEventLog(e, "GetSessionStoreItem"); + throw new ProviderException(ExceptionMessage); + } + + throw; + } + + return item; + } + + private SessionStateStoreData Deserialize(HttpContext context, + string serializedItems, int timeout) + { + using (var ms = + new MemoryStream(Convert.FromBase64String(serializedItems))) + { + var sessionItems = + new SessionStateItemCollection(); + + if (ms.Length > 0) + { + using (var reader = new BinaryReader(ms)) + { + sessionItems = SessionStateItemCollection.Deserialize(reader); + } + } + + return new SessionStateStoreData(sessionItems, + SessionStateUtility.GetSessionStaticObjects(context), + timeout); + } + } + + public override void CreateUninitializedItem(HttpContext context, string id, int timeout) + { + var sessionCollection = GetSessionCollection(); + var doc = new BsonDocument + { + {"_id", id}, + {"ApplicationName", ApplicationName}, + {"Created", DateTime.Now.ToUniversalTime()}, + {"Expires", DateTime.Now.AddMinutes(timeout).ToUniversalTime()}, + {"LockDate", DateTime.Now.ToUniversalTime()}, + {"LockId", 0}, + {"Timeout", timeout}, + {"Locked", false}, + {"SessionItems", ""}, + {"Flags", 1} + }; + + try + { + var result = sessionCollection.InsertOneAsync(doc); + if (result.Status != TaskStatus.RanToCompletion) + { + if (result.Exception != null) + { + throw new Exception(result.Exception.Message); + } + } + } + catch (Exception e) + { + if (WriteExceptionsToEventLog) + { + WriteToEventLog(e, "CreateUninitializedItem"); + throw new ProviderException(ExceptionMessage); + } + + throw; + } + } + + /// + /// This is a helper function that writes exception detail to the + /// event log. Exceptions are written to the event log as a security + /// measure to ensure private database details are not returned to + /// browser. If a method does not return a status or Boolean + /// indicating the action succeeded or failed, the caller also + /// throws a generic exception. + /// + private void WriteToEventLog(Exception e, string action) + { + using (var log = new EventLog()) + { + log.Source = EventSource; + log.Log = EventLog; + + var message = + string.Format( + "An exception occurred communicating with the data source.\n\nAction: {0}\n\nException: {1}", + action, e); + + log.WriteEntry(message); + } + } + + public override void Dispose() + { + } + + public override void EndRequest(HttpContext context) + { + } + + public override void InitializeRequest(HttpContext context) + { + } + + public override void ReleaseItemExclusive(HttpContext context, string id, object lockId) + { + var sessionCollection = GetSessionCollection(); + + var query = Builders.Filter.And(Builders.Filter.Eq("_id", id), + Builders.Filter.Eq("ApplicationName", ApplicationName), + Builders.Filter.Eq("LockId", (int) lockId)); + + var update = Builders.Update + .Set("Expires", DateTime.Now.AddMinutes(_config.Timeout.TotalMinutes).ToUniversalTime()) + .Set("Locked", false); + + try + { + sessionCollection.UpdateOneAsync(query, update); + } + catch (Exception e) + { + if (WriteExceptionsToEventLog) + { + WriteToEventLog(e, "ReleaseItemExclusive"); + throw new ProviderException(ExceptionMessage); + } + throw; + } + } + + public override void RemoveItem(HttpContext context, string id, object lockId, SessionStateStoreData item) + { + var sessionCollection = GetSessionCollection(); + + var query = Builders.Filter.And(Builders.Filter.Eq("_id", id), + Builders.Filter.Eq("ApplicationName", ApplicationName), + Builders.Filter.Eq("LockId", (int) lockId)); + + try + { + sessionCollection.DeleteOneAsync(query); + } + catch (Exception e) + { + if (WriteExceptionsToEventLog) + { + WriteToEventLog(e, "RemoveItem"); + throw new ProviderException(ExceptionMessage); + } + + throw; + } + } + + public override void ResetItemTimeout(HttpContext context, string id) + { + var sessionCollection = GetSessionCollection(); + var query = Builders.Filter.And(Builders.Filter.Eq("_id", id), + Builders.Filter.Eq("ApplicationName", ApplicationName)); + var update = Builders.Update.Set("Expires", + DateTime.Now.AddMinutes(_config.Timeout.TotalMinutes).ToUniversalTime()); + + try + { + sessionCollection.UpdateOneAsync(query, update); + } + catch (Exception e) + { + if (WriteExceptionsToEventLog) + { + WriteToEventLog(e, "ResetItemTimeout"); + throw new ProviderException(ExceptionMessage); + } + throw; + } + } + } +} \ No newline at end of file diff --git a/MongoSessionStateStore/MongoSessionStateStore.csproj b/MongoSessionStateStore/MongoSessionStateStore.csproj index 2945b40..1cd42e8 100644 --- a/MongoSessionStateStore/MongoSessionStateStore.csproj +++ b/MongoSessionStateStore/MongoSessionStateStore.csproj @@ -1,64 +1,74 @@ - - - - Debug - AnyCPU - 8.0.30703 - 2.0 - {2F0DD54F-6EF0-4DDD-9737-6D174B9E4E9D} - Library - Properties - MongoSessionStateStore - MongoSessionStateStore - v4.0 - 512 - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - False - .\MongoDB.Bson.dll - - - False - .\MongoDB.Driver.dll - - - - - - - - - - - - - - - - - + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {2F0DD54F-6EF0-4DDD-9737-6D174B9E4E9D} + Library + Properties + MongoSessionStateStore + MongoSessionStateStore + v4.6 + 512 + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + + ..\packages\MongoDB.Bson.2.0.1\lib\net45\MongoDB.Bson.dll + True + + + ..\packages\MongoDB.Driver.2.0.1\lib\net45\MongoDB.Driver.dll + True + + + ..\packages\MongoDB.Driver.Core.2.0.1\lib\net45\MongoDB.Driver.Core.dll + True + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MongoSessionStateStore/Properties/AssemblyInfo.cs b/MongoSessionStateStore/Properties/AssemblyInfo.cs index db69900..3c8448d 100644 --- a/MongoSessionStateStore/Properties/AssemblyInfo.cs +++ b/MongoSessionStateStore/Properties/AssemblyInfo.cs @@ -1,36 +1,36 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("MongoSessionStateStore")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Microsoft")] -[assembly: AssemblyProduct("MongoSessionStateStore")] -[assembly: AssemblyCopyright("Copyright © Microsoft 2011")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("53451529-6eba-4e18-9307-96b5cc6cd2e5")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.1.0.0")] -[assembly: AssemblyFileVersion("1.1.0.0")] +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("MongoSessionStateStore")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Microsoft")] +[assembly: AssemblyProduct("MongoSessionStateStore")] +[assembly: AssemblyCopyright("Copyright © Microsoft 2011")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("53451529-6eba-4e18-9307-96b5cc6cd2e5")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("2.1.0.0")] +[assembly: AssemblyFileVersion("2.1.0.0")] diff --git a/MongoSessionStateStore/packages.config b/MongoSessionStateStore/packages.config new file mode 100644 index 0000000..afe6905 --- /dev/null +++ b/MongoSessionStateStore/packages.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file