#96 Add a session Id

This commit is contained in:
Chris R 2016-03-04 15:11:35 -08:00
parent 7c60cd5fa6
commit 432de7a158
6 changed files with 136 additions and 30 deletions

View File

@ -0,0 +1,25 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:2481/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"Hosting:Environment": "Development"
}
},
"web": {
"commandName": "web",
"environmentVariables": {
"Hosting:Environment": "Development"
}
}
}
}

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
@ -15,11 +16,14 @@ namespace Microsoft.AspNetCore.Session
{ {
public class DistributedSession : ISession public class DistributedSession : ISession
{ {
private const byte SerializationRevision = 1; private static readonly RandomNumberGenerator CryptoRandom = RandomNumberGenerator.Create();
private const int IdByteCount = 16;
private const byte SerializationRevision = 2;
private const int KeyLengthLimit = ushort.MaxValue; private const int KeyLengthLimit = ushort.MaxValue;
private readonly IDistributedCache _cache; private readonly IDistributedCache _cache;
private readonly string _sessionId; private readonly string _sessionKey;
private readonly TimeSpan _idleTimeout; private readonly TimeSpan _idleTimeout;
private readonly Func<bool> _tryEstablishSession; private readonly Func<bool> _tryEstablishSession;
private readonly IDictionary<EncodedKey, byte[]> _store; private readonly IDictionary<EncodedKey, byte[]> _store;
@ -27,10 +31,12 @@ namespace Microsoft.AspNetCore.Session
private bool _isModified; private bool _isModified;
private bool _loaded; private bool _loaded;
private bool _isNewSessionKey; private bool _isNewSessionKey;
private string _sessionId;
private byte[] _sessionIdBytes;
public DistributedSession( public DistributedSession(
IDistributedCache cache, IDistributedCache cache,
string sessionId, string sessionKey,
TimeSpan idleTimeout, TimeSpan idleTimeout,
Func<bool> tryEstablishSession, Func<bool> tryEstablishSession,
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
@ -41,9 +47,9 @@ namespace Microsoft.AspNetCore.Session
throw new ArgumentNullException(nameof(cache)); throw new ArgumentNullException(nameof(cache));
} }
if (string.IsNullOrEmpty(sessionId)) if (string.IsNullOrEmpty(sessionKey))
{ {
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(sessionId)); throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(sessionKey));
} }
if (tryEstablishSession == null) if (tryEstablishSession == null)
@ -57,7 +63,7 @@ namespace Microsoft.AspNetCore.Session
} }
_cache = cache; _cache = cache;
_sessionId = sessionId; _sessionKey = sessionKey;
_idleTimeout = idleTimeout; _idleTimeout = idleTimeout;
_tryEstablishSession = tryEstablishSession; _tryEstablishSession = tryEstablishSession;
_store = new Dictionary<EncodedKey, byte[]>(); _store = new Dictionary<EncodedKey, byte[]>();
@ -65,6 +71,33 @@ namespace Microsoft.AspNetCore.Session
_isNewSessionKey = isNewSessionKey; _isNewSessionKey = isNewSessionKey;
} }
public string Id
{
get
{
Load(); // TODO: Silent failure
if (_sessionId == null)
{
_sessionId = new Guid(IdBytes).ToString();
}
return _sessionId;
}
}
private byte[] IdBytes
{
get
{
Load(); // TODO: Silent failure
if (_sessionIdBytes == null)
{
_sessionIdBytes = new byte[IdByteCount];
CryptoRandom.GetBytes(_sessionIdBytes);
}
return _sessionIdBytes;
}
}
public IEnumerable<string> Keys public IEnumerable<string> Keys
{ {
get get
@ -122,7 +155,16 @@ namespace Microsoft.AspNetCore.Session
{ {
if (!_loaded) if (!_loaded)
{ {
LoadAsync().GetAwaiter().GetResult(); var data = _cache.Get(_sessionKey);
if (data != null)
{
Deserialize(new MemoryStream(data));
}
else if (!_isNewSessionKey)
{
_logger.AccessingExpiredSession(_sessionKey);
}
_loaded = true;
} }
} }
@ -132,14 +174,14 @@ namespace Microsoft.AspNetCore.Session
{ {
if (!_loaded) if (!_loaded)
{ {
var data = await _cache.GetAsync(_sessionId); var data = await _cache.GetAsync(_sessionKey);
if (data != null) if (data != null)
{ {
Deserialize(new MemoryStream(data)); Deserialize(new MemoryStream(data));
} }
else if (!_isNewSessionKey) else if (!_isNewSessionKey)
{ {
_logger.AccessingExpiredSession(_sessionId); _logger.AccessingExpiredSession(_sessionKey);
} }
_loaded = true; _loaded = true;
} }
@ -149,29 +191,35 @@ namespace Microsoft.AspNetCore.Session
{ {
if (_isModified) if (_isModified)
{ {
var data = await _cache.GetAsync(_sessionId); var data = await _cache.GetAsync(_sessionKey);
if (_logger.IsEnabled(LogLevel.Information) && data == null) if (_logger.IsEnabled(LogLevel.Information) && data == null)
{ {
_logger.SessionStarted(_sessionId); _logger.SessionStarted(_sessionKey, Id);
} }
_isModified = false; _isModified = false;
var stream = new MemoryStream(); var stream = new MemoryStream();
Serialize(stream); Serialize(stream);
await _cache.SetAsync( await _cache.SetAsync(
_sessionId, _sessionKey,
stream.ToArray(), stream.ToArray(),
new DistributedCacheEntryOptions().SetSlidingExpiration(_idleTimeout)); new DistributedCacheEntryOptions().SetSlidingExpiration(_idleTimeout));
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.SessionStored(_sessionKey, Id, _store.Count);
}
} }
else else
{ {
await _cache.RefreshAsync(_sessionId); await _cache.RefreshAsync(_sessionKey);
} }
} }
// Format: // Format:
// Serialization revision: 1 byte, range 0-255 // Serialization revision: 1 byte, range 0-255
// Entry count: 3 bytes, range 0-16,777,215 // Entry count: 3 bytes, range 0-16,777,215
// SessionId: IdByteCount bytes (16)
// foreach entry: // foreach entry:
// key name byte length: 2 bytes, range 0-65,535 // key name byte length: 2 bytes, range 0-65,535
// UTF-8 encoded key name byte[] // UTF-8 encoded key name byte[]
@ -181,6 +229,7 @@ namespace Microsoft.AspNetCore.Session
{ {
output.WriteByte(SerializationRevision); output.WriteByte(SerializationRevision);
SerializeNumAs3Bytes(output, _store.Count); SerializeNumAs3Bytes(output, _store.Count);
output.Write(IdBytes, 0, IdByteCount);
foreach (var entry in _store) foreach (var entry in _store)
{ {
@ -203,6 +252,8 @@ namespace Microsoft.AspNetCore.Session
} }
int expectedEntries = DeserializeNumFrom3Bytes(content); int expectedEntries = DeserializeNumFrom3Bytes(content);
_sessionIdBytes = ReadBytes(content, IdByteCount);
for (int i = 0; i < expectedEntries; i++) for (int i = 0; i < expectedEntries; i++)
{ {
int keyLength = DeserializeNumFrom2Bytes(content); int keyLength = DeserializeNumFrom2Bytes(content);
@ -210,6 +261,12 @@ namespace Microsoft.AspNetCore.Session
int dataLength = DeserializeNumFrom4Bytes(content); int dataLength = DeserializeNumFrom4Bytes(content);
_store[key] = ReadBytes(content, dataLength); _store[key] = ReadBytes(content, dataLength);
} }
if (_logger.IsEnabled(LogLevel.Debug))
{
_sessionId = new Guid(_sessionIdBytes).ToString();
_logger.SessionLoaded(_sessionKey, _sessionId, expectedEntries);
}
} }
private void SerializeNumAs2Bytes(Stream output, int num) private void SerializeNumAs2Bytes(Stream output, int num)

View File

@ -37,11 +37,11 @@ namespace Microsoft.AspNetCore.Session
} }
} }
public ISession Create(string sessionId, TimeSpan idleTimeout, Func<bool> tryEstablishSession, bool isNewSessionKey) public ISession Create(string sessionKey, TimeSpan idleTimeout, Func<bool> tryEstablishSession, bool isNewSessionKey)
{ {
if (string.IsNullOrEmpty(sessionId)) if (string.IsNullOrEmpty(sessionKey))
{ {
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(sessionId)); throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(sessionKey));
} }
if (tryEstablishSession == null) if (tryEstablishSession == null)
@ -49,7 +49,7 @@ namespace Microsoft.AspNetCore.Session
throw new ArgumentNullException(nameof(tryEstablishSession)); throw new ArgumentNullException(nameof(tryEstablishSession));
} }
return new DistributedSession(_cache, sessionId, idleTimeout, tryEstablishSession, _loggerFactory, isNewSessionKey); return new DistributedSession(_cache, sessionKey, idleTimeout, tryEstablishSession, _loggerFactory, isNewSessionKey);
} }
} }
} }

