diff --git a/.openpublishing.redirection.json b/.openpublishing.redirection.json index 3c96ddcb15a4..547eefa798e9 100644 --- a/.openpublishing.redirection.json +++ b/.openpublishing.redirection.json @@ -1010,6 +1010,16 @@ "redirect_url": "/aspnet/core/migration/fx-to-core/systemweb-adapters", "redirect_document_id": false }, + { + "source_path": "aspnetcore/migration/inc/unit-testing.md", + "redirect_url": "/aspnet/core/migration/fx-to-core/systemweb-adapters", + "redirect_document_id": false + }, + { + "source_path": "aspnetcore/migration/fx-to-core/inc/unit-testing.md", + "redirect_url": "/aspnet/core/migration/fx-to-core/systemweb-adapters", + "redirect_document_id": false + }, { "source_path": "aspnetcore/migration/inc/blazor.md", "redirect_url": "/aspnet/core/migration/fx-to-core/inc/blazor", @@ -1022,7 +1032,7 @@ }, { "source_path": "aspnetcore/migration/inc/overview.md", - "redirect_url": "/aspnet/core/migration/fx-to-core/inc/overview", + "redirect_url": "/aspnet/core/migration/fx-to-core/index", "redirect_document_id": false }, { @@ -1047,17 +1057,17 @@ }, { "source_path": "aspnetcore/migration/inc/start.md", - "redirect_url": "/aspnet/core/migration/fx-to-core/inc/start", + "redirect_url": "/aspnet/core/migration/fx-to-core/start", "redirect_document_id": false }, { - "source_path": "aspnetcore/migration/inc/unit-testing.md", - "redirect_url": "/aspnet/core/migration/fx-to-core/inc/unit-testing", + "source_path": "aspnetcore/migration/inc/usage_guidance.md", + "redirect_url": "/aspnet/core/migration/fx-to-core/", "redirect_document_id": false }, { - "source_path": "aspnetcore/migration/inc/usage_guidance.md", - "redirect_url": "/aspnet/core/migration/fx-to-core/inc/usage_guidance", + "source_path": "aspnetcore/migration/fx-to-core/inc/usage_guidance.md", + "redirect_url": "/aspnet/core/migration/fx-to-core/", "redirect_document_id": false }, { @@ -1065,6 +1075,26 @@ "redirect_url": "/aspnet/core/migration/fx-to-core/inc/wrapped", "redirect_document_id": false }, + { + "source_path": "aspnetcore/migration/fx-to-core/inc/remote-session.md", + "redirect_url": "/aspnet/core/migration/fx-to-core/areas/session#remote-app-session-state", + "redirect_document_id": false + }, + { + "source_path": "aspnetcore/migration/fx-to-core/inc/remote-authentication.md", + "redirect_url": "/aspnet/core/migration/fx-to-core/areas/authentication#remote-authentication", + "redirect_document_id": false + }, + { + "source_path": "aspnetcore/migration/fx-to-core/inc/session.md", + "redirect_url": "/aspnet/core/migration/fx-to-core/areas/session", + "redirect_document_id": false + }, + { + "source_path": "aspnetcore/migration/fx-to-core/inc/wrapped.md", + "redirect_url": "/aspnet/core/migration/fx-to-core/areas/session#wrapped-aspnet-core-session-state", + "redirect_document_id": false + }, { "source_path": "aspnetcore/security/blazor/server-side.md", "redirect_url": "/aspnet/core/blazor/security/server/", diff --git a/aspnetcore/migration/fx-to-core/areas/authentication.md b/aspnetcore/migration/fx-to-core/areas/authentication.md new file mode 100644 index 000000000000..cdb1deb314f8 --- /dev/null +++ b/aspnetcore/migration/fx-to-core/areas/authentication.md @@ -0,0 +1,374 @@ +--- +title: ASP.NET Framework to Core Authentication Migration +description: ASP.NET Framework to Core Authentication Migration +author: rick-anderson +ms.author: riande +monikerRange: '>= aspnetcore-6.0' +ms.date: 11/9/2022 +ms.topic: article +uid: migration/fx-to-core/areas/authentication +zone_pivot_groups: migration-remote-app-setup +--- + +# Migrate ASP.NET Framework Authentication to ASP.NET Core + +Authentication is a critical component of web applications, handling user identity verification across HTTP requests. When migrating from ASP.NET Framework to ASP.NET Core, authentication presents unique challenges because the two frameworks handle authentication very differently. + +For general information about authentication in ASP.NET Core, see . For information about authorization, see . + +## Why authentication migration is complex + +ASP.NET Framework and ASP.NET Core have fundamentally different approaches to authentication: + +* **ASP.NET Framework** provides built-in authentication modules with automatic configuration and tight integration with System.Web +* **ASP.NET Core** uses a middleware-based approach with explicit configuration and dependency injection + +These differences mean you can't simply move your authentication code from Framework to Core without changes. + +## Authentication types and migration challenges + +Different authentication types present varying levels of migration complexity: + +### Cookie-based authentication + +* **ASP.NET Framework**: Uses `Microsoft.Owin` cookie authentication middleware +* **ASP.NET Core**: Uses `CookieAuthentication` middleware with different configuration +* **Migration challenge**: Cookie format, encryption keys, and configuration differences + +### JWT Bearer tokens + +* **ASP.NET Framework**: Often handled through custom modules or OWIN middleware +* **ASP.NET Core**: Native support through `Microsoft.AspNetCore.Authentication.JwtBearer` +* **Migration challenge**: Token validation parameter differences and middleware registration + +### Windows authentication + +* **ASP.NET Framework**: Built-in IIS integration +* **ASP.NET Core**: Requires explicit configuration and may need hosting model changes +* **Migration challenge**: Server configuration and authentication flow differences + +### Forms authentication + +* **ASP.NET Framework**: Built-in `System.Web.Security.FormsAuthentication` +* **ASP.NET Core**: No direct equivalent, requires migration to cookie authentication +* **Migration challenge**: Complete authentication system rewrite needed + +### Custom authentication + +* **ASP.NET Framework**: Custom modules implementing `IHttpModule` +* **ASP.NET Core**: Custom middleware or authentication handlers +* **Migration challenge**: Complete architecture change from modules to middleware + +## Migration strategies overview + +You have three main approaches for handling authentication during migration: + +1. **Complete rewrite** - Rewrite all authentication code to use ASP.NET Core's native authentication +2. **Remote authentication** - Use System.Web adapters to delegate authentication to the ASP.NET Framework app +3. **Shared cookie authentication** - Share authentication cookies between Framework and Core applications (for OWIN-based scenarios) + +For most applications, migrating to native ASP.NET Core authentication provides the best performance and maintainability. However, larger applications or those with complex authentication requirements may benefit from an incremental approach using the System.Web adapters. + +## Choose your migration approach + +You have three main options for migrating authentication from ASP.NET Framework to ASP.NET Core. Your choice depends on your authentication type, migration timeline, whether you need to run both applications simultaneously, and how much code you're willing to rewrite. + +### Quick decision guide + +**Answer these questions to choose your approach:** + +1. **Are you doing a complete rewrite or incremental migration?** + * Complete rewrite → [Complete rewrite to ASP.NET Core authentication](#complete-rewrite-to-aspnet-core-authentication) + * Incremental migration → Continue to question 2 + +2. **What type of authentication does your ASP.NET Framework app use?** + * OWIN cookie authentication → Continue to question 3 + * Forms authentication, JWT Bearer tokens, Windows auth, or Custom authentication → [Remote authentication](#remote-authentication) + +3. **Do both your ASP.NET Framework and ASP.NET Core apps need to access the same authentication state?** + * Yes, shared authentication needed → Continue to question 4 + * No, separate authentication is fine → [Complete rewrite to ASP.NET Core authentication](#complete-rewrite-to-aspnet-core-authentication) + +4. **Can you configure matching data protection settings between both apps?** + * Yes → [Shared cookie authentication](#shared-cookie-authentication) (see Alternatives section, best performance) + * No or unsure → [Remote authentication](#remote-authentication) + +### Migration approaches comparison + +| Approach | Code Changes | Performance | Auth Sharing | When to Use | +|----------|-------------|-------------|--------------|-------------| +| **[Complete rewrite](#complete-rewrite-to-aspnet-core-authentication)** | High - Rewrite all auth code | Best | None | Complete rewrites, performance-critical apps, non-OWIN auth | +| **[Remote authentication](#remote-authentication)** | Low - Keep existing patterns | Fair | Full | Incremental migrations, complex auth, Windows auth | +| **[Shared cookies](#shared-cookie-authentication)** | Medium - Update configuration | Good | Full | OWIN cookie auth, performance-critical shared auth (see Alternatives) | + +## Complete rewrite to ASP.NET Core authentication + +Choose this approach when you're performing a complete migration, have non-OWIN authentication, or want the best performance and maintainability. + +ASP.NET Core provides comprehensive authentication support with high performance and extensive customization options. This approach requires rewriting authentication code but offers the most benefits in the long term. + +### Complete rewrite pros and cons + +| Pros | Cons | +|------|------| +| Best performance and security | Requires rewriting all authentication code | +| Native ASP.NET Core implementation | No automatic migration path | +| Full control over authentication flow | Learning curve for new authentication patterns | +| No additional dependencies | Breaking change from Framework patterns | +| Access to latest ASP.NET Core auth features | Potential downtime during migration | + +### Migration considerations + +When migrating to native ASP.NET Core authentication: + +**Choose based on your Framework authentication type:** +* **Forms authentication** → Migrate to [Cookie authentication](xref:security/authentication/cookie) +* **JWT Bearer tokens** → Migrate to [JWT Bearer authentication](xref:security/authentication/configure-jwt-bearer-authentication) +* **Windows authentication** → Use [Windows authentication](xref:security/authentication/windowsauth) +* **OWIN OAuth providers** → Use corresponding ASP.NET Core OAuth providers +* **Custom authentication** → Implement [custom authentication handlers](xref:security/authentication/index#authentication-handler) + +**Code changes required:** +* Replace `HttpContext.User` access patterns (mostly compatible) +* Update authentication middleware registration in `Program.cs` +* Migrate custom authentication logic to ASP.NET Core patterns +* Update authorization attributes and policies + +**When to choose this approach:** +* You can afford to rewrite authentication-related code +* Performance is a top priority +* You're not sharing authentication state with legacy applications +* You want to eliminate System.Web dependencies completely +* Your Framework app uses Forms authentication, custom modules, or JWT tokens + +## Remote authentication + +[!INCLUDE[](~/migration/fx-to-core/includes/uses-systemweb-adapters.md)] + +Choose this approach when you need to share authentication between your ASP.NET Framework and ASP.NET Core applications during incremental migration, or when you have complex authentication that's difficult to migrate. + +The System.Web adapters' remote authentication feature allows an ASP.NET Core app to determine a user's identity by deferring to an ASP.NET app. This enables gradual migration while maintaining a single authentication system. + +### How remote authentication works + +1. When requests are processed by the ASP.NET Core app, if remote app authentication is the default scheme or specified by the request's endpoint, the `RemoteAuthenticationAuthHandler` will attempt to authenticate the user. +2. The handler makes an HTTP request to the ASP.NET app's authenticate endpoint, forwarding configured headers from the current request (by default, `Authorization` and `Cookie` headers). +3. The ASP.NET app processes the authentication and returns either a serialized `ClaimsPrincipal` or an HTTP status code indicating failure. +4. The ASP.NET Core app uses the result to establish the user's identity or handle authentication challenges. + +### Remote authentication pros and cons + +| Pros | Cons | +|------|------| +| Minimal code changes required | Additional HTTP request overhead | +| Works with any Framework auth type | Network dependency between apps | +| Gradual migration capability | More complex debugging | +| Preserves existing auth logic | Requires both apps to be running | +| Handles complex auth scenarios | Limited Windows auth support | + +### When to choose remote authentication + +**Choose Remote Authentication when:** +* Your ASP.NET app doesn't use `Microsoft.Owin` cookie authentication +* You want a flexible solution that works with various authentication mechanisms +* You need to gradually migrate authentication logic to ASP.NET Core +* You have complex custom authentication that's difficult to rewrite +* You're doing an incremental migration and need shared authentication state + +### Remote authentication configuration + +There are just a few small code changes needed to enable remote authentication in a solution that's already set up according to the [Getting Started](xref:migration/fx-to-core/start). + +First, follow the [remote app setup](xref:migration/fx-to-core/inc/remote-app-setup) instructions to connect the ASP.NET Core and ASP.NET apps. Then, there are just a couple extra extension methods to call to enable remote app authentication. + +:::zone pivot="manual" + +#### ASP.NET app configuration + +The ASP.NET app needs to be configured to add the authentication endpoint. Adding the authentication endpoint is done by calling the `AddAuthenticationServer` extension method to set up the HTTP module that watches for requests to the authentication endpoint. Note that remote authentication scenarios typically want to add proxy support as well, so that any authentication related redirects correctly route to the ASP.NET Core app rather than the ASP.NET one. + +:::code language="csharp" source="~/migration/fx-to-core/areas/authentication/samples/AspNetApp.cs" id="snippet_SystemWebAdapterConfiguration" ::: + +#### ASP.NET Core app configuration + +Next, the ASP.NET Core app needs to be configured to enable the authentication handler that will authenticate users by making an HTTP request to the ASP.NET app. Again, this is done by calling `AddAuthenticationClient` when registering System.Web adapters services: + +:::code language="csharp" source="~/migration/fx-to-core/areas/authentication/samples/AspNetCore.cs" id="snippet_AddSystemWebAdapters" highlight="8" ::: + +The boolean that is passed to the `AddAuthenticationClient` call specifies whether remote app authentication should be the default authentication scheme. Passing `true` will cause the user to be authenticated via remote app authentication for all requests, whereas passing `false` means that the user will only be authenticated with remote app authentication if the remote app scheme is specifically requested (with `[Authorize(AuthenticationSchemes = RemoteAppAuthenticationDefaults.AuthenticationScheme)]` on a controller or action method, for example). Passing false for this parameter has the advantage of only making HTTP requests to the original ASP.NET app for authentication for endpoints that require remote app authentication but has the disadvantage of requiring annotating all such endpoints to indicate that they will use remote app auth. + +:::zone-end + +:::zone pivot="aspire" +When using Aspire, the configuration will be done via environment variables and are set by the AppHost. To enable remote session, the option must be enabled: + +```csharp +... + +var coreApp = builder.AddProject("core") + .WithHttpHealthCheck() + .WaitFor(frameworkApp) + .WithIncrementalMigrationFallback(frameworkApp, options => options.RemoteAuthentication = RemoteAuthentication.DefaultScheme); + +... +``` + +Once this is done, it will be automatically hooked up in both the framework and core applications. +:::zone-end + +#### Using Remote Authentication with Specific Endpoints + +When you set the default scheme to `false`, you can specify remote authentication for specific controllers or actions: + +```csharp +[Authorize(AuthenticationSchemes = RemoteAppAuthenticationDefaults.AuthenticationScheme)] +public class SecureController : Controller +{ + // This controller uses remote authentication + public IActionResult Index() + { + return View(); + } +} + +// Or on specific actions +public class HomeController : Controller +{ + [Authorize(AuthenticationSchemes = RemoteAppAuthenticationDefaults.AuthenticationScheme)] + public IActionResult SecureAction() + { + return View(); + } +} +``` + +#### Implementing Custom Authentication Result Processors + +You can implement custom processors to modify authentication results before they are used: + +```csharp +public class CustomAuthResultProcessor : IRemoteAuthenticationResultProcessor +{ + public Task ProcessAsync(RemoteAuthenticationResult result, HttpContext context) + { + // Custom logic to process authentication results + if (result.Headers.ContainsKey("Location")) + { + // Modify redirect URLs or other logic + } + + return Task.CompletedTask; + } +} + +// Register the custom processor +builder.Services.AddScoped(); +``` + +Finally, if the ASP.NET Core app didn't previously include authentication middleware, that will need to be enabled (after routing middleware, but before authorization middleware). For more information about middleware ordering, see : + +:::code language="csharp" source="~/migration/fx-to-core/areas/authentication/samples/AspNetCore.cs" id="snippet_UseAuthentication" ::: + +#### Security Considerations + +When implementing remote authentication, consider the following security aspects: + +* **API Key Security**: The API key used for communication between the ASP.NET Core and ASP.NET apps should be stored securely using [configuration providers](xref:fundamentals/configuration/index) and not hardcoded. +* **Network Security**: Communication between the apps should occur over secure channels (HTTPS) in production environments. +* **Header Forwarding**: Be cautious about which headers you forward to avoid exposing sensitive information. Only forward headers that are necessary for authentication. +* **Endpoint Protection**: The authentication endpoint on the ASP.NET app should only be accessible to the ASP.NET Core app, not external clients. + +#### Troubleshooting + +Common issues when configuring remote authentication: + +* **Authentication failures**: Verify that the API keys match between both applications and that the authentication endpoint path is correctly configured. +* **Redirect loops**: Ensure that the `RedirectUrlProcessor` is properly configured to redirect to the ASP.NET Core app rather than the ASP.NET app. +* **Missing claims**: Verify that the ASP.NET app is properly serializing the `ClaimsPrincipal` and that all required claims are included. + +#### Design + +1. When requests are processed by the ASP.NET Core app, if remote app authentication is the default scheme or specified by the request's endpoint, the `RemoteAuthenticationAuthHandler` will attempt to authenticate the user. + 1. The handler will make an HTTP request to the ASP.NET app's authenticate endpoint. It will copy configured headers from the current request onto this new one in order to forward auth-relevant data. As mentioned above, default behavior is to copy the `Authorization` and `Cookie` headers. The API key header is also added for security purposes. +1. The ASP.NET app will serve requests sent to the authenticate endpoint. As long as the API keys match, the ASP.NET app will return either the current user's serialized into the response body or it will return an HTTP status code (like 401 or 302) and response headers indicating failure. +1. When the ASP.NET Core app's `RemoteAuthenticationAuthHandler` receives the response from the ASP.NET app: + 1. If a ClaimsPrincipal was successfully returned, the auth handler will deserialize it and use it as the current user's identity. + 1. If a ClaimsPrincipal was not successfully returned, the handler will store the result and if authentication is challenged (because the user is accessing a protected resource, for example), the request's response will be updated with the status code and selected response headers from the response from the authenticate endpoint. This enables challenge responses (like redirects to a login page) to be propagated to end users. + 1. Because results from the ASP.NET app's authenticate endpoint may include data specific to that endpoint, users can register `IRemoteAuthenticationResultProcessor` implementations with the ASP.NET Core app which will run on any authentication results before they are used. As an example, the one built-in `IRemoteAuthenticationResultProcessor` is `RedirectUrlProcessor` which looks for `Location` response headers returned from the authenticate endpoint and ensures that they redirect back to the host of the ASP.NET Core app and not the ASP.NET app directly. + +#### Known limitations + +This remote authentication approach has a couple known limitations: + +1. **Windows Authentication**: Because Windows authentication depends on a handle to a Windows identity, Windows authentication is not supported by this feature. Future work is planned to explore how shared Windows authentication might work. See [dotnet/systemweb-adapters#246](https://github.com/dotnet/systemweb-adapters/issues/246) for more information. For Windows authentication scenarios, consider using in your ASP.NET Core application instead. +1. **User Management Actions**: This feature allows the ASP.NET Core app to make use of an identity authenticated by the ASP.NET app, but all actions related to users (logging on, logging off, etc.) still need to be routed through the ASP.NET app. +1. **Performance Considerations**: Each authentication request requires an HTTP call to the ASP.NET app, which may impact performance. Consider using shared cookie authentication (described in the Alternatives section) if performance is critical. + +## Shared cookie authentication + +If authentication in the ASP.NET app is done using `Microsoft.Owin` Cookie Authentication Middleware, an alternative solution to remote authentication is to configure the ASP.NET and ASP.NET Core apps so that they are able to share an authentication cookie. + +### How shared cookies work + +Sharing an authentication cookie enables: + +* Both apps to determine the user identity from the same cookie. +* Signing in or out of one app signs the user in or out of the other app. + +Both applications are configured to: +* Use the same cookie name and domain settings +* Share data protection keys for cookie encryption/decryption +* Use compatible cookie authentication middleware + +This allows users authenticated in one app to be automatically authenticated in the other app when they make requests. + +### Shared cookie authentication pros and cons + +| Pros | Cons | +|------|------| +| Best performance for shared auth | Only works with OWIN cookie auth | +| No additional HTTP requests | Requires matching data protection setup | +| Both apps can handle sign-in/sign-out | More complex initial configuration | +| Seamless user experience | Limited to cookie-based authentication | +| Lower network overhead | Requires coordination of deployments | + +### Authentication limitations with shared cookies + +Note that because signing in typically depends on a specific database, not all authentication functionality will work in both apps: + +* Users should sign in through only one of the apps, either the ASP.NET or ASP.NET Core app, whichever the database is setup to work with. +* Both apps are able to see the users' identity and claims. +* Both apps are able to sign the user out. + +Details on how to configure sharing auth cookies between ASP.NET and ASP.NET Core apps are available in [cookie sharing documentation](xref:security/cookie-sharing). For more information about cookie authentication in ASP.NET Core, see . + +The following samples in the [System.Web adapters](https://github.com/dotnet/systemweb-adapters) GitHub repo demonstrates remote app authentication with shared cookie configuration enabling both apps to sign users in and out: + +* [ASP.NET app](https://github.com/dotnet/systemweb-adapters/tree/main/samples/RemoteAuth/Identity/MvcApp) +* [ASP.NET Core app](https://github.com/dotnet/systemweb-adapters/tree/main/samples/RemoteAuth/Identity/MvcCoreApp) + +### When to choose shared cookie authentication + +**Choose Shared Cookie Authentication when:** +* Your ASP.NET app already uses `Microsoft.Owin` cookie authentication +* You can configure matching data protection settings between both apps +* Performance is critical and you want to minimize HTTP requests +* You want both apps to handle user sign-in/sign-out operations +* You're comfortable managing shared encryption keys + +Sharing authentication is a good option if both the following are true: + +* The ASP.NET app is already using `Microsoft.Owin` cookie authentication. +* It's possible to update the ASP.NET app and ASP.NET Core apps to use matching data protection settings. Matching shared data protection settings includes a shared file path, Redis cache, or Azure Blob Storage for storing data protection keys. For more information, see . + +For other scenarios, the remote authentication approach described previously in this doc is more flexible and is probably a better fit. + +## See also + +* +* +* +* +* +* +* diff --git a/aspnetcore/migration/fx-to-core/inc/samples/remote-authentication/AspNetApp.cs b/aspnetcore/migration/fx-to-core/areas/authentication/samples/AspNetApp.cs similarity index 100% rename from aspnetcore/migration/fx-to-core/inc/samples/remote-authentication/AspNetApp.cs rename to aspnetcore/migration/fx-to-core/areas/authentication/samples/AspNetApp.cs diff --git a/aspnetcore/migration/fx-to-core/inc/samples/remote-authentication/AspNetCore.cs b/aspnetcore/migration/fx-to-core/areas/authentication/samples/AspNetCore.cs similarity index 100% rename from aspnetcore/migration/fx-to-core/inc/samples/remote-authentication/AspNetCore.cs rename to aspnetcore/migration/fx-to-core/areas/authentication/samples/AspNetCore.cs diff --git a/aspnetcore/migration/fx-to-core/areas/claimsprincipal-current.md b/aspnetcore/migration/fx-to-core/areas/claimsprincipal-current.md index a6555eb93426..86e4a51893bb 100644 --- a/aspnetcore/migration/fx-to-core/areas/claimsprincipal-current.md +++ b/aspnetcore/migration/fx-to-core/areas/claimsprincipal-current.md @@ -1,31 +1,202 @@ --- -title: Migrate from ClaimsPrincipal.Current +title: Migrate from static ClaimsPrincipal access author: mjrousos -description: Learn how to migrate away from ClaimsPrincipal.Current to retrieve the current authenticated user's identity and claims in ASP.NET Core. +description: Learn how to migrate away from static ClaimsPrincipal access to retrieve the current authenticated user's identity and claims in ASP.NET Core. ms.author: wpickett ms.custom: mvc -ms.date: 03/26/2019 +ms.date: 06/30/2025 uid: migration/fx-to-core/areas/claimsprincipal-current --- -# Migrate from ClaimsPrincipal.Current +# Migrate from static ClaimsPrincipal access -In ASP.NET 4.x projects, it was common to use to retrieve the current authenticated user's identity and claims. In ASP.NET Core, this property is no longer set. Code that was depending on it needs to be updated to get the current authenticated user's identity through a different means. +The current ClaimsPrincipal is a fundamental component of authenticated web applications, providing access to the current user's identity and claims. When migrating from ASP.NET Framework to ASP.NET Core, accessing this presents unique challenges because the two frameworks have different approaches to user context management. -## Context-specific state instead of static state +## Why static ClaimsPrincipal migration is complex -When using ASP.NET Core, the values of both `ClaimsPrincipal.Current` and `Thread.CurrentPrincipal` aren't set. These properties both represent static state, which ASP.NET Core generally avoids. Instead, ASP.NET Core uses [dependency injection](xref:fundamentals/dependency-injection) (DI) to provide dependencies such as the current user's identity. Getting the current user's identity from DI is more testable, too, since test identities can be easily injected. +ASP.NET Framework and ASP.NET Core have fundamentally different approaches to accessing the current user: -## Retrieve the current user in an ASP.NET Core app +* **ASP.NET Framework** uses static properties like and with automatic context management. These properties are interchangeable and both provide access to the current user's identity. +* **ASP.NET Core** stores the current user in and avoids static state. -There are several options for retrieving the current authenticated user's `ClaimsPrincipal` in ASP.NET Core in place of `ClaimsPrincipal.Current`: +These differences mean you can't simply continue using static principal properties ( or ) in ASP.NET Core without changes. By default, the static properties aren't set, and code depending on them needs to be updated to get the current authenticated user's identity through different means. -* **ControllerBase.User**. MVC controllers can access the current authenticated user with their property. -* **HttpContext.User**. Components with access to the current `HttpContext` (middleware, for example) can get the current user's `ClaimsPrincipal` from . -* **Passed in from caller**. Libraries without access to the current `HttpContext` are often called from controllers or middleware components and can have the current user's identity passed as an argument. -* **IHttpContextAccessor**. The project being migrated to ASP.NET Core may be too large to easily pass the current user's identity to all necessary locations. In such cases, can be used as a workaround. `IHttpContextAccessor` is able to access the current `HttpContext` (if one exists). If DI is being used, see . A short-term solution to getting the current user's identity in code that hasn't yet been updated to work with ASP.NET Core's DI-driven architecture would be: +## Migration strategies overview - * Make `IHttpContextAccessor` available in the DI container by calling [AddHttpContextAccessor](https://github.com/aspnet/Hosting/issues/793) in `Startup.ConfigureServices`. - * Get an instance of `IHttpContextAccessor` during startup and store it in a static variable. The instance is made available to code that was previously retrieving the current user from a static property. - * Retrieve the current user's `ClaimsPrincipal` using `HttpContextAccessor.HttpContext?.User`. If this code is used outside of the context of an HTTP request, the `HttpContext` is null. +You have two main approaches for handling static principal access during migration: -The final option, using an `IHttpContextAccessor` instance stored in a static variable, is contrary to the ASP.NET Core principle of preferring injected dependencies to static dependencies. Plan to eventually retrieve `IHttpContextAccessor` instances from DI instead. A static helper can be a useful bridge, though, when migrating large existing ASP.NET apps that use `ClaimsPrincipal.Current`. +1. **Complete rewrite** - Rewrite all static principal access code to use ASP.NET Core's native patterns +2. **System.Web adapters** - Use adapters to enable static access patterns during incremental migration + +For most applications, migrating to ASP.NET Core's native ClaimsPrincipal access provides the best performance and maintainability. However, larger applications or those with extensive static principal usage may benefit from using System.Web adapters during incremental migration. + +## Choose your migration approach + +You have two main options for migrating static principal access from ASP.NET Framework to ASP.NET Core. Your choice depends on your migration timeline, whether you need to run both applications simultaneously, and how much code you're willing to rewrite. + +### Quick decision guide + +**Answer these questions to choose your approach:** + +1. **Are you doing a complete rewrite or incremental migration?** + * Complete rewrite → [Complete rewrite to ASP.NET Core patterns](#complete-rewrite-to-aspnet-core-patterns) + * Incremental migration → Continue to question 2 + +2. **Do you have extensive static principal usage ( or ) across shared libraries?** + * Yes, lots of shared code → [System.Web adapters](#systemweb-adapters) + * No, isolated static principal usage → [Complete rewrite to ASP.NET Core patterns](#complete-rewrite-to-aspnet-core-patterns) + +### Migration approaches comparison + +| Approach | Code Changes | Performance | Shared Libraries | When to Use | +|----------|-------------|-------------|------------------|-------------| +| **[Complete rewrite](#complete-rewrite-to-aspnet-core-patterns)** | High - Rewrite all static principal access | Best | Requires updates | Complete rewrites, performance-critical apps | +| **[System.Web adapters](#systemweb-adapters)** | Low - Keep existing patterns | Good | Works with existing code | Incremental migrations, extensive static access | + +## Complete rewrite to ASP.NET Core patterns + +Choose this approach when you're performing a complete migration or want the best performance and maintainability. + +ASP.NET Core provides several options for retrieving the current authenticated user's without relying on static properties. This approach requires rewriting static principal access code but offers the most benefits in the long term. + +### Complete rewrite pros and cons + +| Pros | Cons | +|------|------| +| Best performance | Requires rewriting all static principal access code | +| More testable (dependency injection) | No automatic migration path | +| No static dependencies | Learning curve for new patterns | +| Native ASP.NET Core implementation | Breaking change from Framework patterns | +| Thread-safe by design | Potential refactoring across shared libraries | + +### ASP.NET Core ClaimsPrincipal access patterns + +There are several options for retrieving the current authenticated user's in ASP.NET Core in place of : + +* **** +* **** +* **Passed in from caller**. Libraries without access to the current are often called from controllers or middleware components and can have the current user's identity passed as an argument. +* ****. The project being migrated to ASP.NET Core may be too large to easily pass the current user's identity to all necessary locations. In such cases, can be used as a workaround. This is not ideal as it uses a static accessor behind the scenes. Prefer a more direct option if possible. + +### Code examples + +Here are examples of migrating common static principal usage patterns: + +**ASP.NET Framework (before):** +```csharp +public class UserService +{ + public string GetCurrentUserId() + { + // Both ClaimsPrincipal.Current and Thread.CurrentPrincipal work interchangeably + return ClaimsPrincipal.Current?.FindFirst(ClaimTypes.NameIdentifier)?.Value; + // or: return Thread.CurrentPrincipal?.Identity?.Name; + } +} +``` + +**ASP.NET Core (after) - Pass ClaimsPrincipal as parameter:** +```csharp +public class UserService +{ + public string GetCurrentUserId(ClaimsPrincipal user) + { + return user?.FindFirst(ClaimTypes.NameIdentifier)?.Value; + } +} + +// Usage in controller +public class HomeController : Controller +{ + private readonly UserService _userService; + + public HomeController(UserService userService) + { + _userService = userService; + } + + public IActionResult Index() + { + var userId = _userService.GetCurrentUserId(User); + return View(); + } +} +``` + +**ASP.NET Core (after) - Dependency Injection:** +```csharp +public class UserService +{ + private readonly IHttpContextAccessor _httpContextAccessor; + + public UserService(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + public string GetCurrentUserId() + { + return _httpContextAccessor.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value; + } +} +``` + +### When to choose this approach + +* You can afford to rewrite static principal access code +* Performance is a top priority (in this case, prefer passing the identity as a parameter over DI) +* You want to eliminate static dependencies +* You're not sharing code with legacy applications +* You want the most testable and maintainable solution + +## System.Web adapters + +[!INCLUDE[](~/migration/fx-to-core/includes/uses-systemweb-adapters.md)] + +Choose this approach when you need to maintain existing static principal usage patterns during incremental migration, or when you have extensive shared libraries that would be difficult to update. + +The System.Web adapters can enable both and support in ASP.NET Core, allowing you to keep existing code patterns while migrating incrementally. Both properties work interchangeably once adapters are configured. + +### System.Web adapters pros and cons + +| Pros | Cons | +|------|------| +| Minimal code changes required | Performance overhead | +| Works with existing shared libraries | Not thread-safe in all scenarios | +| Enables incremental migration | Requires System.Web adapters dependency | +| Maintains familiar patterns | Should be temporary solution | +| Good for large codebases | Less testable than DI patterns | + +### Setting up static principal support + +To enable static principal support ( and ) with System.Web adapters, endpoints must be annotated with the `SetThreadCurrentPrincipalAttribute` metadata: + +```csharp +// Add to controller or action +[SetThreadCurrentPrincipal] +public class HomeController : Controller +{ + public IActionResult Index() + { + // Both ClaimsPrincipal.Current and Thread.CurrentPrincipal are now available + var user1 = ClaimsPrincipal.Current; + var user2 = Thread.CurrentPrincipal; + return View(); + } +} +``` + +### When to use System.Web adapters + +* You have extensive static principal usage across shared libraries +* You're doing an incremental migration +* You can't afford to rewrite all static principal access code immediately +* You need to maintain compatibility with existing ASP.NET Framework code +* You understand the performance and threading implications + +## Migration considerations + +### Performance implications + +* **Native ASP.NET Core patterns** provide the best performance with no overhead +* **System.Web adapters** introduce some performance overhead but enable gradual migration +* **Static variables** should be avoided as they can cause memory leaks and threading issues diff --git a/aspnetcore/migration/fx-to-core/areas/http-context.md b/aspnetcore/migration/fx-to-core/areas/http-context.md index 0dbe26775c85..cb7d72f65d8a 100644 --- a/aspnetcore/migration/fx-to-core/areas/http-context.md +++ b/aspnetcore/migration/fx-to-core/areas/http-context.md @@ -1,159 +1,284 @@ --- -title: Migrate from System.Web.HttpContext to Micrsoft.AspNetCore.Http.HttpContext -description: Learn how to migrate from System.Web.HttpContext to Micrsoft.AspNetCore.Http.HttpContext +title: Migrate ASP.NET Framework HttpContext to ASP.NET Core +description: Learn how to migrate from System.Web.HttpContext to Microsoft.AspNetCore.Http.HttpContext author: twsouthwick ms.author: tasou ms.date: 6/20/2025 uid: migration/fx-to-core/areas/http-context --- -# Migrate from System.Web.HttpContext to Micrsoft.AspNetCore.Http.HttpContext +# Migrate ASP.NET Framework HttpContext to ASP.NET Core -This article shows how to translate the most commonly used properties of to the equivalent in ASP.NET Core. +HttpContext is a fundamental component of web applications, providing access to HTTP request and response information. When migrating from ASP.NET Framework to ASP.NET Core, HttpContext presents unique challenges because the two frameworks have different APIs and approaches. -## Overview +## Why HttpContext migration is complex -`HttpContext` has significantly changed in ASP.NET Core. When migrating HTTP modules or handlers to middleware, you'll need to update your code to work with the new `HttpContext` API. - -In ASP.NET Core middleware, the `Invoke` method takes a parameter of type `HttpContext`: - -```csharp -public async Task Invoke(HttpContext context) -``` - -This `HttpContext` is different from the ASP.NET Framework version and requires different approaches to access request and response information. - -## HttpContext +ASP.NET Framework and ASP.NET Core have fundamentally different HttpContext implementations: -**HttpContext.Items** translates to: +* **ASP.NET Framework** uses with built-in properties and methods +* **ASP.NET Core** uses with a more modular, extensible design -[!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_Items)] +These differences mean you can't simply move your HttpContext code from Framework to Core without changes. -**Unique request ID (no System.Web.HttpContext counterpart)** +## Migration strategies overview -Gives you a unique id for each request. Very useful to include in your logs. +You have two main approaches for handling HttpContext during migration: -[!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_Trace)] +1. **Complete rewrite** - Rewrite all HttpContext code to use ASP.NET Core's native HttpContext implementation +2. **System.Web adapters** - Use adapters to minimize code changes while migrating incrementally -## HttpContext.Request +For most applications, migrating to ASP.NET Core's native HttpContext provides the best performance and maintainability. However, larger applications or those with extensive HttpContext usage may benefit from using System.Web adapters during incremental migration. -**HttpContext.Request.HttpMethod** translates to: +## Choose your migration approach -[!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_Method)] +You have two main options for migrating HttpContext from ASP.NET Framework to ASP.NET Core. Your choice depends on your migration timeline, whether you need to run both applications simultaneously, and how much code you're willing to rewrite. -**HttpContext.Request.QueryString** translates to: +### Quick decision guide -[!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_Query)] +**Answer these questions to choose your approach:** -**HttpContext.Request.Url** and **HttpContext.Request.RawUrl** translate to: +1. **Are you doing a complete rewrite or incremental migration?** + * Complete rewrite → [Complete rewrite to ASP.NET Core HttpContext](#complete-rewrite-to-aspnet-core-httpcontext) + * Incremental migration → Continue to question 2 -[!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_Url)] +2. **Do you have extensive HttpContext usage across shared libraries?** + * Yes, lots of shared code → [System.Web adapters](#systemweb-adapters) + * No, isolated HttpContext usage → [Complete rewrite to ASP.NET Core HttpContext](#complete-rewrite-to-aspnet-core-httpcontext) -**HttpContext.Request.IsSecureConnection** translates to: +### Migration approaches comparison -[!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_Secure)] +| Approach | Code Changes | Performance | Shared Libraries | When to Use | +|----------|-------------|-------------|------------------|-------------| +| **[Complete rewrite](#complete-rewrite-to-aspnet-core-httpcontext)** | High - Rewrite all HttpContext code | Best | Requires updates | Complete rewrites, performance-critical apps | +| **[System.Web adapters](#systemweb-adapters)** | Low - Keep existing patterns | Good | Works with existing code | Incremental migrations, extensive HttpContext usage | -**HttpContext.Request.UserHostAddress** translates to: +## Important differences -[!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_Host)] +### HttpContext lifetime -**HttpContext.Request.Cookies** translates to: +The adapters are backed by which cannot be used past the lifetime of a request. Thus, when run on ASP.NET Core cannot be used past a request as well, while on ASP.NET Framework it would work at times. An will be thrown in cases where it is used past a request end. -[!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_Cookies)] +**Recommendation**: Store the values needed into a POCO and hold onto that. -**HttpContext.Request.RequestContext.RouteData** translates to: +### Request threading considerations -[!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_Route)] - -**HttpContext.Request.Headers** translates to: - -[!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_Headers)] +> [!WARNING] +> ASP.NET Core does not guarantee thread affinity for requests. If your code requires thread-safe access to `HttpContext`, you must ensure proper synchronization. -**HttpContext.Request.UserAgent** translates to: +In ASP.NET Framework, a request had thread-affinity and would only be available if on that thread. ASP.NET Core does not have this guarantee so will be available within the same async context, but no guarantees about threads are made. -[!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_Agent)] +**Recommendation**: If reading/writing to the , you must ensure you are doing so in a single-threaded way. You can force a request to never run concurrently on any async context by setting the `ISingleThreadedRequestMetadata`. This will have performance implications and should only be used if you can't refactor usage to ensure non-concurrent access. There is an implementation available to add to controllers with `SingleThreadedRequestAttribute`: -**HttpContext.Request.UrlReferrer** translates to: +```csharp +[SingleThreadedRequest] +public class SomeController : Controller +{ + ... +} +``` -[!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_Referrer)] +### Request stream buffering -**HttpContext.Request.ContentType** translates to: +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). -[!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_Type)] +**Recommendation**: 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. -**HttpContext.Request.Form** translates to: +To enable this on all MVC endpoints, there is an extension method that can be used as follows: -[!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_Form)] +```cs +app.MapDefaultControllerRoute() + .PreBufferRequestStream(); +``` -> [!WARNING] -> Read form values only if the content sub type is *x-www-form-urlencoded* or *form-data*. +### Response stream buffering -**HttpContext.Request.InputStream** translates to: +Some APIs on require that the output stream is buffered, such as , , , and . -[!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_Input)] +**Recommendation**: In order to support behavior for that requires buffering the response before sending, endpoints must opt-into it with endpoint metadata implementing `IBufferResponseStreamMetadata`. -> [!WARNING] -> Use this code only in a handler type middleware, at the end of a pipeline. -> ->You can read the raw body as shown above only once per request. Middleware trying to read the body after the first read will read an empty body. -> ->This doesn't apply to reading a form as shown earlier, because that's done from a buffer. +To enable this on all MVC endpoints, there is an extension method that can be used as follows: -## HttpContext.Response - -**HttpContext.Response.Status** and **HttpContext.Response.StatusDescription** translate to: +```cs +app.MapDefaultControllerRoute() + .BufferResponseStream(); +``` -[!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_Status)] +## Complete rewrite to ASP.NET Core HttpContext -**HttpContext.Response.ContentEncoding** and **HttpContext.Response.ContentType** translate to: +Choose this approach when you're performing a complete migration and can rewrite HttpContext-related code to use ASP.NET Core's native implementation. -[!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_RespType)] +ASP.NET Core's HttpContext provides a more modular and extensible design compared to ASP.NET Framework. This approach offers the best performance but requires more code changes during migration. -**HttpContext.Response.ContentType** on its own also translates to: +### Overview -[!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_RespTypeOnly)] +`HttpContext` has significantly changed in ASP.NET Core. When migrating HTTP modules or handlers to middleware, you'll need to update your code to work with the new `HttpContext` API. -**HttpContext.Response.Output** translates to: +In ASP.NET Core middleware, the `Invoke` method takes a parameter of type `HttpContext`: -[!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_Output)] +```csharp +public async Task Invoke(HttpContext context) +``` -**HttpContext.Response.TransmitFile** +This `HttpContext` is different from the ASP.NET Framework version and requires different approaches to access request and response information. -Serving up a file is discussed in . +### Property translations -**HttpContext.Response.Headers** +This section shows how to translate the most commonly used properties of to the equivalent in ASP.NET Core. -Sending response headers is complicated by the fact that if you set them after anything has been written to the response body, they will not be sent. +#### HttpContext properties -The solution is to set a callback method that will be called right before writing to the response starts. This is best done at the start of the `Invoke` method in your middleware. It's this callback method that sets your response headers. +* **** → **** + + [!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_Items)] -The following code sets a callback method called `SetHeaders`: +* ***No equivalent*** → **** + + [!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_Trace)] + + Unique request ID for logging + +#### HttpRequest properties + +* **** → **** + + [!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_Method)] + +* **** → **** + + [!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_Query)] + +* **** / **** → **Multiple properties** + + [!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_Url)] + + Use Request.Scheme, Host, PathBase, Path, QueryString + +* **** → **** + + [!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_Secure)] + +* **** → **** + + [!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_Host)] + +* **** → **** + + [!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_Cookies)] + +* **** → **** + + [!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_Route)] + +* **** → **** + + [!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_Headers)] + +* **** → **** + + [!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_Agent)] + +* **** → **** + + [!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_Referrer)] + +* **** → **** + + [!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_Type)] + +* **** → **** + + [!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_Form)] + + **Warning**: Read form values only if content type is *x-www-form-urlencoded* or *form-data* + +* **** → **** + + [!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_Input)] + + **Warning**: Use only in handler middleware at end of pipeline. Body can only be read once per request + +#### HttpResponse properties + +* **** / **** → **** + + [!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_Status)] + +* **** / **** → **** + + [!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_RespType)] + +* **** → **** + + [!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_RespTypeOnly)] + +* **** → **** + + [!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_Output)] + +* **** → **See request features** + + Serving files is discussed in + +* **** → **** + + [!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_SetHeaders)] + + Must use callback pattern to set headers before response starts + +* **** → **** + + [!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_SetCookies)] + + Must use callback pattern to set cookies before response starts + +* Setting response headers: + + ```csharp + public async Task Invoke(HttpContext httpContext) + { + // Set callback to execute before response starts + httpContext.Response.OnStarting(SetHeaders, state: httpContext); + // ... rest of middleware logic + } + ``` + +* Setting response cookies: ```csharp public async Task Invoke(HttpContext httpContext) { - // ... + // Set callbacks to execute before response starts + httpContext.Response.OnStarting(SetCookies, state: httpContext); httpContext.Response.OnStarting(SetHeaders, state: httpContext); + // ... rest of middleware logic +} ``` -The `SetHeaders` callback method would look like this: +## System.Web adapters -[!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_SetHeaders)] +[!INCLUDE[](~/migration/fx-to-core/includes/uses-systemweb-adapters.md)] -**HttpContext.Response.Cookies** +Choose this approach when you have extensive HttpContext usage across shared libraries or when performing an incremental migration where you want to minimize code changes. -Cookies travel to the browser in a *Set-Cookie* response header. As a result, sending cookies requires the same callback as used for sending response headers: +The [System.Web adapters](~/migration/fx-to-core/inc/systemweb-adapters.md) provide a compatibility layer that allows you to use familiar APIs in ASP.NET Core applications. This approach is particularly useful when: -```csharp -public async Task Invoke(HttpContext httpContext) -{ - // ... - httpContext.Response.OnStarting(SetCookies, state: httpContext); - httpContext.Response.OnStarting(SetHeaders, state: httpContext); -``` +* You have shared libraries that use +* You're performing an incremental migration +* You want to minimize code changes during the migration process + +### Benefits of using System.Web adapters + +* **Minimal code changes**: Keep your existing `System.Web.HttpContext` usage patterns +* **Shared libraries**: Libraries can work with both ASP.NET Framework and ASP.NET Core +* **Incremental migration**: Migrate applications piece by piece without breaking shared dependencies +* **Faster migration**: Reduce the time needed to migrate complex applications + +### Considerations -The `SetCookies` callback method would look like the following: +* **Performance**: While good, adapters introduce some overhead compared to native ASP.NET Core APIs +* **Feature parity**: Not all features are available through adapters +* **Long-term strategy**: Consider eventually migrating to native ASP.NET Core APIs for best performance -[!code-csharp[](sample/Asp.Net.Core/Middleware/HttpContextDemoMiddleware.cs?name=snippet_SetCookies)] +For more information about System.Web adapters, see the [System.Web adapters documentation](~/migration/fx-to-core/inc/systemweb-adapters.md). ## Additional resources @@ -161,3 +286,4 @@ The `SetCookies` callback method would look like the following: * [HttpContext in ASP.NET Core](xref:fundamentals/httpcontext) * [Middleware](xref:fundamentals/middleware/index) * [Configuration](xref:fundamentals/configuration/index) +* [System.Web adapters](~/migration/fx-to-core/inc/systemweb-adapters.md) diff --git a/aspnetcore/migration/fx-to-core/areas/http-handlers.md b/aspnetcore/migration/fx-to-core/areas/http-handlers.md index 92e9c196f547..48758c5911fe 100644 --- a/aspnetcore/migration/fx-to-core/areas/http-handlers.md +++ b/aspnetcore/migration/fx-to-core/areas/http-handlers.md @@ -83,77 +83,7 @@ One solution is to branch the pipeline for requests with a given extension, usin Middleware added to the pipeline before the branch will be invoked on all requests; the branch will have no impact on them. -## Loading middleware options using the options pattern - -Some handlers have configuration options that are stored in *Web.config*. However, in ASP.NET Core a new configuration model is used in place of *Web.config*. - -The new [configuration system](xref:fundamentals/configuration/index) gives you these options to solve this: - -* Directly inject the options into the middleware, as shown in the [next section](#loading-middleware-options-through-direct-injection). - -* Use the [options pattern](xref:fundamentals/configuration/options): - -1. Create a class to hold your middleware options, for example: - - [!code-csharp[](sample/Asp.Net.Core/Middleware/MyMiddlewareWithParams.cs?name=snippet_Options)] - -2. Store the option values - - The configuration system allows you to store option values anywhere you want. However, most sites use `appsettings.json`, so we'll take that approach: - - [!code-json[](sample/Asp.Net.Core/appsettings.json?range=1,14-18)] - - *MyMiddlewareOptionsSection* here is a section name. It doesn't have to be the same as the name of your options class. - -3. Associate the option values with the options class - - The options pattern uses ASP.NET Core's dependency injection framework to associate the options type (such as `MyMiddlewareOptions`) with a `MyMiddlewareOptions` object that has the actual options. - - Update your `Startup` class: - - 1. If you're using `appsettings.json`, add it to the configuration builder in the `Startup` constructor: - - [!code-csharp[](./sample/Asp.Net.Core/Startup.cs?name=snippet_Ctor&highlight=5-6)] - - 2. Configure the options service: - - [!code-csharp[](./sample/Asp.Net.Core/Startup.cs?name=snippet_ConfigureServices&highlight=4)] - - 3. Associate your options with your options class: - - [!code-csharp[](./sample/Asp.Net.Core/Startup.cs?name=snippet_ConfigureServices&highlight=6-8)] - -4. Inject the options into your middleware constructor. This is similar to injecting options into a controller. - - [!code-csharp[](./sample/Asp.Net.Core/Middleware/MyMiddlewareWithParams.cs?name=snippet_MiddlewareWithParams&highlight=4,7,10,15-16)] - - The `UseMiddleware` extension method that adds your middleware to the `IApplicationBuilder` takes care of dependency injection. - - This isn't limited to `IOptions` objects. Any other object that your middleware requires can be injected this way. - -## Loading middleware options through direct injection - -The options pattern has the advantage that it creates loose coupling between options values and their consumers. Once you've associated an options class with the actual options values, any other class can get access to the options through the dependency injection framework. There's no need to pass around options values. - -This breaks down though if you want to use the same middleware twice, with different options. For example an authorization middleware used in different branches allowing different roles. You can't associate two different options objects with the one options class. - -The solution is to get the options objects with the actual options values in your `Startup` class and pass those directly to each instance of your middleware. - -1. Add a second key to `appsettings.json` - - To add a second set of options to the `appsettings.json` file, use a new key to uniquely identify it: - - [!code-json[](sample/Asp.Net.Core/appsettings.json?range=1,10-18&highlight=2-5)] - -2. Retrieve options values and pass them to middleware. The `Use...` extension method (which adds your middleware to the pipeline) is a logical place to pass in the option values: - - [!code-csharp[](sample/Asp.Net.Core/Startup.cs?name=snippet_Configure&highlight=20-23)] - -3. Enable middleware to take an options parameter. Provide an overload of the `Use...` extension method (that takes the options parameter and passes it to `UseMiddleware`). When `UseMiddleware` is called with parameters, it passes the parameters to your middleware constructor when it instantiates the middleware object. - - [!code-csharp[](./sample/Asp.Net.Core/Middleware/MyMiddlewareWithParams.cs?name=snippet_Extensions&highlight=9-14)] - - Note how this wraps the options object in an `OptionsWrapper` object. This implements `IOptions`, as expected by the middleware constructor. +For additional details, see the [middleware documentation](xref:fundamentals/middleware/index) for additional ways to use middleware to replace your usage of handlers. ## Migrating to the new HttpContext @@ -168,7 +98,6 @@ public async Task Invoke(HttpContext context) ## Additional resources * [HTTP Handlers and HTTP Modules Overview](/iis/configuration/system.webserver/) -* [Configuration](xref:fundamentals/configuration/index) * [Application Startup](xref:fundamentals/startup) * [Middleware](xref:fundamentals/middleware/index) * [Migrate from ASP.NET Framework HttpContext to ASP.NET Core](http-context.md) diff --git a/aspnetcore/migration/fx-to-core/areas/index.md b/aspnetcore/migration/fx-to-core/areas/index.md index 118690a5a7ee..0c3f1acbd7b6 100644 --- a/aspnetcore/migration/fx-to-core/areas/index.md +++ b/aspnetcore/migration/fx-to-core/areas/index.md @@ -8,12 +8,15 @@ uid: migration/fx-to-core/areas --- # Technology specific guidance +* [Authentication](authentication.md) +* [ClaimsPrincipal.Current](claimsprincipal-current.md) * [HttpContext](http-context.md) * [HTTP Handlers](http-handlers.md) * [HTTP Modules](http-modules.md) * [Membership](membership.md) +* [Miscellaneous](misc.md) +* [Session State](session.md) * [WebAPI](webapi.md) -* [ClaimsPrincipal.Current](claimsprincipal-current.md) ## Getting help diff --git a/aspnetcore/migration/fx-to-core/areas/misc.md b/aspnetcore/migration/fx-to-core/areas/misc.md index 5e92a481ec05..248d33a8b501 100644 --- a/aspnetcore/migration/fx-to-core/areas/misc.md +++ b/aspnetcore/migration/fx-to-core/areas/misc.md @@ -19,8 +19,41 @@ ASP.NET Core handles URI encoding differently: | `\` | `%5C` | `\` | `/` | | `/` | `%2F` | `%2F` | `/` | -**Solution**: Use `new Uri(this.AspNetCoreHttpRequest.GetEncodedUrl())` for proper URL handling. +**Recommendation**: Use `new Uri(this.AspNetCoreHttpRequest.GetEncodedUrl())` for proper URL handling. ## User Secrets migration User Secrets require special handling. See [GitHub issue #27611](https://github.com/dotnet/AspNetCore.Docs/issues/27611) for current guidance. + +## CultureInfo.CurrentCulture differences + +> [!NOTE] +> ASP.NET Core does not automatically set `CultureInfo.CurrentCulture` for requests like ASP.NET Framework does. You must explicitly configure localization middleware. + +In ASP.NET Framework, was set for a request, but this is not done automatically in ASP.NET Core. Instead, you must add the appropriate middleware to your pipeline. + +**Recommendation**: See [ASP.NET Core Localization](/aspnet/core/fundamentals/localization#localization-middleware) for details on how to enable this. + +Simplest way to enable this with similar behavior as ASP.NET Framework would be to add the following to your pipeline: + +```csharp +app.UseRequestLocalization(); +``` + +### Threading considerations + +[!INCLUDE[](~/migration/fx-to-core/includes/uses-systemweb-adapters.md)] + +ASP.NET Core does not guarantee thread affinity for requests. If your code requires thread affinity, you must ensure proper synchronization or use the `SingleThreadedRequest` attribute: + +```csharp +[SingleThreadedRequest] +public class LegacyController : Controller +{ + public IActionResult Index() + { + // Some code that requires to be run on a single thread + return View(); + } +} +``` diff --git a/aspnetcore/migration/fx-to-core/areas/session.md b/aspnetcore/migration/fx-to-core/areas/session.md new file mode 100644 index 000000000000..ee4b951f5690 --- /dev/null +++ b/aspnetcore/migration/fx-to-core/areas/session.md @@ -0,0 +1,232 @@ +--- +title: ASP.NET to ASP.NET Core session state migration +description: ASP.NET to ASP.NET Core session state migration +author: rick-anderson +ms.author: riande +monikerRange: '>= aspnetcore-6.0' +ms.date: 11/9/2022 +ms.topic: article +uid: migration/fx-to-core/areas/session +zone_pivot_groups: migration-remote-app-setup +--- + +# Migrate ASP.NET Framework Session to ASP.NET Core + +Session state is a critical component of many web applications, storing user-specific data across HTTP requests. When migrating from ASP.NET Framework to ASP.NET Core, session state presents unique challenges because the two frameworks handle sessions very differently. + +## Why session migration is complex + +ASP.NET Framework and ASP.NET Core have fundamentally different approaches to session management: + +* **ASP.NET Framework** provides automatic object serialization and built-in session locking +* **ASP.NET Core** requires manual serialization and offers no session locking guarantees + +These differences mean you can't simply move your session code from Framework to Core without changes. + +## Migration strategies overview + +You have three main approaches for handling session state during migration: + +1. **Complete rewrite** - Rewrite all session code to use ASP.NET Core's native session implementation +2. **Incremental with separate sessions** - Migrate components piece by piece, with each app maintaining its own session state +3. **Incremental with shared sessions** - Migrate components while sharing session data between Framework and Core applications + +For most applications, migrating to [ASP.NET Core session](xref:fundamentals/app-state) provides the best performance and maintainability. However, larger applications or those with complex session requirements may benefit from an incremental approach using the System.Web adapters. + +## Choose your migration approach + +You have three main options for migrating session state from ASP.NET Framework to ASP.NET Core. Your choice depends on your migration timeline, whether you need to run both applications simultaneously, and how much code you're willing to rewrite. + +### Quick decision guide + +**Answer these questions to choose your approach:** + +1. **Are you doing a complete rewrite or incremental migration?** + * Complete rewrite → [Built-in ASP.NET Core session](#built-in-aspnet-core-session-state) + * Incremental migration → Continue to question 2 + +2. **Do both your ASP.NET Framework and ASP.NET Core apps need to access the same session data?** + * Yes, shared session needed → [Remote app session](#remote-app-session-state) + * No, separate sessions are fine → [Wrapped ASP.NET Core session](#wrapped-aspnet-core-session-state) + +### Understanding the differences + +Before diving into implementation details, it's important to understand how ASP.NET Framework and ASP.NET Core handle session state differently: + +* **Object serialization** + * ASP.NET Framework automatically serializes and deserializes objects (unless using in-memory storage) + * ASP.NET Core requires manual serialization/deserialization and stores data as `byte[]` +* **Session locking** + * ASP.NET Framework locks session usage within a session, handling subsequent requests serially + * ASP.NET Core provides no session locking guarantees + +### Migration approaches comparison + +| Approach | Code Changes | Performance | Session Sharing | When to Use | +|----------|-------------|-------------|-----------------|-------------| +| **[Built-in ASP.NET Core](#built-in-aspnet-core-session-state)** | High - Rewrite all session code | Best | None | Complete rewrites, performance-critical apps | +| **[Wrapped ASP.NET Core](#wrapped-aspnet-core-session-state)** | Low - Keep existing session patterns | Good | None | Incremental migrations, no shared state needed | +| **[Remote app](#remote-app-session-state)** | Low - Keep existing session patterns | Fair | Full | Running both apps simultaneously | + +The [System.Web adapters](~/migration/fx-to-core/inc/systemweb-adapters.md) enable the "Wrapped" and "Remote app" approaches by bridging the differences between ASP.NET Framework and Core session implementations through two key interfaces: + +* `Microsoft.AspNetCore.SystemWebAdapters.ISessionManager`: Accepts an and session metadata, returns an `ISessionState` object +* `Microsoft.AspNetCore.SystemWebAdapters.ISessionState`: Describes session object state and backs the type + +## Built-in ASP.NET Core session state + +Choose this approach when you're performing a complete migration and can rewrite session-related code to use ASP.NET Core's native session implementation. + +ASP.NET Core provides a lightweight, high-performance session state implementation that stores data as `byte[]` and requires explicit serialization. This approach offers the best performance but requires more code changes during migration. + +For details on how to set this up and use it, see the [ASP.NET session documentation]((xref:fundamentals/app-state.md). + +### Pros and cons + +| Pros | Cons | +|------|------| +| Best performance and lowest memory footprint | Requires rewriting all session access code | +| Native ASP.NET Core implementation | No automatic object serialization | +| Full control over serialization strategy | No session locking (concurrent requests may conflict) | +| No additional dependencies | Breaking change from ASP.NET Framework patterns | +| Supports all ASP.NET Core session providers | Session keys are case-sensitive (unlike Framework) | + +### Migration considerations + +When migrating to built-in ASP.NET Core session: + +**Code changes required:** +* Replace `Session["key"]` with `HttpContext.Session.GetString("key")` +* Replace `Session["key"] = value` with `HttpContext.Session.SetString("key", value)` +* Add explicit serialization/deserialization for complex objects +* Handle null values explicitly (no automatic type conversion) + +**Data migration:** +* Session data structure changes require careful planning +* Consider running both systems in parallel during migration +* Implement session data import/export utilities if needed + +**Testing strategy:** +* Unit test session serialization/deserialization logic +* Integration test session behavior across requests +* Load test concurrent session access patterns + +**When to choose this approach:** +* You can afford to rewrite session-related code +* Performance is a top priority +* You're not sharing session data with legacy applications +* You want to eliminate System.Web dependencies completely + +## System.Web Adapter Session + +[!INCLUDE[](~/migration/fx-to-core/includes/uses-systemweb-adapters.md)] + +### Serialization configuration + +The object requires serialization for remote app session state. + +In ASP.NET Framework, [BinaryFormatter](/dotnet/api/system.runtime.serialization.formatters.binary.binaryformatter) was used to automatically serialize session value contents. In order to serialize these with for use with the System.Web adapters, the serialization must be explicitly configured using `ISessionKeySerializer` implementations. + +Out of the box, there is a simple JSON serializer that allows each session key to be registered to a known type using `JsonSessionSerializerOptions`: + +* `RegisterKey(string)` - Registers a session key to a known type. This registration is required for correct serialization/deserialization. Missing registrations cause errors and prevent session access. + +:::code language="csharp" source="~/migration/fx-to-core/areas/session/samples/serialization/Program.cs" id="snippet_Serialization" ::: + +If more customization is needed, then `ISessionKeySerializer` can be implemented: + +:::code language="csharp" source="~/migration/fx-to-core/areas/session/samples/serialization/Program_Custom.cs" id="snippet_Serialization" ::: +:::code language="csharp" source="~/migration/fx-to-core/areas/session/samples/serialization/Program_Custom.cs" id="snippet_CustomSerializer" ::: + +> [!NOTE] +> When using the `AddJsonSessionSerializer` registration pattern, there is no need to call `AddSessionSerializer` as it will automatically be added. If you only want to use a customimplementation, then you must manually add it. + +### Enable session + +Session support requires explicit activation. Configure it per-route or globally using ASP.NET Core metadata: + +* Annotate controllers + +:::code language="csharp" source="~/migration/fx-to-core/areas/session/samples/remote/SomeController.cs" id="snippet_Controller" ::: + +* Enable globally for all endpoints + +:::code language="csharp" source="~/migration/fx-to-core/areas/session/samples/remote/Program.cs" id="snippet_RequireSystemWebAdapterSession" ::: + +## Wrapped ASP.NET Core session state + +Choose this approach when your migrated components don't need to share session data with your legacy application. + +The `Microsoft.Extensions.DependencyInjection.WrappedSessionExtensions.AddWrappedAspNetCoreSession` extension method adds a wrapped ASP.NET Core session to work with the adapters. It uses the same backing store as while providing strongly-typed access. + +**Configuration for ASP.NET Core:** + +:::code language="csharp" source="~/migration/fx-to-core/areas/session/samples/wrapped/Program.cs" id="snippet_WrapAspNetCoreSession" ::: + +Your Framework application requires no changes. + +For more information, see the [wrapped session state sample app](https://github.com/dotnet/systemweb-adapters/blob/main/samples/SessionLocal/SessionLocalCore/Program.cs) + +## Remote app session state + +[!INCLUDE[](~/migration/fx-to-core/includes/uses-systemweb-adapters.md)] + +Choose this approach when you need to share session state between your ASP.NET Framework and ASP.NET Core applications during incremental migration. + +Remote app session enables communication between applications to retrieve and set session state by exposing an endpoint on the ASP.NET Framework app. + +### Prerequisites + +Complete the [remote app setup](xref:migration/fx-to-core/inc/remote-app-setup) instructions to connect your ASP.NET Core and ASP.NET Framework applications. + +### Application configuration + +:::zone pivot="manual" +**ASP.NET Core configuration:** + +Call `AddRemoteAppSession` and `AddJsonSessionSerializer` to register known session item types: + +:::code language="csharp" source="~/migration/fx-to-core/areas/session/samples/remote/Program.cs" id="snippet_RemoteConfiguration" ::: + +**ASP.NET Framework configuration:** + +Add this change to `Global.asax.cs`: + +:::code language="csharp" source="~/migration/fx-to-core/areas/session/samples/remote/Global.asax.cs"::: + +:::zone-end + +:::zone pivot="aspire" +When using Aspire, the configuration will be done via environment variables and are set by the AppHost. To enable remote session, the option must be enabled: + +```csharp +... + +var coreApp = builder.AddProject("core") + .WithHttpHealthCheck() + .WaitFor(frameworkApp) + .WithIncrementalMigrationFallback(frameworkApp, options => options.RemoteSession = RemoteSession.Enabled); + +... +``` + +:::zone-end + +### Communication protocol + +#### Readonly sessions + +Readonly sessions retrieve session state without locking. The process uses a single `GET` request that returns session state and closes immediately. + +![Readonly session will retrieve the session state from the framework app](~/migration/fx-to-core/areas/session/_static/readonly_session.png) + +#### Writeable sessions + +Writeable sessions require additional steps: + +* Start with the same `GET` request as readonly sessions +* Keep the initial `GET` request open until the session completes +* Use an additional `PUT` request to update state +* Close the initial request only after updating is complete + +![Writeable session state protocol starts with the same as the readonly](~/migration/fx-to-core/areas/session/_static/writesession.png) diff --git a/aspnetcore/migration/fx-to-core/inc/overview/static/readonly_session.png b/aspnetcore/migration/fx-to-core/areas/session/_static/readonly_session.png similarity index 100% rename from aspnetcore/migration/fx-to-core/inc/overview/static/readonly_session.png rename to aspnetcore/migration/fx-to-core/areas/session/_static/readonly_session.png diff --git a/aspnetcore/migration/fx-to-core/inc/overview/static/writesession.png b/aspnetcore/migration/fx-to-core/areas/session/_static/writesession.png similarity index 100% rename from aspnetcore/migration/fx-to-core/inc/overview/static/writesession.png rename to aspnetcore/migration/fx-to-core/areas/session/_static/writesession.png diff --git a/aspnetcore/migration/fx-to-core/areas/session/samples/remote/Global.asax.cs b/aspnetcore/migration/fx-to-core/areas/session/samples/remote/Global.asax.cs new file mode 100644 index 000000000000..808a8b950058 --- /dev/null +++ b/aspnetcore/migration/fx-to-core/areas/session/samples/remote/Global.asax.cs @@ -0,0 +1,17 @@ +public class Global : HttpApplication +{ + protected void Application_Start() + { + SystemWebAdapterConfiguration.AddSystemWebAdapters(this) + .AddJsonSessionSerializer(options => + { + // Serialization/deserialization requires each session key to be registered to a type + options.RegisterKey("test-value"); + options.RegisterKey("SampleSessionItem"); + }) + // Provide a strong API key that will be used to authenticate the request on the remote app for querying the session + // ApiKey is a string representing a GUID + .AddRemoteAppServer(options => options.ApiKey = ConfigurationManager.AppSettings["RemoteAppApiKey"]) + .AddSessionServer(); + } +} \ No newline at end of file diff --git a/aspnetcore/migration/fx-to-core/inc/samples/remote-session/Program.cs b/aspnetcore/migration/fx-to-core/areas/session/samples/remote/Program.cs similarity index 77% rename from aspnetcore/migration/fx-to-core/inc/samples/remote-session/Program.cs rename to aspnetcore/migration/fx-to-core/areas/session/samples/remote/Program.cs index db9c7941791c..1131e7241717 100644 --- a/aspnetcore/migration/fx-to-core/inc/samples/remote-session/Program.cs +++ b/aspnetcore/migration/fx-to-core/areas/session/samples/remote/Program.cs @@ -8,7 +8,7 @@ }); // -// +// builder.Services.AddSystemWebAdapters() .AddJsonSessionSerializer(options => { @@ -25,6 +25,16 @@ options.ApiKey = builder.Configuration["RemoteAppApiKey"]; }) .AddSessionClient(); +// + +// +builder.Services.AddSystemWebAdapters() + .AddJsonSessionSerializer(options => + { + // Serialization/deserialization requires each session key to be registered to a type + options.RegisterKey("test-value"); + options.RegisterKey("SampleSessionItem"); + }); // var app = builder.Build(); diff --git a/aspnetcore/migration/fx-to-core/inc/samples/remote-session/SomeController.cs b/aspnetcore/migration/fx-to-core/areas/session/samples/remote/SomeController.cs similarity index 100% rename from aspnetcore/migration/fx-to-core/inc/samples/remote-session/SomeController.cs rename to aspnetcore/migration/fx-to-core/areas/session/samples/remote/SomeController.cs diff --git a/aspnetcore/migration/fx-to-core/inc/samples/session/Program.cs b/aspnetcore/migration/fx-to-core/areas/session/samples/serialization/Program.cs similarity index 100% rename from aspnetcore/migration/fx-to-core/inc/samples/session/Program.cs rename to aspnetcore/migration/fx-to-core/areas/session/samples/serialization/Program.cs diff --git a/aspnetcore/migration/fx-to-core/areas/session/samples/serialization/Program_Custom.cs b/aspnetcore/migration/fx-to-core/areas/session/samples/serialization/Program_Custom.cs new file mode 100644 index 000000000000..085ac6903e5f --- /dev/null +++ b/aspnetcore/migration/fx-to-core/areas/session/samples/serialization/Program_Custom.cs @@ -0,0 +1,26 @@ +var builder = WebApplication.CreateBuilder(args); + +// +builder.Services.AddSystemWebAdapters() + .AddSessionSerializer(); + +builder.Services.AddSingleton(); +// + +var app = builder.Build(); +app.Run(); + +// +sealed class CustomSessionKeySerializer : ISessionKeySerializer +{ + public bool TryDeserialize(string key, byte[] bytes, out object? obj) + { + // Custom deserialization logic + } + + public bool TrySerialize(string key, object? value, out byte[] bytes) + { + // Custom serialization logic + } +} +// \ No newline at end of file diff --git a/aspnetcore/migration/fx-to-core/inc/samples/wrapped/Program.cs b/aspnetcore/migration/fx-to-core/areas/session/samples/wrapped/Program.cs similarity index 100% rename from aspnetcore/migration/fx-to-core/inc/samples/wrapped/Program.cs rename to aspnetcore/migration/fx-to-core/areas/session/samples/wrapped/Program.cs diff --git a/aspnetcore/migration/fx-to-core/inc/ab-testing.md b/aspnetcore/migration/fx-to-core/inc/ab-testing.md index 02493d20bfdd..76c93029d1c0 100644 --- a/aspnetcore/migration/fx-to-core/inc/ab-testing.md +++ b/aspnetcore/migration/fx-to-core/inc/ab-testing.md @@ -13,7 +13,7 @@ uid: migration/fx-to-core/inc/ab-testing During incremental migration, new endpoints are brought over to a [YARP](https://dotnet.github.io/yarp/) enabled ASP.NET Core app. With the default setup, these endpoints are automatically served for all requests once deployed. In order to test these endpoints, or be able to turn them off if needed, additional setup is needed. -This document describes how to setup a conditional endpoint selection system to enable A/B testing during incremental migration. It assumes a setup as described in [incremental migration overview](xref:migration/fx-to-core/inc/overview) as a starting point. +This document describes how to setup a conditional endpoint selection system to enable A/B testing during incremental migration. It assumes a setup as described in [incremental migration overview](xref:migration/fx-to-core/index) as a starting point. ## Conditional endpoint selection diff --git a/aspnetcore/migration/fx-to-core/inc/overview.md b/aspnetcore/migration/fx-to-core/inc/overview.md deleted file mode 100644 index 13f7f38519cb..000000000000 --- a/aspnetcore/migration/fx-to-core/inc/overview.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -title: Incremental ASP.NET to ASP.NET Core update -description: Incremental ASP.NET to ASP.NET Core migration -author: rick-anderson -ms.author: riande -monikerRange: '>= aspnetcore-6.0' -ms.date: 11/9/2022 -ms.topic: article -uid: migration/fx-to-core/inc/overview ---- - - - -# Incremental ASP.NET to ASP.NET Core update - -Updating an app from ASP.NET Framework to ASP.NET Core is non-trivial for the majority of production apps. These apps often incorporate new technologies as they become available and are often composed of many legacy decisions. This article provides guidance and links to tools for updating ASP.NET Framework apps to ASP.NET Core with as little change as possible. - -One of the larger challenges is the pervasive use of throughout a code base. Without the incremental approach and tools, a large scale rewrite is required to remove the dependency. The adapters in [dotnet/systemweb-adapters](https://github.com/dotnet/systemweb-adapters) provide a set of runtime helpers to access the types used in the ASP.NET Framework app in a way that works in ASP.NET Core with minimal changes. - -A complete migration may take considerable effort depending on the size of the app, dependencies, and non-portable APIs used. In order to keep deploying an app to production while working on updating, the best pattern is to follow is the [Strangler Fig pattern](/azure/architecture/patterns/strangler-fig). The *Strangler Fig pattern* allows for continual development on the old system with an incremental approach to replacing specific pieces of functionality with new services. This document describes how to apply the Strangler Fig pattern to an ASP.NET app updating towards ASP.NET Core. - -If you'd like to skip this overview article and get started, see [Get started](xref:migration/fx-to-core/inc/start). - -## App migration to ASP.NET Core - -Before starting the migration, the app targets ASP.NET Framework and runs on Windows with its supporting libraries: - -![Before starting the migration](~/migration/fx-to-core/inc/overview/static/1.png) - -Migration starts by introducing a new app based on ASP.NET Core that becomes the entry point. Incoming requests go to the ASP.NET Core app, which either handles the request or proxies the request to the .NET Framework app via [YARP](https://dotnet.github.io/yarp/). At first, the majority of code providing responses is in the .NET Framework app, but the ASP.NET Core app is now set up to start migrating routes: - -![start updating routes](~/migration/fx-to-core/inc/overview/static/nop.png) - -To migrate business logic that relies on `HttpContext`, the libraries need to be built with `Microsoft.AspNetCore.SystemWebAdapters`. Building the libraries with `SystemWebAdapters` allows: - -* The libraries to be built against .NET Framework, .NET Core, or .NET Standard 2.0. -* Ensures that the libraries are using APIs that are available on both ASP.NET Framework and ASP.NET Core. - -![Microsoft.AspNetCore.SystemWebAdapters](~/migration/fx-to-core/inc/overview/static/sys_adapt.png) - -Once the ASP.NET Core app using YARP is set up, you can start updating routes from ASP.NET Framework to ASP.NET Core. For example, WebAPI or MVC controller action methods,handlers, or some other implementation of a route. If the route is available in the ASP.NET Core app, it's matched and served. - -During the migration process, additional services and infrastructure are identified that must be updated to run on .NET Core. Options listed in order of maintainability include: - -1. Move the code to shared libraries -1. Link the code in the new project -1. Duplicate the code - -Eventually, the ASP.NET Core app handles more of the routes than the .NET Framework app: - -![the ASP.NET Core app handles more of the routes](~/migration/fx-to-core/inc/overview/static/sys_adapt.png) - -Once the ASP.NET Framework app is no longer needed and deleted: - -* The app is running on the ASP.NET Core app stack, but is still using the adapters. -* The remaining migration work is removing the use of adapters. - -![final pic](~/migration/fx-to-core/inc/overview/static/final.png) - -The Visual Studio extension [.NET Upgrade Assistant](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.upgradeassistant) can help upgrade ASP.NET Framework web apps to ASP.NET Core. For more information see the blog post [Upgrading your .NET projects with Visual Studio](https://devblogs.microsoft.com/dotnet/upgrade-assistant-now-in-visual-studio/). - -## System.Web Adapters - -The `Microsoft.AspNetCore.SystemWebAdapters` namespace is a collection of runtime helpers that facilitate using code written against `System.Web` while moving to ASP.NET Core. There are a few packages that may be used to use features from these adapters: - -* `Microsoft.AspNetCore.SystemWebAdapters`: This package is used in supporting libraries and provide the System.Web APIs you may have taken a dependency on, such as `HttpContext` and others. This package targets .NET Standard 2.0, .NET Framework 4.5+, and .NET 5+. -* `Microsoft.AspNetCore.SystemWebAdapters.FrameworkServices`: This package only targets .NET Framework and is intended to provide services to ASP.NET Framework applications that may need to provide incremental migrations. This is generally not expected to be referenced from libraries, but rather from the applications themselves. -* `Microsoft.AspNetCore.SystemWebAdapters.CoreServices`: This package only targets .NET 6+ and is intended to provide services to ASP.NET Core applications to configure behavior of `System.Web` APIs as well as opting into any behaviors for incremental migration. This is generally not expected to be referenced from libraries, but rather from the applications themselves. -* `Microsoft.AspNetCore.SystemWebAdapters.Abstractions`: This package is a supporting package that provides abstractions for services used by both the ASP.NET Core and ASP.NET Framework application such as session state serialization. - -For examples of scenarios where this is useful, see [the adapters article](xref:migration/fx-to-core/systemweb-adapters). - -For guidance around usage, see the [usage guidance article](xref:migration/fx-to-core/inc/usage_guidance). - -## Additional Resources - -* [Example migration of eShop to ASP.NET Core](/dotnet/architecture/porting-existing-aspnet-apps/example-migration-eshop) -* [Video:Tooling for Incremental ASP.NET Core Migrations](https://www.youtube.com/watch?v=P96l0pDNVpM) -* diff --git a/aspnetcore/migration/fx-to-core/inc/remote-app-setup.md b/aspnetcore/migration/fx-to-core/inc/remote-app-setup.md index 3b051c4db461..aa21df3514d7 100644 --- a/aspnetcore/migration/fx-to-core/inc/remote-app-setup.md +++ b/aspnetcore/migration/fx-to-core/inc/remote-app-setup.md @@ -7,74 +7,342 @@ monikerRange: '>= aspnetcore-6.0' ms.date: 11/9/2022 ms.topic: article uid: migration/fx-to-core/inc/remote-app-setup +zone_pivot_groups: migration-remote-app-setup --- # Remote app setup +> [!IMPORTANT] +> Framework and Core applications must use identical virtual directory layouts. +> +> The virtual directory setup is used for route generation, authorization, and other services within the system. At this point, no reliable method has been found to enable different virtual directories due to how ASP.NET Framework works. + In some incremental upgrade scenarios, it's useful for the new ASP.NET Core app to be able to communicate with the original ASP.NET app. -Specifically, this capability is used, currently, for [remote app authentication](xref:migration/fx-to-core/inc/remote-authentication) and [remote session](xref:migration/fx-to-core/inc/remote-session) features. +Common scenarios this enables: + +* Fallback to the legacy application with [YARP](~/fundamentals/servers/yarp/yarp-overview.md) +* [Remote app authentication](xref:migration/fx-to-core/areas/authentication#remote-authenticationn) +* [Remote session](xref:migration/fx-to-core/areas/session#remote-app-session-state) + +:::zone pivot="manual" -## Configuration +## Configuration Values To enable the ASP.NET Core app to communicate with the ASP.NET app, it's necessary to make a couple small changes to each app. -### ASP.NET app configuration +You need to configure two configuration values in both applications: -To set up the ASP.NET app to be able to receive requests from the ASP.NET Core app: -1. Install the nuget package [`Microsoft.AspNetCore.SystemWebAdapters.FrameworkServices`](https://www.nuget.org/packages/Microsoft.AspNetCore.SystemWebAdapters) -2. Call the `AddRemoteAppServer` extension method on the `ISystemWebAdapterBuilder`: +* `RemoteAppApiKey`: A key (required to be parseable as a [GUID](/dotnet/api/system.guid)) that is shared between the two applications. This should be a GUID value like `12345678-1234-1234-1234-123456789012`. +* `RemoteAppUri`: The URI of the remote ASP.NET Framework application (only required in the ASP.NET Core application configuration). This should be the full URL where the ASP.NET Framework app is hosted, such as `https://localhost:44300` or `https://myapp.example.com`. -```CSharp -SystemWebAdapterConfiguration.AddSystemWebAdapters(this) - .AddRemoteAppServer(options => - { - // ApiKey is a string representing a GUID - options.ApiKey = ConfigurationManager.AppSettings["RemoteAppApiKey"]; - }); +## Configure ASP.NET Framework Application + +> [!IMPORTANT] +> The ASP.NET Framework application should be hosted with SSL enabled. In the remote app setup for incremental migration, it is not required to have direct access externally. It is recommended to only allow access from the client application via the proxy. + +For ASP.NET Framework applications, add these values to your `web.config` in the `` section: + +> [!IMPORTANT] +> ASP.NET Framework stores its appSettings in `web.config`. However, they can be retrieved from other sources (such as environment variables) with the use of [configuration Builders](/aspnet/config-builder). This makes sharing configuration values much easier between the local and remote applications in this setup. + +```xml + + + ``` -In the options configuration method passed to the `AddRemoteAppServer` call, an API key must be specified. The API key is: +To configure the application to be available to handle the requests from the ASP.NET Core client, set up the following: + +1. Install the NuGet package [`Microsoft.AspNetCore.SystemWebAdapters.FrameworkServices`](https://www.nuget.org/packages/Microsoft.AspNetCore.SystemWebAdapters) -* Used to secure the endpoint so that only trusted callers can make requests to it. -* The same API key provided to the ASP.NET Core app when it is configured. -* A string and must be parsable as a [GUID](/dotnet/api/system.guid). Hyphens in the key are optional. +1. Add the configuration code to the `Application_Start` method in your `Global.asax.cs` file: -3. **Optional :** Add the `SystemWebAdapterModule` module to the `web.config` if it wasn't already added by NuGet. The `SystemWebAdapterModule` module is not added automatically when using SDK style projects for ASP.NET Core. + ```CSharp + protected void Application_Start() + { + SystemWebAdapterConfiguration.AddSystemWebAdapters(this) + .AddRemoteAppServer(options => + { + // ApiKey is a string representing a GUID + options.ApiKey = ConfigurationManager.AppSettings["RemoteAppApiKey"]; + }); + + // ...existing code... + } + ``` + +1. Add the `SystemWebAdapterModule` module to the `web.config` if it wasn't already added by NuGet. This module configuration is required for IIS hosting scenarios. The `SystemWebAdapterModule` module is not added automatically when using SDK style projects for ASP.NET Core. -```diff - - -+ -+ - - + ```diff + + + + + + + + + ``` + +## Configure ASP.NET Core Application + +For ASP.NET Core applications, add these values to your `appsettings.json`: + +```json +{ + "RemoteAppApiKey": "...", + "RemoteAppUri": "https://localhost:44300" +} ``` -### ASP.NET Core app +To set up the ASP.NET Core app to be able to send requests to the ASP.NET app, configure the remote app client by calling `AddRemoteAppClient` after registering System.Web adapter services with `AddSystemWebAdapters`. -To set up the ASP.NET Core app to be able to send requests to the ASP.NET app, you need to make a similar change, calling `AddRemoteApp` after registering System.Web adapter services with `AddSystemWebAdapters`. +Add this configuration to your `Program.cs` file: ```CSharp builder.Services.AddSystemWebAdapters() .AddRemoteAppClient(options => { - options.RemoteAppUrl = new(builder.Configuration["ReverseProxy:Clusters:fallbackCluster:Destinations:fallbackApp:Address"]); + options.RemoteAppUrl = new(builder.Configuration["RemoteAppUri"]); options.ApiKey = builder.Configuration["RemoteAppApiKey"]; }); ``` -In the preceding code: +With both the ASP.NET and ASP.NET Core app updated, extension methods can now be used to set up [remote app authentication](xref:migration/fx-to-core/areas/authentication#remote-authenticationn) or [remote session](xref:migration/fx-to-core/areas/session#remote-app-session-state), as needed. + +### Enable proxying + +To enable proxying from the ASP.NET Core application to the ASP.NET Framework application, you can set up a fallback route that forwards unmatched requests to the legacy application. This allows for a gradual migration where the ASP.NET Core app handles migrated functionality while falling back to the original app for unmigrated features. + +1. Install the YARP (Yet Another Reverse Proxy) NuGet package following the [latest guidance](~/fundamentals/servers/yarp/getting-started.md). + +1. Add the required using statements to your `Program.cs`: + + ```csharp + using Microsoft.Extensions.Options; + using Microsoft.AspNetCore.SystemWebAdapters; + ``` + +1. Register the reverse proxy services in your `Program.cs`: + + ```csharp + builder.Services.AddReverseProxy(); + ``` + +1. After building the app and configuring other middleware, add the fallback route mapping: + + ```csharp + var app = builder.Build(); + + // Configure your other middleware here (authentication, routing, etc.) + + // Map the fallback route + app.MapForwarder("/{**catch-all}", app.ServiceProvider.GetRequiredService>().Value.RemoteAppUrl.OriginalString) + + // Ensures this route has the lowest priority (runs last) + .WithOrder(int.MaxValue) + + // Skips remaining middleware when this route matches + .ShortCircuit(); + + app.Run(); + ``` + +:::zone-end + +:::zone pivot="aspire" + +## Setup Aspire orchestration + +> [!IMPORTANT] +> This is still in preview and not available on NuGet.org, so you must configure your NuGet config to pull libraries from the .NET Libraries daily feed: +> +> ```xml +> +> +> +> +> +> +> +> +> +> ``` +> +> **NOTE**: This requires v2.0.1-preview1.25351.5 of the System.Web adapters or later. + +> [!WARNING] +> **NOTE**: This is a 3rd party component that helps run the application in Aspire. At the current time, ASP.NET Framework applications are not supported directly in Aspire, but this project helps with that. This dependency is intended for build and development, but does not need to be deployed into production. + +1. Add Aspire orchestration for the ASP.NET Framework application +1. Add a new ASP.NET Core application to the solution and add it to your Aspire orchestration +1. Update the AppHost to target Windows as IIS integration requires that: + ```diff + - net9.0 + + net9.0-windows + ``` +1. Add the following Aspire integrations to your app host: + * `Aspire.Hosting.IncrementalMigration` + * `C3D.Extensions.Aspire.IISExpress` +1. Configure IIS Express to locally host your framework application and configure incremental migration fallback: + + ```csharp + var builder = DistributedApplication.CreateBuilder(args); + + var frameworkApp = builder.AddIISExpress("iis") + .AddSiteProject("framework") + .WithDefaultIISExpressEndpoints() + .WithOtlpExporter() + .WithHttpHealthCheck(); + + var coreApp = builder.AddProject("core") + .WithHttpHealthCheck() + .WaitFor(frameworkApp) + .WithIncrementalMigrationFallback(frameworkApp, options => options.RemoteSession = RemoteSession.Enabled); + + builder.Build().Run(); + ``` +1. Configure the options of the incremental migration fallback for the scenarios you want to support. + +## Configure ServiceDefaults to support ASP.NET Framework + +1. Add the package `Aspire.Microsoft.AspNetCore.SystemWebAdapters` to your application. +1. Update the ServiceDefaults project to support .NET Framework. This is based off of the default ServiceDefaults and may different if you have customized anything. + * Update the target framework to multitarget: + ```diff + - net9.0 + + net9.0;net48 + ``` + * Update the PackageReferences to account for the different frameworks: + ```xml + + + + + + + + + + + + + + + + + + + + + ``` + * In the Extensions.cs file, you'll need to conditionally exclude the ServiceDiscovery APIs as those are currently not supported on .NET Framework: + ```diff + + #if NET + builder.Services.AddServiceDiscovery(); + + #endif + + builder.Services.ConfigureHttpClientDefaults(http => + { + // Turn on resilience by default + http.AddStandardResilienceHandler(); + + + #if NET + // Turn on service discovery by default + http.AddServiceDiscovery(); + + #endif + }); + ``` + * To enable telemetry, update the metrics and tracing registrations: + + ```diff + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics + + #if NET + .AddAspNetCoreInstrumentation() + + #else + + .AddAspNetInstrumentation() + + #endif + .AddSqlClientInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddSource(builder.Environment.ApplicationName) + + #if NET + .AddAspNetCoreInstrumentation() + + #else + + .AddAspNetInstrumentation() + + #endif + .AddSqlClientInstrumentation() + .AddHttpClientInstrumentation(); + }); + ``` + * Disable the default endpoints as that only applies for ASP.NET Core: + + ```diff + + #if NET + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Default endpoint registrations + } + + #endif + ``` + +## Configure ASP.NET Framework Application + +1. Reference the ServiceDefaults project +1. Add the configuration code to the `Application_Start` method in your `Global.asax.cs` file: + + ```CSharp + protected void Application_Start() + { + HttpApplicationHost.RegisterHost(builder => + { + builder.AddServiceDefaults(); + builder.AddSystemWebAdapters(); + }); + } + ``` + +## Configure ASP.NET Core Application + +1. Reference the ServiceDefaults project +1. Add the System.Web adapters in Programs.cs: + + ```diff + var builder = WebApplication.CreateBuilder(); + + builder.AddServiceDefaults(); + + builder.AddSystemWebAdapters(); + + ... + + var app = builder.Build(); + + ... + + + // Must be placed after routing if manually added + + app.UseSystemWebAdapters(); + + ... -* The `AddRemoteApp` call is used to configure the remote app's URL and the shared secret API key. -* The `RemoteAppUrl` property specifies the URL of the ASP.NET Framework app that the ASP.NET Core app communicates with. In this example, the URL is read from an existing configuration setting used by the YARP proxy that proxies requests to the ASP.NET Framework app as part of the incremental migration's *strangler fig pattern*. + + app.MapRemoteAppFallback() + + + + // Optional, but recommended unless middleware is needed + + .ShortCircuit(); -With both the ASP.NET and ASP.NET Core app updated, extension methods can now be used to set up [remote app authentication](xref:migration/fx-to-core/inc/remote-authentication) or [remote session](xref:migration/fx-to-core/inc/remote-session), as needed. + app.Run(); + ``` -## Securing the remote app connection +:::zone-end -Because remote app features involve serving requests on new endpoints from the ASP.NET app, it's important that communication to and from the ASP.NET app be secure. +With this configuration: -First, make sure that the API key string used to authenticate the ASP.NET Core app with the ASP.NET app is unique and kept secret. It is a best practice to not store the API key in source control. Instead, load it at runtime from a secure source such as Azure Key Vault or other secure runtime configuration. In order to encourage secure API keys, remote app connections require that the keys be non-empty GUIDs (128-bit hex numbers). +1. **Local routes take precedence**: If the ASP.NET Core application has a matching route, it will handle the request locally +2. **Fallback to legacy app**: Unmatched requests are automatically forwarded to the ASP.NET Framework application +3. **Middleware optimization**: The `.ShortCircuit()` method prevents unnecessary middleware execution when forwarding requests -Second, because it's important for the ASP.NET Core app to be able to trust that it is requesting information from the correct ASP.NET app, the ASP.NET app should use HTTPS in any production scenarios so that the ASP.NET Core app can know responses are being served by a trusted source. +This setup enables a seamless user experience during incremental migration, where users can access both migrated and legacy functionality through a single endpoint. diff --git a/aspnetcore/migration/fx-to-core/inc/remote-authentication.md b/aspnetcore/migration/fx-to-core/inc/remote-authentication.md deleted file mode 100644 index b5a095e57d72..000000000000 --- a/aspnetcore/migration/fx-to-core/inc/remote-authentication.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -title: Remote Authentication -description: Remote Authentication -author: rick-anderson -ms.author: riande -monikerRange: '>= aspnetcore-6.0' -ms.date: 11/9/2022 -ms.topic: article -uid: migration/fx-to-core/inc/remote-authentication ---- - -# Remote Authentication - -The System.Web adapters' remote authentication feature allows an ASP.NET Core app to determine a user's identity (authenticate an HTTP request) by deferring to an ASP.NET app. Enabling the feature adds an endpoint to the ASP.NET app that returns a serialized representing the authenticated user for any requests made to the endpoint. The ASP.NET Core app, then, registers a custom authentication handler that will (for endpoints with remote authentication enabled) determine a user's identity by calling that endpoint on the ASP.NET app and passing selected headers and cookies from the original request received by the ASP.NET Core app. - -## Configuration - -There are just a few small code changes needed to enable remote authentication in a solution that's already set up according to the [Getting Started](xref:migration/fx-to-core/inc/overview). - -First, follow the [remote app setup](xref:migration/fx-to-core/inc/remote-app-setup) instructions to connect the ASP.NET Core and ASP.NET apps. Then, there are just a couple extra extension methods to call to enable remote app authentication. - -### ASP.NET app configuration - -The ASP.NET app needs to be configured to add the authentication endpoint. Adding the authentication endpoint is done by calling the `AddAuthenticationServer` extension method to set up the HTTP module that watches for requests to the authentication endpoint. Note that remote authentication scenarios typically want to add proxy support as well, so that any authentication related redirects correctly route to the ASP.NET Core app rather than the ASP.NET one. - -:::code language="csharp" source="~/migration/fx-to-core/inc/samples/remote-authentication/AspNetApp.cs" id="snippet_SystemWebAdapterConfiguration" ::: - -### ASP.NET Core app configuration - -Next, the ASP.NET Core app needs to be configured to enable the authentication handler that will authenticate users by making an HTTP request to the ASP.NET app. Again, this is done by calling `AddAuthenticationClient` when registering System.Web adapters services: - -:::code language="csharp" source="~/migration/fx-to-core/inc/samples/remote-authentication/AspNetCore.cs" id="snippet_AddSystemWebAdapters" highlight="8" ::: - -The boolean that is passed to the `AddAuthenticationClient` call specifies whether remote app authentication should be the default authentication scheme. Passing `true` will cause the user to be authenticated via remote app authentication for all requests, whereas passing `false` means that the user will only be authenticated with remote app authentication if the remote app scheme is specifically requested (with `[Authorize(AuthenticationSchemes = RemoteAppAuthenticationDefaults.AuthenticationScheme)]` on a controller or action method, for example). Passing false for this parameter has the advantage of only making HTTP requests to the original ASP.NET app for authentication for endpoints that require remote app authentication but has the disadvantage of requiring annotating all such endpoints to indicate that they will use remote app auth. - -In addition to the require boolean, an optional callback may be passed to `AddAuthenticationClient` to modify some other aspects of the remote authentication process's behavior: - -* `RequestHeadersToForward`: This property contains headers that should be forwarded from a request when calling the authenticate API. By default, the only headers forwarded are `Authorization` and `Cookie`. Additional headers can be forwarded by adding them to this list. Alternatively, if the list is cleared (so that no headers are specified), then all headers will be forwarded. -* `ResponseHeadersToForward`: This property lists response headers that should be propagated back from the authenticate request to the original call that prompted authentication in scenarios where identity is challenged. By default, this includes `Location`, `Set-Cookie`, and `WWW-Authenticate` headers. -* `AuthenticationEndpointPath`: The endpoint on the ASP.NET app where authenticate requests should be made. This defaults to `/systemweb-adapters/authenticate` and must match the endpoint specified in the ASP.NET authentication endpoint configuration. - -Finally, if the ASP.NET Core app didn't previously include authentication middleware, that will need to be enabled (after routing middleware, but before authorization middleware): - -:::code language="csharp" source="~/migration/fx-to-core/inc/samples/remote-authentication/AspNetCore.cs" id="snippet_UseAuthentication" ::: - -## Design - -1. When requests are processed by the ASP.NET Core app, if remote app authentication is the default scheme or specified by the request's endpoint, the `RemoteAuthenticationAuthHandler` will attempt to authenticate the user. - 1. The handler will make an HTTP request to the ASP.NET app's authenticate endpoint. It will copy configured headers from the current request onto this new one in order to forward auth-relevant data. As mentioned above, default behavior is to copy the `Authorize` and `Cookie` headers. The API key header is also added for security purposes. -1. The ASP.NET app will serve requests sent to the authenticate endpoint. As long as the API keys match, the ASP.NET app will return either the current user's serialized into the response body or it will return an HTTP status code (like 401 or 302) and response headers indicating failure. -1. When the ASP.NET Core app's `RemoteAuthenticationAuthHandler` receives the response from the ASP.NET app: - 1. If a ClaimsPrincipal was successfully returned, the auth handler will deserialize it and use it as the current user's identity. - 1. If a ClaimsPrincipal was not successfully returned, the handler will store the result and if authentication is challenged (because the user is accessing a protected resource, for example), the request's response will be updated with the status code and selected response headers from the response from the authenticate endpoint. This enables challenge responses (like redirects to a login page) to be propagated to end users. - 1. Because results from the ASP.NET app's authenticate endpoint may include data specific to that endpoint, users can register `IRemoteAuthenticationResultProcessor` implementations with the ASP.NET Core app which will run on any authentication results before they are used. As an example, the one built-in `IRemoteAuthenticationResultProcessor` is `RedirectUrlProcessor` which looks for `Location` response headers returned from the authenticate endpoint and ensures that they redirect back to the host of the ASP.NET Core app and not the ASP.NET app directly. - -## Known limitations - -This remote authentication approach has a couple known limitations: - -1. Because Windows authentication depends on a handle to a Windows identity, Windows authentication is not supported by this feature. Future work is planned to explore how shared Windows authentication might work. See [dotnet/systemweb-adapters#246](https://github.com/dotnet/systemweb-adapters/issues/246) for more information. -1. This feature allows the ASP.NET Core app to make use of an identity authenticated by the ASP.NET app, but all actions related to users (logging on, logging off, etc.) still need to be routed through the ASP.NET app. - -## Alternatives - -If authentication in the ASP.NET app is done using `Microsoft.Owin` Cookie Authentication Middleware, an alternative solution to sharing identity is to configure the ASP.NET and ASP.NET Core apps so that they are able to share an authentication cookie. Sharing an authentication cookie enables: - -* Both apps to determine the user identity from the same cookie. -* Signing in or out of one app signs the user in or out of the other app. - -Note that because signing in typically depends on a specific database, not all authentication functionality will work in both apps: - -* Users should sign in through only one of the apps, either the ASP.NET or ASP.NET Core app, whichever the database is setup to work with. -* Both apps are able to see the users' identity and claims. -* Both apps are able to sign the user out. - -Details on how to configure sharing auth cookies between ASP.NET and ASP.NET Core apps are available in [cookie sharing documentation](xref:security/cookie-sharing). The following samples in the [System.Web adapters](https://github.com/dotnet/systemweb-adapters) GitHub repo demonstrates remote app authentication with shared cookie configuration enabling both apps to sign users in and out : - -* [ASP.NET app](https://github.com/dotnet/systemweb-adapters/tree/main/samples/RemoteAuth/Identity/MvcApp) -* [ASP.NET Core app](https://github.com/dotnet/systemweb-adapters/tree/main/samples/RemoteAuth/Identity/MvcCoreApp) - -Sharing authentication is a good option if both the following are true: - -* The ASP.NET app is already using `Microsoft.Owin` cookie authentication. -* It's possible to update the ASP.NET app and ASP.NET Core apps to use matching data protection settings. Matching shared data protection settings includes a shared file path, Redis cache, or Azure Blob Storage for storing data protection keys. - -For other scenarios, the remote authentication approach described previously in this doc is more flexible and is probably a better fit. diff --git a/aspnetcore/migration/fx-to-core/inc/remote-session.md b/aspnetcore/migration/fx-to-core/inc/remote-session.md deleted file mode 100644 index da7aa64f6438..000000000000 --- a/aspnetcore/migration/fx-to-core/inc/remote-session.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -title: Remote app session state -description: Remote app session state -author: rick-anderson -ms.author: riande -monikerRange: '>= aspnetcore-6.0' -ms.date: 11/9/2022 -ms.topic: article -uid: migration/fx-to-core/inc/remote-session ---- - -# Remote app session state - -Remote app session state will enable communication between the ASP.NET Core and ASP.NET app 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. - -## HttpSessionState serialization - -The object must be serialized for remote app session state to be enabled. This is accomplished through implementation of the type `Microsoft.AspNetCore.SystemWebAdapters.SessionState.Serialization.ISessionSerializer`, of which a default binary writer implementation is provided. This is added by the following code: - -:::code language="csharp" source="~/migration/fx-to-core/inc/samples/remote-session/Program.cs" id="snippet_Serialization" ::: - -## Configuration - -First, follow the [remote app setup](xref:migration/fx-to-core/inc/remote-app-setup) instructions to connect the ASP.NET Core and ASP.NET apps. Then, there are just a couple extra extension methods to call to enable remote app session state. - -Configuration for ASP.NET Core involves calling `AddRemoteAppSession` and `AddJsonSessionSerializer` to register known session item types. The code should look similar to the following: - -:::code language="csharp" source="~/migration/fx-to-core/inc/samples/remote-session/Program.cs" id="snippet_Configuration" ::: - -Session support requires additional work for the ASP.NET Core pipeline, and is not turned on by default. It can be configured on a per-route basis via ASP.NET Core metadata. - -For example, session support requires either to annotate a controller: - -:::code language="csharp" source="~/migration/fx-to-core/inc/samples/remote-session/SomeController.cs" id="snippet_Controller" ::: - -or to enable for all endpoints by default: - -:::code language="csharp" source="~/migration/fx-to-core/inc/samples/remote-session/Program.cs" id="snippet_RequireSystemWebAdapterSession" ::: - -The framework equivalent would look like the following change in `Global.asax.cs`: - -:::code language="csharp" source="~/migration/fx-to-core/inc/samples/remote-session/Global.asax.cs"::: - -## 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 session will retrieve the session state from the framework app](~/migration/fx-to-core/inc/overview/static/readonly_session.png) - -## Writeable - -Writeable session state protocol starts with 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 session state protocol starts with the same as the readonly](~/migration/fx-to-core/inc/overview/static/writesession.png) diff --git a/aspnetcore/migration/fx-to-core/inc/samples/remote-session/Global.asax.cs b/aspnetcore/migration/fx-to-core/inc/samples/remote-session/Global.asax.cs deleted file mode 100644 index 5f12acd75eb4..000000000000 --- a/aspnetcore/migration/fx-to-core/inc/samples/remote-session/Global.asax.cs +++ /dev/null @@ -1,11 +0,0 @@ -SystemWebAdapterConfiguration.AddSystemWebAdapters(this) - .AddJsonSessionSerializer(options => - { - // Serialization/deserialization requires each session key to be registered to a type - options.RegisterKey("test-value"); - options.RegisterKey("SampleSessionItem"); - }) - // Provide a strong API key that will be used to authenticate the request on the remote app for querying the session - // ApiKey is a string representing a GUID - .AddRemoteAppServer(options => options.ApiKey = ConfigurationManager.AppSettings["RemoteAppApiKey"]) - .AddSessionServer(); \ No newline at end of file diff --git a/aspnetcore/migration/fx-to-core/inc/session.md b/aspnetcore/migration/fx-to-core/inc/session.md deleted file mode 100644 index 4f1ea3107f94..000000000000 --- a/aspnetcore/migration/fx-to-core/inc/session.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -title: ASP.NET to ASP.NET Core incremental session state migration -description: ASP.NET to ASP.NET Core incremental session state migration -author: rick-anderson -ms.author: riande -monikerRange: '>= aspnetcore-6.0' -ms.date: 11/9/2022 -ms.topic: article -uid: migration/fx-to-core/inc/session ---- - -# ASP.NET to ASP.NET Core incremental session state migration - -## Session State - -Session state in ASP.NET Framework provided a number of features that ASP.NET Core does not provide. In order to update from ASP.NET Framework to Core, the adapters provide mechanisms to enable populating session state with similar behavior as `System.Web` did. Some of the differences between framework and core are: - -* ASP.NET Framework would lock session usage within a session, so subsequent requests in a session are handled in a serial fashion. This is different than ASP.NET Core that does not provide any of these guarantees. -* ASP.NET Framework would serialize and deserialize objects automatically (unless being done in-memory). ASP.NET Core provides a mechanism to store a `byte[]` given a key. Any object serialization/deserialization has to be done manually by the user. - -The adapter infrastructure exposes two interfaces that can be used to implement any session storage system. These are: - -* `Microsoft.AspNetCore.SystemWebAdapters.ISessionManager`: This has a single method that gets passed an and the session metadata and expects an `ISessionState` object to be returned. -* `Microsoft.AspNetCore.SystemWebAdapters.ISessionState`: This describes the state of a session object. It is used as the backing of the type. - -## Serialization -Since the adapters provide the ability to work with strongly-typed session state, we must be able to serialize and deserialize types. This is customized through the `Microsoft.AspNetCore.SystemWebAdapters.SessionState.Serialization.ISessionKeySerializer`. - -A default JSON implementation is provided that is configured via the `JsonSessionSerializerOptions`: - -* `RegisterKey(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. - - -:::code language="csharp" source="~/migration/fx-to-core/inc/samples/session/Program.cs" id="snippet_Serialization" ::: - -## Implementations - -There are two available implementations of the session state object that currently ship, each with some trade offs of features. The best choice for an app may depend on which part of the migration it is in, and may change over time. - -* Strongly typed: Provides the ability to access an object and can be cast to the expected type -* Locking: Ensures multiple requests within a single session are queued up and aren't accessing the session at the same time -* Standalone: Use when you're not sharing session between ASP.NET Framework and ASP.NET Core to avoid modifying code in class libraries that references SessionState - -Below are the available implementations: - -| Implementation | Strongly typed | Locking | Standalone | -|-------------------------------------------------------------|----------------|---------|------------| -| [Remote app](xref:migration/fx-to-core/inc/remote-session) | ✔️ | ✔️ | ⛔ | -| [Wrapped ASP.NET Core](xref:migration/fx-to-core/inc/wrapped) | ✔️ | ⛔ | ✔️ | diff --git a/aspnetcore/migration/fx-to-core/inc/start.md b/aspnetcore/migration/fx-to-core/inc/start.md deleted file mode 100644 index 9e91f4ba7bab..000000000000 --- a/aspnetcore/migration/fx-to-core/inc/start.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -title: Get started with incremental ASP.NET to ASP.NET Core migration -description: Get started with incremental ASP.NET to ASP.NET Core migration -author: rick-anderson -ms.author: riande -monikerRange: '>= aspnetcore-6.0' -ms.date: 11/9/2022 -ms.topic: article -uid: migration/fx-to-core/inc/start ---- - -# Get started with incremental ASP.NET to ASP.NET Core migration - -For a large migration, we recommend setting up a ASP.NET Core app that proxies to the original .NET Framework app. The new proxy enabled app is shown in the following image: - -![start migrating routes](~/migration/fx-to-core/inc/overview/static/nop.png) - -To understand how this approach is helpful in the migration process, see [Incremental ASP.NET to ASP.NET Core migration](xref:migration/fx-to-core/inc/overview). The rest of this article provides the steps to proceed with an incremental migration. - -## Set up ASP.NET Core Project - -See for help in setting up projects required for incremental migration. - -## Upgrade supporting libraries - -If you have supporting libraries in your solution that you will need to use, they should be upgraded to .NET Standard 2.0, if possible. [Upgrade Assistant](https://github.com/dotnet/upgrade-assistant) is a great tool for this. If libraries are unable to target .NET Standard, you can target .NET 6 or later either along with the .NET Framework target in the original project or in a new project alongside the original. - -The [adapters](xref:migration/fx-to-core/systemweb-adapters) can be used in these libraries to enable support for `System.Web.HttpContext` usage in class libraries. In order to enable `System.Web.HttpContext` usage in a library: - -1. Remove reference to `System.Web` in the project file -1. Add the `Microsoft.AspNetCore.SystemWebAdapters` package -1. Enable multi-targeting and add a .NET 6 target or later, or convert the project to .NET Standard 2.0. -1. Ensure the target framework supports .NET Core. Multi-targeting can be used if .NET Standard 2.0 is not sufficient - -This step may require a number of projects to change depending on your solution structure. Upgrade Assistant can help you identify which ones need to change and automate a number of steps in the process. - -## Enable Session Support - -Session is a commonly used feature of ASP.NET that shares the name with a feature in ASP.NET Core the APIs are much different. See the documentation on [session support](xref:migration/fx-to-core/inc/session). - -## Enable shared authentication support - -It is possible to share authentication between the original ASP.NET app and the new ASP.NET Core app by using the `System.Web` adapters remote authentication feature. This feature allows the ASP.NET Core app to defer authentication to the ASP.NET app. See the [remote app connection](xref:migration/fx-to-core/inc/remote-app-setup) and [remote authentication](xref:migration/fx-to-core/inc/remote-authentication) docs for more details. - -## General Usage Guidance - -There are a number of differences between ASP.NET and ASP.NET Core that the adapters are able to help update. However, there are some features that require an opt-in as they incur some cost. There are also behaviors that cannot be adapted. See [usage guidance](xref:migration/fx-to-core/inc/usage_guidance) for a list of these. diff --git a/aspnetcore/migration/fx-to-core/inc/systemweb-adapters.md b/aspnetcore/migration/fx-to-core/inc/systemweb-adapters.md index ab45898f5937..b3df7ed47a06 100644 --- a/aspnetcore/migration/fx-to-core/inc/systemweb-adapters.md +++ b/aspnetcore/migration/fx-to-core/inc/systemweb-adapters.md @@ -19,11 +19,27 @@ Let's take a look at an example using the adapters moving from .NET Framework to ## Packages -* `Microsoft.AspNetCore.SystemWebAdapters`: This package is used in supporting libraries and provide the System.Web APIs you may have taken a dependency on, such as `HttpContext` and others. This package targets .NET Standard 2.0, .NET Framework 4.5+, and .NET 5+. +* `Microsoft.AspNetCore.SystemWebAdapters`: This package is used in supporting libraries and provides the System.Web APIs you may have taken a dependency on, such as `HttpContext` and others. This package targets .NET Standard 2.0, .NET Framework 4.5+, and .NET 5+. * `Microsoft.AspNetCore.SystemWebAdapters.FrameworkServices`: This package only targets .NET Framework and is intended to provide services to ASP.NET Framework applications that may need to provide incremental migrations. This is generally not expected to be referenced from libraries, but rather from the applications themselves. * `Microsoft.AspNetCore.SystemWebAdapters.CoreServices`: This package only targets .NET 6+ and is intended to provide services to ASP.NET Core applications to configure behavior of `System.Web` APIs as well as opting into any behaviors for incremental migration. This is generally not expected to be referenced from libraries, but rather from the applications themselves. * `Microsoft.AspNetCore.SystemWebAdapters.Abstractions`: This package is a supporting package that provides abstractions for services used by both the ASP.NET Core and ASP.NET Framework application such as session state serialization. +### Converting to System.Web.HttpContext + +To convert between the two representations of HttpContext, you may do the following: + +For to : + +* Implicit casting +* `HttpContext.AsSystemWeb()` + +For to + +* Implicit casting +* `HttpContext.AsAspNetCore()` + +Both of these methods will use a cached representation for the duration of a request. This allows for targeted rewrites to as needed. + ## Example ### ASP.NET Framework @@ -73,6 +89,12 @@ public class SomeController : Controller Notice that since there's a property, they can pass that through, but it generally looks the same. Using implicit conversions, the can be converted into the adapter that could then be passed around through the levels utilizing the code in the same way. -## See also +## Unit Testing + +When unit testing code that uses the System.Web adapters, there are some special considerations to keep in mind. + +In most cases, there's no need to set up additional components for running tests. But if the component being tested uses , it might be necessary to start up the `SystemWebAdapters` service, as shown in the following example: + +:::code language="csharp" source="~/migration/fx-to-core/inc/samples/unit-testing/Program.cs" id="snippet_UnitTestingFixture" ::: -* +The tests must be executed in sequence, not in parallel. The preceding example illustrates how to achieve this by setting [XUnit's `DisableParallelization` option](https://xunit.net/docs/running-tests-in-parallel#parallelism-in-test-frameworks) to `true`. This setting disables parallel execution for a specific test collection, ensuring that the tests within that collection run one after the other, without concurrency. diff --git a/aspnetcore/migration/fx-to-core/inc/unit-testing.md b/aspnetcore/migration/fx-to-core/inc/unit-testing.md deleted file mode 100644 index b4bf29e1730f..000000000000 --- a/aspnetcore/migration/fx-to-core/inc/unit-testing.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -title: Unit Testing -description: Unit Testing -author: afshinm -ms.author: amehrabani -monikerRange: '>= aspnetcore-6.0' -ms.date: 10/03/2023 -ms.topic: article -uid: migration/fx-to-core/inc/unit-testing ---- - -# Unit Testing - -In most cases, there's no need to set up additional components for running tests. But if the component being tested uses , it might be necessary to start up the `SystemWebAdapters` service, as shown in the following example: - -:::code language="csharp" source="~/migration/fx-to-core/inc/samples/unit-testing/Program.cs" id="snippet_UnitTestingFixture" ::: - -The tests must be executed in sequence, not in parallel. The preceding example illustrates how to achieve this by setting [XUnit's `DisableParallelization` option](https://xunit.net/docs/running-tests-in-parallel#parallelism-in-test-frameworks) to `true`. This setting disables parallel execution for a specific test collection, ensuring that the tests within that collection run one after the other, without concurrency. diff --git a/aspnetcore/migration/fx-to-core/inc/usage_guidance.md b/aspnetcore/migration/fx-to-core/inc/usage_guidance.md deleted file mode 100644 index 84401431e1ec..000000000000 --- a/aspnetcore/migration/fx-to-core/inc/usage_guidance.md +++ /dev/null @@ -1,112 +0,0 @@ ---- -title: Incremental ASP.NET to ASP.NET Core Migration Usage Guidance -description: Incremental ASP.NET to ASP.NET Core Migration Usage Guidance -author: rick-anderson -ms.author: riande -monikerRange: '>= aspnetcore-6.0' -ms.date: 11/9/2022 -ms.topic: article -uid: migration/fx-to-core/inc/usage_guidance ---- - -# Usage Guidance - -`Microsoft.AspNetCore.SystemWebAdapters` provides an emulation layer to mimic behavior from ASP.NET framework on ASP.NET Core. Below are some guidelines for some of the considerations when using them: - -## `HttpContext` lifetime - -The adapters are backed by which cannot be used past the lifetime of a request. Thus, when run on ASP.NET Core cannot be used past a request as well, while on ASP.NET Framework it would work at times. An will be thrown in cases where it is used past a request end. - -**Recommendation**: Store the values needed into a POCO and hold onto that. - -## Conversion to - -There are two ways to convert an to a : - -* Implicit casting -* Constructor usage - -**Recommendation**: For the most cases, implicit casting should be preferred as this will cache the created instance and ensure only a single per request. - -## is not set by default - -In ASP.NET Framework, was set for a request, but this is not done automatically in ASP.NET Core. Instead, you must add the appropriate middleware to your pipeline. - -**Recommendation**: See [ASP.NET Core Localization](/aspnet/core/fundamentals/localization#localization-middleware) for details on how to enable this. - -Simplest way to enable this with similar behavior as ASP.NET Framework would be to add the following to your pipeline: - -```csharp -app.UseRequestLocalization(); -``` - -## - -In ASP.NET Framework, and would be set to the current user. This is not available on ASP.NET Core out of the box. Support for this is available with these adapters by adding the `ISetThreadCurrentPrincipal` to the endpoint (available to controllers via the `SetThreadCurrentPrincipalAttribute`). However, it should only be used if the code cannot be refactored to remove usage. - -**Recommendation**: If possible, use the property or instead by passing it through to the call site. If not possible, enable setting the current user and also consider setting the request to be a logical single thread (see below for details). - -## Request thread does not exist in ASP.NET Core - -In ASP.NET Framework, a request had thread-affinity and would only be available if on that thread. ASP.NET Core does not have this guarantee so will be available within the same async context, but no guarantees about threads are made. - -**Recommendation**: If reading/writing to the , you must ensure you are doing so in a single-threaded way. You can force a request to never run concurrently on any async context by setting the `ISingleThreadedRequestMetadata`. This will have performance implications and should only be used if you can't refactor usage to ensure non-concurrent access. There is an implementation available to add to controllers with `SingleThreadedRequestAttribute`: - -```csharp -[SingleThreadedRequest] -public class SomeController : Controller -{ - ... -} -``` - -## may need to be prebuffered - -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). - -**Recommendation**: 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.MapDefaultControllerRoute() - .PreBufferRequestStream(); -``` - -## may require buffering - -Some APIs on require that the output stream is buffered, such as , , , and . - -**Recommendation**: In order to support behavior for that requires buffering the response before sending, endpoints must opt-into it with endpoint metadata implementing `IBufferResponseStreamMetadata`. - -To enable this on all MVC endpoints, there is an extension method that can be used as follows: - -```cs -app.MapDefaultControllerRoute() - .BufferResponseStream(); -``` - -## Shared session state - -In order to support , endpoints must opt-into it via metadata implementing `ISessionMetadata`. - -**Recommendation**: To enable this on all MVC endpoints, there is an extension method that can be used as follows: - -```cs -app.MapDefaultControllerRoute() - .RequireSystemWebAdapterSession(); -``` - -This also requires some implementation of a session store. For details of options here, see [here](xref:migration/fx-to-core/inc/session). - -## Remote session exposes additional endpoint for application - -The [remote session support](xref:migration/fx-to-core/inc/remote-session) exposes an endpoint that allows the core app to retrieve session information. This may cause a potentially long-lived request to exist between the core app and the framework app, but will time out with the current request or the session timeout (by default is 20 minutes). - -**Recommendation**: Ensure the API key used is a strong one and that the connection with the framework app is done over SSL. - -## Virtual directories must be identical for framework and core applications - -The virtual directory setup is used for route generation, authorization, and other services within the system. At this point, no reliable method has been found to enable different virtual directories due to how ASP.NET Framework works. - -**Recommendation**: Ensure your two applications are on different sites (hosts and/or ports) with the same application/virtual directory layout. diff --git a/aspnetcore/migration/fx-to-core/inc/wrapped.md b/aspnetcore/migration/fx-to-core/inc/wrapped.md deleted file mode 100644 index 68c75cad73f2..000000000000 --- a/aspnetcore/migration/fx-to-core/inc/wrapped.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: Wrapped ASP.NET Core session state -description: Wrapped ASP.NET Core session state -author: rick-anderson -ms.author: riande -monikerRange: '>= aspnetcore-6.0' -ms.date: 2/24/2025 -ms.topic: article -uid: migration/fx-to-core/inc/wrapped ---- - -# Wrapped ASP.NET Core session state - -The [AddWrappedAspNetCoreSession](https://github.com/dotnet/systemweb-adapters/blob/main/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/SessionState/Wrapped/WrappedSessionExtensions.cs) implementation wraps the session provided on ASP.NET Core so that it can be used with the adapters. The session uses the same backing store as [`Microsoft.AspNetCore.Http.ISession`](/dotnet/api/microsoft.aspnetcore.http.isession) but provides strongly-typed access to its members. - -Configuration for ASP.NET Core looks similar to the following: - -:::code language="csharp" source="~/migration/fx-to-core/inc/samples/wrapped/Program.cs" id="snippet_WrapAspNetCoreSession" ::: - -The framework app doesn't need any changes to enable this behavior. - -For more information, see the [AddWrappedAspNetCoreSession sample app](https://github.com/dotnet/systemweb-adapters/blob/main/samples/CoreApp/Program.cs) diff --git a/aspnetcore/migration/fx-to-core/includes/uses-systemweb-adapters.md b/aspnetcore/migration/fx-to-core/includes/uses-systemweb-adapters.md new file mode 100644 index 000000000000..323fd406f026 --- /dev/null +++ b/aspnetcore/migration/fx-to-core/includes/uses-systemweb-adapters.md @@ -0,0 +1,2 @@ +> [!NOTE] +> This makes use of the [System.Web Adapters](~/migration/fx-to-core/inc/systemweb-adapters.md) to simplify migration. diff --git a/aspnetcore/migration/fx-to-core/index.md b/aspnetcore/migration/fx-to-core/index.md index 20a00d4fd904..bb0d7e4d5562 100644 --- a/aspnetcore/migration/fx-to-core/index.md +++ b/aspnetcore/migration/fx-to-core/index.md @@ -8,41 +8,84 @@ uid: migration/fx-to-core/index --- # Migrate from ASP.NET Framework to ASP.NET Core -:::moniker range=">= aspnetcore-6.0" + -## Start here: Choose your migration path +Updating an app from ASP.NET Framework to ASP.NET Core is non-trivial for the majority of production apps. These apps often incorporate new technologies as they become available and are often composed of many legacy decisions. This guide provides practical approaches and tools for updating ASP.NET Framework apps to ASP.NET Core with as little change as possible. -Your ASP.NET Framework application can successfully move to ASP.NET Core. The key is choosing the right approach for your specific situation. +## Why migration is challenging -### Quick decision guide +Migrating from ASP.NET Framework to ASP.NET Core involves several complex challenges that make a complete rewrite difficult and risky for most production applications: + +### Technical Debt Accumulation + +Production applications often have accumulated technical debt over years of development: + +* **System.Web dependencies** - The pervasive use of and associated types throughout a code base. +* **Outdated package dependencies** that may not have .NET Core equivalents +* **Legacy build tools and project configurations** that aren't compatible with modern .NET +* **Deprecated API usage** that needs to be replaced with modern alternatives +* **Compiler warnings and code quality issues** that complicate migration + +### Cross-Cutting Concerns + +Many applications have cross-cutting concerns that span multiple layers and need careful coordination during migration: + +* **Session state management** - ASP.NET Framework and ASP.NET Core have fundamentally different session APIs and behaviors +* **Authentication and authorization** - Different authentication models and APIs between frameworks +* **Logging and monitoring** - Need to maintain consistent logging across both applications during migration +* **Caching strategies** - In-memory, distributed, or output caching needs to be maintained consistently +* **Error handling** - Establishing consistent error handling patterns across both applications +* **Configuration management** - Managing settings that need to be shared or synchronized between applications +* **Dependency injection** - Migrating from various DI containers to ASP.NET Core's built-in container + +### Library Dependency Chains -**For most production applications:** Use the [**incremental migration approach**](xref:migration/fx-to-core/inc/overview) - it's safer, faster to start, and keeps your app running in production throughout the process. +Supporting libraries often have complex dependency relationships that require careful upgrade ordering: -**For smaller applications or greenfield rewrites:** Consider the complete migration approach using our specialized guides. +* **Dependency tree complexity** - Libraries must be upgraded in postorder depth-first search ordering +* **Multi-targeting requirements** - Libraries need to support both .NET Framework and .NET Core/.NET Standard +* **API compatibility** - Ensuring libraries work with both framework versions during the migration period +* **Testing complexity** - Each library upgrade requires thorough testing to ensure compatibility -## The incremental approach: Best for most teams +### Application Architecture Differences + +The fundamental differences between ASP.NET Framework and ASP.NET Core create additional challenges: + +* **Hosting models** - Different approaches to application hosting and lifecycle management +* **Middleware pipeline** - Moving from HTTP modules and handlers to middleware +* **Request processing** - Different request processing models and contexts +* **Performance characteristics** - Different memory usage patterns and performance profiles + +These challenges make incremental migration the preferred approach for most production applications, as it allows teams to address these issues gradually while maintaining a working application in production. + +For documentation around important areas that have changed, see the associated topics available at + +## Start here: Choose your migration path + +Your ASP.NET Framework application can successfully move to ASP.NET Core. The key is choosing the right approach for your specific situation. + +### Quick decision guide -Most non-trivial ASP.NET Framework applications should use incremental migration. This approach: +**Answer these questions to choose your approach:** -- **Keeps your current app running** while you migrate piece by piece -- **Reduces risk** by moving functionality gradually -- **Delivers value faster** with immediate deployment of migrated components -- **Uses proven tools** like YARP proxy and System.Web adapters +1. **What's your timeline and risk tolerance?** + * Need to stay in production during migration → [Incremental migration](#incremental-migration) + * Can afford a complete rewrite → [In place migration](#in-place-migration) -**→ [Start your incremental migration](xref:migration/fx-to-core/inc/overview)** +2. **How large is your application?** + * Small to medium apps → [In place migration](#in-place-migration) + * Large production apps → [Incremental migration](#incremental-migration) is safer -## Migration tools and resources +3. **Do you have complex dependencies?** + * Unknown or out of date dependencies → [Incremental migration](#incremental-migration) + * Heavy use of System.Web → [Incremental migration](#incremental-migration) + * Minimal dependencies → [In place migration](#in-place-migration) -### Automated assistance -- **[.NET Upgrade Assistant](https://dotnet.microsoft.com/platform/upgrade-assistant)** - Command-line tool for initial project conversion -- **[Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.upgradeassistant)** - GUI-based upgrade assistance -### Comprehensive guides -- **[Porting ASP.NET Apps eBook](https://aka.ms/aspnet-porting-ebook)** - Complete reference guide -- **[eShop Migration Example](/dotnet/architecture/porting-existing-aspnet-apps/example-migration-eshop)** - Real-world case study +## Incremental Migration -## Changes to technology areas +Incremental migration is an implementation of the Strangler Fig pattern and is best for larger projects or projects that need to continue to stay in production throughout a migration. See to get started migration an application incrementally. -Before you begin, review the [technical differences between ASP.NET Framework and ASP.NET Core](xref:migration/fx-to-core/areas) to understand key changes that may affect your migration. +## In place migration -:::moniker-end \ No newline at end of file +In place migration can work for sufficiently small applications. If possible, this allows for a quick replacement of the application. However, small issues may be compounded if you decide to do an in place migration. See for how Upgrade Assistant can help with an in place migration. diff --git a/aspnetcore/migration/fx-to-core/start.md b/aspnetcore/migration/fx-to-core/start.md new file mode 100644 index 000000000000..34361758c82f --- /dev/null +++ b/aspnetcore/migration/fx-to-core/start.md @@ -0,0 +1,169 @@ +--- +title: Get started with incremental ASP.NET to ASP.NET Core migration +description: Get started with incremental ASP.NET to ASP.NET Core migration +author: rick-anderson +ms.author: riande +monikerRange: '>= aspnetcore-6.0' +ms.date: 11/9/2022 +ms.topic: article +uid: migration/fx-to-core/start +--- + +# Get started with incremental ASP.NET to ASP.NET Core migration + +> [!IMPORTANT] +> **Before you begin**: This article assumes you have read the [ASP.NET Core migration overview](xref:migration/fx-to-core/index). If you haven't read it yet, please start there to understand the concepts, approach, and benefits of incremental migration. + +For a large migration, we recommend setting up a ASP.NET Core app that proxies to the original .NET Framework app. The new proxy enabled app is shown in the following image: + +![start migrating routes](~/migration/fx-to-core/inc/overview/static/nop.png) + +This article provides the practical steps to proceed with an incremental migration after you understand the approach. + +## Prerequisites + +Before starting your incremental migration, ensure you have: + +1. **Read the overview**: [Incremental ASP.NET to ASP.NET Core migration](xref:migration/fx-to-core/index) +2. **A working ASP.NET Framework application** that you want to migrate +3. **Visual Studio 2022** with the latest updates +4. **.NET 8 or later SDK** installed +5. **Understanding of your application's dependencies** and third-party libraries + +## Migration Steps Overview + +The incremental migration process follows these key steps: + +1. [Set up ASP.NET Core Project](#set-up-aspnet-core-project) +2. [Remediate Technical Debt](#remediate-technical-debt) +3. [Identify and address cross-cutting concerns](#identify-and-address-cross-cutting-concerns) +4. [Upgrade supporting libraries](#upgrade-supporting-libraries) + +## Set up ASP.NET Core Project + +The first step is to create the new ASP.NET Core application that will serve as your proxy. + +**What you'll do:** +* Create a new ASP.NET Core project alongside your existing ASP.NET Framework app +* Configure it to proxy requests to your original application using YARP (Yet Another Reverse Proxy) +* Set up the basic infrastructure for incremental migration + +**Detailed instructions:** +* See to understand how to set up an application for incremental migration. +* See for help in setting up projects required for incremental migration using Visual Studio tooling. + +## Remediate Technical Debt + +**When to do this step:** Before upgrading any supporting libraries, address technical debt that could complicate the migration process. + +Before you begin upgrading your supporting libraries, it's important to clean up technical debt that could interfere with the migration process. This step should be completed first to ensure a smoother upgrade experience. + +### Update Package Dependencies + +Review and update your NuGet packages to their latest compatible versions: + +1. **Audit existing packages**: Use Visual Studio's NuGet Package Manager as the `dotnet` CLI does not work for ASP.NET Framework applications +2. **Update packages incrementally**: Update packages one at a time to avoid compatibility issues +3. **Test after each update**: Ensure your application still functions correctly after each package update +4. **Address breaking changes**: Some package updates may introduce breaking changes that need to be addressed + +### Modernize Build Tools + +Update your build tools and project configuration: + +1. **Update tools**: Ensure you're using a recent version of MSBuild/Visual Studio +1. **Migrate to PackageReference for dependencies**: Consider migrating from `packages.config` to `PackageReference` format if you haven't already in the Web application project +1. **Clean up unused references**: Remove any unused assembly references or NuGet packages +1. **Migrate to SDK-style project files**: Convert your existing project files to the modern SDK-style format. This is essential for compatibility with modern .NET projects and provides better tooling support +1. **Update build scripts**: Review and update any custom build scripts or CI/CD configurations + +### Address Code Quality Issues + +Fix known code quality issues that could complicate migration: + +1. **Fix compiler warnings**: Address any compiler warnings, especially those related to deprecated APIs +1. **Remove dead code**: Clean up unused classes, methods, and other code elements +1. **Update deprecated API usage**: Replace usage of deprecated APIs with their modern equivalents where possible + +This preparation work will make the library upgrade process much smoother and reduce the likelihood of encountering complex issues during migration. + +## Identify and address cross-cutting concerns + +**When to do this step:** While remediating technical debt but before upgrading supporting libraries, identify and configure cross-cutting concerns that affect your entire application. + +Cross-cutting concerns are aspects of your application that span multiple layers or components, such as authentication, session management, logging, and caching. These need to be addressed early in the migration process because they affect how your ASP.NET Framework and ASP.NET Core applications communicate and share state during the incremental migration. + +The following sections cover the most common cross-cutting concerns. Configure only the ones that apply to your application: + +### Session Support Configuration + +**Configure this if:** Your ASP.NET Framework application uses session state. + +See the general [session migration documentation](xref:migration/fx-to-core/areas/session#remote-app-session state) for guidance here. + +Session is a commonly used feature of ASP.NET that shares the name with a feature in ASP.NET Core, but the APIs are much different. When upgrading libraries that use session state, you'll need to configure session support. See the documentation on [remote session support](xref:migration/fx-to-core/areas/session) for detailed guidance on how to enable session state sharing between your applications. + +### Authentication Configuration + +**Configure this if:** Your ASP.NET Framework application uses authentication and you want to share authentication state between the old and new applications. + +See the general [authentication migration documentation](xref:migration/fx-to-core/areas/authentication) for guidance here. + +It is possible to share authentication between the original ASP.NET app and the new ASP.NET Core app by using the System.Web adapters remote authentication feature. This feature allows the ASP.NET Core app to defer authentication to the ASP.NET app. See the documentation on [remote authentication](xref:migration/fx-to-core/areas/authentication#remote-authentication) for more details. + +### Other Cross-Cutting Concerns to Consider + +Depending on your application, you may also need to address: + +* **Logging**: Ensure consistent logging across both applications. Consider using a shared logging provider or ensuring logs are aggregated properly. +* **Caching**: If your application uses caching (in-memory, distributed, or output caching), plan how to maintain cache consistency between applications. +* **Error Handling**: Establish consistent error handling and reporting across both the ASP.NET Framework and ASP.NET Core applications. +* **Configuration Management**: Plan how configuration settings will be shared or managed between the two applications. +* **Health Monitoring**: Set up monitoring and health checks for both applications during the migration process. +* **Dependency Injection**: If using a DI container in your ASP.NET Framework app, plan the migration to ASP.NET Core's built-in DI container. + +## Upgrade supporting libraries + +**When to do this step:** Only when you need to migrate specific routes that depend on class libraries containing business logic you'll need to share between the old and new applications. + +> [!NOTE] +> **Incremental approach**: With the incremental migration process, you don't need to upgrade all your supporting libraries at once. You only need to upgrade the libraries that are required for the specific routes you're currently migrating. This allows you to tackle the migration in smaller, more manageable pieces. + +### Library Upgrade Process + +> [!IMPORTANT] +> Supporting libraries must be upgraded in a **postorder depth-first search ordering**. This means: +> +> 1. **Start with leaf dependencies**: Begin with libraries that have no dependencies on other libraries in your solution +> 2. **Work upward through the dependency tree**: Only upgrade a library after all of its dependencies have been successfully upgraded +> 3. **End with the main application**: The main ASP.NET Framework application should be the last item to be modified +> +>This ordering is essential because: +> * It ensures that when you upgrade a library, all of its dependencies are already compatible +> * It prevents circular dependency issues during the upgrade process +> * It allows you to test each library independently before moving to its dependents +> +> **NOTE**: You only need to follow this ordering for the subset of libraries required by the routes you're currently migrating, not your entire solution. + +**Upgrade process for each library:** + +If you have supporting libraries in your solution that you will need to use for the routes you're migrating, they should be upgraded to .NET Standard 2.0, if possible. [Upgrade Assistant](https://github.com/dotnet/upgrade-assistant) is a great tool for this. If libraries are unable to target .NET Standard, you can target .NET 8 or later either along with the .NET Framework target in the original project or in a new project alongside the original. + +The [System.Web adapters](~/migration/fx-to-core/inc/systemweb-adapters.md) can be used in these libraries to enable support for usage in class libraries. In order to enable usage in a library: + +1. Remove reference to `System.Web` in the project file +2. Add the `Microsoft.AspNetCore.SystemWebAdapters` package +3. Enable multi-targeting and add a .NET 8 target or later, or convert the project to .NET Standard 2.0. +4. Ensure the target framework supports .NET Core. Multi-targeting can be used if .NET Standard 2.0 is not sufficient + +This step may require a number of projects to change depending on your solution structure and which routes you're migrating. Upgrade Assistant can help you identify which ones need to change and automate a number of steps in the process. + + +## Next Steps + +Once you've completed the setup and library upgrade steps above: + +1. **Start small**: Begin by migrating simple, stateless endpoints first +2. **Test thoroughly**: Ensure each migrated component works correctly in both environments +3. **Monitor performance**: Watch for any performance impacts from the proxy setup +4. **Iterate**: Continue migrating components incrementally until the migration is complete diff --git a/aspnetcore/migration/fx-to-core/tooling.md b/aspnetcore/migration/fx-to-core/tooling.md index 1f4718a1a725..b781ab0bc350 100644 --- a/aspnetcore/migration/fx-to-core/tooling.md +++ b/aspnetcore/migration/fx-to-core/tooling.md @@ -8,7 +8,7 @@ uid: migration/fx-to-core/tooling --- # Use tooling to help migrate ASP.NET Framework to ASP.NET Core -This article shows how to upgrade ASP.NET Framework applications (MVC, Web API, and Web Forms) to ASP.NET Core using the Visual Studio [.NET Upgrade Assistant](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.upgradeassistant) and the [incremental update](xref:migration/fx-to-core/inc/overview) approach. +This article shows how to upgrade ASP.NET Framework applications (MVC, Web API, and Web Forms) to ASP.NET Core using the Visual Studio [.NET Upgrade Assistant](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.upgradeassistant) and the [incremental update](xref:migration/fx-to-core/index) approach. > [!WARNING] @@ -16,14 +16,14 @@ This article shows how to upgrade ASP.NET Framework applications (MVC, Web API, ## Prerequisites -If your .NET Framework project has supporting libraries in the solution that are required, they should be upgraded to .NET Standard 2.0, if possible. For more information, see [Upgrade supporting libraries](xref:migration/fx-to-core/inc/start#upgrade-supporting-libraries). +If your .NET Framework project has supporting libraries in the solution that are required, they should be upgraded to .NET Standard 2.0, if possible. For more information, see [Upgrade supporting libraries](xref:migration/fx-to-core/start#upgrade-supporting-libraries). 1. Install the [.NET Upgrade Assistant](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.upgradeassistant) Visual Studio extension. 1. Open your ASP.NET Framework solution in Visual Studio. 1. In **Solution Explorer**, right click on the project to upgrade and select **Upgrade**. Select **Side-by-side incremental project upgrade**, which is the only upgrade option. 1. For the upgrade target, select **New project**. -1. Name the project and select the appropriate template: +1. Name the project and select the best fit template (you may add the required services later if you have a solution that uses a mixture of project types): > [!NOTE] > **For MVC projects:** Select **ASP.NET Core MVC** template. diff --git a/aspnetcore/security/cookie-sharing.md b/aspnetcore/security/cookie-sharing.md index 724c2991bc7a..892489eb1169 100644 --- a/aspnetcore/security/cookie-sharing.md +++ b/aspnetcore/security/cookie-sharing.md @@ -83,9 +83,9 @@ When the Identity schema is different among apps, usually because apps are using ## Share authentication cookies between ASP.NET 4.x and ASP.NET Core apps -ASP.NET 4.x apps that use Microsoft.Owin Cookie Authentication Middleware can be configured to generate authentication cookies that are compatible with the ASP.NET Core Cookie Authentication Middleware. This can be useful if a web application consists of both ASP.NET 4.x apps and ASP.NET Core apps that must share a single sign-on experience. A specific example of such a scenario is [incrementally migrating](xref:migration/fx-to-core/inc/overview) a web app from ASP.NET to ASP.NET Core. In such scenarios, it's common for some parts of an app to be served by the original ASP.NET app while others are served by the new ASP.NET Core app. Users should only have to sign in once, though. This can be accomplished by either of the following approaches: +ASP.NET 4.x apps that use Microsoft.Owin Cookie Authentication Middleware can be configured to generate authentication cookies that are compatible with the ASP.NET Core Cookie Authentication Middleware. This can be useful if a web application consists of both ASP.NET 4.x apps and ASP.NET Core apps that must share a single sign-on experience. A specific example of such a scenario is [incrementally migrating](xref:migration/fx-to-core/index) a web app from ASP.NET to ASP.NET Core. In such scenarios, it's common for some parts of an app to be served by the original ASP.NET app while others are served by the new ASP.NET Core app. Users should only have to sign in once, though. This can be accomplished by either of the following approaches: -* Using the System.Web adapters' [remote authentication](xref:migration/fx-to-core/inc/remote-authentication) feature, which uses the ASP.NET app to sign users in. +* Using the System.Web adapters' [remote authentication](xref:migration/fx-to-core/areas/authentication#remote-authentication) feature, which uses the ASP.NET app to sign users in. * Configuring the ASP.NET app to use Microsoft.Owin Cookie Authentication Middleware so that authentication cookies are shared with the ASP.NET Core app. To configure ASP.NET Microsoft.Owin Cookie Authentication Middleware to share cookies with an ASP.NET Core app, follow the preceding instructions to configure the ASP.NET Core app to use a specific cookie name, app name, and to persist data protection keys to a well-known location. See [Configure ASP.NET Core Data Protection](xref:security/data-protection/configuration/overview) for more information on persisting data protection keys. diff --git a/aspnetcore/toc.yml b/aspnetcore/toc.yml index 61e3e17209cb..dc49c844e482 100644 --- a/aspnetcore/toc.yml +++ b/aspnetcore/toc.yml @@ -2098,43 +2098,21 @@ items: items: - name: Overview uid: migration/fx-to-core/index + - name: Get started + uid: migration/fx-to-core/start + - name: Tooling + uid: migration/fx-to-core/tooling - name: Incremental migration displayName: migrate, migration items: - - name: Overview - uid: migration/fx-to-core/inc/overview - - name: Get started - uid: migration/fx-to-core/inc/start - name: System.Web adapters uid: migration/fx-to-core/systemweb-adapters - name: Remote app setup uid: migration/fx-to-core/inc/remote-app-setup - - name: Usage Guidance - uid: migration/fx-to-core/inc/usage_guidance - - name: Wrapped session state - uid: migration/fx-to-core/inc/wrapped - - name: Session state migration - uid: migration/fx-to-core/inc/session - - name: Remote app session state - uid: migration/fx-to-core/inc/remote-session - - name: Remote Authentication - uid: migration/fx-to-core/inc/remote-authentication - - name: Unit testing - uid: migration/fx-to-core/inc/unit-testing - name: A/B Testing endpoints uid: migration/fx-to-core/inc/ab-testing - name: Blazor support with YARP uid: migration/fx-to-core/inc/blazor - - name: Tooling - uid: migration/fx-to-core/tooling - - name: Example Migration - items: - - name: Overview - uid: migration/fx-to-core/examples/overview - - name: Configuration - uid: migration/fx-to-core/examples/configuration - - name: Identity - uid: migration/fx-to-core/examples/identity - name: Technology Areas items: - name: Overview @@ -2145,12 +2123,26 @@ items: uid: migration/fx-to-core/areas/http-modules - name: HTTP Handlers uid: migration/fx-to-core/areas/http-handlers + - name: Session + uid: migration/fx-to-core/areas/session + - name: Authentication + uid: migration/fx-to-core/areas/authentication - name: Membership uid: migration/fx-to-core/areas/membership - name: Web API uid: migration/fx-to-core/areas/webapi - name: ClaimsPrincipal.Current uid: migration/fx-to-core/areas/claimsprincipal-current + - name: Miscellaneous + uid: migration/fx-to-core/areas/misc + - name: Example Migration + items: + - name: Overview + uid: migration/fx-to-core/examples/overview + - name: Configuration + uid: migration/fx-to-core/examples/configuration + - name: Identity + uid: migration/fx-to-core/examples/identity - name: API reference href: /dotnet/api/ - name: Contribute diff --git a/aspnetcore/zone-pivot-groups.yml b/aspnetcore/zone-pivot-groups.yml index 88d0a36b0be4..8bd3c582279f 100644 --- a/aspnetcore/zone-pivot-groups.yml +++ b/aspnetcore/zone-pivot-groups.yml @@ -118,3 +118,11 @@ groups: title: MSTest - id: nunit title: NUnit +- id: migration-remote-app-setup + title: Remote app setup + prompt: Choose how to configure remote app + pivots: + - id: manual + title: Manually + - id: aspire + title: Aspire \ No newline at end of file