Interpreting ASP.NET 5 & MVC6 Series (8): Session and Caching

Interpreting ASP.NET 5 & MVC6 Series (8): Session and Caching

In previous versions, Session existed in System.Web. Since the new version of ASP.NET 5 no longer depends on the System.Web.dll library, Session has become a configurable module (middleware) in ASP.NET 5.

Configure and enable Session

The Session module in ASP.NET 5 exists in the Microsoft.AspNet.Session class library. To enable Session, first add the following to the dependencies node in project.json:

  1. "Microsoft.AspNet.Session" : "1.0.0-beta3"  
  2.  
  3. Then add a reference to the Session in ConfigureServices (and configure it):
  4.  
  5. services.AddCaching(); // These two must be added at the same time because Session depends on Caching  
  6. services.AddSession();
  7. //services.ConfigureSession(null); You can configure it here or later

Finally, in the Configure method, turn on the Session mode. If you have already configured it above, you do not need to pass in the configuration information. Otherwise, you still need to pass in the Session configuration information like the above configuration information. The code is as follows:

  1. app.UseInMemorySession(configure:s => { s.IdleTimeout = TimeSpan.FromMinutes( 30 ); });
  2. //app.UseSession(o => { o.IdleTimeout = TimeSpan.FromSeconds(30); });  
  3. //app.UseInMemorySession(null, null); //Open memory session  
  4. //app.UseDistributedSession(null, null); //Open distributed session, that is, persistent session  
  5. //app.UseDistributedSession(new RedisCache(new RedisCacheOptions() { Configuration = "localhost" }));  

For the UseInMemorySession method, it receives two optional parameters: IMemoryCache can be used to modify the default save address of Session data; Action The delegation allows you to modify the default options, such as the path of the Session cookie, the default expiration time, etc. In this example, we change the default expiration time to 30 minutes.

Note: This method must be called before app.UseMvc, otherwise the Session cannot be obtained in Mvc and an error will occur.

Getting and Setting Session

To get and set the Session object, we usually get it through this.Context.Session in the Controller's action, which gets an instance based on the interface ISessionCollection. This interface can get and set the Session value through methods such as index, Set, and TryGetValue, but we found that when getting and setting the Session, we can only use the byte[] type, and cannot set any type of data like the previous version of Session. The reason is that the new version of Session needs to support storage on a remote server, so it needs to support serialization, so it is mandatory to save it as a byte[] type. So when we save the Session, we need to convert it to byte[] before saving it, and after obtaining it, we need to convert the byte[] to its original type again. This form is too troublesome. Fortunately, Microsoft has added several extension methods for us in the Microsoft.AspNet.Http namespace (belonging to Microsoft.AspNet.Http.Extensions.dll), which are used to set and save byte[] type, int type, and string type. The code is as follows:

  1. public   static   byte [] Get( this ISessionCollection session, string key);
  2. public   static   int ? GetInt( this ISessionCollection session, string key);
  3. public   static string GetString( this ISessionCollection session, string key);
  4. public   static   void Set( this ISessionCollection session, string key, byte [] value);
  5. public   static   void SetInt( this ISessionCollection session, string key, int value);
  6. public   static   void SetString( this ISessionCollection session, string key, string value);

Therefore, after referencing the Microsoft.AspNet.Http namespace in the Controller, we can set and obtain the Session through the following code:

  1. Context.Session.SetString( "Name" , "Mike" );
  2. Context.Session.SetInt( "Age" , 21 );
  3. ViewBag.Name = Context.Session.GetString( "Name" );
  4. ViewBag.Age = Context.Session.GetInt( "Age" );

Setting and getting Session of custom type

As we said before, to save a custom type of Session, you need to convert its type into a byte[] array. In this example, we set and get the code for the bool type Session data. The example is as follows:

  1. public   static   class SessionExtensions
  2. {
  3. public   static bool? GetBoolean( this ISessionCollection session, string key)
  4. {
  5. var data = session.Get(key);
  6. if (data == null )
  7. {
  8. return   null ;
  9. }
  10. return BitConverter.ToBoolean(data, 0 );
  11. }
  12.  
  13. public   static   void SetBoolean( this ISessionCollection session, string key, bool value)
  14. {
  15. session.Set(key, BitConverter.GetBytes(value));
  16. }
  17. }

After defining the extension method of bool type, we can use it like SetInt/GetInt, as shown below:

  1. Context.Session.SetBoolean( "Liar" , true );
  2. ViewBag.Liar = Context.Session.GetBoolean( "Liar" );

