GithubHelp home page GithubHelp logo

leewang0 / redissessionprovider Goto Github PK

View Code? Open in Web Editor NEW
66.0 13.0 42.0 37.79 MB

Provides a drop-in class library that makes ASP.NET's System.Web Session object store its contents in a Redis server or servers.

License: Apache License 2.0

C# 99.93% ASP 0.07%

redissessionprovider's Introduction

RedisSessionProvider

RedisSessionProvider is a class library that changes the behavior of ASP.NET's Session property to efficiently persist data to a Redis server.

To install via NuGet, please go to the NuGet RedisSessionProvider package page

Why a non-locking session is important

In case you don't know, the default ASP.NET Session State behavior has a major flaw. Perhaps it made sense in the early 2000's when AJAX was rare, but locking requests based on Session ID does not sound like a very good idea to me.

I.E. If you have multiple requests with the same session Id at the same time, the first will run and the others will check in 0.5 second intervals until the first request is complete, then the next one will go, and so on. This is great for maintaining correctness of the Session, but horrible for app responsiveness when there's a lot of client to server requests happening at once.

RedisSessionProvider never locks the Session. In fact it goes to great lengths to ensure that you can have as broken a Session as you want. Starting in Version 1.1, however, RedisSessionProvider includes behavior to mitigate the impact of this breaking change from the default .NET Session implementation. Specifically, in V1.0 RedisSessionProvider returned a distinct Dictionary<string, object> instance for each request thread. This would be fine if each request only operated on separate Session keys, since only the changed objects would be written back to Redis. However, if two simultaneous requests changed the same key, the second one to finish would be what was ultimately persisted in Redis. To counter this behavior for reference types, we added a middle layer in V1.1 that returns the same ConcurrentDictionary<string, object> instance to all requests that ask for the same Session. The end result is that multiple requests that do the following:

static object locker = new object();
...
List&lt;string&gt; sessionList = Session["myList"] as List&lt;string&gt;;    
lock(locker) {
    sessionList.Add("some string");
}

Will be correctly persisted to Redis starting in V1.1, where three simultaneous requests calling the .Add will result in three list entries, as opposed to 1 in V1.0. Unfortunately, the call

int x = (int)Session["myInt"];
Session["myInt"] = x + 1;

...will still result in non-deterministic behavior, where the final value of x may 1, 2 or 3 higher with three simultaneous requests. With default ASP.NET Session behavior, because only one request is allowed to run at a time, the additions will happen exclusively of each other (although with higher latency from the perspective of the client), and thus be correct. Of course, you could get around this using RedisSessionProvider by only storing reference types in the Session, and providing thread-safe add calls to any ints by using the System.Threading.Interlocked class, but to get correct behavior with a multi-thread accessible Session you will always have to be conscious of the highly parallel nature of serving your pages. If speed is your primary concern, and you want to remove the bottleneck of Session locking, then RedisSessionProvider is for you. If you are writing an app that does not need that, then use one of the other Redis providers out there. RedisSessionStateStore seems to be the most popular.

Key features:

  • .NET 4.5 library for storing Session data in Redis
  • ASP.NET web Sessions are stored as Redis hashes, with each Session["someKey"] persisting to a field named "someKey" in the Redis hash
  • only performs SET or DEL operations back to Redis if the value has changed
  • batches all SET and DEL operations when the Session is released at the end of the request
  • uses the StackExchange.Redis redis client
  • many configurable options for the particulars of using your redis instance(s), see the options section below
  • JSON serialization format for human-readable Session contents, using Json.NET
  • configuration options for writing your own serializers
  • production-tested on DateHookup a website that serves 25 million page visits (and many more total web requests) a day

High-Level overview:

RedisSessionProvider is intended for new ASP.NET projects or existing sites running on .NET 4.5 that want to store user sessions in Redis as opposed to the built-in options. Some very important behaviors may differ, see the disclaimer above. This library exposes a custom SessionStateStoreProviderBase, a class within the System.Web.SessionState namespace that exists solely for the purpose of changing how your Session data is persisted.

