- | rssFeed | My book on MSBuild and Team Build | Archives and Categories Thursday, March 11, 2010

Web Deployment Tool (MSDeploy) Custom Provider Take 1

Disclaimer: Take what you read here with a grain of salt, I’m not an expert at providers … yet :)

I’ve known for quite a while that the Web Deployment Tool supports custom providers but I’ve never really looked at what it took to get actually write one. Tonight I wanted to write a simple provider to just sync a file from one place to another, just to see what is involved in creating that provider. In this post I describe how I created the provider. First you have to have the Web Deployment Tool installed, I’ve got the RTM version installed, but recently they delivered version 1.1 either should work. First things first, you need to create a class library project in Visual Studio. For this example I used Visual Studio 2010 RC for the reason that it’s the only version of Visual Studio that I have installed on this machine. If you are using Visual Studio 2010 make sure that you specify to build for .NET 3.5 because MSDeploy won’t pickup any providers written in .NET 4.0. To specify that your project should build for .NET 3.5 go to Project->Properties then on the Application tab pick the Target Framework to be .NET 3.5. See the image below for clarification.

targetframework-.net35

You will need to reference the two assemblies Microsoft.Web.Deployment.dll and Microsoft.Web.Delegation.dll. You can find both in the %Program Files%\IIS\Microsoft Web Deploy folder.

After this you need to create the class which is the provider. I called my CustomFileProvider because it will only sync a single file. The class should extend the DeploymentObjectProvider class. There are a couple abstract items that you must implement those are.

CreateKeyAttributeData

From what I can see this method is used to indicate how the “key attribute” is used. For instance when you use a contentPath provider you would use a statement like msdeploy –verb:sync –source:contentPath=C:\one\pathToSync –dest:… So we can see that the value C:\one\pathToSync is passed to the provider without a name. This is the key attribute value. This method for my provider looks like the following.

public override DeploymentObjectAttributeData CreateKeyAttributeData()
{
    DeploymentObjectAttributeData attributeData = new DeploymentObjectAttributeData(
        CustomFileProvider.KeyAttributeName,
        this.FilePath,
        DeploymentObjectAttributeKind.CaseInsensitiveCompare);

    return attributeData;
}

In this case CustomFileProvider.KeyAttributeName is a const whose value is path and its value is provided from the FilePath property. The other item that you have to override is the Name property.

Name

This property returns the name of the provider. In all the samples that I have seen (which is not very much) this name always agrees with the name of the custom provider factory, more on that in a bit. So in their example I had mine return the value customFile which my factory also returns.

Outside of these two items there are some other methods that you need to know about those are covered below.

GetAttributes

The GetAttributes method is kinda interesting. This method will be called on both the source and destination and you need to understand which context its being called in and act accordingly. You can determine if you are executing on the source or dest by using the BaseContext.IsDestinationObject property. So for this provider if you are in the source you want to ensure that the file specified exists, if not then raise a DeploymentFatalExcepton, this will stop the sync. If you are on the destination you could perform some checks to see if the file is up-to-date or not. For a simple provider you can force a sync to occur. You would do this by raising a DeploymentException. When you raise this exception at this time it causes the Add method to be called, which is exactly what we want. Here is my version of the GetAttributes method.

public override void GetAttributes(DeploymentAddAttributeContext addContext)
{
    if (this.BaseContext.IsDestinationObject)
    {
        // if we are on the destination and the file doesn't exist then we need to throw an exception
        // to ensure that the file gets synced. This happens because the Add command will be called for us.

        // Since I'm throwing an exception here Add will always be called, we could check to see if this file
        // was up-to-date and if so then skip this exception.
        throw new DeploymentException();
    }
    else
    {
        // We are acting on the source object here, make sure that the file exists on disk
        if (!File.Exists(this.FilePath))
        {
            string message = string.Format("File <{0}> does not exist",this.FilePath);
            throw new DeploymentFatalException(message);
        }
    }

    base.GetAttributes(addContext);
}

For the most part the only thing left for this simple provider to implement is to override the Add method. First I will show the method then discuss its content. Here is the method.

