Category Archives: .NET/C#

Azure Service Bus brokered messaging – dodatkowe funkcje

2015-03-12

W ostatnim temacie cyklu poświęconego usłudze Azure Service Bus chciałbym przedstawić bardziej rozbudowane scenariusze pracy z wiadomościami takie jak: obsługa pod-kolejek DeadLetter, opóźnione przetwarzanie, wykrywanie zdublowanych wiadomości, harmonogram dostarczania wiadomości, transakcje przy wysyłaniu i odbieraniu wielu wiadomości, sesje oraz praca w modelu request-response.

 

Pod-kolejka DeadLetter

Wiadomości umieszczane są w specjalnej pod-kolejce DeadLetter w następujących przypadkach:

  • Wiadomość wygaśnie i jednocześnie dla kolejki lub subskrypcji atrybut EnableDeadLetteringOnMessageExpiration ustawiony jest na true.
  • Przekroczona zostanie maksymalna liczba prób dostarczenia wiadomości, którą określa się dla kolejki lub subskrypcji poprzez atrybut MaxDeliveryCount (domyślnie 10). Każdorazowe wywołanie metody Abandon na wiadomości powoduje zwiększenie jej licznika prób dostarczenia.
  • Wystąpi wyjątek podczas sprawdzania filtra subskrypcji dla danej wiadomości i jednocześnie dla subskrypcji atrybut EnableDeadLetteringOnFilterEvaluationExceptions ustawiony jest na true.
  • Na wiadomości wywołana zostanie metoda DeadLetter.

Ustawienie domyślnego czasu wygasania wiadomości oraz przenoszenia ich do pod-kolejki DeadLetter (ustawień nie można zmienić po utworzeniu kolejki):

QueueDescription queue = new QueueDescription("queueName")
{
    DefaultMessageTimeToLive = TimeSpan.FromSeconds(60),
    EnableDeadLetteringOnMessageExpiration = true
};

Ustawienie czasu wygaśnięcia wiadomości przed jej wysłaniem:

BrokeredMessage message = new BrokeredMessage();
message.TimeToLive = TimeSpan.FromMinutes(30);

Pobieranie wiadomości z pod-kolejki DeadLetter:

//queueName/$DeadLetterQueue
string queueDeadLetterPath = QueueClient.FormatDeadLetterPath("queueName");
QueueClient deadLetterQueueClient =
  QueueClient.CreateFromConnectionString("connectionString", queueDeadLetterPath, ReceiveMode.ReceiveAndDelete);
BrokeredMessage message = deadLetterQueueClient.Receive();

 

Opóźnione przetwarzanie – Defer

Klasa BrokeredMessage posiada metodę Defer, która pozwala na odłożenie przetwarzania wiadomości pobranej z kolejki lub subskrypcji. Po wywołaniu metody Defer wiadomość pozostaje w kolejce, ale można się do niej ponownie odwołać jedynie poprzez jej numer. Oznacza to konieczność zapamiętania numeru wiadomości przed wywołaniem metody Defer, w przeciwnym wypadku stracimy możliwość jej odczytu i wiadomość pozostanie w kolejce do czasu wygaśnięcia. Do odczytu wiadomości można wykorzystać jej numer jedynie w przypadku wcześniejszego użycia metody Defer.

QueueClient queueClient =
  QueueClient.CreateFromConnectionString("connectionString", "queueName", ReceiveMode.PeekLock);

//Defer
BrokeredMessage message = queueClient.Receive();
long messageNumber = message.SequenceNumber;
message.Defer();

//Receive
BrokeredMessage deferredMessage = queueClient.Receive(messageNumber);
deferredMessage.Complete();

 

Wykrywanie zdublowanych wiadomości

Wykrywanie duplikatów w wysłanych wiadomościach opiera się na atrybucie MessageId wiadomości oraz ustawionym oknie czasowym w kolejce lub topiku. Jeżeli kolejka lub topik jest skonfigurowany pod kątem wykrywania duplikatów, klient może wielokrotnie ponawiać wysłanie tej samej wiadomości, a usługa zignoruje wszystkie duplikaty. Przy włączonym wykrywaniu duplikatów (atrybut RequiresDuplicateDetection) atrybut DuplicateDetectionHistoryTimeWindow określa, przez jaki czas wartości atrybutu MessageId wiadomości będą przechowywane w celu wykrywania zdublowanych obiektów.

QueueDescription queueDescription = new QueueDescription("queueName")
{
    RequiresDuplicateDetection = true,
    DuplicateDetectionHistoryTimeWindow = new TimeSpan(1, 0, 0)
};

 

Harmonogram dostarczania wiadomości

Poprzez atrybut ScheduledEnqueueTimeUtc klasy BrokeredMessage mamy możliwość określenia, kiedy dana wiadomość wysłana do kolejki lub topiku będzie dostępna do pobrania. Wartość wspomnianego atrybutu musi być przekazana jako czas UTC.

QueueClient queueClient =
  QueueClient.CreateFromConnectionString("connectionString", "queueName");

BrokeredMessage message = new BrokeredMessage();
message.ScheduledEnqueueTimeUtc = DateTime.UtcNow.AddDays(7);
queueClient.Send(message);

 

Transakcje

Podczas wysyłania wielu wiadomości do pojedynczej kolejki lub topiku cały proces możemy objąć transakcją, co gwarantuje nam dostarczenie wszystkich wiadomości lub żadnej z nich w przypadku pojawienia się błędu. Z kolei zastosowanie transakcji podczas pobierania wielu wiadomości z danej kolejki lub subskrypcji pozwala na anulowanie całego przetwarzania w przypadku wystąpienia błędu przy jednej z wiadomości. Transakcja może obejmować wiadomości pobierane z różnych subskrypcji o ile należą one do tego samego topiku.

Wysyłanie wiadomości w transakcji:

QueueClient queueClient =
  QueueClient.CreateFromConnectionString("connectionString", "queueName");

using (TransactionScope scope = new TransactionScope())
{
    BrokeredMessage message1 = new BrokeredMessage("Message 1");
    queueClient.Send(message1);

    BrokeredMessage message2 = new BrokeredMessage("Message 2");
    queueClient.Send(message2);

    scope.Complete();
}

Pobieranie wiadomości w transakcji:

QueueClient queueClient =
  QueueClient.CreateFromConnectionString("connectionString", "queueName", ReceiveMode.PeekLock);

using (TransactionScope scope = new TransactionScope())
{
    BrokeredMessage message1 = queueClient.Receive();
    string messageBody1 = message1.GetBody<string>();
    message1.Complete();

    BrokeredMessage message2 = queueClient.Receive();
    string messageBody2 = message2.GetBody<string>();
    message2.Complete();

    scope.Complete();
}

 

Sesje

Usługa Azure Service Bus udostępnia mechanizm sesji pozwalający na grupowanie wysyłanych wiadomości. Sesje są idealnym rozwiązaniem w przypadku konieczności przetworzenia wielu wzajemnie ze sobą powiązanych wiadomości. Przykładem takiego scenariusza jest podzielenie jednej dużej wiadomości na kilka mniejszych (ze względu na dopuszczalny rozmiar) i wysłanie ich w ramach pojedynczej sesji. Załóżmy, że chcemy wysłać obiekt zamówienia składający się z nagłówka oraz wielu pozycji. Możemy wówczas podzielić go na wiele wiadomości, gdzie pierwsza z nich będzie zawierała nagłówek, a kolejne poszczególne pozycje. Tak przygotowany zestaw wiadomości możemy wysłać w ramach pojedynczej sesji, podając jako jej identyfikator np. numer zamówienia. Dzięki temu podczas pobierania wiadomości będziemy w stanie powtórnie zgrupować je w ramach jednego zamówienia.

W celu włączenia obsługi sesji, kolejka lub subskrypcja musi być utworzona z atrybutem RequiresSession ustawionym na true. Wówczas wszystkie wiadomości wysyłane do kolejki lub topiku zawierającego takie subskrypcje muszą mieć ustawioną wartość we właściwości SessionId (dowolna wartość typu string będąca identyfikatorem sesji). Pobieranie wiadomości z tak skonfigurowanej kolejki lub subskrypcji odbywa się poprzez obiekt MessageSession zamiast standardowych QueueClient lub SubscriptionClient. Wywołanie metody AcceptMessageSession na obiekcie klienta kolejki lub subskrypcji spowoduje oczekiwanie przez podany w parametrze czas (lub domyślny minutę) na otrzymanie wiadomości. Jeżeli to nastąpi, zwracany jest obiekt MessageSession zawierający właściwość SessionId i umożliwiający pobranie wszystkich wiadomości w ramach tej sesji. Istnieje również możliwość przekazania do metody AcceptMessageSession identyfikatora sesji, co spowoduje oczekiwanie na wiadomość jedynie z tej konkretnej sesji. Jeżeli przez określony czas nie zostanie odebrana żadna wiadomość, zgłoszony zostaje wyjątek TimeoutException.

