欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

解读ASP.NET 5 & MVC6系列教程(8):Session与Caching

程序员文章站 2023-12-03 09:22:10
在之前的版本中,session存在于system.web中,新版asp.net 5中由于不在依赖于system.web.dll库了,所以相应的,session也就成了asp...

在之前的版本中,session存在于system.web中,新版asp.net 5中由于不在依赖于system.web.dll库了,所以相应的,session也就成了asp.net 5中一个可配置的模块(middleware)了。

配置启用session

asp.net 5中的session模块存在于microsoft.aspnet.session类库中,要启用session,首先需要在project.json中的dependencies节点中添加如下内容:

"microsoft.aspnet.session": "1.0.0-beta3"

然后在configureservices中添加session的引用(并进行配置):

services.addcaching(); // 这两个必须同时添加,因为session依赖于caching
services.addsession();
//services.configuresession(null); 可以在这里配置,也可以再后面进行配置

最后在configure方法中,开启使用session的模式,如果在上面已经配置过了,则可以不再传入配置信息,否则还是要像上面的配置信息一样,传入session的配置信息,代码如下:

app.useinmemorysession(configure:s => { s.idletimeout = timespan.fromminutes(30); });
//app.usesession(o => { o.idletimeout = timespan.fromseconds(30); });
//app.useinmemorysession(null, null); //开启内存session
//app.usedistributedsession(null, null);//开启分布式session,也即持久化session
//app.usedistributedsession(new rediscache(new rediscacheoptions() { configuration = "localhost" }));

对于useinmemorysession方法,接收2个可选参数,分别是:imemorycache可用于修改session数据的默认保存地址;action<sessionoptions>委托则可以让你修改默认选项,比如session cookie的路径、默认的过期时间等。本例中,我们修改默认过期时间为30分钟。

注意:该方法必须在app.usemvc之前调用,否则在mvc里获取不到session,而且会出错。

获取和设置session

获取和设置session对象,一般是在controller的action里通过this.context.session来获取的,其获取的是一个基于接口isessioncollection的实例。该接口可以通过索引、set、trygetvalue等方法进行session值的获取和设置,但我们发现在获取和设置session的时候,我们只能使用byte[]类型,而不能像之前版本的session一样可以设置任意类型的数据。原因是因为,新版本的session要支持在远程服务器上存储,就需要支持序列化,所以才强制要求保存为byte[]类型。所以我们在保存session的时候,需要将其转换为byte[]才能进行保存,并且获取以后要再次将byte[]转换为自己的原有的类型才行。这种形式太麻烦了,好在微软在microsoft.aspnet.http命名空间(所属microsoft.aspnet.http.extensions.dll中)下,为我们添加了几个扩展方法,分别用于设置和保存byte[]类型、int类型、以及string类型,代码如下:

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);

所以,在controller里引用microsoft.aspnet.http命名空间以后,我们就可以通过如下代码进行session的设置和获取了:

context.session.setstring("name", "mike");
context.session.setint("age", 21);

viewbag.name = context.session.getstring("name");
viewbag.age = context.session.getint("age");

自定义类型的session设置和获取

前面我们说了,要保存自定义类型的session,需要将其类型转换成byte[]数组才行,在本例中,我们对bool类型的session数据进行设置和获取的代码,示例如下:

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));
 }
}

定义bool类型的扩展方法以后,我们就可以像setint/getint那样进行使用了,示例如下:

context.session.setboolean("liar", true);
viewbag.liar = context.session.getboolean("liar");

另外,isessioncollection接口上还提供了remove(string key)和clear()两个方法分别用于删除某个session值和清空所有的session值的功能。但同时也需要注意,该接口并没提供之前版本中的abandon方法功能。

基于redis的session管理

使用分布式session,其主要工作就是将session保存的地方从原来的内存换到分布式存储上,本节,我们以redis存储为例来讲解分布式session的处理。

先查看使用分布式session的扩展方法,示例如下,我们可以看到,其session容器需要是一个支持idistributedcache的接口示例。

public static iapplicationbuilder usedistributedsession([notnullattribute]this iapplicationbuilder app, idistributedcache cache, action<sessionoptions> configure = null);

该接口是缓存caching的通用接口,也就是说,只要我们实现了缓存接口,就可以将其用于session的管理。进一步查看该接口发现,该接口中定义的set方法还需要实现一个icachecontext类型的缓存上下文(以便在调用的时候让其它程序进行委托调用),接口定义分别如下:

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);
}

接下来,我们基于redis来实现上述功能,创建rediscache类,并继承idistributedcache,引用stackexchange.redis程序集,然后实现idistributedcache接口的所有方法和属性,代码如下:

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

在上述代码中,我们使用了自定义类rediscacheoptions作为redis的配置信息类,为了实现基于poco的配置定义,我们还继承了ioptions接口,该类的定义如下:

