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.

Reklamy

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.

Zmiana Capabilities dla istniejącego agenta

2014-10-03

Podczas budowy agenta typu Extensible Connection 2.0 w naszej klasie implementujemy między innymi interfejs IMAExtensible2GetCapabilities. Wymaga to dodania do niej publicznej właściwości Capabilities zwracającej obiekt typu MACapabilities. Obiekt ten posiada szereg ustawień konfigurujących pracę naszego agenta. Oto przykładowa implementacja tej właściwości:

public MACapabilities Capabilities
{
  get
  {
    return new MACapabilities()
      {
        ConcurrentOperation = false,
        DeltaImport = false,
        DistinguishedNameStyle = MADistinguishedNameStyle.Generic,
        DeleteAddAsReplace = false,
        ExportType = MAExportType.AttributeReplace,
        NoReferenceValuesInFirstExport = false,
        SupportExport = true
      };
  }
}

Podczas dodawania agenta do Synchronization Service jednym z kroków jest Capabilities:

MaCapabilities

Właśnie w tym momencie następuje odwołanie do właściwości Capabilities w klasie naszego agenta i odczyt znajdujących się tam ustawień. Niestety tu pojawia się pewien problem. Odczyt ten następuje tylko podczas dodawania nowego agenta i pobrane ustawienia są dla niego zapamiętywane w aktualnej postaci. Jeżeli później wejdziemy w edycję istniejącego agenta to kroku Capabilities już nie zobaczymy. Co więcej, nawet jak wyeksportujemy agenta do pliku i następnie utworzymy nowego poprzez import, to ustawienia Capabilities nie zostaną wczytane z biblioteki a jedynie odtworzone z pliku eksportu. Co to oznacza? Jeżeli zmodyfikujemy ustawienia MACapabilities w naszej klasie to pomimo podmiany biblioteki agent cały czas będzie działał zgodnie z ustawieniami zapamiętanymi podczas jego tworzenia. Nie ma możliwości odświeżenia Capabilities dla istniejącego agenta więc nie jesteśmy w stanie zmienić dla niego tych ustawień (Refresh interfaces nie zadziała). Możemy oczywiście usunąć aktualnego agenta i utworzyć nowego, ale takie rozwiązanie przy rozbudowanym agencie (posiadającym kilka typów obiektów, do tego filtry oraz kilkanaście przepływów) oznacza żmudną i podatną na błędy pracę. Jaki jest więc sposób na wprowadzenie zmian w Capabilities dla istniejącego agenta? Ja stosuję następującą metodę:

  1. Modyfikujemy właściwość Capabilities w klasie agenta, kompilujemy i podmieniamy bibliotekę
  2. Dodajemy do Synchronization Service nowego agenta korzystającego z naszej biblioteki (pod dowolną nazwą, bez konfigurowania)
  3. Eksportujemy oryginalnego i nowego agenta do plików xml
  4. Usuwamy obydwu agentów z Synchronization Service
  5. Podmieniamy wartości elementów <capabilities-mask> i <capability-bits> w pliku xml oryginalnego agenta na wartości z pliku nowego agenta
  6. Importujemy oryginalnego agenta

W elementach <capabilities-mask> i <capability-bits> znajdują się ustawienia Capabilities zapisane podczas tworzenia agenta. Po zmianie ich na nowe wartości i imporcie nasz dotychczasowy agent będzie działał zgodnie z oczekiwaniami.

Polecenie TOP z argumentem WITH TIES

2014-09-15

W dzisiejszym wpisie pokażę dosyć mało znaną konstrukcję TOP…WITH TIES. Muszę przyznać, że sam trafiłem na nią całkiem niedawno mimo, iż była ona dostępna już w SQL Server 2000. Załóżmy, że mamy pewien zbiór danych:

ties1

Teraz chcemy wybrać trzy firmy o największej sprzedaży. Company C i Company D mają taką samą wartość więc oczekujemy, że obie firmy znajdą się w wynikach zapytania. Niestety standardowe polecenie TOP 3 zwróci nam dokładnie trzy rekordy, pomijając wyniki ex-aequo.

Do tej pory w celu rozwiązania powyższego problemu stosowałem funkcję RANK(). Najpierw tworzyłem zapytanie z dodatkową kolumną rankingową:

select
  Company,
  TotalSales,
  rank() over(order by TotalSales desc) r
from Sales

Dzięki czemu otrzymywałem taki oto zbiór:

ties2

Następnie tworzyłem ostateczne zapytanie:

select
  Company,
  TotalSales
from
(
  select
    Company,
    TotalSales,
    rank() over(order by TotalSales desc) r
  from Sales
) Sales
where r <= 3

Otrzymując oczekiwane dane:

ties3

Okazuje się jednak, że dzięki poleceniu TOP z argumentem WITH TIES otrzymamy dokładnie ten sam wynik:

select top 3 with ties
  Company,
  TotalSales
from Sales
order by TotalSales desc

Dodanie do polecenia TOP klauzuli WITH TIES powoduje dołączenie do wynikowego zbioru danych wszystkich rekordów posiadających takie same wartości w kolumnach ORDER BY jak ostatni zwrócony rekord.

Najbliższe wydarzenia

2014-09-03

Nadchodząca jesień zapowiada się niezwykle ciekawie pod względem wydarzeń w świecie dev. Oto lista zbliżających się konferencji poświęconych technologiom .NET/SQL Server:

