Interpretation of ASP. NET 5 MVC 6 Series Tutorials (8): Session and Caching

  • 2021-07-26 07:30:42
  • OfStack

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

Configure to enable Session

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


"Microsoft.AspNet.Session": "1.0.0-beta3"

Then add a reference to Session in ConfigureServices (and configure it):


services.AddCaching(); //  Both must be added at the same time because Session Depend on Caching
services.AddSession();
//services.ConfigureSession(null);  You can configure it here or later 

Finally, in the Configure method, the mode of using Session is started. If it has been configured above, the configuration information can no longer be passed in. Otherwise, the configuration information of Session should be passed in like the configuration information 1 above. The code is as follows:


app.UseInMemorySession(configure:s => { s.IdleTimeout = TimeSpan.FromMinutes(30); });
//app.UseSession(o => { o.IdleTimeout = TimeSpan.FromSeconds(30); });
//app.UseInMemorySession(null, null); // Open memory Session
//app.UseDistributedSession(null, null);// Turn on distributed Session That is, persistence Session
//app.UseDistributedSession(new RedisCache(new RedisCacheOptions() { Configuration = "localhost" }));

For the UseInMemorySession method, two optional parameters are accepted, which are: IMemoryCache Can be used to modify the default save address of Session data; Action<SessionOptions> Delegation allows you to modify the default options, such as the path of Session cookie, the default expiration time, etc. In this example, we modify the default expiration time to 30 minutes.

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

Gets and sets Session

Gets and sets the Session object, which is generally used in action of Controller through this.Context.Session To get, which gets a interface-based ISessionCollection Gets or sets an instance of the. This interface can get and set Session values through indexes, Set, TryGetValue and other methods, but we find that when getting and setting Session, we can only use byte [] type, but can't set any type of data like the previous version of Session1. The reason is that the new version of Session requires serialization to support storage on remote servers, so saving as byte[] Type. So when we save Session, we need to convert it to byte[] To save, and get the byte[] Convert to your original type. This form is too troublesome. Fortunately, Microsoft is Microsoft.AspNet.Http Namespace that belongs to Microsoft.AspNet.Http.Extensions.dll In), we have added several extension methods for setting and saving byte[] Type, Action<SessionOptions>0 Type, and string Type, code as follows:


public static byte[] Get(this ISessionCollection session, string key);
public static int? GetInt(this ISessionCollection session, string key);
public static string GetString(this ISessionCollection session, string key);
public static void Set(this ISessionCollection session, string key, byte[] value);
public static void SetInt(this ISessionCollection session, string key, int value);
public static void SetString(this ISessionCollection session, string key, string value);

So, in Controller Quote in Microsoft.AspNet.Http Namespace, we can set and get Session through the following code:


Context.Session.SetString("Name", "Mike");
Context.Session.SetInt("Age", 21);

ViewBag.Name = Context.Session.GetString("Name");
ViewBag.Age = Context.Session.GetInt("Age");

Session Settings and Gets for Custom Types

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


public static class SessionExtensions
{
 public static bool? GetBoolean(this ISessionCollection session, string key)
 {
  var data = session.Get(key);
  if (data == null)
  {
   return null;
  }
  return BitConverter.ToBoolean(data, 0);
 } 

 public static void SetBoolean(this ISessionCollection session, string key, bool value)
 {
  session.Set(key, BitConverter.GetBytes(value));
 }
}

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


Context.Session.SetBoolean("Liar", true);
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 an Session value and empty all Session values respectively. However, it should also be noted that this interface does not provide the Abandon method functionality in previous versions.

Session Management Based on Redis

Using distributed Session, its main work is to change the storage place of Session from the original memory to distributed storage. In this section, we take Redis storage as an example to explain the processing of distributed Session.

First, look at the extension method using distributed Session. The example is as follows. We can see that its Session container needs to be a support IDistributedCache Example of the interface of.


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

This interface is a general interface for caching Caching, that is, as long as we implement the cache interface, we can use it for the management of Session. Looking at the interface in step 1, we find that the Set method defined in this interface also needs to implement a cache context of ICacheContext type (so that other programs can delegate calls when calling). 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<ICacheContext> 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 and create RedisCache Class and inherits the IDistributedCache , quote StackExchange.Redis Assembly, and then implement the IDistributedCache All the methods and properties of the 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<RedisCacheOptions> 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<ICacheContext> 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 used the custom class RedisCacheOptions As the configuration information class of Redis, in order to implement the configuration definition based on POCO, we also inherit this.Context.Session0 Interface, which is defined as follows:


services.AddCaching(); //  Both must be added at the same time because Session Depend on Caching
services.AddSession();
//services.ConfigureSession(null);  You can configure it here or later 
0

Part 3, Defining the Cache Context Class Used in Delegate Invoke this.Context.Session1 The specific code is as follows:


services.AddCaching(); //  Both must be added at the same time because Session Depend on Caching
services.AddSession();
//services.ConfigureSession(null);  You can configure it here or later 
1

The last step defines, RedisCache The shortcut to get the cached value according to the key key is as follows:


services.AddCaching(); //  Both must be added at the same time because Session Depend on Caching
services.AddSession();
//services.ConfigureSession(null);  You can configure it here or later 
2

At this point, all the work is done, and the code method for registering the cache implementation as provider of Session is as follows:


app.UseDistributedSession(new RedisCache(new RedisCacheOptions()
{
 Configuration = " Fill in here  redis The address of ",
 InstanceName = " Fill in the custom instance name here "
}), options =>
{
 options.CookieHttpOnly = true;
});

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

About Caching

By default, the local cache uses an example of the IMemoryCache interface, and you can operate on the local cache by getting an example of that interface, as follows:


services.AddCaching(); //  Both must be added at the same time because Session Depend on Caching
services.AddSession();
//services.ConfigureSession(null);  You can configure it here or later 
4

For the distributed cache, because of AddCaching, the default is to use the IMemoryCache instance as the provider of the distributed cache, and the code is as follows:


services.AddCaching(); //  Both must be added at the same time because Session Depend on Caching
services.AddSession();
//services.ConfigureSession(null);  You can configure it here or later 
5

Therefore, to use the new distributed Caching implementation, we need to register our own implementation with the following code:


services.AddCaching(); //  Both must be added at the same time because Session Depend on Caching
services.AddSession();
//services.ConfigureSession(null);  You can configure it here or later 
6

The basic usage method is as follows:


services.AddCaching(); //  Both must be added at the same time because Session Depend on Caching
services.AddSession();
//services.ConfigureSession(null);  You can configure it here or later 
7

Related articles: