dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
14.55k stars 4.54k forks source link

Conditional Compilation and XmlSerialization #103880

Open robertmuehsig opened 1 week ago

robertmuehsig commented 1 week ago

Description

Hi everyone, I'm not sure if this is the right repo or not, but I have an issue porting an application from .NET 6.0 to .NET 8.0. I'm not even sure if the "root" problem has something to do with the XmlSerializer or not.

Our scenario:

Our solution contains two main applications:

Both applications share some common types and the actual solution is "large" (~100 Projects, multiple web apps (e.g. WebApi, Admin App etc.)).

We try to stay up to date and want to use new languages features, that's why we have a project that contains "BCL-Types" like this:

#if !NET7_0_OR_GREATER

namespace System.Runtime.CompilerServices
{
    /// <summary>Specifies that a type has required members or that a member is required.</summary>
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
// #if SYSTEM_PRIVATE_CORELIB
    public
// #else
//     internal
// #endif
        sealed class RequiredMemberAttribute : Attribute
    { }
}

#endif

This way we can use something like this in the Common area and don't bother if it is used from .NET 4.8 or not:

public required string Name { get; set; }

We use XmlSerialization for configuration stuff in our application and during the .NET 8 migration we found this RunTime Exception:

Could not load type 'System.Runtime.CompilerServices.RequiredMemberAttribute' from assembly 'BclTest.BclTypes, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'.

Details

