Архив рубрики: WCF

Импорт конфигурационных настроек сервисами и клиентами WCF

В некоторых случаях службы и клиенты удобно реализовывать в виде отдельных DLL, которые затем могут быть использованы различными приложениями. Например: MyService.dll и MyClient.dll. Предполагается, что они будут находиться в подкаталогах ./Extensions/MyExtensionName/ хостовых приложений, дабы при необходимости оный контент всегда можно было бы просто удалить даже вручную, не зацепив при этом случайно ресурсы основного приложения.

Однако, будучи подгруженными в хостовое приложение они, как и любые другие DLL, по умолчанию будут искать свои настройки в составе конфигурационного файла этого приложения. Всё же мне бы не хотелось в чужой config-файл вносить правки, необходимые для работы моих сервисов или клиентов, ну или хотелось бы, по крайней мере, минимизировать объём таких корректировок настолько, насколько это возможно... На мой взгляд, предпочтительным способом была бы возможность сохранения нужных мне настроек в отдельных конфигурационных файлах, находящихся рядом с соответствующей DLL моего сервиса или клиента, т.е. в файлах MyService.dll.config и MyClient.dll.config.

Экспорт конфигурационных настроек на стороне сервиса
Начиная с .NET 4.5 в классе реализации сервиса можно определить метод Configure со следующей сигнатурой:

public static void Configure(ServiceConfiguration config)

Этот метод будет вызван WCF перед началом использования нашей службы. В нём можно выполнить загрузку конфигурационных настроек из внешнего файла, например так:

public static void Configure(ServiceConfiguration config) {
    ExeConfigurationFileMap configMap = new ExeConfigurationFileMap();
    configMap.ExeConfigFilename = typeof(MyService).Assembly.Location + ".config";
    Configuration dll_config = ConfigurationManager.OpenMappedExeConfiguration(configMap,
        ConfigurationUserLevel.None);
    config.LoadFromConfiguration(dll_config);
}

Однако в бочке мёда имеется и ложка дёгтя: в процессе импорта настроек WCF будет игнорировать все базовые адреса, обозначенные в импортируемом файле. Соответственно, либо эти базовые адреса нужно будет предварительно указать в конфигурационном файле самого хостового приложения (чего делать не очень хотелось бы, хотя это и не смертельно), либо в составе импортируемого файла настроек для конечных точек подключения и для метаданных использовать только полные адреса. Какой из вариантов выбрать - это уже нужно будет смотреть по ситуации.

Например, если расширение хранится в каталоге %AppData%/CompanyName/ApplicationName/Extensions/MyExtensionName/ с тем, чтобы предоставлять пользователю возможность править конфигурационные настройки или вовсе удалять расширение, то нужным вариантом будет второй.

А если пользователь не имеет административных прав и расширение хранится в каталоге %ProgramFiles%/CompanyName/ApplicationName/Extensions/MyExtensionName/, то в данном случае вариант использования - это дело вкуса администратора.

Экспорт конфигурационных настроек на стороне клиента
На стороне клиента использовать метод Configure в коде наших прокси не получится, но начиная с WCF 4.0 нам в помощь появился класс ConfigurationChannelFactory. Его можно создать в конструкторе создаваемого нами прокси  и использовать в процессе работы например так:

[ServiceBehavior(IncludeExceptionDetailInFaults = true)]
public class Drawing : IDisposable, IDrawing {

    IDrawing Channel;
    ConfigurationChannelFactory<IDrawing> channelFactory;

    public void Open() {
        if (channelFactory.State != CommunicationState.Opened &&
            channelFactory.State != CommunicationState.Opening)
            channelFactory.Open();
    }

    public void Close() {
        if (channelFactory.State == CommunicationState.Opened)
            channelFactory.Close();
    }

    public CommunicationState State
    {
        get { return channelFactory.State; }
    }

    public Drawing(string endpointName) {
        ExeConfigurationFileMap configMap = new ExeConfigurationFileMap();
        configMap.ExeConfigFilename = typeof(Drawing).Assembly.Location + ".config";
        Configuration dll_config = ConfigurationManager.OpenMappedExeConfiguration(configMap,
            ConfigurationUserLevel.None);
        channelFactory =
            new ConfigurationChannelFactory<IDrawing>(endpointName, dll_config, null);
        Channel = channelFactory.CreateChannel();
    }

    public void Dispose() {
        if (channelFactory != null&& channelFactory.State == CommunicationState.Opened)
            channelFactory.Close();
    }