View File

@ -10,6 +10,6 @@ namespace Microsoft.AspNetCore.Session
{ {
bool IsAvailable { get; } bool IsAvailable { get; }
ISession Create(string sessionId, TimeSpan idleTimeout, Func<bool> tryEstablishSession, bool isNewSessionKey); ISession Create(string sessionKey, TimeSpan idleTimeout, Func<bool> tryEstablishSession, bool isNewSessionKey);
} }
} }

View File

@ -9,7 +9,9 @@ namespace Microsoft.Extensions.Logging
{ {
private static Action<ILogger, Exception> _errorClosingTheSession; private static Action<ILogger, Exception> _errorClosingTheSession;
private static Action<ILogger, string, Exception> _accessingExpiredSession; private static Action<ILogger, string, Exception> _accessingExpiredSession;
private static Action<ILogger, string, Exception> _sessionStarted; private static Action<ILogger, string, string, Exception> _sessionStarted;
private static Action<ILogger, string, string, int, Exception> _sessionLoaded;
private static Action<ILogger, string, string, int, Exception> _sessionStored;
static LoggingExtensions() static LoggingExtensions()
{ {
@ -20,11 +22,19 @@ namespace Microsoft.Extensions.Logging
_accessingExpiredSession = LoggerMessage.Define<string>( _accessingExpiredSession = LoggerMessage.Define<string>(
eventId: 2, eventId: 2,
logLevel: LogLevel.Warning, logLevel: LogLevel.Warning,
formatString: "Accessing expired session {SessionId}"); formatString: "Accessing expired session; Key:{sessionKey}");
_sessionStarted = LoggerMessage.Define<string>( _sessionStarted = LoggerMessage.Define<string, string>(
eventId: 3, eventId: 3,
logLevel: LogLevel.Information, logLevel: LogLevel.Information,
formatString: "Session {SessionId} started"); formatString: "Session started; Key:{sessionKey}, Id:{sessionId}");
_sessionLoaded = LoggerMessage.Define<string, string, int>(
eventId: 4,
logLevel: LogLevel.Debug,
formatString: "Session loaded; Key:{sessionKey}, Id:{sessionId}, Count:{count}");
_sessionStored = LoggerMessage.Define<string, string, int>(
eventId: 5,
logLevel: LogLevel.Debug,
formatString: "Session stored; Key:{sessionKey}, Id:{sessionId}, Count:{count}");
} }
public static void ErrorClosingTheSession(this ILogger logger, Exception exception) public static void ErrorClosingTheSession(this ILogger logger, Exception exception)
@ -32,14 +42,24 @@ namespace Microsoft.Extensions.Logging
_errorClosingTheSession(logger, exception); _errorClosingTheSession(logger, exception);
} }
public static void AccessingExpiredSession(this ILogger logger, string sessionId) public static void AccessingExpiredSession(this ILogger logger, string sessionKey)
{ {
_accessingExpiredSession(logger, sessionId, null); _accessingExpiredSession(logger, sessionKey, null);
} }
public static void SessionStarted(this ILogger logger, string sessionId) public static void SessionStarted(this ILogger logger, string sessionKey, string sessionId)
{ {
_sessionStarted(logger, sessionId, null); _sessionStarted(logger, sessionKey, sessionId, null);
}
public static void SessionLoaded(this ILogger logger, string sessionKey, string sessionId, int count)
{
_sessionLoaded(logger, sessionKey, sessionId, count, null);
}
public static void SessionStored(this ILogger logger, string sessionKey, string sessionId, int count)
{
_sessionStored(logger, sessionKey, sessionId, count, null);
} }
} }
} }

