public object CloneObject(object o) { Type t = o.GetType(); PropertyInfo[] properties = t.GetProperties(); Object p = t.InvokeMember("", System.Reflection.BindingFlags.CreateInstance, null, o, null); foreach (PropertyInfo pi in properties) { if (pi.CanWrite) { pi.SetValue(p, pi.GetValue(o, null), null); } } return p; }
środa, 21 maja 2014
Klonowanie obiektu - deep cloning
Post ku pamięci. Jak szybko i łatwo wykonać klonowanie całego obiektu (tzw. deep cloning). Możemy użyć takiej funkcji:
Autor:
Piotr Ptak
o
23:13
0
komentarze
Wyślij pocztą e-mailWrzuć na blogaUdostępnij w XUdostępnij w usłudze FacebookUdostępnij w serwisie Pinterest
Etykiety:
C#
poniedziałek, 19 maja 2014
Prosty klient do konsumowania serwisu ASP.NET Web Api
Załóżmy, że napisaliśmy serwis, który zwraca nam pewne produkty. Nawigując pod odpowiednie dwa adresy URI pobierzemy odpowiednie zasoby:
- http://localhost/api/products - lista wszystkich produktów
- http://localhost/api/product/1 - odpowiedni produkt
public class Product { public int Id { get; set; } public string Name { get; set; } public string Description { get; set; } public decimal Price { get; set; } }W celu połączenia się z naszym Web API, musimy skorzystać z klasy HttpClient. Nasz cały kod może wyglądać następująco:
Metoda, dzięki której tworzymy sobie połączenie do naszego serwisu REST:
private HttpClient GetClient(string mediaType) { string baseUri = @baseAddress + port; HttpClientHandler handler = new HttpClientHandler() { UseDefaultCredentials = false }; handler.Credentials = new NetworkCredential("username", "password"); HttpClient httpClient = new HttpClient(handler); httpClient.BaseAddress = new Uri(baseUri); httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(mediaType)); return httpClient; }Metoda dzięki której pobieramy produkt:
private async void Get() { using (var client = GetClient("application/json")) { HttpResponseMessage response = await client.GetAsync("/api/Products/1"); if (response.IsSuccessStatusCode) { Product product = await response.Content.ReadAsAsyncMetoda za pomocą której dodajemy nasz produkt:(); //Do something with product } } }
private async void Post() { using(var client = GetClient("application/json")) { var message = await client.PosAsJsonAsyncMetoda za pomocą której usuwamy nasz produkt:("api/products", new Product { Id=111, Name="Product 111 name", Description = "Product 111 description", Price=222,22 }); } }
private void Delete() { using(var client = GetClient("application/json")) { var message = await Client.DeleteAsync("api/products/1"); } }Należy pamiętać, że metoda "ReadAsAsync
Autor:
Piotr Ptak
o
00:56
1 komentarze
Wyślij pocztą e-mailWrzuć na blogaUdostępnij w XUdostępnij w usłudze FacebookUdostępnij w serwisie Pinterest
Etykiety:
Web API
ASP.NET Web API - basic authentication
Basic authentication (ASP.NET Web API)
Tworzenie serwisu REST z wykorzystaniem ASP.NET Web API jest stosunkowo proste. Gdy nasz serwis jest już wystawiony na świat, prawdopodobnym stanie się fakt odpowiedniego jego zabezpieczenia. Jednym z najprostszych i najbardziej standardowym sposobem jest zabezpieczenie takiego serwisu z wykorzystaniem basic authentication.
Basic authentication jest najprostszym i najłatwiejszym mechanizmem autentykacji poprzez zastosowanie pary "username/password" w "plaintext".
Basic authentication działa w następujący sposób:
- Jeżeli żądanie "request" wymaga autentykacji, serwer zwraca status code "401" (unauthorized). Odpowiedź taka zawiere "WWW-uthenticate header", które mówi, że serwer wspiera "basic authentication".
- Klient wysyła kolejne żądanie ("request") ale tym razem już z odpowiednimi "credentials" umieszczonymi w "WWW-Authenticate header". Charakteryzują je następujące właściwości:
- sformatowane jako string: "name:password"
- base64-encoded
- brak szyfrowania
Basic authentication wykonywany jest w kontekście pewnej domeny ("realm"). Serwer dodaje nazwę tej domeny do nagłówka WWW-Authenticate header.
WAŻNE:
- Ponieważ "credentials", które przesyłane są od klienta na serwer nie są szyfrowane, stosowanie basic authentication jest sensowne i bezpieczne jedynie jeśli komunikacja jest bezpieczna i przebiega przez szyfrowaną wersję protokołu HTTP czyli HTTPS.
- Basic authentication nie jest odporne na ataki CSRF (cross-site request forgery)
Niniejszy post przedstawia jak zaimplementować taką autentykację w naszym ASP.NET Web API (post nie dotyczy ASP.NET Web API 2, które jest prostsze i zawiera już zaimplementowane techniki autentykacji serwisu bez konieczności pisania dodatkowego kodu).
AuthorizeAttribute
Implementacja basic authentication będzie polegała na stworzeniu własnego atrybutu do autoryzacji. W pierwszej kolejności jednak musimy napisać metodę pomocniczą, która wyłuska nam credentials (nazwę użytkownika i hasło) z wiadomości, która przychodzi do serwera. Metoda taka może wyglądać następująco:private Credentials ParseAuthorizationHeader(string authHeader) { string[] credentials = Encoding.ASCII.GetString(Convert.FromBase64String(authHeader)).Split(new[] { ':' }); if (credentials.Length != 2 || string.IsNullOrEmpty(credentials[0]) || string.IsNullOrEmpty(credentials[1])) return null; return new Credentials() { Username = credentials[0], Password = credentials[1], }; }Teraz wystarczy napisać naszą klasę, która będzie dziedziczyła po "AuthorizeAttribute":
public class CustomAuthorizeAttribute : AuthorizeAttribute { private const string BasicAuthResponseHeader = "WWW-Authenticate"; private const string BasicAuthResponseHeaderValue = "Basic"; protected IPrincipal CurrentUser { get { return Thread.CurrentPrincipal as IPrincipal; } set { Thread.CurrentPrincipal = value as IPrincipal; } } public override void OnAuthorization(System.Web.Http.Controllers.HttpActionContext actionContext) { try { AuthenticationHeaderValue authValue = actionContext.Request.Headers.Authorization; Credentials parsedCredentials = ParseAuthorizationHeader(authValue.Parameter); if (parsedCredentials != null) { this.CurrentUser = new DummyPrincipalProvider().CreatePrincipal(parsedCredentials.Username, parsedCredentials.Password); if (!CurrentUser.Identity.IsAuthenticated) { actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Forbidden); actionContext.Response.Headers.Add(BasicAuthResponseHeader, BasicAuthResponseHeaderValue); return; } } } catch (Exception ex) { actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized); actionContext.Response.Headers.Add(BasicAuthResponseHeader, BasicAuthResponseHeaderValue); return; } } }W powyższej implementacji korzystam z klasy "DummyPrincipalProvider", która jest dziedziczy z IPrincipalProvider i jest moją "fake'ową" implemetacją dostarczyciela tożsamości. Wygląda następująco:
public class DummyPrincipalProvider : IProvidePrincipal { private const string Username = "username"; private const string Password = "password"; public IPrincipal CreatePrincipal(string username, string password) { if (username != Username || password != Password) { return null; } var identity = new GenericIdentity(Username); IPrincipal principal = new GenericPrincipal(identity, new[] { "User" }); return principal; } }W tym miejscu jednak powinna być implemetacja odpowiedniego dortarczyciela tożsamości, np. jeśli dane użytkowników przechowywane są bazie to wtedy najlepszym rozwiązaniem będzie użycie gołego ADO.NET lub z wykorzystaniem EntityFramework do pobrania takich danych z bazy danych. Mając tak napisany atrybut, wystarczy teraz, że klasę lub metodę, którą chcemy zabezpieczyć udekorujemy tym atrybutem:
[CustomAuthorizeAttribute] [HttpGet, HttpHead] public Product Get(int id) { var product = productRepository.Get(id); if (product == null) throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.NotFound)); return product; }
Autor:
Piotr Ptak
o
00:38
0
komentarze
Wyślij pocztą e-mailWrzuć na blogaUdostępnij w XUdostępnij w usłudze FacebookUdostępnij w serwisie Pinterest
Etykiety:
Web API
sobota, 17 maja 2014
Javascript basics - Part 1
Wstęp
Na pewno każdy developer miał styczność z jQuery w mniejszym lub większym stopniu. Tak jak ja, zrobiłem kilka stron www, dla żony, dla znajomych, w których wykorzystywałem jQuery. Jednak tak na prawdę nigdy nie zagłębiałem się w javascript. jQuery jest wspaniałą biblioteką, jednak przyszedł czas aby poznać podstawy javascript. W związku z tym, wziąłem się za zgłębianie tajników tego języka czytając książkę Javascript Patterns. Książkę polecam każdemu, jest ona wspaniałym źródłem wiedzy dla kogoś takiego jak ja, który wcześniej właściwie nie miał styczności z czystym javascript (oczywiście pewne podstawy znałem, których musiałem się nauczyć aby korzystać z jQuery). Seria tych postów zawiera fundamentalne podstawy javascript, które wg. mnie każdy szanujący się developer powinien znać.
Deklarowanie zmiennych
W javascript, jak można przypuszczać mamy zmienne globalne i lokalne. Jednak trzeba wiedzieć jak poprawnie je deklarować, ponieważ może okazać się, że zmienna, którą chcieliśmy zadeklarować lokalnie okaże się być zmienną globalną (tak tak, też wydawało mi się to nieprawdopodobne).
DEKLAROWANIE ZMIENNYCH LOKALNYCH
Zmienna jest zmienną lokalną gdy jest poprawnie zadeklarowana w ciele funkcji:
function foo(){ var localVariable = "this is a localVariable"; }zmienna localVariable w powyższym przykładzie jest zadeklarowana jako zmienna lokalna. Aby poprawnie tworzyć zmienne lokalne musimy zapamiętać następujące 2 zasady:
- musi być zadeklarowana w ciele funkcji
- musi być zadeklarowana ze słowem kluczowym var
Są to dwie podstawowe zasady, które musimy zapamiętać. Nic więcej! Sprawa przedstawia się trochę inaczej jeśli chodzi o zmienne globalne!
DEKLAROWANIE ZMIENNYCH GLOBALNYCH
Podstawowa zasada jest prosta, wszystko co jest deklarowane poza funkcją jest zmienna globalną! Jednakże, jest jeszcze inna zasada. Spójrzmy na poniższy przykład:
var globalVariable = "This is global variable"; secondGlobalVariable = "This is second global variable"; function foo(){ anotherGlobalVariable = "This is another global variable"; }
Wszystko co jest zadeklarowane bez słowa kluczowego var jest zmienną globalną, nawet jeśli zadeklarowane w ciele funkcji! W związku z tym, jakie są przesłanki przy tworzeniu zmiennych globalnych?
- każda zmienna zadeklarowana ze słowem kluczowym var poza funkcją jest zmienną globalną
- każda zmienna zadeklarowana bez słowa kluczowego var jest również zmienna globalną (nawet jeśli jest zadeklarowana w ciele funkcji)
WAŻNE
Przy deklarowaniu zmiennych lokalnych należy pamiętać, że każda zmienna lokalna musi być zadeklarowana ze słowem kluczowych var. Dlaczego jest to takie ważne? Rozważmy następujący przykład:
function foo(){ var variable1 = variable2 = "Some text"; }Zmienna variable1 jest zmienna lokalną, jednak zmienna variable2 jest już zmienną globalną! Jeśli chcemy aby obie były zmiennymi lokalnymi należy je zadeklarować następująco:
function foo(){ var variable1, variable2; variable1 = variable2 = "Some text"; }Tak zadeklarowane będą obie zmiennymi lokalnymi!
poniedziałek, 12 maja 2014
Bezpieczeństwo WCF - Message i Transport Level Security
Wprowadzenie
Bezpieczeństwo jest istotną częścią każdego kazdego oprogramowania a staje się wręcz niezbędną częścią jeśli chodzi o serwisu webowe - które wystawiane na świat i stają się widocznymi celami różnego rodzaju ataków.
WCF posiada dwa podstawowe tryby obsługi bezpieczeństwa:
- bezpieczeństwo
na poziomie transportu (Transport Level Security)
- bezpieczeństwo
na poziomie wiadomości (Message
Level Security)
(istnieje jeszcze jeden tryb który łączy dwa
powyższe - TransportWithMessageCredential)
Bezpieczeństwo wiadomości można zdefiniować za pomocą 3 aspektów, potocznie nazywanych CIA:
- Confidentiality - poufność - oznacza, ze osoba lub podmiot, który powinien widzieć wiadomość w rzeczywistości ta osoba/podmiotem jest
- Integrity - integralność - oznacza, ze wiadomość nie może być w żaden sposób zmieniona lub "zainfekowana" bez wykrycia tego faktu
- Authentication - uwierzytelnianie - potwierdza tożsamość osoby/podmiotu przeglądającego wiadomość
Brzmi dość tragicznie i zniechęcająco? Można to sobie porównać np. do korzystania z konta bankowego. Tylko osoba do której konto należy może przeglądać dane swojego konta i wykonywać w nim odpowiednie operacje (no chyba ze ktoś inny jeszcze zostanie do tego upoważniony). Wpłacając lub wypłacając pieniądze chcemy mieć pewność ze kwota wpłacona/wypłacona będzie miała swoje odzwierciedlenie na naszym koncie (no może nie zawsze zwłaszcza jeśli chodzi o wypłaty ;)). Dodatkowo oczekujemy od banku iż przed dostępem do naszego konta zostaniemy odpowiednio uwierzytelnieni. Logując się online musimy podać np. nr klienta oraz hasło, korzystając z infolinii tez musimy posiadać odpowiednie dane dostępowe. Jak widać wszystkie niuanse prawdziwego życia w tym kontekście mogą być odzwierciedlone w WCF.
Message Level Security
Message Level Security jest pierwszą podstawową kategorią w całej
strukturze bezpieczeństwa Windows Communication Foundation. Jest fizyczną implementacją WS-Security Specification, a jej głównym zadaniem jest rozszerzanie
wiadomości SOAP (Simple Object Access Protocol) w celu zapewnienia poufności, integralności i uwierzytelniania. Oznacza to z wszystkie detale związane z bezpieczeństwem wiadomości przechowywane są w niej samej. Ma to swoje wady i zalety:
Zalety
Wady
Zalety
- End-to-end security. Jak wcześniej zostało napisane, "message level security" rozszerza wiadomość SOAP dodając do niej odpowiednie informacje zabezpieczające. Można powiedzieć, ze w przeciwieństwie do "transport level security" (np. SSL które zabezpiecza wiadomość w przypadku komunikacji punkt-punkt, tzn. ze jeśli istnieją jakieś pośrednie urządzenia to ta informacja jest za każdym razem re-transmitowana), informacje zabezpieczające są z wiadomością przez cały cykl jej życia.
- Increased flexibility - zwiększona elastyczność. Niekoniecznie cała wiadomość lecz część wiadomości może być podpisana lub zaszyfrowana. Dzięki temu pewna część wiadomości może być widoczna dla serwisów pośredniczących a część zaszyfrowana/podpisana co zapewnia jej integralność. Domyślnie, w przypadku stosowanie "message level security", WCF nie szyfruje wiadomości lecz podpisuje ja.
- Support for multiple transports - wsparcie dla wielu typów transportu. Oznacza to, że "message level security" jest niezależne od typu transportu. Zabezpieczoną wiadomość możemy przesłać używając HTTP, TCP czy named pipes.
Za MSDN:
- Wydajność - z racji tego iż wiadomość jest rozszerzona, automatycznie staje się większa
- Nie można używać message stereaming
- Wymaga implementacji mechanizmów bezpieczeństwa na poziomie XML oraz wsparcia dla WS-Security Sepcification co może zmniejszyć interoperacyjność
Kiedy uzywac
Zalety
Wady
Kiedy używać
- w przypadku gdy wiadomość będzie krążyła pomiędzy różnymi serwisami
- w przypadku gdy serwis WCF będzie dostępny poprzez Internet i dostęp do niego będzie wymagał innych serwisów pośredniczących w komunikacji
Transport Message Security
Generalnie, w odróżnieniu od "Message Level Security" tutaj wszystkie dane dotyczące bezpieczeństwa wiadomości przekazywane są za pomocą warstwy transportującej. Oznacza to iż ten typ zabezpieczeń jest zależny od typu wybranego transportu co automatycznie zmniejsza jego możliwości jeśli chodzi o opcje uwierzytelniania. Kazdy z protokolow transportowych (TCP, IPC, MSMQ czy HTTP) implementuje własny mechanizm zabezpieczenia wiadomości. Najpopularniejszym podejściem jest wykorzystanie Secure Socket Layer (SSL) do szyfrowania bądź podpisywania zawartości pakietów wysyłanych przez HTTPS.Zalety
- zapewnia interoperacyjność ponieważ nie musi implementować WS-Security Specification
- większa wydajność
Wady
- zapewnia bezpieczeństwo punkt-punkt
- ograniczona liczba opcji uwierzytelniania w stosunku do message level security
- zależne od protokołu transportującego
Kiedy używać
- gdy wiadomość będzie wysyłana bezpośrednio między dwoma punktami bez angażowania jakichkolwiek pośredników, np. w przypadku bezpośredniej komunikacji klient-serwer
- gdy serwer i klient są w lokalnej sieci intranet
Jest dostępny we wszystkich typach bindingów z wyjątkiem wsDualHttpBinding
Show me the code
Jak właściwie wszystko w WCF, bezpieczeństwo na poziomie wiadomości można zdefiniować używajac kodu deklaratywnego lub imperatywnego. Obie implementacji są banale i wyglądają następująco:
Kod deklaratywny:
Kod deklaratywny:
Kod imperatywny (z wykorzystaniem domyslnego (bezparametrowego kontruktora)):
WSHttpBinding wsHttpSecurity = new WSHttpBinding(); wsHttpSecurity.Security.Mode = SecurityMode.Message;Kod imperatywny (okreslanie security level w konstruktorze):
WSHttpBinding wsHttpSecurity = new WSHttpBinding(SecurityMode.Message);
wtorek, 6 maja 2014
Kontrola wiadomości WCF - IDispatchMessageInspector
Jeśli korzystaliście kiedyś z WCF (lub pisaliście jakikolwiek inny serwis z wykorzystaniem SOAP), możliwe że mieliście zaimplementowany mechanizm kontroli wiadomości - np. mechanizm logowania wiadomości, które przychodzą do serwera. W tym poście chciałbym opisać jak zaimplementować taki mechanizm kontroli wiadomości przed jej przetworzeniem przez serwis WCF lub przed wysłaniem odpowiedzi do klienta. Dlaczego właściwie chcielibyśmy robić taką kontrolę? Tak wcześniej wspomniałem np. aby logować takie wiadomości na przykład w celach statystycznych lub w przypadku gdy chcemy coś szczególnego dodać do naszej wiadomości - odpowiedzi na przykład w nagłówku przed wysłaniem jej do aplikacji klienckiej.
W WCF'ie możemy taką funkcjonalność zaimplementować w "dość prosty" sposób. (Prosty dla kogoś, kto już miał wcześniej styczność z WCF'em).
Na początku musimy stworzyć klasę, w której będziemy robić inspekcję wiadomości WCF - to własnie tak klasa będzie implementowała interfejs IDispatchMessageInspector. Interfejs ten posiada dwie metody:
Dodatkowo, z racji tego iż nasza klasa będzie określała pewne zachowanie dla serwisu lub endpointa musi również implementować odpowiednio:
IServiceBehavior lub IEndpointBehavior lub oba interfejsy jeśli zdecydujemy się, że może być używana zamiennie dla serwisu i endpointa.
Kod naszej klasy wygląda następująco:
W WCF'ie możemy taką funkcjonalność zaimplementować w "dość prosty" sposób. (Prosty dla kogoś, kto już miał wcześniej styczność z WCF'em).
Na początku musimy stworzyć klasę, w której będziemy robić inspekcję wiadomości WCF - to własnie tak klasa będzie implementowała interfejs IDispatchMessageInspector. Interfejs ten posiada dwie metody:
- AfterReceiveRequest - wykonywana w momencie gdy wiadomość została odebrana przez serwis lecz przed przekazaniem jej do odpowiedniej metody serwisu
- BeforeSendReply - wywoływana po przetworzeniu naszej wiadomości przez odpowiednią metodę i przed wysłaniem odpowiedzi do aplikacji klienckiej
Dodatkowo, z racji tego iż nasza klasa będzie określała pewne zachowanie dla serwisu lub endpointa musi również implementować odpowiednio:
IServiceBehavior lub IEndpointBehavior lub oba interfejsy jeśli zdecydujemy się, że może być używana zamiennie dla serwisu i endpointa.
Kod naszej klasy wygląda następująco:
public class ServerInspector : IDispatchMessageInspector, IEndpointBehavior, IServiceBehavior { #region IDispatchMessageInspector Members public object AfterReceiveRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel, System.ServiceModel.InstanceContext instanceContext) { Debug.WriteLine("Called after an inbound message has been received but before the message is forwared to the target operation"); return request; } public void BeforeSendReply(ref System.ServiceModel.Channels.Message reply, object correlationState) { Debug.WriteLine("Called after the operation has returned but before the reply is actually relayed"); } #endregion #region IEndpointBehavior Members public void AddBindingParameters(ServiceEndpoint endpoint, System.ServiceModel.Channels.BindingParameterCollection bindingParameters) { } public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime) { } public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) { endpointDispatcher.DispatchRuntime.MessageInspectors.Add(this); } public void Validate(ServiceEndpoint endpoint) { } #endregion #region IServiceBehavior Members public void AddBindingParameters(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase, System.Collections.ObjectModel.Collectionendpoints, System.ServiceModel.Channels.BindingParameterCollection bindingParameters) { } public void ApplyDispatchBehavior(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase) { foreach (ChannelDispatcher dispatcher in serviceHostBase.ChannelDispatchers) { foreach (var endpoint in dispatcher.Endpoints) { endpoint.DispatchRuntime.MessageInspectors.Add(this); } } } public void Validate(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase) { } #endregion }
Należy zwrócić uwagę na metody ApplyDispatchBehavior w których dodajemy do naszego endpointa aktualnego inspektora. Bez dodania inspektora, nasz kod nie będzie działał. Jeśli chodzi o samą kontrolę wiadomości to może być ona wykonywana w 2 metodach o których wcześniej wspomniałem : AfterReceiveRequest oraz BeforeSendReply. Ja akurat w tych metodach dodałem zwykłe wyświetlanie wiadomości, jednak należy pamiętać, że własnie te metody przeznaczone są do przeprowadzania właściwej kontroli wiadomości WCF.
Kolejnym krokiem jest dodanie "klasy rozszerzającej" która będzie dziedziczyła z BehaviorExtensionElement. Musimy mieć taką klasę ponieważ jak zobaczymy z chwilę, w pliku konfiguracyjnym ustawimy sobie takie własnie rozszerzenie zachowania naszego serwisu. Najważniejszą sprawą jaką trzeba zapamiętać, że taka klasa musi dziedziczyć z klasy BehaviorExtensionElement, w przeciwnym wypadku kompilator nam zaprotestuje i wywali błąd. W związku z tym nasza klasa rozszerzająca wygląda następująco:
public class ServerInspectorExtension : BehaviorExtensionElement { public override Type BehaviorType { get { return typeof(ServerInspector); } } protected override object CreateBehavior() { return new ServerInspector(); } }Jak widzimy klasa ta tworzy nową instancję naszego inspektora, którego dodaliśmy wcześniej. Ostatnim krokiem jest skonfigurowanie "behaviorExtension" w pliku konfiguracyjnym. Nasze rozszerzenie wygląda następująco:
Takie rozszerzenie przypisujemy do odpowiedniego "behavior'a":
Autor:
Piotr Ptak
o
06:43
0
komentarze
Wyślij pocztą e-mailWrzuć na blogaUdostępnij w XUdostępnij w usłudze FacebookUdostępnij w serwisie Pinterest
Etykiety:
WCF
Subskrybuj:
Posty (Atom)