public override void Add(DeploymentObject source, bool whatIf)
{
    // This is called on the Destination so this.FilePath is the dest path not source path
    if (!whatIf && File.Exists(source.ProviderContext.Path))
    {
        // We can let MSDeploy do the actual sync for us using existig provider
        DeploymentProviderOptions sourceProviderOptions = new DeploymentProviderOptions(DeploymentWellKnownProvider.FilePath);
        sourceProviderOptions.Path = source.ProviderContext.Path;

        using (DeploymentObject sourceObject = DeploymentManager.CreateObject(sourceProviderOptions, new DeploymentBaseOptions()))
        {
            DeploymentProviderOptions destProviderOptions = new DeploymentProviderOptions(DeploymentWellKnownProvider.FilePath);
            destProviderOptions.Path = this.FilePath;

            // Make the call to perform an actual sync
            sourceObject.SyncTo(destProviderOptions, new DeploymentBaseOptions(), new DeploymentSyncOptions());
        }
    }
}

First I check to make sure that we are not doing a whatif run (i.e. a run where we don’t want to physically perform the action) and that the source file exists. Take note of the fact that I’m explicitly using source.ProviderContext.Path to get the source path. This provider has a property, FilePath, which contains the path but it could be either source path or dest path depending on which end you are executing in. the source.ProviderContent.Path will always point to the source value. After that you can see that I’m actually leveraging an existing provider the FilePath provider to do the actual sync for me. So all the dirty work is his job! If you are writing a provider make sure to re-use any existing providers that you can, because the code for this part looks like it can get nasty. I’ll leave that for another post.

After I prepare the source options I create an instance of the DeploymentObject class, prepare the FilePath provider and call SyncTo on the object., this is where the physical sync occurs. That is basically it for the provider itself now we need to create a provider factory class which is the guy who knows how to create our providers for us.

Fortunately creating custom provider factories is even easier then creating custom providers themselves. I called mine CustomFileProviderFactory and the entire class is shown below.

[DeploymentProviderFactory]
public class CustomFileProviderFactory : DeploymentProviderFactory
{
    protected override DeploymentObjectProvider Create(DeploymentProviderContext providerContext, DeploymentBaseContext baseContext)
    {
        return new CustomFileProvider(providerContext, baseContext);
    }

    public override string Description
    {
        get { return @"Custom provider to copy a file"; }
    }

    public override string ExamplePath
    {
        get { return @"c:\somefile.txt"; }
    }

    public override string FriendlyName
    {
        get { return "customFile"; }
    }
    public override string Name
    {
        get { return "customFile"; }
    }
}

A few things to make note of; your class should extend the DeploymentProviderFactory class and it should have the DeploymentProviderFactory attribute attached to it. Besides that there are two properties FriendlyName and Name, once again in all the samples I have seen they are always the same and always equal to the Name property on the provider itself. I followed suit and copied them. I’m still trying to figure out more about what each of these actually do, but for now I’m OK with leaving them to be the same. So that is basically it.

In order to have MSDeploy use the provider you have to create a folder named Extensibility under the %Program Files%\IIS\Microsoft Web Deploy folder if it doesn’t exist, and then copy the assembly into that folder. And then you are good to go. Here is the snippet showing my custom provider in action!

C:\temp\MSDeploy>msdeploy -verb:sync -source:customFile=C:\temp\MSDeploy\Source\source.txt -dest:customFile=C:\temp
\MSDeploy\Dest\one.txt -verbose
Verbose: Performing synchronization pass #1.
Info: Adding MSDeploy.customFile (MSDeploy.customFile).
Info: Adding customFile (C:\temp\MSDeploy\Dest\one.txt).
Verbose: The dependency check 'DependencyCheckInUse' found no issues.
Verbose: The synchronization completed in 1 pass(es).
Total changes: 2 (2 added, 0 deleted, 0 updated, 0 parameters changed, 0 bytes copied)

This was a pretty basic provider, but you have to start somewhere. I will post more about custom providers as I find out more.

You can download the entire source at http://sedotech.com/Resources#CustomProviders under the Custom Providers heading of the MSDeploy section.

Sayed Ibrahim Hashimi

Custom Provider | MSDeploy | Web Deployment Tool Thursday, March 11, 2010 6:04:47 AM (GMT Standard Time, UTC+00:00)  #     |