Utworzenie kolejki z obsługą sesji:

QueueDescription orderQueueDescription = new QueueDescription("queueName")
{
    RequiresSession = true
};

Wysłanie kilku wiadomości w ramach jednej sesji:

Order order = new Order();
order.Id = "1/2015";
order.Details = new List<string> { "Item1", "Item2", "Item3" };

QueueClient queueClient =
  QueueClient.CreateFromConnectionString("connectionString", "queueName");

foreach (string orderItem in order.Details)
{
    BrokeredMessage message = new BrokeredMessage(orderItem);
    message.SessionId = order.Id;
    queueClient.Send(message);
}

Pobranie wszystkich wiadomości z jednej sesji:

MessageSession orderSession = null;
try
{
    orderSession = queueClient.AcceptMessageSession();
}
catch (TimeoutException ex)
{
}            

if (orderSession != null)
{
    string orderId = orderSession.SessionId;

    while (true)
    {
        BrokeredMessage orderItemMessage = orderSession.Receive();
        if (orderItemMessage != null)
        {
            string orderItem = orderItemMessage.GetBody<string>();
            orderItemMessage.Complete();
        }
        else
            break;
    }

    orderSession.Close();
}

 

Model Request-Response

Tryb brokered messaging w Azure Service Bus polega na komunikacji asynchronicznej gdzie nadawca i odbiorca pracują niezależnie, a wiadomości są kolejkowane na serwerach Azure. Mimo to, w trybie tym możliwa jest realizacja modelu Request-Response. Do tego celu wykorzystywane są dwie kolejki (żądań i odpowiedzi) oraz sesje. Poniżej znajduje się przykładowy scenariusz, w którym wysyłane jest zapytanie o szczegóły produktu, a następnie pobierana jest odpowiedź dla tego konkretnego żądania.

Klient wysyła do kolejki żądań wiadomość z zapytaniem o szczegóły produktu, podając we właściwości Properties jego identyfikator oraz ustawiając właściwość ReplyToSessionId na własny identyfikator sesji, w ramach której będzie oczekiwał na odpowiedź:

QueueClient requestQueueClient =
  QueueClient.CreateFromConnectionString("connectionString", "ProductRequestQueue");

string sessionId = Guid.NewGuid().ToString();

BrokeredMessage requestMessage = new BrokeredMessage();
requestMessage.Properties.Add("productId", "1");
requestMessage.ReplyToSessionId = sessionId;
requestQueueClient.Send(requestMessage);

Serwer pobiera wiadomość z kolejki żądań i zapamiętuje nadany identyfikator sesji:

QueueClient requestQueueClient =
  QueueClient.CreateFromConnectionString("connectionString", "ProductRequestQueue", ReceiveMode.PeekLock);

BrokeredMessage requestMessage = requestQueueClient.Receive();
string productId = requestMessage.Properties["productId"] as string;
string sessionId = requestMessage.ReplyToSessionId;
requestMessage.Complete();

Serwer przygotowuje wiadomość z informacjami o produkcie (we właściwości Label zapisywane jest to czy produkt został znaleziony) i wysyła ją z ustawionym identyfikatorem sesji do kolejki odpowiedzi:

QueueClient responseQueueClient =
  QueueClient.CreateFromConnectionString("connectionString", "ProductResponseQueue");

string productInfo = "ProductInfo";
BrokeredMessage responseMessage = new BrokeredMessage(productInfo);
responseMessage.SessionId = sessionId;
responseMessage.Label = "Found";
responseQueueClient.Send(responseMessage);

Klient pobiera wiadomość z kolejki odpowiedzi odwołując się do wcześniej nadanego identyfikatora sesji, dzięki czemu mamy gwarancję otrzymania właściwej wiadomości:

QueueClient responseQueueClient =
  QueueClient.CreateFromConnectionString("connectionString", "ProductResponseQueue", ReceiveMode.PeekLock);

MessageSession session = responseQueueClient.AcceptMessageSession(sessionId);
BrokeredMessage responseMessage = session.Receive();
if (responseMessage.Label == "Found")
{
    string productInfo = responseMessage.GetBody<string>();
}
responseMessage.Complete();
session.Close();

 

Service Bus Explorer

Na zakończenie chciałbym wspomnieć o bardzo wygodnym narzędziu pozwalającym zarządzać usługą Azure Service Bus z poziomu aplikacji desktopowej: Service Bus Explorer

 

Linki

Azure Service Bus brokered messaging – Topics, Subscriptions

2014-11-18

Po omówieniu kolejek, dziś opiszę pracę z topikami i subskrypcjami w usłudze Microsoft Azure Service Bus. Tak jak poprzednio, w prezentowanych przykładach wiadomość przesyłana pomiędzy nadawcą i odbiorcą będzie zawierała obiekt zamówienia:

public class Order
{
  public string Id { get; set; }
  public DateTime Date { get; set; }
  public string Customer { get; set; }
  public List<string> Details { get; set; }
}

W kodzie będę się odwoływał do dwóch zmiennych zawierających connection string dla przestrzeni nazw usługi oraz nazwę topiku:

public readonly string connectionString = String.Format(
  "Endpoint={0};SharedAccessKeyName={1};SharedAccessKey={2}",
  "sb://tempnamespace.servicebus.windows.net/",
  "RootManageSharedAccessKey",
  "zFUqkwWoDrem9O8UkdG0pq0sheJze0U/P93f42Aykdc=");

public readonly string topicName = "orderstopic";

 

Zarządzanie topikiem

Zarządzanie topikami odbywa się w analogiczny sposób jak w przypadku kolejek. Tu także korzystamy z obiektu NamespaceManager używając jedynie innych metod, jednak co do zasady są to dokładnie te same mechanizmy. Poniżej znajdują się przykłady tworzenia, modyfikacji oraz usunięcia topiku.

Proste tworzenie topiku:

NamespaceManager namespaceManager =
  NamespaceManager.CreateFromConnectionString(connectionString);

if (!namespaceManager.TopicExists(topicName))
  namespaceManager.CreateTopic(topicName);

Tworzenie topiku za pomocą obiektu TopicDescription:

NamespaceManager namespaceManager =
  NamespaceManager.CreateFromConnectionString(connectionString);

if (!namespaceManager.TopicExists(topicName))
{
  TopicDescription topic = new TopicDescription(topicName)
  {
    MaxSizeInMegabytes = 1024,
    RequiresDuplicateDetection = true
  };
  namespaceManager.CreateTopic(topic);
}

Odczyt właściwości oraz modyfikacja topiku:

NamespaceManager namespaceManager =
  NamespaceManager.CreateFromConnectionString(connectionString);

TopicDescription topic = namespaceManager.GetTopic(topicName);

long subscriptionCount = topic.SubscriptionCount;
long sizeInBytes = topic.SizeInBytes;

topic.MaxSizeInMegabytes = 2048;
namespaceManager.UpdateTopic(topic);

Usunięcie topiku:

NamespaceManager namespaceManager =
  NamespaceManager.CreateFromConnectionString(connectionString);

namespaceManager.DeleteTopic(topicName);

 

Zarządzanie subskrypcjami

Po wysłaniu wiadomości do topiku, w celu jej odczytu odbiorca nie odwołuje się bezpośrednio do topiku tylko korzysta z wybranej subskrypcji. To jakie wiadomości z topiku trafią do danej subskrypcji określane jest poprzez filtry operujące na atrybutach wiadomości. Podstawową różnicą w stosunku do kolejek jest to, iż w przypadku topiku tą samą wiadomość może otrzymać kilku odbiorców. Jeżeli dana wiadomość przesłana do topiku spełni warunki filtrów więcej niż jednej subskrypcji to jej kopia zostanie przekazana do każdej z nich. Należy jednak pamiętać, że filtry subskrypcji sprawdzane są tylko w momencie pojawienia się wiadomości w topiku. Jeżeli utworzymy nową subskrypcję to mogą do niej trafić jedynie wiadomości wysłane od tego momentu, wszystkie już istniejące w topiku mimo spełnionych warunków filtra nie zostaną do niej przekazane. Mamy trzy możliwości definiowania subskrypcji: bez filtra (będą do niej trafiać wszystkie wiadomości z topiku), z filtrem typu CorrelationFilter (filtr odnoszący się do wartości atrybutu CorrelationId wiadomości) lub z filtrem typu SqlFilter (filtr odnoszący się do poszczególnych elementów atrybutu Properties wiadomości).

Subskrypcję bez filtra tworzymy podając jedynie jej nazwę oraz nazwę topiku, z którym będzie powiązana:

string subscriptionName = "ordersall";

NamespaceManager namespaceManager =
  NamespaceManager.CreateFromConnectionString(connectionString);

if (!namespaceManager.SubscriptionExists(topicName, subscriptionName))
  namespaceManager.CreateSubscription(topicName, subscriptionName);

W przypadku tworzenia subskrypcji z filtrem typu CorrelationFilter, oprócz nazwy topiku musimy przekazać definicję filtra. Tworząc filtr podajemy wartość atrybutu CorrelationId wiadomości, które mają trafiać do tej subskrypcji:

