Friday, 12 June 2009

Project assembly reference fun and games

We just had an issue with the build of one of our applications on our CI server. The build in question no longer contained an assembly in the bin folder where previously it had.

Pinpointing the issue

At first I couldn’t understand why the assembly was not in the bin folder as the assembly was clearly marked in Visual Studio as “Copy Local = true”. I then looked through the build history to see when the assembly was last published but instead of finding a point in time that it stopped working I found that the issue only occurred on one of our build agents. Two of the three build agents correctly publish the assembly but the other one did not.

The next step was to compare the configuration of the three build agents. The difference was that the two build agents that worked did not have the specific assembly in the GAC where the failing one did. But hang on a minute “Copy Local = true” should still copy the assembly into the bin folder whether referencing a assembly on the file system or in the GAC (try setting the System assembly to “Copy Local = true”).

Investigating the Issue

My first thought was to remove the assemblies from the GAC on the failing build agent but the voice in my head kept saying “This should work dam it” so instead I cracked open the project file. The first thing I noticed was that on some of the assembly references there was an inner element called private which was set to true where as others, including the affected assembly, had no element:

<Reference Include="Ass1">
    <SpecificVersion>False</SpecificVersion>
    <HintPath>c:\Path\Ass1.ddl</HintPath>
</Reference>
<Reference Include="Ass2">
    <SpecificVersion>False</SpecificVersion>
    <HintPath>c:\Path\Ass2.dll</HintPath>
    <Private>True</Private>
</Reference>

BUT they both show up Visual Studio as “Copy Local = true”. So to test how the project file works I set the System assembly to be local as well and sure enough the inner element “Private” with a value of true appears in the project file where before there was none. As expected when built the System.dll was output to the bin folder. However the curious thing was after reverting the System assembly back to “Copy Local = false” the element was not removed instead the value was changed to false.
So my final step was to add the “Private” element to the affected assembly reference and set its value to true, commit the changes to code repository and force a build on the build agent in question and low and behold it worked.

To see how the project file got into this state I tried adding assembly references to a dummy project where some assemblies were in the GAC and some were not. My results show that if you add an assembly reference from the file system then the Copy Local value is implied as no Private element is added to the assembly reference. The Visual Studio UI will show a value of false if the assembly is in the GAC and true if it is not. What this means is that the build output will be different for different environments.

The work around

The only way round this is either to edit the project file manually or to set the Copy Local value to false and then back to true again when adding a reference to the project which you do want included in the build output.

Wednesday, 10 June 2009

ClickOnce updates via authentication proxy

(Disclaimer: This is my first blog post)

You may be aware that ClickOnce installation and updating is not supported via proxy servers that require authentication credentials other than the users default credentials.

http://social.msdn.microsoft.com/Forums/en-US/winformssetup/thread/82be8797-f14f-4db1-acb8-3206881cc567.

The thread suggests that there is hot fix but as far as I am aware this does not fix the issue which is also highlighted at the end of the thread.

I came across this very issue working on a project for a client who required the application we were writing for them to run inside various different environments using varying proxy servers and configurations. In order to accommodate this we allowed the user to either use the proxy settings provided in IE or to be able to override this with custom proxy settings. This worked fine for WCF, HTTP calls and even for creating BITS jobs but we were unable to get ClickOnce updates working.

And it was all because of this little gem in the System.Deployment.SystemNetDownloader:

protected void DownloadSingleFile(FileDownloader.DownloadQueueItem next)
{
    ...
 
    IWebProxy defaultWebProxy = WebRequest.DefaultWebProxy;
 
    if (defaultWebProxy != null)
    {
        defaultWebProxy.Credentials = CredentialCache.DefaultCredentials;
    }
 
    ...
}

This means that every time .Net tries to download a file via the SystemNetDownloader it is overwriting the credentials on the default web proxy with the users default credentials.

In order to work around this short sighted implementation I created a wrapper around the WebProxy object which implemented the IWebProxy interface, the important part here was to specifically do nothing with the Set on the Credentials property and provide a alternative way of setting the proxy credentials:

public class CustomWebProxy : IWebProxy
{
    private WebProxy webProxy;
 
    public CustomWebProxy(string serverName, int port)
    {
        webProxy = new WebProxy(serverName, port);
        webProxy.UseDefaultCredentials = false;
    }
 
    public ICredentials Credentials
    {
        get { return webProxy.Credentials; }
        set { } //The important part
    }
 
    public void SetCredentials(ICredentials credentials){ … }
    public bool BypassProxyOnLocal { … }
    public Uri GetProxy(Uri destination) { … }
    public bool IsBypassed(Uri host) { … }
    public bool UseDefaultCredentials { … }
    public Uri Address { … }
}

The final piece of the puzzle is to setup the proxy, the following should be placed at an appropriate place in the application workflow:

var networkCredential = new NetworkCredential("UserId", "Password");
var proxy = new CustomWebProxy("server", 8080);
proxy.BypassProxyOnLocal = false;
proxy.SetCredentials(networkCredential);
WebRequest.DefaultWebProxy = proxy;

It is worth pointing out that this only solves the issue with ClickOnce and proxy authentication once the application is installed you will still need to find away to get the application on to the users computer first, distribute by CD for example.

Bro Num