Storing session data in Redis gives you the out-of-process advantages of the StateServer session mode, which keeps Sessions alive during application restarts, as well as the load-balancing behavior of the SQLServer mode without the overhead of maintaining a session-specific database in SQL Server. Please keep in mind, however, that you should still have IP-affinity or "sticky" load balancing in order to take advantage of the shared local cache portion of RedisSessionProvider, which allows for safer multi-threaded Session access (again, see disclaimer above).

Setting up RedisSessionProvider

This assumes you have a running Redis server that is at an address accessible by your web machines. RedisSessionProvider has been actively used on DateHookup since March 2013 on multiple webservers hitting Redis 2.6.14 instances, but has not been tested with earlier or later versions.

Modifying the web.config

Your sessionState element must be changed to mark RedisSessionProvider as the provider of the Session data. This can be done with the following modifications:

<sessionState 
	mode="Custom" 
	customProvider="RedisSessionProvider">
	<providers>
		<add 
			name="RedisSessionProvider" 
			type="RedisSessionProvider.RedisSessionStateStoreProvider, RedisSessionProvider" />
	</providers>
</sessionState>

Configuring your specifics

The sessionState element only provides the entrypoint from your application into the RedisSessionProvider code. In order for RedisSessionProvider to know where to direct requests to Redis, you must set the following properties once in your application (Global.asax application_start is a good place for that):

using RedisSessionProvider.Config;
using StackExchange.Redis

// your application class name may differ but the name or base class should not matter for our purposes
public class MvcApplication : System.Web.HttpApplication
{
	public static ConfigurationOptions redisConfigOpts { get; set; }

	...

	protected void Application_Start()
	{
		// assign your local Redis instance address, can be static
		Application.redisConfigOpts = ConfigurationOptions.Parse("{ip}:{port}");

		// pass it to RedisSessionProvider configuration class
		RedisConnectionConfig.GetSERedisServerConfig = (HttpContextBase context) => {
			return new KeyValuePair<string,ConfigurationOptions>(
				"DefaultConnection",				// if you use multiple configuration objects, please make the keys unique
				Application.redisConfigOpts);
		};
	}
}

Prior to V1.2.1, we used a separate configuration class than StackExchange.Redis. The delegate example for that is:

using RedisSessionProvider.Config;
...
RedisConnectionConfig.GetRedisServerAddress = (HttpContextBase context) => {
	return new RedisConnectionParameters(){
		ServerAddress = "Your redis server IP or hostname",
		ServerPort = 1234, // the port your redis server is listening on, defaults to 6379
		Password = "12345", // if your redis server is password protected, set this, otherwise leave null
		ServerVersion = "2.6.14", // sometimes necessary, defaults to 2.6.14
		UseProxy = Proxy.None     // can specify TwemProxy as a proxy layer
	};
};

Why does it take a lambda function? It takes a lambda in case you want to load-balance across multiple Redis instances, using the context as input. This way, you can dynamically choose your Redis server to suit your needs. If you only have one server, a simple function like the example will suffice. RedisSessionProvider may provide an IOC-based configuration method in future versions, but for now it's a static property. If you use multiple ConfigurationOptions objects, please make sure each one is returned with a unique key since the connections that are made with them are stored in a Dictionary. Different ConfigurationOptions with the same key will always use the first ConfigurationOption to be called.

In V1.2 of the NuGet package, a proxy option was added to the connection parameters that allows you to specify TwemProxy compatible behavior from StackExchange.Redis. By specifying this, you will have a reduced command set available, allowing RedisSessionProvider to talk to a TwemProxy endpoint.

At this point, your application should work, but there are additional configuration options should you need them.

Use with WebAPI, ServiceStack or other non-standard pipelines

Starting in V1.2.2, RedisSessionProvider exposes an IDisposable class called RedisSessionAccessor. The best explanation for its use is this short example:

public class APIController ...
{
	// getting an HttpContext in WebAPI is weird
	public HttpContextBase MyHttpContextBase
    {
        get
        {
            object context;
            if (this.Request != null && this.Request.Properties.TryGetValue(
                "MS_HttpContext", out context))
            {
                return ((HttpContextWrapper)context);
            }
            else if (HttpContext.Current != null)
            {
                return new HttpContextWrapper(HttpContext.Current);
            }

            return null;
        }
    }

