I've been in the guts of an ASP.NET MVC 5 application lately and have some more perspective on the improvements we take for granted in ASP.NET Core.
Tag helpers are far easier to read and write compared to HTML helpers. There were days when I wanted to give up on Razor views, but tag helpers make the Razor engine second to only JSX in terms of smoothness. Of all the different techniques I've used to dynamically generate HTML, JSX and TSX are by far my favorites, and I think that’s because JSX took the approach of embedding the declarative language inside the imperative language (HTML inside of JavaScript). Embedding in the other direction, as Razor and others have done (language X inside of HTML), always seems to create scenarios with awkward syntax.
It is refreshing to work with a framework that embraces dependency injection, like ASP.NET Core. ASP.NET MVC danced around DI and provides a hook for a central dependency resolver, but you always have to wonder if something might not work because you’ve strayed outside the lines.
The artificial separation between ASP.NET MVC and the ASP.NET Web API was unfortunate and unpleasant, but follows Conway’s law.
The startup logic for an ASP.NET MVC application is difficult to follow. There is code in global.asax and three or four other files in the App_Start folder. I haven’t been a big fan of how ASP.NET Core applications organize startup logic (see opinion 7 and 10), but even the worst examples are far better than the MVC approach.
Applications built before attribute routing was popular have the worst API routes.
Front end builds were easy and fast when all you had to do was bundle and minify jQuery and a few other files.
The scaffolding tools in MVC 5 are far ahead of the scaffolding tools for ASP.NET Core. Not only are the older tools considerably faster, but they tend not to throw exceptions as often as the ASP.NET Core tooling. Hopefully, ASP.NET Core will catch up.