public class rediscacheoptions : ioptions<rediscacheoptions>
{
 public string configuration { get; set; }

 public string instancename { get; set; }

 rediscacheoptions ioptions<rediscacheoptions>.options
 {
  get { return this; }
 }

 rediscacheoptions ioptions<rediscacheoptions>.getnamedoptions(string name)
 {
  return this;
 }
}

第三部,定义委托调用时使用的缓存上下文类cachecontext,具体代码如下:

using microsoft.framework.cache.distributed;
using system;
using system.io;

namespace microsoft.framework.caching.redis
{
 internal class cachecontext : icachecontext
 {
  private readonly memorystream _data = new memorystream();

  internal cachecontext(string key)
  {
   key = key;
   creationtime = datetimeoffset.utcnow;
  }

  /// <summary>
  /// the key identifying this entry.
  /// </summary>
  public string key { get; internal set; }

  /// <summary>
  /// the state passed into set. this can be used to avoid closures.
  /// </summary>
  public object state { get; internal set; }

  public stream data { get { return _data; } }

  internal datetimeoffset creationtime { get; set; } // 可以让委托设置创建时间

  internal datetimeoffset? absoluteexpiration { get; private set; }

  internal timespan? slidingexpiration { get; private set; }

  public void setabsoluteexpiration(timespan relative) // 可以让委托设置相对过期时间
  {
   if (relative <= timespan.zero)
   {
    throw new argumentoutofrangeexception("relative", relative, "the relative expiration value must be positive.");
   }
   absoluteexpiration = creationtime + relative;
  }

  public void setabsoluteexpiration(datetimeoffset absolute) // 可以让委托设置绝对过期时间
  {
   if (absolute <= creationtime)
   {
    throw new argumentoutofrangeexception("absolute", absolute, "the absolute expiration value must be in the future.");
   }
   absoluteexpiration = absolute.touniversaltime();
  }

  public void setslidingexpiration(timespan offset) // 可以让委托设置offset过期时间
  {
   if (offset <= timespan.zero)
   {
    throw new argumentoutofrangeexception("offset", offset, "the sliding expiration value must be positive.");
   }
   slidingexpiration = offset;
  }

  internal long? getexpirationinseconds()
  {
   if (absoluteexpiration.hasvalue && slidingexpiration.hasvalue)
   {
    return (long)math.min((absoluteexpiration.value - creationtime).totalseconds, slidingexpiration.value.totalseconds);
   }
   else if (absoluteexpiration.hasvalue)
   {
    return (long)(absoluteexpiration.value - creationtime).totalseconds;
   }
   else if (slidingexpiration.hasvalue)
   {
    return (long)slidingexpiration.value.totalseconds;
   }
   return null;
  }

  internal byte[] getbytes()
  {
   return _data.toarray();
  }
 }
}

最后一步定义,rediscache中需要的根据key键获取缓存值的快捷方法,代码如下:

using stackexchange.redis;
using system;

namespace microsoft.framework.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 redismembers = new redisvalue[members.length];
   for (int i = 0; i < members.length; i++)
   {
    redismembers[i] = (redisvalue)members[i];
   }
   var result = cache.scriptevaluate(hmgetscript, new rediskey[] { key }, redismembers);
   // todo: error checking?
   return (redisvalue[])result;
  }
 }
}

至此,所有的工作就完成了,将该缓存实现注册为session的provider的代码方法如下:

app.usedistributedsession(new rediscache(new rediscacheoptions()
{
 configuration = "此处填写 redis的地址",
 instancename = "此处填写自定义实例名"
}), options =>
{
 options.cookiehttponly = true;
});

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

关于caching

默认情况下,本地缓存使用的是imemorycache接口的示例,可以通过获取该接口的示例来对本地缓存进行操作,示例代码如下:

var cache = app.applicationservices.getrequiredservice<imemorycache>();
var obj1 = cache.get("key1");
bool obj2 = cache.get<bool>("key2");

对于,分布式缓存,由于addcaching,默认将imemorycache实例作为分布式缓存的provider了,代码如下:

public static class cachingservicesextensions
{
 public static iservicecollection addcaching(this iservicecollection collection)
 {
  collection.addoptions();
  return collection.addtransient<idistributedcache, localcache>()
   .addsingleton<imemorycache, memorycache>();
 }
}

所以,要使用新的分布式caching实现,我们需要注册自己的实现,代码如下:

services.addtransient<idistributedcache, rediscache>();
services.configure<rediscacheoptions>(opt =>
{
 opt.configuration = "此处填写 redis的地址";
 opt.instancename = "此处填写自定义实例名";
});

基本的使用方法如下:

var cache = app.applicationservices.getrequiredservice<idistributedcache>();
cache.connect();
var obj1 = cache.get("key1"); //该对象是流,需要将其转换为强类型,或自己再编写扩展方法
var bytes = obj1.readallbytes();