You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
PR #71 added nullable reference type annotations. This required switching VersOne.Epub project to C# 10 and adding an explicit C# version override in the csproj file:
This approach worked with a few caveats described in the issue #65.
Making some properties of a class non-nullable means that such class cannot have the default constructor because C# compiler requires all non-nullable members to be initialized when an instance of the class is created. The solution to this problem is to create a custom constructor and pass the initialization data through the constructor parameters.
This in turn caused an issue with integration tests which rely on JSON deserialization to load the test data. JSON deserializer can't set the property values if they don't have a setter, and doesn't know how to invoke the custom constructor. JSON.NET is able to use custom constructors, but only when the number of properties is equal to the number of constructor parameters and they have matching names. This and some other limitations described in the issue #77 made it unusable for the integration tests. The solution was to switch from Json.NET to System.Text.Json and implement a custom object deserializer that would find the most suitable class constructor and use it to initialize non-writable object properties (see ObjectDeserializer.cs file in PR #78 for reference).
This approach works well for integration tests, but doesn't work when this custom deserialization cannot be reused. One of such scenarios is running unit tests from Visual Studio Test Explorer window. xUnit allows unit tests to be marked with the [Theory] attribute which will cause that test executed multiple times, once for every set of test data:
Visual Studio Test Explorer will shows these test executions as separate test cases grouped under one test, will show the statuses (passed / failed) for every test case, and will let the user to restart individual test cases. The only restriction is to have all test data to be serializable. xUnit will serialize primitive types like int or string automatically, but it will skip all other data types. If xUnit is not able to serialize test data for any of the test cases, it will switch to the single-test mode which will cause Visual Studio Test Explorer to show all test cases as a single test in its window. Additionally, a failure in one test case causes the whole test fail, and there is no way to restart individual test cases. xUnit allows to supply custom serializers and deserializers for the test data, but using the same custom serializer implemented for the integration tests would be too cumbersome in this case.
A simpler solution to all these problems would be to add a default parameterless constructor, mark the properties as required, and add an init setter. This will solve the deserialization issue while retaining the nullable reference type information. Unfortunately, the init keyword was added only in C# 9 and required was added in C# 11. While it is possible to add an explicit C# version 11 override, like it was done before with C# 10, the resulting code will not compile because these features require certain types that exist only in newer .NET versions (init uses IsExternalInit class added in .NET 5 and required uses RequiredMember attribute from .NET 7, among other auxiliary types from .NET 7). One workaround is to copy these classes directly into VersOne.Epub project, but even then the consumers of the Nuget package will not be able to use the init-only properties, if they are not targeting .NET 7 or not using C# 11.
This means that in order to use required and init properties in VersOne.Epub project, the following changes need to be made to the supported .NET versions (see C# language versioning for more details):
.NET Framework support needs to be dropped, as the latest .NET Framework 4.8.1 supports only C# 7.3.
.NET Standard 1.0 and 2.0 support needs to be dropped, since even the latest .NET Standard 2.1 supports only C# 8.
.NET 7 needs to be made the minimum supported version as it's the first .NET version to support C# 11.
A quick search among the VersOne.Epub consumers shows that most of them will not be affected, but there are still a few projects that target older versions of .NET (mostly, .NET 6) or .NET Framework.
One additional benefit of adding default constructors and marking properties as required and init-only is that the VersOne.Epub consumers will be able to serialize and deserialize VersOne.Epub.Schema types without any issues.
Proposed solution
Mention in the release notes that the 3.x version branches will be the last ones that support .NET Framework and .NET Standard.
Change the minimum supported version to .NET 7 and increase the major version of the project (which will become 4.0).
Migrate VersOne.Epub.WpfDemo project to .NET 9.
Continue backporting new features and bug fixes into 3.x version branches until January 2026 to give the application developers time to migrate their projects to a newer version of .NET.
The text was updated successfully, but these errors were encountered:
Description
PR #71 added nullable reference type annotations. This required switching
VersOne.Epub
project to C# 10 and adding an explicit C# version override in the csproj file:EpubReader/Source/VersOne.Epub/VersOne.Epub.csproj
Lines 12 to 13 in 6bc8abd
This approach worked with a few caveats described in the issue #65.
Making some properties of a class non-nullable means that such class cannot have the default constructor because C# compiler requires all non-nullable members to be initialized when an instance of the class is created. The solution to this problem is to create a custom constructor and pass the initialization data through the constructor parameters.
This in turn caused an issue with integration tests which rely on JSON deserialization to load the test data. JSON deserializer can't set the property values if they don't have a setter, and doesn't know how to invoke the custom constructor. JSON.NET is able to use custom constructors, but only when the number of properties is equal to the number of constructor parameters and they have matching names. This and some other limitations described in the issue #77 made it unusable for the integration tests. The solution was to switch from Json.NET to System.Text.Json and implement a custom object deserializer that would find the most suitable class constructor and use it to initialize non-writable object properties (see
ObjectDeserializer.cs
file in PR #78 for reference).This approach works well for integration tests, but doesn't work when this custom deserialization cannot be reused. One of such scenarios is running unit tests from Visual Studio Test Explorer window. xUnit allows unit tests to be marked with the
[Theory]
attribute which will cause that test executed multiple times, once for every set of test data:EpubReader/Source/VersOne.Epub.Test/Unit/Utils/VersionUtilsTests.cs
Lines 8 to 13 in 6bc8abd
Visual Studio Test Explorer will shows these test executions as separate test cases grouped under one test, will show the statuses (passed / failed) for every test case, and will let the user to restart individual test cases. The only restriction is to have all test data to be serializable. xUnit will serialize primitive types like
int
orstring
automatically, but it will skip all other data types. If xUnit is not able to serialize test data for any of the test cases, it will switch to the single-test mode which will cause Visual Studio Test Explorer to show all test cases as a single test in its window. Additionally, a failure in one test case causes the whole test fail, and there is no way to restart individual test cases. xUnit allows to supply custom serializers and deserializers for the test data, but using the same custom serializer implemented for the integration tests would be too cumbersome in this case.A simpler solution to all these problems would be to add a default parameterless constructor, mark the properties as
required
, and add aninit
setter. This will solve the deserialization issue while retaining the nullable reference type information. Unfortunately, theinit
keyword was added only in C# 9 andrequired
was added in C# 11. While it is possible to add an explicit C# version 11 override, like it was done before with C# 10, the resulting code will not compile because these features require certain types that exist only in newer .NET versions (init
usesIsExternalInit
class added in .NET 5 andrequired
usesRequiredMember
attribute from .NET 7, among other auxiliary types from .NET 7). One workaround is to copy these classes directly intoVersOne.Epub
project, but even then the consumers of the Nuget package will not be able to use the init-only properties, if they are not targeting .NET 7 or not using C# 11.This means that in order to use
required
andinit
properties inVersOne.Epub
project, the following changes need to be made to the supported .NET versions (see C# language versioning for more details):A quick search among the
VersOne.Epub
consumers shows that most of them will not be affected, but there are still a few projects that target older versions of .NET (mostly, .NET 6) or .NET Framework.One additional benefit of adding default constructors and marking properties as
required
andinit
-only is that theVersOne.Epub
consumers will be able to serialize and deserializeVersOne.Epub.Schema
types without any issues.Proposed solution
VersOne.Epub.WpfDemo
project to .NET 9.The text was updated successfully, but these errors were encountered: