.NET Standard is great; .NET Standard is an interface that allows you to write libraries once and consume them from different applications on different .NET platforms, including ones that don't even exist yet.
Standalone libraries are often straightforward to upgrade to .NET Standard, but what about libraries with dependencies? In particular, libraries with dependencies on technologies that target frameworks other than .NET Standard.
I received a question by email from R. J. Lewis III, who has an ASP.NET MVC 5 project and associated library that target .NET Framework v4.6.2. R. J. wants to upgrade the library to target .NET Standard 2.0, but is unsure how to proceed as "the library needs the ModelStateDictionary
and ModelState
classes" to produce some JSON outputs.
In this post, I answer R. J.'s question and provide guidance on how to upgrade ASP.NET libraries to .NET Standard.
Five Different Strategies
Here are the 5 different strategies that I have used to successfully upgrade libraries with dependencies in different situations.
- Use Compatibility Mode
- Partition the Library
- Use Abstractions
- Don't Upgrade
- Rewrite the Library
While some of this guidance is specific to ASP.NET and .NET Standard, much of it is more general and relevant to generic libraries and arbitrary dependencies.
Strategy 1: Use Compatibility Mode
One great feature of .NET Standard 2.0 is compatibility mode, which allows you to reference .NET Framework libraries from .NET Standard projects.
Provided that the library does not invoke any functionality that is beyond what is available in .NET Standard 2.0, compatibility mode will work.
Example
To use ModelState
and ModelStateDictionary
in a .NET Standard 2.0 library, R. J. could simply add a reference to the NuGet package Microsoft.AspNet.Mvc
, which includes System.Web.Mvc
. Doing so invokes compatibility mode and produces the following warning:
Package 'Microsoft.AspNet.Mvc 5.2.3' was restored using '.NETFramework,Version=v4.6.1' instead of the project target framework '.NETStandard,Version=v2.0'. This package may not be fully compatible with your project.
From my limited experiments, this worked for me, so R. J. should be able to read from the ModelState
and ModelStateDictionary
classes and produce the desired JSON output.
When to Use
You can use compatibility mode as an interim solution if only supported .NET Standard 2.0 APIs are used. You should plan to use one of the other four strategies as a more permanent solution.
Strategy 2: Partition the Library
Libraries often end up with disparate features, with some used broadly across different applications and others used more narrowly.
If only a few features have dependencies beyond the scope of .NET Standard, a good strategy is to split the library in two. One containing the features beyond the scope of .NET Standard and the other with the remaining features.
Example
In R. J.'s case, the feature that turns the ModelState
into JSON could be split from the rest of the library.
If the library is reused in non ASP.NET MVC projects, they would not be using this feature anyway, as it is ASP.NET MVC specific.
If the library were to be reused in an ASP.NET Core project, a third library could be created to implement an ASP.NET Core specific version that turned its ModelState
into JSON.
When to Use
You should partition the library when only a small percentage of the library's functionality is dependent on features outside the scope of .NET Standard.
Strategy 3: Use Abstractions
If the library's ASP.NET dependencies are used liberally throughout the library, a good strategy is for the library to use its own abstractions.
It is possible to model these abstractions in the library using classes, but generally, I find it is better to use interfaces.
Interfaces give the consuming application a lot more flexibility of interaction with the library, as the consuming application owns the implementation and controls creation.
Example
Instead of taking a dependency on ModelState
and ModelStateDictionary
, the library can define one or more abstractions.
In R. J.'s case, I would define an interface, IValidationState
, containing only what is necessary for producing the JSON output. This interface would be part of and owned by the library.
I would then create an adapter (see Adapter Pattern) to transform the ModelState
and ModelStateDictionary
into IValidationState
. This adapter belongs in the application.
Finally, the library would need to be refactored to use the interface and the application would need to be refactored to use the adapter.
When to Use
You should favour using abstractions over dependencies in all your libraries; this enables consuming applications to control and implement the dependencies as they see fit. This leads to simpler and more reusable code.
You should especially favour abstractions over dependencies when you use only a very small part of a dependency's functionality. For example, taking a dependency on all of ASP.NET MVC for just ModelState
.
Strategy 4: Don't Upgrade
If your library is deeply reliant on .NET Framework features or libraries such as Entity Framework or ASP.NET, it may not be practical to upgrade your library to .NET Standard.
In this situation, the best solution may be to leave the library targeting .NET Framework.
Example
In R. J.'s case, it sounds like Entity Framework is used extensively. Currently, Entity Framework 6 does not support .NET Standard 2.0.
Therefore, it may simply not be practical to upgrade to .NET Standard, unless Entity Framework 6 can be upgraded to Entity Framework Core, which supports .NET Standard.
When to Use
If your library is only used in a single project that targets a single platform and there is no desire to use it in other projects or on different platforms, then there is no need to upgrade.
Strategy 5: Rewrite the Library
If you need the functionality of the library in newer .NET Core applications, you can write a new library targeting .NET Standard that provides the same features, but which is not reliant on .NET Framework features or libraries.
When writing the new .NET Standard library, you can use abstractions as in strategy three to make the library more reusable and more compatible with different applications and frameworks.
Once the new .NET Standard library has been written, you can then update the old applications that are still using your old library to use the new .NET Standard library.
When to Use
When the existing library is tightly coupled to an existing framework, but the functionality of the library needs to be used more widely in the future.
Conclusion
You have seen five different strategies for upgrading ASP.NET libraries to target .NET Standard, many of which apply to generic libraries and arbitrary dependencies.
You have also seen when to use each strategy and examples of how they can be applied to R. J.'s situation with ModelState
and ModelStateDictionary
.
I use abstractions (strategy three) in all my libraries to avoid these issues all together. However, in legacy libraries and when tight coupling is necessary, the other strategies work well.