Обновление WCF конфигурации online
Текущий проект на работе очень хорошо сконфигурирован и разнесен по слоям и уровням, проект имеет модульную структуру, построен на мелких и относительно автономных сервисах и все в нем хорошо! Кроме настройки портов для сервисов.
Сервисов сейчас больше 50 и каждый имеет свой отдельный адрес и порт. Количество сервисов постепенно растет, и я не удивлюсь, если через полгода их уже будет 100+. Естественно в таком подходе вопрос настройки сервиса и клиента встает в полный рост. Руками такое править сложно, да и не хочется. При таком количестве сервисов риск того, что порт будет уже занят, сильно отличен от нуля.
В начале проекта не особенно задумывались об автоматической конфигурации, потому что сервисов было мало, а потом, как обычно это бывает, стало не до этого, так как конфликтов по портам не было и редактирование конфигов и портов было делом редким и привычным, что автоматически снижает ценность автоматизации до нуля.
Дополнительного веселья добавляет тот факт, что сервисы могут запускаться от имени пользователя не входящего в группу Администраторов. Для этого надо будет резервировать для него пространство имен, в рамках которого пользователь сможет создавать WCF сервисы.
Наступает момент выпуска беты и время рвать волосы на голове, так как настройка приложения получается совсем не тривиальной, как для разработчиков, так и для пользователей. Чрезмерно сложные программы обновления файлов конфигураций, проблема доставки конфигураций до клиентов – куча никому не нужной мороки.
В конце концов, у всех заинтересованных лиц в голове рождается мысль, что клиент должен настраиваться на сервисы автоматически и, по возможности, без подсказки пользователя. Серверная часть должна тоже автоматически регистрировать адреса и забирать свободные порты самостоятельно в указанном оператором диапазоне.
Подготовка
Дано: большая программа, где все сервисы уже прописаны руками, есть простыни кода с созданием хостов для сервисов и созданием каналов на клиенте. Конфигурационные файлы и там и там на 50+ сервисов.
Требуется: без переписывания существующего кода сделать автоматическое обновление конфигураций на клиенте – Задача минимум. Задачей максимум будет автоматическая регистрация серверной части и уведомление подключенных клиентов, если что-то меняется.
Пока что будет рассмотрена только задача минимум.
Для полной аутентичности в примере хост WCF сервисов находится в консольном приложении, сервисов 2. Есть клиентское приложение, которое их использует.
Чтобы не отвлекаться на код сервисов, возьмем простейшие задачи для них. Пусть они находят минимальный и максимальный элементы в массиве, а так же сумму членов массива. Все делается очень просто одной строкой.
public class ServiceMax : IServiceMax {
public int Execute(List<int> array) {
return array.Max();
}
}
public class ServiceSum : IServiceSum {
public int Execute(List<int> array) {
return array.Sum();
}
}
public class ServiceMin : IServiceMin {
public int Execute(List<int> array) {
return array.Min();
}
}
Первые два сервиса я поместил в службу А, третий в службу В. Вот как это примерно выглядит у меня.
ServiceConnection это библиотека, которая делает все что необходимо нам. Она будет включаться в серверную и в клиентскую части.
Поднятие сервисов выглядит тоже достаточно просто, без заморочек. Пример для сервиса А:
internal static class Program {
private static ServiceHost hostMax;
private static ServiceHost hostSum;
private static void Main() {
StartAsConsole();
}
private static void StartAsConsole() {
hostMax = new ServiceHost(typeof (ServiceMax));
hostSum = new ServiceHost(typeof (ServiceSum));
hostMax.Open();
hostSum.Open();
Console.WriteLine("press any key to stop...");
Console.ReadLine();
hostMax.Close();
hostSum.Close();
}
}
В примере видно, что хосты сервисов создаются и открываются в лоб, без всяких примочек. Теперь представьте что строчек типа
private static ServiceHost hostMax; private static ServiceHost hostSum;
50 с небольшим штук. И есть тенденция к увеличению. Так как нужно минимальное вмешательство в код, а массовое переименование и наследования вызовет реакцию близкую к shit hits the fan, то мы пойдем другим путем.
Главные слова при внедрении чего-то нового звучат так: «Надо дописать всего две-три строки в существующий код и все!». Абсолютно неважно, что при этом надо будет подключить некоторое количество библиотек. Хотя если у вас все поместилось в одну, то это тоже несомненный плюс о котором стоит сообщить.
На клиенте соединение с серверной частью тоже выглядит немудрено, так как все в конфигурации:
private ServiceSumClient serviceSumClient;
private ServiceMinClient serviceMinClient;
private ServiceMaxClient serviceMaxClient;
private void InitServices() {
serviceSumClient = new ServiceSumClient("WSHttpBinding_IServiceSum");
serviceMinClient = new ServiceMinClient("WSHttpBinding_IServiceMin");
serviceMaxClient = new ServiceMaxClient("WSHttpBinding_IServiceMax");
}
Ради простоты примера весь код клиента написан в классе самой формы. Как вы можете догадаться тут тоже, желательно ничего не менять и не переписывать. Только если немного дописать.
Как уже упоминалось выше, основная идея будет в том, чтобы поднять внутрисетевой канал, по которому передавать сведения об изменениях серверной части.
Использование
О реализации будет позже, пока что покажу, как выглядит использование API библиотеки.
На сервере надо будет задать адрес служебного сервиса, указать в какой сборке искать хосты WCF и запустить его.
private static void Main() {
starter = new Starter();
starter.StartDiscoveryService("net.tcp://localhost:10001/ClientDiscoveryHostServiceB");
// ...
starter.StopDiscoveryService();
}
private static void StartAsConsole() {
hostMax = new ServiceHost(typeof (ServiceMax));
hostSum = new ServiceHost(typeof (ServiceSum));
// ... описание остальных хостов
starter.AddDiscoveryInfoBy(typeof (Program));
// ...
}
В начале создаем класс Starter, запускаем сервис обнаружения и обновления, говорим по какому адресу он будет доступен. После инициализации всех хостов говорим нашему служебному классу найти всю информацию по хостам в указанном классе. Всего получилось 4 строчки кода.
Такую вставку кода надо будет сделать в каждом модуле программы, который поднимает WCF сервисы.
Для клиента надо будет написать примерно такое же количество строк кода.
hostDiscoveryA = new HostDiscovery(new Uri("net.tcp://localhost:10000/ClientDiscoveryHostServiceA"));
hostDiscoveryA.GetAddressesFor(Assembly.GetExecutingAssembly());
hostDiscoveryA.UpdateEndPoints(this);
hostDiscoveryA.UpdateConfig(xDocument);
– где хDocument это файл конфигурации клиента.
- Первым делом организуем канал обновления. Указываем необходимый адрес.
- Получаем все адреса с сервера для используемых сервисов.
- Так как клиентские классы объявлены в code behind формы, и там же находится этот код, то в метод UpdateEndPoints передается объект формы. Обновляем адреса на лету.
- Обновляем файл конфигурации.
Все, больше на клиенте ничего не надо.
Основная библиотека для наращивания функционала и для повторного использования –это ServiceConnection. Можно взять и начать использовать в любом проекте, на любой стадии.
Да, как большинство уже догадалось, цена такому решению – рефлексия. Но не все так страшно как может показаться. Тестовый пример можно дополнить выполнением в фоновом режиме.
Скачать и посмотреть исходный код можно с Assembla
Для запуска проекта предварительно надо будет запустить AddServices.bat с правами администратора, чтобы зарезервировать адреса для работы.
Описание работы
Основная библиотека состоит из сервиса, класса инициализации для серверной части и класса поддержки для клиентской части.
Серверная часть:
public class Starter {
private static ServiceHost serviceHost;
/// <summary>
/// Создание и открытие канала синхронизации\конфигурации
/// </summary>
/// <param name="discoveryChannel">адрес сервиса. Должен задаваться на серверной и клиентской стороне</param>
public void StartDiscoveryService(string discoveryChannel) { … }
/// <summary>
/// Остановка канала для синхронизации\конфигурации
/// </summary>
public void StopDiscoveryService() {
serviceHost.Close();
}
/// <summary>
/// Обнаружение и сохранение адресов "рабочих" сервисов/служб
/// </summary>
/// <typeparam name="T">Интерфейс службы/сервиса</typeparam>
/// <param name="host">Служба/сервис за настройками которой надо следить</param>
public void AddDiscoveryInfoFor<T>(ServiceHost host) { … }
private static void AddDiscoveryInfoFor(object host) { … }
/// <summary>
/// Обнаружение ServiceHost в указанном классе
/// </summary>
/// <param name="obj">Класс в котором надо найти ServiceHost'ы</param>
public void AddDiscoveryInfoBy(Type obj) { … }
}
Из интерфейса класса очевидна его работа. Наиболее интересные методы, это:
- AddDiscoveryInfoFor – позволяет точечно добавлять хосты и интерфейсы соответствия для отслеживания изменения конфигурации.
- AddDiscoveryInfoBy – самостоятельно находит и добавляет хосты сервисов.
Так или иначе оба метода в итоге обращаются к
void AddDiscoveryInfoFor(object host)
который уже и занимается разбором оконечной точки сервиса. Никакого rocket science, вся информация содержится в оконечной точке в доступном виде. После разбора информация об интерфейсе и адресе заносится в ClientDiscoveryHostService, чтобы клиент мог получить эту информацию.
private static void AddDiscoveryInfoFor(object host) {
ServiceHost xhost;
if (host is ServiceHost) {
xhost = (ServiceHost) host;
}
else {
return;
}
var baseAddresses = xhost.BaseAddresses;
if (xhost.Description.Endpoints.Count == 0) return;
var serviceEndpoint = xhost.Description.Endpoints[0];
var contractDescription = serviceEndpoint.Contract;
ClientDiscoveryHostService.AddEndPoints(contractDescription.Name, baseAddresses.ToList().ConvertAll(i => i.ToString()));
}
Клиентская часть
Клиентская часть чуть больше по реализации, но не сложнее.
public class HostDiscovery {
private readonly IClientDiscoveryHostService discoveryHost;
private static readonly Dictionary<string, string> addresses = new Dictionary<string, string>();
/// <summary>
/// Создние клиентской части
/// </summary>
/// <param name="discoveryServiceUri">Адрес до серверной части служебного сервиса</param>
public HostDiscovery(Uri discoveryServiceUri) { … }
/// <summary>
/// Получить адрес для указанного типа "рабочего" сервиса
/// </summary>
/// <param name="type">"Рабочий" сервис</param>
/// <returns>Полный адрес сервиса</returns>
public IEnumerable<string> GetAddressFor(Type type) { … }
/// <summary>
/// Получает адреса для всех "рабочих" сервисов из указанной сборки
/// </summary>
/// <param name="assembly">Сборка для сканирования</param>
/// <returns>Список адресов для сервисов из переданной сборки</returns>
public IEnumerable<string> GetAddressesFor(Assembly assembly) { … }
/// <summary>
/// Обновление адресов полученных от сервера на лету.
/// Выполнять после получения адресов.
/// </summary>
/// <param name="obj">Класс, где объявлены клиентские сервисы</param>
public void UpdateEndPoints<T>(T obj) where T : class { … }
private static void UpdateEndpointAddress(ServiceEndpoint serviceEndpoint) { … }
/// <summary>
/// Обновляет файл конфигурации приложения по итогам получения адресов. Перед использованием надо вызвать
/// либо GetAddressFor, либо GetAddressesFor
/// </summary>
/// <param name="document">Конфигурация сервисов</param>
/// <returns>Документ с новыми адресами</returns>
public XDocument UpdateConfig(XDocument document) { … }
}
Поднятие сервиса обнаружения и обновления делается полностью в коде, не требуется никаких файлов конфигураций.
Реализация методов не составляет большого труда, если четко себе проговорить, как собираешься искать нужные элементы. Например метод GetAddressesFor:
- Получить все типы в сборке
- Найти все интерфейсы для которых указан атрибут ServiceContract. Без этого не поднять WCF сервис.
- Получить оконечные точки для найденых интерфейсов.
- Опросить оконечные точки на предмет их адреса.
public IEnumerable<string> GetAddressesFor(Assembly assembly) {
var list = new List<string>();
var types = assembly.GetTypes();
foreach (var type in types) {
var interfaces = type.GetInterfaces()
.Where(i => i.GetCustomAttributes(false)
.OfType<ServiceContractAttribute>()
.Count() > 0)
.SelectMany(GetAddressFor);
list.AddRange(interfaces);
}
return list.Distinct().AsEnumerable();
}
Код может не самый оптимальный, но наглядный )))
Обновление оконечных точек на лету в методе UpdateEndPoints тоже не сложный процесс, если помнить о том, что надо попробовать найти всевозможные поля и свойства класса в зависимости от модификаторов доступности и видимости.
И еще одни маленький нюанс. Так как идет поиск по клиентским автосгенерированным классам, которые объявляются как
public class ServiceSumClient : ClientBase<IServiceSum>, IServiceSum { … }
public class ServiceMinClient : ClientBase<IServiceMin>, IServiceMin { … }
то легче всего обратиться к базовому классу через приведение к dynamic
var genericTypeDefinition = baseType.GetGenericTypeDefinition();
if (genericTypeDefinition == typeof (ClientBase<>)) {
var serviceEndpoint = ((ServiceEndpoint) ((dynamic) info.GetValue(obj)).Endpoint);
UpdateEndpointAddress(serviceEndpoint);
}
Кратенько наверно это все, что я хотел рассказать.
Скачать и посмотреть исходный код можно с Assembla
Hard’n’heavy!
Нет обратных ссылок на эту запись.