Ja zamierzam wybrać się na .NET DeveloperDays i SQLDay. Do zobaczenia 🙂

Użycie własnego atrybutu w Function Evaluator

2014-08-30

Jeżeli w FIM utworzymy nowy typ obiektu z własnymi atrybutami okazuje się, że atrybuty te nie są widoczne w konfiguracji activity Function Evaluator:

FunctionEvaluator1

FunctionEvaluator2

Na szczęście mimo iż nie można ich wybrać bezpośrednio z kontrolek, istnieje sposób odwołania się do nich. Wystarczy użyć wyrażenia [//Target/CustomAttribute] (oczywiście podając właściwą nazwę atrybutu). W przypadku atrybutu docelowego wystarczy wpisać powyższe wyrażenie do destination, z kolei aby odwołać się do wartości takiego atrybutu musimy użyć CustomExpression:

FunctionEvaluator3

Opisany problem nie dotyczy własnych atrybutów bindowanych do standardowych typów obiektów. W takim przypadku są one widoczne w kontrolkach razem z innymi atrybutami.

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.

FIM Portal – dopuszczalne wartości atrybutu i Constant Specifier

2014-06-05

W dzisiejszym wpisie przedstawię pewien problem związany z walidacją wartości atrybutów w portalu FIM. Załóżmy, że dany obiekt posiada atrybut tekstowy, w którym mogą znaleźć się jedynie wartości z określonego zbioru. Chcemy aby użytkownik podczas tworzenia obiektu miał do wyboru dwie wartości atrybutu: MyValue-1 oraz MyValue-2. Standardowo zaczynamy od ustawienia walidacji bindingu na odpowiednie wyrażenie regularne:

fim1

Następnie w widoku tworzenia obiektu definiujemy kontrolkę dla naszego atrybutu. Będzie to DropDownList z ItemSource ustawionym na właściwość LocalizedAllowedValues atrybutu:

<my:Control my:Name="MailboxType" my:TypeName="UocDropDownList"
	my:Caption="{Binding Source=schema, Path=MailboxType.DisplayName}"
	my:Description="{Binding Source=schema, Path=MailboxType.Description}"
	my:RightsLevel="{Binding Source=rights, Path=MailboxType}">
	<my:Properties>
		<my:Property my:Name="Required" my:Value="{Binding Source=schema, Path=MailboxType.Required}" />
		<my:Property my:Name="ValuePath" my:Value="Value"/>
		<my:Property my:Name="CaptionPath" my:Value="Caption"/>
		<my:Property my:Name="HintPath" my:Value="Hint"/>
		<my:Property my:Name="ItemSource" my:Value="{Binding Source=schema, Path=MailboxType.LocalizedAllowedValues}"/>
		<my:Property my:Name="SelectedValue" my:Value="{Binding Source=object, Path=MailboxType, Mode=TwoWay}"/>
	</my:Properties>
</my:Control>

Teoretycznie wszystko zrobiliśmy poprawnie ale podczas tworzenia nowego obiektu okazuje się, że kontrolka DropDownList jest pusta. Oczekiwaliśmy, że będzie ona zwierała listę dopuszczalnych wartości dla interesującego nas atrybutu:

fim2

Przyczyną takiego zachowania jest użycie w wyrażeniu regularnym znaku „-„. Przy definiowaniu walidacji nie możemy używać znaków specjalnych takich jak  „-„, „.”, „?”, „$”, „|”, itp. No dobrze, ale co zrobić, jeżeli mimo wszystko chcemy aby w kontrolce wyświetlane były wartości z myślnikami? Rozwiązaniem jest użycie obiektów Constant Specifier. Wchodzimy do All Resources, wybieramy typ Constant Specifier i tworzymy nowe obiekty dla każdej z dopuszczalnych wartości. Podczas tworzenia obiektu Constant Specifier podajemy wartość jaka będzie wyświetlana w kontrolce, wartość klucza bez znaków specjalnych oraz wskazujemy atrybut i typ obiektu:

fim3

Teraz pozostało jedynie zmodyfikowanie wyrażenia regularnego w walidacji tak, aby zawierało wartości kluczy zamiast wartości wyświetlanych:

fim4

Po tej zmianie (i wykonaniu IISRESET) wszystko działa tak jak powinno, podczas tworzenia obiektu możemy wybrać jedną z dopuszczalnych wartości:

fim5

Powyżej pokazałem tylko jedno z zastosowań obiektów Constant Specifier. Generalnie pozwalają one na rozdzielenie dopuszczalnych wartości atrybutu na wartość fizycznie zapisywaną w obiekcie (Constant Value Key) oraz wartość wyświetlaną w UI (Display Name). Takie podejście jest dobrą praktyką z kilku powodów. Po pierwsze wartości atrybutów przechowywane w bazie danych mogą być niezrozumiałe dla użytkownika. Po drugie operowanie w kodzie na wartościach wyświetlanych powoduje konieczność jego modyfikacji przy każdej zmianie tych wartości. Ostatnią zaletą jest możliwość zdefiniowania wyświetlanych wartości dla różnych języków. Wystarczy podczas tworzenia obiektu Constant Specifier przejść na zakładkę Localization i dla poszczególnych języków (Supported Languages) podać wartość, jaka będzie wyświetlana w kontrolkach (Localized Display name).

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.