8
votes

I want to register a .net assembly as COM.

In fact what that means as far as I know is, that instead of HKEY_CLASSES_ROOT I want the entries to be written in HKEY_CURRENT_USER/Software/Classes, so that UAC/Admin rights are not needed.

Found two solutions of that problem, with both I'm struggling:

1) Programmatically way, with following code:

        IntPtr key;
        var openKeyresult = RegOpenKeyEx(HKEY_CURRENT_USER, "SOFTWARE\\Classes", 0, (int)RegistrySecurity.KEY_WOW64_64KEY, out key);
        var overrideKeyResult = RegOverridePredefKey(HKEY_CLASSES_ROOT, key);
        var registerResult = Registrar.RegisterAssembly(GetAssembly(), AssemblyRegistrationFlags.SetCodeBase);

in this approach, overrideKeyResult is 6 which corresponds to ERROR_INVALID_HANDLE, thus, the RegisterAssembly throw a "access denied" exception because it tries to write to HKEY_CLASSES_ROOT.

on a side note: each time I run the RegOpenKeyEx the key value is different, is that ok?

2) with regasm

by using regasm.exe with the /regfile flag, and then replacing in the generated .reg file all HKEY_CLASSES_ROOT occurences with HKEY_CURRENT_USER/Software/Classes

this I think should work, but how do I unregister such assembly when I wan't to uninstall my Outlook AddIn?

as I see I can't make it in the same way as register because:

var openKeyresult = RegOpenKeyEx(HKEY_CURRENT_USER, "SOFTWARE\\Classes", 0, (int)RegistrySecurity.KEY_WOW64_64KEY, out key);

2

2 Answers

9
votes

For what it's worth, I've written a set of C# utilities that register/unregister a .NET type (should be marked as ComVisible of course) in user's registry without requiring regasm, nor UAC prompts, you can use it like this:

// register into current user registry, needs no specific rights
ComUtilities.RegisterComObject(ComUtilities.Target.User, typeof(MyClass));

// unregister from user registry, needs no specific rights
ComUtilities.UnregisterComObject(ComUtilities.Target.User, typeof(MyClass));

// register into machine registry (needs admin, UAC, etc.)
ComUtilities.RegisterComObject(ComUtilities.Target.Machine, typeof(MyClass));

// unregister from machine registry (needs admin, UAC, etc.)
ComUtilities.UnregisterComObject(ComUtilities.Target.Machine, typeof(MyClass));


public static class ComUtilities
{
    private const string ClsidRegistryKey = @"Software\Classes\CLSID";

    public enum Target
    {
        Machine,    // registers or unregisters a .NET COM object in HKEY_LOCAL_MACHINE, for all users, needs proper rights
        User        // registers or unregisters a .NET COM object in HKEY_CURRENT_USER to avoid UAC prompts
    }

    public static void RegisterComObject(Target target, Type type)
    {
        RegisterComObject(target, type, null);
    }

    public static void RegisterComObject(Target target, Type type, string assemblyPath)
    {
        RegisterComObject(target, type, assemblyPath, null);
    }

    public static void RegisterComObject(Target target, Type type, string assemblyPath, string runtimeVersion)
    {
        if (type == null)
            throw new ArgumentNullException(nameof(type));

        if (type.Assembly == null)
            throw new ArgumentException(null, nameof(type));

        // note we don't check if the type is marked as ComVisible, maybe we should

        if (assemblyPath == null)
        {
            assemblyPath = new Uri(type.Assembly.Location).LocalPath;
        }

        if (runtimeVersion == null)
        {
            runtimeVersion = GetRuntimeVersion(type.Assembly);
        }

        var root = target == Target.User ? Registry.CurrentUser : Registry.LocalMachine;

        using (RegistryKey key = EnsureSubKey(root, Path.Combine(ClsidRegistryKey, type.GUID.ToString("B"), "InprocServer32")))
        {
            key.SetValue(null, "mscoree.dll");
            key.SetValue("Assembly", type.Assembly.FullName);
            key.SetValue("Class", type.FullName);
            key.SetValue("ThreadingModel", "Both");
            if (assemblyPath != null)
            {
                key.SetValue("CodeBase", assemblyPath);
            }

            key.SetValue("RuntimeVersion", runtimeVersion);
        }

        using (RegistryKey key = EnsureSubKey(root, Path.Combine(ClsidRegistryKey, type.GUID.ToString("B"))))
        {
            // cf http://stackoverflow.com/questions/2070999/is-the-implemented-categories-key-needed-when-registering-a-managed-com-compon
            using (RegistryKey cats = EnsureSubKey(key, @"Implemented Categories\{62C8FE65-4EBB-45e7-B440-6E39B2CDBF29}"))
            {
                // do nothing special
            }

            var att = type.GetCustomAttribute<ProgIdAttribute>();
            if (att != null && !string.IsNullOrEmpty(att.Value))
            {
                using (RegistryKey progid = EnsureSubKey(key, "ProgId"))
                {
                    progid.SetValue(null, att.Value);
                }
            }
        }
    }

    public static void UnregisterComObject(Target target, Type type)
    {
        if (type == null)
            throw new ArgumentNullException(nameof(type));

        var root = target == Target.User ? Registry.CurrentUser : Registry.LocalMachine;
        using (RegistryKey key = root.OpenSubKey(ClsidRegistryKey, true))
        {
            if (key == null)
                return;

            key.DeleteSubKeyTree(type.GUID.ToString("B"), false);
        }
    }

    // kind of hack to determine clr version of an assembly
    private static string GetRuntimeVersion(Assembly asm)
    {
        string def = "v4.0.30319"; // use CLR4 as the default
        try
        {
            var mscorlib = asm.GetReferencedAssemblies().FirstOrDefault(a => a.Name == "mscorlib");
            if (mscorlib != null && mscorlib.Version.Major < 4)
                return "v2.0.50727"; // use CLR2
        }
        catch
        {
            // too bad, assume CLR4
        }
        return def;
    }

    private static RegistryKey EnsureSubKey(RegistryKey root, string name)
    {
        RegistryKey key = root.OpenSubKey(name, true);
        if (key != null)
            return key;

        string parentName = Path.GetDirectoryName(name);
        if (string.IsNullOrEmpty(parentName))
            return root.CreateSubKey(name);

        using (RegistryKey parentKey = EnsureSubKey(root, parentName))
        {
            return parentKey.CreateSubKey(Path.GetFileName(name));
        }
    }
}
1
votes

Solved it by manually adding the needed registry keys to HKLM it turned out it's not that hard to do, and regasm.exe or Registrar.RegisterAssembly doesn't do that much (at least when it comes to my use case of registering an Outlook AddIn without admin rights)