View File

@ -262,9 +262,11 @@ namespace Microsoft.AspNetCore.Session
var sessionLogMessages = sink.Writes.OnlyMessagesFromSource<DistributedSession>().ToArray(); var sessionLogMessages = sink.Writes.OnlyMessagesFromSource<DistributedSession>().ToArray();
Assert.Single(sessionLogMessages); Assert.Equal(2, sessionLogMessages.Length);
Assert.Contains("started", sessionLogMessages[0].State.ToString()); Assert.Contains("started", sessionLogMessages[0].State.ToString());
Assert.Equal(LogLevel.Information, sessionLogMessages[0].LogLevel); Assert.Equal(LogLevel.Information, sessionLogMessages[0].LogLevel);
Assert.Contains("stored", sessionLogMessages[1].State.ToString());
Assert.Equal(LogLevel.Debug, sessionLogMessages[1].LogLevel);
} }
} }
@ -315,11 +317,13 @@ namespace Microsoft.AspNetCore.Session
var sessionLogMessages = sink.Writes.OnlyMessagesFromSource<DistributedSession>().ToArray(); var sessionLogMessages = sink.Writes.OnlyMessagesFromSource<DistributedSession>().ToArray();
Assert.Equal(2, sessionLogMessages.Length); Assert.Equal(3, sessionLogMessages.Length);
Assert.Contains("started", sessionLogMessages[0].State.ToString()); Assert.Contains("started", sessionLogMessages[0].State.ToString());
Assert.Contains("expired", sessionLogMessages[1].State.ToString()); Assert.Contains("stored", sessionLogMessages[1].State.ToString());
Assert.Contains("expired", sessionLogMessages[2].State.ToString());
Assert.Equal(LogLevel.Information, sessionLogMessages[0].LogLevel); Assert.Equal(LogLevel.Information, sessionLogMessages[0].LogLevel);
Assert.Equal(LogLevel.Warning, sessionLogMessages[1].LogLevel); Assert.Equal(LogLevel.Debug, sessionLogMessages[1].LogLevel);
Assert.Equal(LogLevel.Warning, sessionLogMessages[2].LogLevel);
} }
} }