string subscriptionName = "ordersdepartmenta";

CorrelationFilter filter = new CorrelationFilter("Department A");

NamespaceManager namespaceManager =
  NamespaceManager.CreateFromConnectionString(connectionString);

if (!namespaceManager.SubscriptionExists(topicName, subscriptionName))
  namespaceManager.CreateSubscription(topicName, subscriptionName, filter);

Przy użyciu powyżej zdefiniowanej subskrypcji otrzymamy wszystkie wiadomości wysłane do topiku, które w atrybucie CorrelationId będą miały wartość „Department A”.

Jeżeli chcemy mieć większe możliwości filtrowania możemy utworzyć subskrypcję z filtrem typu SqlFilter. W tym przypadku tworząc filtr podajemy wyrażenie logiczne operujące na elementach atrybutu Properties wiadomości (jest to atrybut typu IDictionary<string, object>). Budując wyrażenie mamy do dyspozycji następujące operatory: =, >, <, !=, <>, and, or, (, ). Daje to bardzo duże możliwości tworzenia warunków filtrowania. Przykładowy filtr może wyglądać tak: „(Region != ‚PL’ or ItemsCount > 10) and (Sender = ‚Jan Kowalski’)”, gdzie Region, ItemsCount i Sender to klucze we właściwości Properties wiadomości.

Oto w jaki sposób utworzyć subskrypcję z filtrem SqlFilter:

string subscriptionName = "orderswholesalepl";

SqlFilter filter = new SqlFilter("Region = 'PL' and ItemsCount > 10");

NamespaceManager namespaceManager =
  NamespaceManager.CreateFromConnectionString(connectionString);

if (!namespaceManager.SubscriptionExists(topicName, subscriptionName))
  namespaceManager.CreateSubscription(topicName, subscriptionName, filter);

Do powyższej subskrypcji trafią wszystkie wiadomości z topiku, które we właściwości Properties będą posiadały klucz Region z wartością „PL” oraz klucz ItemsCount z wartością większą od 10.

Do utworzenia subskrypcji możemy także użyć obiektu SubscriptionDescription, pozwalającego na dodatkową konfigurację:

string subscriptionName = "orderswholesalepl";

SqlFilter filter = new SqlFilter("Region = 'PL' and ItemsCount > 10");

NamespaceManager namespaceManager =
  NamespaceManager.CreateFromConnectionString(connectionString);

if (!namespaceManager.SubscriptionExists(topicName, subscriptionName))
{
  SubscriptionDescription subscription =
    new SubscriptionDescription(topicName, subscriptionName)
  {
    RequiresSession = false,
    LockDuration = TimeSpan.FromMinutes(1)
  };
  namespaceManager.CreateSubscription(subscription, filter);
}

Z obiektu SubscriptionDescription możemy również skorzystać w celu odczytu informacji o subskrypcji oraz jej modyfikacji:

string subscriptionName = "ordersall";

NamespaceManager namespaceManager =
  NamespaceManager.CreateFromConnectionString(connectionString);

SubscriptionDescription subscription =
  namespaceManager.GetSubscription(topicName, subscriptionName);

long messageCount = subscription.MessageCount;

subscription.MaxDeliveryCount = 5;
namespaceManager.UpdateSubscription(subscription);

Usunięcie subskrypcji sprowadza się do wywołania metody DeleteSubscription na obiekcie NamespaceManager:

string subscriptionName = "ordersall";

NamespaceManager namespaceManager =
  NamespaceManager.CreateFromConnectionString(connectionString);

namespaceManager.DeleteSubscription(topicName, subscriptionName);

 

Wysyłanie wiadomości do topiku

Wysłanie wiadomości do topiku wygląda praktycznie tak samo jak w przypadku kolejki. Mamy dwie możliwości, albo użyjemy dedykowanego obiektu TopicClient albo uniwersalnego MessageSender znanego z kolejek (tym razem tworzonego poprzez podanie nazwy topiku):

TopicClient topicClient =
  TopicClient.CreateFromConnectionString(connectionString, topicName);

lub:

MessagingFactory messagingFactory =
  MessagingFactory.CreateFromConnectionString(connectionString);

TopicClient topicClient = messagingFactory.CreateTopicClient(topicName);

lub:

MessagingFactory messagingFactory =
  MessagingFactory.CreateFromConnectionString(connectionString);

MessageSender messageSender =
  messagingFactory.CreateMessageSender(topicName);

Samo przygotowanie wiadomości nie różni się niczym od tego, co opisywałem przy kolejkach. Tworzymy obiekt klasy BrokeredMessage i przekazujemy go do metody Send klienta topiku:

Order order = new Order()
{
  Id = "1",
  Date = DateTime.UtcNow,
  Customer = "Company ABC",
  Details = new List<string> { "Product1", "Product2", "Product3" }
};

BrokeredMessage message = new BrokeredMessage(order)
{
  MessageId = Guid.NewGuid().ToString(),
  Label = "Application X",
  CorrelationId = "Department A",
  TimeToLive = TimeSpan.FromHours(12)
};
message.Properties.Add("Sender", "Jan Kowalski");
message.Properties.Add("Region", "PL");
message.Properties.Add("ItemsCount", order.Details.Count);

messageSender.Send(message);

messageSender.Close();

 

Odbieranie wiadomości przy użyciu subskrypcji

W celu odczytu wiadomości wysłanych do topiku odbiorca nie odwołuje się bezpośrednio do topiku tylko korzysta z określonej subskrypcji, która z jego punktu widzenia posiada funkcjonalność kolejki. Przy użyciu wybranej subskrypcji będziemy w stanie odczytać jedynie te wiadomości, które spełniły warunki jej filtra. Podobnie jak w przypadku kolejek odczyt wiadomości może być realizowany w trybie ReceiveAndDelete lub PeekLock. Po więcej szczegółów odsyłam do poprzedniego tematu. Aby odebrać wiadomość musimy utworzyć obiekt klienta subskrypcji. Możemy skorzystać z klasy SubscriptionClient lub MessageReceiver. W przypadku tego drugiego w celu wskazania konkretnej subskrypcji podajemy ścieżkę w formacie „topicName/subscriptions/subscriptionName„:

string subscriptionName = "ordersall";

SubscriptionClient subscriptionClient =
  SubscriptionClient.CreateFromConnectionString(
    connectionString, topicName, subscriptionName, ReceiveMode.PeekLock);

lub:

string subscriptionName = "ordersall";

MessagingFactory messagingFactory =
  MessagingFactory.CreateFromConnectionString(connectionString);

SubscriptionClient subscriptionClient =
  messagingFactory.CreateSubscriptionClient(
    topicName, subscriptionName, ReceiveMode.PeekLock);

lub:

string subscriptionName = "ordersall";

MessagingFactory messagingFactory =
  MessagingFactory.CreateFromConnectionString(connectionString);

MessageReceiver messageReceiver =
  messagingFactory.CreateMessageReceiver(
    String.Format("{0}/subscriptions/{1}", topicName, subscriptionName),
    ReceiveMode.PeekLock);

Pobieranie wiadomości nie różni się niczym w stosunku do tego, co opisywałem przy kolejkach. Tu także mamy do dyspozycji metody Peek, Receive oraz ReceiveBatch, a odebrana wiadomość to znany już obiekt klasy BrokeredMessage:

BrokeredMessage message =
  messageReceiver.Receive(TimeSpan.FromSeconds(5));

if (message != null)
{
  try
  {
    long messageNumber = message.SequenceNumber;
    string messageId = message.MessageId;
    string label = message.Label;
    string correlationId = message.CorrelationId;
    string orderSender = message.Properties["Sender"].ToString();
    string orderRegion = message.Properties["Region"].ToString();

    Order order = message.GetBody<Order>();

    if (orderRegion == "PL")
    {
      //Processing Order...

      message.Complete();
    }
    else
      message.DeadLetter();
  }
  catch (Exception ex)
  {
    message.Abandon();
  }
}
messageReceiver.Close();

W tym i poprzednim wpisie pokazałem podstawowe operacje związane z wysyłaniem i odbieraniem wiadomości przy wykorzystaniu kolejek, topików i subskrypcji. W następnym temacie przedstawię bardziej zaawansowane scenariusze pracy z wiadomościami.

Azure Service Bus brokered messaging – Queues

2014-11-07

W poprzednim temacie opisałem czym jest Azure Service Bus oraz jak rozpocząć korzystanie z tej usługi. Dziś zajmę się tematem kolejek (Queues). Pokażę w jaki sposób zarządzać kolejkami z poziomu kodu oraz jak oprogramować wysyłanie i odbieranie wiadomości. W prezentowanych przykładach wiadomość przesyłana pomiędzy nadawcą i odbiorcą będzie zawierała obiekt zamówienia:

public class Order
{
  public string Id { get; set; }
  public DateTime Date { get; set; }
  public string Customer { get; set; }
  public List<string> Details { get; set; }
}

Na początek zdefiniuję dwa pola, do których będę się odwoływał w kolejnych przykładach. Pierwszym z nich jest connectionString zawierający pobrany z portalu Microsoft Azure connection string dla utworzonej przestrzeni nazw. Drugim polem będzie queueName z nazwą kolejki:

public readonly string connectionString = String.Format(
  "Endpoint={0};SharedAccessKeyName={1};SharedAccessKey={2}",
  "sb://tempnamespace.servicebus.windows.net/",
  "RootManageSharedAccessKey",
  "zFUqkwWoDrem9O8UkdG0pq0sheJze0U/P93f42Aykdc=");

public readonly string queueName = "ordersqueue";

 

Zarządzanie kolejką

Zarządzanie kolejkami odbywa się za pośrednictwem obiektu NamespaceManager, który odpowiada za obsługę przestrzeni nazw Azure Service Bus wskazanej w connection string. Poniższy kod przygotowuje instancję obiektu NamespaceManager, sprawdza czy kolejka o podanej nazwie nie istnieje i tworzy ją w przestrzeni nazw usługi:

NamespaceManager namespaceManager =
  NamespaceManager.CreateFromConnectionString(connectionString);

if (!namespaceManager.QueueExists(queueName))
  namespaceManager.CreateQueue(queueName);

Stworzona w ten sposób kolejka we wszystkich atrybutach posiada domyślne wartości. Jeżeli kolejkę chcemy odpowiednio skonfigurować, do jej utworzenia możemy użyć obiektu QueueDescription:

NamespaceManager namespaceManager =
  NamespaceManager.CreateFromConnectionString(connectionString);

if (!namespaceManager.QueueExists(queueName))
{
  QueueDescription queue = new QueueDescription(queueName)
  {
    MaxSizeInMegabytes = 1024,
    RequiresSession = false,
    RequiresDuplicateDetection = true,
    LockDuration = TimeSpan.FromMinutes(1)
  };
  namespaceManager.CreateQueue(queue);
}

Modyfikacja ustawień istniejącej kolejki również wykonywana jest za pomocą obiektu QueueDescription zwróconego przez metodę GetQueue:

NamespaceManager namespaceManager =
  NamespaceManager.CreateFromConnectionString(connectionString);

QueueDescription queue = namespaceManager.GetQueue(queueName);
queue.MaxDeliveryCount = 5;
namespaceManager.UpdateQueue(queue);

W podobny sposób możemy pobrać wartości atrybutów tylko do odczytu prezentujących aktualny status kolejki:

NamespaceManager namespaceManager =
  NamespaceManager.CreateFromConnectionString(connectionString);

QueueDescription queue = namespaceManager.GetQueue(queueName);
long messageCount = queue.MessageCount;
long sizeInBytes = queue.SizeInBytes;

Usunięcie kolejki sprowadza się do wywołania metody DeleteQueue:

NamespaceManager namespaceManager =
  NamespaceManager.CreateFromConnectionString(connectionString);

namespaceManager.DeleteQueue(queueName);

 

Wysyłanie wiadomości

W celu wysłania lub odebrania wiadomości do/z kolejki musimy utworzyć obiekt klienta kolejki. Mamy trzy sposoby:

QueueClient queueClient =
  QueueClient.CreateFromConnectionString(connectionString, queueName);

lub:

MessagingFactory messagingFactory =
  MessagingFactory.CreateFromConnectionString(connectionString);

QueueClient queueClient =
  messagingFactory.CreateQueueClient(queueName);

lub:

MessagingFactory messagingFactory =
  MessagingFactory.CreateFromConnectionString(connectionString);

MessageSender messageSender =
  messagingFactory.CreateMessageSender(queueName);

Jak widać, w pierwszym i drugim przypadku tworzymy obiekt klasy QueueClient, natomiast w trzecim przypadku obiekt klasy MessageSender. Ja preferuję ostatnie podejście, ponieważ MessageSender jest bardziej uniwersalnym obiektem. W zależności od tego czy jako parametr metody CreateMessageSender przekażemy nazwę kolejki czy topiku, zwrócony obiekt MessageSender za pomocą tych samych metod będzie odwoływał się do właściwego składnika usługi. Pozwala to na dowolne przełączanie naszej aplikacji na pracę z kolejką lub topikiem bez modyfikacji kodu.

Wiadomość wysyłana do kolejki reprezentowana jest przez obiekt klasy BrokeredMessage. Tworząc wiadomość, w konstruktorze przekazujemy obiekt stanowiący jej treść (body) – w naszym przypadku będzie to obiekt klasy Order. W poszczególnych atrybutach obiektu BrokeredMessage możemy umieścić dodatkowe informacje opisujące wiadomość. Dzięki atrybutowi Properties (typu IDictionary<string, object>) do przesyłanej wiadomości możemy dołączyć listę dowolnych parametrów. W celu wysłania przygotowanej wiadomości wystarczy wywołać metodę Send na obiekcie klienta kolejki:

Order order = new Order()
{
  Id = "1",
  Date = DateTime.UtcNow,
  Customer = "Company ABC",
  Details = new List<string> { "Product1", "Product2", "Product3" }
};

BrokeredMessage message = new BrokeredMessage(order)
{
  MessageId = Guid.NewGuid().ToString(),
  Label = "Application X",
  CorrelationId = "Department A",
  TimeToLive = TimeSpan.FromHours(12)
};
message.Properties.Add("Sender", "Jan Kowalski");
message.Properties.Add("Region", "PL");

messageSender.Send(message);

messageSender.Close();

 

Odbieranie wiadomości

Odczyt wiadomości z kolejki może się odbywać w dwóch trybach: ReceiveAndDelete lub PeekLock. W trybie ReceiveAndDelete wiadomość w momencie pobrania jest usuwana z kolejki. Jeżeli przetwarzanie nie powiedzie się to my musimy zadbać o to, żeby wiadomość nie została utracona. W trybie PeekLock po odebraniu wiadomości nie jest ona usuwana z kolejki, a jedynie ustawiana jest na niej odpowiednia blokada zapobiegająca ponownemu pobraniu. Domyślnie blokada zakładana jest na minutę, ale jej czas możemy określić podczas tworzenia kolejki za pomocą atrybutu LockDuration (maksymalnie 5 minut). Po zakończeniu przetwarzania wiadomości musimy poinformować usługę o jego statusie. Mamy do wyboru następujące metody na obiekcie wiadomości:

  • Complete – wiadomość przetworzona poprawnie, zostaje usunięta z kolejki
  • Defer – odłożenie przetwarzania, wiadomość pozostaje w kolejce, ale można się do niej ponownie odwołać jedynie poprzez jej numer (ten scenariusz opiszę w kolejnych tematach)
  • DeadLetter – przeniesienie wiadomości do specjalnej pod-kolejki zawierającej „wymarłe” wiadomości (ten scenariusz także opiszę w kolejnych tematach)
  • Abandon – anulowanie przetwarzania, z wiadomości zdejmowana jest blokada i staje się ona ponownie dostępna do pobrania

Aby odebrać wiadomość musimy utworzyć obiekt klienta kolejki. Domyślnie odbieranie wiadomości realizowane jest w trybie PeekLock, ale możemy to określić podczas tworzenia obiektu. Podobnie jak przy wysyłaniu wiadomości tu także możemy korzystać bezpośrednio z obiektu QueueClient lub bardziej uniwersalnej wersji MessageReceiver:

QueueClient queueClient =
  QueueClient.CreateFromConnectionString(connectionString, queueName, ReceiveMode.PeekLock);

lub:

MessagingFactory messagingFactory =
  MessagingFactory.CreateFromConnectionString(connectionString);

MessageReceiver messageReceiver =
  messagingFactory.CreateMessageReceiver(queueName, ReceiveMode.PeekLock);

W celu pobrania wiadomości z kolejki wywołujemy metodę Receive, z możliwością podania timeout-u dla operacji. Wiadomości zwracane są w kolejności FIFO i jeżeli  kolejka nie jest pusta w wyniku otrzymamy obiekt BrokeredMessage (w przeciwnym wypadku będzie to null). Odczyt właściwej treści wiadomości odbywa się poprzez metodę GetBody<T>:

BrokeredMessage message =
  messageReceiver.Receive(TimeSpan.FromSeconds(5));

if (message != null)
{
  try
  {
    long messageNumber = message.SequenceNumber;
    string messageId = message.MessageId;
    string label = message.Label;
    string correlationId = message.CorrelationId;
    string orderSender = message.Properties["Sender"].ToString();
    string orderRegion = message.Properties["Region"].ToString();

    Order order = message.GetBody<Order>();

    if (orderRegion == "PL")
    {
      //Processing Order...

      message.Complete();
    }
    else
      message.DeadLetter();
  }
  catch (Exception ex)
  {
    message.Abandon();
  }
}
messageReceiver.Close();

