diff --git a/.gitignore b/.gitignore index e3325d77..f5c884f7 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ csx ; Visual Studio user state *.user *.suo +.vs ; VC++/C# debug info files *.idb diff --git a/Microsoft.CacheProviders.sln b/Microsoft.CacheProviders.sln index ea3994b8..f21c9879 100644 --- a/Microsoft.CacheProviders.sln +++ b/Microsoft.CacheProviders.sln @@ -25,6 +25,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RedisOutputCacheProvider.Un EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RedisOutputCacheProvider.FunctionalTests", "test\OutputCacheProviderFunctionalTests\RedisOutputCacheProvider.FunctionalTests.csproj", "{0F2B2EE8-83D9-4442-B71A-73BC2033D323}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ObjectCache", "ObjectCache", "{264192AF-0B17-4E07-B093-DC7B3E504628}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RedisObjectCache", "src\RedisObjectCache\RedisObjectCache.csproj", "{63C645E9-906A-4463-9BA7-8CB52206480C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RedisObjectCache.FunctionalTests", "test\RedisObjectCache.FunctionalTests\RedisObjectCache.FunctionalTests.csproj", "{5C77A3EA-BD24-4093-A763-9358E837173C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RedisObjectCache.UnitTests", "test\RedisObjectCache.UnitTests\RedisObjectCache.UnitTests.csproj", "{C4CF15B8-F965-4192-9008-7244CFAA9A03}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -55,6 +63,18 @@ Global {0F2B2EE8-83D9-4442-B71A-73BC2033D323}.Debug|Any CPU.Build.0 = Debug|Any CPU {0F2B2EE8-83D9-4442-B71A-73BC2033D323}.Release|Any CPU.ActiveCfg = Release|Any CPU {0F2B2EE8-83D9-4442-B71A-73BC2033D323}.Release|Any CPU.Build.0 = Release|Any CPU + {63C645E9-906A-4463-9BA7-8CB52206480C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {63C645E9-906A-4463-9BA7-8CB52206480C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {63C645E9-906A-4463-9BA7-8CB52206480C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {63C645E9-906A-4463-9BA7-8CB52206480C}.Release|Any CPU.Build.0 = Release|Any CPU + {5C77A3EA-BD24-4093-A763-9358E837173C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5C77A3EA-BD24-4093-A763-9358E837173C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5C77A3EA-BD24-4093-A763-9358E837173C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5C77A3EA-BD24-4093-A763-9358E837173C}.Release|Any CPU.Build.0 = Release|Any CPU + {C4CF15B8-F965-4192-9008-7244CFAA9A03}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C4CF15B8-F965-4192-9008-7244CFAA9A03}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C4CF15B8-F965-4192-9008-7244CFAA9A03}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C4CF15B8-F965-4192-9008-7244CFAA9A03}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -66,5 +86,8 @@ Global {A829BDCA-3B7D-4C22-AF85-C82012A8FE2F} = {A5E4A982-2782-43BF-871C-596106C96B14} {9BC29B91-CCB5-499A-B68B-5AAC0492B7CB} = {A5E4A982-2782-43BF-871C-596106C96B14} {0F2B2EE8-83D9-4442-B71A-73BC2033D323} = {A5E4A982-2782-43BF-871C-596106C96B14} + {63C645E9-906A-4463-9BA7-8CB52206480C} = {264192AF-0B17-4E07-B093-DC7B3E504628} + {5C77A3EA-BD24-4093-A763-9358E837173C} = {264192AF-0B17-4E07-B093-DC7B3E504628} + {C4CF15B8-F965-4192-9008-7244CFAA9A03} = {264192AF-0B17-4E07-B093-DC7B3E504628} EndGlobalSection EndGlobal diff --git a/src/OutputCacheProvider/RedisOutputCacheConnectionWrapper.cs b/src/OutputCacheProvider/RedisOutputCacheConnectionWrapper.cs index 74053a20..1e008c3b 100644 --- a/src/OutputCacheProvider/RedisOutputCacheConnectionWrapper.cs +++ b/src/OutputCacheProvider/RedisOutputCacheConnectionWrapper.cs @@ -11,7 +11,7 @@ internal class RedisOutputCacheConnectionWrapper : IOutputCacheConnection { internal static RedisSharedConnection sharedConnection; static object lockForSharedConnection = new object(); - internal static RedisUtility redisUtility; + internal static RedisUtility redisUtility; internal IRedisClientConnection redisConnection; ProviderConfiguration configuration; diff --git a/src/RedisObjectCache/IObjectCacheConnection.cs b/src/RedisObjectCache/IObjectCacheConnection.cs new file mode 100644 index 00000000..cf1fc783 --- /dev/null +++ b/src/RedisObjectCache/IObjectCacheConnection.cs @@ -0,0 +1,15 @@ +using System; +using System.Globalization; + +namespace Microsoft.Web.Redis +{ + internal interface IObjectCacheConnection + { + void ResetExpiry(string key, DateTime utcExpiry, string regionName = null); + bool Exists(string key, string regionName = null); + void Set(string key, object entry, DateTime utcExpiry, string regionName = null); + object AddOrGetExisting(string key, object entry, DateTime utcExpiry, string regionName = null); + object Get(string key, string regionName = null); + object Remove(string key, string regionName = null); + } +} \ No newline at end of file diff --git a/src/RedisObjectCache/Properties/AssemblyInfo.cs b/src/RedisObjectCache/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..b969d600 --- /dev/null +++ b/src/RedisObjectCache/Properties/AssemblyInfo.cs @@ -0,0 +1,56 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// + +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: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("RedisObjectCacheProvider")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: AssemblyMetadata("Serviceable", "True")] + +#if !CODESIGNING +[assembly: InternalsVisibleTo("Microsoft.Web.RedisObjectCache.Unit.Tests")] +[assembly: InternalsVisibleTo("Microsoft.Web.RedisObjectCache.Functional.Tests")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] +#endif + +// 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)] +#if !NOCOMMONASSEMBLYVERSION +[assembly: AssemblyVersion("1.1.0.0")] +[assembly: AssemblyFileVersion("1.1.0.0")] +#endif +[assembly: AssemblyTitle("Cache Providers")] + +namespace System.Reflection +{ + /// + /// Provided as a down-level stub for the 4.5 AssemblyMetaDataAttribute class. + /// All released assemblies should define [AssemblyMetadata("Serviceable", "True")]. + /// + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true, Inherited = false)] + internal sealed class AssemblyMetadataAttribute : Attribute + { + public AssemblyMetadataAttribute(string key, string value) + { + Key = key; + Value = value; + } + + public string Key { get; set; } + public string Value { get; set; } + } +} diff --git a/src/RedisObjectCache/RedisObjectCache.cs b/src/RedisObjectCache/RedisObjectCache.cs new file mode 100644 index 00000000..f99ea55f --- /dev/null +++ b/src/RedisObjectCache/RedisObjectCache.cs @@ -0,0 +1,253 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Configuration; +using System.Globalization; +using System.Runtime.Caching; + +namespace Microsoft.Web.Redis +{ + public class RedisObjectCache : ObjectCache + { + // static holder for instance, need to use lambda to construct since constructor private + private static readonly Lazy Instance = new Lazy(() => new RedisObjectCache()); + + private static readonly TimeSpan OneYear = TimeSpan.FromDays(365); + + internal IObjectCacheConnection cache; + private readonly ProviderConfiguration configuration; + + private RedisObjectCache() + : this("Default") + { + } + + public RedisObjectCache(string name) + : this (name, GetConfig(name)) + { + } + + public RedisObjectCache(string name, string connectionString) + : this (name, new NameValueCollection { { "connectionString", connectionString } }) + { + } + + internal RedisObjectCache(string name, NameValueCollection config) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentNullException(nameof(name)); + if (config == null) + throw new ArgumentNullException(nameof(config)); + + Name = name; + configuration = ProviderConfiguration.ProviderConfigurationForObjectCache(config, name); + cache = new RedisObjectCacheConnectionWrapper(configuration, name); + } + + private static NameValueCollection GetConfig(string name) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentNullException(nameof(name)); + + ProviderSettings providerSettings = RedisObjectCacheConfiguration.Instance.Caches[name]; + + if (providerSettings == null) + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, RedisProviderResource.NoConfigForCache, name), nameof(name)); + + NameValueCollection pars = providerSettings.Parameters; + NameValueCollection config = new NameValueCollection(pars.Count, StringComparer.Ordinal); + foreach (string key in pars) + config[key] = pars[key]; + return config; + } + + public static RedisObjectCache Default => Instance.Value; + + public override DefaultCacheCapabilities DefaultCacheCapabilities { get; } = DefaultCacheCapabilities.AbsoluteExpirations + | DefaultCacheCapabilities.SlidingExpirations + | DefaultCacheCapabilities.OutOfProcessProvider + | DefaultCacheCapabilities.CacheRegions; + + public override string Name { get; } + + public override object this[string key] + { + get { return Get(key); } + set { Set(key, value, InfiniteAbsoluteExpiration); } + } + + public override bool Contains(string key, string regionName = null) + { + return cache.Exists(key, regionName); + } + + public override object AddOrGetExisting(string key, object value, DateTimeOffset absoluteExpiration, string regionName = null) + { + CacheItemPolicy policy = new CacheItemPolicy { AbsoluteExpiration = absoluteExpiration }; + return AddOrGetExisting(key, value, policy, regionName); + } + + public override CacheItem AddOrGetExisting(CacheItem value, CacheItemPolicy policy) + { + return new CacheItem(value.Key, AddOrGetExisting(value.Key, value.Value, policy), value.RegionName); + } + + public override object AddOrGetExisting(string key, object value, CacheItemPolicy policy, string regionName = null) + { + ValidatePolicy(policy); + try + { + DateTime utcExpiry; + if (policy.SlidingExpiration != NoSlidingExpiration) + { + utcExpiry = DateTime.UtcNow + policy.SlidingExpiration; + value = new SlidingExpiryCacheItem(value, policy.SlidingExpiration); + } + else + utcExpiry = policy.AbsoluteExpiration.UtcDateTime; + + object oldValue = cache.AddOrGetExisting(key, value, utcExpiry, regionName); + oldValue = HandleSlidingExpiry(key, oldValue, regionName); + return oldValue; + } + catch (Exception e) + { + LogUtility.LogError("Error in AddOrGetExisting: " + e.Message); + if (configuration.ThrowOnError) + throw; + } + return null; + } + + public override CacheItem GetCacheItem(string key, string regionName = null) + { + return new CacheItem(key, Get(key, regionName), regionName); + } + + public override object Get(string key, string regionName = null) + { + try + { + object value = cache.Get(key, regionName); + value = HandleSlidingExpiry(key, value, regionName); + return value; + } + catch (Exception e) + { + LogUtility.LogError("Error in Get: " + e.Message); + if (configuration.ThrowOnError) + throw; + } + return null; + } + + public override void Set(string key, object value, DateTimeOffset absoluteExpiration, string regionName = null) + { + CacheItemPolicy policy = new CacheItemPolicy { AbsoluteExpiration = absoluteExpiration }; + Set(key, value, policy, regionName); + } + public override void Set(CacheItem item, CacheItemPolicy policy) + { + Set(item.Key, item.Value, policy, item.RegionName); + } + + public override void Set(string key, object value, CacheItemPolicy policy, string regionName = null) + { + ValidatePolicy(policy); + try + { + DateTime utcExpiry; + if (policy.SlidingExpiration != NoSlidingExpiration) + { + utcExpiry = DateTime.UtcNow + policy.SlidingExpiration; + value = new SlidingExpiryCacheItem(value, policy.SlidingExpiration); + } + else + utcExpiry = policy.AbsoluteExpiration.UtcDateTime; + + cache.Set(key, value, utcExpiry, regionName); + } + catch (Exception e) + { + LogUtility.LogError("Error in Set: " + e.Message); + if (configuration.ThrowOnError) + throw; + } + } + + public override object Remove(string key, string regionName = null) + { + try + { + return cache.Remove(key, regionName); + } + catch (Exception e) + { + LogUtility.LogError("Error in Remove: " + e.Message); + if (configuration.ThrowOnError) + throw; + } + return null; + } + + private object HandleSlidingExpiry(string key, object value, string regionName) + { + SlidingExpiryCacheItem item = value as SlidingExpiryCacheItem; + + if (item == null) + return value; + + cache.ResetExpiry(key, DateTime.UtcNow + item.SlidingExpiration, regionName); + return item.Value; + } + + private void ValidatePolicy(CacheItemPolicy policy) + { + if (policy.RemovedCallback != null) + throw new NotSupportedException(RedisProviderResource.RemovedCallbackNotSupported); + + if (policy.UpdateCallback != null) + throw new NotSupportedException(RedisProviderResource.UpdateCallbackNotSupported); + + if (policy.ChangeMonitors.Count != 0) + throw new NotSupportedException(RedisProviderResource.ChangeMonitorsNotSupported); + + if (policy.Priority == CacheItemPriority.NotRemovable) + throw new NotSupportedException(RedisProviderResource.NotRemovableNotSupported); + + // copied from MemoryCache.ValidatPolicy() + if (policy.AbsoluteExpiration != InfiniteAbsoluteExpiration && policy.SlidingExpiration != NoSlidingExpiration) + throw new ArgumentException(RedisProviderResource.InvalidExpirationPolicy , nameof(policy)); + + if (policy.SlidingExpiration < NoSlidingExpiration || OneYear < policy.SlidingExpiration) + throw new ArgumentOutOfRangeException(nameof(policy)); + + if (policy.Priority != CacheItemPriority.Default && policy.Priority != CacheItemPriority.NotRemovable) + throw new ArgumentOutOfRangeException(nameof(policy)); + } + + #region Not Implemented + + public override IDictionary GetValues(IEnumerable keys, string regionName = null) + { + throw new NotImplementedException(); + } + + public override long GetCount(string regionName = null) + { + throw new NotImplementedException(); + } + + protected override IEnumerator> GetEnumerator() + { + throw new NotImplementedException(); + } + + public override CacheEntryChangeMonitor CreateCacheEntryChangeMonitor(IEnumerable keys, string regionName = null) + { + throw new NotSupportedException(RedisProviderResource.ChangeMonitorsNotSupported); + } + + #endregion + } +} diff --git a/src/RedisObjectCache/RedisObjectCache.csproj b/src/RedisObjectCache/RedisObjectCache.csproj new file mode 100644 index 00000000..75765c6c --- /dev/null +++ b/src/RedisObjectCache/RedisObjectCache.csproj @@ -0,0 +1,154 @@ + + + + + + Debug + AnyCPU + {63C645E9-906A-4463-9BA7-8CB52206480C} + Library + Properties + Microsoft.Web.Redis + Microsoft.Caching.RedisObjectCache + v4.0 + 512 + + true + true + dummy.snk + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.dll + True + + + ..\..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.dll + True + + + ..\..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.Desktop.dll + True + + + ..\..\packages\StackExchange.Redis.StrongName.1.2.1\lib\net40\StackExchange.Redis.StrongName.dll + True + + + + + ..\..\packages\Microsoft.Bcl.1.1.10\lib\net40\System.IO.dll + True + + + + ..\..\packages\Microsoft.Bcl.1.1.10\lib\net40\System.Runtime.dll + True + + + + + ..\..\packages\Microsoft.Bcl.1.1.10\lib\net40\System.Threading.Tasks.dll + True + + + + + + + BinarySerializer.cs + + + ChangeTrackingSessionStateItemCollection.cs + + + IRedisClientConnection.cs + + + ISerializer.cs + + + LogUtility.cs + + + ProviderConfiguration.cs + + + RedisNull.cs + + + RedisSharedConnection.cs + + + RedisUtility.cs + + + StackExchangeClientConnection.cs + + + ValueWrapper.cs + + + + + + + + RedisProviderResource.resx + True + True + + + + + + + + + + ResXFileCodeGenerator + RedisProviderResource.Designer.cs + + + + + $(OutputPath)$(TargetFrameworkVersion)\ + + + $(OutputPath)$(TargetFrameworkVersion)\ + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/src/RedisObjectCache/RedisObjectCacheConfiguration.cs b/src/RedisObjectCache/RedisObjectCacheConfiguration.cs new file mode 100644 index 00000000..f0b2c9b5 --- /dev/null +++ b/src/RedisObjectCache/RedisObjectCacheConfiguration.cs @@ -0,0 +1,59 @@ +using System.Configuration; +using System.Linq; +using System.Xml; + +namespace Microsoft.Web.Redis +{ + // Use the following web.config file. + // + // + // + //
+ // + // + // + // + // + // + // + public sealed class RedisObjectCacheConfiguration : ConfigurationSection + { + // Properties + public static RedisObjectCacheConfiguration Instance + { + get { return (RedisObjectCacheConfiguration)ConfigurationManager.GetSection("redisObjectCache"); } + } + + protected override void DeserializeSection(XmlReader reader) + { + // ProviderSettingsCollection and ProviderSettings needs to have a type-attribute on the add-element + // for the cache configuration type is not needed. To prevent the user from having to add an empty + // type-attribute, this piece of code just adds an empty attribute. + reader.Read(); + string xml = reader.ReadOuterXml(); + XmlDocument document = new XmlDocument(); + document.LoadXml(xml); + + foreach (XmlElement childNode in document.DocumentElement.SelectNodes("//add[not(@type)]").OfType()) + childNode.SetAttribute("type", string.Empty); + + using (XmlReader innerReader = new XmlNodeReader(document)) + base.DeserializeSection(innerReader); + } + + [ConfigurationProperty("caches", IsRequired = true)] + public ProviderSettingsCollection Caches + { + get { return (ProviderSettingsCollection)this["caches"]; } + } + } +} \ No newline at end of file diff --git a/src/RedisObjectCache/RedisObjectCacheConnectionWrapper.cs b/src/RedisObjectCache/RedisObjectCacheConnectionWrapper.cs new file mode 100644 index 00000000..f9066015 --- /dev/null +++ b/src/RedisObjectCache/RedisObjectCacheConnectionWrapper.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; + +namespace Microsoft.Web.Redis +{ + internal class RedisObjectCacheConnectionWrapper : IObjectCacheConnection + { + private static Dictionary sharedConnections = new Dictionary(); + private static object lockForSharedConnection = new object(); + internal static RedisUtility redisUtility; + + private IRedisClientConnection redisConnection; + private ProviderConfiguration configuration; + + public RedisObjectCacheConnectionWrapper(ProviderConfiguration configuration, string name) + { + this.configuration = configuration; + + // Shared connection is created by server when it starts. don't want to lock everytime when check == null. + // so that is why pool == null exists twice. + if (!sharedConnections.ContainsKey(name)) + { + lock (lockForSharedConnection) + { + if (!sharedConnections.ContainsKey(name)) + { + sharedConnections[name] = new RedisSharedConnection(configuration, () => new StackExchangeClientConnection(configuration)); + redisUtility = new RedisUtility(configuration); + } + } + } + redisConnection = sharedConnections[name].TryGetConnection(); + } + + /*-------Start of Add operation-----------------------------------------------------------------------------------------------------------------------------------------------*/ + // KEYS = { key } + // ARGV = { page data, expiry time in miliseconds } + // retArray = { page data from cache or new } + static readonly string addScript = (@" + local retVal = redis.call('GET',KEYS[1]) + if retVal == false then + redis.call('PSETEX',KEYS[1],ARGV[2],ARGV[1]) + retVal = nil + end + return retVal + "); + + public object AddOrGetExisting(string key, object entry, DateTime utcExpiry, string regionName = null) + { + key = GetKeyForRedis(key, regionName); + TimeSpan expiryTime = utcExpiry - DateTime.UtcNow; + string[] keyArgs = new string[] { key }; + object[] valueArgs = new object[] { redisUtility.GetBytesFromObject(entry), (long)expiryTime.TotalMilliseconds }; + + object rowDataFromRedis = redisConnection.Eval(addScript, keyArgs, valueArgs); + return redisUtility.GetObjectFromBytes(redisConnection.GetOutputCacheDataFromResult(rowDataFromRedis)); + } + + /*-------End of Add operation-----------------------------------------------------------------------------------------------------------------------------------------------*/ + + public void ResetExpiry(string key, DateTime utcExpiry, string regionName = null) + { + key = GetKeyForRedis(key, regionName); + redisConnection.Expiry(key, (int)(utcExpiry - DateTime.UtcNow).TotalSeconds); + } + + public bool Exists(string key, string regionName = null) + { + key = GetKeyForRedis(key, regionName); + return redisConnection.Exists(key); + } + + public void Set(string key, object entry, DateTime utcExpiry, string regionName = null) + { + key = GetKeyForRedis(key, regionName); + byte[] data = redisUtility.GetBytesFromObject(entry); + redisConnection.Set(key, data, utcExpiry); + } + + public object Get(string key, string regionName = null) + { + key = GetKeyForRedis(key, regionName); + byte[] data = redisConnection.Get(key); + return redisUtility.GetObjectFromBytes(data); + } + + public object Remove(string key, string regionName = null) + { + key = GetKeyForRedis(key, regionName); + object value = Get(key, regionName); + redisConnection.Remove(key); + return value; + } + + private string GetKeyForRedis(string key, string regionName = null) + { + if (regionName != null) + regionName += "_"; + + return configuration.ApplicationName + "_" + regionName + key; + } + } +} diff --git a/src/RedisObjectCache/RedisProviderResource.Designer.cs b/src/RedisObjectCache/RedisProviderResource.Designer.cs new file mode 100644 index 00000000..5a178b03 --- /dev/null +++ b/src/RedisObjectCache/RedisProviderResource.Designer.cs @@ -0,0 +1,162 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.Web.Redis { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class RedisProviderResource { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal RedisProviderResource() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.Web.Redis.RedisProviderResource", typeof(RedisProviderResource).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Change Monitors are not supported. + /// + internal static string ChangeMonitorsNotSupported { + get { + return ResourceManager.GetString("ChangeMonitorsNotSupported", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The specified class '{0}' could not be loaded. Please make sure that the value specified is an assembly qualified class name.. + /// + internal static string ClassNotFound { + get { + return ResourceManager.GetString("ClassNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Either use the combination of parameters "settingsClassName" and "settingsMethodName" to provide the value of connection string or use the parameter "connectionString" but not both.. + /// + internal static string ConnectionStringException { + get { + return ResourceManager.GetString("ConnectionStringException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid expiration combination. Either set an absolute expiration or a sliding expiration.. + /// + internal static string InvalidExpirationPolicy { + get { + return ResourceManager.GetString("InvalidExpirationPolicy", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The specified method '{0}' on the class '{1}' could not be found or does meet the required method signature. Please make sure that it exists, is public and doesn't take any parameters.. + /// + internal static string MethodNotFound { + get { + return ResourceManager.GetString("MethodNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The specified method '{0}' on the class '{1}' does not match the required method signature. The method must be defined as \"static\".. + /// + internal static string MethodNotStatic { + get { + return ResourceManager.GetString("MethodNotStatic", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The specified method '{0}' on the class '{1}' does not match the required method signature. The method must have a return type of \"{2}\".. + /// + internal static string MethodWrongReturnType { + get { + return ResourceManager.GetString("MethodWrongReturnType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No configuration has been supplied for Redis Object Cache with name '{0}'. + /// + internal static string NoConfigForCache { + get { + return ResourceManager.GetString("NoConfigForCache", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to NotRemovable is not supported as Cache item priority. + /// + internal static string NotRemovableNotSupported { + get { + return ResourceManager.GetString("NotRemovableNotSupported", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Removed Callback is not supported. + /// + internal static string RemovedCallbackNotSupported { + get { + return ResourceManager.GetString("RemovedCallbackNotSupported", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Update Callback is not supported. + /// + internal static string UpdateCallbackNotSupported { + get { + return ResourceManager.GetString("UpdateCallbackNotSupported", resourceCulture); + } + } + } +} diff --git a/src/RedisObjectCache/RedisProviderResource.resx b/src/RedisObjectCache/RedisProviderResource.resx new file mode 100644 index 00000000..76da4adf --- /dev/null +++ b/src/RedisObjectCache/RedisProviderResource.resx @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Change Monitors are not supported + + + The specified class '{0}' could not be loaded. Please make sure that the value specified is an assembly qualified class name. + + + Either use the combination of parameters "settingsClassName" and "settingsMethodName" to provide the value of connection string or use the parameter "connectionString" but not both. + + + Invalid expiration combination. Either set an absolute expiration or a sliding expiration. + + + The specified method '{0}' on the class '{1}' could not be found or does meet the required method signature. Please make sure that it exists, is public and doesn't take any parameters. + + + The specified method '{0}' on the class '{1}' does not match the required method signature. The method must be defined as \"static\". + + + The specified method '{0}' on the class '{1}' does not match the required method signature. The method must have a return type of \"{2}\". + + + No configuration has been supplied for Redis Object Cache with name '{0}' + + + NotRemovable is not supported as Cache item priority + + + Removed Callback is not supported + + + Update Callback is not supported + + \ No newline at end of file diff --git a/src/RedisObjectCache/SlidingExpiryCacheItem.cs b/src/RedisObjectCache/SlidingExpiryCacheItem.cs new file mode 100644 index 00000000..f034c981 --- /dev/null +++ b/src/RedisObjectCache/SlidingExpiryCacheItem.cs @@ -0,0 +1,18 @@ +using System; + +namespace Microsoft.Web.Redis +{ + [Serializable] + internal class SlidingExpiryCacheItem + { + public SlidingExpiryCacheItem(object value, TimeSpan slidingExpiration) + { + Value = value; + SlidingExpiration = slidingExpiration; + } + + public object Value { get; } + + public TimeSpan SlidingExpiration { get; } + } +} \ No newline at end of file diff --git a/src/RedisObjectCache/app.config b/src/RedisObjectCache/app.config new file mode 100644 index 00000000..e03ea0a5 --- /dev/null +++ b/src/RedisObjectCache/app.config @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/RedisObjectCache/packages.config b/src/RedisObjectCache/packages.config new file mode 100644 index 00000000..96294b2e --- /dev/null +++ b/src/RedisObjectCache/packages.config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/Shared/IRedisClientConnection.cs b/src/Shared/IRedisClientConnection.cs index fe63cc86..60339378 100644 --- a/src/Shared/IRedisClientConnection.cs +++ b/src/Shared/IRedisClientConnection.cs @@ -18,6 +18,7 @@ internal interface IRedisClientConnection int GetSessionTimeout(object rowDataFromRedis); bool IsLocked(object rowDataFromRedis); ISessionStateItemCollection GetSessionData(object rowDataFromRedis); + bool Exists(string key); void Set(string key, byte[] data, DateTime utcExpiry); byte[] Get(string key); void Remove(string key); diff --git a/src/Shared/ProviderConfiguration.cs b/src/Shared/ProviderConfiguration.cs index c3423252..cdf9468c 100644 --- a/src/Shared/ProviderConfiguration.cs +++ b/src/Shared/ProviderConfiguration.cs @@ -72,6 +72,24 @@ internal static ProviderConfiguration ProviderConfigurationForOutputCache(NameVa return configuration; } + internal static ProviderConfiguration ProviderConfigurationForObjectCache(NameValueCollection config, string cacheName) + { + ProviderConfiguration configuration = new ProviderConfiguration(config); + + configuration.RetryTimeout = TimeSpan.Zero; + configuration.ThrowOnError = GetBoolSettings(config, "throwOnError", false); + + // Session state specific attribute which are not applicable to output cache + configuration.RequestTimeout = TimeSpan.Zero; + configuration.SessionTimeout = TimeSpan.Zero; + + configuration.ApplicationName += "_ObjectCache_" + cacheName; + + LogUtility.LogInfo("Host: {0}, Port: {1}, UseSsl: {2}, DatabaseId: {3}, ApplicationName: {4}, ThrowOnError: {5}", + configuration.Host, configuration.Port, configuration.UseSsl, configuration.DatabaseId, configuration.ApplicationName, configuration.ThrowOnError); + return configuration; + } + private ProviderConfiguration(NameValueCollection config) { EnableLoggingIfParametersAvailable(config); diff --git a/src/Shared/RedisSharedConnection.cs b/src/Shared/RedisSharedConnection.cs index 292e96b3..dddac274 100644 --- a/src/Shared/RedisSharedConnection.cs +++ b/src/Shared/RedisSharedConnection.cs @@ -33,7 +33,7 @@ public IRedisClientConnection TryGetConnection() //case 2: we are allowed to create first connection lock (lockObject) { - // make suer it is not created by other request in between + // make sure it is not created by other request in between if (connection == null) { connection = factory.Invoke(); diff --git a/src/Shared/StackExchangeClientConnection.cs b/src/Shared/StackExchangeClientConnection.cs index c7a58fb9..5f1a9dc8 100644 --- a/src/Shared/StackExchangeClientConnection.cs +++ b/src/Shared/StackExchangeClientConnection.cs @@ -243,6 +243,12 @@ public ISessionStateItemCollection GetSessionData(object rowDataFromRedis) return sessionData; } + public bool Exists(string key) + { + RedisKey redisKey = key; + return connection.KeyExists(redisKey); + } + public void Set(string key, byte[] data, DateTime utcExpiry) { RedisKey redisKey = key; diff --git a/test/RedisObjectCache.FunctionalTests/Properties/AssemblyInfo.cs b/test/RedisObjectCache.FunctionalTests/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..766150f3 --- /dev/null +++ b/test/RedisObjectCache.FunctionalTests/Properties/AssemblyInfo.cs @@ -0,0 +1,38 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Xunit; + +// 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("RedisObjectCache.FunctionalTests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("RedisObjectCache.FunctionalTests")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: CollectionBehavior(DisableTestParallelization = true)] + +// 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("5c77a3ea-bd24-4093-a763-9358e837173c")] + +// 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.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/test/RedisObjectCache.FunctionalTests/RedisObjectCache.FunctionalTests.csproj b/test/RedisObjectCache.FunctionalTests/RedisObjectCache.FunctionalTests.csproj new file mode 100644 index 00000000..d5abe14f --- /dev/null +++ b/test/RedisObjectCache.FunctionalTests/RedisObjectCache.FunctionalTests.csproj @@ -0,0 +1,118 @@ + + + + + + + Debug + AnyCPU + {5C77A3EA-BD24-4093-A763-9358E837173C} + Library + Properties + Microsoft.Web.Redis.FunctionalTests + Microsoft.Web.RedisObjectCache.Functional.Tests + v4.5 + 512 + + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\..\packages\StackExchange.Redis.StrongName.1.2.1\lib\net45\StackExchange.Redis.StrongName.dll + True + + + + + + + + + + + + + + + ..\..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll + True + + + ..\..\packages\xunit.assert.2.1.0\lib\dotnet\xunit.assert.dll + True + + + ..\..\packages\xunit.extensibility.core.2.1.0\lib\dotnet\xunit.core.dll + True + + + ..\..\packages\xunit.extensibility.execution.2.1.0\lib\net45\xunit.execution.desktop.dll + True + + + + + RedisServer.cs + + + + + + + {63c645e9-906a-4463-9ba7-8cb52206480c} + RedisObjectCache + + + + + + + + Designer + + + Designer + + + + + $(OutputPath)$(TargetFrameworkVersion)\ + false + + + $(OutputPath)$(TargetFrameworkVersion)\ + false + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/test/RedisObjectCache.FunctionalTests/RedisObjectCacheFunctionalTests.cs b/test/RedisObjectCache.FunctionalTests/RedisObjectCacheFunctionalTests.cs new file mode 100644 index 00000000..abdf5bb5 --- /dev/null +++ b/test/RedisObjectCache.FunctionalTests/RedisObjectCacheFunctionalTests.cs @@ -0,0 +1,277 @@ +using System; +using System.Collections.Specialized; +using System.Runtime.Caching; +using Xunit; + +namespace Microsoft.Web.Redis.FunctionalTests +{ + public class RedisObjectCacheFunctionalTests + { + [Fact] + public void NameTest() + { + NameValueCollection config = new NameValueCollection(); + config.Add("ssl", "false"); + RedisObjectCache provider = new RedisObjectCache("test", config); + + Assert.Equal("test", provider.Name); + } + + [Fact] + public void GetWithoutSetTest() + { + using (new RedisServer()) + { + NameValueCollection config = new NameValueCollection(); + config.Add("ssl", "false"); + RedisObjectCache provider = new RedisObjectCache("test", config); + + Assert.Equal(null, provider.Get("key1")); + } + } + + [Fact] + public void SetGetTest() + { + using (new RedisServer()) + { + NameValueCollection config = new NameValueCollection(); + config.Add("ssl", "false"); + RedisObjectCache provider = new RedisObjectCache("test", config); + + DateTime utxExpiry = DateTime.UtcNow.AddSeconds(3); + provider.Set("key2", "data2", utxExpiry, "testRegion"); + object data = provider.Get("key2", "testRegion"); + Assert.Equal("data2", data); + } + } + + [Fact] + public void SetGetIndexerTest() + { + using (new RedisServer()) + { + NameValueCollection config = new NameValueCollection(); + config.Add("ssl", "false"); + RedisObjectCache provider = new RedisObjectCache("test", config); + + DateTime utxExpiry = DateTime.UtcNow.AddSeconds(3); + provider["key2"] = "data2"; + object data = provider["key2"]; + Assert.Equal("data2", data); + } + } + + [Fact] + public void AddWithExistingSetTest() + { + using (new RedisServer()) + { + NameValueCollection config = new NameValueCollection(); + config.Add("ssl", "false"); + RedisObjectCache provider = new RedisObjectCache("test", config); + + DateTime utxExpiry = DateTime.UtcNow.AddSeconds(3); + provider.Set("key3", "data3", utxExpiry, "testRegion"); + Assert.False(provider.Add("key3", "data3.1", utxExpiry, "testRegion")); + object data = provider.Get("key3", "testRegion"); + Assert.Equal("data3", data); + } + } + + [Fact] + public void AddWithoutSetTest() + { + using (new RedisServer()) + { + NameValueCollection config = new NameValueCollection(); + config.Add("ssl", "false"); + + RedisObjectCache provider = new RedisObjectCache("test", config); + + DateTime utxExpiry = DateTime.UtcNow.AddSeconds(3); + Assert.True(provider.Add("key4", "data4", utxExpiry, "testRegion")); + object data = provider.Get("key4", "testRegion"); + Assert.Equal("data4", data); + } + } + + [Fact] + public void AddWhenSetExpiresTest() + { + using (new RedisServer()) + { + NameValueCollection config = new NameValueCollection(); + config.Add("ssl", "false"); + RedisObjectCache provider = new RedisObjectCache("test", config); + + DateTime utxExpiry = DateTime.UtcNow.AddSeconds(1); + provider.Set("key5", "data5", utxExpiry, "testRegion"); + object data = provider.Get("key5", "testRegion"); + Assert.Equal("data5", data); + + // Wait for 1.1 seconds so that data will expire + System.Threading.Thread.Sleep(1100); + utxExpiry = DateTime.UtcNow.AddSeconds(3); + Assert.True(provider.Add("key5", "data5.1", utxExpiry, "testRegion")); + data = provider.Get("key5", "testRegion"); + Assert.Equal("data5.1", data); + } + } + + [Fact] + public void RemoveWithoutSetTest() + { + using (new RedisServer()) + { + NameValueCollection config = new NameValueCollection(); + config.Add("ssl", "false"); + RedisObjectCache provider = new RedisObjectCache("test", config); + + provider.Remove("key6", "testRegion"); + object data = provider.Get("key6", "testRegion"); + Assert.Equal(null, data); + } + } + + [Fact] + public void RemoveTest() + { + using (new RedisServer()) + { + NameValueCollection config = new NameValueCollection(); + config.Add("ssl", "false"); + RedisObjectCache provider = new RedisObjectCache("test", config); + + DateTime utxExpiry = DateTime.UtcNow.AddSeconds(3); + provider.Set("key7", "data7", utxExpiry, "testRegion"); + provider.Remove("key7", "testRegion"); + object data = provider.Get("key7", "testRegion"); + Assert.Equal(null, data); + } + } + + [Fact] + public void ExpiryTest() + { + using (new RedisServer()) + { + NameValueCollection config = new NameValueCollection(); + config.Add("ssl", "false"); + RedisObjectCache provider = new RedisObjectCache("test", config); + + DateTime utxExpiry = DateTime.UtcNow.AddSeconds(1); + provider.Set("key8", "data8", utxExpiry, "testRegion"); + // Wait for 1.1 seconds so that data will expire + System.Threading.Thread.Sleep(1100); + object data = provider.Get("key8", "testRegion"); + Assert.Equal(null, data); + } + } + + [Fact] + public void AddScriptFixForExpiryTest() + { + using (new RedisServer()) + { + NameValueCollection config = new NameValueCollection(); + config.Add("ssl", "false"); + RedisObjectCache provider = new RedisObjectCache("test", config); + + DateTime utxExpiry = DateTime.UtcNow.AddSeconds(1); + provider.Add("key9", "data9", utxExpiry, "testRegion"); + object data = provider.Get("key9", "testRegion"); + Assert.Equal("data9", data); + // Wait for 1.1 seconds so that data will expire + System.Threading.Thread.Sleep(1100); + data = provider.Get("key9", "testRegion"); + Assert.Equal(null, data); + } + } + + [Fact] + public void SlidingExpiryTest() + { + using (new RedisServer()) + { + NameValueCollection config = new NameValueCollection(); + config.Add("ssl", "false"); + RedisObjectCache provider = new RedisObjectCache("test", config); + + CacheItemPolicy policy = new CacheItemPolicy + { + SlidingExpiration = TimeSpan.FromSeconds(4) + }; + + provider.Set("key8", "data8", policy, "testRegion"); + // Wait for 500 seconds, get the data to reset the exiration + System.Threading.Thread.Sleep(2000); + object data = provider.Get("key8", "testRegion"); + Assert.Equal("data8", data); + + // 1.1 sec after intial insert, but should still be there. + System.Threading.Thread.Sleep(2400); + data = provider.Get("key8", "testRegion"); + Assert.Equal("data8", data); + + // Wait for 1.1 seconds so that data will expire + System.Threading.Thread.Sleep(4400); + data = provider.Get("key8", "testRegion"); + Assert.Equal(null, data); + + } + } + + [Fact] + public void ContainsTest() + { + using (new RedisServer()) + { + NameValueCollection config = new NameValueCollection(); + config.Add("ssl", "false"); + RedisObjectCache provider = new RedisObjectCache("test", config); + + DateTime utxExpiry = DateTime.UtcNow.AddSeconds(1); + provider.Set("key10", "data10", utxExpiry, "testRegion"); + Assert.True(provider.Contains("key10", "testRegion")); + } + } + + [Fact] + public void DoesNotContainsTest() + { + using (new RedisServer()) + { + NameValueCollection config = new NameValueCollection(); + config.Add("ssl", "false"); + RedisObjectCache provider = new RedisObjectCache("test", config); + + DateTime utxExpiry = DateTime.UtcNow.AddMinutes(3); + Assert.False(provider.Contains("key10", "testRegion")); + } + } + + [Fact] + public void RegionsTest() + { + using (new RedisServer()) + { + NameValueCollection config = new NameValueCollection(); + config.Add("ssl", "false"); + RedisObjectCache provider = new RedisObjectCache("test", config); + + DateTime utxExpiry = DateTime.UtcNow.AddMinutes(3); + provider.Set("key11", "data11.1", utxExpiry, "region1"); + provider.Set("key11", "data11.2", utxExpiry, "region2"); + + object region1Data = provider.Get("key11", "region1"); + object region2Data = provider.Get("key11", "region2"); + object regionlessData = provider.Get("key11"); + + Assert.Equal("data11.1", region1Data); + Assert.Equal("data11.2", region2Data); + Assert.Equal(null, regionlessData); + } + } + } +} \ No newline at end of file diff --git a/test/RedisObjectCache.FunctionalTests/app.config b/test/RedisObjectCache.FunctionalTests/app.config new file mode 100644 index 00000000..858b34aa --- /dev/null +++ b/test/RedisObjectCache.FunctionalTests/app.config @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/test/RedisObjectCache.FunctionalTests/packages.config b/test/RedisObjectCache.FunctionalTests/packages.config new file mode 100644 index 00000000..75cc2689 --- /dev/null +++ b/test/RedisObjectCache.FunctionalTests/packages.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/test/RedisObjectCache.UnitTests/Properties/AssemblyInfo.cs b/test/RedisObjectCache.UnitTests/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..f2dbe462 --- /dev/null +++ b/test/RedisObjectCache.UnitTests/Properties/AssemblyInfo.cs @@ -0,0 +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("RedisObjectCache.UnitTests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("RedisObjectCache.UnitTests")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[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("c4cf15b8-f965-4192-9008-7244cfaa9a03")] + +// 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.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/test/RedisObjectCache.UnitTests/RedisObjectCache.UnitTests.csproj b/test/RedisObjectCache.UnitTests/RedisObjectCache.UnitTests.csproj new file mode 100644 index 00000000..4a423242 --- /dev/null +++ b/test/RedisObjectCache.UnitTests/RedisObjectCache.UnitTests.csproj @@ -0,0 +1,115 @@ + + + + + + + Debug + AnyCPU + {C4CF15B8-F965-4192-9008-7244CFAA9A03} + Library + Properties + Microsoft.Web.Redis.UnitTests + Microsoft.Web.RedisObjectCache.Unit.Tests + v4.5 + 512 + + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\..\packages\FakeItEasy.2.1.0\lib\net40\FakeItEasy.dll + True + + + ..\..\packages\StackExchange.Redis.StrongName.1.2.1\lib\net45\StackExchange.Redis.StrongName.dll + True + + + + + + + + + + + + + ..\..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll + True + + + ..\..\packages\xunit.assert.2.1.0\lib\dotnet\xunit.assert.dll + True + + + ..\..\packages\xunit.extensibility.core.2.1.0\lib\dotnet\xunit.core.dll + True + + + ..\..\packages\xunit.extensibility.execution.2.1.0\lib\net45\xunit.execution.desktop.dll + True + + + + + + + + + + Designer + + + + + {63C645E9-906A-4463-9BA7-8CB52206480C} + RedisObjectCache + + + + + + + + $(OutputPath)$(TargetFrameworkVersion)\ + false + + + $(OutputPath)$(TargetFrameworkVersion)\ + false + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/test/RedisObjectCache.UnitTests/RedisObjectCacheUnitTests.cs b/test/RedisObjectCache.UnitTests/RedisObjectCacheUnitTests.cs new file mode 100644 index 00000000..2858f5e1 --- /dev/null +++ b/test/RedisObjectCache.UnitTests/RedisObjectCacheUnitTests.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Specialized; +using System.Runtime.Caching; +using FakeItEasy; +using Xunit; + +namespace Microsoft.Web.Redis.UnitTests +{ + public class RedisObjectCacheUnitTests + { + [Fact] + public void TryGet() + { + var fake = A.Fake(); + A.CallTo(() => fake.Get("key1", null)).Returns(new ArgumentException("foo")); + RedisObjectCache cache = new RedisObjectCache("unitTest", new NameValueCollection()); + cache.cache = fake; + var obj = cache.Get("key1"); + Assert.IsType(obj); + } + + [Fact] + public void GetWithSlidingExpiration() + { + var fake = A.Fake(); + A.CallTo(() => fake.Get("key1", null)).Returns(new SlidingExpiryCacheItem("foo", TimeSpan.FromMinutes(1))); + RedisObjectCache cache = new RedisObjectCache("unitTest", new NameValueCollection()); + cache.cache = fake; + var obj = cache.Get("key1"); + Assert.Equal("foo", obj); + A.CallTo(() => fake.ResetExpiry("key1", A.Ignored, null)).MustHaveHappened(); + } + + [Fact] + public void TryAdd() + { + var fake = A.Fake(); + DateTime utcExpiry = DateTime.Now; + A.CallTo(() => fake.AddOrGetExisting("key1", "object", A.Ignored, null)).Returns(null); + RedisObjectCache cache = new RedisObjectCache("unitTest", new NameValueCollection()); + cache.cache = fake; + var obj = cache.Add("key1", "object", utcExpiry); + Assert.True(obj); + } + + [Fact] + public void AddWithSlidingExperation() + { + var fake = A.Fake(); + A.CallTo(() => fake.AddOrGetExisting("key1", A.Ignored, A.Ignored, null)).Returns(null); + RedisObjectCache cache = new RedisObjectCache("unitTest", new NameValueCollection()); + cache.cache = fake; + var obj = cache.Add("key1", "object", new CacheItemPolicy { SlidingExpiration = TimeSpan.FromMinutes(5) }); + //Assert.IsType(obj); + //Assert.True(obj); + A.CallTo(() => fake.AddOrGetExisting("key1", A.That.Matches(s => (string)s.Value == "object"), A.Ignored, null)).MustHaveHappened(); + } + + [Fact] + public void TrySet() + { + var fake = A.Fake(); + A.CallTo(() => fake.Set("key1", "object", A.Ignored, null)); + DateTime utcExpiry = DateTime.Now; + RedisObjectCache cache = new RedisObjectCache("unitTest", new NameValueCollection()); + cache.cache = fake; + cache.Set("key1", "object", utcExpiry); + A.CallTo(() => fake.Set("key1", "object", A.Ignored, null)).MustHaveHappened(); + } + [Fact] + public void TryRemove() + { + var fake = A.Fake(); + A.CallTo(() => fake.Remove("key1", null)); + DateTime utcExpiry = DateTime.Now; + RedisObjectCache cache = new RedisObjectCache("unitTest", new NameValueCollection()); + cache.cache = fake; + cache.Remove("key1"); + A.CallTo(() => fake.Remove("key1", null)).MustHaveHappened(); + } + //[Fact] + //public void TryInitialize() + //{ + // var fake = A.Fake(); + // RedisObjectCache cache = new RedisObjectCache("unitTest", new NameValueCollection()); + // cache.cache = fake; + // NameValueCollection config = new NameValueCollection(); + // config.Add("host", "localhost"); + // config.Add("port", "1234"); + // config.Add("accessKey", "hello world"); + // config.Add("ssl", "true"); + // cache.Initialize("name", config); + + // Assert.Equal(RedisOutputCacheProvider.configuration.Host, "localhost"); + // Assert.Equal(RedisOutputCacheProvider.configuration.Port, 1234); + // Assert.Equal(RedisOutputCacheProvider.configuration.AccessKey, "hello world"); + // Assert.Equal(RedisOutputCacheProvider.configuration.UseSsl, true); + //} + } +} diff --git a/test/RedisObjectCache.UnitTests/app.config b/test/RedisObjectCache.UnitTests/app.config new file mode 100644 index 00000000..858b34aa --- /dev/null +++ b/test/RedisObjectCache.UnitTests/app.config @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/test/RedisObjectCache.UnitTests/packages.config b/test/RedisObjectCache.UnitTests/packages.config new file mode 100644 index 00000000..1af27924 --- /dev/null +++ b/test/RedisObjectCache.UnitTests/packages.config @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/test/RedisSessionStateProviderUnitTest/RedisSharedConnectionTests.cs b/test/RedisSessionStateProviderUnitTest/RedisSharedConnectionTests.cs index 3ec3b4c6..60c4993c 100644 --- a/test/RedisSessionStateProviderUnitTest/RedisSharedConnectionTests.cs +++ b/test/RedisSessionStateProviderUnitTest/RedisSharedConnectionTests.cs @@ -81,6 +81,11 @@ public ISessionStateItemCollection GetSessionData(object rowDataFromRedis) return null; } + public bool Exists(string key) + { + return false; + } + public void Set(string key, byte[] data, DateTime utcExpiry) { } diff --git a/test/Shared/RedisServer.cs b/test/Shared/RedisServer.cs index 95e8ddbc..96a70bf0 100644 --- a/test/Shared/RedisServer.cs +++ b/test/Shared/RedisServer.cs @@ -3,7 +3,6 @@ // Licensed under the MIT License. See License.txt in the project root for license information. // -using StackExchange.Redis; using System; using System.Collections.Generic; using System.Diagnostics; @@ -39,28 +38,37 @@ private void WaitForRedisToStart() public RedisServer() { - KillRedisServers(); - _server = new Process(); - _server.StartInfo.FileName = "..\\..\\..\\..\\..\\..\\packages\\redis-64.3.0.503\\tools\\redis-server.exe"; - _server.StartInfo.Arguments = "--maxmemory 20000000"; - _server.StartInfo.WindowStyle = ProcessWindowStyle.Hidden; - _server.Start(); - WaitForRedisToStart(); + // only start a redis cache is all instances could be killed + if (KillRedisServers()) + { + _server = new Process(); + _server.StartInfo.FileName = "..\\..\\..\\..\\..\\..\\packages\\redis-64.2.8.17\\redis-server.exe"; + _server.StartInfo.Arguments = "--maxmemory 20mb"; + _server.StartInfo.WindowStyle = ProcessWindowStyle.Hidden; + _server.Start(); + WaitForRedisToStart(); + } } // Make sure that no redis-server instance is running - private void KillRedisServers() + private bool KillRedisServers() { foreach (var proc in Process.GetProcessesByName("redis-server")) { + // redis running as a service, cannot be killed anyway, so lets use it instead + if (proc.SessionId != Process.GetCurrentProcess().SessionId) + return false; + try { proc.Kill(); } catch { + return false; } } + return true; } public void Dispose()