    // here is other members...   
}
Обратите внимание на то, что в процессе реализации нужного нам прокси мы обошлись без наследования от класса ClientBase.

Ссылки:

И снова о хостинге WCF-сервисов в 2016-м accoreconsole.exe…

Подумал тут... Вполне возможно, что Autodesk умышленно прикрыла возможность хостинга сервисов в 2016-м accoreconsole.exe, дабы если не полностью пресечь (т.к. это не возможно), то хотя бы максимально усложнить возможность использования инструмента в системе распределённых приложений взаимодействующих через сервисы...

Наличие возможности хостинга WCF-сервисов в accoreconsole.exe для ряда случаев может существенно сократить потребность в количестве приобретаемых лицензий AutoCAD на предприятиях, где выполнение некоторого (если даже не всего) объёма работ можно автоматизировать (т.е. сделать программно). Одним сервисом или набором сервисов (читать как "одной лицензией AutoCAD") в таком случае сможет пользоваться неограниченное количество сотрудников компании одновременно. Для компании клиента это, безусловно, экономия... Но вот для компании Autodesk - это потеря денег... Т.е. налицо явный конфликт интересов и вполне очевидно, кто в данной ситуации имеет на руках "все козыри".

Кроме того, не стоит забывать, что теперь в Autodesk хотят одних и тех же овец стричь каждый год (новая ценовая политика) в виду чего даже сама по себе потенциальная возможность  сокращения лицензий вряд ли будет воспринята ими положительно. Т.о., как я уже писал выше: не исключено, что в 2016-м AutoCAD "кислород" сервисам был перекрыт преднамеренно...

Резюме
Я не удивлюсь, если в более новых версиях AutoCAD приложение accoreconsole.exe будет вовсе тихо изъято, мол "за ненадобностью"...

О хостинге WCF-сервисов в accoreconsole.exe (продолжение)

В продолжение предыдущей записи по обозначенной теме...

Экспериментальным путём выяснил, что проблема хостинга WCF сервисов в accoreconsole.exe присутствует в AutoCAD 2016, но отсутствует во всех более ранних версиях (2013-2015). Проверялось на AutoCAD 2016 x64 SP1 English с установленным Hotfix 3. Проверить наличие обозначенной проблемы в AutoCAD 2017 нет возможности за неимением оного, но очень даже не исключено, что обозначенный баг будет теперь и в нём и во всех последующих версиях...

Примечание:
Во всех без исключениях версиях accoreconsole.exe наблюдал ещё и такую проблему: то, что программно отправляется в консоль AutoCAD через Editor.WriteLine(...) по факту в консоли не появляется... Можно вместо этого воспользоваться Application.ShowAlertDialog(...) - в этом случае текст попадает в консоль, но это очень похоже на выдёргивание зубов плоскогубцами через зад...

О хостинге WCF-сервисов в accoreconsole.exe (AutoCAD 2016)

Как известно, WCF-сервисы могут в качестве хостинга использовать не только IIS и WAS, но так же и произвольные приложения (консольные или GUI). Как показывает практика, в качестве хоста можно использовать acad.exe. В идеале хотелось бы иметь возможность хостить службы в accoreconsole.exe, но не забываем, что это Autodesk, а это означает, что скучать не придётся...


Когда это может оказаться интересным?

Как известно, в параметрах запуска accoreconsole.exe можно указывать набор ключей, в т.ч. и ключ /s, при помощи которого разрешено передавать имя файла скрипта (SCR-файла). По завершению работы скрипта приложение так же автоматически завершит свою работу. Однако порой может возникнуть потребность интерактивного использования функционала, предоставляемого accoreconsole.exe (т.е. само по себе консольное окно при этом не требуется) не закрывая приложение столько времени, сколько потребуется (можно просто скрыть консольное окошко).

Хостинг службы в AutoCAD позволяет другим приложениям (т.н. клиентам) взаимодействовать с ним не прибегая к использованию AutoCAD COM API и при этом получая возможность задействовать возможности AutoCAD .NET API. Кроме того, в любой момент служба может быть перемещена на любой др. компьютер абсолютно прозрачно для клиентов.

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

Это не означает, что на удалённой или локальной машинке обязательно должен быть постоянно запущен AutoCAD. Нет. Клиент может обратиться к службе, которая хостится в IIS или WAS с требованием что-то сделать в AutoCAD. Эта служба запускает AutoCAD (устанавливая видимость его окна в False) и в свою очередь является клиентом для другой службы, хостящейся в AutoCAD, передавая ей ваши запросы, а вам - её ответы. После того, как необходимый вам набор операций в AutoCAD будет выполнен служба, хостящаяся в IIS или WAS завершает работу AutoCAD и ждёт следующих обращений. В случае необходимости, параллельно может быть запущено несколько экземпляров AutoCAD, выполняющих каждый свою задачу, полученную от клиента. Для упрощения в данной теме я не использую промежуточную службу.

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