	// some public WebAPI method
	public SomeObject PostMethod(SomeParams parmesan)
	{
		using(var sessAcc = new RedisSessionAccessor(
			new HttpContextWrapper(MyHttpContextBase)))
		{
			// within this using block, you now have access to the same Session as web pages, at the
			//		cost of possibly reading from Redis in the constructor of RedisSessionAccessor.
			//		Keep in mind that RedisSessionProvider shares one instance of the Session 
			//		collection between all currently serving requests, so if you happen to have another
			//		request going on at the same instant as this constructor, it is more or less free.
			var aha = sessAcc.Session["currentUser"]

			// now here, at the } where the dispose happens, the RedisSessionAccessor will write
			//		the changes back to Redis, just like in a normal ASP.NET Session module
			//		SetAndReleaseItemExclusive event
		}
	}
}

There are plenty of flame wars to be had over whether or not this violates REST-ful webservice design practices. I could care less, the accessor is just there for convenience if you need it. The Session property that it exposes inherits from SessionStateBase, but it does not implement all of its methods. See the FakeHttpSessionState class definition for details.

More configuration options

Within the RedisSessionProvider.Config namespace, there are two other classes in addition to RedisConnectionConfig that you may find useful for your specific needs.

RedisConnectionConfig additional properties

RedisConnectionConfig.LogRedisCommandsSentDel 

A lambda function you can specify which will log every 30 seconds the number of Redis commands sent

RedisConnectionConfig.LogRedisCommandsReceivedDel 

A lambda function you can specify which will log every 30 seconds the number of Redis replies received

RedisSerializationConfig additional properties

RedisSerializationConfig.SessionDataSerializer

This is an instance of a class of type IRedisSerializer, which you can replace with your own implementation if you have a need to serialize to something other than JSON within Redis. The default implementation is called RedisSessionProvider.Serialization.RedisJSONSerializer. Alternative serializers will be made available as they are written. The method signatures in IRedisSerializer contain detailed descriptions of each method's purpose if you do choose to roll your own. Please consider adding a pull request if you do.

RedisSerializationConfig.SerializerExceptionLoggingDel

This logging lambda function is used within try-catch blocks of the RedisJSONSerializer so you can get detailed exception messages if anything is going wrong at the serialization level. Hopefully (and likely) you won't need it. Personally, I had to log all serializations once upon a time because of a legacy code block storing DataTable instances in Session. Don't do that, they aren't meant to be serialized easily, though we do have a workaround in place to handle it within RedisJSONSerializer. But please don't do it.

RedisSessionConfig additional properties

RedisSessionConfig.SessionExceptionLoggingDel

This lambda function is used in the top-level RedisSessionProvider.RedisSessionStateStoreProvider class to log all exceptions that bubble up through all the other classes. If something is just not working, adding this method may help you pinpoint the issue.

RedisSessionConfig.RedisKeyFromSessionIdDel

If you share your Redis server with other users or applications, it may be that the default behavior of using the auto-generated .NET session id as the Redis key name is insufficient. Setting this lambda function will allow you to have fine-grained control over how an HttpContext and given session id are translated into a Redis key name to store the hash of Session values.

redissessionprovider's People

Contributors

damirainullin avatar elph avatar leewang0 avatar thefm avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

redissessionprovider's Issues

RedisJSONSerializer - cast error

I'm applying your RedisSessionProvider to our project to improve it.
All was working fine until I realized that RedisJSONSerializer give us some problems.
The issue is when RedisJSONSerializer serialized a ComplexType (SerializeOne function).
It's storing fine, but when it's come time to deserialize is converting int type to long.

Error running Sample app

I have issues running the Sample app that has been provided. I below error

