Converting Web App to ASP.Net Core

September 2016

Follow along at:

http://itsnull.com/

http://itsnull.com/presentations/moveToCore/

Created by Kip Streithorst / @itsnull

Goal

  • Have an simple existing web app (VS 2013 using WebAPI)
  • Discuss why ASP.Net Core, .Net Core
  • Convert to ASP.Net Core and .NET Core
  • Demo converted app running on Linux

Starting App - DEMO

  • Chore tracking application, built during previous set of meetups
  • WebAPI v2, VS 2013
  • No MVC, has static HTML
  • No database
  • Has custom in-memory persistence w/ optional write to disk

What is .Net Core?

  • Cross platform, open-source .NET runtime
  • Runs on Linux, Mac OS X and Windows, supported by MS
  • Doesn't have to install globally, smaller footprint
  • Can run isolated and multiple versions side by side

Why not .Net Core?

  • Removed - App Domains, Remoting, Binary serialization, Sandboxing
  • Gone, but might come back - System.Data.DataTable, System.Drawing, XSD, XSLT, System.Net.Mail
  • Slight changes - Reflection
  • As of today, most NuGet packages may not run on it. This should improve over time.

What is ASP.Net Core?

  • Re-write of MVC and WebAPI
  • Removes dependency on IIS and System.Web
  • Merges WebAPI and MVC pipelines together, only one controller
  • Higher performance in general, pay as you play performance model
  • Runs on .NET Framework or .NET Core

Why not ASP.Net Core?

  • Web.config gone - IIS provided that
  • Manual conversion - MVC, WebAPI pipelines merged, new class names and namespaces.
  • WebForms is not part of ASP.NET Core
  • SignalR is missing in 1.0.0 release of ASP.Net Core

So, why again?

  • More choices - choice of OS, choice of server
  • Higher performance for ASP.Net Core
  • Not at all tied to Visual Studio
  • Smaller footprint, e.g. Docker containers
  • Can be xcopy deployed, including .NET Core Runtime
  • Still C#, still MVC, WebAPI pipeline with minor syntax changes

Convert to .NET Core?

Convert to .NET Core? Maybe not

  • ASP.Net Core will run on either .NET Core or .NET Framework
  • My advice: port to ASP.Net Core first, then .NET Core
  • Porting this way only affects one tier of your application
  • For my presentation and web app, I'm porting both at the same time.

Converting to ASP.Net Core

  • Use VS 2015 Update 3, update Microsoft ASP.Net and Web Tools extension
  • File -> New project in VS 2015
  • Decide between .NET Framework or .NET Core, you can change your mind later

Converting to ASP.Net Core

  • Select WebAPI on the next screen for the template
  • Demo template and discuss
  • Demo in VS 2015, with IIS Express and console app
  • Demo in VS Code
  • Demo dotnet command-line

Converting to ASP.Net Core

  • Copy C# code from WebAPI project into new project
  • ApiController class gone, HttpResponseException class gone
  • New controller base class, exception gone, routing and model binding changed
  • Found WebApiCompatShim, part of ASP.Net Core
  • Contains ApiController, HttpResponseException, original routing, etc...

Shim or Not?

  • WebApiCompatShim makes transition much easier
  • But new code is using a shim?
  • Haven't researched a MVC shim or know if one is needed
  • Decided to do 2 conversions: first with shim, second without shim

Enabling WebApiCompatShim

  • Add nuget package to project.json
    "dependencies": {
      "Microsoft.AspNetCore.Mvc.WebApiCompatShim": "1.0.0",
    }            
                
  • Update Startup.cs, ConfigureServices method
    
    public void ConfigureServices(IServiceCollection services) {
        //from this: services.AddMvc();
        //to this:
        services.AddMvc().AddWebApiConventions();
    }
                

Enabling WebApiCompatShim, Cont.

  • Update Startup.cs, Configure
    
    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) {
      //from this: app.UseMvc();
      //to this:
      app.UseMvc(options => options.MapWebApiRoute("DefaultApi", "api/{controller}/{id?}"));
    }
                
  • [Route] or [HttpGet], [HttpPost] attributes, moved to new namespace using Microsoft.AspNetCore.Mvc;

Compiling, but not MapPath

  • This app has custom persistence, requires HostingEnvironment.MapPath("/App_Data/") to locate App_Data folder
  • That class is part of System.Web, which ASP.Net core no longer uses.
  • Replaced with IHostingEnvironment.ContentRootFileProvider. GetFileInfo("/App_Data").PhysicalPath

Fixing MapPath

  • IHostingEnvironment is interface that my ChoreRepository needs
  • So, ChoreRepository has to be configured to use ASP.Net Core Dependency Injection
  • Update ChoreRepository constructor to take IHostingEnvironment parameter
  • ChoreRepository previous had .GetInstance() method, convert all Controllers to receive ChoreRepository as constructor parameter

Fixing MapPath, Cont.

  • Enlist ChoreRepository into DI system in Startup.cs
    
    public void ConfigureServices(IServiceCollection services) {
      services.AddSingleton<ChoreRepository, ChoreRepository>();
    }
                

Api works

  • Success! Compiles and Runs!
  • At this point, Api appears to work when tested manually
  • Need static HTML and JS now

