loading multiple.net assemblies with same AssemblyVersion from a plugin - c#

I'm in dll hell.
I'm building a plugin for a huge, ancient and very powerful software suite called ANSYS. They have a plugin framework. I had hoped that they would magically handle everything for me via AssemblyContexts or AppDomains or some other clever dotnet device that I don't understand. They do not.
The result is that I've created an application that depends on GRPC.core 1.16.0 via nuget. I wrote a little application that drives my plugin with a winform host. It loads and works perfectly, finding my library in ~/myproject/bin/debug/grpc.core.1.1.16.dll that exists right beside the class-library that is my plugin, no problem.
When I run my plugin in the ANSYS process space, which happens to also depend on grpc 1.0.0.0, the linker finds C:\Program FIles\ANSYS\...\WIN64\grpc.core.dll. No Good.
One odd thing about the Nuget GRPC package is that it adds a reference with a "reference version" of 1.0.0.0, where most other nuget packages have their reference version match the nuget package version. If i manually change the reference version the compiler wont find the library.
<Reference Include="Grpc.Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=d754f35622e28bad">
<HintPath>..\packages\Grpc.Core.1.16.1\lib\net45\Grpc.Core.dll</HintPath>
</Reference>
edit: the key is in the above line. The Nuget published Grpc.core artifact is at AssemblyInformationVersion=1.16.1.0, AssemblyFileVersion=1.16.1.0, AssemblyVersion=1.0.0.0. I logged this as a request against GRPC. More Below.
Thus I need to tell the runtime linking facilities not to use grpc.core...dll found in ANSYS's own binary directoryWhats more, there is exactly one dll (and its dependents) that I wish to load from my parent processes context: and that's ANSYS API dlls themselves, which are probably already in the GAC. In my project I've included this as a non-nuget reference with "build action: do not copy" selected.
So my questions:
is there something simple and easy I can do at runtime to tell the runtime-linker "when somebody loads a type from an assembly you think should be grpc.core, do not load 1.0.0.0, find 1.16.0.0 exactly"?
the runtime was already matching the needed library by "strong
name". The problem is that the 1.16.0 is a misnomer. That version
string was informational, but the assembly itself was version
1.0.0.0. Fusion was loading the library I wanted by exact match already.
is there something smarter I can do with appdomains or contexts or another C# device to explictly enter some kind of nested scope? Could I go so far as to log this as a bug in ANSYS's API?
I've tried digging into this myself, but I'm not a dotnet expert and finding out whether I'm looking at a nuget package configuration option --which isn't relevant to me, or an old-fashioned dotnet runtime option, has been very tricky.
update 1:
I've tried using AppDomain.CreateDomain, and it does indeed solve my problem, but it also requires me to provide a marshalling strategy for the already-loaded API objects. In other words, if you're programming against a plugin framework that has an api similar to:
public void DoMyPluginsFunctionality(ApiProvidedInputContext context){
var myPlugin = AppDomain.Create(
strongName: "MyCompany.MyPlugin.; Version=1.2.3.4 ...",
baseDirectory: "C:\\Program Files\\MyPlugin\\bin"
)
//success! MyCompany.MyPlugin loads the version of GRPC I want!
myPlugin.unWrapAsDynamicProxy().doFunctionality(context)
//error: No marshalling strategy and/or not serializable and/or swizzling errors
}
Then the runtime will require you to marshall (serialize) the context variable, because .net will not let you share memory across AppDomain boundaries.
So my new question:
- given I cant use AppDomains myself
- given that Grpc.core is always published as AssemblyVersion=1.0.0.0
What are my options?
Stop using newer features of GRPC.core and live in fear of my parent processes dependencies
use a strategy similar to shading. Is there something like shading in the .net world?
Edit the published binary's version metadata. Can I dynamically edit a published binaries version?
rebuild GRPC myself with the version string updated --effectively a private fork of GRPC.
update 2:
The GRPC build system seems like its quite large and well maintained, so I'm hoping I can simply build it and change a vcproj file to include an updated version string.
Unfortunately it also seems quite complex, and I haven't quite got the targeting/cross-compiling (x64 targeting x86) worked out.

Related

could not load file or assembly exception NET 5/6 of NuGet package for NET Standard

