解读ASP.NET 5 & MVC6系列教程(8):Session与Caching
在之前的版本中,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();
推荐阅读
-
解读ASP.NET 5 & MVC6系列教程(8):Session与Caching
-
解读ASP.NET 5 & MVC6系列教程(12):基于Lamda表达式的强类型Routing实现
-
解读ASP.NET 5 & MVC6系列教程(6):Middleware详解
-
解读ASP.NET 5 & MVC6系列教程(11):Routing路由
-
解读ASP.NET 5 & MVC6系列教程(13):TagHelper
-
解读ASP.NET 5 & MVC6系列教程(10):Controller与Action
-
解读ASP.NET 5 & MVC6系列教程(14):View Component
-
解读ASP.NET 5 & MVC6系列教程(17):MVC中的其他新特性
-
解读ASP.NET 5 & MVC6系列教程(16):自定义View视图文件查找逻辑
-
解读ASP.NET 5 & MVC6系列教程(2):初识项目