{"id":260,"date":"2014-10-07T11:12:44","date_gmt":"2014-10-07T15:12:44","guid":{"rendered":"http:\/\/ryepup.unwashedmeme.com\/blog\/?p=260"},"modified":"2014-10-09T11:57:43","modified_gmt":"2014-10-09T15:57:43","slug":"deploying-com-objects-with-msbuild","status":"publish","type":"post","link":"http:\/\/ryepup.unwashedmeme.com\/blog\/2014\/10\/07\/deploying-com-objects-with-msbuild\/","title":{"rendered":"Deploying .NET COM objects with MSBuild"},"content":{"rendered":"<p>At work I&#8217;ve been working with a very old classic ASP website, running on a very old hardware. After a few weeks of hacking vbscript, I&#8217;m<br \/>\nsorely missing C#, and especially unit tests. I feel so exposed when there&#8217;s no tests proving my code does what I think it does. For reasons I&#8217;ll not go into, deploying aspx pages is not an option, so I spent some time exploring creating COM objects using .NET, and consuming them from vbscript using <a href=\"http:\/\/msdn.microsoft.com\/en-us\/library\/ms524786%28v=vs.90%29.aspx\">Server.CreateObject<\/a>. This seems like it will work really well for my purpose. This morning I sorted out deployment, which didn&#8217;t have any easy google answers.<\/p>\n<p>My goals is to deploy via msbuild with a few commands:<\/p>\n<ul>\n<li><code>MSBuild.exe \/t:DeployCOM \/p:env=test<\/code> to deploy to my test site<\/li>\n<li><code>MSBuild.exe \/t:DeployCOM \/p:env=prod<\/code> to deploy to production<\/li>\n<\/ul>\n<p>TL;DR: see <a href=\"https:\/\/gist.github.com\/ryepup\/0f005f6a4ec2506d3435\">deploy-com-dll.targets<\/a><\/p>\n<h3>Requirements<\/h3>\n<p>Before getting into XML, to deploy a COM DLL, there are a few things to do:<\/p>\n<ul>\n<li>unregister the old version of the DLL. I&#8217;m trying to avoid <a href=\"http:\/\/en.wikipedia.org\/wiki\/DLL_Hell\">DLL Hell<\/a> by only having one version of my DLL installed on the server, and explicitly not maintaining backward compatibility<\/li>\n<li>restart IIS; if my COM object has been loaded in IIS, then the DLL file on disk is locked<\/li>\n<li>copy new DLL to the server<\/li>\n<li>register the new DLL<\/li>\n<\/ul>\n<h3><code>MSBuild<\/code> design<\/h3>\n<p>MSBuild conveniently includes the <a href=\"http:\/\/msdn.microsoft.com\/en-us\/library\/dakwb8wf.aspx\">RegisterAssembly<\/a> and <a href=\"http:\/\/msdn.microsoft.com\/en-us\/library\/a8d5b2y5.aspx\">UnregisterAssembly<\/a> tasks to handle the COM registration, but unfortunately these don&#8217;t work with remote machines, so we have to do things by hand.<\/p>\n<ul>\n<li>conditional <a href=\"http:\/\/msdn.microsoft.com\/en-us\/library\/t4w159bs.aspx\">PropertyGroup<\/a>s for each deployment target (<code>test<\/code> and <code>prod<\/code>)<\/li>\n<li>unconditional <a href=\"http:\/\/msdn.microsoft.com\/en-us\/library\/t4w159bs.aspx\">PropertyGroup<\/a> for common settings<\/li>\n<li>use the <a href=\"http:\/\/msdn.microsoft.com\/en-us\/library\/x8zx72cd.aspx\">Exec<\/a> task with <a href=\"http:\/\/technet.microsoft.com\/en-us\/sysinternals\/bb897553.aspx\">PsExec.exe<\/a> to make run remote commands<\/li>\n<li>use <a href=\"http:\/\/msdn.microsoft.com\/en-us\/library\/tzat5yw6%28v=vs.110%29.aspx\">RegAsm.exe<\/a> to register \/ unregister our assembly<\/li>\n<li>use <a href=\"http:\/\/support.microsoft.com\/kb\/202013\">iisreset.exe<\/a> to restart IIS<\/li>\n<li>use <a href=\"http:\/\/msdn.microsoft.com\/en-us\/library\/3e54c37h.aspx\">Copy<\/a> to copy our DLL over file share using UNC paths<\/li>\n<li>wrap it up in a <code>DeployCOM<\/code> <a href=\"http:\/\/msdn.microsoft.com\/en-us\/library\/t50z2hka.aspx\">Target<\/a><\/li>\n<\/ul>\n<h3><code>MSBuild<\/code> implementation<\/h3>\n<p>The XML is available as <a href=\"https:\/\/gist.github.com\/ryepup\/0f005f6a4ec2506d3435\">deploy-com-dll.targets<\/a>.<\/p>\n<p>First up, our conditional <a href=\"http:\/\/msdn.microsoft.com\/en-us\/library\/t4w159bs.aspx\">PropertyGroup<\/a>s:<\/p>\n<pre><code>&lt;Propertygroup Condition=\"'$(Env)' == 'test'\"&gt;\n  &lt;!-- UNC path where we publish code--&gt;\n  &lt;PublishFolder&gt;\\\\dev-server\\Sites\\my-classic-asp-site&lt;\/PublishFolder&gt;\n  &lt;!-- the remote path to where our code is deployed --&gt;\n  &lt;LocalPublishFolder&gt;D:\\WebRoot\\my-classic-asp-site&lt;\/LocalPublishFolder&gt;\n  &lt;!-- the computer name for psexec --&gt;\n  &lt;Computer&gt;\\\\dev-server&lt;\/Computer&gt;\n&lt;\/PropertyGroup&gt;\n\n&lt;PropertyGroup Condition=\"'$(Env)' == 'prod'\"&gt;\n  &lt;PublishFolder&gt;\\\\prod-server\\WebRoot\\my-classic-asp-site&lt;\/PublishFolder&gt;\n  &lt;LocalPublishFolder&gt;D:\\sites\\my-classic-asp-site&lt;\/LocalPublishFolder&gt;\n  &lt;Computer&gt;\\\\prod-server&lt;\/Computer&gt;\n&lt;\/PropertyGroup&gt;\n<\/code><\/pre>\n<p>Pretty straightfoward, tells us where to copy our files to, and gives the remote path so when we call <code>regasm<\/code> we can tell it where to look.<\/p>\n<p>Next, our unconditional <a href=\"http:\/\/msdn.microsoft.com\/en-us\/library\/t4w159bs.aspx\">PropertyGroup<\/a>:<\/p>\n<pre><code>&lt;PropertyGroup&gt;\n  &lt;!-- where to copy our COM assembly --&gt;\n  &lt;COMPublishFolder&gt;$(PublishFolder)\\COM&lt;\/COMPublishFolder&gt;\n  &lt;!-- remote path to our where we published our COM assembly --&gt;\n  &lt;COMFolder&gt;$(LocalPublishFolder)\\COM&lt;\/COMFolder&gt;\n  &lt;!-- remote path to our COM assembly --&gt;\n  &lt;COMAssembly&gt;$(COMFolder)\\My.Interop.dll&lt;\/COMAssembly&gt;\n  &lt;!-- remote path to our typelib --&gt;\n  &lt;COMTypeLib&gt;$(COMFolder)\\My.Interop.tlb&lt;\/COMTypeLib&gt;\n  &lt;!-- local path to psexec--&gt;\n  &lt;PsExec&gt;$(MSBuildProjectDirectory)\\tools\\PsExec.exe&lt;\/PsExec&gt;\n  &lt;!-- remote path to regasm.exe --&gt;\n  &lt;RegAsm&gt;C:\\WINDOWS\\Microsoft.NET\\Framework\\v4.0.30319\\RegAsm.exe&lt;\/RegAsm&gt;\n  &lt;!-- remote path to iisreset.exe --&gt;\n  &lt;iisreset&gt;c:\\WINDOWS\\system32\\iisreset.exe&lt;\/iisreset&gt;\n&lt;\/PropertyGroup&gt;\n<\/code><\/pre>\n<p>Again, pretty straightforward. A lot of derived properties, specifying where to publish COM code, more remote paths so <code>psexec<\/code> can find the <code>regasm<\/code> and <code>iisreset<\/code> executables and our COM assembly.<\/p>\n<p>Lastly, our <a href=\"http:\/\/msdn.microsoft.com\/en-us\/library\/t50z2hka.aspx\">Target<\/a>:<\/p>\n<pre><code>&lt;Target Name=\"DeployCOM\"&gt;\n  &lt;!-- local path our compiled, strongly-named COM assembly and it's dependencies --&gt;\n  &lt;ItemGroup&gt;\n    &lt;FilesToCopy Include=\"$(MSBuildProjectDirectory)\\src\\My.Interop\\bin\\Release\\*.*\"\/&gt;\n  &lt;\/ItemGroup&gt;\n  &lt;MakeDir Directories=\"$(COMPublishFolder)\"\/&gt;\n  &lt;!-- unregister the old version of the DDL --&gt;\n  &lt;Exec Command=\"$(PsExecBinary) $(Computer) $(RegAsm) \/u \/codebase \/tlb:$(COMTypeLib) $(COMAssembly)\"\n        IgnoreExitCode=\"true\"\/&gt;\n  &lt;!-- restart IIS --&gt;\n  &lt;Exec Command=\"$(PsExecBinary) $(Computer) $(iisreset) \/restart\" IgnoreExitCode=\"true\"\/&gt;\n  &lt;!-- copy new DLL to the server --&gt;\n  &lt;Copy SourceFiles=\"@(FilesToCopy)\"\n        DestinationFiles=\"@(FilesToCopy-&gt;'$(COMPublishFolder)\\%(RecursiveDir)%(Filename)%(Extension)')\"\n        SkipUnchangedFiles=\"true\"\n        \/&gt;\n  &lt;!-- register the new DDL --&gt;\n  &lt;Exec Command=\"$(PsExecBinary) $(Computer) $(RegAsm) \/codebase \/tlb:$(COMTypeLib) $(COMAssembly)\"\/&gt;\n&lt;\/Target&gt;\n<\/code><\/pre>\n<p>There are some comments inline there breaking out the steps. A few important notes:<\/p>\n<ul>\n<li>we&#8217;re using the <code>\/codebase<\/code> flag to <code>regasm<\/code>; this means we are NOT going to be installing our .NET in the global assembly cache, so the COM system needs to know to look in our <code>COMFolder<\/code> to resolve .NET references. Without this, we would need to also call <code>gacutil<\/code> to install our .NET in the system, and any third-party libraries we&#8217;ve referenced<\/li>\n<li>the COM assembly needs to be strongly named, and therefore all it&#8217;s dependencies need to be strongly named. Compiling and signing the assembly is out of scope here, but there are plenty of good tutorials out there.<\/li>\n<li>in a few places we ignore the exit code of the exec call; on first deployment those tasks might fail<\/li>\n<li>this is a really heavy handed approach with IIS. Using more of the versioning features in COM and .NET assembly will let you install multiple versions of everything so you can publish a new version, then update your vbscript code to use that new version. In my case, I have huge maintenance windows every night to do iis restarts, but that option is not available to everyone.<\/li>\n<\/ul>\n<h3>Summary<\/h3>\n<p>It&#8217;s ugly, but it works and is a hell of a lot prettier than writing vbscript, and I can write tests and get some confidence that my code works before it&#8217;s in production.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>At work I&#8217;ve been working with a very old classic ASP website, running on a very old hardware. After a few weeks of hacking vbscript, I&#8217;m sorely missing C#, and especially unit tests. I feel so exposed when there&#8217;s no tests proving my code does what I think it does. For reasons I&#8217;ll not go [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2,46,6],"tags":[],"class_list":["post-260","post","type-post","status-publish","format-standard","hentry","category-c","category-msbuild","category-windows"],"_links":{"self":[{"href":"http:\/\/ryepup.unwashedmeme.com\/blog\/wp-json\/wp\/v2\/posts\/260","targetHints":{"allow":["GET"]}}],"collection":[{"href":"http:\/\/ryepup.unwashedmeme.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/ryepup.unwashedmeme.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/ryepup.unwashedmeme.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/ryepup.unwashedmeme.com\/blog\/wp-json\/wp\/v2\/comments?post=260"}],"version-history":[{"count":4,"href":"http:\/\/ryepup.unwashedmeme.com\/blog\/wp-json\/wp\/v2\/posts\/260\/revisions"}],"predecessor-version":[{"id":265,"href":"http:\/\/ryepup.unwashedmeme.com\/blog\/wp-json\/wp\/v2\/posts\/260\/revisions\/265"}],"wp:attachment":[{"href":"http:\/\/ryepup.unwashedmeme.com\/blog\/wp-json\/wp\/v2\/media?parent=260"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/ryepup.unwashedmeme.com\/blog\/wp-json\/wp\/v2\/categories?post=260"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/ryepup.unwashedmeme.com\/blog\/wp-json\/wp\/v2\/tags?post=260"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}