В идеале, конечно же, лучше всего на роль хоста службы подошёл бы accoreconsole.exe...


Служба

Предположим, что служба, предназначенная для хостинга в AutoCAD, реализует такой интерфейс:

namespace Bushman.CAD.Services {

    [ServiceContract(Namespace = "www.gpsm.ru")]
    interface IMyContract {
        [OperationContract]
        void Write(string msg); // Write message into AutoCAD command console.

        [OperationContract]
        string GetVersion(); // Get AutoCAD version
    }
}

Реализуем обозначенный интерфейс как-то так:

using cad = Autodesk.AutoCAD.ApplicationServices.Core.Application;
using Autodesk.AutoCAD.ApplicationServices;

namespace Bushman.CAD.Services {
    class MyService : IMyContract {
        public string GetVersion() {
            return cad.Version.ToString();
        }

        public void Write(string msg) {
            Document doc = cad.DocumentManager.MdiActiveDocument;
            if (null != doc) {
                doc.Editor.WriteMessage(msg);
            }
        }
    }

Никаких команд определять не будем, а инициализацию расширения выполним следующим образом:

using System;
using System.ServiceModel;
using cad = Autodesk.AutoCAD.ApplicationServices.Core.Application;
usingAutodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.Runtime;

[assembly: ExtensionApplication(typeof(Bushman.CAD.Services.ExtensionApplication))]

namespace Bushman.CAD.Services {
    public class ExtensionApplication : IExtensionApplication {

        static ServiceHost host = null;

        static ExtensionApplication() {
            try{
                host = new ServiceHost(typeof(MyService));
                host.Open();
                host.UnknownMessageReceived += Host_UnknownMessageReceived;
                AppDomain.CurrentDomain.ProcessExit += ProcessExit;
            }
            catch(System.Exception ex) {
                Document doc = cad.DocumentManager.MdiActiveDocument;
                if (null != doc) doc.Editor.WriteMessage(ex.Message);
            }
        }

        private static void Host_UnknownMessageReceived(object sender,
            UnknownMessageReceivedEventArgs e) {
            Document doc = cad.DocumentManager.MdiActiveDocument;
            if(null != doc) doc.Editor.WriteMessage(e.Message.ToString());
        }

        private static void ProcessExit(object sender, EventArgs e) {
            if(null != host) host.Close();
        }

        public void Initialize() {
            string status = null == host ? "null" : host.State.ToString();
            Document doc = cad.DocumentManager.MdiActiveDocument;
            if(null != doc) doc.Editor.WriteMessage("\nHost status: {0}.\n", status);
        }

        public void Terminate() {
            // Nothing is here.
        }
    }
}


Клиент

Клиента реализуем следующим образом:

using System;
using System.ServiceModel;
usingBushman.MyClient.ServiceReference1;

namespace Bushman.MyClient {
    classProgram {
        static void Main(string[] args) {
            Console.Title = "CAD client";
            try{
                using (MyContractClient client = new MyContractClient("http")) {
                    if (client.InnerChannel.State != CommunicationState.Faulted) {
                        client.Open();
                        string version = client.GetVersion();
                        Console.WriteLine("CAD version: {0}", version);
                        client.Write("Client said: Hello, AutoCAD.\n");
                    }
                }
            }
            catch(Exception ex) {
                Console.WriteLine(ex.Message);
            }

            Console.WriteLine("Press any key for exit...");
            Console.ReadKey();
        }
    }
}


Конфигурационные файлы acad.exe.config и accoreconsole.exe.config настраиваю на работу с обозначенной выше службой:


<configuration>

  <startup useLegacyV2RuntimeActivationPolicy="true">
    <supportedRuntime version="v4.0"/>
  </startup>

  <!--All assemblies in AutoCAD are fully trusted so there's no point generating publisher evidence-->
  <runtime>
    <generatePublisherEvidence enabled="false"/>
  </runtime>

