在之前的版本中,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();
发表评论