Continuing my series on porting to .NET Core, I have mostly converted a production .NET 4.5.x application to .NET Core 2.2. As I mentioned, there are lots of conversion points that are worth mentioning for anyone else endeavoring to modernize a .NET application.
In my previous post, I mentioned moving Authentication from OWIN to NetCore, I so won’t talk about that again. Also, I have talked about porting EF 6.x to EF Core previously, so I won’t go into much detail regarding that aspect, either, in this post.
To start, I came up with a base csproj replacement – this is the base I used to simply go through and replace the contents of every *.csproj class library. The WebApp had a slightly different format so that Visual Studio would recognize it as a WebApp – I just used the default VS template for that replacement. I found that, based on the initial imported references, Visual Studio would recognize my class libraries as WebApps. This is why I experimented a tad and came up with a base template.
In my scenario, I have multiple environments, but your environments may differ. Also, in the process of migrating, I did not want to deal with libraries that are unavailable or incompatible with netcoreapp2.2 target. You will see that my target is “net472” as a result.
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net472</TargetFramework> <DebugType>full</DebugType> <Configurations>Debug;Release;DEV;PROD</Configurations> <RootNamespace>[your root namespace]</RootNamespace> <AssemblyName>[your assembly name]</AssemblyName> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='DEV|AnyCPU'"> <Optimize>true</Optimize> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='PROD|AnyCPU'"> <Optimize>true</Optimize> </PropertyGroup> <ItemGroup> <PackageReference Include="Newtonsoft.Json" Version="12.0.2" /> </ItemGroup> </Project>
After replacing the *.csproj content of the WebApp and all related class libraries, in the WebApp, I removed/commented out these files:
- Global.asax/.cs
- BundlgConfig.cs
- FilterConfig.cs
- OpenIdConnectCachingSecurityTokenPRovider.cs
- RouteConfig.cs
- Startup.cs
- WebApiConfig.cs
The functionality provided in these files is either no longer needed or has to be factored into the “new” Startup.cs.
HttpResponseMessage:
Generally speaking outputting an HttpResponseMessage in controller actions is slightly different in .NET Core. Here are a link that discusses the change a bit: https://stackoverflow.com/questions/41992033/asp-net-core-webapi-httpresponsemessage-create-custom-message
FileUploads:
Handling file uploads – much of the functionality that was used with ApiControllers is removed in AspNetCore Controllers. Preferred method is to use the IFormFile interface.
https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-2.2 https://github.com/aspnet/AspNetCore.Docs/blob/master/aspnetcore/mvc/models/file-uploads/sample/FileUploadSample/MultipartRequestHelper.cs https://code-maze.com/file-upload-aspnetcore-mvc/
A straight-forward implementation of a controller action called “Upload” might look like this:
public async Task<List<MyFileObject>> Upload(List<IFormFile> files) { Exception<ArgumentException>.ThrowOn(() => files.Count != 1, "Incorrect number of input files provided."); var file = files.First(); var fileName = file.FileName.Trim('\"'); var extension = Path.GetExtension(fileName).ToLower(); // Put the stream into memory so we can get byte array var memorySteam = new MemoryStream(); await file.OpenReadStream().CopyToAsync(memorySteam); byte[] fileBytes = memorySteam.ToArray(); Exception<ArgumentException>.ThrowOn(() => fileBytes.Length == 0, "Invalid input file."); return await SaveUploadedFile(fileBytes, fileName); }
Query parameters and RequestUri
Request.GetQueryNameValuePairs -> Request.Query.ToDictionary(q => q.Key, q => q.Value)
this.Request.RequestUri => new Uri(this.Request.GetEncodedUrl()) https://stackoverflow.com/questions/31617345/what-is-the-asp-net-core-mvc-equivalent-to-request-requesturi
ResponseType:
Generally replaced with IActionResult
https://stackoverflow.com/questions/48318381/responsetype-in-asp-net-core
ClientAssertionCertificate (Microsoft.Identity.Client nuget):
https://azure.microsoft.com/en-us/resources/samples/active-directory-dotnetcore-daemon-v2/
https://github.com/Azure-Samples/active-directory-dotnetcore-daemon-v2/blob/msal3x/daemon-console/daemon-console.csproj
AuthenticationContext:
Moving modules/HTTP handlers to Middleware:
https://docs.microsoft.com/en-us/aspnet/core/migration/http-modules?view=aspnetcore-2.2
System.Web and HttpRunTime removal
There is no more static reference to HttpContext.Current. IHttpContextAccessor must be injected wherever HttpContext is desired. There are many classes in DCAT that use HttpContext.Current and also use it within static constructors (ugh!). This requires a fair bit of refactoring.
As a stop gap, we can create helpers for things that are removed such as HttpContext.Current:
public static class HttpContextHelper { private static IHttpContextAccessor _contextAccessor; public static void Configure(IHttpContextAccessor httpContextAccessor) { _contextAccessor = httpContextAccessor; } public static HttpContext Current => _contextAccessor?.HttpContext; }
In our Startup.cs, we can set the static/singleton reference to IHttpContextAccessor:
// Initialize our HttpContextHelper HttpContextHelper.Configure(ServiceProviderFactory.ServiceProvider.GetRequiredService<IHttpContextAccessor>());
System.Runtime.Cache is not recommended to be used although it is possible to use it with .NET Core
https://docs.microsoft.com/en-us/aspnet/core/performance/caching/memory?view=aspnetcore-2.2
The preferred interface is IMemoryCache for cases where a memory cache is desired. In places where there are static references to System.Runtime.Cache or HttpRuntime.Cache, we can create a simple helper:
public static class MemoryCacheHelper { private static IMemoryCache _memoryCache; public static void Configure(IMemoryCache memoryCache) { _memoryCache = memoryCache; } public static IMemoryCache Current => _memoryCache; }
// Initialize our HttpContextHelper
// Initialize our MemoryCacheHelper MemoryCacheHelper.Configure(ServiceProviderFactory.ServiceProvider.GetRequiredService<IMemoryCache>());
Bond:
For Bond to work with .NET core, any .bond files must be added as <itemGroup> elements to the *.csproj
Controllers:
RoutePrefix attribute becomes, simply, Route.
Filters:
Filters can be added by attribute or globally. If global, filters are added in Startup.cs within AddMvc(options => options.Filters.Add(…)) extension.
AuthorizeAttribute – this is typically replaced with IAuthorizationFilter
ActionDescription is replaced by casting AuthorizationFilterContext.ActionDescription to ControllerActionDescriptor and retrieving ActionName.
ActionFilter – actions filters can generally be directly ported with some caveats.
HttpActionExecutedContext becomes ActionExecutedContext – HttpContext is used for accessing Request/Response
Casting context.Response.Content as ObjectContent is no longer available. Instead, cast ActionExecutedContext.Result as JsonResult or ObjectResult
Modifying Response body requires only modifying/replacing ActionExecutedContext.Result with a different result (such as JsonResult)
Many filters are better off being rewritten as middleware.
Binders:
IModlerBinder behaves significantly differently in .NET Core vs. MVC5/6. It appears that values bound via FromBody are not passed into custom model binders. As such, you can take two different approaches if your application uses a custom model binder.
First, you can replace the custom model binder with custom ValueProviders. This can have undesired side-effects since they are applied to all values. IE – a global delimited parser would produce undesirable effects if, in some cases, the delimiter can be part of a valid model property.
Second option is to port the custom model binders directly and provide a ModelBinderProvider for the explicit types that are being bound with the custom provider.
In DCAT, I took the second option after deciding that I didn’t want to introduce edge-case weirdness with custom ValueProviders. To deal with the [FromBody] scenario, the BindingContext in the ModelBinderProvider does not apply the a custom binder when the BindingContext is Body.
ValueProviders:
There is one value provider to grab header values and prefixed-type stuff. I refactored that to work within the new framework assemblies.
For the delimited list scenario, I created a new ValueProvider/Factory that will simply look at the string values in the query parameter value. If it sees the delimiter, then it will change this single StringValue to a collection of StringValues. In doing this, the default binders (ArrayModelBinder) will get the array of strings and handle the casting.
However, after investigating, I do not apply the custom ValueProviders.
Startup:
This is the big one. In older MVC6, there was a lot of setup for serialization, filters, OWIN authentication, etc etc.
Of this is replaced with a single Startup.cs file that attaches DI, binds configurations, Middleware (custom, MVC, authentication, etc), and runs any other necessary code such as instantiating the helpers mentioned above. This consolidated startup makes it fairly easy to see what the application is doing.
There is also a Program.cs file that executes kestrel and begins the hosting of the WebApp. It’s invocation is relatively straight-forward.
Configuration
Ideally, this will simply use the existing .NET Core “AppSettings” IOptions methodology. For now, though, I have create a single object that can be bound to. Within the class, it has a static reference to itself (this) so that there is very little code change required throughout the application. Accessing Configs.Current is a static reference to the initial configuration object that is bound.
Web JavaScript and CSS bundling
Gulp/minification (also see my working sample)s
The built-in bundler that were part of the older MVC are no longer present. The preferred method with static js/css is to bundle it via gulp. This allows a relatively robust mechanism to bundle and minify. It does require node and gulp to be installed.
Effectively a gulp task runner file (gulpfile.js) replaces BundleConfig.cs. A build task can be put in the *.csproj to execute this gulp file on every build. A watcher can also be defined so that building manually is not needed to reflect updates. Another option is to use WebPack, but that’s a bit more complex for simply service static files. Webpack, imho, is more useful for compiled JavaScript and components (such as Angular using TypeScript).
Additionally, @Script and @Style tags are no longer part of Razor views. Scripts and CSS files are referenced via standard HTML stags with a relative path to the wwwroot folder.
CorrelationId –
.NET Core already has a built-in HttpContext.TraceIdentifier. However, Middleware must be used to bind to this property. The Middleware is relatively straightforward to intercept headers on the request and to write the headers on the response. A good example is here:
https://www.stevejgordon.co.uk/asp-net-core-correlation-ids
A sample implementation, where a Guid is desired, would look like this:
public class CorrelationIdMiddleware { private readonly RequestDelegate _next; public CorrelationIdMiddleware(RequestDelegate next) { _next = next ?? throw new ArgumentNullException(nameof(next)); } public Task Invoke(HttpContext context) { Guid rslt; StringValues correlationIds; if (context.Request.Headers.TryGetValue("X-Correlation-ID", out correlationIds)) { if (Guid.TryParse(correlationIds.FirstOrDefault(), out rslt)) { context.TraceIdentifier = rslt.ToString(); } } else if (context.Request.Headers.TryGetValue("X-Request-ID", out correlationIds)) { if (Guid.TryParse(correlationIds.FirstOrDefault(), out rslt)) { context.TraceIdentifier = rslt.ToString(); } } else { context.TraceIdentifier = Guid.NewGuid().ToString(); } // apply the correlation ID to the response header for client side tracking context.Response.OnStarting(() =&amp;amp;gt; { context.Response.Headers.Add("X-Correlation-ID", new[] { context.TraceIdentifier }); return Task.CompletedTask; }); return _next(context); } }
Custom UserPrincipal –
.NET Core defaults to utilizing a ClaimsPrincipal on the context for authentication. If you’re utilizing a custom principal, it generally isn’t possible to principal directly, even if subclassed from ClaimsPrincipal. If you want to continue with your custom principal, you must attach your own middleware directly after the authentication middleware. A simple example middleware invocation might look like this:
public async Task Invoke(HttpContext context) { try { var userPrincipal = new UserPrincipal(context.User.Identity); context.User = userPrincipal; } } catch (Exception ex) { Console.WriteLine(ex); } await _next(context); }
App_Data
NetCore no longer has the concept of a special folder called App_Data. If you want to continue using this folder you can specify it in the ConfigServices portion of the Startup.cs like so:
// Make App_Data available AppDomain.CurrentDomain.SetData("DataDirectory", Path.Combine(env.ContentRootPath, "App_Data"));
It can be accessed like so:
var pathStr = $"{AppDomain.CurrentDomain.GetData("DataDirectory")}";
Misc Links:
https://edi.wang/post/2018/10/31/migrating-old-aspnet-applications-to-net-core
https://www.mikesdotnetting.com/article/302/server-mappath-equivalent-in-asp-net-core
https://source.dot.net/