From b8aedff3148dfa01aa9fa4eb1f8b436e7558b361 Mon Sep 17 00:00:00 2001 From: Kendall Bennett Date: Sun, 17 May 2015 13:44:13 -0700 Subject: [PATCH] Got rid of disposing of Couchbase buckets since that is not desired in 2.1, changed references from 'client' to 'bucket', added clear documentation to the session state store based on our private SqlSessionStateStore code and cleaned up code formatting using Resharper and a solution specific settings file. --- Couchbase.AspNet.sln.DotSettings | 176 +++++++ Couchbase.AspNet/Couchbase.AspNet.csproj | 4 +- ...ntFactory.cs => CouchbaseBucketFactory.cs} | 94 ++-- ...tFactory.cs => ICouchbaseBucketFactory.cs} | 84 +-- .../CouchbaseOutputCacheProvider.cs | 47 +- Couchbase.AspNet/ProviderHelper.cs | 71 ++- .../CouchbaseSessionStateProvider.cs | 480 ++++++++++++------ CouchbaseAspNetSample/Web.config | 4 +- README.md | 12 +- 9 files changed, 696 insertions(+), 276 deletions(-) create mode 100644 Couchbase.AspNet.sln.DotSettings rename Couchbase.AspNet/{CouchbaseClientFactory.cs => CouchbaseBucketFactory.cs} (69%) rename Couchbase.AspNet/{ICouchbaseClientFactory.cs => ICouchbaseBucketFactory.cs} (67%) diff --git a/Couchbase.AspNet.sln.DotSettings b/Couchbase.AspNet.sln.DotSettings new file mode 100644 index 0000000..90187c3 --- /dev/null +++ b/Couchbase.AspNet.sln.DotSettings @@ -0,0 +1,176 @@ + + Enabled + Left + 200 + ON_ENTER + True + True + True + VISIBLE_FILES + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + HINT + ERROR + HINT + DO_NOT_SHOW + DO_NOT_SHOW + SUGGESTION + DO_NOT_SHOW + HINT + <?xml version="1.0" encoding="utf-16"?><Profile name="Test"><CSUseVar><BehavourStyle>CAN_CHANGE_BOTH</BehavourStyle><LocalVariableStyle>IMPLICIT_EXCEPT_PRIMITIVE_TYPES</LocalVariableStyle><ForeachVariableStyle>ALWAYS_IMPLICIT</ForeachVariableStyle></CSUseVar></Profile> + + True + False + True + True + True + True + True + END_OF_LINE + 1 + 1 + 0 + 0 + 0 + END_OF_LINE + 1 + ALWAYS_ADD + ALWAYS_ADD + ALWAYS_ADD + ALWAYS_ADD + ALWAYS_ADD + ALWAYS_ADD + True + True + END_OF_LINE + 1 + 1 + True + END_OF_LINE + False + False + False + False + False + True + True + False + LINE_BREAK + False + True + False + False + True + False + True + CHOP_IF_LONG + CHOP_IF_LONG + True + 160 + CHOP_ALWAYS + CHOP_ALWAYS + CHOP_IF_LONG + CHOP_ALWAYS + ZeroIndent + System + System.Linq + + True + + True + Copyright (C) 2013-$CURRENT_YEAR$ AMain.com, Inc. +All Rights Reserved + AB + AJAX + API + BO + CB + CM + CRLF + CS + CSS + DB + DOM + FA + FB + FK + FS + GTIN + HTML + ID + IP + ISO + JB + JS + JSON + KMP + LF + MAP + MD + PD + PHP + PO + POPHP + QC + SEO + SMTP + SQL + SSL + UN + UOW + UPS + UR + URL + USPS + WCF + WS + <Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /> + <Policy><Descriptor Staticness="Instance" AccessRightKinds="Protected, ProtectedInternal" Description="Fields (protected)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /></Policy> + ID + URL + XHR + BOTH_SIDES + Sources + True + True + True + False + False + 1 + True + [0,165.5](1024,1099) + Xml + NEVER + NotOverridden + NotOverridden + NotOverridden + NotOverridden + NotOverridden + NotOverridden + NotOverridden + NotOverridden + NotOverridden + NotOverridden \ No newline at end of file diff --git a/Couchbase.AspNet/Couchbase.AspNet.csproj b/Couchbase.AspNet/Couchbase.AspNet.csproj index ac3a774..a481fc4 100644 --- a/Couchbase.AspNet/Couchbase.AspNet.csproj +++ b/Couchbase.AspNet/Couchbase.AspNet.csproj @@ -70,9 +70,9 @@ - + - + diff --git a/Couchbase.AspNet/CouchbaseClientFactory.cs b/Couchbase.AspNet/CouchbaseBucketFactory.cs similarity index 69% rename from Couchbase.AspNet/CouchbaseClientFactory.cs rename to Couchbase.AspNet/CouchbaseBucketFactory.cs index bd5066c..5a45767 100644 --- a/Couchbase.AspNet/CouchbaseClientFactory.cs +++ b/Couchbase.AspNet/CouchbaseBucketFactory.cs @@ -1,45 +1,51 @@ -using System.Collections.Specialized; -using Couchbase.Core; - -namespace Couchbase.AspNet -{ - public sealed class CouchbaseClientFactory : ICouchbaseClientFactory - { - public IBucket Create(string name, NameValueCollection config, out bool disposeClient) - { - // This client should be disposed of as it is not shared - disposeClient = true; - - // Get the bucket name to use from the configuration file and use a specific bucket if specified - var bucketName = ProviderHelper.GetAndRemove(config, "bucket", false); - if (!string.IsNullOrEmpty(bucketName)) - return ClusterHelper.GetBucket(bucketName); - - // If no bucket is specified, simply use the default bucket (which will be the first in the list) - return ClusterHelper.Get().OpenBucket(); - } - } -} - -#region [ License information ] -/* ************************************************************ - * - * @author Couchbase - * @copyright 2012 Couchbase, Inc. - * @copyright 2012 Attila Kiskó, enyim.com - * @copyright 2012 Good Time Hobbies, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * ************************************************************/ +using System.Collections.Specialized; +using Couchbase.Core; + +namespace Couchbase.AspNet +{ + public sealed class CouchbaseBucketFactory : ICouchbaseBucketFactory + { + /// + /// Returns a Couchbase bucket or create one if it does not exist + /// + /// Name of the section from the configuration file + /// Configuration section information from the config file + /// Instance of the couchbase bucket to use + public IBucket GetBucket( + string name, + NameValueCollection config) + { + // Get the bucket name to use from the configuration file and use a specific bucket if specified + var bucketName = ProviderHelper.GetAndRemove(config, "bucket", false); + if (!string.IsNullOrEmpty(bucketName)) + return ClusterHelper.GetBucket(bucketName); + + // If no bucket is specified, simply use the default bucket (which will be the first in the list) + return ClusterHelper.Get().OpenBucket(); + } + } +} + +#region [ License information ] +/* ************************************************************ + * + * @author Couchbase + * @copyright 2012 Couchbase, Inc. + * @copyright 2012 Attila Kiskó, enyim.com + * @copyright 2012 Good Time Hobbies, Inc. + * @copyright 2015 AMain.com, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ************************************************************/ #endregion \ No newline at end of file diff --git a/Couchbase.AspNet/ICouchbaseClientFactory.cs b/Couchbase.AspNet/ICouchbaseBucketFactory.cs similarity index 67% rename from Couchbase.AspNet/ICouchbaseClientFactory.cs rename to Couchbase.AspNet/ICouchbaseBucketFactory.cs index 02a4a81..60c3993 100644 --- a/Couchbase.AspNet/ICouchbaseClientFactory.cs +++ b/Couchbase.AspNet/ICouchbaseBucketFactory.cs @@ -1,42 +1,42 @@ -using System.Collections.Specialized; -using Couchbase.Core; - -namespace Couchbase.AspNet -{ - public interface ICouchbaseClientFactory - { - /// - /// Returns a Couchbase client. This will be called by the provider's Initialize method. Note - /// that the instance of the client returned will be owned by the called, and will be disposed. - /// So make sure you don't return a shared instance, but create a new one. - /// - /// Name of the section from the configuration file - /// Configuration section information from the config file - /// True if the client should be disposed of or not - /// Instance of the couchbase client to use - IBucket Create(string name, NameValueCollection config, out bool disposeClient); - } -} - -#region [ License information ] -/* ************************************************************ - * - * @author Couchbase - * @copyright 2012 Couchbase, Inc. - * @copyright 2012 Attila Kiskó, enyim.com - * @copyright 2012 Good Time Hobbies, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * ************************************************************/ -#endregion +using System.Collections.Specialized; +using Couchbase.Core; + +namespace Couchbase.AspNet +{ + public interface ICouchbaseBucketFactory + { + /// + /// Returns a Couchbase bucket or create one if it does not exist + /// + /// Name of the section from the configuration file + /// Configuration section information from the config file + /// Instance of the couchbase bucket to use + IBucket GetBucket( + string name, + NameValueCollection config); + } +} + +#region [ License information ] +/* ************************************************************ + * + * @author Couchbase + * @copyright 2012 Couchbase, Inc. + * @copyright 2012 Attila Kiskó, enyim.com + * @copyright 2012 Good Time Hobbies, Inc. + * @copyright 2015 AMain.com, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ************************************************************/ +#endregion \ No newline at end of file diff --git a/Couchbase.AspNet/OutputCache/CouchbaseOutputCacheProvider.cs b/Couchbase.AspNet/OutputCache/CouchbaseOutputCacheProvider.cs index 75ee964..5245d78 100644 --- a/Couchbase.AspNet/OutputCache/CouchbaseOutputCacheProvider.cs +++ b/Couchbase.AspNet/OutputCache/CouchbaseOutputCacheProvider.cs @@ -10,9 +10,15 @@ namespace Couchbase.AspNet.OutputCache { public class CouchbaseOutputCacheProvider : OutputCacheProvider { - private IBucket client; - private bool disposeClient; - private static readonly string Prefix = (System.Web.Hosting.HostingEnvironment.SiteName ?? String.Empty).Replace(" ", "-") + "+" + System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath + "cache-"; + private IBucket _bucket; + + /// + /// Defines the prefix for the actual cache data stored in the Couchbase bucket. Must be unique to ensure it does not conflict with + /// other applications that might be using the Couchbase bucket. + /// + private static readonly string _prefix = + (System.Web.Hosting.HostingEnvironment.SiteName ?? string.Empty).Replace(" ", "-") + "+" + + System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath + "cache-"; /// /// Function to initialize the provider @@ -23,9 +29,13 @@ public class CouchbaseOutputCacheProvider : OutputCacheProvider string name, NameValueCollection config) { + // Initialize the base class base.Initialize(name, config); - client = ProviderHelper.GetClient(name, config, () => (ICouchbaseClientFactory)new CouchbaseClientFactory(), out disposeClient); + // Create our Couchbase bucket instance + _bucket = ProviderHelper.GetBucket(name, config); + + // Make sure no extra attributes are included ProviderHelper.CheckForUnknownAttributes(config); } @@ -35,10 +45,10 @@ public class CouchbaseOutputCacheProvider : OutputCacheProvider /// /// Key to sanitize /// Sanitized key - private string SanitizeKey( + private static string SanitizeKey( string key) { - return Prefix + Convert.ToBase64String(Encoding.UTF8.GetBytes(key), Base64FormattingOptions.None); + return _prefix + Convert.ToBase64String(Encoding.UTF8.GetBytes(key), Base64FormattingOptions.None); } /// @@ -75,17 +85,16 @@ public class CouchbaseOutputCacheProvider : OutputCacheProvider // We should only store the item if it's not in the cache. So try to add it and if it // succeeds, return the value we just stored var expiration = ToExpiration(utcExpiry); - if (client.Insert(key, Serialize(entry), expiration).Success) + if (_bucket.Insert(key, Serialize(entry), expiration).Success) return entry; // If it's in the cache we should return it - var retval = DeSerialize(client.Get(key).Value); + var retval = DeSerialize(_bucket.Get(key).Value); // If the item got evicted between the Add and the Get (very rare) we store it anyway, // but this time with Set to make sure it always gets into the cache - if (retval == null) - { - client.Insert(key, entry, expiration); + if (retval == null) { + _bucket.Insert(key, entry, expiration); retval = entry; } @@ -103,7 +112,7 @@ public class CouchbaseOutputCacheProvider : OutputCacheProvider public override object Get( string key) { - var result = client.Get(SanitizeKey(key)); + var result = _bucket.Get(SanitizeKey(key)); return DeSerialize(result.Value); } @@ -114,7 +123,7 @@ public class CouchbaseOutputCacheProvider : OutputCacheProvider public override void Remove( string key) { - client.Remove(SanitizeKey(key)); + _bucket.Remove(SanitizeKey(key)); } /// @@ -128,7 +137,7 @@ public class CouchbaseOutputCacheProvider : OutputCacheProvider object entry, DateTime utcExpiry) { - client.Insert(SanitizeKey(key), Serialize(entry), ToExpiration(utcExpiry)); + _bucket.Insert(SanitizeKey(key), Serialize(entry), ToExpiration(utcExpiry)); } /// @@ -139,8 +148,7 @@ public class CouchbaseOutputCacheProvider : OutputCacheProvider private byte[] Serialize( object value) { - using (var ms = new MemoryStream()) - { + using (var ms = new MemoryStream()) { new BinaryFormatter().Serialize(ms, value); return ms.ToArray(); } @@ -154,12 +162,10 @@ public class CouchbaseOutputCacheProvider : OutputCacheProvider private object DeSerialize( byte[] bytes) { - if (bytes == null) - { + if (bytes == null) { return null; } - using (var ms = new MemoryStream(bytes)) - { + using (var ms = new MemoryStream(bytes)) { return new BinaryFormatter().Deserialize(ms); } } @@ -173,6 +179,7 @@ public class CouchbaseOutputCacheProvider : OutputCacheProvider * @copyright 2012 Couchbase, Inc. * @copyright 2012 Attila Kiskó, enyim.com * @copyright 2012 Good Time Hobbies, Inc. + * @copyright 2015 AMain.com, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Couchbase.AspNet/ProviderHelper.cs b/Couchbase.AspNet/ProviderHelper.cs index 42c7c3e..8d17cc9 100644 --- a/Couchbase.AspNet/ProviderHelper.cs +++ b/Couchbase.AspNet/ProviderHelper.cs @@ -6,48 +6,76 @@ namespace Couchbase.AspNet { internal static class ProviderHelper { - public static string GetAndRemove(NameValueCollection nvc, string name, bool required) + /// + /// Gets and removes an value from the configuration section + /// + /// Name value collection to examine + /// Name of the value to retrieve + /// True if the value is required, false if optional + /// Value returned from the configuration section, null if not found + public static string GetAndRemove( + NameValueCollection config, + string name, + bool required) { - var tmp = nvc[name]; - if (tmp == null) { + var value = config[name]; + if (value == null) { if (required) throw new System.Configuration.ConfigurationErrorsException("Missing parameter: " + name); } else { - nvc.Remove(name); + config.Remove(name); } - return tmp; + return value; } - public static void CheckForUnknownAttributes(NameValueCollection nvc) + /// + /// Helper to make sure there are no unknown attributes left over in the config section + /// + /// Name value collection to examine + public static void CheckForUnknownAttributes( + NameValueCollection config) { - if (nvc.Count > 0) - throw new System.Configuration.ConfigurationErrorsException("Unknown parameter: " + nvc.Keys[0]); + if (config.Count > 0) + throw new System.Configuration.ConfigurationErrorsException("Unknown parameter: " + config.Keys[0]); } - public static IBucket GetClient(string name, NameValueCollection config, Func createDefault, out bool disposeClient) + /// + /// Gets a Couchbase bucket using the couchbase factory configured in the Web.config file. If the factory is + /// not defined in the Web.config file section, then the default factory is used. + /// + /// Name of the bucket + /// Name value collection from config file + /// Instance of the couchbase bucket to use + public static IBucket GetBucket( + string name, + NameValueCollection config) { - var factory = GetFactoryInstance(ProviderHelper.GetAndRemove(config, "factory", false), createDefault); + var factory = GetFactoryInstance(GetAndRemove(config, "factory", false)); System.Diagnostics.Debug.Assert(factory != null, "factory == null"); - - return factory.Create(name, config, out disposeClient); + return factory.GetBucket(name, config); } - private static ICouchbaseClientFactory GetFactoryInstance(string typeName, Func createDefault) + /// + /// Internal function to get the couchbase factory + /// + /// Name of the factory type extracted from the config file + /// Instance of the couchbase factory to use + private static ICouchbaseBucketFactory GetFactoryInstance( + string typeName) { - if (String.IsNullOrEmpty(typeName)) - return createDefault(); + // If the factory type is not provided, use the default one + if (string.IsNullOrEmpty(typeName)) + return new CouchbaseBucketFactory(); + // Attempt to create the custom factory instance var type = Type.GetType(typeName, false); if (type == null) throw new System.Configuration.ConfigurationErrorsException("Could not load type: " + typeName); - - if (!typeof(ICouchbaseClientFactory).IsAssignableFrom(type)) - throw new System.Configuration.ConfigurationErrorsException("Type '" + typeName + "' must implement IMemcachedClientFactory"); - - return (ICouchbaseClientFactory )Activator.CreateInstance(type); + if (!typeof(ICouchbaseBucketFactory).IsAssignableFrom(type)) + throw new System.Configuration.ConfigurationErrorsException("Type '" + typeName + "' must implement ICouchbaseBucketFactory"); + return (ICouchbaseBucketFactory)Activator.CreateInstance(type); } - } } @@ -57,6 +85,7 @@ private static ICouchbaseClientFactory GetFactoryInstance(string typeName, Func< * @author Couchbase * @copyright 2012 Couchbase, Inc. * @copyright 2012 Attila Kiskó, enyim.com + * @copyright 2015 AMain.com, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Couchbase.AspNet/SessionState/CouchbaseSessionStateProvider.cs b/Couchbase.AspNet/SessionState/CouchbaseSessionStateProvider.cs index 8008e84..8fc8348 100644 --- a/Couchbase.AspNet/SessionState/CouchbaseSessionStateProvider.cs +++ b/Couchbase.AspNet/SessionState/CouchbaseSessionStateProvider.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Specialized; using System.Web.SessionState; using System.Web; using System.IO; @@ -8,34 +9,92 @@ namespace Couchbase.AspNet.SessionState { - public class CouchbaseSessionStateProvider : SessionStateStoreProviderBase - { - private IBucket client; - private bool disposeClient; - private static bool exclusiveAccess; - - public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config) - { + public class CouchbaseSessionStateProvider : SessionStateStoreProviderBase + { + private IBucket _bucket; + private static bool _exclusiveAccess; + + /// + /// Function to initialize the provider + /// + /// Name of the element in the configuration file + /// Configuration values for the provider from the Web.config file + public override void Initialize( + string name, + NameValueCollection config) + { // Initialize the base class - base.Initialize(name, config); + base.Initialize(name, config); - // Create our Couchbase client instance - client = ProviderHelper.GetClient(name, config, () => (ICouchbaseClientFactory)new CouchbaseClientFactory(), out disposeClient); + // Create our Couchbase bucket instance + _bucket = ProviderHelper.GetBucket(name, config); // By default use exclusive session access. But allow it to be overridden in the config file var exclusive = ProviderHelper.GetAndRemove(config, "exclusiveAccess", false) ?? "true"; - exclusiveAccess = (String.Compare(exclusive, "true", true) == 0); + _exclusiveAccess = (string.Compare(exclusive, "true", StringComparison.OrdinalIgnoreCase) == 0); // Make sure no extra attributes are included - ProviderHelper.CheckForUnknownAttributes(config); - } + ProviderHelper.CheckForUnknownAttributes(config); + } - public override SessionStateStoreData CreateNewStoreData(HttpContext context, int timeout) - { - return new SessionStateStoreData(new SessionStateItemCollection(), SessionStateUtility.GetSessionStaticObjects(context), timeout); - } + /// + /// Handle disposing of the session-state store object + /// + public override void Dispose() + { + } - public override void CreateUninitializedItem(HttpContext context, string id, int timeout) + /// + /// Takes as input the HttpContext instance for the current request and performs any + /// initialization required by the session-state store provider. + /// + /// HttpContext for the current request + public override void InitializeRequest( + HttpContext context) + { + } + + /// + /// Takes as input the HttpContext instance for the current request and performs any + /// cleanup required by the session-state store provider. + /// + /// HttpContext for the current request + public override void EndRequest( + HttpContext context) + { + } + + /// + /// Takes as input the HttpContext instance for the current request and the Timeout value for the + /// current session, and returns a new SessionStateStoreData object with an empty + /// ISessionStateItemCollection object, an HttpStaticObjectsCollection collection, and the + /// specified Timeout value. The HttpStaticObjectsCollection instance for the ASP.NET application + /// can be retrieved using the GetSessionStaticObjects method. + /// + /// HttpContext for the current request + /// Timeout value for the session + /// New SessionStateStoreData object for storing the session state data + public override SessionStateStoreData CreateNewStoreData( + HttpContext context, + int timeout) + { + return new SessionStateStoreData(new SessionStateItemCollection(), + SessionStateUtility.GetSessionStaticObjects(context), + timeout); + } + + /// + /// Creates an uninitialized item in the database. This is only used for cookieless sessions + /// regenerateExpiredSessionId attribute is set to true, which causes SessionStateModule to + /// generate a new SessionID value when an expired session ID is encountered. + /// + /// HttpContext for the current request + /// Session ID for the new session + /// Timeout value for the session + public override void CreateUninitializedItem( + HttpContext context, + string id, + int timeout) { var e = new SessionStateItem { Data = new SessionStateItemCollection(), @@ -44,44 +103,83 @@ public override void CreateUninitializedItem(HttpContext context, string id, int Timeout = timeout }; - e.Save(client, id, false, false); + e.Save(_bucket, id, false, false); } - public override void Dispose() - { - if (disposeClient) { - client.Dispose(); - } - } - - public override void EndRequest(HttpContext context) { } - - public override SessionStateStoreData GetItem(HttpContext context, string id, out bool locked, out TimeSpan lockAge, out object lockId, out SessionStateActions actions) + /// + /// Returns read-only session-state data from the session data store + /// + /// HttpContext for the current request + /// Session ID for the session + /// Returns true if the session item is locked, otherwise false + /// Returns the amount of time that the item has been locked + /// Returns lock identifier for the current request + /// Indicates whether the current sessions is an uninitialized, cookieless session + /// SessionStateStoreData object containing the session state data + public override SessionStateStoreData GetItem( + HttpContext context, + string id, + out bool locked, + out TimeSpan lockAge, + out object lockId, + out SessionStateActions actions) { - var e = Get(client, context, false, id, out locked, out lockAge, out lockId, out actions); - - return (e == null) - ? null - : e.ToStoreData(context); + var e = GetSessionStoreItem(_bucket, context, false, id, out locked, out lockAge, out lockId, out actions); + return (e == null) ? null : e.ToStoreData(context); } - public override SessionStateStoreData GetItemExclusive(HttpContext context, string id, out bool locked, out TimeSpan lockAge, out object lockId, out SessionStateActions actions) + /// + /// Returns read-write session-state data from the session data store + /// + /// HttpContext for the current request + /// Session ID for the session + /// Returns true if the session item is locked, otherwise false + /// Returns the amount of time that the item has been locked + /// Returns lock identifier for the current request + /// Indicates whether the current sessions is an uninitialized, cookieless session + /// SessionStateStoreData object containing the session state data + public override SessionStateStoreData GetItemExclusive( + HttpContext context, + string id, + out bool locked, + out TimeSpan lockAge, + out object lockId, + out SessionStateActions actions) { - var e = Get(client, context, true, id, out locked, out lockAge, out lockId, out actions); - - return (e == null) - ? null - : e.ToStoreData(context); + var e = GetSessionStoreItem(_bucket, context, true, id, out locked, out lockAge, out lockId, out actions); + return (e == null) ? null : e.ToStoreData(context); } - public static SessionStateItem Get(IBucket client, HttpContext context, bool acquireLock, string id, out bool locked, out TimeSpan lockAge, out object lockId, out SessionStateActions actions) + /// + /// Retrieves the session data from the data source. If the lockRecord parameter is true + /// (in the case of GetItemExclusive), then the record is locked and we return a new lockId + /// and lockAge. + /// + /// Reference to the couchbase bucket we are using + /// HttpContext for the current request + /// True to aquire the lock, false to not aquire it + /// Session ID for the session + /// Returns true if the session item is locked, otherwise false + /// Returns the amount of time that the item has been locked + /// Returns lock identifier for the current request + /// Indicates whether the current sessions is an uninitialized, cookieless session + /// SessionStateItem object containing the session state data + public static SessionStateItem GetSessionStoreItem( + IBucket bucket, + HttpContext context, + bool acquireLock, + string id, + out bool locked, + out TimeSpan lockAge, + out object lockId, + out SessionStateActions actions) { locked = false; lockId = null; lockAge = TimeSpan.Zero; actions = SessionStateActions.None; - var e = SessionStateItem.Load(client, id, false); + var e = SessionStateItem.Load(bucket, id, false); if (e == null) return null; @@ -96,12 +194,12 @@ public static SessionStateItem Get(IBucket client, HttpContext context, bool acq actions = e.Flag; - e.LockId = exclusiveAccess ? e.HeadCas : 0; + e.LockId = _exclusiveAccess ? e.HeadCas : 0; e.LockTime = DateTime.UtcNow; e.Flag = SessionStateActions.None; // try to update the item in the store - if (e.Save(client, id, true, exclusiveAccess)) { + if (e.Save(bucket, id, true, _exclusiveAccess)) { locked = true; lockId = e.LockId; @@ -109,7 +207,7 @@ public static SessionStateItem Get(IBucket client, HttpContext context, bool acq } // it has been modified between we loaded and tried to save it - e = SessionStateItem.Load(client, id, false); + e = SessionStateItem.Load(bucket, id, false); if (e == null) return null; } @@ -123,17 +221,68 @@ public static SessionStateItem Get(IBucket client, HttpContext context, bool acq return acquireLock ? null : e; } - public override void InitializeRequest(HttpContext context) + /// + /// Updates the session-item information in the session-state data store with values + /// from the current request, and clears the lock on the data + /// + /// HttpContext for the current request + /// Session ID for the session + /// The session state item to be stored + /// The lock identifier for the current request + /// True if this is a new session item, false if it is an existing item + public override void SetAndReleaseItemExclusive( + HttpContext context, + string id, + SessionStateStoreData item, + object lockId, + bool newItem) { + SessionStateItem e; + do { + if (!newItem) { + var tmp = (ulong)lockId; + + // Load the entire item with CAS (need the DataCas value also for the save) + e = SessionStateItem.Load(_bucket, id, false); + + // if we're expecting an existing item, but + // it's not in the cache + // or it's locked by someone else, then quit + if (e == null || e.LockId != tmp) { + return; + } + } else { + // Create a new item if it requested + e = new SessionStateItem(); + } + + // Set the new data and reset the locks + e.Timeout = item.Timeout; + e.Data = (SessionStateItemCollection)item.Items; + e.Flag = SessionStateActions.None; + e.LockId = 0; + e.LockTime = DateTime.MinValue; + + // Attempt to save with CAS and loop around if it fails + } while (!e.Save(_bucket, id, false, _exclusiveAccess && !newItem)); } - public override void ReleaseItemExclusive(HttpContext context, string id, object lockId) + /// + /// Releases a lock on an item in the session data store + /// + /// HttpContext for the current request + /// Session ID for the session + /// The lock identifier for the current request + public override void ReleaseItemExclusive( + HttpContext context, + string id, + object lockId) { var tmp = (ulong)lockId; SessionStateItem e; do { // Load the header for the item with CAS - e = SessionStateItem.Load(client, id, true); + e = SessionStateItem.Load(_bucket, id, true); // Bail if the entry does not exist, or the lock ID does not match our lock ID if (e == null || e.LockId != tmp) { @@ -143,133 +292,153 @@ public override void ReleaseItemExclusive(HttpContext context, string id, object // Attempt to clear the lock for this item and loop around until we succeed e.LockId = 0; e.LockTime = DateTime.MinValue; - } while (!e.Save(client, id, true, exclusiveAccess)); + } while (!e.Save(_bucket, id, true, _exclusiveAccess)); } - public override void RemoveItem(HttpContext context, string id, object lockId, SessionStateStoreData item) + /// + /// Deletes item data from the session data store + /// + /// HttpContext for the current request + /// Session ID for the session + /// The lock identifier for the current request + /// Item to be deleted (ignored) + public override void RemoveItem( + HttpContext context, + string id, + object lockId, + SessionStateStoreData item) { var tmp = (ulong)lockId; - var e = SessionStateItem.Load(client, id, true); + var e = SessionStateItem.Load(_bucket, id, true); if (e != null && e.LockId == tmp) { - SessionStateItem.Remove(client, id); + SessionStateItem.Remove(_bucket, id); } } - public override void ResetItemTimeout(HttpContext context, string id) + /// + /// Updates the expiration date and time of an item in the session data store + /// + /// HttpContext for the current request + /// Session ID for the session + public override void ResetItemTimeout( + HttpContext context, + string id) { SessionStateItem e; do { // Load the item with CAS - e = SessionStateItem.Load(client, id, false); + e = SessionStateItem.Load(_bucket, id, false); if (e == null) { break; } // Try to save with CAS, and loop around until we succeed - } while (!e.Save(client, id, false, exclusiveAccess)); - } - - public override void SetAndReleaseItemExclusive(HttpContext context, string id, SessionStateStoreData item, object lockId, bool newItem) - { - SessionStateItem e = null; - do { - if (!newItem) { - var tmp = (ulong)lockId; - - // Load the entire item with CAS (need the DataCas value also for the save) - e = SessionStateItem.Load(client, id, false); - - // if we're expecting an existing item, but - // it's not in the cache - // or it's locked by someone else, then quit - if (e == null || e.LockId != tmp) { - return; - } - } else { - // Create a new item if it requested - e = new SessionStateItem(); - } - - // Set the new data and reset the locks - e.Timeout = item.Timeout; - e.Data = (SessionStateItemCollection)item.Items; - e.Flag = SessionStateActions.None; - e.LockId = 0; - e.LockTime = DateTime.MinValue; - - // Attempt to save with CAS and loop around if it fails - } while (!e.Save(client, id, false, exclusiveAccess && !newItem)); + } while (!e.Save(_bucket, id, false, _exclusiveAccess)); } - public override bool SetItemExpireCallback(SessionStateItemExpireCallback expireCallback) + /// + /// Function to set the session data expiration callback. Since this is not supported + /// by our database, we simply return false here and do manual session cleanup. + /// + /// Session expiration callback to set + /// False, since we don't support this feature + public override bool SetItemExpireCallback( + SessionStateItemExpireCallback expireCallback) { return false; } - #region [ SessionStateItem ] + #region [ SessionStateItem ] + /// + /// Internal class for handling the storage of the session items in Couchbase + /// public class SessionStateItem { - private static readonly string HeaderPrefix = (System.Web.Hosting.HostingEnvironment.SiteName ?? String.Empty).Replace(" ", "-") + "+" + System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath + "info-"; - private static readonly string DataPrefix = (System.Web.Hosting.HostingEnvironment.SiteName ?? String.Empty).Replace(" ", "-") + "+" + System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath + "data-"; + /// + /// Defines the prefix for header data in the Couchbase bucket. Must be unique to ensure it does not conflict with + /// other applications that might be using the Couchbase bucket. + /// + private static readonly string _headerPrefix = + (System.Web.Hosting.HostingEnvironment.SiteName ?? string.Empty).Replace(" ", "-") + "+" + + System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath + "info-"; + + /// + /// Defines the prefix for the actual session store data stored in the Couchbase bucket. Must also be unique for + /// the same reasons outlined above. + /// + private static readonly string _dataPrefix = + (System.Web.Hosting.HostingEnvironment.SiteName ?? string.Empty).Replace(" ", "-") + "+" + + System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath + "data-"; public SessionStateItemCollection Data; public SessionStateActions Flag; public ulong LockId; public DateTime LockTime; - // this is in minutes + // Timeout value for the session store item (defined in minutes) public int Timeout; public ulong HeadCas; public ulong DataCas; - private void SaveHeader(MemoryStream ms) + /// + /// Writes the header to the stream + /// + /// Stream to write the header to + private void WriteHeader( + Stream s) { var p = new Pair( - (byte)1, - new Triplet( - (byte)Flag, - Timeout, - new Pair( - LockId, - LockTime.ToBinary() - ) - ) - ); - - new ObjectStateFormatter().Serialize(ms, p); + (byte)1, + new Triplet( + (byte)Flag, + Timeout, + new Pair( + LockId, + LockTime.ToBinary())) + ); + new ObjectStateFormatter().Serialize(s, p); } - public bool Save(IBucket client, string id, bool metaOnly, bool useCas) + /// + /// Saves the session store into Couchbase + /// + /// Couchbase bucket to save to + /// Session ID + /// True to only save the meta data + /// True to use a check and set, false to simply store it + /// True if the value was saved, false if not + public bool Save( + IBucket bucket, + string id, + bool metaOnly, + bool useCas) { using (var ms = new MemoryStream()) { - // Save the header first - SaveHeader(ms); + // Write the header first + WriteHeader(ms); var ts = TimeSpan.FromMinutes(Timeout); - // Attempt to save the header and fail if the CAS fails + // Attempt to write the header and fail if the CAS fails var retval = useCas - ? client.Upsert(HeaderPrefix + id, new ArraySegment(ms.GetBuffer(), 0, (int) ms.Length), HeadCas, ts) - : client.Upsert(HeaderPrefix + id, new ArraySegment(ms.GetBuffer(), 0, (int) ms.Length), ts); - - if (!retval.Success) { + ? bucket.Upsert(_headerPrefix + id, new ArraySegment(ms.GetBuffer(), 0, (int)ms.Length), HeadCas, ts) + : bucket.Upsert(_headerPrefix + id, new ArraySegment(ms.GetBuffer(), 0, (int)ms.Length), ts); + if (!retval.Success) return false; - } - // Save the data + // Now save the data if (!metaOnly) { - ms.Position = 0; - // Serialize the data + ms.Position = 0; using (var bw = new BinaryWriter(ms)) { Data.Serialize(bw); // Attempt to save the data and fail if the CAS fails retval = useCas - ? client.Upsert(DataPrefix + id, new ArraySegment(ms.GetBuffer(), 0, (int)ms.Length), DataCas, ts) - : client.Upsert(DataPrefix + id, new ArraySegment(ms.GetBuffer(), 0, (int)ms.Length), ts); + ? bucket.Upsert(_dataPrefix + id, new ArraySegment(ms.GetBuffer(), 0, (int)ms.Length), DataCas, ts) + : bucket.Upsert(_dataPrefix + id, new ArraySegment(ms.GetBuffer(), 0, (int)ms.Length), ts); } } @@ -278,9 +447,15 @@ public bool Save(IBucket client, string id, bool metaOnly, bool useCas) } } - private static SessionStateItem LoadItem(MemoryStream ms) + /// + /// Loads a sessions store header data from the passed in stream + /// + /// Stream to load the item from + /// Value read from the stream, null on failure + private static SessionStateItem LoadHeader( + Stream s) { - var graph = new ObjectStateFormatter().Deserialize(ms) as Pair; + var graph = new ObjectStateFormatter().Deserialize(s) as Pair; if (graph == null) return null; @@ -288,10 +463,10 @@ private static SessionStateItem LoadItem(MemoryStream ms) return null; var t = (Triplet)graph.Second; - var retval = new SessionStateItem(); - - retval.Flag = (SessionStateActions)((byte)t.First); - retval.Timeout = (int)t.Second; + var retval = new SessionStateItem { + Flag = (SessionStateActions)((byte)t.First), + Timeout = (int)t.Second + }; var lockInfo = (Pair)t.Third; @@ -301,15 +476,20 @@ private static SessionStateItem LoadItem(MemoryStream ms) return retval; } - public static SessionStateItem Load(IBucket client, string id, bool metaOnly) - { - return Load(HeaderPrefix, DataPrefix, client, id, metaOnly); - } - - public static SessionStateItem Load(string headerPrefix, string dataPrefix, IBucket client, string id, bool metaOnly) + /// + /// Loads a session state item from the bucket + /// + /// Couchbase bucket to load from + /// Session ID + /// True to load only meta data + /// Session store item read, null on failure + public static SessionStateItem Load( + IBucket bucket, + string id, + bool metaOnly) { - // Load the header for the item - var header = client.Get(headerPrefix + id); + // Read the header value from Couchbase + var header = bucket.Get(_headerPrefix + id); if (header.Status == ResponseStatus.KeyNotFound) { return null; } @@ -317,7 +497,7 @@ public static SessionStateItem Load(string headerPrefix, string dataPrefix, IBuc // Deserialize the header values SessionStateItem entry; using (var ms = new MemoryStream(header.Value)) { - entry = SessionStateItem.LoadItem(ms); + entry = LoadHeader(ms); } entry.HeadCas = header.Cas; @@ -326,8 +506,8 @@ public static SessionStateItem Load(string headerPrefix, string dataPrefix, IBuc return entry; } - // Load the data for the item - var data = client.Get(dataPrefix + id); + // Read the data for the item from Couchbase + var data = bucket.Get(_dataPrefix + id); if (data.Value == null) { return null; } @@ -344,20 +524,33 @@ public static SessionStateItem Load(string headerPrefix, string dataPrefix, IBuc return entry; } - public SessionStateStoreData ToStoreData(HttpContext context) + /// + /// Creates a session store data object from the session data + /// + /// HttpContext to use + /// Session store data for this session item + public SessionStateStoreData ToStoreData( + HttpContext context) { return new SessionStateStoreData(Data, SessionStateUtility.GetSessionStaticObjects(context), Timeout); } - public static void Remove(IBucket client, string id) + /// + /// Removes a session store item from the bucket + /// + /// Bucket to remove from + /// Session ID + public static void Remove( + IBucket bucket, + string id) { - client.Remove(DataPrefix + id); - client.Remove(HeaderPrefix + id); + bucket.Remove(_dataPrefix + id); + bucket.Remove(_headerPrefix + id); } } - #endregion - } + #endregion + } } #region [ License information ] @@ -367,6 +560,7 @@ public static void Remove(IBucket client, string id) * @copyright 2012 Couchbase, Inc. * @copyright 2012 Attila Kiskó, enyim.com * @copyright 2012 Good Time Hobbies, Inc. + * @copyright 2015 AMain.com, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/CouchbaseAspNetSample/Web.config b/CouchbaseAspNetSample/Web.config index e6c537d..7d640f3 100644 --- a/CouchbaseAspNetSample/Web.config +++ b/CouchbaseAspNetSample/Web.config @@ -83,13 +83,13 @@ - + - + diff --git a/README.md b/README.md index 02b7835..7fdc854 100644 --- a/README.md +++ b/README.md @@ -58,11 +58,11 @@ If you would like to use a different bucket than the default one, you may do so -If you would like to use a custom client factory, you may do so by specifying a value in the "factory" attribute of the provider entry. The example below sets it to the default factory, but you can replace this with your own factory class to have full control over the creation and lifecycle of the Couchbase client. +If you would like to use a custom bucket factory, you may do so by specifying a value in the "factory" attribute of the provider entry. The example below sets it to the default factory, but you can replace this with your own factory class to have full control over the creation and lifecycle of the Couchbase client. - + @@ -118,6 +118,14 @@ If you would like to use a different bucket than the default one, you may do so +If you would like to use a custom bucket factory, you may do so by specifying a value in the "factory" attribute of the provider entry. The example below sets it to the default factory, but you can replace this with your own factory class to have full control over the creation and lifecycle of the Couchbase client. + + + + + + + Once configured, simply enable output cache as you already do with ASP.NET MVC [OutputCache(Duration = 60, VaryByParam="foo")]