``` System.TypeLoadException HResult=0x80131522 Message=Could not load type 'System.Runtime.CompilerServices.RequiredMemberAttribute' from assembly 'BclTest.BclTypes, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'. Source=System.Private.CoreLib StackTrace: at System.ModuleHandle.ResolveType(QCallModule module, Int32 typeToken, IntPtr* typeInstArgs, Int32 typeInstCount, IntPtr* methodInstArgs, Int32 methodInstCount, ObjectHandleOnStack type) at System.ModuleHandle.ResolveTypeHandle(Int32 typeToken, RuntimeTypeHandle[] typeInstantiationContext, RuntimeTypeHandle[] methodInstantiationContext) at System.Reflection.RuntimeModule.ResolveType(Int32 metadataToken, Type[] genericTypeArguments, Type[] genericMethodArguments) at System.Reflection.CustomAttribute.FilterCustomAttributeRecord(MetadataToken caCtorToken, MetadataImport& scope, RuntimeModule decoratedModule, MetadataToken decoratedToken, RuntimeType attributeFilterType, Boolean mustBeInheritable, ListBuilder`1& derivedAttributes, RuntimeType& attributeType, IRuntimeMethodInfo& ctorWithParameters, Boolean& isVarArg) at System.Reflection.CustomAttribute.AddCustomAttributes(ListBuilder`1& attributes, RuntimeModule decoratedModule, Int32 decoratedMetadataToken, RuntimeType attributeFilterType, Boolean mustBeInheritable, ListBuilder`1 derivedAttributes) at System.Reflection.CustomAttribute.GetCustomAttributes(RuntimeModule decoratedModule, Int32 decoratedMetadataToken, Int32 pcaCount, RuntimeType attributeFilterType) at System.Reflection.CustomAttribute.GetCustomAttributes(RuntimeType type, RuntimeType caType, Boolean inherit) at System.Xml.Serialization.TempAssembly.LoadGeneratedAssembly(Type type, String defaultNamespace, XmlSerializerImplementation& contract) at System.Xml.Serialization.XmlSerializer..ctor(Type type, String defaultNamespace) at CommonTypes.Person.DeserializeFromXml(String xml) in C:\Users\muehsig\source\repos\BclTest.BclTypes\CommonTypes\Person.cs:line 13 at TestProject1.UnitTest1.TestMethod1() in C:\Users\muehsig\source\repos\BclTest.BclTypes\TestProject1\UnitTest1.cs:line 33 at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor) at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr) ```

It seems, that the XmlSerializer uses the "wrong" type during runtime.

Reproduction Steps

I made a small test repo here.

The smallest repo would be something like this:

- Common Lib -> TargetFramework "netstandard2.0"
- .NET 4.8 Console App -> TargetFramework "48"
- .NET 8 Console App -> TargetFramework "net8.0"
- BCL-Type Lib -> TargetFramework "netstandard2.0,net8.0"
- Unit Test Project -> TargetFramework "net8.0"

In the Common Lib is a class like this:

    public class Person
    {
        public required string Name { get; set; }

        public static Person DeserializeFromXml(string xml)
        {
            XmlSerializer serializer = new XmlSerializer(typeof(Person));
            using (StringReader reader = new StringReader(xml))
            {
                return (Person)serializer.Deserialize(reader);
            }
        }
    }

In the .NET 8 Console App is a class like this:

    internal class FoobarJob
    {
        public void Run([NotNullWhen(returnValue: false)] out string? errorMessage)
        {
            errorMessage = null;
            Console.WriteLine("Running FoobarJob");
        }
    }

In the Unit Test we have this:

    [TestClass]
    public class UnitTest1
    {
        [TestMethod]
        public void TestMethod1()
        {
            // works
            Person person = new Person() { Name = "Test" };

            string xml = @"<Person><Name>John Doe</Name></Person>";
            Person personX = Person.DeserializeFromXml(xml);
            Console.WriteLine(personX.Name);
        }
    }

Expected behavior

The Common type can be consumed from the .NET 4.8 client and the .NET 8 application and we can use modern language features in the .NET 8 application.

Actual behavior

With the TargetFrameworks in the BCL class like this "netstandard2.0,net8.0" the runtime error occures:

Could not load type 'System.Runtime.CompilerServices.RequiredMemberAttribute' from assembly 'BclTest.BclTypes, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'.

I thought, that the TargetFramework might be "wrong" and I just use "netstandard2.0" in the BCL-Type lib, but then I get this compiler error:

FoobarJob.cs(12,26,12,37): error CS0433: The type 'NotNullWhenAttribute' exists in both 'BclTest.BclTypes, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' and 'System.Runtime, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'
3>Done building project "NetEightApp.csproj" -- FAILED.

Regression?

If I use "net6.0" instead of "net8.0" the issue is resolved, but the reason for this is simply this condition: #if !NET7_0_OR_GREATER

Known Workarounds

No response

Configuration

.NET Framework 4.8 & .NET 8

Other information

No response

huoyaoyuan commented 1 week ago

This is because binary compatibility is broken between target frameworks.

To achieve compatibility, you need to redirect the custom defined type to BCL defined type on higher versions, like this:

#if !NET7_0_OR_GREATER
// definition of RequiredMemberAttribute
#else
[TypeForwardedTo(typeof(RequiredMemberAttribute))]
#endif
robertmuehsig commented 1 week ago

@huoyaoyuan Thats a good hint, but

#if !NET7_0_OR_GREATER
namespace System.Runtime.CompilerServices
{
    ...
}
#else
[TypeForwardedTo(typeof(System.Runtime.CompilerServices.RequiredMemberAttribute))]
#endif

Results in error CS0116: A namespace cannot directly contain members such as fields, methods or statements

huoyaoyuan commented 1 week ago

Are you typing correctly? The content under #if !NET7_0_OR_GREATER should behave as-is.

robertmuehsig commented 1 week ago

Ok, this compiles:

#if !NET7_0_OR_GREATER

namespace System.Runtime.CompilerServices
{
    /// <summary>Specifies that a type has required members or that a member is required.</summary>
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
// #if SYSTEM_PRIVATE_CORELIB
    public
// #else
//     internal
// #endif
        sealed class RequiredMemberAttribute : Attribute
    { }
}
#else
[assembly:System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Runtime.CompilerServices.RequiredMemberAttribute))]
#endif

But the runtime error still appears:

System.TypeLoadException
  HResult=0x80131522
  Message=Could not load type 'System.Runtime.CompilerServices.CompilerFeatureRequiredAttribute' from assembly 'BclTest.BclTypes, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'.
  Source=System.Private.CoreLib
  ...
robertmuehsig commented 1 week ago

Found a workaround, but I'm still confused why.

If I add net8.0 in the TargetFrameworks of the Common Lib, then it works:

CommonTypes.csproj:

<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>

The TypeForwarding is not needed in this case.

But I wonder if this is by design or not?

huoyaoyuan commented 1 week ago

If I add net8.0 in the TargetFrameworks of the Common Lib, then it works:

In this case, the net8.0 version of unit test will use net8.0 version of Common, which uses the BCL version of attributes. It's expected to work in this case.