In addition, the ISessionCollection interface also provides two methods, Remove(string key) and Clear(), which are used to delete a session value and clear all session values ​​respectively. However, it should also be noted that this interface does not provide the Abandon method function in previous versions.

Session management based on Redis

The main task of using distributed sessions is to change the location where sessions are stored from the original memory to distributed storage. In this section, we will use Redis storage as an example to explain the processing of distributed sessions.

First, let's look at the extension method for using distributed Session. The example is as follows. We can see that its Session container needs to be an interface example that supports IDistributedCache.

public static IApplicationBuilder UseDistributedSession([NotNullAttribute]this IApplicationBuilder app, IDistributedCache cache, Action configure = null);

This interface is a common interface for caching, that is, as long as we implement the caching interface, we can use it for session management. Looking further into the interface, we find that the Set method defined in the interface also needs to implement a cache context of type ICacheContext (so that other programs can delegate the call when calling it). The interface definitions are as follows:

public interface IDistributedCache
{
    void Connect();
    void Refresh(string key);
    void Remove(string key);
    Stream Set(string key, object state, Action create); bool TryGetValue(string key, out Stream value); } public interface ICacheContext { Stream Data { get; } string Key { get; } object State { get; } void SetAbsoluteExpiration(TimeSpan relative); void SetAbsoluteExpiration(DateTimeOffset absolute); void SetSlidingExpiration(TimeSpan offset); }

Next, we implement the above functions based on Redis, create the RedisCache class, inherit IDistributedCache, reference the StackExchange.Redis assembly, and then implement all methods and properties of the IDistributedCache interface. The code is as follows:

using Microsoft.Framework.Cache.Distributed;
using Microsoft.Framework.OptionsModel;
using StackExchange.Redis;
using System;
using System.IO;

