diff --git a/src/Session/.gitignore b/src/Session/.gitignore
new file mode 100644
index 0000000000..f332e76e0f
--- /dev/null
+++ b/src/Session/.gitignore
@@ -0,0 +1,33 @@
+[Oo]bj/
+[Bb]in/
+TestResults/
+.nuget/
+*.sln.ide/
+_ReSharper.*/
+packages/
+artifacts/
+PublishProfiles/
+*.user
+*.suo
+*.cache
+*.docstates
+_ReSharper.*
+nuget.exe
+*net45.csproj
+*net451.csproj
+*k10.csproj
+*.psess
+*.vsp
+*.pidb
+*.userprefs
+*DS_Store
+*.ncrunchsolution
+*.*sdf
+*.ipch
+.vs/
+.vscode/
+project.lock.json
+.build/
+.testPublish/
+launchSettings.json
+global.json
diff --git a/src/Session/Directory.Build.props b/src/Session/Directory.Build.props
new file mode 100644
index 0000000000..c2790fd6a6
--- /dev/null
+++ b/src/Session/Directory.Build.props
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+ Microsoft ASP.NET Core
+ https://github.com/aspnet/Session
+ git
+ $(MSBuildThisFileDirectory)
+ $(MSBuildThisFileDirectory)build\Key.snk
+ true
+ true
+ true
+
+
+
diff --git a/src/Session/Directory.Build.targets b/src/Session/Directory.Build.targets
new file mode 100644
index 0000000000..53b3f6e1da
--- /dev/null
+++ b/src/Session/Directory.Build.targets
@@ -0,0 +1,7 @@
+
+
+ $(MicrosoftNETCoreApp20PackageVersion)
+ $(MicrosoftNETCoreApp21PackageVersion)
+ $(NETStandardLibrary20PackageVersion)
+
+
diff --git a/src/Session/NuGetPackageVerifier.json b/src/Session/NuGetPackageVerifier.json
new file mode 100644
index 0000000000..b153ab1515
--- /dev/null
+++ b/src/Session/NuGetPackageVerifier.json
@@ -0,0 +1,7 @@
+{
+ "Default": {
+ "rules": [
+ "DefaultCompositeRule"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/src/Session/README.md b/src/Session/README.md
new file mode 100644
index 0000000000..976f9f21cf
--- /dev/null
+++ b/src/Session/README.md
@@ -0,0 +1,12 @@
+Session
+================
+
+AppVeyor: [](https://ci.appveyor.com/project/aspnetci/Session/branch/dev)
+
+Travis: [](https://travis-ci.org/aspnet/Session)
+
+Contains libraries for session state middleware for ASP.NET Core.
+
+For ASP.NET 4.x session state, please go to https://github.com/aspnet/AspNetSessionState.
+
+This project is part of ASP.NET Core. You can find samples, documentation and getting started instructions for ASP.NET Core at the [Home](https://github.com/aspnet/home) repo.
diff --git a/src/Session/Session.sln b/src/Session/Session.sln
new file mode 100644
index 0000000000..d8d95dc295
--- /dev/null
+++ b/src/Session/Session.sln
@@ -0,0 +1,74 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.26621.2
+MinimumVisualStudioVersion = 15.0.26730.03
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{E9D63F97-6078-42AD-BFD3-F956BF921BB5}"
+ ProjectSection(SolutionItems) = preProject
+ test\Directory.Build.props = test\Directory.Build.props
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A189F10C-3A9C-4F81-83D0-32E5FE50DAD8}"
+ ProjectSection(SolutionItems) = preProject
+ src\Directory.Build.props = src\Directory.Build.props
+ EndProjectSection
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Session", "src\Microsoft.AspNetCore.Session\Microsoft.AspNetCore.Session.csproj", "{71802736-F640-4733-9671-02D267EDD76A}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Session.Tests", "test\Microsoft.AspNetCore.Session.Tests\Microsoft.AspNetCore.Session.Tests.csproj", "{8C131A0A-BC1A-4CF3-8B77-8813FBFE5639}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{94E80ED2-9F27-40AC-A9EF-C707BDFAA3BE}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SessionSample", "samples\SessionSample\SessionSample.csproj", "{FE0B9969-3BDE-4A7D-BE1B-47EAE8DBF365}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionItems", "{3B45F658-5BF1-4E07-BE9C-6F5110AC2277}"
+ ProjectSection(SolutionItems) = preProject
+ .appveyor.yml = .appveyor.yml
+ .gitattributes = .gitattributes
+ .gitignore = .gitignore
+ .travis.yml = .travis.yml
+ Directory.Build.props = Directory.Build.props
+ Directory.Build.targets = Directory.Build.targets
+ NuGet.config = NuGet.config
+ NuGetPackageVerifier.json = NuGetPackageVerifier.json
+ README.md = README.md
+ version.xml = version.xml
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{4F21221F-2813-41B7-AAFC-E03FD52971CC}"
+ ProjectSection(SolutionItems) = preProject
+ build\common.props = build\common.props
+ build\dependencies.props = build\dependencies.props
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {71802736-F640-4733-9671-02D267EDD76A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {71802736-F640-4733-9671-02D267EDD76A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {71802736-F640-4733-9671-02D267EDD76A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {71802736-F640-4733-9671-02D267EDD76A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8C131A0A-BC1A-4CF3-8B77-8813FBFE5639}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8C131A0A-BC1A-4CF3-8B77-8813FBFE5639}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8C131A0A-BC1A-4CF3-8B77-8813FBFE5639}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8C131A0A-BC1A-4CF3-8B77-8813FBFE5639}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FE0B9969-3BDE-4A7D-BE1B-47EAE8DBF365}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FE0B9969-3BDE-4A7D-BE1B-47EAE8DBF365}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FE0B9969-3BDE-4A7D-BE1B-47EAE8DBF365}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FE0B9969-3BDE-4A7D-BE1B-47EAE8DBF365}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {71802736-F640-4733-9671-02D267EDD76A} = {A189F10C-3A9C-4F81-83D0-32E5FE50DAD8}
+ {8C131A0A-BC1A-4CF3-8B77-8813FBFE5639} = {E9D63F97-6078-42AD-BFD3-F956BF921BB5}
+ {FE0B9969-3BDE-4A7D-BE1B-47EAE8DBF365} = {94E80ED2-9F27-40AC-A9EF-C707BDFAA3BE}
+ {4F21221F-2813-41B7-AAFC-E03FD52971CC} = {3B45F658-5BF1-4E07-BE9C-6F5110AC2277}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {6AE224B9-B604-4E47-9617-9D114DAE9BE5}
+ EndGlobalSection
+EndGlobal
diff --git a/src/Session/build/Key.snk b/src/Session/build/Key.snk
new file mode 100644
index 0000000000..e10e4889c1
Binary files /dev/null and b/src/Session/build/Key.snk differ
diff --git a/src/Session/build/dependencies.props b/src/Session/build/dependencies.props
new file mode 100644
index 0000000000..6e86d21f90
--- /dev/null
+++ b/src/Session/build/dependencies.props
@@ -0,0 +1,36 @@
+
+
+ $(MSBuildAllProjects);$(MSBuildThisFileFullPath)
+
+
+
+
+ 2.1.3-rtm-15802
+ 2.0.0
+ 2.1.2
+ 15.6.1
+ 2.0.3
+ 2.3.1
+ 2.4.0-beta.1.build3945
+
+
+
+
+
+
+
+ 2.1.1
+ 2.1.1
+ 2.1.1
+ 2.1.2
+ 2.1.1
+ 2.1.1
+ 2.1.1
+ 2.1.1
+ 2.1.1
+ 2.1.1
+ 2.1.1
+ 2.1.1
+ 2.1.1
+
+
\ No newline at end of file
diff --git a/src/Session/build/repo.props b/src/Session/build/repo.props
new file mode 100644
index 0000000000..dab1601c88
--- /dev/null
+++ b/src/Session/build/repo.props
@@ -0,0 +1,15 @@
+
+
+
+
+
+ Internal.AspNetCore.Universe.Lineup
+ 2.1.0-rc1-*
+ https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json
+
+
+
+
+
+
+
diff --git a/src/Session/build/sources.props b/src/Session/build/sources.props
new file mode 100644
index 0000000000..9215df9751
--- /dev/null
+++ b/src/Session/build/sources.props
@@ -0,0 +1,17 @@
+
+
+
+
+ $(DotNetRestoreSources)
+
+ $(RestoreSources);
+ https://dotnet.myget.org/F/dotnet-core/api/v3/index.json;
+ https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json;
+ https://dotnet.myget.org/F/aspnetcore-tools/api/v3/index.json;
+
+
+ $(RestoreSources);
+ https://api.nuget.org/v3/index.json;
+
+
+
diff --git a/src/Session/samples/SessionSample/Properties/launchSettings.json b/src/Session/samples/SessionSample/Properties/launchSettings.json
new file mode 100644
index 0000000000..6bab3d3602
--- /dev/null
+++ b/src/Session/samples/SessionSample/Properties/launchSettings.json
@@ -0,0 +1,27 @@
+{
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:2481/",
+ "sslPort": 0
+ }
+ },
+ "profiles": {
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "SessionSample": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "launchUrl": "http://localhost:5000",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Session/samples/SessionSample/SessionSample.csproj b/src/Session/samples/SessionSample/SessionSample.csproj
new file mode 100644
index 0000000000..67abefae09
--- /dev/null
+++ b/src/Session/samples/SessionSample/SessionSample.csproj
@@ -0,0 +1,20 @@
+
+
+
+ netcoreapp2.1;net461
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Session/samples/SessionSample/Startup.cs b/src/Session/samples/SessionSample/Startup.cs
new file mode 100644
index 0000000000..41fcb566c5
--- /dev/null
+++ b/src/Session/samples/SessionSample/Startup.cs
@@ -0,0 +1,90 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace SessionSample
+{
+ public class Startup
+ {
+ public void ConfigureServices(IServiceCollection services)
+ {
+ // Adds a default in-memory implementation of IDistributedCache
+ services.AddDistributedMemoryCache();
+
+ // Uncomment the following line to use the Microsoft SQL Server implementation of IDistributedCache.
+ // Note that this would require setting up the session state database.
+ //services.AddSqlServerCache(o =>
+ //{
+ // o.ConnectionString = "Server=.;Database=ASPNET5SessionState;Trusted_Connection=True;";
+ // o.SchemaName = "dbo";
+ // o.TableName = "Sessions";
+ //});
+
+ // Uncomment the following line to use the Redis implementation of IDistributedCache.
+ // This will override any previously registered IDistributedCache service.
+ //services.AddDistributedRedisCache(o =>
+ //{
+ // o.Configuration = "localhost";
+ // o.InstanceName = "SampleInstance";
+ //});
+
+ services.AddSession(o =>
+ {
+ o.IdleTimeout = TimeSpan.FromSeconds(10);
+ });
+ }
+
+ public void Configure(IApplicationBuilder app)
+ {
+ app.UseSession();
+
+ app.Map("/session", subApp =>
+ {
+ subApp.Run(async context =>
+ {
+ int visits = 0;
+ visits = context.Session.GetInt32("visits") ?? 0;
+ context.Session.SetInt32("visits", ++visits);
+ await context.Response.WriteAsync("Counting: You have visited our page this many times: " + visits);
+ });
+ });
+
+ app.Run(async context =>
+ {
+ int visits = 0;
+ visits = context.Session.GetInt32("visits") ?? 0;
+ await context.Response.WriteAsync("
");
+ if (visits == 0)
+ {
+ await context.Response.WriteAsync("Your session has not been established.
");
+ await context.Response.WriteAsync(DateTime.Now + "
");
+ await context.Response.WriteAsync("Establish session.
");
+ }
+ else
+ {
+ context.Session.SetInt32("visits", ++visits);
+ await context.Response.WriteAsync("Your session was located, you've visited the site this many times: " + visits);
+ }
+ await context.Response.WriteAsync("");
+ });
+ }
+
+ public static void Main(string[] args)
+ {
+ var host = new WebHostBuilder()
+ .ConfigureLogging(factory => factory.AddConsole())
+ .UseKestrel()
+ .UseIISIntegration()
+ .UseStartup()
+ .Build();
+
+ host.Run();
+ }
+ }
+}
diff --git a/src/Session/src/Directory.Build.props b/src/Session/src/Directory.Build.props
new file mode 100644
index 0000000000..1e0980f663
--- /dev/null
+++ b/src/Session/src/Directory.Build.props
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/src/Session/src/Microsoft.AspNetCore.Session/CookieProtection.cs b/src/Session/src/Microsoft.AspNetCore.Session/CookieProtection.cs
new file mode 100644
index 0000000000..64a3a3fbbf
--- /dev/null
+++ b/src/Session/src/Microsoft.AspNetCore.Session/CookieProtection.cs
@@ -0,0 +1,71 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Text;
+using Microsoft.AspNetCore.DataProtection;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Session
+{
+ internal static class CookieProtection
+ {
+ internal static string Protect(IDataProtector protector, string data)
+ {
+ if (protector == null)
+ {
+ throw new ArgumentNullException(nameof(protector));
+ }
+ if (string.IsNullOrEmpty(data))
+ {
+ return data;
+ }
+
+ var userData = Encoding.UTF8.GetBytes(data);
+
+ var protectedData = protector.Protect(userData);
+ return Convert.ToBase64String(protectedData).TrimEnd('=');
+ }
+
+ internal static string Unprotect(IDataProtector protector, string protectedText, ILogger logger)
+ {
+ try
+ {
+ if (string.IsNullOrEmpty(protectedText))
+ {
+ return string.Empty;
+ }
+
+ var protectedData = Convert.FromBase64String(Pad(protectedText));
+ if (protectedData == null)
+ {
+ return string.Empty;
+ }
+
+ var userData = protector.Unprotect(protectedData);
+ if (userData == null)
+ {
+ return string.Empty;
+ }
+
+ return Encoding.UTF8.GetString(userData);
+ }
+ catch (Exception ex)
+ {
+ // Log the exception, but do not leak other information
+ logger.ErrorUnprotectingSessionCookie(ex);
+ return string.Empty;
+ }
+ }
+
+ private static string Pad(string text)
+ {
+ var padding = 3 - ((text.Length + 3) % 4);
+ if (padding == 0)
+ {
+ return text;
+ }
+ return text + new string('=', padding);
+ }
+ }
+}
diff --git a/src/Session/src/Microsoft.AspNetCore.Session/DistributedSession.cs b/src/Session/src/Microsoft.AspNetCore.Session/DistributedSession.cs
new file mode 100644
index 0000000000..76ab3f3cf5
--- /dev/null
+++ b/src/Session/src/Microsoft.AspNetCore.Session/DistributedSession.cs
@@ -0,0 +1,425 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Caching.Distributed;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Session
+{
+ public class DistributedSession : ISession
+ {
+ private static readonly RandomNumberGenerator CryptoRandom = RandomNumberGenerator.Create();
+ private const int IdByteCount = 16;
+
+ private const byte SerializationRevision = 2;
+ private const int KeyLengthLimit = ushort.MaxValue;
+
+ private readonly IDistributedCache _cache;
+ private readonly string _sessionKey;
+ private readonly TimeSpan _idleTimeout;
+ private readonly TimeSpan _ioTimeout;
+ private readonly Func _tryEstablishSession;
+ private readonly ILogger _logger;
+ private IDictionary _store;
+ private bool _isModified;
+ private bool _loaded;
+ private bool _isAvailable;
+ private bool _isNewSessionKey;
+ private string _sessionId;
+ private byte[] _sessionIdBytes;
+
+ public DistributedSession(
+ IDistributedCache cache,
+ string sessionKey,
+ TimeSpan idleTimeout,
+ TimeSpan ioTimeout,
+ Func tryEstablishSession,
+ ILoggerFactory loggerFactory,
+ bool isNewSessionKey)
+ {
+ if (cache == null)
+ {
+ throw new ArgumentNullException(nameof(cache));
+ }
+
+ if (string.IsNullOrEmpty(sessionKey))
+ {
+ throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(sessionKey));
+ }
+
+ if (tryEstablishSession == null)
+ {
+ throw new ArgumentNullException(nameof(tryEstablishSession));
+ }
+
+ if (loggerFactory == null)
+ {
+ throw new ArgumentNullException(nameof(loggerFactory));
+ }
+
+ _cache = cache;
+ _sessionKey = sessionKey;
+ _idleTimeout = idleTimeout;
+ _ioTimeout = ioTimeout;
+ _tryEstablishSession = tryEstablishSession;
+ _store = new Dictionary();
+ _logger = loggerFactory.CreateLogger();
+ _isNewSessionKey = isNewSessionKey;
+ }
+
+ public bool IsAvailable
+ {
+ get
+ {
+ Load();
+ return _isAvailable;
+ }
+ }
+
+ public string Id
+ {
+ get
+ {
+ Load();
+ if (_sessionId == null)
+ {
+ _sessionId = new Guid(IdBytes).ToString();
+ }
+ return _sessionId;
+ }
+ }
+
+ private byte[] IdBytes
+ {
+ get
+ {
+ if (IsAvailable && _sessionIdBytes == null)
+ {
+ _sessionIdBytes = new byte[IdByteCount];
+ CryptoRandom.GetBytes(_sessionIdBytes);
+ }
+ return _sessionIdBytes;
+ }
+ }
+
+ public IEnumerable Keys
+ {
+ get
+ {
+ Load();
+ return _store.Keys.Select(key => key.KeyString);
+ }
+ }
+
+ public bool TryGetValue(string key, out byte[] value)
+ {
+ Load();
+ return _store.TryGetValue(new EncodedKey(key), out value);
+ }
+
+ public void Set(string key, byte[] value)
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+
+ if (IsAvailable)
+ {
+ var encodedKey = new EncodedKey(key);
+ if (encodedKey.KeyBytes.Length > KeyLengthLimit)
+ {
+ throw new ArgumentOutOfRangeException(nameof(key),
+ Resources.FormatException_KeyLengthIsExceeded(KeyLengthLimit));
+ }
+
+ if (!_tryEstablishSession())
+ {
+ throw new InvalidOperationException(Resources.Exception_InvalidSessionEstablishment);
+ }
+ _isModified = true;
+ byte[] copy = new byte[value.Length];
+ Buffer.BlockCopy(src: value, srcOffset: 0, dst: copy, dstOffset: 0, count: value.Length);
+ _store[encodedKey] = copy;
+ }
+ }
+
+ public void Remove(string key)
+ {
+ Load();
+ _isModified |= _store.Remove(new EncodedKey(key));
+ }
+
+ public void Clear()
+ {
+ Load();
+ _isModified |= _store.Count > 0;
+ _store.Clear();
+ }
+
+ private void Load()
+ {
+ if (!_loaded)
+ {
+ try
+ {
+ var data = _cache.Get(_sessionKey);
+ if (data != null)
+ {
+ Deserialize(new MemoryStream(data));
+ }
+ else if (!_isNewSessionKey)
+ {
+ _logger.AccessingExpiredSession(_sessionKey);
+ }
+ _isAvailable = true;
+ }
+ catch (Exception exception)
+ {
+ _logger.SessionCacheReadException(_sessionKey, exception);
+ _isAvailable = false;
+ _sessionId = string.Empty;
+ _sessionIdBytes = null;
+ _store = new NoOpSessionStore();
+ }
+ finally
+ {
+ _loaded = true;
+ }
+ }
+ }
+
+ // This will throw if called directly and a failure occurs. The user is expected to handle the failures.
+ public async Task LoadAsync(CancellationToken cancellationToken = default)
+ {
+ if (!_loaded)
+ {
+ using (var timeout = new CancellationTokenSource(_ioTimeout))
+ {
+ var cts = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, cancellationToken);
+ try
+ {
+ cts.Token.ThrowIfCancellationRequested();
+ var data = await _cache.GetAsync(_sessionKey, cts.Token);
+ if (data != null)
+ {
+ Deserialize(new MemoryStream(data));
+ }
+ else if (!_isNewSessionKey)
+ {
+ _logger.AccessingExpiredSession(_sessionKey);
+ }
+ }
+ catch (OperationCanceledException oex)
+ {
+ if (timeout.Token.IsCancellationRequested)
+ {
+ _logger.SessionLoadingTimeout();
+ throw new OperationCanceledException("Timed out loading the session.", oex, timeout.Token);
+ }
+ throw;
+ }
+ }
+ _isAvailable = true;
+ _loaded = true;
+ }
+ }
+
+ public async Task CommitAsync(CancellationToken cancellationToken = default)
+ {
+ using (var timeout = new CancellationTokenSource(_ioTimeout))
+ {
+ var cts = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, cancellationToken);
+ if (_isModified)
+ {
+ if (_logger.IsEnabled(LogLevel.Information))
+ {
+ // This operation is only so we can log if the session already existed.
+ // Log and ignore failures.
+ try
+ {
+ cts.Token.ThrowIfCancellationRequested();
+ var data = await _cache.GetAsync(_sessionKey, cts.Token);
+ if (data == null)
+ {
+ _logger.SessionStarted(_sessionKey, Id);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ }
+ catch (Exception exception)
+ {
+ _logger.SessionCacheReadException(_sessionKey, exception);
+ }
+ }
+
+ var stream = new MemoryStream();
+ Serialize(stream);
+
+ try
+ {
+ cts.Token.ThrowIfCancellationRequested();
+ await _cache.SetAsync(
+ _sessionKey,
+ stream.ToArray(),
+ new DistributedCacheEntryOptions().SetSlidingExpiration(_idleTimeout),
+ cts.Token);
+ _isModified = false;
+ _logger.SessionStored(_sessionKey, Id, _store.Count);
+ }
+ catch (OperationCanceledException oex)
+ {
+ if (timeout.Token.IsCancellationRequested)
+ {
+ _logger.SessionCommitTimeout();
+ throw new OperationCanceledException("Timed out committing the session.", oex, timeout.Token);
+ }
+ throw;
+ }
+ }
+ else
+ {
+ try
+ {
+ await _cache.RefreshAsync(_sessionKey, cts.Token);
+ }
+ catch (OperationCanceledException oex)
+ {
+ if (timeout.Token.IsCancellationRequested)
+ {
+ _logger.SessionRefreshTimeout();
+ throw new OperationCanceledException("Timed out refreshing the session.", oex, timeout.Token);
+ }
+ throw;
+ }
+ }
+ }
+ }
+
+ // Format:
+ // Serialization revision: 1 byte, range 0-255
+ // Entry count: 3 bytes, range 0-16,777,215
+ // SessionId: IdByteCount bytes (16)
+ // foreach entry:
+ // key name byte length: 2 bytes, range 0-65,535
+ // UTF-8 encoded key name byte[]
+ // data byte length: 4 bytes, range 0-2,147,483,647
+ // data byte[]
+ private void Serialize(Stream output)
+ {
+ output.WriteByte(SerializationRevision);
+ SerializeNumAs3Bytes(output, _store.Count);
+ output.Write(IdBytes, 0, IdByteCount);
+
+ foreach (var entry in _store)
+ {
+ var keyBytes = entry.Key.KeyBytes;
+ SerializeNumAs2Bytes(output, keyBytes.Length);
+ output.Write(keyBytes, 0, keyBytes.Length);
+ SerializeNumAs4Bytes(output, entry.Value.Length);
+ output.Write(entry.Value, 0, entry.Value.Length);
+ }
+ }
+
+ private void Deserialize(Stream content)
+ {
+ if (content == null || content.ReadByte() != SerializationRevision)
+ {
+ // Replace the un-readable format.
+ _isModified = true;
+ return;
+ }
+
+ int expectedEntries = DeserializeNumFrom3Bytes(content);
+ _sessionIdBytes = ReadBytes(content, IdByteCount);
+
+ for (int i = 0; i < expectedEntries; i++)
+ {
+ int keyLength = DeserializeNumFrom2Bytes(content);
+ var key = new EncodedKey(ReadBytes(content, keyLength));
+ int dataLength = DeserializeNumFrom4Bytes(content);
+ _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)
+ {
+ if (num < 0 || ushort.MaxValue < num)
+ {
+ throw new ArgumentOutOfRangeException(nameof(num), Resources.Exception_InvalidToSerializeIn2Bytes);
+ }
+ output.WriteByte((byte)(num >> 8));
+ output.WriteByte((byte)(0xFF & num));
+ }
+
+ private int DeserializeNumFrom2Bytes(Stream content)
+ {
+ return content.ReadByte() << 8 | content.ReadByte();
+ }
+
+ private void SerializeNumAs3Bytes(Stream output, int num)
+ {
+ if (num < 0 || 0xFFFFFF < num)
+ {
+ throw new ArgumentOutOfRangeException(nameof(num), Resources.Exception_InvalidToSerializeIn3Bytes);
+ }
+ output.WriteByte((byte)(num >> 16));
+ output.WriteByte((byte)(0xFF & (num >> 8)));
+ output.WriteByte((byte)(0xFF & num));
+ }
+
+ private int DeserializeNumFrom3Bytes(Stream content)
+ {
+ return content.ReadByte() << 16 | content.ReadByte() << 8 | content.ReadByte();
+ }
+
+ private void SerializeNumAs4Bytes(Stream output, int num)
+ {
+ if (num < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(num), Resources.Exception_NumberShouldNotBeNegative);
+ }
+ output.WriteByte((byte)(num >> 24));
+ output.WriteByte((byte)(0xFF & (num >> 16)));
+ output.WriteByte((byte)(0xFF & (num >> 8)));
+ output.WriteByte((byte)(0xFF & num));
+ }
+
+ private int DeserializeNumFrom4Bytes(Stream content)
+ {
+ return content.ReadByte() << 24 | content.ReadByte() << 16 | content.ReadByte() << 8 | content.ReadByte();
+ }
+
+ private byte[] ReadBytes(Stream stream, int count)
+ {
+ var output = new byte[count];
+ int total = 0;
+ while (total < count)
+ {
+ var read = stream.Read(output, total, count - total);
+ if (read == 0)
+ {
+ throw new EndOfStreamException();
+ }
+ total += read;
+ }
+ return output;
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/Session/src/Microsoft.AspNetCore.Session/DistributedSessionStore.cs b/src/Session/src/Microsoft.AspNetCore.Session/DistributedSessionStore.cs
new file mode 100644
index 0000000000..49050af588
--- /dev/null
+++ b/src/Session/src/Microsoft.AspNetCore.Session/DistributedSessionStore.cs
@@ -0,0 +1,47 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Caching.Distributed;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Session
+{
+ public class DistributedSessionStore : ISessionStore
+ {
+ private readonly IDistributedCache _cache;
+ private readonly ILoggerFactory _loggerFactory;
+
+ public DistributedSessionStore(IDistributedCache cache, ILoggerFactory loggerFactory)
+ {
+ if (cache == null)
+ {
+ throw new ArgumentNullException(nameof(cache));
+ }
+
+ if (loggerFactory == null)
+ {
+ throw new ArgumentNullException(nameof(loggerFactory));
+ }
+
+ _cache = cache;
+ _loggerFactory = loggerFactory;
+ }
+
+ public ISession Create(string sessionKey, TimeSpan idleTimeout, TimeSpan ioTimeout, Func tryEstablishSession, bool isNewSessionKey)
+ {
+ if (string.IsNullOrEmpty(sessionKey))
+ {
+ throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(sessionKey));
+ }
+
+ if (tryEstablishSession == null)
+ {
+ throw new ArgumentNullException(nameof(tryEstablishSession));
+ }
+
+ return new DistributedSession(_cache, sessionKey, idleTimeout, ioTimeout, tryEstablishSession, _loggerFactory, isNewSessionKey);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Session/src/Microsoft.AspNetCore.Session/EncodedKey.cs b/src/Session/src/Microsoft.AspNetCore.Session/EncodedKey.cs
new file mode 100644
index 0000000000..ac169542f5
--- /dev/null
+++ b/src/Session/src/Microsoft.AspNetCore.Session/EncodedKey.cs
@@ -0,0 +1,80 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Text;
+
+namespace Microsoft.AspNetCore.Session
+{
+ // Keys are stored in their utf-8 encoded state.
+ // This saves us from de-serializing and re-serializing every key on every request.
+ internal class EncodedKey
+ {
+ private string _keyString;
+ private int? _hashCode;
+
+ internal EncodedKey(string key)
+ {
+ _keyString = key;
+ KeyBytes = Encoding.UTF8.GetBytes(key);
+ }
+
+ public EncodedKey(byte[] key)
+ {
+ KeyBytes = key;
+ }
+
+ internal string KeyString
+ {
+ get
+ {
+ if (_keyString == null)
+ {
+ _keyString = Encoding.UTF8.GetString(KeyBytes, 0, KeyBytes.Length);
+ }
+ return _keyString;
+ }
+ }
+
+ internal byte[] KeyBytes { get; private set; }
+
+ public override bool Equals(object obj)
+ {
+ var otherKey = obj as EncodedKey;
+ if (otherKey == null)
+ {
+ return false;
+ }
+ if (KeyBytes.Length != otherKey.KeyBytes.Length)
+ {
+ return false;
+ }
+ if (_hashCode.HasValue && otherKey._hashCode.HasValue
+ && _hashCode.Value != otherKey._hashCode.Value)
+ {
+ return false;
+ }
+ for (int i = 0; i < KeyBytes.Length; i++)
+ {
+ if (KeyBytes[i] != otherKey.KeyBytes[i])
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public override int GetHashCode()
+ {
+ if (!_hashCode.HasValue)
+ {
+ _hashCode = SipHash.GetHashCode(KeyBytes);
+ }
+ return _hashCode.Value;
+ }
+
+ public override string ToString()
+ {
+ return KeyString;
+ }
+ }
+}
diff --git a/src/Session/src/Microsoft.AspNetCore.Session/ISessionStore.cs b/src/Session/src/Microsoft.AspNetCore.Session/ISessionStore.cs
new file mode 100644
index 0000000000..247ba2307f
--- /dev/null
+++ b/src/Session/src/Microsoft.AspNetCore.Session/ISessionStore.cs
@@ -0,0 +1,13 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Session
+{
+ public interface ISessionStore
+ {
+ ISession Create(string sessionKey, TimeSpan idleTimeout, TimeSpan ioTimeout, Func tryEstablishSession, bool isNewSessionKey);
+ }
+}
\ No newline at end of file
diff --git a/src/Session/src/Microsoft.AspNetCore.Session/LoggingExtensions.cs b/src/Session/src/Microsoft.AspNetCore.Session/LoggingExtensions.cs
new file mode 100644
index 0000000000..2552ac20de
--- /dev/null
+++ b/src/Session/src/Microsoft.AspNetCore.Session/LoggingExtensions.cs
@@ -0,0 +1,135 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+
+namespace Microsoft.Extensions.Logging
+{
+ internal static class LoggingExtensions
+ {
+ private static Action _errorClosingTheSession;
+ private static Action _accessingExpiredSession;
+ private static Action _sessionStarted;
+ private static Action _sessionLoaded;
+ private static Action _sessionStored;
+ private static Action _sessionCacheReadException;
+ private static Action _errorUnprotectingCookie;
+ private static Action _sessionLoadingTimeout;
+ private static Action _sessionCommitTimeout;
+ private static Action _sessionCommitCanceled;
+ private static Action _sessionRefreshTimeout;
+ private static Action _sessionRefreshCanceled;
+
+ static LoggingExtensions()
+ {
+ _errorClosingTheSession = LoggerMessage.Define(
+ eventId: 1,
+ logLevel: LogLevel.Error,
+ formatString: "Error closing the session.");
+ _accessingExpiredSession = LoggerMessage.Define(
+ eventId: 2,
+ logLevel: LogLevel.Information,
+ formatString: "Accessing expired session, Key:{sessionKey}");
+ _sessionStarted = LoggerMessage.Define(
+ eventId: 3,
+ logLevel: LogLevel.Information,
+ formatString: "Session started; Key:{sessionKey}, Id:{sessionId}");
+ _sessionLoaded = LoggerMessage.Define(
+ eventId: 4,
+ logLevel: LogLevel.Debug,
+ formatString: "Session loaded; Key:{sessionKey}, Id:{sessionId}, Count:{count}");
+ _sessionStored = LoggerMessage.Define(
+ eventId: 5,
+ logLevel: LogLevel.Debug,
+ formatString: "Session stored; Key:{sessionKey}, Id:{sessionId}, Count:{count}");
+ _sessionCacheReadException = LoggerMessage.Define(
+ eventId: 6,
+ logLevel: LogLevel.Error,
+ formatString: "Session cache read exception, Key:{sessionKey}");
+ _errorUnprotectingCookie = LoggerMessage.Define(
+ eventId: 7,
+ logLevel: LogLevel.Warning,
+ formatString: "Error unprotecting the session cookie.");
+ _sessionLoadingTimeout = LoggerMessage.Define(
+ eventId: 8,
+ logLevel: LogLevel.Warning,
+ formatString: "Loading the session timed out.");
+ _sessionCommitTimeout = LoggerMessage.Define(
+ eventId: 9,
+ logLevel: LogLevel.Warning,
+ formatString: "Committing the session timed out.");
+ _sessionCommitCanceled = LoggerMessage.Define(
+ eventId: 10,
+ logLevel: LogLevel.Information,
+ formatString: "Committing the session was canceled.");
+ _sessionRefreshTimeout = LoggerMessage.Define(
+ eventId: 11,
+ logLevel: LogLevel.Warning,
+ formatString: "Refreshing the session timed out.");
+ _sessionRefreshCanceled = LoggerMessage.Define(
+ eventId: 12,
+ logLevel: LogLevel.Information,
+ formatString: "Refreshing the session was canceled.");
+ }
+
+ public static void ErrorClosingTheSession(this ILogger logger, Exception exception)
+ {
+ _errorClosingTheSession(logger, exception);
+ }
+
+ public static void AccessingExpiredSession(this ILogger logger, string sessionKey)
+ {
+ _accessingExpiredSession(logger, sessionKey, null);
+ }
+
+ public static void SessionStarted(this ILogger logger, string sessionKey, string sessionId)
+ {
+ _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);
+ }
+
+ public static void SessionCacheReadException(this ILogger logger, string sessionKey, Exception exception)
+ {
+ _sessionCacheReadException(logger, sessionKey, exception);
+ }
+
+ public static void ErrorUnprotectingSessionCookie(this ILogger logger, Exception exception)
+ {
+ _errorUnprotectingCookie(logger, exception);
+ }
+
+ public static void SessionLoadingTimeout(this ILogger logger)
+ {
+ _sessionLoadingTimeout(logger, null);
+ }
+
+ public static void SessionCommitTimeout(this ILogger logger)
+ {
+ _sessionCommitTimeout(logger, null);
+ }
+
+ public static void SessionCommitCanceled(this ILogger logger)
+ {
+ _sessionCommitCanceled(logger, null);
+ }
+
+ public static void SessionRefreshTimeout(this ILogger logger)
+ {
+ _sessionRefreshTimeout(logger, null);
+ }
+
+ public static void SessionRefreshCanceled(this ILogger logger)
+ {
+ _sessionRefreshCanceled(logger, null);
+ }
+ }
+}
diff --git a/src/Session/src/Microsoft.AspNetCore.Session/Microsoft.AspNetCore.Session.csproj b/src/Session/src/Microsoft.AspNetCore.Session/Microsoft.AspNetCore.Session.csproj
new file mode 100644
index 0000000000..3da00b9032
--- /dev/null
+++ b/src/Session/src/Microsoft.AspNetCore.Session/Microsoft.AspNetCore.Session.csproj
@@ -0,0 +1,20 @@
+
+
+
+ ASP.NET Core session state middleware.
+ netstandard2.0
+ $(NoWarn);CS1591
+ true
+ true
+ aspnetcore;session;sessionstate
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Session/src/Microsoft.AspNetCore.Session/NoOpSessionStore.cs b/src/Session/src/Microsoft.AspNetCore.Session/NoOpSessionStore.cs
new file mode 100644
index 0000000000..6a89ad3900
--- /dev/null
+++ b/src/Session/src/Microsoft.AspNetCore.Session/NoOpSessionStore.cs
@@ -0,0 +1,59 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Microsoft.AspNetCore.Session
+{
+ internal class NoOpSessionStore : IDictionary
+ {
+ public byte[] this[EncodedKey key]
+ {
+ get
+ {
+ return null;
+ }
+
+ set
+ {
+
+ }
+ }
+
+ public int Count { get; } = 0;
+
+ public bool IsReadOnly { get; } = false;
+
+ public ICollection Keys { get; } = new EncodedKey[0];
+
+ public ICollection Values { get; } = new byte[0][];
+
+ public void Add(KeyValuePair item) { }
+
+ public void Add(EncodedKey key, byte[] value) { }
+
+ public void Clear() { }
+
+ public bool Contains(KeyValuePair item) => false;
+
+ public bool ContainsKey(EncodedKey key) => false;
+
+ public void CopyTo(KeyValuePair[] array, int arrayIndex) { }
+
+ public IEnumerator> GetEnumerator() => Enumerable.Empty>().GetEnumerator();
+
+ public bool Remove(KeyValuePair item) => false;
+
+ public bool Remove(EncodedKey key) => false;
+
+ public bool TryGetValue(EncodedKey key, out byte[] value)
+ {
+ value = null;
+ return false;
+ }
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+ }
+}
diff --git a/src/Session/src/Microsoft.AspNetCore.Session/Properties/Resources.Designer.cs b/src/Session/src/Microsoft.AspNetCore.Session/Properties/Resources.Designer.cs
new file mode 100644
index 0000000000..d071f6b698
--- /dev/null
+++ b/src/Session/src/Microsoft.AspNetCore.Session/Properties/Resources.Designer.cs
@@ -0,0 +1,126 @@
+//
+namespace Microsoft.AspNetCore.Session
+{
+ using System.Globalization;
+ using System.Reflection;
+ using System.Resources;
+
+ internal static class Resources
+ {
+ private static readonly ResourceManager _resourceManager
+ = new ResourceManager("Microsoft.AspNetCore.Session.Resources", typeof(Resources).GetTypeInfo().Assembly);
+
+ ///
+ /// The key cannot be longer than '{0}' when encoded with UTF-8.
+ ///
+ internal static string Exception_KeyLengthIsExceeded
+ {
+ get { return GetString("Exception_KeyLengthIsExceeded"); }
+ }
+
+ ///
+ /// The key cannot be longer than '{0}' when encoded with UTF-8.
+ ///
+ internal static string FormatException_KeyLengthIsExceeded(object p0)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("Exception_KeyLengthIsExceeded"), p0);
+ }
+
+ ///
+ /// The session cannot be established after the response has started.
+ ///
+ internal static string Exception_InvalidSessionEstablishment
+ {
+ get { return GetString("Exception_InvalidSessionEstablishment"); }
+ }
+
+ ///
+ /// The session cannot be established after the response has started.
+ ///
+ internal static string FormatException_InvalidSessionEstablishment()
+ {
+ return GetString("Exception_InvalidSessionEstablishment");
+ }
+
+ ///
+ /// The value cannot be serialized in two bytes.
+ ///
+ internal static string Exception_InvalidToSerializeIn2Bytes
+ {
+ get { return GetString("Exception_InvalidToSerializeIn2Bytes"); }
+ }
+
+ ///
+ /// The value cannot be serialized in two bytes.
+ ///
+ internal static string FormatException_InvalidToSerializeIn2Bytes()
+ {
+ return GetString("Exception_InvalidToSerializeIn2Bytes");
+ }
+
+ ///
+ /// The value cannot be serialized in three bytes.
+ ///
+ internal static string Exception_InvalidToSerializeIn3Bytes
+ {
+ get { return GetString("Exception_InvalidToSerializeIn3Bytes"); }
+ }
+
+ ///
+ /// The value cannot be serialized in three bytes.
+ ///
+ internal static string FormatException_InvalidToSerializeIn3Bytes()
+ {
+ return GetString("Exception_InvalidToSerializeIn3Bytes");
+ }
+
+ ///
+ /// The value cannot be negative.
+ ///
+ internal static string Exception_NumberShouldNotBeNegative
+ {
+ get { return GetString("Exception_NumberShouldNotBeNegative"); }
+ }
+
+ ///
+ /// The value cannot be negative.
+ ///
+ internal static string FormatException_NumberShouldNotBeNegative()
+ {
+ return GetString("Exception_NumberShouldNotBeNegative");
+ }
+
+ ///
+ /// Argument cannot be null or empty string.
+ ///
+ internal static string ArgumentCannotBeNullOrEmpty
+ {
+ get { return GetString("ArgumentCannotBeNullOrEmpty"); }
+ }
+
+ ///
+ /// Argument cannot be null or empty string.
+ ///
+ internal static string FormatArgumentCannotBeNullOrEmpty()
+ {
+ return GetString("ArgumentCannotBeNullOrEmpty");
+ }
+
+ private static string GetString(string name, params string[] formatterNames)
+ {
+ var value = _resourceManager.GetString(name);
+
+ System.Diagnostics.Debug.Assert(value != null);
+
+ if (formatterNames != null)
+ {
+ for (var i = 0; i < formatterNames.Length; i++)
+ {
+ value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}");
+ }
+ }
+
+ return value;
+ }
+ }
+}
diff --git a/src/Session/src/Microsoft.AspNetCore.Session/Resources.resx b/src/Session/src/Microsoft.AspNetCore.Session/Resources.resx
new file mode 100644
index 0000000000..5bb1c8e0bb
--- /dev/null
+++ b/src/Session/src/Microsoft.AspNetCore.Session/Resources.resx
@@ -0,0 +1,138 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ The key cannot be longer than '{0}' when encoded with UTF-8.
+
+
+ The session cannot be established after the response has started.
+
+
+ The value cannot be serialized in two bytes.
+
+
+ The value cannot be serialized in three bytes.
+
+
+ The value cannot be negative.
+
+
+ Argument cannot be null or empty string.
+
+
\ No newline at end of file
diff --git a/src/Session/src/Microsoft.AspNetCore.Session/SessionDefaults.cs b/src/Session/src/Microsoft.AspNetCore.Session/SessionDefaults.cs
new file mode 100644
index 0000000000..e9f590a0c7
--- /dev/null
+++ b/src/Session/src/Microsoft.AspNetCore.Session/SessionDefaults.cs
@@ -0,0 +1,21 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Microsoft.AspNetCore.Session
+{
+ ///
+ /// Represents defaults for the Session.
+ ///
+ public static class SessionDefaults
+ {
+ ///
+ /// Represent the default cookie name, which is ".AspNetCore.Session".
+ ///
+ public static readonly string CookieName = ".AspNetCore.Session";
+
+ ///
+ /// Represents the default path used to create the cookie, which is "/".
+ ///
+ public static readonly string CookiePath = "/";
+ }
+}
\ No newline at end of file
diff --git a/src/Session/src/Microsoft.AspNetCore.Session/SessionFeature.cs b/src/Session/src/Microsoft.AspNetCore.Session/SessionFeature.cs
new file mode 100644
index 0000000000..44a378e614
--- /dev/null
+++ b/src/Session/src/Microsoft.AspNetCore.Session/SessionFeature.cs
@@ -0,0 +1,13 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+
+namespace Microsoft.AspNetCore.Session
+{
+ public class SessionFeature : ISessionFeature
+ {
+ public ISession Session { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Session/src/Microsoft.AspNetCore.Session/SessionMiddleware.cs b/src/Session/src/Microsoft.AspNetCore.Session/SessionMiddleware.cs
new file mode 100644
index 0000000000..bd74afa9cb
--- /dev/null
+++ b/src/Session/src/Microsoft.AspNetCore.Session/SessionMiddleware.cs
@@ -0,0 +1,174 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Security.Cryptography;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.DataProtection;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.Session
+{
+ ///
+ /// Enables the session state for the application.
+ ///
+ public class SessionMiddleware
+ {
+ private static readonly RandomNumberGenerator CryptoRandom = RandomNumberGenerator.Create();
+ private const int SessionKeyLength = 36; // "382c74c3-721d-4f34-80e5-57657b6cbc27"
+ private static readonly Func ReturnTrue = () => true;
+ private readonly RequestDelegate _next;
+ private readonly SessionOptions _options;
+ private readonly ILogger _logger;
+ private readonly ISessionStore _sessionStore;
+ private readonly IDataProtector _dataProtector;
+
+ ///
+ /// Creates a new .
+ ///
+ /// The representing the next middleware in the pipeline.
+ /// The representing the factory that used to create logger instances.
+ /// The used to protect and verify the cookie.
+ /// The representing the session store.
+ /// The session configuration options.
+ public SessionMiddleware(
+ RequestDelegate next,
+ ILoggerFactory loggerFactory,
+ IDataProtectionProvider dataProtectionProvider,
+ ISessionStore sessionStore,
+ IOptions options)
+ {
+ if (next == null)
+ {
+ throw new ArgumentNullException(nameof(next));
+ }
+
+ if (loggerFactory == null)
+ {
+ throw new ArgumentNullException(nameof(loggerFactory));
+ }
+
+ if (dataProtectionProvider == null)
+ {
+ throw new ArgumentNullException(nameof(dataProtectionProvider));
+ }
+
+ if (sessionStore == null)
+ {
+ throw new ArgumentNullException(nameof(sessionStore));
+ }
+
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ _next = next;
+ _logger = loggerFactory.CreateLogger();
+ _dataProtector = dataProtectionProvider.CreateProtector(nameof(SessionMiddleware));
+ _options = options.Value;
+ _sessionStore = sessionStore;
+ }
+
+ ///
+ /// Invokes the logic of the middleware.
+ ///
+ /// The .
+ /// A that completes when the middleware has completed processing.
+ public async Task Invoke(HttpContext context)
+ {
+ var isNewSessionKey = false;
+ Func tryEstablishSession = ReturnTrue;
+ var cookieValue = context.Request.Cookies[_options.Cookie.Name];
+ var sessionKey = CookieProtection.Unprotect(_dataProtector, cookieValue, _logger);
+ if (string.IsNullOrWhiteSpace(sessionKey) || sessionKey.Length != SessionKeyLength)
+ {
+ // No valid cookie, new session.
+ var guidBytes = new byte[16];
+ CryptoRandom.GetBytes(guidBytes);
+ sessionKey = new Guid(guidBytes).ToString();
+ cookieValue = CookieProtection.Protect(_dataProtector, sessionKey);
+ var establisher = new SessionEstablisher(context, cookieValue, _options);
+ tryEstablishSession = establisher.TryEstablishSession;
+ isNewSessionKey = true;
+ }
+
+ var feature = new SessionFeature();
+ feature.Session = _sessionStore.Create(sessionKey, _options.IdleTimeout, _options.IOTimeout, tryEstablishSession, isNewSessionKey);
+ context.Features.Set(feature);
+
+ try
+ {
+ await _next(context);
+ }
+ finally
+ {
+ context.Features.Set(null);
+
+ if (feature.Session != null)
+ {
+ try
+ {
+ await feature.Session.CommitAsync(context.RequestAborted);
+ }
+ catch (OperationCanceledException)
+ {
+ _logger.SessionCommitCanceled();
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorClosingTheSession(ex);
+ }
+ }
+ }
+ }
+
+ private class SessionEstablisher
+ {
+ private readonly HttpContext _context;
+ private readonly string _cookieValue;
+ private readonly SessionOptions _options;
+ private bool _shouldEstablishSession;
+
+ public SessionEstablisher(HttpContext context, string cookieValue, SessionOptions options)
+ {
+ _context = context;
+ _cookieValue = cookieValue;
+ _options = options;
+ context.Response.OnStarting(OnStartingCallback, state: this);
+ }
+
+ private static Task OnStartingCallback(object state)
+ {
+ var establisher = (SessionEstablisher)state;
+ if (establisher._shouldEstablishSession)
+ {
+ establisher.SetCookie();
+ }
+ return Task.FromResult(0);
+ }
+
+ private void SetCookie()
+ {
+ var cookieOptions = _options.Cookie.Build(_context);
+
+ _context.Response.Cookies.Append(_options.Cookie.Name, _cookieValue, cookieOptions);
+
+ _context.Response.Headers["Cache-Control"] = "no-cache";
+ _context.Response.Headers["Pragma"] = "no-cache";
+ _context.Response.Headers["Expires"] = "-1";
+ }
+
+ // Returns true if the session has already been established, or if it still can be because the response has not been sent.
+ internal bool TryEstablishSession()
+ {
+ return (_shouldEstablishSession |= !_context.Response.HasStarted);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Session/src/Microsoft.AspNetCore.Session/SessionMiddlewareExtensions.cs b/src/Session/src/Microsoft.AspNetCore.Session/SessionMiddlewareExtensions.cs
new file mode 100644
index 0000000000..c273124379
--- /dev/null
+++ b/src/Session/src/Microsoft.AspNetCore.Session/SessionMiddlewareExtensions.cs
@@ -0,0 +1,50 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.AspNetCore.Session;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.Builder
+{
+ ///
+ /// Extension methods for adding the to an application.
+ ///
+ public static class SessionMiddlewareExtensions
+ {
+ ///
+ /// Adds the to automatically enable session state for the application.
+ ///
+ /// The .
+ /// The .
+ public static IApplicationBuilder UseSession(this IApplicationBuilder app)
+ {
+ if (app == null)
+ {
+ throw new ArgumentNullException(nameof(app));
+ }
+
+ return app.UseMiddleware();
+ }
+
+ ///
+ /// Adds the to automatically enable session state for the application.
+ ///
+ /// The .
+ /// The .
+ /// The .
+ public static IApplicationBuilder UseSession(this IApplicationBuilder app, SessionOptions options)
+ {
+ if (app == null)
+ {
+ throw new ArgumentNullException(nameof(app));
+ }
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ return app.UseMiddleware(Options.Create(options));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Session/src/Microsoft.AspNetCore.Session/SessionOptions.cs b/src/Session/src/Microsoft.AspNetCore.Session/SessionOptions.cs
new file mode 100644
index 0000000000..4d456b0f60
--- /dev/null
+++ b/src/Session/src/Microsoft.AspNetCore.Session/SessionOptions.cs
@@ -0,0 +1,126 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Threading;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Session;
+
+namespace Microsoft.AspNetCore.Builder
+{
+ ///
+ /// Represents the session state options for the application.
+ ///
+ public class SessionOptions
+ {
+ private CookieBuilder _cookieBuilder = new SessionCookieBuilder();
+
+ ///
+ /// Determines the settings used to create the cookie.
+ ///
+ /// defaults to .
+ /// defaults to .
+ /// defaults to .
+ /// defaults to true
+ /// defaults to false
+ ///
+ ///
+ public CookieBuilder Cookie
+ {
+ get => _cookieBuilder;
+ set => _cookieBuilder = value ?? throw new ArgumentNullException(nameof(value));
+ }
+
+ ///
+ /// The IdleTimeout indicates how long the session can be idle before its contents are abandoned. Each session access
+ /// resets the timeout. Note this only applies to the content of the session, not the cookie.
+ ///
+ public TimeSpan IdleTimeout { get; set; } = TimeSpan.FromMinutes(20);
+
+ ///
+ /// The maximim amount of time allowed to load a session from the store or to commit it back to the store.
+ /// Note this may only apply to asynchronous operations. This timeout can be disabled using .
+ ///
+ public TimeSpan IOTimeout { get; set; } = TimeSpan.FromMinutes(1);
+
+ #region Obsolete API
+ ///
+ ///
+ /// This property is obsolete and will be removed in a future version. The recommended alternative is on .
+ ///
+ ///
+ /// Determines the cookie name used to persist the session ID.
+ ///
+ ///
+ [Obsolete("This property is obsolete and will be removed in a future version. The recommended alternative is " + nameof(Cookie) + "." + nameof(CookieBuilder.Name) + ".")]
+ public string CookieName { get => Cookie.Name; set => Cookie.Name = value; }
+
+ ///
+ ///
+ /// This property is obsolete and will be removed in a future version. The recommended alternative is on .
+ ///
+ ///
+ /// Determines the domain used to create the cookie. Is not provided by default.
+ ///
+ ///
+ [Obsolete("This property is obsolete and will be removed in a future version. The recommended alternative is " + nameof(Cookie) + "." + nameof(CookieBuilder.Domain) + ".")]
+ public string CookieDomain { get => Cookie.Domain; set => Cookie.Domain = value; }
+
+ ///
+ ///
+ /// This property is obsolete and will be removed in a future version. The recommended alternative is on .
+ ///
+ ///
+ /// Determines the path used to create the cookie.
+ /// Defaults to .
+ ///
+ ///
+ [Obsolete("This property is obsolete and will be removed in a future version. The recommended alternative is " + nameof(Cookie) + "." + nameof(CookieBuilder.Path) + ".")]
+ public string CookiePath { get => Cookie.Path; set => Cookie.Path = value; }
+
+ ///
+ ///
+ /// This property is obsolete and will be removed in a future version. The recommended alternative is on .
+ ///
+ ///
+ /// Determines if the browser should allow the cookie to be accessed by client-side JavaScript. The
+ /// default is true, which means the cookie will only be passed to HTTP requests and is not made available
+ /// to script on the page.
+ ///
+ ///
+ [Obsolete("This property is obsolete and will be removed in a future version. The recommended alternative is " + nameof(Cookie) + "." + nameof(CookieBuilder.HttpOnly) + ".")]
+ public bool CookieHttpOnly { get => Cookie.HttpOnly; set => Cookie.HttpOnly = value; }
+
+ ///
+ ///
+ /// This property is obsolete and will be removed in a future version. The recommended alternative is on .
+ ///
+ ///
+ /// Determines if the cookie should only be transmitted on HTTPS requests.
+ ///
+ ///
+ [Obsolete("This property is obsolete and will be removed in a future version. The recommended alternative is " + nameof(Cookie) + "." + nameof(CookieBuilder.SecurePolicy) + ".")]
+ public CookieSecurePolicy CookieSecure { get => Cookie.SecurePolicy; set => Cookie.SecurePolicy = value; }
+ #endregion
+
+ private class SessionCookieBuilder : CookieBuilder
+ {
+ public SessionCookieBuilder()
+ {
+ Name = SessionDefaults.CookieName;
+ Path = SessionDefaults.CookiePath;
+ SecurePolicy = CookieSecurePolicy.None;
+ SameSite = SameSiteMode.Lax;
+ HttpOnly = true;
+ // Session is considered non-essential as it's designed for ephemeral data.
+ IsEssential = false;
+ }
+
+ public override TimeSpan? Expiration
+ {
+ get => null;
+ set => throw new InvalidOperationException(nameof(Expiration) + " cannot be set for the cookie defined by " + nameof(SessionOptions));
+ }
+ }
+ }
+}
diff --git a/src/Session/src/Microsoft.AspNetCore.Session/SessionServiceCollectionExtensions.cs b/src/Session/src/Microsoft.AspNetCore.Session/SessionServiceCollectionExtensions.cs
new file mode 100644
index 0000000000..628390fbe3
--- /dev/null
+++ b/src/Session/src/Microsoft.AspNetCore.Session/SessionServiceCollectionExtensions.cs
@@ -0,0 +1,57 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Session;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ ///
+ /// Extension methods for adding session services to the DI container.
+ ///
+ public static class SessionServiceCollectionExtensions
+ {
+ ///
+ /// Adds services required for application session state.
+ ///
+ /// The to add the services to.
+ /// The so that additional calls can be chained.
+ public static IServiceCollection AddSession(this IServiceCollection services)
+ {
+ if (services == null)
+ {
+ throw new ArgumentNullException(nameof(services));
+ }
+
+ services.TryAddTransient();
+ services.AddDataProtection();
+ return services;
+ }
+
+ ///
+ /// Adds services required for application session state.
+ ///
+ /// The to add the services to.
+ /// The session options to configure the middleware with.
+ /// The so that additional calls can be chained.
+ public static IServiceCollection AddSession(this IServiceCollection services, Action configure)
+ {
+ if (services == null)
+ {
+ throw new ArgumentNullException(nameof(services));
+ }
+
+ if (configure == null)
+ {
+ throw new ArgumentNullException(nameof(configure));
+ }
+
+ services.Configure(configure);
+ services.AddSession();
+
+ return services;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Session/src/Microsoft.AspNetCore.Session/SipHash.cs b/src/Session/src/Microsoft.AspNetCore.Session/SipHash.cs
new file mode 100644
index 0000000000..bad98fcab3
--- /dev/null
+++ b/src/Session/src/Microsoft.AspNetCore.Session/SipHash.cs
@@ -0,0 +1,202 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Microsoft.AspNetCore.Session
+{
+ // A byte[] equality comparer based on the SipHash-2-4 algorithm. Key differences:
+ // (a) we output 32-bit hashes instead of 64-bit hashes, and
+ // (b) we don't care about endianness since hashes are used only in hash tables
+ // and aren't returned to user code.
+ //
+ // Derived from the implementation in SignalR and modified to address byte[] instead of string. This derived version is not for cryptographic use, just hash codes.
+ // https://github.com/aspnet/SignalR-Server/blob/75f74169c81a51780f195d06b798302b2d76dbde/src/Microsoft.AspNetCore.SignalR.Server/Infrastructure/SipHashBasedStringEqualityComparer.cs
+ // Derivative work of https://github.com/tanglebones/ch-siphash.
+ internal static class SipHash
+ {
+ internal static int GetHashCode(byte[] bytes)
+ {
+ // Assume SipHash-2-4 is a strong PRF, therefore truncation to 32 bits is acceptable.
+ return (int)SipHash_2_4_UlongCast_ForcedInline(bytes);
+ }
+
+ private static ulong SipHash_2_4_UlongCast_ForcedInline(byte[] bytes)
+ {
+ unsafe
+ {
+ ulong v0 = 0x736f6d6570736575;
+ ulong v1 = 0x646f72616e646f6d;
+ ulong v2 = 0x6c7967656e657261;
+ ulong v3 = 0x7465646279746573;
+
+ uint inlen = (uint)bytes.Length;
+ fixed (byte* finb = bytes)
+ {
+ var b = ((ulong)inlen) << 56;
+
+ if (inlen > 0)
+ {
+ var inb = finb;
+ var left = inlen & 7;
+ var end = inb + inlen - left;
+ var linb = (ulong*)finb;
+ var lend = (ulong*)end;
+ for (; linb < lend; ++linb)
+ {
+ v3 ^= *linb;
+
+ v0 += v1;
+ v1 = (v1 << 13) | (v1 >> (64 - 13));
+ v1 ^= v0;
+ v0 = (v0 << 32) | (v0 >> (64 - 32));
+
+ v2 += v3;
+ v3 = (v3 << 16) | (v3 >> (64 - 16));
+ v3 ^= v2;
+
+ v0 += v3;
+ v3 = (v3 << 21) | (v3 >> (64 - 21));
+ v3 ^= v0;
+
+ v2 += v1;
+ v1 = (v1 << 17) | (v1 >> (64 - 17));
+ v1 ^= v2;
+ v2 = (v2 << 32) | (v2 >> (64 - 32));
+ v0 += v1;
+ v1 = (v1 << 13) | (v1 >> (64 - 13));
+ v1 ^= v0;
+ v0 = (v0 << 32) | (v0 >> (64 - 32));
+
+ v2 += v3;
+ v3 = (v3 << 16) | (v3 >> (64 - 16));
+ v3 ^= v2;
+
+ v0 += v3;
+ v3 = (v3 << 21) | (v3 >> (64 - 21));
+ v3 ^= v0;
+
+ v2 += v1;
+ v1 = (v1 << 17) | (v1 >> (64 - 17));
+ v1 ^= v2;
+ v2 = (v2 << 32) | (v2 >> (64 - 32));
+
+ v0 ^= *linb;
+ }
+ for (var i = 0; i < left; ++i)
+ {
+ b |= ((ulong)end[i]) << (8 * i);
+ }
+ }
+
+ v3 ^= b;
+ v0 += v1;
+ v1 = (v1 << 13) | (v1 >> (64 - 13));
+ v1 ^= v0;
+ v0 = (v0 << 32) | (v0 >> (64 - 32));
+
+ v2 += v3;
+ v3 = (v3 << 16) | (v3 >> (64 - 16));
+ v3 ^= v2;
+
+ v0 += v3;
+ v3 = (v3 << 21) | (v3 >> (64 - 21));
+ v3 ^= v0;
+
+ v2 += v1;
+ v1 = (v1 << 17) | (v1 >> (64 - 17));
+ v1 ^= v2;
+ v2 = (v2 << 32) | (v2 >> (64 - 32));
+ v0 += v1;
+ v1 = (v1 << 13) | (v1 >> (64 - 13));
+ v1 ^= v0;
+ v0 = (v0 << 32) | (v0 >> (64 - 32));
+
+ v2 += v3;
+ v3 = (v3 << 16) | (v3 >> (64 - 16));
+ v3 ^= v2;
+
+ v0 += v3;
+ v3 = (v3 << 21) | (v3 >> (64 - 21));
+ v3 ^= v0;
+
+ v2 += v1;
+ v1 = (v1 << 17) | (v1 >> (64 - 17));
+ v1 ^= v2;
+ v2 = (v2 << 32) | (v2 >> (64 - 32));
+ v0 ^= b;
+ v2 ^= 0xff;
+
+ v0 += v1;
+ v1 = (v1 << 13) | (v1 >> (64 - 13));
+ v1 ^= v0;
+ v0 = (v0 << 32) | (v0 >> (64 - 32));
+
+ v2 += v3;
+ v3 = (v3 << 16) | (v3 >> (64 - 16));
+ v3 ^= v2;
+
+ v0 += v3;
+ v3 = (v3 << 21) | (v3 >> (64 - 21));
+ v3 ^= v0;
+
+ v2 += v1;
+ v1 = (v1 << 17) | (v1 >> (64 - 17));
+ v1 ^= v2;
+ v2 = (v2 << 32) | (v2 >> (64 - 32));
+ v0 += v1;
+ v1 = (v1 << 13) | (v1 >> (64 - 13));
+ v1 ^= v0;
+ v0 = (v0 << 32) | (v0 >> (64 - 32));
+
+ v2 += v3;
+ v3 = (v3 << 16) | (v3 >> (64 - 16));
+ v3 ^= v2;
+
+ v0 += v3;
+ v3 = (v3 << 21) | (v3 >> (64 - 21));
+ v3 ^= v0;
+
+ v2 += v1;
+ v1 = (v1 << 17) | (v1 >> (64 - 17));
+ v1 ^= v2;
+ v2 = (v2 << 32) | (v2 >> (64 - 32));
+ v0 += v1;
+ v1 = (v1 << 13) | (v1 >> (64 - 13));
+ v1 ^= v0;
+ v0 = (v0 << 32) | (v0 >> (64 - 32));
+
+ v2 += v3;
+ v3 = (v3 << 16) | (v3 >> (64 - 16));
+ v3 ^= v2;
+
+ v0 += v3;
+ v3 = (v3 << 21) | (v3 >> (64 - 21));
+ v3 ^= v0;
+
+ v2 += v1;
+ v1 = (v1 << 17) | (v1 >> (64 - 17));
+ v1 ^= v2;
+ v2 = (v2 << 32) | (v2 >> (64 - 32));
+ v0 += v1;
+ v1 = (v1 << 13) | (v1 >> (64 - 13));
+ v1 ^= v0;
+ v0 = (v0 << 32) | (v0 >> (64 - 32));
+
+ v2 += v3;
+ v3 = (v3 << 16) | (v3 >> (64 - 16));
+ v3 ^= v2;
+
+ v0 += v3;
+ v3 = (v3 << 21) | (v3 >> (64 - 21));
+ v3 ^= v0;
+
+ v2 += v1;
+ v1 = (v1 << 17) | (v1 >> (64 - 17));
+ v1 ^= v2;
+ v2 = (v2 << 32) | (v2 >> (64 - 32));
+ }
+
+ return v0 ^ v1 ^ v2 ^ v3;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Session/src/Microsoft.AspNetCore.Session/baseline.netcore.json b/src/Session/src/Microsoft.AspNetCore.Session/baseline.netcore.json
new file mode 100644
index 0000000000..ff9ee6811f
--- /dev/null
+++ b/src/Session/src/Microsoft.AspNetCore.Session/baseline.netcore.json
@@ -0,0 +1,687 @@
+{
+ "AssemblyIdentity": "Microsoft.AspNetCore.Session, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
+ "Types": [
+ {
+ "Name": "Microsoft.Extensions.DependencyInjection.SessionServiceCollectionExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "AddSession",
+ "Parameters": [
+ {
+ "Name": "services",
+ "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection"
+ }
+ ],
+ "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddSession",
+ "Parameters": [
+ {
+ "Name": "services",
+ "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection"
+ },
+ {
+ "Name": "configure",
+ "Type": "System.Action"
+ }
+ ],
+ "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Builder.SessionMiddlewareExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "UseSession",
+ "Parameters": [
+ {
+ "Name": "app",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "UseSession",
+ "Parameters": [
+ {
+ "Name": "app",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Builder.SessionOptions"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Builder.SessionOptions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Cookie",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.CookieBuilder",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Cookie",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Http.CookieBuilder"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_IdleTimeout",
+ "Parameters": [],
+ "ReturnType": "System.TimeSpan",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_IdleTimeout",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.TimeSpan"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_IOTimeout",
+ "Parameters": [],
+ "ReturnType": "System.TimeSpan",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_IOTimeout",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.TimeSpan"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_CookieName",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_CookieName",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_CookieDomain",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_CookieDomain",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_CookiePath",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_CookiePath",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_CookieHttpOnly",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_CookieHttpOnly",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_CookieSecure",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.CookieSecurePolicy",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_CookieSecure",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Http.CookieSecurePolicy"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Session.DistributedSession",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Http.ISession"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_IsAvailable",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Http.ISession",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Id",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Http.ISession",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Keys",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IEnumerable",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Http.ISession",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "TryGetValue",
+ "Parameters": [
+ {
+ "Name": "key",
+ "Type": "System.String"
+ },
+ {
+ "Name": "value",
+ "Type": "System.Byte[]",
+ "Direction": "Out"
+ }
+ ],
+ "ReturnType": "System.Boolean",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Http.ISession",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Set",
+ "Parameters": [
+ {
+ "Name": "key",
+ "Type": "System.String"
+ },
+ {
+ "Name": "value",
+ "Type": "System.Byte[]"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Http.ISession",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Remove",
+ "Parameters": [
+ {
+ "Name": "key",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Http.ISession",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Clear",
+ "Parameters": [],
+ "ReturnType": "System.Void",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Http.ISession",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "LoadAsync",
+ "Parameters": [
+ {
+ "Name": "cancellationToken",
+ "Type": "System.Threading.CancellationToken",
+ "DefaultValue": "default(System.Threading.CancellationToken)"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Http.ISession",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "CommitAsync",
+ "Parameters": [
+ {
+ "Name": "cancellationToken",
+ "Type": "System.Threading.CancellationToken",
+ "DefaultValue": "default(System.Threading.CancellationToken)"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Http.ISession",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "cache",
+ "Type": "Microsoft.Extensions.Caching.Distributed.IDistributedCache"
+ },
+ {
+ "Name": "sessionKey",
+ "Type": "System.String"
+ },
+ {
+ "Name": "idleTimeout",
+ "Type": "System.TimeSpan"
+ },
+ {
+ "Name": "ioTimeout",
+ "Type": "System.TimeSpan"
+ },
+ {
+ "Name": "tryEstablishSession",
+ "Type": "System.Func"
+ },
+ {
+ "Name": "loggerFactory",
+ "Type": "Microsoft.Extensions.Logging.ILoggerFactory"
+ },
+ {
+ "Name": "isNewSessionKey",
+ "Type": "System.Boolean"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Session.DistributedSessionStore",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Session.ISessionStore"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Create",
+ "Parameters": [
+ {
+ "Name": "sessionKey",
+ "Type": "System.String"
+ },
+ {
+ "Name": "idleTimeout",
+ "Type": "System.TimeSpan"
+ },
+ {
+ "Name": "ioTimeout",
+ "Type": "System.TimeSpan"
+ },
+ {
+ "Name": "tryEstablishSession",
+ "Type": "System.Func"
+ },
+ {
+ "Name": "isNewSessionKey",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Http.ISession",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Session.ISessionStore",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "cache",
+ "Type": "Microsoft.Extensions.Caching.Distributed.IDistributedCache"
+ },
+ {
+ "Name": "loggerFactory",
+ "Type": "Microsoft.Extensions.Logging.ILoggerFactory"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Session.ISessionStore",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Create",
+ "Parameters": [
+ {
+ "Name": "sessionKey",
+ "Type": "System.String"
+ },
+ {
+ "Name": "idleTimeout",
+ "Type": "System.TimeSpan"
+ },
+ {
+ "Name": "ioTimeout",
+ "Type": "System.TimeSpan"
+ },
+ {
+ "Name": "tryEstablishSession",
+ "Type": "System.Func"
+ },
+ {
+ "Name": "isNewSessionKey",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Http.ISession",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Session.SessionDefaults",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Field",
+ "Name": "CookieName",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "CookiePath",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Session.SessionFeature",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Http.Features.ISessionFeature"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Session",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.ISession",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.ISessionFeature",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Session",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Http.ISession"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.ISessionFeature",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Session.SessionMiddleware",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Invoke",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "next",
+ "Type": "Microsoft.AspNetCore.Http.RequestDelegate"
+ },
+ {
+ "Name": "loggerFactory",
+ "Type": "Microsoft.Extensions.Logging.ILoggerFactory"
+ },
+ {
+ "Name": "dataProtectionProvider",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider"
+ },
+ {
+ "Name": "sessionStore",
+ "Type": "Microsoft.AspNetCore.Session.ISessionStore"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.Extensions.Options.IOptions"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/Session/test/Directory.Build.props b/src/Session/test/Directory.Build.props
new file mode 100644
index 0000000000..270e1fa209
--- /dev/null
+++ b/src/Session/test/Directory.Build.props
@@ -0,0 +1,14 @@
+
+
+
+
+ netcoreapp2.1
+ $(DeveloperBuildTestTfms)
+ netcoreapp2.1;netcoreapp2.0
+ $(StandardTestTfms);net461
+
+
+
+
+
+
diff --git a/src/Session/test/Microsoft.AspNetCore.Session.Tests/Microsoft.AspNetCore.Session.Tests.csproj b/src/Session/test/Microsoft.AspNetCore.Session.Tests/Microsoft.AspNetCore.Session.Tests.csproj
new file mode 100644
index 0000000000..9dc11b9d2f
--- /dev/null
+++ b/src/Session/test/Microsoft.AspNetCore.Session.Tests/Microsoft.AspNetCore.Session.Tests.csproj
@@ -0,0 +1,22 @@
+
+
+
+ $(StandardTestTfms)
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Session/test/Microsoft.AspNetCore.Session.Tests/SessionTests.cs b/src/Session/test/Microsoft.AspNetCore.Session.Tests/SessionTests.cs
new file mode 100644
index 0000000000..f98a018bc5
--- /dev/null
+++ b/src/Session/test/Microsoft.AspNetCore.Session.Tests/SessionTests.cs
@@ -0,0 +1,1016 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.Caching.Distributed;
+using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Internal;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Logging.Testing;
+using Microsoft.Extensions.Options;
+using Microsoft.Net.Http.Headers;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Session
+{
+ public class SessionTests
+ {
+ [Fact]
+ public async Task ReadingEmptySessionDoesNotCreateCookie()
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseSession();
+
+ app.Run(context =>
+ {
+ Assert.Null(context.Session.GetString("NotFound"));
+ return Task.FromResult(0);
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddDistributedMemoryCache();
+ services.AddSession();
+ });
+
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var response = await client.GetAsync(string.Empty);
+ response.EnsureSuccessStatusCode();
+ Assert.False(response.Headers.TryGetValues("Set-Cookie", out var _));
+ }
+ }
+
+ [Fact]
+ public async Task SettingAValueCausesTheCookieToBeCreated()
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseSession();
+ app.Run(context =>
+ {
+ Assert.Null(context.Session.GetString("Key"));
+ context.Session.SetString("Key", "Value");
+ Assert.Equal("Value", context.Session.GetString("Key"));
+ return Task.FromResult(0);
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddDistributedMemoryCache();
+ services.AddSession();
+ });
+
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var response = await client.GetAsync(string.Empty);
+ response.EnsureSuccessStatusCode();
+ Assert.True(response.Headers.TryGetValues("Set-Cookie", out var values));
+ Assert.Single(values);
+ Assert.True(!string.IsNullOrWhiteSpace(values.First()));
+ }
+ }
+
+ [Theory]
+ [InlineData(CookieSecurePolicy.Always, "http://example.com/testpath", true)]
+ [InlineData(CookieSecurePolicy.Always, "https://example.com/testpath", true)]
+ [InlineData(CookieSecurePolicy.None, "http://example.com/testpath", false)]
+ [InlineData(CookieSecurePolicy.None, "https://example.com/testpath", false)]
+ [InlineData(CookieSecurePolicy.SameAsRequest, "http://example.com/testpath", false)]
+ [InlineData(CookieSecurePolicy.SameAsRequest, "https://example.com/testpath", true)]
+ public async Task SecureSessionBasedOnHttpsAndSecurePolicy(
+ CookieSecurePolicy cookieSecurePolicy,
+ string requestUri,
+ bool shouldBeSecureOnly)
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseSession(new SessionOptions
+ {
+ Cookie =
+ {
+ Name = "TestCookie",
+ SecurePolicy = cookieSecurePolicy
+ }
+ });
+ app.Run(context =>
+ {
+ Assert.Null(context.Session.GetString("Key"));
+ context.Session.SetString("Key", "Value");
+ Assert.Equal("Value", context.Session.GetString("Key"));
+ return Task.FromResult(0);
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddDistributedMemoryCache();
+ services.AddSession();
+ });
+
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var response = await client.GetAsync(requestUri);
+ response.EnsureSuccessStatusCode();
+ Assert.True(response.Headers.TryGetValues("Set-Cookie", out var values));
+ Assert.Single(values);
+ if (shouldBeSecureOnly)
+ {
+ Assert.Contains("; secure", values.First());
+ }
+ else
+ {
+ Assert.DoesNotContain("; secure", values.First());
+ }
+ }
+ }
+
+ [Fact]
+ public async Task SessionCanBeAccessedOnTheNextRequest()
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseSession();
+ app.Run(context =>
+ {
+ int? value = context.Session.GetInt32("Key");
+ if (context.Request.Path == new PathString("/first"))
+ {
+ Assert.False(value.HasValue);
+ value = 0;
+ }
+ Assert.True(value.HasValue);
+ context.Session.SetInt32("Key", value.Value + 1);
+ return context.Response.WriteAsync(value.Value.ToString());
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddDistributedMemoryCache();
+ services.AddSession();
+ });
+
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var response = await client.GetAsync("first");
+ response.EnsureSuccessStatusCode();
+ Assert.Equal("0", await response.Content.ReadAsStringAsync());
+
+ client = server.CreateClient();
+ var cookie = SetCookieHeaderValue.ParseList(response.Headers.GetValues("Set-Cookie").ToList()).First();
+ client.DefaultRequestHeaders.Add("Cookie", new CookieHeaderValue(cookie.Name, cookie.Value).ToString());
+ Assert.Equal("1", await client.GetStringAsync("/"));
+ Assert.Equal("2", await client.GetStringAsync("/"));
+ Assert.Equal("3", await client.GetStringAsync("/"));
+ }
+ }
+
+ [Fact]
+ public async Task RemovedItemCannotBeAccessedAgain()
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseSession();
+ app.Run(context =>
+ {
+ int? value = context.Session.GetInt32("Key");
+ if (context.Request.Path == new PathString("/first"))
+ {
+ Assert.False(value.HasValue);
+ value = 0;
+ context.Session.SetInt32("Key", 1);
+ }
+ else if (context.Request.Path == new PathString("/second"))
+ {
+ Assert.True(value.HasValue);
+ Assert.Equal(1, value);
+ context.Session.Remove("Key");
+ }
+ else if (context.Request.Path == new PathString("/third"))
+ {
+ Assert.False(value.HasValue);
+ value = 2;
+ }
+ return context.Response.WriteAsync(value.Value.ToString());
+ });
+ })
+ .ConfigureServices(
+ services =>
+ {
+ services.AddDistributedMemoryCache();
+ services.AddSession();
+ });
+
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var response = await client.GetAsync("first");
+ response.EnsureSuccessStatusCode();
+ Assert.Equal("0", await response.Content.ReadAsStringAsync());
+
+ client = server.CreateClient();
+ var cookie = SetCookieHeaderValue.ParseList(response.Headers.GetValues("Set-Cookie").ToList()).First();
+ client.DefaultRequestHeaders.Add("Cookie", new CookieHeaderValue(cookie.Name, cookie.Value).ToString());
+ Assert.Equal("1", await client.GetStringAsync("/second"));
+ Assert.Equal("2", await client.GetStringAsync("/third"));
+ }
+ }
+
+ [Fact]
+ public async Task ClearedItemsCannotBeAccessedAgain()
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseSession();
+ app.Run(context =>
+ {
+ int? value = context.Session.GetInt32("Key");
+ if (context.Request.Path == new PathString("/first"))
+ {
+ Assert.False(value.HasValue);
+ value = 0;
+ context.Session.SetInt32("Key", 1);
+ }
+ else if (context.Request.Path == new PathString("/second"))
+ {
+ Assert.True(value.HasValue);
+ Assert.Equal(1, value);
+ context.Session.Clear();
+ }
+ else if (context.Request.Path == new PathString("/third"))
+ {
+ Assert.False(value.HasValue);
+ value = 2;
+ }
+ return context.Response.WriteAsync(value.Value.ToString());
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddDistributedMemoryCache();
+ services.AddSession();
+ });
+
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var response = await client.GetAsync("first");
+ response.EnsureSuccessStatusCode();
+ Assert.Equal("0", await response.Content.ReadAsStringAsync());
+
+ client = server.CreateClient();
+ var cookie = SetCookieHeaderValue.ParseList(response.Headers.GetValues("Set-Cookie").ToList()).First();
+ client.DefaultRequestHeaders.Add("Cookie", new CookieHeaderValue(cookie.Name, cookie.Value).ToString());
+ Assert.Equal("1", await client.GetStringAsync("/second"));
+ Assert.Equal("2", await client.GetStringAsync("/third"));
+ }
+ }
+
+ [Fact]
+ public async Task SessionStart_LogsInformation()
+ {
+ var sink = new TestSink(
+ TestSink.EnableWithTypeName,
+ TestSink.EnableWithTypeName);
+ var loggerFactory = new TestLoggerFactory(sink, enabled: true);
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseSession();
+ app.Run(context =>
+ {
+ context.Session.SetString("Key", "Value");
+ return Task.FromResult(0);
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddSingleton(typeof(ILoggerFactory), loggerFactory);
+ services.AddDistributedMemoryCache();
+ services.AddSession();
+ });
+
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var response = await client.GetAsync(string.Empty);
+ response.EnsureSuccessStatusCode();
+ }
+
+ var sessionLogMessages = sink.Writes.ToList();
+
+ Assert.Equal(2, sessionLogMessages.Count);
+ Assert.Contains("started", sessionLogMessages[0].State.ToString());
+ Assert.Equal(LogLevel.Information, sessionLogMessages[0].LogLevel);
+ Assert.Contains("stored", sessionLogMessages[1].State.ToString());
+ Assert.Equal(LogLevel.Debug, sessionLogMessages[1].LogLevel);
+ }
+
+ [Fact]
+ public async Task ExpiredSession_LogsInfo()
+ {
+ var sink = new TestSink(
+ TestSink.EnableWithTypeName,
+ TestSink.EnableWithTypeName);
+ var loggerFactory = new TestLoggerFactory(sink, enabled: true);
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseSession();
+ app.Run(context =>
+ {
+ int? value = context.Session.GetInt32("Key");
+ if (context.Request.Path == new PathString("/first"))
+ {
+ Assert.False(value.HasValue);
+ value = 1;
+ context.Session.SetInt32("Key", 1);
+ }
+ else if (context.Request.Path == new PathString("/second"))
+ {
+ Assert.False(value.HasValue);
+ value = 2;
+ }
+ return context.Response.WriteAsync(value.Value.ToString());
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddSingleton(typeof(ILoggerFactory), loggerFactory);
+ services.AddDistributedMemoryCache();
+ services.AddSession(o => o.IdleTimeout = TimeSpan.FromMilliseconds(30));
+ });
+
+ string result;
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var response = await client.GetAsync("first");
+ response.EnsureSuccessStatusCode();
+
+ client = server.CreateClient();
+ var cookie = SetCookieHeaderValue.ParseList(response.Headers.GetValues("Set-Cookie").ToList()).First();
+ client.DefaultRequestHeaders.Add("Cookie", new CookieHeaderValue(cookie.Name, cookie.Value).ToString());
+ Thread.Sleep(50);
+ result = await client.GetStringAsync("/second");
+ }
+
+ var sessionLogMessages = sink.Writes.ToList();
+
+ Assert.Equal("2", result);
+ Assert.Equal(3, sessionLogMessages.Count);
+ Assert.Contains("started", sessionLogMessages[0].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.Debug, sessionLogMessages[1].LogLevel);
+ Assert.Equal(LogLevel.Information, sessionLogMessages[2].LogLevel);
+ }
+
+ [Fact]
+ public async Task RefreshesSession_WhenSessionData_IsNotModified()
+ {
+ var clock = new TestClock();
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseSession();
+ app.Run(context =>
+ {
+ string responseData = string.Empty;
+ if (context.Request.Path == new PathString("/AddDataToSession"))
+ {
+ context.Session.SetInt32("Key", 10);
+ responseData = "added data to session";
+ }
+ else if (context.Request.Path == new PathString("/AccessSessionData"))
+ {
+ var value = context.Session.GetInt32("Key");
+ responseData = (value == null) ? "No value found in session." : value.ToString();
+ }
+ else if (context.Request.Path == new PathString("/DoNotAccessSessionData"))
+ {
+ responseData = "did not access session data";
+ }
+
+ return context.Response.WriteAsync(responseData);
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddSingleton(typeof(ILoggerFactory), NullLoggerFactory.Instance);
+ services.AddDistributedMemoryCache();
+ services.AddSession(o => o.IdleTimeout = TimeSpan.FromMinutes(20));
+ services.Configure(o => o.Clock = clock);
+ });
+
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var response = await client.GetAsync("AddDataToSession");
+ response.EnsureSuccessStatusCode();
+
+ client = server.CreateClient();
+ var cookie = SetCookieHeaderValue.ParseList(response.Headers.GetValues("Set-Cookie").ToList()).First();
+ client.DefaultRequestHeaders.Add(
+ "Cookie", new CookieHeaderValue(cookie.Name, cookie.Value).ToString());
+
+ for (var i = 0; i < 5; i++)
+ {
+ clock.Add(TimeSpan.FromMinutes(10));
+ await client.GetStringAsync("/DoNotAccessSessionData");
+ }
+
+ var data = await client.GetStringAsync("/AccessSessionData");
+ Assert.Equal("10", data);
+ }
+ }
+
+ [Fact]
+ public async Task SessionFeature_IsUnregistered_WhenResponseGoingOut()
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.Use(async (httpContext, next) =>
+ {
+ await next();
+
+ Assert.Null(httpContext.Features.Get());
+ });
+
+ app.UseSession();
+
+ app.Run(context =>
+ {
+ context.Session.SetString("key", "value");
+ return Task.FromResult(0);
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddDistributedMemoryCache();
+ services.AddSession();
+ });
+
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var response = await client.GetAsync(string.Empty);
+ response.EnsureSuccessStatusCode();
+ }
+ }
+
+ [Fact]
+ public async Task SessionFeature_IsUnregistered_WhenResponseGoingOut_AndAnUnhandledExcetionIsThrown()
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.Use(async (httpContext, next) =>
+ {
+ var exceptionThrown = false;
+ try
+ {
+ await next();
+ }
+ catch
+ {
+ exceptionThrown = true;
+ }
+
+ Assert.True(exceptionThrown);
+ Assert.Null(httpContext.Features.Get());
+ });
+
+ app.UseSession();
+
+ app.Run(context =>
+ {
+ throw new InvalidOperationException("An error occurred.");
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddDistributedMemoryCache();
+ services.AddSession();
+ });
+
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var response = await client.GetAsync(string.Empty);
+ }
+ }
+
+ [Fact]
+ public async Task SessionKeys_AreCaseSensitive()
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseSession();
+ app.Run(context =>
+ {
+ context.Session.SetString("KEY", "VALUE");
+ context.Session.SetString("key", "value");
+ Assert.Equal("VALUE", context.Session.GetString("KEY"));
+ Assert.Equal("value", context.Session.GetString("key"));
+ return Task.FromResult(0);
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddDistributedMemoryCache();
+ services.AddSession();
+ });
+
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var response = await client.GetAsync(string.Empty);
+ response.EnsureSuccessStatusCode();
+ }
+ }
+
+ [Fact]
+ public async Task SessionLogsCacheReadException()
+ {
+ var sink = new TestSink(
+ TestSink.EnableWithTypeName,
+ TestSink.EnableWithTypeName);
+ var loggerFactory = new TestLoggerFactory(sink, enabled: true);
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseSession();
+ app.Run(context =>
+ {
+ Assert.False(context.Session.TryGetValue("key", out var value));
+ Assert.Null(value);
+ Assert.Equal(string.Empty, context.Session.Id);
+ Assert.False(context.Session.Keys.Any());
+ return Task.FromResult(0);
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddSingleton(typeof(ILoggerFactory), loggerFactory);
+ services.AddSingleton(new UnreliableCache(new MemoryCache(new MemoryCacheOptions()))
+ {
+ DisableGet = true
+ });
+ services.AddSession();
+ });
+
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var response = await client.GetAsync(string.Empty);
+ response.EnsureSuccessStatusCode();
+ }
+
+ var message = Assert.Single(sink.Writes);
+ Assert.Contains("Session cache read exception", message.State.ToString());
+ Assert.Equal(LogLevel.Error, message.LogLevel);
+ }
+
+ [Fact]
+ public async Task SessionLogsCacheLoadAsyncException()
+ {
+ var sink = new TestSink(
+ TestSink.EnableWithTypeName,
+ TestSink.EnableWithTypeName);
+ var loggerFactory = new TestLoggerFactory(sink, enabled: true);
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseSession();
+ app.Run(async context =>
+ {
+ await Assert.ThrowsAsync(() => context.Session.LoadAsync());
+ Assert.False(context.Session.IsAvailable);
+ Assert.Equal(string.Empty, context.Session.Id);
+ Assert.False(context.Session.Keys.Any());
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddSingleton(typeof(ILoggerFactory), loggerFactory);
+ services.AddSingleton(new UnreliableCache(new MemoryCache(new MemoryCacheOptions()))
+ {
+ DisableGet = true
+ });
+ services.AddSession();
+ });
+
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var response = await client.GetAsync(string.Empty);
+ response.EnsureSuccessStatusCode();
+ }
+
+ var message = Assert.Single(sink.Writes);
+ Assert.Contains("Session cache read exception", message.State.ToString());
+ Assert.Equal(LogLevel.Error, message.LogLevel);
+ }
+
+ [Fact]
+ public async Task SessionLogsCacheLoadAsyncTimeoutException()
+ {
+ var sink = new TestSink(
+ TestSink.EnableWithTypeName,
+ TestSink.EnableWithTypeName);
+ var loggerFactory = new TestLoggerFactory(sink, enabled: true);
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseSession(new SessionOptions()
+ {
+ IOTimeout = TimeSpan.FromSeconds(0.5)
+ });
+ app.Run(async context =>
+ {
+ await Assert.ThrowsAsync(() => context.Session.LoadAsync());
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddSingleton(typeof(ILoggerFactory), loggerFactory);
+ services.AddSingleton(new UnreliableCache(new MemoryCache(new MemoryCacheOptions()))
+ {
+ DelayGetAsync = true
+ });
+ services.AddSession();
+ });
+
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var response = await client.GetAsync(string.Empty);
+ response.EnsureSuccessStatusCode();
+ }
+
+ var message = Assert.Single(sink.Writes);
+ Assert.Contains("Loading the session timed out.", message.State.ToString());
+ Assert.Equal(LogLevel.Warning, message.LogLevel);
+ }
+
+ [Fact]
+ public async Task SessionLoadAsyncCanceledException()
+ {
+ var sink = new TestSink(
+ TestSink.EnableWithTypeName,
+ TestSink.EnableWithTypeName);
+ var loggerFactory = new TestLoggerFactory(sink, enabled: true);
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseSession();
+ app.Run(async context =>
+ {
+ var cts = new CancellationTokenSource();
+ var token = cts.Token;
+ cts.Cancel();
+ await Assert.ThrowsAsync(() => context.Session.LoadAsync(token));
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddSingleton(typeof(ILoggerFactory), loggerFactory);
+ services.AddSingleton(new UnreliableCache(new MemoryCache(new MemoryCacheOptions()))
+ {
+ DelayGetAsync = true
+ });
+ services.AddSession();
+ });
+
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var response = await client.GetAsync(string.Empty);
+ response.EnsureSuccessStatusCode();
+ }
+
+ Assert.Empty(sink.Writes);
+ }
+
+ [Fact]
+ public async Task SessionLogsCacheCommitException()
+ {
+ var sink = new TestSink(
+ writeContext =>
+ {
+ return writeContext.LoggerName.Equals(typeof(SessionMiddleware).FullName)
+ || writeContext.LoggerName.Equals(typeof(DistributedSession).FullName);
+ },
+ beginScopeContext =>
+ {
+ return beginScopeContext.LoggerName.Equals(typeof(SessionMiddleware).FullName)
+ || beginScopeContext.LoggerName.Equals(typeof(DistributedSession).FullName);
+ });
+ var loggerFactory = new TestLoggerFactory(sink, enabled: true);
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseSession();
+ app.Run(context =>
+ {
+ context.Session.SetInt32("key", 0);
+ return Task.FromResult(0);
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddSingleton(typeof(ILoggerFactory), loggerFactory);
+ services.AddSingleton(new UnreliableCache(new MemoryCache(new MemoryCacheOptions()))
+ {
+ DisableSetAsync = true
+ });
+ services.AddSession();
+ });
+
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var response = await client.GetAsync(string.Empty);
+ response.EnsureSuccessStatusCode();
+ }
+
+ var sessionLogMessage = sink.Writes.Where(message => message.LoggerName.Equals(typeof(DistributedSession).FullName, StringComparison.Ordinal)).Single();
+
+ Assert.Contains("Session started", sessionLogMessage.State.ToString());
+ Assert.Equal(LogLevel.Information, sessionLogMessage.LogLevel);
+
+ var sessionMiddlewareLogMessage = sink.Writes.Where(message => message.LoggerName.Equals(typeof(SessionMiddleware).FullName, StringComparison.Ordinal)).Single();
+
+ Assert.Contains("Error closing the session.", sessionMiddlewareLogMessage.State.ToString());
+ Assert.Equal(LogLevel.Error, sessionMiddlewareLogMessage.LogLevel);
+ }
+
+ [Fact]
+ public async Task SessionLogsCacheCommitTimeoutException()
+ {
+ var sink = new TestSink(
+ writeContext =>
+ {
+ return writeContext.LoggerName.Equals(typeof(SessionMiddleware).FullName)
+ || writeContext.LoggerName.Equals(typeof(DistributedSession).FullName);
+ },
+ beginScopeContext =>
+ {
+ return beginScopeContext.LoggerName.Equals(typeof(SessionMiddleware).FullName)
+ || beginScopeContext.LoggerName.Equals(typeof(DistributedSession).FullName);
+ });
+ var loggerFactory = new TestLoggerFactory(sink, enabled: true);
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseSession(new SessionOptions()
+ {
+ IOTimeout = TimeSpan.FromSeconds(0.5)
+ });
+ app.Run(context =>
+ {
+ context.Session.SetInt32("key", 0);
+ return Task.FromResult(0);
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddSingleton(typeof(ILoggerFactory), loggerFactory);
+ services.AddSingleton(new UnreliableCache(new MemoryCache(new MemoryCacheOptions()))
+ {
+ DelaySetAsync = true
+ });
+ services.AddSession();
+ });
+
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var response = await client.GetAsync(string.Empty);
+ response.EnsureSuccessStatusCode();
+ }
+
+ var sessionLogMessages = sink.Writes.Where(message => message.LoggerName.Equals(typeof(DistributedSession).FullName, StringComparison.Ordinal)).ToList();
+
+ Assert.Contains("Session started", sessionLogMessages[0].State.ToString());
+ Assert.Equal(LogLevel.Information, sessionLogMessages[0].LogLevel);
+
+ Assert.Contains("Committing the session timed out.", sessionLogMessages[1].State.ToString());
+ Assert.Equal(LogLevel.Warning, sessionLogMessages[1].LogLevel);
+
+ var sessionMiddlewareLogs = sink.Writes.Where(message => message.LoggerName.Equals(typeof(SessionMiddleware).FullName, StringComparison.Ordinal)).ToList();
+
+ Assert.Contains("Committing the session was canceled.", sessionMiddlewareLogs[0].State.ToString());
+ Assert.Equal(LogLevel.Information, sessionMiddlewareLogs[0].LogLevel);
+ }
+
+ [Fact]
+ public async Task SessionLogsCacheCommitCanceledException()
+ {
+ var sink = new TestSink(
+ writeContext =>
+ {
+ return writeContext.LoggerName.Equals(typeof(SessionMiddleware).FullName)
+ || writeContext.LoggerName.Equals(typeof(DistributedSession).FullName);
+ },
+ beginScopeContext =>
+ {
+ return beginScopeContext.LoggerName.Equals(typeof(SessionMiddleware).FullName)
+ || beginScopeContext.LoggerName.Equals(typeof(DistributedSession).FullName);
+ });
+ var loggerFactory = new TestLoggerFactory(sink, enabled: true);
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseSession();
+ app.Run(async context =>
+ {
+ context.Session.SetInt32("key", 0);
+ var cts = new CancellationTokenSource();
+ var token = cts.Token;
+ cts.Cancel();
+ await Assert.ThrowsAsync(() => context.Session.CommitAsync(token));
+ context.RequestAborted = token;
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddSingleton(typeof(ILoggerFactory), loggerFactory);
+ services.AddSingleton(new UnreliableCache(new MemoryCache(new MemoryCacheOptions()))
+ {
+ DelaySetAsync = true
+ });
+ services.AddSession();
+ });
+
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var response = await client.GetAsync(string.Empty);
+ response.EnsureSuccessStatusCode();
+ }
+
+ Assert.Empty(sink.Writes.Where(message => message.LoggerName.Equals(typeof(DistributedSession).FullName, StringComparison.Ordinal)));
+
+ var sessionMiddlewareLogs = sink.Writes.Where(message => message.LoggerName.Equals(typeof(SessionMiddleware).FullName, StringComparison.Ordinal)).ToList();
+
+ Assert.Contains("Committing the session was canceled.", sessionMiddlewareLogs[0].State.ToString());
+ Assert.Equal(LogLevel.Information, sessionMiddlewareLogs[0].LogLevel);
+ }
+
+ [Fact]
+ public async Task SessionLogsCacheRefreshException()
+ {
+ var sink = new TestSink(
+ TestSink.EnableWithTypeName,
+ TestSink.EnableWithTypeName);
+ var loggerFactory = new TestLoggerFactory(sink, enabled: true);
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseSession();
+ app.Run(context =>
+ {
+ // The middleware calls context.Session.CommitAsync() once per request
+ return Task.FromResult(0);
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddSingleton(typeof(ILoggerFactory), loggerFactory);
+ services.AddSingleton(new UnreliableCache(new MemoryCache(new MemoryCacheOptions()))
+ {
+ DisableRefreshAsync = true
+ });
+ services.AddSession();
+ });
+
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var response = await client.GetAsync(string.Empty);
+ response.EnsureSuccessStatusCode();
+ }
+
+ var message = Assert.Single(sink.Writes);
+ Assert.Contains("Error closing the session.", message.State.ToString());
+ Assert.Equal(LogLevel.Error, message.LogLevel);
+ }
+
+ private class TestClock : ISystemClock
+ {
+ public TestClock()
+ {
+ UtcNow = new DateTimeOffset(2013, 1, 1, 1, 0, 0, TimeSpan.Zero);
+ }
+
+ public DateTimeOffset UtcNow { get; private set; }
+
+ public void Add(TimeSpan timespan)
+ {
+ UtcNow = UtcNow.Add(timespan);
+ }
+ }
+
+ private class UnreliableCache : IDistributedCache
+ {
+ private readonly MemoryDistributedCache _cache;
+
+ public bool DisableGet { get; set; }
+ public bool DisableSetAsync { get; set; }
+ public bool DisableRefreshAsync { get; set; }
+ public bool DelayGetAsync { get; set; }
+ public bool DelaySetAsync { get; set; }
+ public bool DelayRefreshAsync { get; set; }
+
+ public UnreliableCache(IMemoryCache memoryCache)
+ {
+ _cache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions()));
+ }
+
+ public byte[] Get(string key)
+ {
+ if (DisableGet)
+ {
+ throw new InvalidOperationException();
+ }
+ return _cache.Get(key);
+ }
+
+ public Task GetAsync(string key, CancellationToken token = default)
+ {
+ if (DisableGet)
+ {
+ throw new InvalidOperationException();
+ }
+ if (DelayGetAsync)
+ {
+ token.WaitHandle.WaitOne(TimeSpan.FromSeconds(10));
+ token.ThrowIfCancellationRequested();
+ }
+ return _cache.GetAsync(key, token);
+ }
+
+ public void Refresh(string key) => _cache.Refresh(key);
+
+ public Task RefreshAsync(string key, CancellationToken token = default)
+ {
+ if (DisableRefreshAsync)
+ {
+ throw new InvalidOperationException();
+ }
+ if (DelayRefreshAsync)
+ {
+ token.WaitHandle.WaitOne(TimeSpan.FromSeconds(10));
+ token.ThrowIfCancellationRequested();
+ }
+ return _cache.RefreshAsync(key);
+ }
+
+ public void Remove(string key) => _cache.Remove(key);
+
+ public Task RemoveAsync(string key, CancellationToken token = default) => _cache.RemoveAsync(key);
+
+ public void Set(string key, byte[] value, DistributedCacheEntryOptions options) => _cache.Set(key, value, options);
+
+ public Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default)
+ {
+ if (DisableSetAsync)
+ {
+ throw new InvalidOperationException();
+ }
+ if (DelaySetAsync)
+ {
+ token.WaitHandle.WaitOne(TimeSpan.FromSeconds(10));
+ token.ThrowIfCancellationRequested();
+ }
+ return _cache.SetAsync(key, value, options);
+ }
+ }
+ }
+}
diff --git a/src/Session/version.props b/src/Session/version.props
new file mode 100644
index 0000000000..669c874829
--- /dev/null
+++ b/src/Session/version.props
@@ -0,0 +1,12 @@
+
+
+ 2.1.1
+ rtm
+ $(VersionPrefix)
+ $(VersionPrefix)-$(VersionSuffix)-final
+ t000
+ a-
+ $(FeatureBranchVersionPrefix)$(VersionSuffix)-$([System.Text.RegularExpressions.Regex]::Replace('$(FeatureBranchVersionSuffix)', '[^\w-]', '-'))
+ $(VersionSuffix)-$(BuildNumber)
+
+