I'm at the end of my rope trying to figure out why my working DLL cannot load a NuGet package that it could always load before (like for a whole year). My system has 70 NET 6 projects and half a dozen of them use the same NuGet package to read XML files. Recently (a few months ago) I upgraded everything to NET 6, and even after that upgrade, everything worked fine.
But now, one of my C# class libraries generates an exception in a constructor because it can't find the NuGet package that sits right beside it in the filesystem app folder where everything is loaded from. The error message was:
.ctor Could not load file or assembly xxx blah blah. The system could not find the file specified.
For what it is worth, here is a trace of the build options in the batch file that I have been using for months.
Deleting hscore\hscore\bin Deleting hscore\hscore\obj Deleting
hscore\hscore\packages "C:\Program Files\Microsoft Visual
Studio\2022\Community\MSBuild\Current\Bin\msbuild.exe" -nologo -m
-V:minimal -t:clean;restore;publish /p:Platform=AnyCPU /p:PlatformTarget=AnyCPU /p:Configuration=Debug
/p:TargetFramework=net6.0-windows7.0 /p:RuntimeIdentifier=win-x64
/p:SelfContained=false /p:PublishProtcol=FileSystem
/p:DeleteExistingFiles=true /p:PublishDir=c:\dev\holding\core.plt
-nowarn:MSB3305 c:\dev\products\hscore\hscore\hscore.csproj
Determining projects to restore... Restored
c:\dev\products\hscore\hscore\hscore.csproj (in 874 ms).
hscore -> c:\dev\products\hscore\hscore\bin\Debug\net6.0-windows7.0\win-x64\hscore.dll
hscore -> c:\dev\holding\core.plt\
I have done the following to debug it, without success:
deleted the bin/obj folders of the failing class library
checked the target framework of all projects (= net6.0-windows7.0)
checked the runtime identifier of all projects (= win-x64)
checked the build configuration (Debug, AnyCPU)
removed and re-added the NuGet package (with a specific version)
the Nuget package depends on NET Standard, compatible with NET 6
and I use the same version in other NET 6 programs that are working fine
restored and rebuilt the DLL
published it to the destination folder where it is run from
checked that the expected Nuget package is there (it was)
and yet it still fails to find the Nuget package
I tried to trace DLL loads of the process with the SysInternals Process Monitor, but I am not good enough to do that (if it is even possible). I can filter events to see the parent app process load, but the failing DLL is called by the parent process and I don't know how to see the search paths it is using to find the Nuget package.
I want to believe that the problem is a version mismatch between NET 5 and NET 6 and the version of the Nuget package sitting in the folder, because I once had a problem like that. For example, if I had a NET Framework version of the Nuget package, it would not be "found" by a NET 5 DLL that wanted to load it. (I am NOT saying that is my problem, because I have no NET Framework projects anymore.)
The most recent thing I have been working on is the build system and options. I can build the whole system with batch files or with parallel builds (when the system is working). I use the same msbuild options in both cases, and the batch method has been reliable and shows no build errors, no publish errors, all the correct build options, and no warnings or errors whatsoever.
The Nuget package pulls in 10 or 20 dependencies, but that is all automatic, and the package works with other console programs and apps that are working fine. So, I don't think there is anything wrong with Nuget package. All the working apps use the same Nuget package from the same runtime folder. I don't know why this one DLL is having a problem. AND I have not changed anything in the failing DLL class library for months (and it has been working fine).
Does anyone have suggestions on what else I might try to debug the problem and get the system working again? Thank you.
UPDATE - New version and build, but still a runtime failure.
Just to be sure, I upgraded the Nuget package that could not be found to the latest version and replaced all references in my 70 projects to the new version. Then I rebuilt the system (no warnings, no errors). But that did not make a difference. Now the new version cannot be found.
All unit tests of the code in question work flawlessly in VStudio, presumably because VS loads the Nuget package properly. I am mystified and would appreciate thoughts on how to proceed. Thank you.
UPDATE 2 - installed 'dotnet-trace' and traced DLL loads
I found a page that described how to dotnet tool install --global dotnet-trace and then captured a trace of my app trying to find the elusive Nuget package that is sitting right beside the requesting DLL. Although I could my app loading various app DLLs (and tons of system DLLs) with found pathnames, the trace showed nothing useful about the Nuget package.
The trace entries show the system searching in stages FindInLoadContext, then ApplicationAssemblies, then AppDomainAssemblyResolveEvent. After that, the system throws an exception because it can't find the Nuget package.
I would have thought the system FindInLoadContext would find it in the same folder as the executing and requesting assemblies, but it seems not so.
Ideas? I'm lost. And everything used to work fine.
UPDATE 3 - A small console app calls the DLL and it finds the Nuget package
The problem scenario in this question is: WindowsFormsApp -> loads MyCore.dll and calls new MyCore.MyObject(), which in the constructor tries to read some XML files using MyUtils.dll, which tries to reference the mystery Nuget package for reading XML files. MyUtils.dll cannot find the package, no matter what I do.
I wrote a small console program to load MyUtils.dll and called the exact same MyUtils.ReadMyXmlFiles API to read the files. The console app (actually, the system assembly loader) correctly found the Nuget package and read the XML files properly. This all occurred in the same folder that contains the Forms app and the Nuget package.
The only difference now is that the WindowsForms app calls an intermediate DLL that calls a constructor that references MyUtils.dll.
Could the intermediate DLL + constructor call be changing the Assembly Load Context (FindInLoadContext search rules) for the Nuget package load operation? Very strange.
The overall problem was that my Windows Forms app failed during boot (in the Form_Load event) because a Nuget package could not be found to read some XML configuration files.
Four assemblies were involved in the problem. Assembly1 (the app) called Assembly2 (.. new Assembly2Object()), whose constructor called an Assembly3 method (utilities.ReadXMLFile) which called Assembly4 (NugetPackage.XMLReader).
You can see that only Assembly4 references the Nugetpackage which could not be found. Normally, there should be no reason for Assemblies 1 or 2 to know about the existence of the NugetXmlReader used by Assembly3. But, the exceptions and assembly load traces of dotnet-trace and procmon clearly showed that Assembly3 (utilities.ReadXmlFile) could not find the NugetPackage at runtime, even though the right package was in the folder beside the executing assemblies (1->3).
The "solution" for my case was to add a Nuget package dependency to
Assembly1 (the WinFormsApp). As soon as I did that, Assembly3 (utilities.ReadXmlFile) could find Assembly4 (Nugetpackage).
My working theory is that during a WinForms boot sequence (including the Form_Shown event), the AssemblyLoadContext from Assembly1 is used to look up all assemblies in any call chain (like Assembly2 calling Assembly3 calling NugetPackage4).
Because the load context of Assembly1 has no reference to NugetPackage, and because the .deps.json file for Assembly1 is present (the doc says it is used to form the load context), Assembly3 - using the assembly load context for Assembly1 - could not find the NugetPackage.
When I added a dependency to Assembly4 (Nugetpackage) to Assembly1, then the assembly load context from Assembly1 was used by the System AssemblyLoader FindInLoadContext phase to search for (and find) the NugetPackage4 wanted by (the referencing assembly) Assembly3.
I used to think that each assembly in a calling chain would have its own assembly load context used by AssemblyLoad/FindInLoadContext. But I think that no longer. At least for my case of a Windows Forms app booting up, the assembly load context from the top-level app is being used to look up Nuget packages far down the calling chain.
It's worth repeating that my little test console apps always found the Nuget package without adding the package as a dependency to the top-level console program. I wrote console programs to test the calling chain from the bottom up: Test1) console calls Nuget directly - found; Test2) console (w/o Nuget dependency) calls Assembly3 - Nuget found; (Test3) console (w/o Nuget dependency) calls Assembly2 - Nuget found.
Then I wrote a skeleton WindowsFormsTestApp to call Assembly2 (just like the console program did) - Nuget NOT found. When I added a Nuget dependency to the WindowsFormsTestApp - Nuget was found by Assembly3.
The final step was to add a Nuget package dependency to the WinFormsApp that started this whole mess. Presto! Assembly3 found the Nuget package immediately.
My (unproven) belief is that somewhere along the line of upgrades from NET 5 - NET 6 (several SDK versions of each one), the assembly load context rules changed somehow. I could be wrong, but I lean toward this belief because my code from App->Assembly2->Assembly3->Nuget did not change during several months of successful operation. And just a couple of weeks ago after another NET 6 SDK upgrade, things broke.
Hopefully, this record might help someone someday. It seems completely unintuitive to me to add a Nuget package dependency to the top-level Forms app to help Assembly3 find a Nuget package sitting in the same folder as all the other assemblies.
After all this, I am coming around to the idea that the top-level app must/should include dependencies on anything the app ever calls, including packages used by dependent assemblies. (Although I am still puzzled as to why the code worked for months before without the Nuget package dependency.)

Why are some nuget package references required by applications?

I have a question that has been bothering me for awhile. I ran across this problem a few years back when I was dealing with writing a generic logging wrapper around some hosted provider instances using log4net.
The idea was simple enough, I wanted to write a logging and metrics that hid all the implementation in a separate visual studio project so when you wanted to add any telemetry support to another application you could just include the project, new up an instance of the logger and start logging using generic calls. If you ever switched providers or tweak logging settings, it wouldn't require any changes to the host applications.
This creates a strong decoupling point, where the main application used an interface in a logging class library, but would know nothing about the packages or providers that the logging class library was using to do the real work.
When I did this and tried out using Loggly's nuget package and log4net, I found that the calling application had to have a ref to the nuget package or else the dependent assembly would not be copied to the build directory. At the time I just wrote this off as something odd that they Loggly engineers were doing. But I have since encountered the same behavior in some, but not all other packages. (DogstatsD doesn't have a problem, Raygun does, etc..)
I have noticed that some nuget packages in assemblies are automatically copied into the parent output directory, but when I look for the setting that controls this, I cannot find it.
I have written dozens of class libraries over the years, and I have never had a problem with 'chained dependency assemblies (a refs b, b refs c, etc.) resolving when I build. It only seems to be some nuget packages that are a problem.
How do I force nuget packages referenced by a class library project to copy into the build directory without an explicit reference in the application?
Ok, I figured this one out.
This is a problem only for the Log4Net & Loggly wrapper assembly combo in particular because it is referenced entirely at runtime. Log4net loads up its required log appenders at runtime, and because .net doesn't see a ref to the assembly at build time, it assumes that it isn't being used and omits copying the required assembly to the bin directory. The solution when you know this is simple, just write an empty dummy method in the referenced library that you can call in the main application. This will cause .net to include the assembly in the build.
While this problem is specific to the Log4net library, it could occur anywhere that you are using an assembly that is only used with runtime reflection.

Identifying problematic dependencies in C++/CLI project

My application compiles fine, but I get the following runtime error:
System.IO.FileNotFoundException was unhandled
HResult=-2147024770
Message=Could not load file or assembly {Wrapper} or one of its dependencies. The specified module could not be found.
The reference to Wrapper in the calling Application looks correct. The Wrapper dll exists in the correct location.
This project used to build and run on someone else's system, I saw it demonstrated several times. That person/computer is no longer available. Some paths of some dependencies have changed since the last time it was successfully built and run, I have fixed all compile errors related to this.
Just to clarify my project structure:
Digraph G
{
App [ label = "My C# Application"]
Wrapper [ label = "C++/CLI Wrapper"]
Lib [ label = "C++ Library"]
Dll [ label = "My helper C# DLL"]
CDep [ label = "Series of deep C++ dependencies managed \n by CMake for Lib, hard coded relative paths for Wrapper."]
App->Wrapper->Lib->CDep;
App->Dll->Wrapper->CDep;
}
Wrapper is a C++/CLI wrapper around a C++ library. The error is triggered when we try to load a class in the Application that has a using statement for the Wrapper.
Wrapper does have a lot of dependencies, but the error message does not indicate which dependency is problematic. This is a large, complicated system, most of which is built by other teams. The C++ components use CMake to get all the dependencies correct, but CMake does not natively support C#.
I tried using fuslogvw to find the binding error, but it showed me absolutely nothing until I changed settings to include all binds, and then it only showed successful binds.
http://msdn.microsoft.com/en-us/library/e74a18c4(v=vs.100).aspx
The paths are long, but not over 256 characters.
I had a warning in the compile for Dll (see graph above):
Warning 1 There was a mismatch between the processor architecture of the project being built "MSIL" and the processor architecture of the reference "{cli_wrapper.dll}", "AMD64". This mismatch may cause runtime failures. Please consider changing the targeted processor architecture of your project through the Configuration Manager so as to align the processor architectures between your project and references, or take a dependency on references with a processor architecture that matches the targeted processor architecture of your project.
In Configuration Manager, the Dll is building for Platform "Any CPU" and Wrapper is building for "x64". I changed the dll to x64. I still get the runtime error.
Could not load file or assembly or one of its dependencies
Performed a clean, deleted build directory contents. No change.
Re-opened Visual Studio. No change.
Tried changing assembly name, default namespace, and project name to match. No change.
Could not load file or assembly 'xxx' or one of its dependencies. An attempt was made to load a program with an incorrect format
I believe we have to compile for 64 bit. We are dependant on a 64 bit C++ library.
Could not load file or assembly ... The parameter is incorrect
I am local administrator.
How to enable assembly bind failure logging (Fusion) in .NET
Tried the registry settings, but they appear to just be fuslogvw settings. No improvement in available log data.
Many other similar questions had ASP or service install specific answers.
I found the problem. There was more than one version of the library available to me, and I was using the include files from one version and the compiled library of the other. Dependancy Walker was key to finding which library had the problem, and if Aschratt had posted that as an answer rather than as a comment I would have accepted his answer.
I had the same error with a similar project earlier this week. First, when using C++/CLI, "Any CPU" doesn't exist. I had to build both for x86 to get through things.
Also, though my wrapper's dependencies were satisfied, it's the wrapper's exception that is caught by the CLR so it claims that the wrapper is missing a dependency. I was actually missing a dependency for the C++ DLL my wrapper wrapped (in my case, it was just a matter of forgetting to drop SDL2.dll and OpenAL32.dll in my new "Release" configuration output folder... I'd previously only worked with the Debug configuration where I'd already dropped those DLLs.
If you're positive you've got your wrapper's dependencies worked out, I'd recommend using Dependency Walker to check out the native DLL's dependencies. You could use Assembly.GetReferencedAssemblies with an outside script/program to double-check your wrapper as well (or ILSpy: http://ilspy.net/).
Last but not least, take a look at this: http://www.codeproject.com/Articles/442784/Best-gotchas-of-Cplusplus-CLI. He details the first two recommendations I made as well as some others.
Good luck!
One alternate root cause I've been running into quite a bit later, is that the application I'm working on uses impersonation, and the impersonated user does not have permissions to the GAC or to the folder that some dlls are in. You need to either give that user permissions, or change the way you do impersonation to load the dependencies before impersonation starts.

How do I work with shared assemblies and projects?

To preface, I've been working with C# for a few months, but I'm completely unfamiliar with concepts like deployment and assemblies, etc. My questions are many and varied, although I'm furiously Googling and reading about them to no avail (I currently have Pro C# 2008 and the .NET 3.5 Platform in front of me).
We have this process and it's composed of three components: an engine, a filter, and logic for the process. We love this process so much we want it reused in other projects. So now I'm starting to explore the space beyond one solution, one project.
Does this sound correct? One huge Solution:
Process A, exe
Process B, exe
Process C, exe
Filter, dll
Engine, dll
The engine is shared code for all of the processes, so I'm assuming that can be a shared assembly? If a shared assembly is in the same solution as a project that consumes it, how does it get consumed if it's supposed to be in the GAC? I've read something about a post build event. Does that mean the engine.dll has to be reployed on every build?
Also, the principle reason we separated the filter from the process (only one process uses it) is so that we can deploy the filter independently from the process so that the process executable doesn't need to be updated. Regardless of if that's best practice, let's just roll with it. Is this possible? I've read that assemblies link to specific versions of other assemblies, so if I update the DLL only, it's actually considered tampering. How can I update the DLL without changing the EXE? Is that what a publisher policy is for?
By the way, is any of this stuff Google-able or Amazon-able? What should I look for? I see lots of books about C# and .NET, but none about deployment or building or testing or things not related to the language itself.
I agree with Aequitarum's analysis. Just a couple additional points:
The engine is shared code for all of the processes, so I'm assuming that can be a shared assembly?
That seems reasonable.
If a shared assembly is in the same solution as a project that consumes it, how does it get consumed if it's supposed to be in the GAC?
Magic.
OK, its not magic. Let's suppose that in your solution your process project has a reference to the engine project. When you build the solution, you'll produce a project assembly that has a reference to the engine assembly. Visual Studio then copies the various files to the right directories. When you execute the process assembly, the runtime loader knows to look in the current directory for the engine assembly. If it cannot find it there, it looks in the global assembly cache. (This is a highly simplified view of loading policy; the real policy is considerably more complex than that.)
Stuff in the GAC should be truly global code; code that you reasonably expect large numbers of disparate projects to use.
Does that mean the engine.dll has to be reployed on every build?
I'm not sure what you mean by "redeployed". Like I said, if you have a project-to-project reference, the build system will automatically copy the files around to the right places.
the principle reason we separated the filter from the process (only one process uses it) is so that we can deploy the filter independently from the process so that the process executable doesn't need to be updated
I question whether that's actually valuable. Scenario one: no filter assembly, all filter code is in project.exe. You wish to update the filter code; you update project.exe. Scenario two: filter.dll, project.exe. You wish to update the filter code; you update filter.dll. How is scenario two cheaper or easier than scenario one? In both scenarios you're updating a file; why does it matter what the name of the file is?
However, perhaps it really is cheaper and easier for your particular scenario. The key thing to understand about assemblies is assemblies are the smallest unit of independently versionable and redistributable code. If you have two things and it makes sense to version and ship them independently of each other, then they should be in different assemblies; if it does not make sense to do that, then they should be in the same assembly.
I've read that assemblies link to specific versions of other assemblies, so if I update the DLL only, it's actually considered tampering. How can I update the DLL without changing the EXE? Is that what a publisher policy is for?
An assembly may be given a "strong name". When you name your assembly Foo.DLL, and you write Bar.EXE to say "Bar.EXE depends on Foo.DLL", then the runtime will load anything that happens to be named Foo.DLL; file names are not strong. If an evil hacker gets their own version of Foo.DLL onto the client machine, the loader will load it. A strong name lets Bar.EXE say "Bar.exe version 1.2 written by Bar Corporation depends on Foo.DLL version 1.4 written by Foo Corporation", and all the verifications are done against the cryptographically strong keys associated with Foo Corp and Bar Corp.
So yes, an assembly may be configured to bind only against a specific version from a specific company, to prevent tampering. What you can do to update an assembly to use a newer version is create a little XML file that tells the loader "you know how I said I wanted Foo.DLL v1.4? Well, actually if 1.5 is available, its OK to use that too."
What should I look for? I see lots of books about C# and .NET, but none about deployment or building or testing or things not related to the language itself.
Deployment is frequently neglected in books, I agree.
I would start by searching for "ClickOnce" if you're interested in deployment of managed Windows applications.
Projects can reference assemblies or projects.
When you reference another assembly/project, you are allowed to use all the public classes/enums/structs etc in the referenced assembly.
You do not need to have all of them in one solution. You can have three solutions, one for each Process, and all three solutions can load Engine and Filter.
Also, you could have Process B and Process C reference the compiled assemblies (the .dll's) of the Engine and Filter and have similar effect.
As long as you don't set the property in the reference to an assembly to require a specific version, you can freely update DLLs without much concern, providing the only code changes were to the DLL.
Also, the principle reason we
separated the filter from the process
(only one process uses it) is so that
we can deploy the filter independently
from the process so that the process
executable doesn't need to be updated.
Regardless of if that's best practice,
let's just roll with it. Is this
possible?
I actually prefer this method of updating. Less overhead to update only files that changed rather than everything everytime.
As for using the GAC, whole other level of complexity I won't get into.
Tamper proofing your assemblies can be done by signing them, which is required to use the GAC in the first place, but you should still be fine so long as a specific version is not required.
My recommendation is to read a book about the .NET framework. This will really help you understand the CLR and what you're doing.
Applied Microsoft .NET Framework Programming was a book I really enjoyed reading.
You mention the engine is shared code, which is why you put it in a separate project under your solution. There's nothing wrong with doing it this way, and it's not necessary to add this DLL to the GAC. During your development phase, you can just add a reference to your engine project, and you'll be able to call the code from that assembly. When you want to deploy this application, you can either deploy the engine DLL with it, or you can add the engine DLL to the GAC (which is another ball of wax in and of itself). I tend to lean against GAC deployments unless it's truly necessary. One of the best features of .NET is the ability to deploy everything you need to run your application in one folder without having to copy stuff to system folders (i.e. the GAC).
If you want to achieve something like dynamically loading DLL's and calling member methods from your processor without caring about specific version, you can go a couple of routes. The easiest route is to just set the Specific Version property to False when you add the reference. This will give you the liberty of changing the DLL later, and as long as you don't mess with method signatures, it shouldn't be a problem. The second option is the MEF (which uses Reflection and will be part of the framework in .NET 4.0). The idea with the MEF is that you can scan a "plugins" style folder for DLL's that implement specific functionality and then call them dynamically. This gives you some additional flexibility in that you can add new assemblies later without the need to modify your references.
Another thing to note is that there are Setup and Deployment project templates built into Visual Studio that you can use to generate MSI packages for deploying your projects. MSDN has lots of documentation related to this subject that you can check out, here:
http://msdn.microsoft.com/en-us/library/ybshs20f%28VS.80%29.aspx
Do not use the GAC on your build machine, it is a deployment detail. Visual Studio automatically copies the DLL into build directory of your application when you reference the DLL. That ensures that you'll run and debug with the expected version of the DLL.
When you deploy, you've got a choice. You can ship the DLL along with the application that uses it, stored in the EXE installation folder. Nothing special is needed, the CLR can always find the DLL and you don't have to worry about strong names or versions. A bug fix update is deployed simply by copying the new DLL into the EXE folder.
When you have several installed apps with a dependency on the DLL then deploying bug fix updates can start to get awkward. Since you have to copy to the DLL repeatedly, once for each app. And you can get into trouble when you update some apps but not others. Especially so when there's a breaking change in the DLL interface that requires the app to be recompiled. That's DLL Hell knocking, the GAC can solve that.
We found some guidance on this issue at MSDN. We started with two separate solution with no shared code, and then abstracted the commonalities to a shared assemblies. We struggled with ways to isolate changes in the shared code to impact only the projects that were ready for it. We were terrible at Open/Close.
We tried
branching the shared code for each project that used it and including it in the solution
copying the shared assembly from the shared solution when we made changes
coding pre-build events to build the shared code solution and copy the assembly
Everything was a real pain. We ended up using one large solution with all the projects in it. We branch each project as we want to stage features closer to production. This branches the shared code as well. It's simplified things a lot and we get a better idea of what tests fail across all projects, as the common code changes.
As far as deployment, our build scripts are setup to build the code and copy only the files that have changed, including the assemblies, to our environments.
By default, you have a hardcoded version number in your project (1.0.0.0). As long as you don't change it, you can use all Filter builds with the Process assembly (it only knows it should use the 1.0.0.0 version). This is not the best solution, however, because how do you distinguish between various builds yourself?
Another option is use different versions of the Filter by the same Process. You should add an app.config file to the Process project, and include a bindingRedirect element (see the docs). Whenever the Runtime looks for a particular version of the Filter, it's "redirected" to a version indicated in the config. Unfortunately, this means that although you don't have to update the Process assembly, you'll have to update the config file with the new version.
Whenever you encounter versioning problems, you can use Fuslogvw.exe (fusion log viewer) to troubleshoot these.
Have fun!
ulu

.Net Add-ins and versioning

Our media center add-in is shipped as a single DLL which lives in the GAC (mediabrowser.dll), we allow users to write extensions for our add-in by referencing our DLL and accessing the pre-defined extensibility points.
On load we search through a plug-in directory, load all the assemblies in the directory, search the assemblies for a type implementing IPlugin and execute initialiaztion routine on an instance of the plugin. I am aware that this is not the most robust design (for example: we may want to look at appdomain isolation for plugin later on) but it works alright now.
As it stands, this seems to be working fine, except for one big caveat.
When plugin writers compile their plugins the plugin references mediabrowser.dll with a specific version. Later on when we revise our dll (to fix bugs or add features) all addins that were written against earlier versions of mediabrowser.dll break.
I have thought of a few solutions to this problem (note the assembly is in the GAC):
Ship a publisher policy with with mediabrowser.dll that will redirect all earlier compatible versions of mediabrowser.dll to the current version (this must also live in the GAC).
Ship a separate assembly which contains all the fixed extension points and contracts, be extra prudent about changing this assembly, have plugin writers link against this assembly. (but still look at using publisher policies for non-breaking changes to the interfaces)
Let a third party worry about this stuff and leverage MEF or some other framework that takes care of this kind of stuff.
Hookup AppDomain.CurrentDomain.AssemblyResolve and resolve the earlier versions of the assembly to the current version. This will only work if the assembly of that specific version is not in the GAC.
Are there any other solutions to this problem that I am missing?
Update I ended up going with option 4.
I see you have picked an answer but if you are still open to ideas there is another option to consider (the very one used by the .NET framework): do not increment your assembly version between builds (but do increment your assembly build number).
This will allow your assembly to retain it's same strong name, not breaking plugin compat, and still allow you to distinguish builds from each other (using the assembly build number).
You can see this in action in .NET 2.0 through 3.5. Those releases all use the assembly version 2.0.50727 but have distinct build versions.
As long as you do not break your interface contracts (which you should never do anyway) this approach is quite reasonable.

Categories

Resources