namespace Microsoft.Framework.Caching.Redis
{
    public class RedisCache : IDistributedCache
    {
        // KEYS[1] == key
        // ARGV[1] = absolute-expiration - ticks as long (-1 for none)
        // ARGV[2] = sliding-expiration - ticks as long (-1 for none)
        // ARGV[3] = relative-expiration (long, in seconds, -1 for none) - Min(absolute-expiration - Now, sliding-expiration)
        // ARGV[4] = data - byte[]
        // this order should not change LUA script depends on it
        private const string SetScript = (@"
                redis.call('HMSET', KEYS[1], 'absexp', ARGV[1], 'sldexp', ARGV[2], 'data', ARGV[4])
                if ARGV[3] ~= '-1' then
                  redis.call('EXPIRE', KEYS[1], ARGV[3]) 
                end
                return 1");
        private const string AbsoluteExpirationKey = "absexp";
        private const string SlidingExpirationKey = "sldexp";
        private const string DataKey = "data";
        private const long NotPresent = -1;

        private ConnectionMultiplexer _connection;
        private IDatabase _cache;

        private readonly RedisCacheOptions _options;
        private readonly string _instance;

        public RedisCache(IOptions optionsAccessor) { _options = optionsAccessor.Options; // This allows partitioning a single backend cache for use with multiple apps/services. _instance = _options.InstanceName ?? string.Empty; } public void Connect() { if (_connection == null) { _connection = ConnectionMultiplexer.Connect(_options.Configuration); _cache = _connection.GetDatabase(); } } public Stream Set(string key, object state, Action create) { Connect(); var context = new CacheContext(key) { State = state }; create(context); var value = context.GetBytes(); var result = _cache.ScriptEvaluate(SetScript, new RedisKey[] { _instance + key }, new RedisValue[] { context.AbsoluteExpiration?.Ticks ?? NotPresent, context.SlidingExpiration?.Ticks ?? NotPresent, context.GetExpirationInSeconds() ?? NotPresent, value }); // TODO: Error handling return new MemoryStream(value, writable: false); } public bool TryGetValue(string key, out Stream value) { value = GetAndRefresh(key, getData: true); return value != null; } public void Refresh(string key) { var ignored = GetAndRefresh(key, getData: false); } private Stream GetAndRefresh(string key, bool getData) { Connect(); // This also resets the LRU status as desired. // TODO: Can this be done in one operation on the server side? Probably, the trick would just be the DateTimeOffset math. RedisValue[] results; if (getData) { results = _cache.HashMemberGet(_instance + key, AbsoluteExpirationKey, SlidingExpirationKey, DataKey); } else { results = _cache.HashMemberGet(_instance + key, AbsoluteExpirationKey, SlidingExpirationKey); } // TODO: Error handling if (results.Length >= 2) { // Note we always get back two results, even if they are all null. // These operations will no-op in the null scenario. DateTimeOffset? absExpr; TimeSpan? sldExpr; MapMetadata(results, out absExpr, out sldExpr); Refresh(key, absExpr, sldExpr); } if (results.Length >= 3 && results[2].HasValue) { return new MemoryStream(results[2], writable: false); } return null; } private void MapMetadata(RedisValue[] results, out DateTimeOffset? absoluteExpiration, out TimeSpan? slidingExpiration) { absoluteExpiration = null; slidingExpiration = null; var absoluteExpirationTicks = (long?)results[0]; if (absoluteExpirationTicks.HasValue && absoluteExpirationTicks.Value != NotPresent) { absoluteExpiration = new DateTimeOffset(absoluteExpirationTicks.Value, TimeSpan.Zero); } var slidingExpirationTicks = (long?)results[1]; if (slidingExpirationTicks.HasValue && slidingExpirationTicks.Value != NotPresent) { slidingExpiration = new TimeSpan(slidingExpirationTicks.Value); } } private void Refresh(string key, DateTimeOffset? absExpr, TimeSpan? sldExpr) { // Note Refresh has no effect if there is just an absolute expiration (or neither). TimeSpan? expr = null; if (sldExpr.HasValue) { if (absExpr.HasValue) { var relExpr = absExpr.Value - DateTimeOffset.Now; expr = relExpr <= sldExpr.Value ? relExpr : sldExpr; } else { expr = sldExpr; } _cache.KeyExpire(_instance + key, expr); // TODO: Error handling } } public void Remove(string key) { Connect(); _cache.KeyDelete(_instance + key); // TODO: Error handling } } }

In the above code, we use the custom class RedisCacheOptions as the configuration information class of Redis. In order to implement the POCO-based configuration definition, we also inherit the IOptions interface. The definition of this class is as follows:

public class RedisCacheOptions : IOptions { public string Configuration { get; set; } public string InstanceName { get; set; } RedisCacheOptions IOptions .Options { get { return this; } } RedisCacheOptions IOptions .GetNamedOptions(string name) { return this; } }

The third part is to define the cache context class CacheContext used when delegating the call. The specific code is as follows:

  1. using Microsoft.Framework.Cache.Distributed;
  2. using System;
  3. using System.IO;
  4.  
  5. namespace Microsoft.Framework.Caching.Redis
  6. {
  7. internal class CacheContext : ICacheContext
  8. {
  9. private readonly MemoryStream _data = new MemoryStream();
  10.  
  11. internal CacheContext(string key)
  12. {
  13. Key = key;
  14. CreationTime = DateTimeOffset.UtcNow;
  15. }
  16.  
  17. /// /// The key identifying this entry. ///  
  18. public string Key { get; internal set; }
  19.  
  20. /// /// The state passed into Set. This can be used to avoid closures. ///  
  21. public object State { get; internal set; }
  22.  
  23. public Stream Data { get { return _data; } }
  24.  
  25. internal DateTimeOffset CreationTime { get; set; } // Allows the delegate to set the creation time  
  26.  
  27. internal DateTimeOffset? AbsoluteExpiration { get; private set; }
  28.  
  29. internal TimeSpan? SlidingExpiration { get; private set; }
  30.  
  31. public   void SetAbsoluteExpiration(TimeSpan relative) // Allows the delegate to set a relative expiration time  
  32. {
  33. if (relative <= TimeSpan.Zero)
  34. {
  35. throw   new ArgumentOutOfRangeException( "relative" , relative, "The relative expiration value must be positive." );
  36. }
  37. AbsoluteExpiration = CreationTime + relative;
  38. }
  39.  
  40. public   void SetAbsoluteExpiration(DateTimeOffset absolute) // Allows the delegate to set the absolute expiration time  
  41. {
  42. if (absolute <= CreationTime)
  43. {
  44. throw   new ArgumentOutOfRangeException( "absolute" , absolute, "The absolute expiration value must be in the future." );
  45. }
  46. AbsoluteExpiration = absolute.ToUniversalTime();
  47. }
  48.  
  49. public   void SetSlidingExpiration(TimeSpan offset) // Allows the delegate to set the offset expiration time  
  50. {
  51. if (offset <= TimeSpan.Zero)
  52. {
  53. throw   new ArgumentOutOfRangeException( "offset" , offset, "The sliding expiration value must be positive." );
  54. }
  55. SlidingExpiration = offset;
  56. }
  57.  
  58. internal long ? GetExpirationInSeconds()
  59. {
  60. if (AbsoluteExpiration.HasValue && SlidingExpiration.HasValue)
  61. {
  62. return ( long )Math.Min((AbsoluteExpiration.Value - CreationTime).TotalSeconds, SlidingExpiration.Value.TotalSeconds);
  63. }
  64. else   if (AbsoluteExpiration.HasValue)
  65. {
  66. return ( long )(AbsoluteExpiration.Value - CreationTime).TotalSeconds;
  67. }
  68. else   if (SlidingExpiration.HasValue)
  69. {
  70. return ( long )SlidingExpiration.Value.TotalSeconds;
  71. }
  72. return   null ;
  73. }
  74.  
  75. internal byte [] GetBytes()
  76. {
  77. return _data.ToArray();
  78. }
  79. }
  80. }

The last step is to define the quick method needed in RedisCache to get the cache value according to the key. The code is as follows:

  1. using StackExchange.Redis;
  2. using System;
  3.  
  4. namespace Microsoft.Framework.Caching.Redis
  5. {
  6. internal static   class RedisExtensions
  7. {
  8. private   const string HmGetScript = (@ "return redis.call('HMGET', KEYS[1], unpack(ARGV))" );
  9.  
  10. internal static RedisValue[] HashMemberGet( this IDatabase cache, string key, params string[] members)
  11. {
  12. var redisMembers = new RedisValue[members.Length];
  13. for ( int i = 0 ; i < members.Length; i++)
  14. {
  15. redisMembers[i] = (RedisValue)members[i];
  16. }
  17. var result = cache.ScriptEvaluate(HmGetScript, new RedisKey[] { key }, redisMembers);
  18. // TODO: Error checking?  
  19. return (RedisValue[])result;
  20. }
  21. }
  22. }

At this point, all the work is done. The code method to register the cache implementation as the Session provider is as follows:

  1. app.UseDistributedSession( new RedisCache( new RedisCacheOptions()
  2. {
  3. Configuration = "Fill in the redis address here" ,
  4. InstanceName = "Enter your custom instance name here"  
  5. }), options =>
  6. {
  7. options.CookieHttpOnly = true ;
  8. });

Reference: http://www.mikesdotnetting.com/article/270/sessions-in-asp-net-5

About Caching

By default, the local cache uses an instance of the IMemoryCache interface. You can operate the local cache by obtaining an instance of the interface. The sample code is as follows:

  1. var cache = app.ApplicationServices.GetRequiredService(); var obj1 = cache.Get( "key1" ); bool obj2 = cache.Get( "key2" );
  2.  
  3. For distributed cache, due to AddCaching, the IMemoryCache instance is used as the provider of distributed cache by default. The code is as follows:
  4.  
  5. public   static   classCachingServicesExtensions
  6. {
  7. public   static IServiceCollection AddCaching( this IServiceCollection collection)
  8. {
  9. collection.AddOptions();
  10. return collection.AddTransient

Therefore, to use the new distributed Caching implementation, we need to register our own implementation. The code is as follows:

  1. services.AddTransient

The basic usage is as follows:

var cache = app.ApplicationServices.GetRequiredService (); cache.Connect(); var obj1 = cache.Get("key1"); //This object is a stream, you need to convert it to a strong type, or write your own extension method var bytes = obj1.ReadAllBytes();

<<:  ASP.NET 5 & MVC6 Series (5): Configuration Information Management

>>:  Interpreting ASP.NET 5 & MVC6 Series (9): Logging Framework

Recommend

The new Apple MacBook is like this?

Will Apple's next MacBook use fingerprint rec...

In the Red Sea of ​​Homogeneity: Who Will Innovate Internet TV?

The era of TVs selling for money solely through h...

30 excellent copywriting sentences all use this technique

The next 30 sentences all use the same technique....

Google I/O 2016: Artificial Intelligence Becomes Product DNA

Following the developer conferences of Microsoft ...

Could it be possible that the original sculptor of the Sphinx was not a human?

The ancient pyramids and the Sphinx are probably ...

Are melamine and clenbuterol food additives? Can we eat additives?

When it comes to food additives, people have grad...

It would be so cool to go to work like Wu Lei every day!

Wu Lei’s cycling vlog is popular again. From the ...

How to plan an event? Activity planning process conception

1. Principles of activity task allocation 1) Spec...