Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions Microsoft.AspNetCore.SystemWebAdapters.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.1.32127.271
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.SystemWebAdapters", "src\Microsoft.AspNetCore.SystemWebAdapters\Microsoft.AspNetCore.SystemWebAdapters.csproj", "{55C1BBE0-B922-46B0-8F2C-8472BC9A5F33}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.SystemWebAdapters.Tests", "test\Microsoft.AspNetCore.SystemWebAdapters.Tests\Microsoft.AspNetCore.SystemWebAdapters.Tests.csproj", "{DA28CCAB-3C88-46F1-B779-43C573ED49E5}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{95915611-30BF-4AFF-AE41-5CDC6F57DCF7}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ClassLibrary", "samples\ClassLibrary\ClassLibrary.csproj", "{11DD4D64-7A95-4635-A273-775715E18852}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{67BB2CCD-5EF4-4A0C-8272-C7219A3B02D2}"
ProjectSection(SolutionItems) = preProject
README.md = README.md
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MvcApp", "samples\MvcApp\MvcApp.csproj", "{174A36F1-27ED-43FC-A3A1-00DA58C4E30C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MvcCoreApp", "samples\MvcCoreApp\MvcCoreApp.csproj", "{B1D06F62-B315-4ED8-8109-168B4D4E4B86}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{F9DB9323-C919-49E8-8F96-B923D2F42E60}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{A1BDA50C-D70B-416C-97F1-74B0649797C5}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.SystemWebAdapters.SessionState", "src\Microsoft.AspNetCore.SystemWebAdapters.SessionState\Microsoft.AspNetCore.SystemWebAdapters.SessionState.csproj", "{2029D409-07E3-49F8-BB6A-77114DE7B337}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.SystemWebAdapters.SessionState.Tests", "test\Microsoft.AspNetCore.SystemWebAdapters.SessionState.Tests\Microsoft.AspNetCore.SystemWebAdapters.SessionState.Tests.csproj", "{9AFF3DCE-5DEF-4337-B5BC-C98ABEA6BEDC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{55C1BBE0-B922-46B0-8F2C-8472BC9A5F33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{55C1BBE0-B922-46B0-8F2C-8472BC9A5F33}.Debug|Any CPU.Build.0 = Debug|Any CPU
{55C1BBE0-B922-46B0-8F2C-8472BC9A5F33}.Release|Any CPU.ActiveCfg = Release|Any CPU
{55C1BBE0-B922-46B0-8F2C-8472BC9A5F33}.Release|Any CPU.Build.0 = Release|Any CPU
{DA28CCAB-3C88-46F1-B779-43C573ED49E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DA28CCAB-3C88-46F1-B779-43C573ED49E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DA28CCAB-3C88-46F1-B779-43C573ED49E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DA28CCAB-3C88-46F1-B779-43C573ED49E5}.Release|Any CPU.Build.0 = Release|Any CPU
{11DD4D64-7A95-4635-A273-775715E18852}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{11DD4D64-7A95-4635-A273-775715E18852}.Debug|Any CPU.Build.0 = Debug|Any CPU
{11DD4D64-7A95-4635-A273-775715E18852}.Release|Any CPU.ActiveCfg = Release|Any CPU
{11DD4D64-7A95-4635-A273-775715E18852}.Release|Any CPU.Build.0 = Release|Any CPU
{174A36F1-27ED-43FC-A3A1-00DA58C4E30C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{174A36F1-27ED-43FC-A3A1-00DA58C4E30C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{174A36F1-27ED-43FC-A3A1-00DA58C4E30C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{174A36F1-27ED-43FC-A3A1-00DA58C4E30C}.Release|Any CPU.Build.0 = Release|Any CPU
{B1D06F62-B315-4ED8-8109-168B4D4E4B86}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B1D06F62-B315-4ED8-8109-168B4D4E4B86}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B1D06F62-B315-4ED8-8109-168B4D4E4B86}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B1D06F62-B315-4ED8-8109-168B4D4E4B86}.Release|Any CPU.Build.0 = Release|Any CPU
{2029D409-07E3-49F8-BB6A-77114DE7B337}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2029D409-07E3-49F8-BB6A-77114DE7B337}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2029D409-07E3-49F8-BB6A-77114DE7B337}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2029D409-07E3-49F8-BB6A-77114DE7B337}.Release|Any CPU.Build.0 = Release|Any CPU
{9AFF3DCE-5DEF-4337-B5BC-C98ABEA6BEDC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9AFF3DCE-5DEF-4337-B5BC-C98ABEA6BEDC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9AFF3DCE-5DEF-4337-B5BC-C98ABEA6BEDC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9AFF3DCE-5DEF-4337-B5BC-C98ABEA6BEDC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{55C1BBE0-B922-46B0-8F2C-8472BC9A5F33} = {F9DB9323-C919-49E8-8F96-B923D2F42E60}
{DA28CCAB-3C88-46F1-B779-43C573ED49E5} = {A1BDA50C-D70B-416C-97F1-74B0649797C5}
{11DD4D64-7A95-4635-A273-775715E18852} = {95915611-30BF-4AFF-AE41-5CDC6F57DCF7}
{174A36F1-27ED-43FC-A3A1-00DA58C4E30C} = {95915611-30BF-4AFF-AE41-5CDC6F57DCF7}
{B1D06F62-B315-4ED8-8109-168B4D4E4B86} = {95915611-30BF-4AFF-AE41-5CDC6F57DCF7}
{2029D409-07E3-49F8-BB6A-77114DE7B337} = {F9DB9323-C919-49E8-8F96-B923D2F42E60}
{9AFF3DCE-5DEF-4337-B5BC-C98ABEA6BEDC} = {A1BDA50C-D70B-416C-97F1-74B0649797C5}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {DABA3C65-9D74-4EB6-9B1C-730328710EAD}
EndGlobalSection
EndGlobal
9 changes: 9 additions & 0 deletions Microsoft.AspNetCore.SystemWebAdapters.slnf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"solution": {
"path": "Microsoft.AspNetCore.SystemWebAdapters.sln",
"projects": [
"src\\Microsoft.AspNetCore.SystemWebAdapters\\Microsoft.AspNetCore.SystemWebAdapters.csproj",
"test\\Microsoft.AspNetCore.SystemWebAdapters.Tests\\Microsoft.AspNetCore.SystemWebAdapters.Tests.csproj"
]
}
}
154 changes: 154 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,157 @@
# System.Web Adapters