  <system.serviceModel>
    <services>
      <service name="Bushman.CAD.Services.MyService" behaviorConfiguration="MEXGET">
        <host>
          <baseAddresses>
            <add baseAddress="http://win7x64ac2:8001"/>
          </baseAddresses>
        </host>
        <endpoint name="http"
                  binding="wsHttpBinding"
                  address="MyService"
                  bindingConfiguration="MyContract"
                  contract ="Bushman.CAD.Services.IMyContract">
        </endpoint>
      </service>
    </services>
    <bindings>
      <wsHttpBinding>
        <binding name="MyContract"/>
      </wsHttpBinding>
    </bindings>
    <behaviors>
      <serviceBehaviors>
        <behavior name="MEXGET">
          <serviceMetadata httpGetEnabled="true"/>
        </behavior>
      </serviceBehaviors>
    </behaviors>

    <diagnostics wmiProviderEnabled="true">
      <messageLogging
           logEntireMessage="true"
           logMalformedMessages="true"
           logMessagesAtServiceLevel="true"
           logMessagesAtTransportLevel="true"
           maxMessagesToLog="3000"
       />
    </diagnostics>

  </system.serviceModel>

  <system.diagnostics>
    <sources>
      <source name="System.ServiceModel"
              switchValue="Information, ActivityTracing"
              propagateActivity="true" >
        <listeners>
          <add name="xml"/>
        </listeners>
      </source>
      <source name="System.ServiceModel.MessageLogging">
        <listeners>
          <add name="xml"/>
        </listeners>
      </source>
      <source name="myUserTraceSource"
              switchValue="Information, ActivityTracing">
        <listeners>
          <add name="xml"/>
        </listeners>
      </source>
    </sources>
    <sharedListeners>
      <add name="xml"
           type="System.Diagnostics.XmlWriterTraceListener"
                 initializeData="\\hyprostroy\dfs\Обмен\Бушман\logs\Service-Traces.svclog" />
    </sharedListeners>
  </system.diagnostics>
</configuration>


Конфигурационный файл клиента выглядит так:

<?xml version="1.0" encoding="utf-8" ?>

<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" />
  </startup>
  <system.serviceModel>
    <bindings>
      <wsHttpBinding>
        <binding name="http" />
      </wsHttpBinding>
    </bindings>
    <client>
      <endpoint address="http://win7x64ac2:8001/MyService" binding="wsHttpBinding"
          bindingConfiguration="http" contract="ServiceReference1.IMyContract"
          name="http">
        <identity>
          <!-- WARNING: change the value according your user principal name. -->
          <userPrincipalName value="admin@hyprostr" />
        </identity>
      </endpoint>
    </client>

    <diagnostics wmiProviderEnabled="true">
      <messageLogging
           logEntireMessage="true"
           logMalformedMessages="true"
           logMessagesAtServiceLevel="true"
           logMessagesAtTransportLevel="true"
           maxMessagesToLog="3000"
       />
    </diagnostics>

  </system.serviceModel>

  <system.diagnostics>
    <sources>
      <source name="System.ServiceModel"
              switchValue="Information, ActivityTracing"
              propagateActivity="true" >
        <listeners>
          <add name="xml"/>
        </listeners>
      </source>
      <source name="System.ServiceModel.MessageLogging">
        <listeners>
          <add name="xml"/>
        </listeners>
      </source>
      <source name="myUserTraceSource"
              switchValue="Information, ActivityTracing">
        <listeners>
          <add name="xml"/>
        </listeners>
      </source>
    </sources>
    <sharedListeners>
      <add name="xml"
           type="System.Diagnostics.XmlWriterTraceListener"
                 initializeData="\\hyprostroy\dfs\Обмен\Бушман\logs\Client-Traces.svclog" />
    </sharedListeners>
  </system.diagnostics>

</configuration>


Запускаем службу и клиента
Служба и клиент могут находится как на одном компьютере, так и на разных компьютерах в сети (или в Интернет). Если в качестве хоста использовать acad.exe, то всё нормально работает:



Но вот если в качестве хоста использовать accoreconsole.exe, то достучаться до сервиса не удастся даже с локального компьютера, на котором запущен хост сервиса. При этом сервис успешно запускается, но не доступен:




В логах клиента можно посмотреть описание проблемы:



По обозначенной теме мне ответил Августо Гонсалес:
Augusto Goncalves (API Evangelist at Autodesk):
As far as I remember trying, accoreconsole.exe doesn't accept new calls (from automation) after is open (but I haven't tried with WCF). Acad.exe is a little different... I don't believe accoreconsole will remain receiving calls after it was launched, it was designed to launch with a list of commands on the .scr file.
Если Августо прав, то это будет очередной, весьма досадный недостаток accoreconsole.exe...

Продолжение темы - здесь...