Asp. net Core uses Redis to store Session

  • 2021-09-04 23:51:20
  • OfStack

Preface

Asp. net Core changed the previous closed, and now it is open source and open. Let's use Redis to store Session for a simple test, or middleware (middleware).

For Session, there are no praises or criticisms. Many people say not to use it directly, and many people are using it. This has no absolute meaning. Personally, I think that as long as it doesn't shadow anything and can be easily realized, it can be used. Now I don't make a statement on whether it can be used or not. We only care about realization.

Class library reference

This is much more convenient than the previous. net, and the following contents need to be added to the dependencies node in project. json:


  "StackExchange.Redis": "1.1.604-alpha",
  "Microsoft.AspNetCore.Session": "1.1.0-alpha1-21694"

Implementation of Redis

This is not my implementation, but I don't know why there was this class library before, but now there is no NUGET. In order not to affect the future upgrade of my namespace, I also use Microsoft. Extensions. Caching. Redis

You can see that Microsoft has four classes here. In fact, we only need three, and the fourth one will make mistakes:


using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Options;
using StackExchange.Redis;

namespace Microsoft.Extensions.Caching.Redis
{
  public class RedisCache : IDistributedCache, IDisposable
  {
    // 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)
    {
      if (optionsAccessor == null)
      {
        throw new ArgumentNullException(nameof(optionsAccessor));
      }

      _options = optionsAccessor.Value;

      // This allows partitioning a single backend cache for use with multiple apps/services.
      _instance = _options.InstanceName ?? string.Empty;
    }

    public byte[] Get(string key)
    {
      if (key == null)
      {
        throw new ArgumentNullException(nameof(key));
      }

      return GetAndRefresh(key, getData: true);
    }

    public async Task<byte[]> GetAsync(string key)
    {
      if (key == null)
      {
        throw new ArgumentNullException(nameof(key));
      }

      return await GetAndRefreshAsync(key, getData: true);
    }

    public void Set(string key, byte[] value, DistributedCacheEntryOptions options)
    {
      if (key == null)
      {
        throw new ArgumentNullException(nameof(key));
      }

      if (value == null)
      {
        throw new ArgumentNullException(nameof(value));
      }

      if (options == null)
      {
        throw new ArgumentNullException(nameof(options));
      }

      Connect();

      var creationTime = DateTimeOffset.UtcNow;

      var absoluteExpiration = GetAbsoluteExpiration(creationTime, options);

      var result = _cache.ScriptEvaluate(SetScript, new RedisKey[] { _instance + key },
        new RedisValue[]
        {
            absoluteExpiration?.Ticks ?? NotPresent,
            options.SlidingExpiration?.Ticks ?? NotPresent,
            GetExpirationInSeconds(creationTime, absoluteExpiration, options) ?? NotPresent,
            value
        });
    }

    public async Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options)
    {
      if (key == null)
      {
        throw new ArgumentNullException(nameof(key));
      }

      if (value == null)
      {
        throw new ArgumentNullException(nameof(value));
      }

      if (options == null)
      {
        throw new ArgumentNullException(nameof(options));
      }

      await ConnectAsync();

      var creationTime = DateTimeOffset.UtcNow;

      var absoluteExpiration = GetAbsoluteExpiration(creationTime, options);

      await _cache.ScriptEvaluateAsync(SetScript, new RedisKey[] { _instance + key },
        new RedisValue[]
        {
            absoluteExpiration?.Ticks ?? NotPresent,
            options.SlidingExpiration?.Ticks ?? NotPresent,
            GetExpirationInSeconds(creationTime, absoluteExpiration, options) ?? NotPresent,
            value
        });
    }

    public void Refresh(string key)
    {
      if (key == null)
      {
        throw new ArgumentNullException(nameof(key));
      }

      GetAndRefresh(key, getData: false);
    }

    public async Task RefreshAsync(string key)
    {
      if (key == null)
      {
        throw new ArgumentNullException(nameof(key));
      }

      await GetAndRefreshAsync(key, getData: false);
    }

    private void Connect()
    {
      if (_connection == null)
      {
        _connection = ConnectionMultiplexer.Connect(_options.Configuration);
        _cache = _connection.GetDatabase();
      }
    }

    private async Task ConnectAsync()
    {
      if (_connection == null)
      {
        _connection = await ConnectionMultiplexer.ConnectAsync(_options.Configuration);
        _cache = _connection.GetDatabase();
      }
    }

    private byte[] GetAndRefresh(string key, bool getData)
    {
      if (key == null)
      {
        throw new ArgumentNullException(nameof(key));
      }

      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 results[2];
      }

