fsprojects / FSharp.Data.SqlClient

A set of F# Type Providers for statically typed access to MS SQL database
http://fsprojects.github.io/FSharp.Data.SqlClient/
Other
205 stars 71 forks source link

dotnet SDK projects using FSharp.Core version 7.0.0 breaks at runtime #433

Open sgryt opened 9 months ago

sgryt commented 9 months ago

Issue Summary

After migrating a .NET Framework console application to the dotnet SDK format, we are facing runtime assembly load problems for FSharp.Core.

To Reproduce

Create a console app in the SDK project format, and restrict the FSharp.Core version to 7.0.0 via a <PackageReference Update="FSharp.Core" version="7.0.0" />

Run any query generated by the library that returns data with a property having value of type 'a option.

Error

An exception with this in the stack trace:

...snipped...
Unhandled Exception: System.IO.FileLoadException: Could not load file or assembly 'FSharp.Core, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' or one of its dependencies. The located assembly's manifest definition does not match the assembly reference. (Exception from HRESULT: 0x80131040)
 at System.RuntimeTypeHandle.GetTypeByName(String name, Boolean throwOnError, Boolean ignoreCase, Boolean reflectionOnly, StackCrawlMarkHandle stackMark, IntPtr pPrivHostBinder, Boolean loadTypeFromPartialName, ObjectHandleOnStack type)
 at System.RuntimeTypeHandle.GetTypeByName(String name, Boolean throwOnError, Boolean ignoreCase, Boolean reflectionOnly, StackCrawlMark& stackMark, IntPtr pPrivHostBinder, Boolean loadTypeFromPartialName)
 at System.Type.GetType(String typeName, Boolean throwOnError)
 at FSharp.Data.SqlClient.Internals.ISqlCommand Implementation..ctor(DesignTimeConfig cfg, FSharpChoice`3 connection, Int32 commandTimeout)
...snipped...

The version of FSharp.Core in the compiled output is "7.0.0.", and if we add the following assembly binding redirect in app.config, the problem disappears:

      <dependentAssembly>
        <assemblyIdentity name="FSharp.Core" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-8.0.0.0" newVersion="7.0.0.0" />
      </dependentAssembly>

Expected behavior

The library should use the same version of FSharp.Core as the project it is used by.

What you can do

smoothdeveloper commented 9 months ago

@sgryt thanks for the report.

From what I understand, it is more likely to be a dotnet/sdk issue or (maybe) dotnet/fsharp one, there is nothing in the FSharp.Data.SqlClient nuget package that is related to deciding which FSharp.Core your console application should use, and the binding redirect handling is specific to msbuild, AFAIU.

I have had my own issues over the years of using dotnet framework, related to assembly bindings, and generally resort to using paket to handle the the consistency of those, but your mileage may vary.

I'll keep the ticket open to track the progress on resolving it, but encourage you to gather a minimum reproduce project, and opening an issue on dotnet/sdk.

Hope you'll be able to understand why this is occurring, and confirmation it reproduces (please provide a self contained zipped sample where it is minimum steps from command line to build, to make sure the sdk team can look into it).

ronnieholm commented 9 months ago

@smoothdeveloper: I'm not deeply familiar with type provider internals, but I believe to have identified the underlying cause of the exception. Please correct me if my reasoning is off.

The exception appears to be caused by a general limitation across type providers when the F# compiler is referencing a different version of FSharp.Core than the application being compiled.

Code like

type Datawarehouse = SqlProgrammabilityProvider<"name=DataWarehouse">
let checkReady = new Datawarehouse.read_json.check_if_ready_for_load(connectionString)
async {
    // Exception raised here. isReady is of type option<bool> which is important later.
    let! isReady = checkReady.AsyncExecute()
    ...
}

results in

System.IO.FileLoadException: 'Could not load file or assembly 'FSharp.Core, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' or one of its dependencies. The located assembly's manifest definition does not match the assembly reference. (Exception from HRESULT: 0x80131040)'

with a stack trace of

[Managed to Native Transition]  
mscorlib.dll!System.RuntimeTypeHandle.GetTypeByName(string name, bool throwOnError, bool ignoreCase, bool reflectionOnly, ref System.Threading.StackCrawlMark stackMark, System.IntPtr pPrivHostBinder, bool loadTypeFromPartialName)   Unknown
mscorlib.dll!System.Type.GetType(string typeName, bool throwOnError)    Unknown
FSharp.Data.SqlClient.dll!FSharp.Data.SqlClient.Internals.ISqlCommand Implementation.ISqlCommand Implementation(FSharp.Data.SqlClient.Internals.DesignTimeConfig cfg, Microsoft.FSharp.Core.FSharpChoice<string, System.Data.SqlClient.SqlConnection, System.Data.SqlClient.SqlTransaction> connection, int commandTimeout) Unknown

The offending type provider code is at https://github.com/fsprojects/FSharp.Data.SqlClient/blob/master/src/SqlClient/ISqlCommand.fs#L111:

let itemType = Type.GetType( itemTypeName, throwOnError = true)

Here itemTypeName is a "type string", baked into the application by the SqlClient.DesignTime.

De-sugaring the async computation expression above and reverse compiling the design-time generated code to C#, DesignTimeConfig (from the stack trace) is instantiated as

ISqlCommand\u0020Implementation checkReady = (ISqlCommand\u0020Implementation)new ISqlCommand\u0020Implementation(new DesignTimeConfig("SELECT read_json.check_if_ready_for_load()", false, Array.Empty<SqlParameter>(), ResultType.Records, ResultRank.ScalarValue, Mapper.GetMapperWithNullsToOptions(Dsl.Consumption.checkReady@170.@_instance, Dsl.Consumption.checkReady@170-1.@_instance), "Microsoft.FSharp.Core.FSharpOption`1[[System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], FSharp.Core, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", new Tuple<string, string>[]
            {
                new Tuple<string, string>("", "System.Boolean")
            }), FSharpChoice<string, SqlConnection, SqlTransaction>.NewChoice1Of3(item), commandTimeout);

Notice how part of the instantiation consists of FSharp.Core, Version=8.0.0.0:

"Microsoft.FSharp.Core.FSharpOption`1[[System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], FSharp.Core, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"

which is the return type from calling the type provider and the underlying source of the exception.

Digging through SqlClient.DesignTime, I came upon https://github.com/fsprojects/FSharp.Data.SqlClient/blob/master/src/SqlClient.DesignTime/DesignTime.fs#L31:

| Some x -> Expr.Value( x.ErasedTo.AssemblyQualifiedName)

where I believe x.ErasedTo is the System.Type returned by the type provider, i.e., option<bool> in this case.

The underlying issue can be illustrated by creating a .NET console application and in its project file pin FSharp.Core to a version different from the latest version installed on the machine (.NET 4.8 and .NET 8 in this case):

<PackageReference Update="FSharp.Core" version="7.0.0" />

Then include the following code:

open Microsoft.FSharp.Quotations

[<EntryPoint>]
let main _ =
    let t: System.Type = typeof<option<bool>>
    let e: Expr = Expr.Value(t.AssemblyQualifiedName)
    printfn "%A" e
    // Value ("Microsoft.FSharp.Core.FSharpOption`1[
    //   [System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], 
    //     FSharp.Core, Version=7.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")
    0

Not pinning FSharp.Core makes e become Version=8.0.0.0.

As during compilation FSharp.Data.SqlClient.DesignTime.dll is loaded into the F# compiler process, code in the design-time executes in the context of the compiler, i.e., not subject to any assembly redirect in the program being compiled. The compiler itself doesn't have an assembly redirect downgrading its version of FSharp.Core to 7.0.0.0.

When the equivalent of

let t: System.Type = typeof<option<bool>>
let e: Expr = Expr.Value(t.AssemblyQualifiedName)

evaluates inside the compiler, the result is a "type string" containing FSharp.Core, Version=8.0.0.0.

Now, the compiler is aware of the FSharp.Core version it's compiling the application against. MSBuild resolves and passes this information on the command-line when invoking the compiler:

C:\Program Files\Microsoft Visual Studio\2022\Community/Common7/IDE/CommonExtensions/Microsoft/FSharp/Tools/fscAnyCpu.exe -o:obj\Debug\net48\Test.exe
...
-r:C:\Users\tester\.nuget\packages\fsharp.core\7.0.0\lib\netstandard2.0\FSharp.Core.dll
...
Dsl.fs
Program.fs

But this information may be lost/hard to get at inside the design-time of a type provider -- at least with current type provider infrastructure.

So either (1) upgrade the application to the latest version of F# installed, (2) include an assembly redirect downgrading FSharp.Core, or (3) add a global.json pinning the .NET version.

smoothdeveloper commented 9 months ago

I believe to have identified the underlying cause of the exception. Please correct me if my reasoning is off.

I'm unlikely to "correct", but share my understanding (which can be misguided, as this is either a fsharp or TP SDK issue, not an dotnet/sdk one), and your analysis seems thorough (based on looking up IL types emitted by the compiler, and investigating the quotations in the type provider implementation).

I believed the binding redirects were not needed anymore but in .NET framework, but it sounds I was too hopeful.

The analysis you make about the TP in the compiler is correct, I'm not sure if the newer versions of the Type Provider SDK (we are on one of the early that supported .net core, sdk v2 era) help solving the issue, nor if the type provider implementation should define things for the TP SDK to do the relinking, I only know any time I tried to upgrade the TP SDK, it was a bit though, or I couldn't make it work (but also due to sdk v2 in the master branch and not enough know how).

Let's keep the issue open (others will find useful information, and learn about binding redirect when they have same error), and when I'm able to update Type Provider SDK while keeping everything green on CI, we should give it another look, in case there is some form of detection between runtime references versus what the compiler binds against.

Thanks @ronnieholm for giving a shot at reproducing @sgryt issue, and all the analysis.

For now, I effectively can't solve those use cases, and only encourage people to closely care for their binding redirects, msbuild or paket can be used to deal with it in ways which aren't 100% manual.