Oprócz pokazanego standardowego pobierania wiadomości mamy jeszcze dwie opcje. Pierwszą z nich jest podgląd, czyli możliwość odczytu wiadomości bez usuwania z kolejki i zakładania blokady. Służy do tego metoda Peek:

BrokeredMessage message = messageReceiver.Peek();

Drugą opcją jest pobranie za jednym razem większej liczby wiadomości. Wystarczy wywołać metodę ReceiveBatch z parametrem określającym liczbę wiadomości do pobrania:

IEnumerable<BrokeredMessage> messages = messageReceiver.ReceiveBatch(10);

To wszystko jeżeli chodzi o podstawowe operacje na kolejkach Azure Service Bus. Oczywiście jest to tylko niezbędne minimum pozwalające zbudować pierwsze działające rozwiązanie. W kolejnych tematach przedstawię znacznie bardziej rozbudowane scenariusze pracy z wiadomościami takie jak: obsługa pod-kolejek DeadLetter, opóźnione przetwarzanie, wykrywanie zdublowanych wiadomości, harmonogram dostarczania wiadomości, sesje, transakcje przy wysyłaniu i odbieraniu wielu wiadomości oraz praca w modelu request-response. Ale zanim przejdę do tych tematów, w następnym wpisie zajmę się topikami i subskrypcjami.

Azure Service Bus brokered messaging – komunikacja w systemach rozproszonych

2014-10-25

Dzisiejszym wpisem chciałbym rozpocząć cykl kilku tematów poświęconych omówieniu Azure Service Bus – jednego ze składników Microsoft Azure umożliwiającego wymianę informacji w systemach rozproszonych. Azure Service Bus pozwala na komunikację w dwóch trybach: relayed messaging oraz brokered messaging. Tryb relayed polega na synchronicznej wymianie wiadomości, podczas której nadawca i odbiorca muszą być dostępni on-line (analogicznie do WebService). Z kolei tryb brokered pozwala na komunikację asynchroniczną gdzie nadawca i odbiorca pracują niezależnie, a wiadomości są kolejkowane na serwerach Azure. Znacznie ciekawszy jest drugi ze wspomnianych trybów i właśnie jemu poświęcę ten cykl. Dziś przedstawię ogólną koncepcję komunikacji w trybie brokered messaging, omówię poszczególne elementy Azure Service Bus takie jak Queue, Topic, Subscription, Brokered Message oraz pokażę kroki niezbędne do rozpoczęcia budowy konkretnego rozwiązania. W następnych wpisach znajdzie się już znacznie więcej kodu prezentującego praktyczne wykorzystanie omawianych elementów.

Jak wspomniałem wcześniej, tryb brokered messaging w Azure Service Bus pozwala na asynchroniczną wymianę informacji pomiędzy nadawcą i odbiorcą. Nadawca wysyłając wiadomość nie wie kiedy oraz przez kogo zostanie ona odczytana, jego jedynym zadaniem jest jej utworzenie i wysłanie do usługi Service Bus. Z kolei odbiorca w dowolnym momencie łączy się z usługą w celu pobrania (w kolejności FIFO) i przetworzenia oczekujących wiadomości. Największą zaletą tego rozwiązania jest fakt, że za cały mechanizm kolejkowania i zwracania wiadomości odpowiada Azure, naszą rolą jest jedynie oprogramowanie wysyłania i odbierania wiadomości. Budując rozwiązanie komunikacyjne oparte o Service Bus w trybie brokered messaging mamy do wyboru dwa podejścia: kolejki (Queues) oraz topiki z subskrypcjami (Topics, Subscriptions).

 

Queue

Poniższy schemat prezentuje zasadę komunikacji przy użyciu kolejek:

sb_queues

Źródło: MSDN

Najważniejszym elementem jest tutaj kolejka (Queue), która pełni rolę pojemnika na wiadomości. Nadrzędnym elementem jest Namespace, który może zawierać wiele kolejek (ich nazwy muszą być unikalne). Nadawcą może być dowolna aplikacja lub usługa, której rolą jest nawiązanie połączenia z usługą Azure Service Bus, utworzenie wiadomości oraz wysłanie jej do konkretnej kolejki. Odbiorcą również może być dowolny rodzaj aplikacji, a co najważniejsze z jednej kolejki może korzystać wielu odbiorców równocześnie. Jak to działa? Wiadomości zwracane są z kolejki w kolejności ich dodawania (FIFO). Po każdorazowym zgłoszeniu się odbiorcy po wiadomość usługa Azure Service Bus gwarantuje zwrócenie danej wiadomości tylko raz. W wyniku każdego kolejnego zgłoszenia otrzymamy następną wiadomość niezależnie czy będzie to ten sam odbiorca czy inny. Jak łatwo zauważyć, daje to bardzo duże możliwości skalowania naszego rozwiązania bez żadnych nakładów pracy. Jeżeli widzimy, że nasza aplikacja odbierająca nie nadąża z przetwarzaniem wiadomości, wystarczy uruchomić jej kopię na innej maszynie i automatycznie otrzymujemy wzrost wydajności dzięki zrównolegleniu operacji.

 

Topic, Subscription

Drugim podejściem jest wykorzystanie topików i subskrypcji. Oto jak wygląda schemat komunikacji w tym przypadku:

sb_topics

Źródło: MSDN

Od strony nadawcy praktycznie nic się nie zmienia w stosunku do kolejek. Tworzenie wiadomości odbywa się w identyczny sposób, a jedyną różnicą jest wysyłanie ich do topiku zamiast do kolejki. Z punktu widzenia nadawcy topik funkcjonalnie odpowiada kolejce. Większe różnice są po stronie odbiorcy. W celu odczytu wiadomości odbiorca nie odwołuje się bezpośrednio do topiku tylko korzysta z wybranej subskrypcji, która z jego punktu widzenia posiada funkcjonalność kolejki. To jakie wiadomości z topiku trafią do danej subskrypcji określane jest poprzez filtry operujące na atrybutach wiadomości. Podstawową różnicą w stosunku do kolejek jest to, iż w przypadku topiku tą samą wiadomość może otrzymać kilku odbiorców. Jeżeli dana wiadomość przesłana do topiku spełni warunki filtrów więcej niż jednej subskrypcji to jej kopia zostanie przekazana do każdej z nich. Warto dodać, że filtry subskrypcji sprawdzane są tylko w momencie pojawienia się wiadomości w topiku. Jeżeli utworzymy nową subskrypcję to mogą do niej trafić jedynie wiadomości wysłane od tego momentu, wszystkie wcześniejsze mimo spełnionych warunków filtra nie zostaną do niej przekazane. Możemy również utworzyć subskrypcję bez filtra, wówczas trafią do niej wszystkie wiadomości przesłane do topiku. Sam mechanizm odczytu wiadomości z konkretnej subskrypcji nie różni się niczym od kolejki. Z jednej subskrypcji może równocześnie korzystać wielu odbiorców, gdzie podobnie jak w przypadku kolejki gwarantowane jest dostarczenie danej wiadomości tylko do jednego z nich.

 

Brokered Message

Niezależnie od wybranego schematu komunikacji (kolejki lub topiki z subskrypcjami) wiadomość przesyłana pomiędzy nadawcą i odbiorcą ma następującą strukturę:

sb_message

Składa się ona z nagłówka (header) oraz treści (body). Nagłówek posiada szereg zdefiniowanych atrybutów, w których możemy umieścić dodatkowe informacje opisujące naszą wiadomość (m.in. MessageId, CorrelationId, SessionId, Label, Properties). Dzięki atrybutowi Properties (typu IDictionary<string, object>) do przesyłanej wiadomości możemy dołączyć listę dowolnych parametrów. Wspomniane wcześniej filtry subskrypcji decydujące o tym, które wiadomości z topiku zostaną przekazane do danej subskrypcji mogą korzystać z właściwości CorrelationId lub poszczególnych elementów właściwości Properties. Główną częścią wiadomości jest jej treść – czyli sekcja body, w której możemy umieścić konkretny obiekt. W tym miejscu trzeba zaznaczyć, iż maksymalny rozmiar przesyłanej wiadomości to 256kB, z czego 64kB przypada na nagłówek a 192kB na treść. Może się wydawać, że dla wielu scenariuszy nie jest to rozmiar wystarczający, istnieje jednak sposób na poradzenie sobie z tym ograniczeniem, co pokażę w kolejnych wpisach.

 

Konfiguracja Azure Service Bus

Przed rozpoczęciem tworzenia konkretnego rozwiązania musimy skonfigurować usługę Azure Service Bus. Logujemy się do portalu Microsoft Azure, wybieramy sekcję Service Bus i klikamy Create w celu utworzenia przestrzeni nazw (Namespace), w której będą znajdować się nasze kolejki, topiki i subskrypcje.

azure_sb

Podajemy nazwę dla tworzonego obiektu Namespace, wybieramy region i wskazujemy typ MESSAGING. Po zakończeniu operacji musimy pobrać Connection String dla utworzonej przestrzeni nazw, za pomocą którego nasze aplikacje będą łączyły się z usługą. Klikamy Connection Information i kopiujemy widoczny Connection String:

