Все записи автора Андрей Бушман

NuGet-пакет для Revit 2017

На https://www.nuget.org не нашёл от Autodesk NuGet-пакетов для Revit 2017. Соответственно, сделал свой.

Результат здесь: https://www.nuget.org/packages/Revit-2017x64.Base/1.0.0

Пакет подключает к проекту файлы RevitAPI.dll, RevitAPIUI.dll, RevitAPIIFC.dll, RevitAPIMacros.dll, и RevitAPIUIMacros.dll. В процессе подключения обозначенных сборок, пакет назначает их свойству Copy Local значение False.

NuGet-пакет для Revit 2017

На https://www.nuget.org не нашёл от Autodesk NuGet-пакетов для Revit 2017. Соответственно, сделал свой.

Результат здесь: https://www.nuget.org/packages/Revit-2017x64.Base/1.0.0

Пакет подключает к проекту файлы RevitAPI.dll, RevitAPIUI.dll, RevitAPIIFC.dll, RevitAPIMacros.dll, и RevitAPIUIMacros.dll. В процессе подключения обозначенных сборок, пакет назначает их свойству Copy Local значение False.

CHM: о наименовании каталогов и файлов исходников.

Недавно столкнулся с проблемой, когда Help and Manual не мог успешно компилировать мой проект в документацию CHM-формата. Причём тот же самый проект успешно опубликовывался в PDF-версию.

При формировании CHM-файла программа Help and Manual использует "родной" инструмент от Microsoft - утилиту hhc.exe. Мой проблемный проект находился в каталоге "%projects%MyProjectName.help" и файл проекта имел имя "MyProjectName.help.hmxp". Любые попытки собрать на его основе CHM-документацию приводили к ошибкам компиляции:



Группа технической поддержки Help and Manual помогла мне разобраться с причиной возникновения подобных ошибок (за что я им весьма признателен):
In your case the error may be caused by the dot and the word "help" in the folder name and CHM file name. The combination of the dot and the letter "h" (and possibly also the word "help") in filenames and folder names is poison for the Microsoft CHM system on some versions of Windows. It is very ancient and has not been updated or bugfixed since it was originally released with Windows 98. 

Получив такой ответ, я заменил "." на "_" (перед "help") в имени каталога проекта, а так же в имени HMXP-файла, после чего проблема со сборкой CHM-справки сразу же исчезла.

CHM: о наименовании каталогов и файлов исходников.

Недавно столкнулся с проблемой, когда Help and Manual не мог успешно компилировать мой проект в документацию CHM-формата. Причём тот же самый проект успешно опубликовывался в PDF-версию.

При формировании CHM-файла программа Help and Manual использует "родной" инструмент от Microsoft - утилиту hhc.exe. Мой проблемный проект находился в каталоге "%projects%\MyProjectName.help\" и файл проекта имел имя "MyProjectName.help.hmxp". Любые попытки собрать на его основе CHM-документацию приводили к ошибкам компиляции:



Группа технической поддержки Help and Manual помогла мне разобраться с причиной возникновения подобных ошибок (за что я им весьма признателен):
In your case the error may be caused by the dot and the word "help" in the folder name and CHM file name. The combination of the dot and the letter "h" (and possibly also the word "help") in filenames and folder names is poison for the Microsoft CHM system on some versions of Windows. It is very ancient and has not been updated or bugfixed since it was originally released with Windows 98. 

Получив такой ответ, я заменил "." на "_" (перед "help") в имени каталога проекта, а так же в имени HMXP-файла, после чего проблема со сборкой CHM-справки сразу же исчезла.

Динамический вызов функций неуправляемых DLL

В .NET-атрибутах можно указывать только константные выражения. Т.о. атрибуту DllImport нужно указывать имя библиотеки статически, дабы это имя было известно на этапе компиляции. Порой это порождает уродливые конструкции, разрастающиеся как снежный ком, по мере появления новых версий AutoCAD. Наглядный тому пример можно увидеть здесь в коде Александра Ривилиса: по мере появления AutoCAD 2018, 2019 и т.д. - этот код придётся каждый раз дописывать.


Как правило, имя нужной DLL достаточно просто выяснить во время выполнения кода. Например, в обозначенном выше коде Александра Ривилиса нужная функция хранится в файлах acdb17.dll, acdb18.dll, ac1st19.dll, ac1st20.dll, ac1st21.dll. Т.е. мы видим, что каждая библиотека в качестве суффикса использует значение Major версии приложения. Кроме того, мы видим, что начиная с версии 19 (AutoCAD 2013) функция была перенесена в др. библиотеку (т.е. изменился префикс в её имени). Зная обозначенные выше правила, можно динамически вычислить имя DLL файла, в котором находится интересующая нас функция.

Далее показываю небольшой пример того, как можно динамически вызывать функцию acedSetEnv в разных версиях AutoCAD, не создавая для неё статических обёрток. На всякий случай напоминаю, что до версии AutoCAD 2013 эта функция находилась в файле acad.exe, а начиная с AutoCAD 2013 была перенесена в accore.dll.

Код проверялся в AutoCAD 2009 и 2016.

-------------------------------------------------------------
/* Utils.cs */
using System;
using System.Text;
using System.Runtime.InteropServices;
using cad = Autodesk.AutoCAD.ApplicationServices.Application;

namespace Bushman.AutoCAD.Sandbox {

    [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet
        = CharSet.Auto)]
    public delegate int acedSetEnvDelegate(string envName,
        StringBuilder NewValue);

    public static class Utils {

        [DllImport("kernel32.dll")]
        public static extern IntPtr LoadLibrary(string
            dllToLoad);

        [DllImport("kernel32.dll")]
        public static extern IntPtr GetProcAddress(IntPtr
            hModule, string procedureName);


        [DllImport("kernel32.dll")]
        public static extern bool FreeLibrary(IntPtr hModule);

        const int AutoCAD_2013_Major = 19;

        public static string GetDllName() {

            if (cad.Version.Major < AutoCAD_2013_Major)
                return "acad.exe";
            else
                return "accore.dll";
        }

        public static acedSetEnvDelegate acedSetEnv;

        static Utils() {
            acedSetEnv = GetAcedSetEnv();
        }

        static acedSetEnvDelegate GetAcedSetEnv() {

            string dllName = Utils.GetDllName();
            IntPtr pDll = Utils.LoadLibrary(dllName);

            if (pDll == IntPtr.Zero) {
                return null;
            }

            string funcName = "acedSetEnv";

            IntPtr pAddressOfFunctionToCall = Utils
                .GetProcAddress(pDll, funcName);

            if (pAddressOfFunctionToCall == IntPtr.Zero) {

                return null;
            }

            acedSetEnvDelegate acedSetEnv = (
                acedSetEnvDelegate) Marshal
                .GetDelegateForFunctionPointer(
                pAddressOfFunctionToCall,
                typeof(acedSetEnvDelegate));

            bool result = Utils.FreeLibrary(pDll);

            return acedSetEnv;
        }
    }
}

 ------------------------------------------------------------