2016-02-09 15:02:42.1379 SampleUsageMVCApp.MvcApplication Unhandled RedisSessionProvider exception
System.MissingMethodException: Method not found: 'System.Nullable`1<Int32> StackExchange.Redis.ConfigurationOptions.get_DefaultDatabase()'.
   at RedisSessionProvider.Redis.RedisConnectionWrapper..ctor(String connIdentifier, ConfigurationOptions connOpts)
   at RedisSessionProvider.RedisSessionStateStoreProvider.RedisConnWrapperFromContext(HttpContextBase context) in RedisSessionStateStoreProvider.cs:line 463
   at RedisSessionProvider.RedisSessionStateStoreProvider.SerializeToRedis(HttpContextBase context, RedisSessionStateItemCollection redisItems, String currentRedisHashId, TimeSpan expirationTimeout) in RedisSessionStateStoreProvider.cs:line 374
   at RedisSessionProvider.RedisSessionStateStoreProvider.SetAndReleaseItemExclusive(HttpContext context, String id, SessionStateStoreData item, Object lockId, Boolean newItem) in RedisSessionStateStoreProvider.cs:line 302

I didn't change the code except for changing the Redis IP address.

How to provide Password

Hi,

My current redis instance is password protected.
In the obsolete 'GetRedisServerAddress' Method, we have privilege to add password for redis server.

Can you please provide code snippet how to add 'password' information using ConfigurationOptions.

This is what i've used. It didn't work.

  Application["redisConfigurationOptions"] = new ConfigurationOptions
        {
            AllowAdmin = true,
            Password = "Omg@123"                
        };

        Application["redisConfigurationOptions"] = ConfigurationOptions.Parse("localhost:6379");          

        RedisConnectionConfig.GetSERedisServerConfig = (HttpContextBase context) =>
        {
            return new KeyValuePair<string, ConfigurationOptions>(
                "redisConnection",   
                (ConfigurationOptions)Application["redisConfigurationOptions"]);
        };

Redis sentinel

Hello, I want to use a redis setup that consists of a master and some slaves, also I want to configure in my infrastructure redis sentinel, so HA can be achieved.

I believe in stackexchange.redis that can accept multiple redis instances and it will work out what is the master and the slave.

If we use your session state provider can we do the same thing?

So instead of specifying one redis instance, we pass in multiple and it then will use the master for writing to and if the master fails and then is rolled over to one of the slaves then the application using the session state wont try and connect or write to the original master as that would potentially be down or a read only slave.

Hope I am making some sense there! Your advice would be appreciated.

Session keys should be case-INsensitive to match default ASP.NET behavior, or at least configurable

When switching from StateServer to RedisSessionProvider, I found that you're handling session keys case-sensitively, because you're using the default ConcurrentDictionary IStringEquality behavior.

MSDN docs don't specify, but at least based on Internet discussions: http://stackoverflow.com/questions/1731283/net-httpsessionstate-case-insensitivity that to match classic ASP behavior, session keys along with other ASP.NET collections are case-insensitive.

To fix, all constructors need the following:

this.Items = new ConcurrentDictionary<string, object>(concLevel, numItems, StringComparer.InvariantCultureIgnoreCase);
this.SerializedRawData = new ConcurrentDictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);

Add database index to RedisConnectionConfig.GetSERedisServerConfig

No current way to specify which database index to use for the redis connection, in case the application stores session data across multiple Redis databases. See merged commit from theFm for its usage in Redis.RedisConnectionWrapper.

This will probably involve changing the return type from RedisConnectionConfig.GetSERedisServerConfig from KeyValuePair to some custom object (sigh) because StackExchange.Redis.ConfigurationOptions does not itself have a database index property.

Actually, scratch that, since it defeats the purpose of having no wrapper around StackExchange.Redis.ConfigurationOptions, so rather than change the return type, just add a new delegate with higher precendence. The functionality of adding a database index still contains all the capabilities one has by using the current GetSERedisServerConfig method anyways.

ASP.net session - Prod Version of the component

Hi,
I am currently exploring using the this component for production in one of my application. Would really appreciate if you could provide more details about below,

  • Do you have a production version ready code?
  • whether this component have any support model?

Regards,
Suresh

Cast Error when using Trace

Thanks for a great implementation of a Redis Session provider!

I found a bug when Trace is enabled (have it at the application level). In web.config
<system.web>
<trace enabled="true" requestLimit="90" pageOutput="false" traceMode="SortByTime" localOnly="false" />
</system.web>

Causes this exception
Exception: System.InvalidCastException
Unable to cast object of type 'System.Collections.Generic.KeyValuePair`2[System.String,System.Object]' to type 'System.String'.