sb_namespace_cs

Składa się on z trzech elementów: adresu Namespace (Endpoint), nazwy polisy zawierającej klucz dostępu (SharedAccessKeyName) oraz samego klucza (SharedAccessKey):

Endpoint=sb://tempnamespace.servicebus.windows.net/;
SharedAccessKeyName=RootManageSharedAccessKey;
SharedAccessKey=zFUqkwWoDrem9O8UkdG0pq0sheJze0U/P93f42Aykdc=

Polisami oraz kluczami możemy zarządzać w opcji Configure naszej przestrzeni nazw:

sb_ns_configure

Jak widać domyślnie tworzona jest polisa RootManageSharedAccessKey z uprawnieniami zarządzania przestrzenią (Manage) oraz wysyłania i odbierania wiadomości (Send, Listen). Jeżeli dla danej aplikacji chcemy ograniczyć te prawa (np. tylko wysyłanie wiadomości) możemy utworzyć nową polisę i pobrać odpowiedni Connection String. Każda polisa posiada dwa klucze dostępowe (primary i secondary) działające równocześnie. Pozwala to na okresową zmianę kluczy bez konieczności natychmiastowej aktualizacji wszystkich klientów.

Przestrzeń nazw posiada także opcje Queues oraz Topics pozwalające na tworzenie kolejek, topików i subskrypcji, ale ja w kolejnych wpisach pokażę jak robić to z poziomu kodu.

 

Konfiguracja projektu Visual Studio

Ostatnim elementem jaki pozostał jest podłączenie odpowiednich referencji do naszego projektu w celu skorzystania z API usługi Azure Service Bus. W tym celu klikamy prawym klawiszem na References, wybieramy Manage NuGet Packages, wyszukujemy paczkę Microsoft Azure Service Bus i instalujemy. W efekcie do naszego projektu dodane zostaną referencje do bibliotek Microsoft.ServiceBus oraz Microsoft.WindowsAzure.Configuration.

W kolejnych wpisach znajdzie się zdecydowanie więcej kodu, a rozpocznę od pokazania jak oprogramować wysyłanie i odbieranie wiadomości do/z kolejki.

Zarządzanie serwerem Exchange 2010/2013 poprzez Exchange Management Shell

2014-07-20

W jednym z poprzednich tematów opisałem sposób zarządzania serwerem Exchange 2007 poprzez polecenia PowerShell (tzw. cmdlets) wywoływane z poziomu .NET. Jeżeli tego samego rozwiązania chcielibyśmy użyć do obsługi serwerów Exchange 2010/2013 musimy nieco zmodyfikować nasz kod. W tych wersjach Exchange korzystanie ze środowiska Exchange Management Shell odbywa się poprzez zdalne sesje PowerShell (PowerShell remoting), a nie tak jak wcześniej przez załadowanie odpowiedniego PSSnapIn. Istnieje co prawda możliwość wykorzystania PSSnapIn „Microsoft.Exchange.Management.PowerShell.E2010” ale jest to metoda niezalecana. W przypadku Exchange 2010/2013 kod metody InvokeCommand (zamieszczonej we wspomnianym wcześniej wpisie) powinien wyglądać tak:

public void InvokeCommand(
	string exchangeEndpointUri,
	string command,
	Dictionary<string, object> parameters,
	out List<Dictionary<string, string>> results,
	out List<string> errors)
{
	results = new List<Dictionary<string, string>>();
	errors = new List<string>();

	var connectionInfo = new WSManConnectionInfo(
		new Uri(exchangeEndpointUri),
		"http://schemas.microsoft.com/powershell/Microsoft.Exchange",
		(PSCredential)null);
	connectionInfo.AuthenticationMechanism = AuthenticationMechanism.Kerberos;
	connectionInfo.SkipCACheck = true;
	connectionInfo.SkipCNCheck = true;

	using (Runspace myRunSpace = RunspaceFactory.CreateRunspace(connectionInfo))
	{
		myRunSpace.Open();
		using (PowerShell powershell = PowerShell.Create())
		{
			powershell.Runspace = myRunSpace;
			powershell.AddCommand(command);
			powershell.AddParameters(parameters);

			Collection<PSObject> commandResults = powershell.Invoke();

			if (powershell.Streams.Error != null && powershell.Streams.Error.Count > 0)
				foreach (ErrorRecord error in powershell.Streams.Error)
					errors.Add(error.ToString());

			if (commandResults != null)
				foreach (PSObject commandResult in commandResults)
				{
					var result = new Dictionary<string, string>();
					foreach (PSPropertyInfo property in commandResult.Properties)
					{
						string propertyName = property.Name;
						string propertyValue = property.Value == null ? "" : property.Value.ToString();

						result.Add(propertyName, propertyValue);
					}
					results.Add(result);
				}
		}
		myRunSpace.Close();
	}
}

Główna zmiana polega na wykorzystaniu klasy WSManConnectionInfo do połączenia z serwerem Exchange (wymagane jest podanie adresu serwera oraz w przypadku uwierzytelniania konkretnym użytkownikiem – obiektu PSCredential). Poprzednio do tego celu używany był odpowiedni plik PSSnapIn ładowany za pomocą metody RunspaceConfiguration.AddPSSnapIn. W powyższej metodzie do wywoływania poleceń cmdlet zastosowałem klasę PowerShell, ale można wykorzystać poprzednio użyte klasy Pipeline i Command. Największą zaletą nowego podejścia jest fakt, że nasz kod możemy teraz uruchomić na dowolnej maszynie mogącej połączyć się z serwerem Exchange 2010/2013.

Przełączanie skrzynek Exchange pomiędzy kontami Active Directory z wykorzystaniem Exchange Management Shell

2014-05-20

Od pewnego czasu mam okazję budować system, którego część funkcjonalności dotyczy automatyzacji czynności administracyjnych związanych z zarządzaniem użytkownikami Active Directory. Jedną z takich czynności jest przełączanie skrzynek pocztowych Exchange 2007 pomiędzy dwoma kontami użytkowników. W dzisiejszym wpisie pokażę, w jaki sposób zrealizować to zadanie wykorzystując Exchange Management Shell (EMS). EMS jest środowiskiem skryptowym pozwalającym na zarządzanie serwerami Exchange poprzez polecenia PowerShell (tzw. cmdlets). Możliwość wywoływania poleceń cmdlets z kodu .NET zapewnia biblioteka System.Management.Automation.dll. Domyślnie znajduje się ona w katalogu C:\Program Files\Reference Assemblies\Microsoft\WindowsPowerShell\v1.0, a dostępna jest po zainstalowaniu Windows PowerShell SDK będącego składnikiem Windows Platform SDK.
Poniżej znajduje się kod stworzonej przeze mnie metody pozwalającej na wywoływanie poleceń cmdlets. W argumentach przyjmuje nazwę polecenia oraz jego parametry w postaci Dictionary<string, object> (nazwa parametru, wartość). Poprzez argument results zwracana jest lista obiektów Dictionary<string, string>, gdzie każdy słownik reprezentuje właściwości danego obiektu zwróconego przez cmdlet (nazwa właściwości, wartość). Z kolei w przypadku wystąpienia błędów, ich lista zwracana jest poprzez argument errors.

using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Collections.ObjectModel;
public void InvokeCommand(string command, Dictionary<string, object> parameters,
	out List<Dictionary<string, string>> results, out List<string> errors)
{
	results = new List<Dictionary<string, string>>();
	errors = new List<string>();

	RunspaceConfiguration rsConfig = RunspaceConfiguration.Create();
	PSSnapInException snapInException = null;
	PSSnapInInfo info = rsConfig.AddPSSnapIn("Microsoft.Exchange.Management.PowerShell.Admin", out snapInException);

	using (Runspace myRunSpace = RunspaceFactory.CreateRunspace(rsConfig))
	{
		myRunSpace.Open();
		using (Pipeline pipeLine = myRunSpace.CreatePipeline())
		{
			Command myCommand = new Command(command);

			foreach (var parameter in parameters)
				myCommand.Parameters.Add(new CommandParameter(parameter.Key, parameter.Value));

			pipeLine.Commands.Add(myCommand);
			Collection<PSObject> commandResults = pipeLine.Invoke();

			if (pipeLine.Error != null && pipeLine.Error.Count > 0)
				foreach (PSObject error in pipeLine.Error.ReadToEnd())
					errors.Add(error.ToString());

			if (commandResults != null)
				foreach (PSObject commandResult in commandResults)
				{
					var result = new Dictionary<string, string>();
					foreach (PSPropertyInfo property in commandResult.Properties)
					{
						string propertyName = property.Name;
						string propertyValue = property.Value == null ? "" : property.Value.ToString();

						result.Add(propertyName, propertyValue);
					}
					results.Add(result);
				}
		}
		myRunSpace.Close();
	}
}

Aby wywołać cmdlet musimy utworzyć obiekt Runspace przekazując instancję RunspaceConfiguration z dodanym PSSnapIn-em „Microsoft.Exchange.Management.PowerShell.Admin”. Następnie tworzymy obiekt Command (podając nazwę polecenia), ustawiamy jego parametry, po czym dodajemy go do utworzonego za pomocą metody CreatePipeline potoku komend obiektu Runspace. Do potoku możemy dodać więcej niż jedną komendę. Cmdlets uruchamiamy poprzez metodę Invoke potoku, w wyniku otrzymując kolekcję obiektów typu PSObject. Jeżeli podczas przetwarzania zgłoszone zostały błędy, mamy do nich dostęp poprzez właściwość Error obiektu Pipeline. Więcej informacji na temat wykorzystania Exchange Management Shell w kodzie .NET można znaleźć w artykule Using Exchange Management Shell Commands With Managed Code.

Skoro mamy już metodę pozwalającą na uruchamianie poleceń cmdlet, możemy przejść do realizacji wspomnianego we wstępie przełączania skrzynki pocztowej Exchange pomiędzy dwoma kontami Active Directory. Zadanie to składa się z następujących kroków:

1. Odczyt informacji o przełączanej skrzynce pocztowej
2. Odłączenie skrzynki od aktualnego konta Active Directory
3. Podłączenie skrzynki do nowego konta Active Directory
4. Utworzenie skrzynki dla nowego konta jeżeli odłączona skrzynka została usunięta

Odczyt informacji o skrzynce

Informacje jakich potrzebujemy to identyfikator skrzynki (ExchangeGuid) oraz baza, w której ta skrzynka się znajduje (Database). Do odczytu informacji o skrzynce służy polecenie Get-Mailbox, do którego przekazujemy parametr Identity identyfikujący skrzynkę. W tym przypadku jako jego wartość przekazywany jest MailNickname (Alias) pochodzący z aktualnego konta Active Directory. Możemy skorzystać z wartości innych atrybutów: GUID, DN, Display name, Domain\Account, UPN, SmtpAddress. Oto kod pobierający wymagane informacje o skrzynce:

string mailNickname = "jan.kowalski";
var parameters = new Dictionary<string, object>();
List<Dictionary<string, string>> results;
List<string> errors;

parameters.Add("Identity", mailNickname);
InvokeCommand("Get-Mailbox", parameters, out results, out errors);

if (errors.Count > 0)
{
	//Obsługa błędów
}
else
	if (results.Count > 0)
	{
		string exchangeGuid = results[0]["ExchangeGuid"];
		string database = results[0]["Database"];
	}

Odłączenie skrzynki

Kolejnym krokiem jest odłączenie skrzynki od aktualnego konta Active Directory. Realizowane jest to poprzez polecenie Disable-Mailbox. Podobnie jak wcześniej przekazujemy parametr Identity (tym razem używamy odczytanego ExchangeGuid) oraz parametr Confirm ustawiony na false (dzięki temu nie jest zadawane pytanie o potwierdzenie odłączenia). W efekcie tej operacji z konta Active Directory usunięte zostają wszystkie atrybuty związane ze skrzynką pocztową Exchange.

var parameters = new Dictionary<string, object>();
List<Dictionary<string, string>> results;
List<string> errors;

parameters.Add("Identity", exchangeGuid);
parameters.Add("Confirm", false);
InvokeCommand("Disable-Mailbox", parameters, out results, out errors);

if (errors.Count > 0)
{
	//Obsługa błędów
}

Podłączenie skrzynki

Teraz przyszła kolej na podłączenie skrzynki do nowego konta Actice Directory. Do tego celu służy polecenie Connect-Mailbox. Przekazywane do niego parametry to: Identity (ExchangeGuid), User (AccountName nowego konta), Alias (MailNickname nowego konta) oraz Database (baza skrzynki).

string accountName = "JKowalski";
string mailNickname = "jkowalski";
var parameters = new Dictionary<string, object>();
List<Dictionary<string, string>> results;
List<string> errors;

parameters.Add("Identity", exchangeGuid);
parameters.Add("User", accountName);
parameters.Add("Alias", mailNickname);
parameters.Add("Database", database);
InvokeCommand("Connect-Mailbox", parameters, out results, out errors);

if (errors.Count > 0)
{
	//Obsługa błędów
}

Utworzenie skrzynki

Jeżeli dana skrzynka nie została nigdy użyta, po odłączeniu od konta zostaje ona usunięta. W konsekwencji operacja podłączenia jej do nowego konta nie powiedzie się. Rozwiązaniem jest utworzenie dla tego konta nowej skrzynki za pomocą polecenia Enable-Mailbox. Przekazywane do niego parametry to: Identity (AccountName nowego konta), Alias (MailNickname nowego konta) oraz Database (baza skrzynki).

string accountName = "JKowalski";
string mailNickname = "jan.kowalski2";
var parameters = new Dictionary<string, object>();
List<Dictionary<string, string>> results;
List<string> errors;

parameters.Add("Identity", accountName);
parameters.Add("Alias", mailNickname);
parameters.Add("Database", database);
InvokeCommand("Enable-Mailbox", parameters, out results, out errors);

if (errors.Count > 0)
{
		//Obsługa błędów
}

Więcej informacji na temat zarządzania skrzynkami Exchange poprzez polecenia cmdlet: Mailbox cmdlets.

Odblokowanie konta w Active Directory

2014-04-16

W jednym z tworzonych przeze mnie systemów pojawiła się konieczność dodania funkcjonalności pozwalającej na odblokowywanie kont użytkowników w Active Directory (zablokowanych na skutek kilkukrotnego podania błędnego hasła). Standardowa procedura odblokowania konta to wejście do konsoli Active Directory i zaznaczenie odpowiedniej opcji w ustawieniach danego użytkownika:

ADUnlockAccount

Aby zrealizować to zadanie z poziomu kodu wystarczy skorzystać z klas PrincipalContext i UserPrincipal znajdujących się w przestrzeni System.DirectoryServices.AccountManagement:

string domain = "domain.com";
string container = "OU=Test,DC=domain,DC=com";
string accountName = "SC10006";

using (PrincipalContext principalContext = new PrincipalContext(ContextType.Domain, domain, container))
{
	using (UserPrincipal userPrincipal = UserPrincipal.FindByIdentity(principalContext, accountName))
	{
		if (userPrincipal != null)
		{
			userPrincipal.UnlockAccount();
			userPrincipal.Save();
		}
	}
}

Klasa PrincipalContext pozwala na połączenie z usługą Active Directory. W konstruktorze podajemy typ kontekstu (Machine, Domain, ApplicationDirectory) oraz opcjonalnie: nazwę dla wybranego typu kontekstu (w tym przypadku nazwę domeny), lokalizację szukanego obiektu (w tym przypadku OU, w którym znajduje się użytkownik) oraz dane uwierzytelniające. Jeżeli nie podamy tych ostatnich, operacja wykonana zostanie w kontekście bieżącego użytkownika (musi on więc posiadać do niej uprawnienia).
Klasa UserPrincipal odpowiada za odnalezienie użytkownika i wykonanie operacji odblokowania konta.

Więcej informacji na MSDN: System.DirectoryServices.AccountManagement, PrincipalContext, UserPrincipal.

DataGridView – płynne przewijanie zawartości

2013-05-19

Przy dużych zbiorach danych prezentowanych w kontrolce DataGridView może pojawić się problem z płynnością ich przewijania. Sposobem na wyeliminowanie tego efektu jest ustawienie właściwości DoubleBuffered na true. Jest to właściwość protected więc mamy dwie możliwości: stworzyć własną klasę dziedziczącą po DataGridView i w niej ustawić wartość właściwości DoubleBuffered lub stworzyć metodę rozszerzającą, która przy użyciu refleksji odwoła się do omawianej właściwości i ustawi jej wartość. Poniżej znajduje się implementacja drugiego z wymienionych sposobów:

public static class DataGridViewExtensions
{
	public static void DoubleBuffered(this DataGridView dgv, bool setting)
	{
		Type dgvType = dgv.GetType();
		PropertyInfo pi = dgvType.GetProperty("DoubleBuffered",
			BindingFlags.Instance | BindingFlags.NonPublic);
		pi.SetValue(dgv, setting, null);
	}
}

Użycie powyższej metody:

dataGridView1.DoubleBuffered(true);

Dzięki właściwości DoubleBuffered jesteśmy w stanie wyeliminować problemy z brakiem płynności oraz odświeżaniem kontrolki DataGridView podczas przewijania jej zawartości.

WPF – odwołanie do CollectionView.CurrentItem w kodzie XAML

2013-03-25

Podczas korzystania z obiektu CollectionView w widoku, oprócz samej prezentacji kolekcji często mamy także dodatkowe kontrolki powiązane z właściwościami aktualnie wybranego elementu. Standardowym podejściem przy realizacji tego zadania jest utworzenie w klasie ViewModel właściwości udostępniającej aktualnie wybrany element kolekcji, a następnie zdefiniowanie w kontrolkach wiązań do interesujących nas właściwości tego elementu:

ViewModel:

public ICollectionView Persons
{
	get { return persons; }
}

public Person SelectedPerson
{
	get { return Persons.CurrentItem as Person; }
}

View:

<TextBox Text="{Binding SelectedPerson.LastName}" IsReadOnly="True" />

Istnieje jednak prostsza metoda, możemy odwołać się do właściwości CollectionView.CurrentItem bezpośrednio w kodzie XAML. Służy do tego znak slash „/”:

<TextBox Text="{Binding Persons/LastName}" IsReadOnly="True" />

Zastosowanie wyrażenia Persons/LastName oznacza odwołanie do właściwości LastName aktualnie wybranego elementu kolekcji Persons. Po tej modyfikacji właściwość SelectedPerson w klasie ViewModel jest zbędna.

Więcej informacji można znaleźć na MSDN: Binding.Path Property.

Implementacja mechanizmu skórek w WPF

2013-02-20

W WPF przy wykorzystaniu stylów oraz szablonów w prosty sposób możemy stworzyć mechanizm skórek, dzięki któremu możliwa będzie zmiana wyglądu naszej aplikacji w trakcie jej działania. W dzisiejszym wpisie pokażę jak taki mechanizm stworzyć. Zacznijmy od prostego okna składającego się z kontenera Grid oraz trzech przycisków. Po naciśnięciu każdego z nich załadowana zostanie odpowiednia skórka zmieniająca wygląd aplikacji:

<Window x:Class="Skin.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="194" Width="536">
    <Grid>
        <Button Name="GraySkinButton" Content="SZARY"
                HorizontalAlignment="Left" VerticalAlignment="Top"
                Margin="27,53,0,0" Width="140" Height="50" FontSize="14"
                Click="GraySkinButton_Click" />
        <Button Name="BlackSkinButton" Content="CZARNY"
                HorizontalAlignment="Left" VerticalAlignment="Top"
                Margin="189,53,0,0" Width="140" Height="50" FontSize="14"
                Click="BlackSkinButton_Click" />
        <Button Name="BlueSkinButton" Content="NIEBIESKI"
                HorizontalAlignment="Left" VerticalAlignment="Top"
                Margin="351,53,0,0" Width="140" Height="50" FontSize="14"
                Click="BlueSkinButton_Click" />
    </Grid>
</Window>

Teraz stwórzmy definicje naszych skórek: szarej, czarnej oraz niebieskiej. Z reguły poszczególne elementy umieszcza się w oddzielnych plikach (np. style i szablony) ale na potrzeby tego wpisu kod każdej skórki umieszczę w jednym pliku.

Szara skórka (plik GraySkin.xaml) składa się jedynie ze stylu zmieniającego tło grida:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <Style TargetType="Grid">
        <Setter Property="Background" Value="#FFF3F3F9" />
    </Style>

</ResourceDictionary>

Czarna skórka (plik BlackSkin.xaml) posiada definicję szablonu przycisku oraz style dla przycisku i grida:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <ControlTemplate x:Key="button" TargetType="Button">
        <Border x:Name="border"
				Background="{TemplateBinding Background}"
				BorderBrush="{TemplateBinding BorderBrush}"
				BorderThickness="{TemplateBinding BorderThickness}">
            <ContentPresenter x:Name="content" HorizontalAlignment="Center"
                              VerticalAlignment="Center" />
        </Border>
        <ControlTemplate.Triggers>
            <Trigger Property="IsMouseOver" Value="True">
                <Setter Property="BorderThickness" Value="3" TargetName="border" />
            </Trigger>
            <Trigger Property="IsEnabled" Value="False">
                <Setter Property="TextBlock.Foreground" Value="LightGray"
                        TargetName="content" />
            </Trigger>
            <Trigger Property="IsPressed" Value="True">
                <Setter TargetName="content" Property="RenderTransform">
                    <Setter.Value>
                        <TranslateTransform X="1" Y="1" />
                    </Setter.Value>
                </Setter>
            </Trigger>
        </ControlTemplate.Triggers>
    </ControlTemplate>

    <Style TargetType="Button">
        <Setter Property="Template" Value="{StaticResource button}" />
        <Setter Property="Foreground" Value="White" />
        <Setter Property="Background" Value="#FF15A015" />
        <Setter Property="BorderBrush" Value="#FF97F85C" />
        <Setter Property="BorderThickness" Value="0" />
    </Style>

    <Style TargetType="Grid">
        <Setter Property="Background" Value="Black" />
    </Style>

</ResourceDictionary>

Niebieska skórka (plik BlueSkin.xaml) podobnie jak czarna zawiera szablon przycisku oraz style przycisku i grida:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <LinearGradientBrush x:Key="buttonBackground" StartPoint="0.5,0" EndPoint="0.5,1">
        <GradientStop Color="#FF00255A" Offset="0.0" />
        <GradientStop Color="#FF016CC5" Offset="1.0" />
    </LinearGradientBrush>

    <LinearGradientBrush x:Key="activeButtonBackground" StartPoint="0.5,0" EndPoint="0.5,1">
        <GradientStop Color="#FF7FCBED" Offset="0.0" />
        <GradientStop Color="#FFD3ECF9" Offset="1.0" />
    </LinearGradientBrush>

    <SolidColorBrush x:Key="buttonBorderBrush" Color="White" />
    <SolidColorBrush x:Key="buttonForeground" Color="White" />
    <SolidColorBrush x:Key="activeButtonForeground" Color="#FF004990" />

    <DropShadowEffect x:Key="shadow" Color="Gray" ShadowDepth="0" Opacity="0.9" />

    <ControlTemplate x:Key="button" TargetType="Button">
        <Border x:Name="border"
				Background="{StaticResource buttonBackground}"
				BorderBrush="{StaticResource buttonBorderBrush}"
				BorderThickness="3"
				CornerRadius="6">
            <TextBlock x:Name="content" Text="{TemplateBinding Content}"
                       HorizontalAlignment="Center" VerticalAlignment="Center"
                       Foreground="{StaticResource buttonForeground}"
                       TextWrapping="Wrap" TextAlignment="Center" />
        </Border>
        <ControlTemplate.Triggers>
            <Trigger Property="IsMouseOver" Value="True">
                <Setter TargetName="border" Property="Background"
                        Value="{StaticResource activeButtonBackground}" />
                <Setter TargetName="content" Property="Foreground"
                        Value="{StaticResource activeButtonForeground}" />
                <Setter TargetName="content" Property="FontWeight"
                        Value="SemiBold" />
            </Trigger>
            <Trigger Property="IsEnabled" Value="False">
                <Setter Property="Background" Value="Gray" TargetName="border" />
                <Setter Property="BorderBrush" Value="Gray" TargetName="border" />
            </Trigger>
            <Trigger Property="IsPressed" Value="True">
                <Setter TargetName="content" Property="RenderTransform">
                    <Setter.Value>
                        <TranslateTransform X="1" Y="1" />
                    </Setter.Value>
                </Setter>
            </Trigger>
        </ControlTemplate.Triggers>
    </ControlTemplate>

    <Style TargetType="Button">
        <Setter Property="Template" Value="{StaticResource button}" />
        <Setter Property="Effect" Value="{StaticResource shadow}" />
    </Style>

    <Style TargetType="Grid">
        <Setter Property="Background" Value="#FFE3ECFD" />
    </Style>

</ResourceDictionary>

Dysponując definicjami skórek możemy przejść do obsługi ich przełączania. Cały mechanizm polega na dynamicznym ładowaniu plików ze skórkami do zasobów aplikacji:

public partial class MainWindow : Window
{
	private ResourceDictionary currentSkin;
	private Collection<ResourceDictionary> appResources;

	public MainWindow()
	{
		InitializeComponent();
		appResources = App.Current.Resources.MergedDictionaries;
		SetSkin(LoadResourceDictionary("GraySkin.xaml"));
	}

	private ResourceDictionary LoadResourceDictionary(string dictionaryName)
	{
		return (ResourceDictionary)App.LoadComponent(
			new Uri(dictionaryName, UriKind.Relative));
	}

	private void SetSkin(ResourceDictionary newSkin)
	{
		if (newSkin == null)
			return;

		if (currentSkin != null)
			appResources.Remove(currentSkin);

		appResources.Add(newSkin);
		currentSkin = newSkin;
	}

	private void GraySkinButton_Click(object sender, RoutedEventArgs e)
	{
		SetSkin(LoadResourceDictionary("GraySkin.xaml"));
	}

	private void BlackSkinButton_Click(object sender, RoutedEventArgs e)
	{
		SetSkin(LoadResourceDictionary("BlackSkin.xaml"));
	}

	private void BlueSkinButton_Click(object sender, RoutedEventArgs e)
	{
		SetSkin(LoadResourceDictionary("BlueSkin.xaml"));
	}
}

Oto efekt działania aplikacji:

GraySkin

BlackSkin

BlueSkin