Add Static HTML, JS

  • Content has to be in wwwroot/ folder
  • Move css/, fonts/, images/, scripts/, index.html into wwwroot/ folder
  • However, still doesn't work

Enable Static Serving

  • Must enable static file serving
  • Add nuget package to project.json
    "dependencies": {
      "Microsoft.AspNetCore.StaticFiles": "1.0.0",
    }            
                

Enable Static Serving, Cont.

  • Update Startup.cs, Configure
    
    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) {
      app.UseStaticFiles();
    }
                
  • http://localhost:5000/index.html works
  • http://localhost:5000/ doesn't

Enable default routing

  • Must enable default routing
  • Update Startup.cs, Configure
    
    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) {
      app.UseDefaultFiles();
      //must be before app.UseStaticFiles
    }
                
  • Now, http://localhost:5000/ works

Working???

  • This probably doesn't count

JSON Serialization

  • WebAPI would serialize a C# class to JSON with the exact same casing
  • ASP.Net Core will serialize as JS case by default, so:
  • class Person { string FirstName {get; set;} }
  • Will serialize as "{firstName: ""}" with ASP.Net Core
  • This is a breaking change

Fix JSON serialization

  • So, either update all your JS code
  • And any clients that use your api
  • Or, have ASP.Net Core revert to prior behavior

Fix JSON serialization, Cont.

  • Update Startup.cs, ConfigureServices method
    
    public void ConfigureServices(IServiceCollection services) {
      //append AddJsonOptions to AddMvc() call
      services.AddMvc().AddJsonOptions(opt => {
          var res = opt.SerializerSettings.ContractResolver as DefaultContractResolver;
          if (res != null) {
              res.NamingStrategy = null;
          }
      });
    }
                

It Works!

Model Binding Bug

  • Two of my api calls don't work
  • I get a 415 Unsupported Media Type response
  • Why?

Model Binding Bug, Cont.

  • Two ways to send complex data from client to server
  • //BODY:
    $.ajax({
      type: 'POST',
      url: '/someUrl',
      contentType: 'application/json',
      data: JSON.stringify({ Name: 'Jane', Age: 2 })
    });
    //FORM:
    $.ajax({
      type: 'POST',
      url: '/someUrl',
      data: { Name: 'Jane', Age: 2 }
    });
                

Model Binding Bug, Cont.

Model Binding Bug, Cont.

  • ASP.Net Core default if you don't do anything is assume you typed [FromForm]
  • However, using WebApiCompatShim changes the default to [FromBody]
  • Application JS that caused server error was submitting using Form, so I had to add [FromFrom] to that C# controller method

Demo on Linux

  • Copy code over
  • dotnet restore
  • dotnet run
  • http://localhost:5000/

Without the shim

  • Remove WebApiCompatShim from project.json and Startup.cs
  • Must add [FromBody] to controller methods that take complex types, e.g. POST, PUT
  • Controller no longer has a subclass at all, previously ApiController

Without the shim, Cont.

  • Add [Route("api/{controller}")] to each controller class
  • Add [HttpGet()] to /api/controller/ methods
  • Add [HttpGet("{id}")] to /api/controller/id/ methods
  • Add [HttpPost()] to post methods, e.g. create
  • Add [HttpPut("{id}")] to put methods, e.g. update
  • Add [HttpDelete("{id}")] to delete methods

Without the shim, Cont.

  • You might have to fix old [Route] attributes
  • Previously, I had [Route("api/chores/complete")]
  • But now that controller had [Route("api/{controller}"] added
  • The two urls combined into /api/chores/api/chores/complete
  • Updated existing attribute to [Route("complete")]

Without the shim, Cont.

  • Had to fix usage of HttpResponseException
  • Created three custom subclasses of Exception: DataConflictException, DataMissingException and InvalidRequestException
  • Also switched to using built-in TimeoutException
  • Had to write custom filter for ASP.Net Core pipeline

Without the shim, Cont.

  • Converts exception into status code response
  • public override void OnException(ExceptionContext context) {
        var statusCode = HttpStatusCode.OK;
        if (context.Exception is DataConflictException) {
            statusCode = HttpStatusCode.Conflict;
        } else if (context.Exception is DataMissingException) {
            statusCode = HttpStatusCode.NotFound;
        } else if (context.Exception is InvalidRequestException) {
            statusCode = HttpStatusCode.BadRequest;
        } else if (context.Exception is TimeoutException) {
            statusCode = HttpStatusCode.RequestTimeout;
        }            
        if (statusCode != HttpStatusCode.OK) {
            context.ExceptionHandled = true;
            context.Result = new StatusCodeResult((int)statusCode);
        } 
    }     
                

Without the shim, Cont.

  • Must register custom filter into pipeline
  • Update Startup.cs, ConfigureServices method
    
    public void ConfigureServices(IServiceCollection services) {
      services.AddMvc(opt => {
          opt.Filters.Add(new DataExceptionFilterAttribute());
      });
    }
                

Demo on Linux

  • Copy code over
  • VS code works just as well
  • code .
  • Debug with VS code
  • http://localhost:5000/

All code is available

Thanks, Any Questions?