      return null;
    }

    private async Task<byte[]> GetAndRefreshAsync(string key, bool getData)
    {
      if (key == null)
      {
        throw new ArgumentNullException(nameof(key));
      }

      await ConnectAsync();

      // 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 = await _cache.HashMemberGetAsync(_instance + key, AbsoluteExpirationKey, SlidingExpirationKey, DataKey);
      }
      else
      {
        results = await _cache.HashMemberGetAsync(_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);
        await RefreshAsync(key, absExpr, sldExpr);
      }

      if (results.Length >= 3 && results[2].HasValue)
      {
        return results[2];
      }

      return null;
    }

    public void Remove(string key)
    {
      if (key == null)
      {
        throw new ArgumentNullException(nameof(key));
      }

      Connect();

      _cache.KeyDelete(_instance + key);
      // TODO: Error handling
    }

    public async Task RemoveAsync(string key)
    {
      if (key == null)
      {
        throw new ArgumentNullException(nameof(key));
      }

      await ConnectAsync();

      await _cache.KeyDeleteAsync(_instance + key);
      // TODO: Error handling
    }

    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)
    {
      if (key == null)
      {
        throw new ArgumentNullException(nameof(key));
      }

      // 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
      }
    }

    private async Task RefreshAsync(string key, DateTimeOffset? absExpr, TimeSpan? sldExpr)
    {
      if (key == null)
      {
        throw new ArgumentNullException(nameof(key));
      }

      // 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;
        }
        await _cache.KeyExpireAsync(_instance + key, expr);
        // TODO: Error handling
      }
    }

    private static long? GetExpirationInSeconds(DateTimeOffset creationTime, DateTimeOffset? absoluteExpiration, DistributedCacheEntryOptions options)
    {
      if (absoluteExpiration.HasValue && options.SlidingExpiration.HasValue)
      {
        return (long)Math.Min(
          (absoluteExpiration.Value - creationTime).TotalSeconds,
          options.SlidingExpiration.Value.TotalSeconds);
      }
      else if (absoluteExpiration.HasValue)
      {
        return (long)(absoluteExpiration.Value - creationTime).TotalSeconds;
      }
      else if (options.SlidingExpiration.HasValue)
      {
        return (long)options.SlidingExpiration.Value.TotalSeconds;
      }
      return null;
    }

    private static DateTimeOffset? GetAbsoluteExpiration(DateTimeOffset creationTime, DistributedCacheEntryOptions options)
    {
      if (options.AbsoluteExpiration.HasValue && options.AbsoluteExpiration <= creationTime)
      {
        throw new ArgumentOutOfRangeException(
          nameof(DistributedCacheEntryOptions.AbsoluteExpiration),
          options.AbsoluteExpiration.Value,
          "The absolute expiration value must be in the future.");
      }
      var absoluteExpiration = options.AbsoluteExpiration;
      if (options.AbsoluteExpirationRelativeToNow.HasValue)
      {
        absoluteExpiration = creationTime + options.AbsoluteExpirationRelativeToNow;
      }

      return absoluteExpiration;
    }

    public void Dispose()
    {
      if (_connection != null)
      {
        _connection.Close();
      }
    }
  }
}


using Microsoft.Extensions.Options;

namespace Microsoft.Extensions.Caching.Redis
{
  /// <summary>
  /// Configuration options for <see cref="RedisCache"/>.
  /// </summary>
  public class RedisCacheOptions : IOptions<RedisCacheOptions>
  {
    /// <summary>
    /// The configuration used to connect to Redis.
    /// </summary>
    public string Configuration { get; set; }

    /// <summary>
    /// The Redis instance name.
    /// </summary>
    public string InstanceName { get; set; }

    RedisCacheOptions IOptions<RedisCacheOptions>.Value
    {
      get { return this; }
    }
  }
}


using System.Threading.Tasks;
using StackExchange.Redis;

namespace Microsoft.Extensions.Caching.Redis
{
  internal static class RedisExtensions
  {
    private const string HmGetScript = (@"return redis.call('HMGET', KEYS[1], unpack(ARGV))");

    internal static RedisValue[] HashMemberGet(this IDatabase cache, string key, params string[] members)
    {
      var result = cache.ScriptEvaluate(
        HmGetScript,
        new RedisKey[] { key },
        GetRedisMembers(members));

      // TODO: Error checking?
      return (RedisValue[])result;
    }

    internal static async Task<RedisValue[]> HashMemberGetAsync(
      this IDatabase cache,
      string key,
      params string[] members)
    {
      var result = await cache.ScriptEvaluateAsync(
        HmGetScript,
        new RedisKey[] { key },
        GetRedisMembers(members));

      // TODO: Error checking?
      return (RedisValue[])result;
    }

    private static RedisValue[] GetRedisMembers(params string[] members)
    {
      var redisMembers = new RedisValue[members.Length];
      for (int i = 0; i < members.Length; i++)
      {
        redisMembers[i] = (RedisValue)members[i];
      }

      return redisMembers;
    }
  }
}

Configure to enable Session

We increase ConfigureServices in Startup


services.AddSingleton<IDistributedCache>(
        serviceProvider =>
          new RedisCache(new RedisCacheOptions
          {
            Configuration = "192.168.178.141:6379",
            InstanceName = "Sample:"
          }));
      services.AddSession();

Configure increases in Startup


app.UseSession(new SessionOptions() { IdleTimeout = TimeSpan.FromMinutes(30) });

At this point, our configuration is finished, and we can test whether 1 is written in Redis

Verification result

In the Mvc project, let's implement the following code


if (string.IsNullOrEmpty(HttpContext.Session.GetString("D")))
      {
        var d = DateTime.Now.ToString();
        HttpContext.Session.SetString("D", d);
        HttpContext.Response.ContentType = "text/plain";
        await HttpContext.Response.WriteAsync("Hello First timer///" + d);
      }
      else
      {
        HttpContext.Response.ContentType = "text/plain";
        await HttpContext.Response.WriteAsync("Hello old timer///" + HttpContext.Session.GetString("D"));
      }

After running, we found that the words Hello First timer appeared for the first time, and the words Hello old timer appeared after refreshing, which proved that Session was successful. After checking Redis for one time, it was valuable, so that a distributed Session was successfully realized.

For the above example, I put the source code in: demo download

Tianwei. Microsoft. Extensions. Caching. Redis, only ID with Tianwei space name or Microsoft. Extensions. Caching. Redis

From the above example, we find that Microsoft is really open this time, which also means that we can write our own extensions if it is not convenient or appropriate to use certain classes


Related articles: