5

I am writing a validation tool that checks the versions of files referenced in a project. I want to use the same resolution process that MSBuild uses.

For example, Assembly.Load(..) requires a fully-qualified assembly name. However, in the project file, we may only have something like "System.Xml". MSBuild probably uses the project's target framework version and some other heuristics to decide which version of System.Xml to load.

How would you go about mimicking (or directly using) msbuild's assembly resolution process?

In other words, at run-time, I want to take the string "System.Xml", along with other info found in a .csproj file and find the same file that msbuild would find.

Neil
  • 7,227
  • 5
  • 42
  • 43

7 Answers7

3

I had this problem today, and I found this old blog post on how to do it:

http://blogs.msdn.com/b/jomo_fisher/archive/2008/05/22/programmatically-resolve-assembly-name-to-full-path-the-same-way-msbuild-does.aspx

I tried it out, works great! I modified the code to find 4.5.1 versions of assemblies when possible, this is what I have now:

#if INTERACTIVE
#r "Microsoft.Build.Engine" 
#r "Microsoft.Build.Framework"
#r "Microsoft.Build.Tasks.v4.0"
#r "Microsoft.Build.Utilities.v4.0"
#endif

open System
open System.Reflection
open Microsoft.Build.Tasks
open Microsoft.Build.Utilities
open Microsoft.Build.Framework
open Microsoft.Build.BuildEngine

/// Reference resolution results. All paths are fully qualified.
type ResolutionResults = {
    referencePaths:string array
    referenceDependencyPaths:string array
    relatedPaths:string array
    referenceSatellitePaths:string array
    referenceScatterPaths:string array
    referenceCopyLocalPaths:string array
    suggestedBindingRedirects:string array
    }


let resolve (references:string array, outputDirectory:string) =
    let x = { new IBuildEngine with
                member be.BuildProjectFile(projectFileName, targetNames, globalProperties, targetOutputs) = true
                member be.LogCustomEvent(e) = ()
                member be.LogErrorEvent(e) = ()
                member be.LogMessageEvent(e) = ()
                member be.LogWarningEvent(e) = ()
                member be.ColumnNumberOfTaskNode with get() = 1
                member be.ContinueOnError with get() = true
                member be.LineNumberOfTaskNode with get() = 1
                member be.ProjectFileOfTaskNode with get() = "" }

    let rar = new ResolveAssemblyReference()
    rar.BuildEngine <- x
    rar.IgnoreVersionForFrameworkReferences <- true
    rar.TargetFrameworkVersion <- "v4.5.1"
    rar.TargetedRuntimeVersion <- "v4.5.1"
    rar.TargetFrameworkDirectories <- [||] //[|@"C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.5\"|]
    rar.Assemblies <- [|for r in references -> new Microsoft.Build.Utilities.TaskItem(r) :> ITaskItem|]
    rar.AutoUnify <- true
    rar.SearchPaths <- [| "{CandidateAssemblyFiles}"
                          "{HintPathFromItem}"
                          "{TargetFrameworkDirectory}"
                         // "{Registry:Software\Microsoft\.NetFramework,v3.5,AssemblyFoldersEx}"
                          "{AssemblyFolders}"
                          "{GAC}"
                          "{RawFileName}"
                          outputDirectory |]

    rar.AllowedAssemblyExtensions <- [| ".exe"; ".dll" |]
    rar.TargetProcessorArchitecture <- "x86"
    if not (rar.Execute()) then
        failwith "Could not resolve"
    {
        referencePaths = [| for p in rar.ResolvedFiles -> p.ItemSpec |]
        referenceDependencyPaths = [| for p in rar.ResolvedDependencyFiles -> p.ItemSpec |]
        relatedPaths = [| for p in rar.RelatedFiles -> p.ItemSpec |]
        referenceSatellitePaths = [| for p in rar.SatelliteFiles -> p.ItemSpec |]
        referenceScatterPaths = [| for p in rar.ScatterFiles -> p.ItemSpec |]
        referenceCopyLocalPaths = [| for p in rar.CopyLocalFiles -> p.ItemSpec |]
        suggestedBindingRedirects = [| for p in rar.SuggestedRedirects -> p.ItemSpec |]
    }



[<EntryPoint>]
let main argv = 
    try
      let s = resolve([| "System"
                         "System.Data"
                         "System.Core, Version=4.0.0.0"
                         "Microsoft.SqlServer.Replication" |], "")
      printfn "%A" s.referencePaths
    finally
      ignore (System.Console.ReadKey())

    0
Robert Jeppesen
  • 7,837
  • 3
  • 35
  • 50
2

If you target the Framework version you want to be compatible with instead of targeting 3.5, Visual Studio 2008 SP1 and FxCop 1.36 RTM added rule CA 1903: Use only API from targeted framework to ensure you stay compatible with the target framework version. Turning that rule on and treating it as an error will fail your Build and provide the behavior you want.

Here is sample code demonstrating a violation when you are targeting framework version 2:

using System.Runtime;

class Program
{
    static void Main()
    {
        GCSettings.LatencyMode = GCLatencyMode.LowLatency;
    }
}
David Silva Smith
  • 11,498
  • 11
  • 67
  • 91
2

This should show you how to do what you really want, but I think you should use the FXCop answer I provided.

static void Main()
    {
        string targetFile = @"test.csproj";
        XDocument xmlDoc = XDocument.Load(targetFile);
        XNamespace ns = "http://schemas.microsoft.com/developer/msbuild/2003";

        var references = from reference in xmlDoc.Descendants(ns + "ItemGroup").Descendants(ns + "Reference")
                         select reference.Attribute("Include").Value;

        foreach (var reference in references)
        {
            Assembly.LoadWithPartialName(reference);
        }

        foreach (var item in AppDomain.CurrentDomain.GetAssemblies())
        {
            var assemblyVersion = ((AssemblyFileVersionAttribute)item.GetCustomAttributes(typeof(AssemblyFileVersionAttribute), true)[0]).Version.ToString();
            Console.WriteLine("\r\nFullname:\t{0}\r\nFileVersion:\t{1}", item.FullName, assemblyVersion);

        }
        Console.WriteLine("\r\nPress any key to continue");
        Console.ReadKey();
    }
David Silva Smith
  • 11,498
  • 11
  • 67
  • 91
2

Why not just call msbuild against your project or solution file, pass it the /v:d extension, and parse the output file for the information you want? For instance, you'll see something like the following for each assembly resolution:

  Primary reference "System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089".
      Resolved file path is "c:\WINNT\Microsoft.NET\Framework\v2.0.50727\System.Data.dll".
      Reference found at search path location "{TargetFrameworkDirectory}".
          For SearchPath "{TargetFrameworkDirectory}".
          Considered "C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.5\System.Data.exe", but it didn't exist.
          Considered "C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.5\System.Data.dll", but it didn't exist.
          Considered "C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\System.Data.exe", but it didn't exist.
          Considered "C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\System.Data.dll", but it didn't exist.
          Considered "c:\WINNT\Microsoft.NET\Framework\v3.5\System.Data.exe", but it didn't exist.
          Considered "c:\WINNT\Microsoft.NET\Framework\v3.5\System.Data.dll", but it didn't exist.
          Considered "c:\WINNT\Microsoft.NET\Framework\v3.0\System.Data.exe", but it didn't exist.
          Considered "c:\WINNT\Microsoft.NET\Framework\v3.0\System.Data.dll", but it didn't exist.
          Considered "c:\WINNT\Microsoft.NET\Framework\v2.0.50727\System.Data.exe", but it didn't exist.
      This reference is not "CopyLocal" because it's a prerequisite file.

Alternatively, MSBuild delegates the task of resolving assemblies to the Microsoft.Build.Tasks.ResolveAssemblyReference class from the Microsoft.Build.Tasks.v3.5 assembly (in my case, building against the 3.5 framework). You can parse the project file and supply an instance of ResolveAssemblyReference with the appropriate (meta)data, and let it perform the resolution for you - seems perfect, since that's exactly what MSBuild does.

Dathan
  • 7,266
  • 3
  • 27
  • 46
1

If you get yourself a free copy of Reflector, you can examine the internals of the MSBuild.exe file itself. I notice there is a class

Microsoft.Build.Shared.TypeLoader

that has a method called

internal LoadedType Load(string typeName, AssemblyLoadInfo assembly);

which may help?

Anyway, with reflector you can get the code, and hopefully reuse the system directly.

Steve Cooper
  • 20,542
  • 15
  • 71
  • 88
0

To directly mimic the CLR resolution process you could write a custom MSBuild task although I don't see what it would achieve.

MSBuild doesn't resolve assemblies. They are resolved by the CLR. This article describes how the runtime resolves assemblies: http://msdn.microsoft.com/en-us/library/yx7xezcf.aspx

When you are in Visual Studio the System assemblies come from the filesystem, but when they are loaded at runtime they come from the GAC. http://p3net.mvps.org/Topics/Basics/IntegratingGACWithVS.aspx

If you still have questions please clarify.

David Silva Smith
  • 11,498
  • 11
  • 67
  • 91
  • I am interested in where assemblies come from at compile time. At run-time, I want to look at the XML for a reference whose Version attribute is "System.Xml" and find the same assembly that msbuild finds. – Neil Jun 26 '09 at 19:34
  • Thank you for trying to answer. Unfortunately, neither of those are run-time solutions. – Neil Jun 30 '09 at 22:49
  • What exactly are you trying to do? I can probably do it if I understood. Are you trying to parse a .csproj for system assemblies, determine their paths, and then get the assembly version? Are you trying to do this with non-system assemblies? All the System assemblies will be loaded from the Global Assembly Cache at runtime, but from a different location at compile time so this could be an important distinction. – David Silva Smith Jul 01 '09 at 15:24
  • Yes. I want to parse a .csproj file, locate the assembly files, and get their versions. I am only concerned with system assemblies right now, but others might be useful in the future. – Neil Jul 01 '09 at 15:48
  • A little more detail: I have a set of validation routines that I run against csproj files. I want to add one that checks the versions of referenced assemblies. For reasons having to do with keeping our options open (which I don't necessarily agree with), it was decided that even though our projects will target the .NET framework version 3.5, none of our assembly references should be to files above version 3.3 (yet). – Neil Jul 01 '09 at 15:54
0

This might help: Resolving Binary References in MSBuild

Alex Yakunin
  • 6,330
  • 3
  • 33
  • 52