ASP.NET Core 2.1.0-preview1: Introducing compatibility version in MVC

This post was written by Ryan Nowak

In 2.1 we’re adding a feature to address a long-standing problem for maintaining MVC – how do we make improvements to framework code without making it too hard for developers to upgrade to the latest version? This is not an easy concern to solve – and with 7 major releases of MVC (dating back to 2009) there are a few things we’d like to leave in the past.

Unlike most other parts of ASP.NET Core, MVC is a framework – our code calls your code in lots of idiosyncratic ways. If we change what methods we call or in what order, or how we handle exceptions – it’s very easy for working code to become non-working code. In our experience, it’s also just not good enough for the team to just expect developers to rely on the documented behavior and punish those who don’t.

This last bit is summed up with Hyrum’s Law, or if you prefer, the XKCD version. We make decisions with the assumption that some developers have built working applications that rely on our bugs.

Despite these challenges, we think it’s worthwhile to keep moving forward. We’re disappointed too when we get a good piece of feedback that we can’t act upon because it’s incompatible with our legacy behavior.

What we’re doing

Our plan is to continue to make improvements to framework behaviors – where we think we’ve made a mistake – or where we can update a feature to be unequivocally better. However, we’re going to make these changes opt-in, and make it easy to opt-in. New applications created from our templates will opt-in to the current release’s behaviors by default.

When we reach the next major release (3.0 – not any time soon) – we will remove the old behaviors.

Opt-in means that updating your package references don’t give you different behavior. You have to choose the new version and the new behavior.

Right now this looks like:

OR

What this means

I think this does a few things that are valuable. Consider all of the below as goals or success criteria. We still have to do a good job understanding your feedback and communicating for these things to happen.

For you and for us: We can continue to invest in new ideas and adapt to a changing web landscape.

For you: It’s easy to adopt new versions in small steps.

For us: Streamlines things that require a lot of effort to support, document, and respond to feedback.

For us: Simplifies the decision process of how to make and communicate a change.

What we’re not doing

While we’re giving you fine-grained control over which new behaviors you get, we don’t intend on keeping old things forever. This is not a license to live in the past. As stated above, our plan is to update things that are broken and keep moving forward by removing old behaviors over time.

We’re also not treating this new capability as *open-season* on breaking changes. Making any change that impacts developers on our platform has to be justified in providing enough value, and needs to be comprehensible and actionable by those that are impacted – because we expect all developers to deal with it eventually.

A good candidate change is one that:

  • adds a feature, but with a small break risk for a minority of users (areas for Razor Pages)
  • fixes a big problem, but with a comprehensible impact (exception handling for input formatters)
  • never worked the way we thought (bug), and streamlines something complicated (combining authorization filters)

Note that in all of the cases above, the new behaviors are easier for us to explain and document. We would recommend that everyone choose the new behaviors, it’s not a matter of preference.

Give us feedback about this. If you think this plan leaves you out in the cold, let us know how and why.

What’s happening now?

Most of the known work for us has already happened. Have made about 5 design changes to features inside MVC during the 2.1 milestone that deserved a compatibility switch.

You can find a summary of these things here below. My hope is that the documentation added to the specific options and types explains what is changing when you opt-in to each setting and why we feel it’s important.

General MVC

Combine Authorization Filters

Smarter exception handling for formatters

Smarter validation for enums

Allow non-string types with HeaderModelBinder (2.1.0-preview-2)

JSON Formatter

Better error messages

Razor Pages

Areas for Pages

Appendix A: an auspicious example

I think exception handling for input formatters is probably the best illustrative example of how this philosophy works.

The best starting place is probably to look at the docs that I added in this PR. We have a problem in the 1.X and 2.0 family of MVC releases where any exception thrown by an IInputFormatter will be swallowed by the infrastructure and turned into a model state error. This includes TypeLoadException, NullReferenceException, ThreadAbortException and all other kinds of esoterica.

This is the case because we didn’t have an exception type that says “I failed to process the input, report an error to the client”. We added this in 2.1 and we’ve updated our formatters to use it in the appropriate cases (the XML serializers throw exceptions). However this can’t help formatters we didn’t write.

This leads to the need for a switch. If you need to use a formatter written against 1.0 that throws an exception and expects MVC to handle it, that will still work until you opt-in to the new behavior. We do plan on removing the old way in 3.0, but this eases the pressure – instead of this problem blocking you from adopting 2.1, you have time to figure out a solution before 3.0 (a long time away).

——

I hope this example provides a little insight into what our process is like. See the relevant links for the in-code documentation about the other changes. We are looking forward to feedback on this, either on GitHub or as comments on this post.