Далее создадим тестовую команду, которая изменит значение переменной "ACAD":

------------------------------------------------------------
/* Commands.cs */
using System;
using System.Runtime.InteropServices;
using System.Text;
using cad = Autodesk.AutoCAD.ApplicationServices.Application;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;

namespace Bushman.AutoCAD.Sandbox {

    public sealed class Commands {

        [CommandMethod("Test", CommandFlags.Modal)]
        public void Test() {

            Document doc = cad.DocumentManager
                .MdiActiveDocument;

            if (doc == null)
                return;

            Editor ed = doc.Editor;

            acedSetEnvDelegate acedSetEnv = Utils.acedSetEnv;

            if (acedSetEnv == null) {
                ed.WriteMessage("acedSetEnv function was " +
                    "not found.");
                return;
            }

            /* For example, we'll edit the "Support File Search
             * Path" value: */
            string varName = "ACAD";
            StringBuilder sb = new StringBuilder(
                @"C:\abc\def");

            int theResult = acedSetEnv(varName, sb); // 5100
        }
    }
}

---------------------------------------------------------------

Динамическое связывание и изменение системой переменной ACAD успешно происходит в обоих тестируемых версиях AutoCAD: 2009 и 2016.



Динамический вызов функций неуправляемых DLL

В .NET-атрибутах можно указывать только константные выражения. Т.о. атрибуту DllImport нужно указывать имя библиотеки статически, дабы это имя было известно на этапе компиляции. Порой это порождает уродливые конструкции, разрастающиеся как снежный ком, по мере появления новых версий AutoCAD. Наглядный тому пример можно увидеть здесь в коде Александра Ривилиса: по мере появления AutoCAD 2018, 2019 и т.д. - этот код придётся каждый раз дописывать.


Как правило, имя нужной DLL достаточно просто выяснить во время выполнения кода. Например, в обозначенном выше коде Александра Ривилиса нужная функция хранится в файлах acdb17.dll, acdb18.dll, ac1st19.dll, ac1st20.dll, ac1st21.dll. Т.е. мы видим, что каждая библиотека в качестве суффикса использует значение Major версии приложения. Кроме того, мы видим, что начиная с версии 19 (AutoCAD 2013) функция была перенесена в др. библиотеку (т.е. изменился префикс в её имени). Зная обозначенные выше правила, можно динамически вычислить имя DLL файла, в котором находится интересующая нас функция.

Далее показываю небольшой пример того, как можно динамически вызывать функцию acedSetEnv в разных версиях AutoCAD, не создавая для неё статических обёрток. На всякий случай напоминаю, что до версии AutoCAD 2013 эта функция находилась в файле acad.exe, а начиная с AutoCAD 2013 была перенесена в accore.dll.

Код проверялся в AutoCAD 2009 и 2016.

-------------------------------------------------------------
/* Utils.cs */
using System;
using System.Text;
using System.Runtime.InteropServices;
using cad = Autodesk.AutoCAD.ApplicationServices.Application;

namespace Bushman.AutoCAD.Sandbox {

    [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet
        = CharSet.Auto)]
    public delegate int acedSetEnvDelegate(string envName,
        StringBuilder NewValue);

    public static class Utils {

        [DllImport("kernel32.dll")]
        public static extern IntPtr LoadLibrary(string
            dllToLoad);

        [DllImport("kernel32.dll")]
        public static extern IntPtr GetProcAddress(IntPtr
            hModule, string procedureName);


        [DllImport("kernel32.dll")]
        public static extern bool FreeLibrary(IntPtr hModule);

        const int AutoCAD_2013_Major = 19;

        public static string GetDllName() {

            if (cad.Version.Major < AutoCAD_2013_Major)
                return "acad.exe";
            else
                return "accore.dll";
        }

        public static acedSetEnvDelegate acedSetEnv;

        static Utils() {
            acedSetEnv = GetAcedSetEnv();
        }

        static acedSetEnvDelegate GetAcedSetEnv() {

            string dllName = Utils.GetDllName();
            IntPtr pDll = Utils.LoadLibrary(dllName);

            if (pDll == IntPtr.Zero) {
                return null;
            }

            string funcName = "acedSetEnv";

            IntPtr pAddressOfFunctionToCall = Utils
                .GetProcAddress(pDll, funcName);

            if (pAddressOfFunctionToCall == IntPtr.Zero) {

                return null;
            }

            acedSetEnvDelegate acedSetEnv = (
                acedSetEnvDelegate) Marshal
                .GetDelegateForFunctionPointer(
                pAddressOfFunctionToCall,
                typeof(acedSetEnvDelegate));

            bool result = Utils.FreeLibrary(pDll);

            return acedSetEnv;
        }
    }
}

 ------------------------------------------------------------

Далее создадим тестовую команду, которая изменит значение переменной "ACAD":

------------------------------------------------------------
/* Commands.cs */
using System;
using System.Runtime.InteropServices;
using System.Text;
using cad = Autodesk.AutoCAD.ApplicationServices.Application;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;

namespace Bushman.AutoCAD.Sandbox {

    public sealed class Commands {

        [CommandMethod("Test", CommandFlags.Modal)]
        public void Test() {

            Document doc = cad.DocumentManager
                .MdiActiveDocument;

            if (doc == null)
                return;

            Editor ed = doc.Editor;

            acedSetEnvDelegate acedSetEnv = Utils.acedSetEnv;

            if (acedSetEnv == null) {
                ed.WriteMessage("acedSetEnv function was " +
                    "not found.");
                return;
            }

            /* For example, we'll edit the "Support File Search
             * Path" value: */
            string varName = "ACAD";
            StringBuilder sb = new StringBuilder(
                @"C:\abc\def");

            int theResult = acedSetEnv(varName, sb); // 5100
        }
    }
}

---------------------------------------------------------------

Динамическое связывание и изменение системой переменной ACAD успешно происходит в обоих тестируемых версиях AutoCAD: 2009 и 2016.



Работа с реестром в операционных системах Windows (часть 2)

Когда-то здесь я выкладывал инструменты по работе с реестром для .NET 3.5 SP1. Обнаружилось, что начиная с .NET 4.0 сигнатура нужных для обозначенного кода конструкторов класса RegistryKey была изменена. Как следствие - если наша сборка, скомпилированная под .NET 3.5 SP1 в дальнейшем окажется загруженной в .NET 4.0 (или любую более новую), то мы будем получать исключение времени выполнения при вызове некоторых методов опубликованного кода.


Я внёс некоторые правки в исходный код класса RegistryExtensions и написал несколько интеграционных тестов, проверяющих корректность работы кода в различных ситуациях (в т.ч. избавился от всех директив препроцессора - теперь они не нужны). Проверял в .NET 3.5 SP1, 4.0 и 4.6.1 для платформ x86|x64|AnyCPU:


Обозначенный код я поддерживаю в виду того, что у меня имеется в наработке некоторый объем кода, компилируемого под .NET 3.5 SP1 (т.к. AutoCAD 2009 не поддерживает более новые версии .Net Framework). Этот код должен без проблем компилироваться и успешно работать  не только в .NET 3.5 SP1, но и во всех более новых версиях .NET. Кроме того, необходимо, чтобы ранее скомпилированные мною под .NET 3.5 SP1 сборки успешно загружались и работали в .NET 4.0 - 4.6.1.

Итак, ниже - обновлённый вариант кода, устраняющий проблему, упомянутую в начале заметки. Сразу за ним размещён исходный код интеграционных тестов. Пришлось воспользоваться интеграционными тестами, т.к. модульные применительно к данной задаче не подойдут.

======================================================================
/* AcProducts
 * RegistryExtensions.cs
 * © Андрей Бушман, 2014
 *
 * В файле RegistryExtensions.cs определён дополнительный
 * функционал, расширяющий возможности работы с реестром из
 * .NET приложений, написанных на .NET 3.5 SP1.
 *
 * ИЗМЕНЕНИЯ:
 * 10-авг-2016
 *      В код внесены правки для того, чтобы он мог
 *      использоваться не только в .NET 3.5 SP1, но и во всех
 *      более новых версиях .NET Framework: например, когда
 *      сборка, скомпилированная под .NET 3.5 SP1 загружается в
 *      код .Net 4.6.1. Код не зависит от разрядности (x86|x64|
 *      AnyCPU).
 */
using System;
using Microsoft.Win32;
using System.Reflection;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;

namespace Bushman.AcProducts {

    /// <summary>
    /// Данный класс предназначен для предоставления 32-битным
    /// приложениям доступа к 64-битным разделам реестра. Класс
    /// так же может использоваться для предоставления
    /// 64-битным приложениям ветке реестра, предназначенной
    /// для 32-битных. За основу взят код, опубликованный в
    /// блоге http://clck.ru/96A9U
    /// </summary>

    public static class RegistryExtensions {

        /// <summary>
        /// Открытие ключа реестра, с указанием того, какую
        /// именно часть следует открывать: записи для
        /// 32-битных приложений, или же записи для 64-битных.
        ///
        /// ВНИМАНИЕ
        /// У данного метода имеется побочный эффект: свойство
        /// `Name` у возвращаемого объекта `RegistryKey` даёт
        /// пустую строку. Однако это не страшно, т.к. имя нам
        /// известно - первую часть можно получить из родителя,
        /// а вторую часть этого имени мы и так знаем,
        /// поскольку передаём её в виде параметра.
        /// </summary>
        /// <param name="parentKey">Родительский элемент
        /// RegistryKey, в котором следует выполнить открытие
        /// подраздера.</param>
        /// <param name="subKeyName">Name of the key to be
        /// opened</param>
        /// <param name="writable">true - открывать для чтения
        /// и записи; false - открывать только для чтения.
        /// </param>
        /// <param name="options">Какую именно часть реестра
        /// следует открывать: относящуюся к 32-битным
        /// приложениям или же относящуюся к 64-битным.
        /// </param>
        /// <returns>Возвращается RegistryKey или null, если по
        /// каким-либо причинам получить RegistryKey не
        /// удалось.</returns>
        public static RegistryKey OpenSubKey(this RegistryKey
            parentKey, String subKeyName, Boolean writable,
            RegWow64Options options) {

            // Проверка работоспособности
            if (parentKey == null || GetPtr(
                parentKey) == IntPtr.Zero) {

                return null;
            }

            // Назначение прав
            Int32 rights = (Int32) (writable ? RegistryRights
                .WriteKey : RegistryRights.ReadKey);

            // Вызов функций неуправляемого кода
            Int32 subKeyHandle, result = RegOpenKeyEx(
                GetPtr(parentKey), subKeyName, 0,
                rights | (Int32) options, out subKeyHandle);

            // Если мы ошиблись - возвращаем null
            if (result != 0) {

                return null;
            }

            /* Получаем ключ, представленный указателем,
             * возвращённым из RegOpenKeyEx */
            RegistryKey subKey = PtrToRegistryKey((IntPtr)
                subKeyHandle, writable, false, options);

            return subKey;
        }

        /// <summary>
        /// Получить указатель на ключ реестра.
        /// </summary>
        /// <param name="registryKey">Ключ реестра, указатель
        /// на который нужно получить.
        /// </param>
        /// <returns>Возвращается объект IntPtr. Если не
        /// удалось получить указатель на обозначенный объект
        /// RegistryKey, то возвращается IntPtr.Zero.</returns>
        public static IntPtr GetPtr(this RegistryKey
            registryKey) {

            if (registryKey == null)
                return IntPtr.Zero;

            /* The `RegistryKey.Handle` property appears since
             * .Net 4.0, therefore for .Net 3.5 I get it
             * through reflection. */
            Type registryKeyType = typeof(RegistryKey);

            FieldInfo fieldInfo = registryKeyType.GetField(
                "hkey", BindingFlags.NonPublic | BindingFlags
                .Instance);

            SafeHandle handle = (SafeHandle) fieldInfo.GetValue
                (registryKey);

            IntPtr unsafeHandle = handle.DangerousGetHandle(
                );

            return unsafeHandle;
        }

        /// <summary>
        /// Получить ключ реестра на основе его указателя.
        /// </summary>
        /// <param name="hKey">Указатель на ключ реестра
        /// </param>
        /// <param name="writable">true - открыть для записи;
        /// false - для чтения.</param>
        /// <param name="ownsHandle">Владеем ли мы
        /// дескриптором: true - да, false - нет.</param>
        /// <returns>Возвращается объект RegistryKey,
        /// соответствующий полученному указателю.</returns>
        public static RegistryKey PtrToRegistryKey(IntPtr
            hKey, Boolean writable, Boolean ownsHandle,
            RegWow64Options opt) {

            if (IntPtr.Zero == hKey) {
                return null;
            }

            Type safeRegistryHandleType =
                typeof(SafeHandleZeroOrMinusOneIsInvalid)
                .Assembly.GetType("Microsoft.Win32." +
                "SafeHandles.SafeRegistryHandle");

            /* Получаем массив типов, соответствующих
             * аргументом конструктора, который нам нужен. */
            Type[] argTypes = new Type[] { typeof(IntPtr),
                typeof(Boolean) };

            BindingFlags flags = default(BindingFlags);

            if (Environment.Version.Major < 4) {
                flags = BindingFlags.Instance | BindingFlags
                    .NonPublic;
            }
            else {
                flags = BindingFlags.Instance | BindingFlags
                    .Public;
            }

            // Получаем ConstructorInfo для нашего объекта
            ConstructorInfo safeRegistryHandleCtorInfo =
                safeRegistryHandleType.GetConstructor(flags,
                    null, argTypes, null);

            /* Вызываем конструктор для SafeRegistryHandle.
             * Класс SafeRegistryHandle появился начиная с .NET
             * 4.0. */
            Object safeHandle = safeRegistryHandleCtorInfo
                .Invoke(new Object[] { hKey, ownsHandle });

            Type registryKeyType = typeof(RegistryKey);
            Type registryViewType = null;

            /*Получаем массив типов, соответствующих аргументом
             * конструктора, который нам нужен */
            Type[] registryKeyConstructorTypes = null;

            if (Environment.Version.Major < 4) {
                registryKeyConstructorTypes = new Type[] {
                safeRegistryHandleType, typeof(bool) };
            }
            else {
                registryViewType = typeof(
                    SafeHandleZeroOrMinusOneIsInvalid).Assembly
                    .GetType("Microsoft.Win32.RegistryView");

                registryKeyConstructorTypes = new Type[] {
                    safeRegistryHandleType, typeof(bool),
                    registryViewType };

                flags = BindingFlags.Instance | BindingFlags
                    .NonPublic;
            }

            // Получаем ConstructorInfo для нашего объекта
            ConstructorInfo registryKeyCtorInfo =
                registryKeyType.GetConstructor(flags, null,
                registryKeyConstructorTypes, null);

            RegistryKey resultKey = null;

            if (Environment.Version.Major < 4) {
                // Вызываем конструктор для RegistryKey
                resultKey = (RegistryKey) registryKeyCtorInfo
                    .Invoke(new Object[] {
                    safeHandle, writable });
            }
            else {
                // Вызываем конструктор для RegistryKey
                resultKey = (RegistryKey) registryKeyCtorInfo
                    .Invoke(new Object[] {
                    safeHandle, writable, (int) opt});
            }

            // возвращаем полученный ключ реестра
            return resultKey;
        }

        /// <summary>
        /// Получение числового значения указателя на искомый
        /// подраздел реестра.
        /// </summary>
        /// <param name="hKey">Указатель на родительский раздел
        /// реестра.</param>
        /// <param name="subKey">Имя искомого подраздела.
        /// </param>
        /// <param name="ulOptions">Этот параметр
        /// зарезервирован и всегда должен быть равным 0.
        /// </param>
        /// <param name="samDesired">Права доступа (чтение\
        /// запись) и указание того, как именно следует
        /// открывать реестр. Значение этого параметра
        /// формируется путём применения операции логического
        /// "И" для объектов перечислений RegistryRights и
        /// RegWow64Options.</param>
        /// <param name="phkResult">Ссылка на переменную, в
        /// которую следует сохранить полученное числовое
        /// значение указателя на искомый подраздел.</param>
        /// <returns>В случае успеха возвращается 0.</returns>
        [DllImport("advapi32.dll", CharSet = CharSet.Auto)]
        static extern Int32 RegOpenKeyEx(IntPtr hKey,
            String subKey, Int32 ulOptions, Int32 samDesired,
            out Int32 phkResult);
    }

    /// <summary>
    /// Перечисление указывает, какую именно часть реестра
    /// следует открывать: относящуюся к 32-битным приложениям
    /// или же относящуюся к 64-битным.
    /// </summary>
    public enum RegWow64Options {

        /// <summary>
        /// Открывать ту часть реестра, которая хранит
        /// информацию приложений, разрядность которых
        /// соответствует разрядности текущего приложения
        /// (x86\x64).</summary>
        None = 0,
        /// <summary>
        /// Открывать часть реестра, относящуюся к 64-битным
        /// приложениям.
        /// </summary>
        KEY_WOW64_64KEY = 0x0100,
        /// <summary>
        /// Открывать часть реестра, относящуюся к 32-битным
        /// приложениям.
        /// </summary>
        KEY_WOW64_32KEY = 0x0200
    }

    /// <summary>
    /// Перечисление, указывающее на то, с каким уровнем
    /// доступа следует открывать ветку реестра: для чтения,
    /// или же для чтения\записи.
    /// </summary>
    public enum RegistryRights {

        /// <summary>
        /// Открыть только для чтения.
        /// </summary>
        ReadKey = 131097,
        /// <summary>
        /// Открыть для чтения и записи.
        /// </summary>
        WriteKey = 131078
    }
}
==================================================================

Интеграционные тесты:

==================================================================
/* AcProducts.IntegrationTests
 * RegistryExtensionsTests.cs
 * © Andrey Bushman, 2016
 *
 * Integration tests of Bushman.AcProducts.RegistryExtensions
 * class.
 */
using System;
using Microsoft.Win32;
using NUnit.Framework;

namespace Bushman.AcProducts.IntegrationTests {

    [TestFixture]
    public class RegistryExtensionsTests {

        [Test]
        public void GetPtr_Returns_ValidPtr() {

            // `Registry.LocalMachine` key always exists.
            RegistryKey rk = Registry.LocalMachine;
            IntPtr rkPtr = rk.Handle.DangerousGetHandle();

            /* The `RegistryKey.Handle` property appears since
             * .Net 4.0, therefore for .Net 3.5 in the code
             * under test I get it through reflection. */
            IntPtr rkPtr2 = RegistryExtensions.GetPtr(rk);

            Assert.AreEqual(rkPtr, rkPtr2);
        }

        [Test]
        public void GetPtr_ReturnsZero_ForNull() {

            /* The `RegistryKey.Handle` property appears since
             * .Net 4.0, therefore for .Net 3.5 in the code
             * under test I get it through reflection. */
            IntPtr ptr = RegistryExtensions.GetPtr(null);

            Assert.AreEqual(IntPtr.Zero, ptr);
        }

        [TestCase(@"SOFTWARE\7-Zip", RegWow64Options
            .KEY_WOW64_64KEY, false)]

        [TestCase(@"SOFTWARE\7-Zip", RegWow64Options
            .KEY_WOW64_32KEY, true)]

        [TestCase(@"SOFTWARE\Notepad++",
            RegWow64Options.KEY_WOW64_64KEY, true)]

        [TestCase(@"SOFTWARE\Notepad++",
            RegWow64Options.KEY_WOW64_32KEY, false)]

        [Description("7-Zip and Notepad++ are to be installed"
            )]
        public void OpenSubKey_ReturnsValidValue(string subkey,
            RegWow64Options opt, bool isNull) {

            RegistryKey rk = Registry.LocalMachine;

            RegistryKey rk2 = RegistryExtensions.OpenSubKey(rk,
                subkey, false, opt);

            if (isNull) {
                Assert.IsNull(rk2);
            }
            else {
                Assert.IsNotNull(rk2);
            }
        }

        [Test]
        public void OpenSubKey_ReturnsNull_ForInvalidSubkey() {

            string subkey =
                "{F28A3464-0A5D-48FB-AFF6-B07F058D3EFC}";

            RegistryKey rk = RegistryExtensions.OpenSubKey(
                Registry.LocalMachine, subkey, false,
                RegWow64Options.None);

            Assert.IsNull(rk);
        }

        [Test]
        public void PtrToRegistryKey_Returns_ValidValie() {

            RegistryKey rk = Registry.LocalMachine;
            IntPtr rkPtr = rk.Handle.DangerousGetHandle();

            RegistryKey rk2 = RegistryExtensions
                .PtrToRegistryKey(rkPtr, false, false,
                RegWow64Options.None);
            IntPtr rkPtr2 = rk2.Handle.DangerousGetHandle();

            Assert.AreEqual(rkPtr, rkPtr2);
        }

        [Test]
        public void PtrToRegistryKey_ReturnsNull_ForZero() {

            RegistryKey rk = RegistryExtensions
                .PtrToRegistryKey(IntPtr.Zero, false, false,
                RegWow64Options.None);

            Assert.IsNull(rk);
        }
    }
}
==================================================================

Работа с реестром в операционных системах Windows (часть 2)

Когда-то здесь я выкладывал инструменты по работе с реестром для .NET 3.5 SP1. Обнаружилось, что начиная с .NET 4.0 сигнатура нужных для обозначенного кода конструкторов класса RegistryKey была изменена. Как следствие - если наша сборка, скомпилированная под .NET 3.5 SP1 в дальнейшем окажется загруженной в .NET 4.0 (или любую более новую), то мы будем получать исключение времени выполнения при вызове некоторых методов опубликованного кода.


Я внёс некоторые правки в исходный код класса RegistryExtensions и написал несколько интеграционных тестов, проверяющих корректность работы кода в различных ситуациях (в т.ч. избавился от всех директив препроцессора - теперь они не нужны). Проверял в .NET 3.5 SP1, 4.0 и 4.6.1 для платформ x86|x64|AnyCPU:


Обозначенный код я поддерживаю в виду того, что у меня имеется в наработке некоторый объем кода, компилируемого под .NET 3.5 SP1 (т.к. AutoCAD 2009 не поддерживает более новые версии .Net Framework). Этот код должен без проблем компилироваться и успешно работать  не только в .NET 3.5 SP1, но и во всех более новых версиях .NET. Кроме того, необходимо, чтобы ранее скомпилированные мною под .NET 3.5 SP1 сборки успешно загружались и работали в .NET 4.0 - 4.6.1.

Итак, ниже - обновлённый вариант кода, устраняющий проблему, упомянутую в начале заметки. Сразу за ним размещён исходный код интеграционных тестов. Пришлось воспользоваться интеграционными тестами, т.к. модульные применительно к данной задаче не подойдут.

======================================================================
/* AcProducts
 * RegistryExtensions.cs
 * © Андрей Бушман, 2014
 *
 * В файле RegistryExtensions.cs определён дополнительный
 * функционал, расширяющий возможности работы с реестром из
 * .NET приложений, написанных на .NET 3.5 SP1.
 *
 * ИЗМЕНЕНИЯ:
 * 10-авг-2016
 *      В код внесены правки для того, чтобы он мог
 *      использоваться не только в .NET 3.5 SP1, но и во всех
 *      более новых версиях .NET Framework: например, когда
 *      сборка, скомпилированная под .NET 3.5 SP1 загружается в
 *      код .Net 4.6.1. Код не зависит от разрядности (x86|x64|
 *      AnyCPU).
 */
using System;
using Microsoft.Win32;
using System.Reflection;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;

namespace Bushman.AcProducts {

    /// <summary>
    /// Данный класс предназначен для предоставления 32-битным
    /// приложениям доступа к 64-битным разделам реестра. Класс
    /// так же может использоваться для предоставления
    /// 64-битным приложениям ветке реестра, предназначенной
    /// для 32-битных. За основу взят код, опубликованный в
    /// блоге http://clck.ru/96A9U
    /// </summary>

    public static class RegistryExtensions {

        /// <summary>
        /// Открытие ключа реестра, с указанием того, какую
        /// именно часть следует открывать: записи для
        /// 32-битных приложений, или же записи для 64-битных.
        ///
        /// ВНИМАНИЕ
        /// У данного метода имеется побочный эффект: свойство
        /// `Name` у возвращаемого объекта `RegistryKey` даёт
        /// пустую строку. Однако это не страшно, т.к. имя нам
        /// известно - первую часть можно получить из родителя,
        /// а вторую часть этого имени мы и так знаем,
        /// поскольку передаём её в виде параметра.
        /// </summary>
        /// <param name="parentKey">Родительский элемент
        /// RegistryKey, в котором следует выполнить открытие
        /// подраздера.</param>
        /// <param name="subKeyName">Name of the key to be
        /// opened</param>
        /// <param name="writable">true - открывать для чтения
        /// и записи; false - открывать только для чтения.
        /// </param>
        /// <param name="options">Какую именно часть реестра
        /// следует открывать: относящуюся к 32-битным
        /// приложениям или же относящуюся к 64-битным.
        /// </param>
        /// <returns>Возвращается RegistryKey или null, если по
        /// каким-либо причинам получить RegistryKey не
        /// удалось.</returns>
        public static RegistryKey OpenSubKey(this RegistryKey
            parentKey, String subKeyName, Boolean writable,
            RegWow64Options options) {

            // Проверка работоспособности
            if (parentKey == null || GetPtr(
                parentKey) == IntPtr.Zero) {

                return null;
            }

            // Назначение прав
            Int32 rights = (Int32) (writable ? RegistryRights
                .WriteKey : RegistryRights.ReadKey);

            // Вызов функций неуправляемого кода
            Int32 subKeyHandle, result = RegOpenKeyEx(
                GetPtr(parentKey), subKeyName, 0,
                rights | (Int32) options, out subKeyHandle);

            // Если мы ошиблись - возвращаем null
            if (result != 0) {

                return null;
            }

            /* Получаем ключ, представленный указателем,
             * возвращённым из RegOpenKeyEx */
            RegistryKey subKey = PtrToRegistryKey((IntPtr)
                subKeyHandle, writable, false, options);

            return subKey;
        }

        /// <summary>
        /// Получить указатель на ключ реестра.
        /// </summary>
        /// <param name="registryKey">Ключ реестра, указатель
        /// на который нужно получить.
        /// </param>
        /// <returns>Возвращается объект IntPtr. Если не
        /// удалось получить указатель на обозначенный объект
        /// RegistryKey, то возвращается IntPtr.Zero.</returns>
        public static IntPtr GetPtr(this RegistryKey
            registryKey) {

            if (registryKey == null)
                return IntPtr.Zero;

            /* The `RegistryKey.Handle` property appears since
             * .Net 4.0, therefore for .Net 3.5 I get it
             * through reflection. */
            Type registryKeyType = typeof(RegistryKey);

            FieldInfo fieldInfo = registryKeyType.GetField(
                "hkey", BindingFlags.NonPublic | BindingFlags
                .Instance);

            SafeHandle handle = (SafeHandle) fieldInfo.GetValue
                (registryKey);

            IntPtr unsafeHandle = handle.DangerousGetHandle(
                );

            return unsafeHandle;
        }

        /// <summary>
        /// Получить ключ реестра на основе его указателя.
        /// </summary>
        /// <param name="hKey">Указатель на ключ реестра
        /// </param>
        /// <param name="writable">true - открыть для записи;
        /// false - для чтения.</param>
        /// <param name="ownsHandle">Владеем ли мы
        /// дескриптором: true - да, false - нет.</param>
        /// <returns>Возвращается объект RegistryKey,
        /// соответствующий полученному указателю.</returns>
        public static RegistryKey PtrToRegistryKey(IntPtr
            hKey, Boolean writable, Boolean ownsHandle,
            RegWow64Options opt) {

            if (IntPtr.Zero == hKey) {
                return null;
            }

            Type safeRegistryHandleType =
                typeof(SafeHandleZeroOrMinusOneIsInvalid)
                .Assembly.GetType("Microsoft.Win32." +
                "SafeHandles.SafeRegistryHandle");

            /* Получаем массив типов, соответствующих
             * аргументом конструктора, который нам нужен. */
            Type[] argTypes = new Type[] { typeof(IntPtr),
                typeof(Boolean) };

            BindingFlags flags = default(BindingFlags);

            if (Environment.Version.Major < 4) {
                flags = BindingFlags.Instance | BindingFlags
                    .NonPublic;
            }
            else {
                flags = BindingFlags.Instance | BindingFlags
                    .Public;
            }

            // Получаем ConstructorInfo для нашего объекта
            ConstructorInfo safeRegistryHandleCtorInfo =
                safeRegistryHandleType.GetConstructor(flags,
                    null, argTypes, null);

            /* Вызываем конструктор для SafeRegistryHandle.
             * Класс SafeRegistryHandle появился начиная с .NET
             * 4.0. */
            Object safeHandle = safeRegistryHandleCtorInfo
                .Invoke(new Object[] { hKey, ownsHandle });

            Type registryKeyType = typeof(RegistryKey);
            Type registryViewType = null;

            /*Получаем массив типов, соответствующих аргументом
             * конструктора, который нам нужен */
            Type[] registryKeyConstructorTypes = null;

            if (Environment.Version.Major < 4) {
                registryKeyConstructorTypes = new Type[] {
                safeRegistryHandleType, typeof(bool) };
            }
            else {
                registryViewType = typeof(
                    SafeHandleZeroOrMinusOneIsInvalid).Assembly
                    .GetType("Microsoft.Win32.RegistryView");

                registryKeyConstructorTypes = new Type[] {
                    safeRegistryHandleType, typeof(bool),
                    registryViewType };

                flags = BindingFlags.Instance | BindingFlags
                    .NonPublic;
            }

            // Получаем ConstructorInfo для нашего объекта
            ConstructorInfo registryKeyCtorInfo =
                registryKeyType.GetConstructor(flags, null,
                registryKeyConstructorTypes, null);

            RegistryKey resultKey = null;

            if (Environment.Version.Major < 4) {
                // Вызываем конструктор для RegistryKey
                resultKey = (RegistryKey) registryKeyCtorInfo
                    .Invoke(new Object[] {
                    safeHandle, writable });
            }
            else {
                // Вызываем конструктор для RegistryKey
                resultKey = (RegistryKey) registryKeyCtorInfo
                    .Invoke(new Object[] {
                    safeHandle, writable, (int) opt});
            }

            // возвращаем полученный ключ реестра
            return resultKey;
        }

        /// <summary>
        /// Получение числового значения указателя на искомый
        /// подраздел реестра.
        /// </summary>
        /// <param name="hKey">Указатель на родительский раздел
        /// реестра.</param>
        /// <param name="subKey">Имя искомого подраздела.
        /// </param>
        /// <param name="ulOptions">Этот параметр
        /// зарезервирован и всегда должен быть равным 0.
        /// </param>
        /// <param name="samDesired">Права доступа (чтение\
        /// запись) и указание того, как именно следует
        /// открывать реестр. Значение этого параметра
        /// формируется путём применения операции логического
        /// "И" для объектов перечислений RegistryRights и
        /// RegWow64Options.</param>
        /// <param name="phkResult">Ссылка на переменную, в
        /// которую следует сохранить полученное числовое
        /// значение указателя на искомый подраздел.</param>
        /// <returns>В случае успеха возвращается 0.</returns>
        [DllImport("advapi32.dll", CharSet = CharSet.Auto)]
        static extern Int32 RegOpenKeyEx(IntPtr hKey,
            String subKey, Int32 ulOptions, Int32 samDesired,
            out Int32 phkResult);
    }

    /// <summary>
    /// Перечисление указывает, какую именно часть реестра
    /// следует открывать: относящуюся к 32-битным приложениям
    /// или же относящуюся к 64-битным.
    /// </summary>
    public enum RegWow64Options {

        /// <summary>
        /// Открывать ту часть реестра, которая хранит
        /// информацию приложений, разрядность которых
        /// соответствует разрядности текущего приложения
        /// (x86\x64).</summary>
        None = 0,
        /// <summary>
        /// Открывать часть реестра, относящуюся к 64-битным
        /// приложениям.
        /// </summary>
        KEY_WOW64_64KEY = 0x0100,
        /// <summary>
        /// Открывать часть реестра, относящуюся к 32-битным
        /// приложениям.
        /// </summary>
        KEY_WOW64_32KEY = 0x0200
    }

    /// <summary>
    /// Перечисление, указывающее на то, с каким уровнем
    /// доступа следует открывать ветку реестра: для чтения,
    /// или же для чтения\записи.
    /// </summary>
    public enum RegistryRights {

        /// <summary>
        /// Открыть только для чтения.
        /// </summary>
        ReadKey = 131097,
        /// <summary>
        /// Открыть для чтения и записи.
        /// </summary>
        WriteKey = 131078
    }
}
==================================================================

Интеграционные тесты:

==================================================================
/* AcProducts.IntegrationTests
 * RegistryExtensionsTests.cs
 * © Andrey Bushman, 2016
 *
 * Integration tests of Bushman.AcProducts.RegistryExtensions
 * class.
 */
using System;
using Microsoft.Win32;
using NUnit.Framework;

namespace Bushman.AcProducts.IntegrationTests {

    [TestFixture]
    public class RegistryExtensionsTests {

        [Test]
        public void GetPtr_Returns_ValidPtr() {

            // `Registry.LocalMachine` key always exists.
            RegistryKey rk = Registry.LocalMachine;
            IntPtr rkPtr = rk.Handle.DangerousGetHandle();

            /* The `RegistryKey.Handle` property appears since
             * .Net 4.0, therefore for .Net 3.5 in the code
             * under test I get it through reflection. */
            IntPtr rkPtr2 = RegistryExtensions.GetPtr(rk);

            Assert.AreEqual(rkPtr, rkPtr2);
        }

        [Test]
        public void GetPtr_ReturnsZero_ForNull() {

            /* The `RegistryKey.Handle` property appears since
             * .Net 4.0, therefore for .Net 3.5 in the code
             * under test I get it through reflection. */
            IntPtr ptr = RegistryExtensions.GetPtr(null);

            Assert.AreEqual(IntPtr.Zero, ptr);
        }

        [TestCase(@"SOFTWARE\7-Zip", RegWow64Options
            .KEY_WOW64_64KEY, false)]

        [TestCase(@"SOFTWARE\7-Zip", RegWow64Options
            .KEY_WOW64_32KEY, true)]

        [TestCase(@"SOFTWARE\Notepad++",
            RegWow64Options.KEY_WOW64_64KEY, true)]

        [TestCase(@"SOFTWARE\Notepad++",
            RegWow64Options.KEY_WOW64_32KEY, false)]

        [Description("7-Zip and Notepad++ are to be installed"
            )]
        public void OpenSubKey_ReturnsValidValue(string subkey,
            RegWow64Options opt, bool isNull) {

            RegistryKey rk = Registry.LocalMachine;

            RegistryKey rk2 = RegistryExtensions.OpenSubKey(rk,
                subkey, false, opt);

            if (isNull) {
                Assert.IsNull(rk2);
            }
            else {
                Assert.IsNotNull(rk2);
            }
        }

        [Test]
        public void OpenSubKey_ReturnsNull_ForInvalidSubkey() {

            string subkey =
                "{F28A3464-0A5D-48FB-AFF6-B07F058D3EFC}";

            RegistryKey rk = RegistryExtensions.OpenSubKey(
                Registry.LocalMachine, subkey, false,
                RegWow64Options.None);

            Assert.IsNull(rk);
        }

        [Test]
        public void PtrToRegistryKey_Returns_ValidValie() {

            RegistryKey rk = Registry.LocalMachine;
            IntPtr rkPtr = rk.Handle.DangerousGetHandle();

            RegistryKey rk2 = RegistryExtensions
                .PtrToRegistryKey(rkPtr, false, false,
                RegWow64Options.None);
            IntPtr rkPtr2 = rk2.Handle.DangerousGetHandle();

            Assert.AreEqual(rkPtr, rkPtr2);
        }

        [Test]
        public void PtrToRegistryKey_ReturnsNull_ForZero() {

            RegistryKey rk = RegistryExtensions
                .PtrToRegistryKey(IntPtr.Zero, false, false,
                RegWow64Options.None);

            Assert.IsNull(rk);
        }
    }
}
==================================================================

NuGet пакеты для AutoCAD .NET API

Пару лет назад, компания Autodesk начала (наконец-то!) опубликовывать NuGet-пакеты для AutoCAD .NET API. Об этом так же было радостно сообщено в блоге ADN. Однако, как это обычно и бывает у Autodesk, тестированием пакетов перед их публикацией в Autodesk не заморачиваются (как собственно и тестированием самого AutoCAD, ибо длительная, печальная практика показывает, что группа тестирования даже самого AutoCAD в компании Autodesk в принципе нарисована только "для галочки" - примеров, доказывающих это - множество, в т.ч. и в моём блоге).

 Пробежавшись по комментариям, оставленным пользователями в обозначенной выше записи блога ADN можно сразу заметить, что пакеты работают криво. Т.е. их тестированием себя никто не утруждал. С тех пор Autodesk выпустил ещё две версии AutoCAD. Казалось бы, что за два года можно было вполне довести до ума пакеты, на создание которых (с нуля) уходит не более получаса (причём сразу под все версии AutoCAD от 2009 и выше)... Но печальная практика в который раз демонстрирует обратное...

Я попробовал установить самую свежую на сегодняшний день версию пакета AutoCAD.NET (version 21.0.1). В душе теплилась надежда, что за два года проблему с CopyLocal в Autodesk всё же победили, тем более что её решение находится в Google за пару секунд по элементарной фразе: "NuGet CopyLocal false". Однако, как оказалось, воз и поныне там - ни одна сборка, подключенная путём установки NuGet пакета не имеет свойства CopyLocal установленного в false.

Я не верю в то, что в Autodesk работают настолько глупые люди (глупых бы не взяли на работу, как мне кажется). То, что я систематически вижу, всякий раз подтверждает моё давно сформировавшееся убеждение в том, что в Autodesk работают люди, многим из которых просто наплевать на качество своей работы (это ещё хуже, чем первый вариант)... К таким людям (ИМХО) на все 100% относится группа тестирования (во главе с Михаилом Белиловским, насколько я помню), которая выполняет свою работу (если вообще выполняет) из рук вон плохо - достаточно посмотреть на качество работы того же accoreconsole.exe. Подобным отношением к своей работе страдает и тот сотрудник Autodesk, которому поручили собирать и опубликовывать NuGet пакеты AutoCAD.NET. Можно сколько угодно с умным видом разъезжать по различным конференциям, изображая бурную радость и воодушевление в процессе разговоров о программировании в AutoCAD, но если ты не выполняешь свою прямую, первостепенную обязанность - отвечать за качество тестирования продукта, то грош цена таким разговорам, улыбкам... да и самим специалистам (ИМХО).

 Ладно, не буду более трепать себе понапрасну нервы из-за качества работы обозначенных выше персонажей (моих нервов они не стоят :) ) - давайте лучше вернёмся к их злосчастным NuGet пакетам...

Отсутствие нужного значения в свойстве CopyLocal не является единственной проблемой. Текущая реализация пакетов подразумевает, что каждый раз, запуская команду Update-Package (без опции -Version) вы тем самым будете обновлять все сборки AutoCAD, подключенные в вашем проекте, до самой последней версии AutoCAD (для которой имеется соответствующий NuGet пакет).

Однако вовсе не факт, что разработчик захочет именно такое поведение. Лично я хочу, чтобы если я в своём проекте указал использование сборок от AutoCAD 2016, то и все обновления должны происходить только в рамках интересующей меня версии AutoCAD. Мне не нужно, чтобы происходило автоматическое обновление сборок до версии AutoCAD 2017, 2018 или 2020 - не надо за меня принимать подобных решений(!!!)...

Если мне понадобится создать отдельную сборку для более новой версии AutoCAD, то я сам создам дополнительный проект в составе своего решения, и уже в него подключу NuGet пакет более новой, нужной мне версии AutoCAD, с последующим добавлением файлов исходного кода из предыдущего проекта (добавленных посредством ссылок на оригиналы).

Для того, чтобы избежать обозначенной выше проблемы, созданной Autodesk'ом, нужно для каждой версии AutoCAD формировать (всего лишь!) уникальное имя пакета (т.е. его Id). Значения Minor и Major для версий этих пакетов можно брать либо из соответствующий значений версий AutoCAD, либо начинать с 1 (это не имеет значения, но выбрав стиль нумерации версий, следует придерживаться его и в др. пакетах во избежание путанницы).

Autodesk предоставляет NuGet пакеты только начиная с версии AutoCAD  2015. Для более поздних версий пакеты не предоставляются.

Поскольку NuGet пакеты компани Autodesk не пригодны к полноценному использованию в том виде, в котором они уже который год поставляются пользователям (ИМХО), а так же поскольку отсутствуют пакеты для интересующих меня версий AutoCAD, то я создал свои NuGet пакеты, предоставляющие AutoCAD .NET API, в которых отсутствуют обозначеные мною выше проблемы.

Чаще всего я компилирую код своих проектов под AutoCAD 2009 и 2013. Результат компиляции первого из них может затем успешно загружаться в AutoCAD 2009-2012, а второго - в AutoCAD 2013 и во все более новые версии. В виду этого, в первую очередь я создал NuGet пакеты именно для AutoCAD 2009 и 2013.

В ближайшее время я, на всякий случай, создам аналогичные NuGet пакеты для всех остальных версий AutoCAD из диапазона AutoCAD 2009-2017 (на тот случай, если мне в каком-то проекте, по какой-то причине, понадобится иная версия API).

UPD
Опубликовал NuGet пакеты для AutoCAD 2009-2017 (на каждую версию AutoCAD по три пакета).

NuGet пакеты для AutoCAD .NET API

Пару лет назад, компания Autodesk начала (наконец-то!) опубликовывать NuGet-пакеты для AutoCAD .NET API. Об этом так же было радостно сообщено в блоге ADN. Однако, как это обычно и бывает у Autodesk, тестированием пакетов перед их публикацией в Autodesk не заморачиваются (как собственно и тестированием самого AutoCAD, ибо длительная, печальная практика показывает, что группа тестирования даже самого AutoCAD в компании Autodesk в принципе нарисована только "для галочки" - примеров, доказывающих это - множество, в т.ч. и в моём блоге).

 Пробежавшись по комментариям, оставленным пользователями в обозначенной выше записи блога ADN можно сразу заметить, что пакеты работают криво. Т.е. их тестированием себя никто не утруждал. С тех пор Autodesk выпустил ещё две версии AutoCAD. Казалось бы, что за два года можно было вполне довести до ума пакеты, на создание которых (с нуля) уходит не более получаса (причём сразу под все версии AutoCAD от 2009 и выше)... Но печальная практика в который раз демонстрирует обратное...

Я попробовал установить самую свежую на сегодняшний день версию пакета AutoCAD.NET (version 21.0.1). В душе теплилась надежда, что за два года проблему с CopyLocal в Autodesk всё же победили, тем более что её решение находится в Google за пару секунд по элементарной фразе: "NuGet CopyLocal false". Однако, как оказалось, воз и поныне там - ни одна сборка, подключенная путём установки NuGet пакета не имеет свойства CopyLocal установленного в false.

Я не верю в то, что в Autodesk работают настолько глупые люди (глупых бы не взяли на работу, как мне кажется). То, что я систематически вижу, всякий раз подтверждает моё давно сформировавшееся убеждение в том, что в Autodesk работают люди, многим из которых просто наплевать на качество своей работы (это ещё хуже, чем первый вариант)... К таким людям (ИМХО) на все 100% относится группа тестирования (во главе с Михаилом Белиловским, насколько я помню), которая выполняет свою работу (если вообще выполняет) из рук вон плохо - достаточно посмотреть на качество работы того же accoreconsole.exe. Подобным отношением к своей работе страдает и тот сотрудник Autodesk, которому поручили собирать и опубликовывать NuGet пакеты AutoCAD.NET. Можно сколько угодно с умным видом разъезжать по различным конференциям, изображая бурную радость и воодушевление в процессе разговоров о программировании в AutoCAD, но если ты не выполняешь свою прямую, первостепенную обязанность - отвечать за качество тестирования продукта, то грош цена таким разговорам, улыбкам... да и самим специалистам (ИМХО).

 Ладно, не буду более трепать себе понапрасну нервы из-за качества работы обозначенных выше персонажей (моих нервов они не стоят :) ) - давайте лучше вернёмся к их злосчастным NuGet пакетам...

Отсутствие нужного значения в свойстве CopyLocal не является единственной проблемой. Текущая реализация пакетов подразумевает, что каждый раз, запуская команду Update-Package (без опции -Version) вы тем самым будете обновлять все сборки AutoCAD, подключенные в вашем проекте, до самой последней версии AutoCAD (для которой имеется соответствующий NuGet пакет).

Однако вовсе не факт, что разработчик захочет именно такое поведение. Лично я хочу, чтобы если я в своём проекте указал использование сборок от AutoCAD 2016, то и все обновления должны происходить только в рамках интересующей меня версии AutoCAD. Мне не нужно, чтобы происходило автоматическое обновление сборок до версии AutoCAD 2017, 2018 или 2020 - не надо за меня принимать подобных решений(!!!)...

Если мне понадобится создать отдельную сборку для более новой версии AutoCAD, то я сам создам дополнительный проект в составе своего решения, и уже в него подключу NuGet пакет более новой, нужной мне версии AutoCAD, с последующим добавлением файлов исходного кода из предыдущего проекта (добавленных посредством ссылок на оригиналы).

Для того, чтобы избежать обозначенной выше проблемы, созданной Autodesk'ом, нужно для каждой версии AutoCAD формировать (всего лишь!) уникальное имя пакета (т.е. его Id). Значения Minor и Major для версий этих пакетов можно брать либо из соответствующий значений версий AutoCAD, либо начинать с 1 (это не имеет значения, но выбрав стиль нумерации версий, следует придерживаться его и в др. пакетах во избежание путанницы).

Autodesk предоставляет NuGet пакеты только начиная с версии AutoCAD  2015. Для более поздних версий пакеты не предоставляются.

Поскольку NuGet пакеты компани Autodesk не пригодны к полноценному использованию в том виде, в котором они уже который год поставляются пользователям (ИМХО), а так же поскольку отсутствуют пакеты для интересующих меня версий AutoCAD, то я создал свои NuGet пакеты, предоставляющие AutoCAD .NET API, в которых отсутствуют обозначеные мною выше проблемы.

Чаще всего я компилирую код своих проектов под AutoCAD 2009 и 2013. Результат компиляции первого из них может затем успешно загружаться в AutoCAD 2009-2012, а второго - в AutoCAD 2013 и во все более новые версии. В виду этого, в первую очередь я создал NuGet пакеты именно для AutoCAD 2009 и 2013.

В ближайшее время я, на всякий случай, создам аналогичные NuGet пакеты для всех остальных версий AutoCAD из диапазона AutoCAD 2009-2017 (на тот случай, если мне в каком-то проекте, по какой-то причине, понадобится иная версия API).

UPD
Опубликовал NuGet пакеты для AutoCAD 2009-2017 (на каждую версию AutoCAD по три пакета).