This project provides a collection of adapters that help migrating from `System.Web.dll` based ASP.NET projects to ASP.NET Core projects. Adapters currently include:

- `System.Web.HttpContext`: Subset of the APIs from `System.Web.dll` backed by `Microsoft.AspNetCore.Http` types.

## Examples

A common use case that this is aimed at solving is in a project with multiple class libraries. Let's take a look at an example using the proposed adapters moving from .NET Framework to ASP.NET Core.

### ASP.NET Framework
Consider a controller that does something such as:

```cs
public class SomeController : Controller
{
public ActionResult Index()
{
SomeOtherClass.SomeMethod(HttpContext.Current);
}
}
```

which then has logic in a separate assembly passing that `HttpContext` around until finally, some inner method does some logic on it such as:

```cs
public class Class2
{
public bool PerformSomeCheck(HttpContext context)
{
return context.Request.Headers["SomeHeader"] == "ExpectedValue";
}
}
```

### ASP.NET Core

In order to run the above logic in ASP.NET Core, a developer will need to add the `Microsoft.AspNetCore.SystemWebAdapters` package, that will enable the projects to work on both platforms.

The libraries would need to be updated to understand the adapters, but it will be as simple as adding the package and recompiling. If these are the only dependencies a system has on `System.Web.dll`, then the libraries will be able to target .NET Standard to facillitate a simpler building process while migrating.

The controller in ASP.NET Core will now look like this:

```cs
public class SomeController : Controller
{
[Route("/")]
public IActionResult Index()
{
SomeOtherClass.SomeMethod(Context);
}
}
```

Notice that since there's a `Controller.Context` property, they can pass that through, but it generally looks the same. Using implicit conversions, the `Microsoft.AspNetCore.Http.HttpContext` can be converted into the adapter that could then be passed around through the levels utilizing the code in the same way.

## Set up
Below are the steps needed to start using these adapters in your project:

