How to write a DCOM server in C#

Updated 12/06/2006: https://msdn2.microsoft.com/en-gb/library/system.runtime.interopservices.registrationservices.unregistertypeforcomclients.aspx states that PInvoking CoRegisterClassObject is a technique which is not supported.

Well, why DCOM and not .NET Remoting? For one thing, DCOM offers a secure interprocess communication channel through TCP/IP... which .NET remoting doesn't have unfortunately. Also, a DCOM server can be hosted in almost any process, including Windows Services!

The ideas are described below (this is pretty straightforward assuming you already know COM)
1) Your server process will expose a COM class factory that would just create your .NET object.
2) In COM you register the class factory using the standard CoRegisterClassObjects API
3) Make sure you call CoInitializeSecurity on your first process, for example to allow only Administrators to call in
4) Register your .NET assemblies with REGASM.EXE. Make sure your .NET class is visible through COM so CCW can be created around it (more details in MSDN on COM Interop section).
5) Remove the auto-generated InprocServer32 key after registration (REGASM puts it there but we are going out-of-proc)
6) Add the standard LocalServer32 / AppID registry keys.

That's it! Now you have a secure DCOM service implemented entirely in C# :-)

Here is the C# source code:

 using System;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.ServiceProcess;
using System.Threading;
using System.Runtime.InteropServices;

namespace Test
{
 // 
 // .NET class, interface exposed through DCOM
 //

 // exposed COM interface
 [GuidAttribute(MyService.guidIMyInterface), ComVisible(true)]
 public interface IMyInterface
 {
  string GetDateTime(string prefix); 
 }

 // exposed COM class
 [GuidAttribute(MyService.guidMyClass), ComVisible(true)]
 public class CMyClass: IMyInterface
 {
  // Print date & time and the current EXE name
  public string GetDateTime(string prefix) 
  { 
   Process currentProcess = Process.GetCurrentProcess();
   return string.Format("{0}: {1} [server-side COM call executed on {2}]", 
    prefix, DateTime.Now, currentProcess.MainModule.ModuleName);
  } 
 }

 //
 // My hosting Windows service
 //
 internal class MyService : 
  ServiceBase
 {
  public MyService()
  {
   // Initialize COM security
   Thread.CurrentThread.ApartmentState = ApartmentState.STA;
   UInt32 hResult = ComAPI.CoInitializeSecurity(
    IntPtr.Zero, // Add here your Security descriptor
    -1,
    IntPtr.Zero,
    IntPtr.Zero,
    ComAPI.RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
    ComAPI.RPC_C_IMP_LEVEL_IDENTIFY,
    IntPtr.Zero,
    ComAPI.EOAC_DISABLE_AAA 
    | ComAPI.EOAC_SECURE_REFS 
    | ComAPI.EOAC_NO_CUSTOM_MARSHAL,
    IntPtr.Zero);
   if (hResult != 0)
    throw new ApplicationException(
     "CoIntializeSecurity failed" + hResult.ToString("X"));
  }

  // The main entry point for the process
  static void Main()
  {
   ServiceBase.Run(new ServiceBase[] { new MyService() });
  }
  /// 
  /// On start, register the COM class factory
  /// 
  protected override void OnStart(string[] args)
  {
   Guid CLSID_MyObject = new Guid(MyService.guidMyClass);
   UInt32 hResult = ComAPI.CoRegisterClassObject(
    ref CLSID_MyObject, 
    new MyClassFactory(), 
    ComAPI.CLSCTX_LOCAL_SERVER, 
    ComAPI.REGCLS_MULTIPLEUSE, 
    out _cookie);
   if (hResult != 0)
    throw new ApplicationException(
     "CoRegisterClassObject failed" + hResult.ToString("X"));  
  }
  /// 
  /// On stop, remove the COM class factory registration
  /// 
  protected override void OnStop()
  {
   if (_cookie != 0)
    ComAPI.CoRevokeClassObject(_cookie);
  }
  private int _cookie = 0;

  //
  // Public constants
  //
  public const string serviceName = "MyService";
  public const string guidIMyInterface = "e88d15a5-0510-4115-9aee-a8421c96decb";
  public const string guidMyClass = "f681abd0-41de-46c8-9ed3-d0f4eba19891";
 }

 //
 // Standard installer 
 //
 [RunInstaller(true)]
 public class MyServiceInstaller : 
  System.Configuration.Install.Installer
 {
  public MyServiceInstaller()
  {
   processInstaller = new ServiceProcessInstaller();
   serviceInstaller = new ServiceInstaller();
   // Add a new service running under Local SYSTEM
   processInstaller.Account = ServiceAccount.LocalSystem;
   serviceInstaller.StartType = ServiceStartMode.Manual;
   serviceInstaller.ServiceName = MyService.serviceName;
   Installers.Add(serviceInstaller);
   Installers.Add(processInstaller);
  }
  private ServiceInstaller serviceInstaller;
  private ServiceProcessInstaller processInstaller;
 }

 //
 // Internal COM Stuff
 //

 /// 
 /// P/Invoke calls
 /// 
 internal class ComAPI
 {
  [DllImport("OLE32.DLL")]
  public static extern UInt32 CoInitializeSecurity(
   IntPtr securityDescriptor, 
   Int32 cAuth,
   IntPtr asAuthSvc,
   IntPtr reserved,
   UInt32 AuthLevel,
   UInt32 ImpLevel,
   IntPtr pAuthList,
   UInt32 Capabilities,
   IntPtr reserved3
   );
  [DllImport ("ole32.dll")]
  public static extern UInt32 CoRegisterClassObject (
   ref Guid rclsid, 
   [MarshalAs (UnmanagedType.Interface)]IClassFactory pUnkn, 
   int dwClsContext, 
   int flags, 
   out int lpdwRegister);
  [DllImport ("ole32.dll")]
  public static extern UInt32 CoRevokeClassObject (int dwRegister);
  public const int RPC_C_AUTHN_LEVEL_PKT_PRIVACY = 6; // Encrypted DCOM communication
  public const int RPC_C_IMP_LEVEL_IDENTIFY = 2;  // No impersonation really required
  public const int CLSCTX_LOCAL_SERVER = 4; 
  public const int REGCLS_MULTIPLEUSE = 1;
  public const int EOAC_DISABLE_AAA = 0x1000;  // Disable Activate-as-activator
  public const int EOAC_NO_CUSTOM_MARSHAL = 0x2000; // Disable custom marshalling
  public const int EOAC_SECURE_REFS = 0x2;   // Enable secure DCOM references
  public const int CLASS_E_NOAGGREGATION = unchecked((int)0x80040110);
  public const int E_NOINTERFACE = unchecked((int)0x80004002);
  public const string guidIClassFactory = "00000001-0000-0000-C000-000000000046";
  public const string guidIUnknown = "00000000-0000-0000-C000-000000000046";
 }

 /// 
 /// IClassFactory declaration
 /// 
 [ComImport (), InterfaceType (ComInterfaceType.InterfaceIsIUnknown), 
 Guid (ComAPI.guidIClassFactory)]
 internal interface IClassFactory
 {
  [PreserveSig]
  int CreateInstance (IntPtr pUnkOuter, ref Guid riid, out IntPtr ppvObject);
  [PreserveSig]
  int LockServer (bool fLock);
 }

 /// 
 /// My Class factory implementation
 /// 
 internal class MyClassFactory : IClassFactory
 {
  public int CreateInstance (IntPtr pUnkOuter, 
   ref Guid riid, 
   out IntPtr ppvObject)
  {
   ppvObject = IntPtr.Zero;
   if (pUnkOuter != IntPtr.Zero)
    Marshal.ThrowExceptionForHR (ComAPI.CLASS_E_NOAGGREGATION);
   if (riid == new Guid(MyService.guidIMyInterface) 
    || riid == new Guid(ComAPI.guidIUnknown))
   {
    //
    // Create the instance of my .NET object
    //
    ppvObject = Marshal.GetComInterfaceForObject(
        new CMyClass(), typeof(IMyInterface));
   }
   else
    Marshal.ThrowExceptionForHR (ComAPI.E_NOINTERFACE);
   return 0;
  }
  public int LockServer (bool lockIt)
  {
   return 0;
  } 
 }
}

The registration CMD script:

 set EXE_FULL_PATH=%~dp0windowsservice1.exe
if not exist %EXE_FULL_PATH% @echo Executable %EXE_FULL_PATH% not present in the current directory! & @goto :EOF
installutil /u %EXE_FULL_PATH%
installutil %EXE_FULL_PATH%
regasm %EXE_FULL_PATH% /codebase
REG.EXE ADD HKCR\AppID\{9922b97d-ce4a-4cc8-a26f-4944708e652d} /v LocalService /t REG_SZ /d MyService /f
REG.EXE ADD HKCR\CLSID\{F681ABD0-41DE-46C8-9ED3-D0F4EBA19891}\LocalServer32 /ve /t REG_SZ /d %EXE_FULL_PATH% /f
REG.EXE DELETE HKCR\CLSID\{F681ABD0-41DE-46C8-9ED3-D0F4EBA19891}\InprocServer32 /f

And a test VBS script that will exercise our service from a separate process obviously:

 Dim obj
Set obj = CreateObject( "Test.CMyClass" )
wscript.echo obj.GetDateTime("Current date: ")

One more comment: The code above allows everybody to call into the process. This is probably not very useful since you might want to allow only administrators to call into your service. There is a solution though: all you have to do is to pass a certain security descriptor to CoInitializeSecurity that will allow only certain classes of users to call into the process. This is actually not very hard, and I'll probably post a sample code in the future. Now I have to get back to work, so see you for the next time!