当前位置: 移动技术网 > IT编程>开发语言>.net > 解读ASP.NET 5 & MVC6系列教程(8):Session与Caching

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

2017年12月12日  | 移动技术网IT编程  | 我要评论

张雅晴简历,希尔顿香烟价格,java教程视频

在之前的版本中,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;
});

参考:

关于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();

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网