at System.Web.TraceContext.EndRequest()
at System.Web.UI.Page.ProcessRequestEndTrace()
at System.Web.UI.Page.ProcessRequest(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)
at System.Web.UI.Page.ProcessRequest()
at System.Web.UI.Page.ProcessRequestWithNoAssert(HttpContext context)
at System.Web.UI.Page.ProcessRequest(HttpContext context)
at ASP.sobobacasino_default_aspx.ProcessRequest(HttpContext context) in c:\Users\dcate.MARKETMETRIX\AppData\Local\Temp\Temporary ASP.NET Files\root\ec897ff3\55745380\App_Web_qw3cpivu.0.cs:line 0
at System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

I can run fine with Trace turned off, but it might be needed from time to time.

Thanks,

  • Darryl

Need to migrate from BookSleeve to StackExchange.Redis

I tried to fork and migrate this from Booksleeve to StackExchange.Redis, since StackExchange.Redis supercedes now abandoned Booksleeve, but there are too many differences to make this a trivial migration. I am green to both Booksleeve and StackExchange.Redis so I don't think I'm the right person to learn up on Booksleeve so I can successfully migrate this to StackExchange.Redis.

Also, in the migration, the new multiplexer connection with multiple hosts in a single connection string would be the way I would expect to connect to a "Database", rather than building up a single connection with a single host and a single port etc.

Multiplexer connection config

StackExchange.Redis.ConnectionMultiplexer.Connect(..) accepts a configuration string that can look like "servername:6379" or it can also look like "redis0:6380,redis1:6380,allowAdmin=true". It would be ideal if we could use this alternate constructor when setting up a new RedisConnectionParameters in RedisSessionProvider.

Might also consider switching RedisConnectionParameters as param type to ConfigurationOptions...

https://github.com/StackExchange/StackExchange.Redis/blob/master/Docs/Configuration.md#automatic-and-manual-configuration

.. which theoretically can facilitate the former suggestion with ConfigurationOptions.Parse(configString);

'System.Web.HttpApplicationState' does not contain a definition for 'redisConfigOpts'

I followed the instructions exactly, adding in the web.config portion and the using statements. I'm getting an error on the code in Application_Start:

Application.redisConfigOpts = ConfigurationOptions.Parse("127.0.0.1:6379");

Error 3 'System.Web.HttpApplicationState' does not contain a definition for 'redisConfigOpts' and no extension method 'redisConfigOpts' accepting a first argument of type 'System.Web.HttpApplicationState' could be found (are you missing a using directive or an assembly reference?)

Am I doing something wrong?

Session_Start in Global.asax will not be raised

If I removed RedisSessionProvider, it will be raised by following code path.

MPWeb.dll!WeChat.MP.MvcApplication.Session_Start() 行 25 C#
[本机到托管的转换]
System.Web.dll!System.Web.Util.ArglessEventHandlerProxy.Callback(object sender, System.EventArgs e) 未知
System.Web.dll!System.Web.SessionState.SessionStateModule.RaiseOnStart(System.EventArgs e) 未知
System.Web.dll!System.Web.SessionState.SessionStateModule.CompleteAcquireState() 未知
System.Web.dll!System.Web.SessionState.SessionStateModule.BeginAcquireState(object source, System.EventArgs e, System.AsyncCallback cb, object extraData) 未知
System.Web.dll!System.Web.HttpApplication.AsyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() 未知

Session.RemoveAll and Session.Remove not removing keys from Redis

When a user logs out I am calling the Session.RemoveAll() method to remove all session keys that were created. It is not removing any of them nor is it removing a key when I call Session.Remove("key"). I followed the configuration and everything seems to be working correctly when inserting and updating, just not the remove.

Add possibility to pass JsonSerializerSettings into RedisJSONSerializer

It would be great to have a possibility to pass custom JsonSerializerSettings.
Sometimes it requires to modify default reference loop handling (or preserve references handling) for serialization(deserialization).
For now, there are two ways:

  1. apply this settings globally
  2. create a new IRedisSerializer implementation which will be pretty the same, but with only one different thing - possibility to pass custom JsonSerializerSettings.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.