1. Set up `NuGet.config` to point to the CI feed:
```xml
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<!--To inherit the global NuGet package sources remove the <clear/> line below -->
<clear />
<add key="nuget" value="https://api.nuget.org/v3/index.json" />
<add key="dotnet6" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet6/nuget/v3/index.json" />
</packageSources>
</configuration>
```
2. Install `Microsoft.AspNetCore.SystemWebAdapters`
3. In your framework application:
- The package installation will add a new handler to your web.config. This is to enable shared session state. If you will not need to use `HttpContext.Session`, feel free to remove this. Please see the section on [session state](#shared-session-state) to configure this
4. In your class libraries:
- Class libraries can target .NET Standard 2.0 if desired which will ensure you are using the shared surface area
- If you find that there's still some missing APIs, you may cross-compile with .NET Framework to maintain that behavior and handle it in .NET core in some other way
- There should be no manual changes to enable using supported surface area of the adapters. If a member is not found, it is not currently supported on ASP.NET Core
5. For your ASP.NET Core application:
- Register the adapter services:
```cs
builder.Services.AddSystemWebAdapters();
```
- Add the middleware after routing but before endpoints (if present);
```cs
app.UseSystemWebAdapters();
```
- For additional configuration, please see the [configuration](#configuration) section

## Usage
The ASP.NET Core implementation of `System.Web.HttpContext` attempts to bring behavior from ASP.NET framework, but can be configured. There is some behavior that can cause additional work to be done that may impact performance and memory usage that is configurable.

### Access `HttpContext`
Your code can operate on `System.Web.HttpContext` or `Microsoft.AspNetCore.Http.HttpContext`. This library provides implicit casting to each of these using a caching mechanism. For a given instance of `Microsoft.AspNetCore.Http.HttpContext`, implicit casting will always return the same `System.Web.HttpContext` instance. If you need a new instance (which will in turn create new instances of the request/response/and other objects), you may use the `HttpContext` constructor.

### `HttpContext.Request`
By default, the incoming request is not always seekable nor fully available. In order to get behavior seen in .NET Framework, you can opt into prebuffering the input stream. This will fully read the incoming stream and buffer it to memory or disk (depending on settings).

This can be enabled by applying endpoint metadata that implements the `IPreBufferRequestStreamMetadata` interface. This is available as an attribute `PreBufferRequestStreamAttribute` that can be applied to controllers or methods.

To enable this on all MVC endpoints, there is an extension method that can be used as follows:

```cs
app.UseEndpoints(endpoints =>
{
app.MapDefaultControllerRoute()
.PreBufferRequestStream();
});
```

### `HttpContext.Response`
In order to support behavior for `HttpContext.Response` that requires buffering the response before sending, endpoints must opt-into it with endpoint metadata implementing `IBufferResponseStreamMetadata`.

This enables APIs such as `HttpResponse.Output`, `HttpResponse.End()`, `HttpResponse.Clear()`, and `HttpResponse.SuppressContent`.

To enable this on all MVC endpoints, there is an extension method that can be used as follows:

```cs
app.UseEndpoints(endpoints =>
{
app.MapDefaultControllerRoute()
.BufferResponseStream();
});
```

### Shared session state
In order to support `HttpContext.Session`, endpoints must opt-into it via metadata implementing `ISessionMetadata`.

To enable this on all MVC endpoints, there is an extension method that can be used as follows:

```cs
app.UseEndpoints(endpoints =>
{
app.MapDefaultControllerRoute()
.RequireSystemWebAdapterSession();
});
```

This also requires some implementation of a session store. An initial implementation is being included that accesses a running ASP.NET Framework app and grabs session information from it. For details see [here](./docs/session-state/remote-session.md) for details.

## Supported Targets
- .NET Core App 3.1: This will implement the adapters against ASP.NET Core `HttpContext`. This will provide the following:
- Conversions between ASP.NET Core `HttpContext` and `System.Web` adapter `HttpContext` (with appropriate caching so it will not cause perf hits for GC allocations)
- Default implementations against `Microsoft.AspNetCore.Http.HttpContext`
- Services that can be implemented to override some functionality such as session/caching/etc that may need to be customized to match experience.
- .NET Standard 2.0: This will essentially be a reference assembly. There will be no constructors for the types as ASP.NET Core will construct them based on their `HttpContext` and on framework there are already other constructors. However, this will allow class libraries to target .NET Standard instead of needing to multi-target which will then require everything it depends on to multi-target.
- .NET Framework 4.7.2: This will type forward the adapter classes to `System.Web` so that they can be unified and enable libraries built against .NET Standard 2.0 to run on .NET Framework instances.

## Known Limitations

Below are some of the limitations of the APIs in the adapters. These are usually due to building off of types used in ASP.NET Core that cannot be fully implemented in ASP.NET Core. In the future, analyzers may be used to flag usage to recommend better patterns.

- A number of APIs in `System.Web.HttpContext` are exposed as `NameValueCollection` instances. In order to reduce copying, many of these are implemented on ASP.NET Core using the core containers. This makes it so that for many of these collections, `Get(int)` (and any API that requires that such as `.Keys` or `.GetEnumerator()`) are unavailable as most of the containers in ASP.NET Core (such as `IHeaderDictionary`) does not have the ability to index by position.

# Contributing

This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [[email protected]](mailto:[email protected]) with any additional questions or comments.
Binary file added docs/session-state/readonly-remote-session.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
51 changes: 51 additions & 0 deletions docs/session-state/remote-session.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Remote App Session State

Remote app session state will enable communication between the ASP.NET Core and ASP.NET app and to retrieve the session state. This is enabled by exposing an endpoint on the ASP.NET app that can be queried to retrieve and set the session state.

## Configuration

In order to configure it, both the framework and core app must set an API key as well as register known app settings types. These properties are:

- `ApiKeyHeader` - header name that will contain an API key to secure the endpoint added on .NET Framework
- `ApiKey` - the shared API key that will be validated in the .NET Framework handler
- `RegisterKey<T>(string)` - Registers a session key to a known type. This is required in order to serialize/deserialize the session state correctly. If a key is found that there is no registration for, an error will be thrown and session will not be available.

Configuration for ASP.NET Core would look similar to the following:

```csharp
builder.Services.AddSystemWebAdapters()
.AddRemoteAppSession(options =>
{
options.RemoteApp = new(builder.Configuration["ReverseProxy:Clusters:fallbackCluster:Destinations:fallbackApp:Address"]);
options.ApiKey = "test-key";
options.RegisterKey<int>("test-value");
options.RegisterKey<SessionDemoModel>("SampleSessionItem");
});
```

The framework equivalent would look like the following change in `Global.asax.cs`:

```csharp
Application.AddSystemWebAdapters()
.AddRemoteAppSession(options=>
{
options.ApiKey = "test-key";
options.RegisterKey<int>("test-value");
options.RegisterKey<SessionDemoModel>("SampleSessionItem");
});
```
# Protocol

## Readonly
Readonly session will retrieve the session state from the framework app without any sort of locking. This consists of a single `GET` request that will return a session state and can be closed immediately.

![Readonly protocol](./readonly-remote-session.png)

## Writeable

Writeable session state protocol starts with the the same as the readonly, but differs in the following:

- Requires an additional `PUT` request to update the state
- The initial `GET` request must be kept open until the session is done; if closed, the session will not be able to be updated

![Writeable protocl](writeable-remote-session.png)
Binary file added docs/session-state/writeable-remote-session.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions samples/ClassLibrary/Class1.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Web;

namespace ClassLibrary;

public static class Helper
{
public static string UserAgent => HttpContext.Current.Request.UserAgent;

public static string GetUserAgent(HttpContextBase context) => context.Request.UserAgent;
}
16 changes: 16 additions & 0 deletions samples/ClassLibrary/ClassLibrary.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>10</LangVersion>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.SystemWebAdapters\Microsoft.AspNetCore.SystemWebAdapters.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
</ItemGroup>

</Project>
43 changes: 43 additions & 0 deletions samples/ClassLibrary/CookieTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System.Web;

namespace ClassLibrary;

public class CookieTests
{
public static void RequestCookies(HttpContext context)
{
using (var writer = new SimpleJsonWriter(context.Response))
{
writer.Write("InitialCount", context.Request.Cookies.Count);
writer.Write("InitialHeader", context.Request.Headers["cookie"]);

// Add cookie
context.Request.Cookies.Add(new HttpCookie("cookie1", "cookie1value"));
writer.Write("AfterAddCount", context.Request.Cookies.Count);
writer.Write("AfterAddHeader", context.Request.Headers["cookie"]);

// remove cookie
context.Request.Cookies.Remove("ASP.NET_SessionId");
writer.Write("AfterAddCount", context.Request.Cookies.Count);
writer.Write("AfterAddHeader", context.Request.Headers["cookie"]);
}

context.Response.End();
}

public static void ResponseCookies(HttpContext context)
{
using (var writer = new SimpleJsonWriter(context.Response))
{
writer.Write("InitialCount", context.Response.Cookies.Count);
writer.Write("InitialHeader", context.Response.Headers["cookie"]);

// Add cookie
context.Response.Cookies.Add(new HttpCookie("cookie1", "cookie1value"));
writer.Write("AfterAddCount", context.Response.Cookies.Count);
writer.Write("AfterAddHeader", context.Response.Headers["set-cookie"]);
}

context.Response.End();
}
}
Loading