Vítejte na blog.vyvojar.cz Přihlásit | Registrovat | Pomoc

 

I’ve developed simple CLR Trigger which uses WCF to send some data. So when I try to deploy CLR Trigger I got exception

Deploy error SQL01268: .Net SqlClient Data Provider: Msg 6503, Level 16, State 12, Line 1 Assembly 'system.servicemodel, version=3.0.0.0, culture=neutral, publickeytoken=b77a5c561934e089.' was not found in the SQL catalog.
  An error occurred while the batch was being executed.

Ok, let’s go to add System.ServiceModel assembly to sql server.

image

But if I try add System.ServiceModel assembly I got exception

image

Ok, I just try add smdiagnostics…done. So I just try add system.servicemodel again….ohh no another exception

image

Adding system.runtime.serialization….done. Adding system.servicemodel again … exception

image

Adding system.web…fail, because system.web requires system.drawing.dll

image

Adding system.drawing … done. Adding system.web again … fail and exception

image

Adding system.directoryservices…done. Adding system.web again…exception

image

Adding system.directoryservices.protocols…done. Adding system.web again !!!!!…exception

image

Adding system.enterpriseservices…exception

image

Adding system.runtime.remoting … exception

image

Summary

I can’t add System.Web assembly because it requires System.EnterpriseServices assembly which requires System.Runtime.Remoting assembly which requires System.Web assembly again !!!!! I have absolutely no idea who designed this ugly dependency.

V mém posledním velmi krátkém příspěvku jsem zde dělal reklamu na web www.aglight.cz . Na tomto webu se sice stále pracuje, ale zatím velmi pomalu a proto nevidíte jednu funkcionalitu, která sice implementována je, ale jak říkám není “vidět”. Jedná se o podporu RSS kanálů!

Více na www.aglight.cz

Před pár dny mi začal blbnout windows průzkumník. Jakmile jsem chtěl vytvořit novou složku, začalo se mi točit “kolečko” a u názvu složky se objevilo známé “not responding”. Bohužel jsem se vytvoření nikdy nedočkal a tak jsem musel ukončit proces. Jakmile jsem se ovšem podíval zpátky do míst, kde jsem vytvářel novou složku tak opravdu “new folder” byla zde. Druhým chováním, kdy nedocházelo k aktualizaci složky či plochy bylo v případě kopírování, mazání a přesouvání. Vždy po provedení těchto operací jsem musel stisknout F5 aby se provedla aktualizace.

Po prostudování tohoto vlákna jsem objevil řešení, které pro mně zafungovalo. Tady je:

Hi all,
I've read this thread to the end and I've tried all the solutions posted... obviously to no avail.
The bug repeats itself, not randomly but constantly, in any situation, folders, libraries - sorted by name or by whatever.
I then tried something different, which instead WORKED , and here it is.

  1. I backed up the register.
  2. Searched for occurrences of the word refresh and found this key: HKEY_CLASSES_ROOT\CLSID\{BDEADE7F-C265-11D0-BCED-00A0C90AB50F}\Instance which contains three subkeys. One of them, curiously enough, is DontRefresh and it has a value of 1 (turned ON)
  3. I then changed the value to 0 (turned OFF) and hit F5 to refresh and save the now changed register.


Guess what? Now, everything (moving, deleting, copying) works and even the Trash Bin refreshes immediately.
I will keep you posted in the next few days to tell you whether the fix still works.
Good luck.

Routování při použití WebHttp Services může být velice nápomocné při snaze rozdělit funkcionalitu službu na menší části, kde každá část bude mít svojí speciální adresu. Jak provést rozdělení a příslušné napamování si ukážeme na následující ukázce.

Máme vytvořenou službu, která nabízí pro jednoduchost pouze dvě metody

[ServiceContract]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Required)]
public class EshopService
{
[WebGet(UriTemplate = "/Products?format={format}")]
[Description("Vrati seznam vsech produktu")]
public ProductCollection FindAll(string format)
{
if (string.Equals("json", format, StringComparison.OrdinalIgnoreCase))
WebOperationContext.Current.OutgoingResponse.Format = WebMessageFormat.Json;

return FakeDatabase.Products;
}

[WebGet(UriTemplate = "/Product/{id}")]
[Description("Najde produkt podle zadaneho id")]
public Product Find(string id)
{
int parsedId = 0;
if (int.TryParse(id, out parsedId))
return FakeDatabase.Products.Where(p => p.ID == parsedId).FirstOrDefault();

return null;
}
}

Pro zobrazení všech produktů zadáme do prohlížeče adresu http://localhost:8090/Eshop/Products. Jakmile budeme chtít ovšem zobrazovat např. objednávky, uživatele, kategorie, apod. stane se naše třída EshopService velice nepřehlednou a tudíž složitější na údržbu. Vytvořme si tedy novou třídu nazvanou OrderService, která bude sloužit pro práci s objednávkami. Stávající třídu EshopService přejmenujeme na ProductService.

Ještě před vytvořením nové třídy by jsme si mohly vytvořit rozhraní, ve kterém budeme specifikovat příslušné metody. Rozhraní si můžeme pojmenovat IEntity a vypadá následovně:

public interface IEntity<out TColl, out TEntity> where TColl : Collection<TEntity>
{
/// <summary>
/// Vrati seznam vsech polozek
/// </summary>
/// <param name="format">format (json, xml)</param>
/// <returns></returns>
TColl FindAll(string format);

/// <summary>
/// Podle id nalezne prislusnou polozku
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
TEntity Find(string id);
}

A nyní již implementace zmiňované třídy OrderService. Všimněme si, že oproti původní EshopService je upravená adresa (UriTemplate) atributu WebGet. Jelikož namapujeme adresu Orders na třídu OrderService, není potřeba aby byla adresa operace ve tvaru /Orders/id ale stačí pouze id či ?format=format…

[ServiceContract]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Required)]
public class OrderService : IEntity<OrderCollection, Order>
{
#region Implementation of IEntity<out OrderCollection,out Order>

[WebGet(UriTemplate = "?format={format}")]
[Description("Vrati seznam vsech objednavek")]
public OrderCollection FindAll(string format)
{
if (string.Equals("json", format, StringComparison.OrdinalIgnoreCase))
WebOperationContext.Current.OutgoingResponse.Format = WebMessageFormat.Json;

return FakeDatabase.Orders;
}

[WebGet(UriTemplate = "{id}")]
[Description("Najde objednavku podle zadaneho id")]
public Order Find(string id)
{
int parsedId = 0;
if (int.TryParse(id, out parsedId))
return FakeDatabase.Orders.Where(p => p.ID == parsedId).FirstOrDefault();

return null;
}

#endregion
}

a přejmenování EshopService na ProductService a úprava UriTemplate (viz. OrderService)

[ServiceContract]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Required)]
public class ProductService : IEntity<ProductCollection, Product>
{
[WebGet(UriTemplate = "?format={format}")]
[Description("Vrati seznam vsech produktu")]
public ProductCollection FindAll(string format)
{
if (string.Equals("json", format, StringComparison.OrdinalIgnoreCase))
WebOperationContext.Current.OutgoingResponse.Format = WebMessageFormat.Json;

return FakeDatabase.Products;
}

[WebGet(UriTemplate = "{id}")]
[Description("Najde produkt podle zadaneho id")]
public Product Find(string id)
{
int parsedId = 0;
if (int.TryParse(id, out parsedId))
return FakeDatabase.Products.Where(p => p.ID == parsedId).FirstOrDefault();

return null;
}
}

Když jsme si vytvořily takto dvě třídy byly by ideální aby se po zadání určitých dotazů provedly příslušné akce:

Původní EshopService byla tedy rozdělena na ProductService a OrderService a v posledním kroku můžeme provést namapování, tak aby naše směrování bylo úspěšné. Otevřeme si soubor Global.asax, kde v metodě RegisterRoutes provedeme příslušné namapování

private void RegisterRoutes()
{
var factory = new WebServiceHostFactory();

// Edit the base address of Service1 by replacing the "Service1" string below
RouteTable.Routes.Add(new ServiceRoute("Products", factory, typeof(ProductService)));
RouteTable.Routes.Add(new ServiceRoute("Orders", factory, typeof(OrderService)));
}

Závěr

Jak můžeme vidět, implementace routování byla v naší aplikaci na dva řádky, takže nic moc složitého. V dalších dílech si ukážeme jak pracovat s jinými formáty než jenom xml či json, jak cachovat data, apod.

Posted 14. března 2010 2:23 by lukaashek | 0 Comments
Vedeno pod:

Když jsme vytvářely WCF služby komunikující klasicky pomocí SOAP zpráv, tak jsem využívaly FaultException pro odeslání chybové zprávy zpět klientovi. WebHttp Services nabízí podobnou funkcionalitu, akorát se používá třída WebFaultException, která dědí právě z FaultException. WebFaultException nabízí dvě varianty, klasickou a generickou.

Když se podíváme na sluřbu EshopService, tak vidíme, že jsme si minule vytvořily metodu pro aktualizaci ceny nazvanou UpdateProductPrice. Právě na této metodě si ukážeme první použití zpracování výjimek. Metoda přijímá dva parametry: id a price, kde oba tyto parametry musí být čísla. Přidáme tedy první podmínku, pokud se nepovede přetypovat řetězec na číslo, ukončíme metodu vyvoláním příslušné výjimky. Při vytváření WebFaultException nám stačí když uvedeme text chybové zprávy a příslušný HttpStatusCode (existuje celá řada těchto statusů např. NotFound, BadRequest, ServiceUnavailable, apod.). Upravená metoda tedy vypadá následovně (popis v komentářích)

[Description("Aktualizuje cenu u produktu s danym id")]
[WebInvoke(UriTemplate = "/UpdateProductPrice/{id}?price={price}", Method = "PUT")]
public void UpdateProductPrice(string id, string price)
{
int parsedId;
decimal parsedPrice;

// nepodari-li se prevest id na cislo vyhod vyjimky
if (!int.TryParse(id, out parsedId))
{
throw new WebFaultException<string>("parameter id must be a number", System.Net.HttpStatusCode.BadRequest);
}

// nepodari-li se prevest price na cislo vyhod vyjimku
if (!decimal.TryParse(price, out parsedPrice))
{
throw new WebFaultException<string>("parameter price must be a number", System.Net.HttpStatusCode.BadRequest);
}
// najdu produkt
Product p = (from c in FakeDatabase.Products
where c.ID == parsedId
select c).FirstOrDefault();

// pokud neexistuje, vyhod vyjimku
if (p == null)
throw new WebFaultException<string>(string.Format("Product with id={0} does not exist", parsedId),
System.Net.HttpStatusCode.NotFound);
else
p.Price = parsedPrice;

}

A na straně klienta pouze upravíme již existující metodu UpdateProductPrice_1, kde jsme přidaly 3 parametry: id, price a format aby jsme si ukázaly jak vypadají výjimky odeslané buď pomocí XML či JSON. Kromě přidání parametrů si ještě vypíšeme StatusCode.

private static void UpdateProductPrice_1(string id, string price, string format)
{
Console.WriteLine("ID: " + id);
Console.WriteLine("Price: " + price);
Console.WriteLine("Format: " + format);
Console.WriteLine("\n\n");

using (HttpClient client = new HttpClient(URI))
{
// budu aktualizovat produkt s id = 1
string strUri = "UpdateProductPrice/" + id + "?price=" + price;

// pridame hlavicku s informaci o formatu odpovedi
using (HttpRequestMessage request = new HttpRequestMessage("PUT", strUri))
{
request.Headers.Accept.AddString(format);
request.Content = HttpContent.CreateEmpty();

// odeslu pozadavek
HttpResponseMessage response = client.Send(request);

// zobrazim status a vysledek
Console.WriteLine("Status: " + response.StatusCode);
Console.WriteLine("Response: " + response.Content.ReadAsString());
Console.WriteLine("\n\n");
}
}
}

Pro otestování zavoláme metodu UpdateProductPrice_1 hned několikrát:

// bad request: id
UpdateProductPrice_1("str", null, "application/json");
UpdateProductPrice_1("str", null, "application/xml");
// bad request: price
UpdateProductPrice_1("-1",null, "application/json");
UpdateProductPrice_1("-1", null, "application/xml");
// not found
UpdateProductPrice_1("0", "234", "application/json");
UpdateProductPrice_1("0", "234", "application/xml");
// ok
UpdateProductPrice_1("1", "234", "application/json");
UpdateProductPrice_1("1", "234", "application/xml");

A výsledek bude vypadat následovně:

============================================================
ID: str
Price: NULL
Format: application/json


Status: BadRequest
Response: "parameter id must be a number"


============================================================
ID: str
Price: NULL
Format: application/xml


Status: BadRequest
Response: <;string xmlns="http://schemas.microsoft.com/2003/10/Serialization/">parameter id must be a number</string>


============================================================
ID: -1
Price: NULL
Format: application/json


Status: BadRequest
Response: "parameter price must be a number"


============================================================
ID: -1
Price: NULL
Format: application/xml


Status: BadRequest
Response: <;string xmlns="http://schemas.microsoft.com/2003/10/Serialization/">parameter price must be a number</string>


============================================================
ID: 0
Price: 234
Format: application/json


Status: NotFound
Response: "Product with id=0 does not exist"


============================================================
ID: 0
Price: 234
Format: application/xml


Status: NotFound
Response: <;string xmlns="http://schemas.microsoft.com/2003/10/Serialization/">Product with id=0 does not exist</string>


============================================================
ID: 1
Price: 234
Format: application/json


Status: OK
Response:


============================================================
ID: 1
Price: 234
Format: application/xml


Status: OK
Response:

Vylepšení:

Místo vyhození výjimky s pouhým textovým popisem si vytvoříme třídu, která bude obsahovat více informací o vzniklém problému. Vytvoříme si tedy třídu NotFoundException, která bude obsahovat následující vlastnosti Code, Message a Description (tyto vlastnosti jsou pouze pro demonstraci použití):

public class NotFoundException
{
public int Code { get; set; }
public string Message { get; set; }
public string Description { get; set; }
}

Když si tedy uživatel zažádá o produkt s ID, které neexistuje, vrátíme mu trošku více informací proč ten produkt s ID třeba neexistuje

// pokud neexistuje, vyhod vyjimku
if (p == null)
{
NotFoundException detail = new NotFoundException
{
Code = 404,
Message = "Product not found",
Description = string.Format("There is no product with id={0}. Change ID and try again", parsedId)
};
throw new WebFaultException<NotFoundException>(detail, System.Net.HttpStatusCode.NotFound);
}

A na straně klienta jsme si vytvořily pomocnou metodu, která vezme objekt HttpResponseMessage a format a je-li StatusCode roven NotFound, provedeme deserializaci v závislosti na uvedeném formátu. Deserializací získáme objekt NotFoundException a budeme si moct přečíst doplňující informace

private static void GetExceptionDetail(HttpResponseMessage response, string format)
{
// abych mohl vicekrat cist obsah nahraju ho do bufferu
response.Content.LoadIntoBuffer();

// zobrazim StatusCode
Console.WriteLine("Status: " + response.StatusCode);

if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{

NotFoundException detail = format == "application/xml"
? response.Content.ReadAsDataContract<NotFoundException>()
: response.Content.ReadAsJsonDataContract<NotFoundException>();
if (detail != null)
{
Console.WriteLine(" Code: {0}", detail.Code);
Console.WriteLine(" Message: {0}", detail.Message);
Console.WriteLine(" Description: {0}", detail.Description);
}
}
}

Po opětovném zavolání metod UpdateProductPrice_1 se výsledek změní pouze u té se StatusCode = NotFound následovně

============================================================
ID: 0
Price: 234
Format: application/json


Status: NotFound
Code: 404
Message: Product not found
Description: There is no product with id=0. Change ID and try again
Response: {"Code":404,"Description":"There is no product with id=0. Change ID and try again","Message":"Product not found"}


============================================================
ID: 0
Price: 234
Format: application/xml


Status: NotFound
Code: 404
Message: Product not found
Description: There is no product with id=0. Change ID and try again
Response: <;NotFoundException xmlns="http://schemas.datacontract.org/2004/07/Eshop.Service" xmlns:i="http://www.w3.org/2001/XMLSchema-instance"><Code>404</Code><Description>There is no product with id=0. Change ID and try again</Description><Message>Product not found</Message></NotFoundException>


============================================================

Závěr:

V dnešním krátkém příspěvku jsme si ukázaly jak můžeme na straně služby vyvolat výjimku a na straně klienta si přečíst informace o výjimce. Kromě jednoduchých textových výjimek jsme si ukázaly možnost, jak vytvořit vlastní třídy s vlastnostmi obsahující mnohem více informací o výjimce a jak tuto třídu poté deserializovat na straně klienta ke zjištění oněch dodatečných informací. Jak vidíte, práce s výjimkami je zde velice jednoduchá. V příštím článku se nejspíše podíváme na možnosti cacheování.

Posted 17. února 2010 13:28 by lukaashek | 0 Comments
Vedeno pod: ,

WebHttp Services s sebou přínáší další zajímavou funkcionalitu, kterou si na následujících několika řádcích představíme. V minulých dílech jsem komunikoval se službou pouze prostřednictvím xml requestů a výsledkem byl vždy i xml response. Pokud služby nejsou nijak nastaveny, jejich defaultní chování je právě zabalit response do xml.

Pojďme se tedy podívat jak ovlivnit výsledný formát odpovědi.

První důležitou informací je, že pokud pošlu dotaz ve formátu XML či JSON, tak ho vždy dokáže moje služba zpracovat i v případě, že bude v konfiguračním souboru web.config nastaveno automaticFormatSelectionEnabled="false". Toto nastavení se týká pouze odchozích (response) zpráv !

V mojí aplikaci jsem si vytvořil novou metodu nazvanou AddProduct, na které budu demonstrovat použití jednotlivýc způsobů k tomu aby jsme docílil požadovaného výsledku:

Eshop.Service

[Description("Vlozi produkt do kolekce")]
[WebInvoke(UriTemplate = "/AddProduct", Method = "POST")]
public Product AddProduct(Product product)
{
product.ID = FakeDatabase.Products.Max(p => p.ID) + 1;
FakeDatabase.Products.Add(product);
return product;
}

Eshop.Client

private static void AddProduct(Product productToCreate)
{
using (HttpClient client = new HttpClient(URI))
{
HttpContent content = HttpContentExtensions.CreateDataContract<Product>(productToCreate);
// poslu pozadavek POST na adresu http://localhost:8090/Eshop/AddProduct
using (HttpResponseMessage response = client.Post("AddProduct", content))
{
Console.WriteLine("Response:");
Console.WriteLine(response.Content.ReadAsString());
}
}
}

Zde si vytvořím metodu AddProduct, kde pouze zavolám mojí nově vytvořenou metodu na straně služby. Než ovšem porvedeme zavolání mojí metody nastavíme ve web.configu hodnotu automaticFormatSelectionEnabled na false

automaticFormatSelectionEnabled="false"

Po zavolání AddProduct, tedy pošleme na službu XML a služba nám vrátí rovněž XML (defaultní chování)

image

Pokud ovšem někdo pošle dotaz ve formátu JSON tak jak jsem uvedl výše, služba se s ním lehce vypořádá a výsledek bude stejný. Akorát v metodě AddProduct na straně klienta použíjeme HttpContentExtensions.CreateJsonDataContract místo původního ttpContentExtensions.CreateDataContract. Pokud budu chtít odpovídat ve formátu JSON, stačí pouze nastavit vlastnost ResponseFormat atributu WebInvoke na WebMessageFormat.Json. Z předchozích pár řádků vyplývá, že pokud pošleme dotaz v jakémkoliv formátu (xml nebo json), odpověd bude mít vždy takový tvar jaký bude uveden ve vlastnosti ResponseFormat (default: XML)

automaticFormatSelectionEnabled="true"

Pokud ovšem nastavíme automaticFormatSelectionEnabled na true, bude chování trošku odlišnější. Při volbě formátu pro odchozí zprávu se postupuje následujícími kroky:

  1. WCF se podívá na vlastnost AcceptHeader objektu request, pokud nenajde odpovídající honodtu formátu podívá se dále na …
  2. vlastnost ContentType objektu request, pokud nenajde odpovídající hodnotu formátu podívá se dále na …
  3. hodnotu ResponseFormat odpovídající operace

Tzn. v našen případě, když pošleme dotaz ve formátu XML, vrátí se nám odpověd v XML, pokud bude požadavek v JSON, rovněž i odpověď bude v JSON.

Co když ale budeme chtít poslat dotaz ve formátu XML, ale jako odpověď budeme vyžadovat JSON? Jak na to? Když se podíváte o pár řádků výše, podle čeho WCF rozhoduje o formátu odpovědi, tak na prvním místě je AcceptHeader a hned za ním ContentType. My když pomocí CreateDataContract či CreateJsonDataContract vytvoříme objekt HttpContent, tak v něm je nastaven pouze ContentType bud na “application/xml” nebo “application/json” tzn. že pomocí AcceptHeader můžeme ovlivnit výstupní formát tím, že upravíme hlavičku dotazu. Na následujícím kódu můžete vidět jak na to:

private static void AddProduct(Product productToCreate)
{
using (HttpClient client = new HttpClient(URI))
{
using (HttpRequestMessage request = new HttpRequestMessage("POST", "AddProduct"))
{
// do hlavicky pridam informace o vystupnim formatu
request.Headers.Accept.AddString("application/xml");
// vytvorim si obsah ktery budu posilat
request.Content = HttpContentExtensions.CreateJsonDataContract<Product>(productToCreate);
// poslu pozadavek na adresu http://localhost:8090/Eshop/AddProduct
using (HttpResponseMessage response = client.Send(request))
{
Console.WriteLine("Response:");
Console.WriteLine(response.Content.ReadAsString());
}
}
}
}

Vytvořím si pouze objekt HttpRequestMessage, kde specifikuji o jakou operaci se jedná a na jakou adresu ten dotaz budu posílat. Pak přidám do hlavičky informací o formátu výstupu a nakonec tento požadavek odešllu. Jak můžete vidět, vytvářím požadavek v JSON formátu, ale výsledek chci ve formátu xml

image

V poslední ukázce se podíváme na možnost jak si explicitně zvolit výstupní formát. pro tuto ukázku použíjeme již vytvořenou metodu GetProducts, které přidáme parametr nazvaný format, podle kterého si bude moct klient specifikovat v jakém formátu data očekává

[WebGet(UriTemplate = "/Products?format={format}")]
public ProductCollection Products(string format)
{
if (string.Equals("json", format, StringComparison.OrdinalIgnoreCase))
WebOperationContext.Current.OutgoingResponse.Format = WebMessageFormat.Json;
return FakeDatabase.Products;
}

Přidáním parametru format, jsme musely upravit vlastnost UriTemplate a přidat příslušnou podmínku. Pokud je hodnota parametru format rovna json, nastavím pomocí WebOperationContext třídy Format pro odchozí zpráv na JSON, jinak nechám defaultní hodnotu (XML). Na straně klienta musíme upravit naší metodu GetProducts následovně: (pouze přidáme informace o formátu)

private static void GetProducts(string format)
{
// ujistete se ze vase adresa konci lomitkem !!!
using (HttpClient client = new HttpClient(URI))
{
// poslu pozadavek GET na adresu http://localhost:8090/Eshop/Products
HttpResponseMessage response = client.Get("Products?format=" + format);

Console.WriteLine("Response: [format={0}]", format);
Console.WriteLine(response.Content.ReadAsString());
Console.WriteLine();
}
}

Pro otestování zavolám GetProducts(“json”) a GetProducts(“default”) a výsledkem bude při prvním volání odpověď ve formátu json, podruhé ve formátu xml

image

Závěr

V dnešním článku jsme si ukázali jak se chovají WebHttp services při zpracování požadavků v XML či JSON formátu, poukázaly na důležitost nastavení atributu automatiFormatSelectionEnabled ve web.config souboru a celé to zakončili ukázkou jak ovlivnit formát odchozí zprávy. V dalším článku se podíváme na možnosti jak zpracovávat výjimky.

Posted 9. února 2010 14:41 by lukaashek | 0 Comments
Vedeno pod: , ,

V minulém článku jsme si vytvořili jednoduchou službu, která obsahovala jednu metodu Products. Dnes si ukážeme jak vytvořit klienta, který nám umožní volat jednotlivé metody na straně služby.

Pro vytvoření klienta máme několik možností, můžeme vužít WebChannelFactory, HttpWebRequest nebo WebClient Avšak dopuručené je použití třídy HttpClient. Tato třída je součástí knihovny WCF Rest Starter Kit Preview 2.

Vytvoříme si tedy Consolovou aplikaci v mém případě pojmenovanou Eshop.Client a přidáme potřebné reference na

  • Microsoft.Http
  • Microsoft.HttpExtensions

Obě tyto knihovny najdeme po nainstalování WCF Rest Starter Kitu ve složce C:\Program Files\Microsoft WCF REST\WCF REST Starter Kit Preview 2\Assemblies\ Dále musíme ve vlastnostech projektu změnit cílový framework na “.NET 4” z původního “.NET 4 client profile”. Je to z toho důvodu, že třída HttpClient má nějaké závislosti na plném .NETu 4

Nyní se můžeme pustit do vytvoření klienta. Třída HttpClient nabízí 3 způsoby jak nainicializovat náš objekt

  • HttpClient()
  • HttpClient(string baseAddress) – zadáme adresu služby jako řetězec
  • HttpClient(Uri baseAddress) – zadáme adresu služby jako Uri objekt

Já si ve své ukázkové aplikaci zvolím zlatou střední cestu a použiju adresu služby jako obyčejný řetězec. Instanci máme vytvořenou a ted musíme pouze poslat GET požadavek na danou adresu (Products) a služba by nám měla vrátit seznam všech produktů. Pro tento účel nabízí třída HttpClient metodu Get, která má opět několik variant, kde můžeme zadat např. parametry, ale já chci zavolat metodu, která nemá parametry tak použiju pouze variantu s jedním parametrem a to adresou (další varianty si ukážeme později)

// ujistete se ze vase adresa konci lomitkem !!!
using (HttpClient client = new HttpClient("http://localhost:8090/Eshop/"))
{
// poslu pozadavek GET na adresu http://localhost:8090/Eshop/Products
HttpResponseMessage response = client.Get("Products");
// vysledek vypisu do console
Console.WriteLine(response.Content);
}

Výsledkem po volání metody Get je objekt typu HttpResponseMessage, kde pouze pomocí vlastnosti vypíšu obsah odpovědi do console. Docela užitečnou vlastností je StatusCode, kde jsme schopni zjistit stav – 200 OK, atd.

Po spuštění naší klientské aplikace můžeme vidět následující odpověď

image

Tato odpověď není příliš dobrá a proto upravíme výpis do console. Na vlasntosti Content zavoláme metodu ReadAsString(), která nám převedě odpověď na smysluplnější řetězec. Další možností je zavolat metodu ReadAsStream(), která vrátí objekt Stream. Po úpravě může vypadat stejné volání následovně:

image

Už je to o něco lepší, ale pořád se v tom docela špatně čte, takže uděláme poslední úpravu. Přidáme using na System.Xml.Linq a místo metody ReadAsString() použijeme metodu ReadAsXElement(). Výsledek bude vypadat následovně:

image

Momentálně vypadá formát odpovědi docela pěkně na čtení, ale lépe než s XML souborem se nám bude asi pracovat přímo z daným objektem, v tomto případě s kolekcí ProductCollection. Pro tento případ budeme muset přidat referenci na System.Runtime.Serialization a samozřejmě na naší službu Eshop.Service. Upravíme tedy kód následovně:

// ujistete se ze vase adresa konci lomitkem !!!
using (HttpClient client = new HttpClient("http://localhost:8090/Eshop/"))
{
// poslu pozadavek GET na adresu http://localhost:8090/Eshop/Products
HttpResponseMessage response = client.Get("Products");

ProductCollection coll = response.Content.ReadAsDataContract<ProductCollection>();

// projdu kolekci a vypisu informace
foreach (Product p in coll)
{
Console.WriteLine("ID: {0}\tName: {1}", p.ID, p.Name);
}
}

A nyní vypadá výsledek takto:

image

Poznámka: Na začátku při vytváření mojí fake databáze jsem udělal menší chybičku, která by nám mohla vadit a to v případě, že provedeme update nějakého produktu tak při zavolání metody ReadAsDataContract<ProductCollection> dojde k zavolání bezparametrického konstrukturou, což nám vždycky vrátí defaultní hodnoty. Proto si ve třídy Product Collection vytvoříme další konstruktor s nějakým parametrem a načítání dat přesuneme do tohoto konstruktoru:

public ProductCollection(bool loadData)
{
if (loadData)
{
this.Add(new Product { ID = 1, Name = "Bunda", CategoryId = 1, Price = 3000 });
this.Add(new Product { ID = 2, Name = "Triko", CategoryId = 1, Price = 500 });
this.Add(new Product { ID = 3, Name = "Mikina", CategoryId = 1, Price = 200 });
this.Add(new Product { ID = 4, Name = "Kosile", CategoryId = 1, Price = 1500 });
this.Add(new Product { ID = 5, Name = "Parfem", CategoryId = 2, Price = 5000 });
this.Add(new Product { ID = 6, Name = "Hodinky", CategoryId = 2, Price = 6000 });
this.Add(new Product { ID = 7, Name = "Retizek", CategoryId = 2, Price = 200 });
}
}

První část máme za sebou a dále si vytvoříme na straně služby novou metodu UpdateProductPrice, která bude sloužit pro změnu ceny daného produktu. Jako parametry bude id produktu a nová cena. Jak si můžete všimnout o pár řádků níže služba je označená atributem WebInvoke, který nabízí kromě stejných vlastností jako WebGet i jednu vlastnost navíc nazvanou Method. Pomocí této vlastnosti můžeme specifikovat o jakou akci se bude jednat např. POST, PUT.

[Description("Aktualizuje cenu u produktu s danym id")]
[WebInvoke(UriTemplate = "/UpdateProductPrice/{id}?price={price}", Method = "PUT")]
public void UpdateProductPrice(string id, string price)
{
int parsedId;
decimal parsedPrice;

if (int.TryParse(id, out parsedId))
{
if (decimal.TryParse(price, out parsedPrice))
{
Product p = (from c in Products()
where c.ID == parsedId
select c).FirstOrDefault();
if (p != null)
p.Price = parsedPrice;
}
}
}

Co je v tomto kódu nového:

  • Description – jedná se o atribut, kterým můžeme popsat naší metodu. Tento popis bude viditelný na stránce s nápovědou (/help)
  • UriTemplate obsahuje následující adresu "/UpdateProductPrice/{id}?price={price}"
    • UpdateProductPrice – název metody
    • {id} – id produktu, nad kterým budeme chtít provést update
    • za otazníkem následují další parametry. V našem případě price={price}"

Poznámka 1: Názvy ve složených závorkách musí odpovídat názvům parametrům v dané metodě. Např. použití price={price1}" vyvolá výjimku při spuštění služby, ale price123={price}" je naprosto v pohodě

Poznámka 2: Vstupní parametry musí být typu string či jiný objekt (nějaká naše třída), ale nelze použít int, decimal atd.

Na straně klienta vytvoříme příslušnou metodu, která provede update daného produktu:

private static void UpdateProductPrice_1()
{
using (HttpClient client = new HttpClient(URI))
{
// budu aktualizovat produkt s id = 1
string strUri = "UpdateProductPrice/1?price=19900";
HttpResponseMessage response = client.Put(strUri, HttpContent.CreateEmpty());
}
}

Jediné co zde udělám vytvoření instance HttpClienta, nastavení Uri adresy metody s příslušnými parametry a zavoláním metody Put, která má několik variant kde jedna z nich přijímá jako parametr adresu operace a HttpContent, který můžeme nastavit na Empty, jelikož vše potřebné nastavíme pomocí parametrů v adrese.

Pro otestování si zabalíme kód pro získání všech produktů do metody pojmenované GetProducts a upravíme v cyklu foreach kolekci coll tak, aby vzala pouze první prvek (aby jsme si nevypisovaly zbytečně všechny produkty) a do detailu o produktu přidáme informaci o ceně. Nejdříve tedy zavoláme metodu GetProducts, hned po ní Updateproduct_1, která změní původní cenu na novou a nakonec opět GetProducts, aby jsme si ověřili, zda opravdu došlo ke změně hodnoty:

static void Main(string[] args)
{
GetProducts();
UpdateProductPrice_1();
GetProducts();
}

Výsledek po volání vypadá následovně:

image 

Další způsob aktualizace produktu bude takový, že jako parametr bude přímo objekt Product. Vytvoříme metody UpdateProduct, která bude velice podobná metodě UpdateProductPrice, akorát se bude lišit druhý parametr:

[Description("Aktualizuje produkt s danym id")]
[WebInvoke(UriTemplate = "/UpdateProduct/{id}", Method = "PUT")]
public void UpdateProduct(string id, Product product)
{
int parsedId;

if (int.TryParse(id, out parsedId))
{
Product p = (from c in Products()
where c.ID == parsedId
select c).FirstOrDefault();
if (p != null)
{
p.Name = product.Name;
p.CategoryId = product.CategoryId;
p.Price = product.Price;
}
}
}

A zavolání této metod z klienta provedu tak, že jako uri adresu nastavím UpdateProduct/1, vytvořím si novou instanci třídy Product a pomocí metody CreateDataContract třídy HttpContentExtensions vytvořím objekt typu HttpContent, který předám jako druhý parametr metody Put: Objekt product totiž nelze předat pomocí query stringu a tak musíme tento objekt zabalit a poslat na příslušnou adresu. Když se podíváme na stránku s nápovědou http://localhost:8090/Eshop/help/operations/UpdateProduct můžeme vidět ukázku jak má vypadat požadavek buď ve tvaru XML či JSON:

image

Kód na straně klienta:

private static void UpdateProduct_2()
{
using (HttpClient client = new HttpClient(URI))
{
// budu aktualizovat produkt s id = 1
string strUri = "UpdateProduct/1";
// vytvorim si novy produkt u ktereho provedu update
Product p = new Product
{
Name = "Novy produkt",
CategoryId = 2,
Price = 2500
};
HttpContent content = HttpContentExtensions.CreateDataContract<Product>(p);
HttpResponseMessage response = client.Put(strUri, content);
}
}

A aby jsme to mohly odzkoušet, ještě upravíme main metodu kde místo UpdateProductPrice_1() budeme volat naší nově vytvořenou metodu UpdateProduct_2()

Výsledek může vypadat takto:

image

Ještě než dnešní povídání zakončím, vytvoříme si nějakou metodu pro vytvoření:

[Description("Metoda pro vytvoreni noveho produktu")]
[WebInvoke(UriTemplate = "/CreateProduct", Method = "POST")]
public void CreateProduct(Product productToCretae)
{
// pokud neexistuje produkt s danym id, vloz ho do kolekce produktu
if (!FakeDatabase.Products.Any(p => p.ID == productToCretae.ID))
{
FakeDatabase.Products.Add(productToCretae);
}
}

No a klient můžeme velice jednoduše tuto metodu zavolat a vytvořit nový produkt: Kód vypadá téměř totožně jako metoda pro update

private static void CreateProduct()
{
using (HttpClient client = new HttpClient(URI))
{
// budu aktualizovat produkt s id = 1
string strUri = "CreateProduct";
Product p = new Product
{
ID = 22,
Name = "Nov ytvoreny produkt",
CategoryId = 2,
Price = 600
};
HttpResponseMessage response = client.Post(strUri, HttpContentExtensions.CreateDataContract<Product>(p));
}
}

Pro otestování jsem si opět upravil metodu GetProducts, kde si nyní z kolekce vyberu poslední prvek, abych zjistil zda se mi můj produkt opravdu vytvořil. Zde je výsledek: Nejdříve jsem si získal poslední produkt (ID=7), provedl vytvoření nového produktu a poté získal znovu poslední produkt, v tomto případě to už bude nově vytvořený produkt (ID=22)

image

Závěr:

Dnes jsme si ukázali jak vytvořit klienta a volat jednotlivé metody na straně služby, jak tuto odpověď zobrazit či deserializovat do konkrétního typu. Dále jak vytvořit metodu pro update, která přijímala buď jednotlivé parametry jako string hodnoty nebo parametry jako uživatelem definovaný typ např. třída Product. A v posledním případě zde byla ukázka jak vytvořit a volat metody pro vytváření nějakých objektů v našem případě vytvoření nového produktu. V příším díle bych rád ukázal jednotlivé možnosti formátování

  • XML – XML = tenhle způsob komunikace jsme momentálně používal ve svých ukázkách
  • XML – JSON – ukázka jak poslat request ve formátu XML a očekávat response ve formátu JSON
  • JSON – XML – ukázka jak poslat request ve formátu JSON a očekávat response ve formátu XML
  • JSON – JSON – ukázka jak poslat request a očekávat response ve formátu JSON
  • jak přidat jednotlivým metodám parametr, podle kterého si uživatel zvolí v jakém formátu chce data očekávat

a další různé věci z této oblasti.

Posted 1. února 2010 15:16 by lukaashek | 0 Comments
Vedeno pod: ,

Další novinkou ve WCF 4 budou WebHttp Services kde budeme mít možnost komunikovat se službou pomocí NE SOAP zpráv, ale klasických xml či json requestů. Dále můžeme poslat dotaz ve formátu xml (json) a služba nám odpoví v xml (json) formátu. Vše záleží na nás jak se nám to bude líbit. Co si ovšem většina z nás vývojářů zalíbí je možnost mít plně pod kontrolou jednotlivé adresy všech metod dané služby.

Poznámka:

Jste-li vlastníkem VS 2010 ve verzi Beta, budete si muset doinstalovat přes extension managera šablonu pro vytvoření WebHtp služby (WCF Rest Service Template) a jakmile si tuto šablonu stáhnete pak budete moct vytvořit novou aplikaci typu “WCF Rest Service Application”

Šablona obsahuje předvytvořené třídy a další soubory, které si v rychlosti popíšeme:

  • Global.asax – bude nám sloužit pro nastavení routování – více později
  • SampleItem.cs – soubor, která představuje nějaké data v naší službě
  • Service1.cs – nmaše služba, která nabízí nějaké ukázkové metody
  • Web.config – standardní configurační soubor

Pro mojí ukázku si vytvoříme fiktivní databázi (třída) obsahující kategorie (CategoryCollection) a produkty (ProductCollection)

public class FakeDatabase
{
static FakeDatabase()
{
// nactu categorie a produkty
Categories = new CategoryCollection();
Products = new ProductCollection();
}

public static ProductCollection Products { get; private set; }
public static CategoryCollection Categories { get; private set; }
}

ProductCollection + Product

public class ProductCollection : Collection<Product>
{
public ProductCollection()
{
this.Add(new Product { ID = 1, Name = "Bunda", CategoryId = 1, Price = 3000 });
this.Add(new Product { ID = 2, Name = "Triko", CategoryId = 1, Price = 500 });
this.Add(new Product { ID = 3, Name = "Mikina", CategoryId = 1, Price = 200 });
this.Add(new Product { ID = 4, Name = "Kosile", CategoryId = 1, Price = 1500 });
this.Add(new Product { ID = 5, Name = "Parfem", CategoryId = 2, Price = 5000 });
this.Add(new Product { ID = 6, Name = "Hodinky", CategoryId = 2, Price = 6000 });
this.Add(new Product { ID = 7, Name = "Retizek", CategoryId = 2, Price = 200 });
}
}

public class Product
{
public int ID { get; set; }
public string Name { get; set; }
public int CategoryId { get; set; }
public decimal Price { get; set; }
}

CategoryCollection + Category

public class CategoryCollection : Collection<Category>
{
public CategoryCollection()
{
this.Add(new Category { ID = 1, Name = "Obleceni" });
this.Add(new Category { ID = 2, Name = "Doplnky" });
}
}

public class Category
{
public int ID { get; set; }
public string Name { get; set; }
public ProductCollection Products { get; set; }
}

Můžeme začít implementovat naší službu. Nejdříve si přejmenujeme službu Service1.cs na EshopService. Vymažeme všechny metody uvnitř třídy a vytvoříme jednu novou metodu nazvanou Products, která vrátí seznam všech produktů v naší imaginární databázi:

[WebGet(UriTemplate = "/Products")]
public ProductCollection Products()
{
return FakeDatabase.Products;
}

Jak můžeme vidět, služba je označená atributem WebGet, pomocí kterého zpřístupníme naší službu na GET požadavky ze strany klientů a pomocí vlastnosti UriTemplate nastavíme příslušnou adresu, na které bude metoda dostupná. Kromě vlastnosti UriTemplate nabízí třída WebGet další zajímavé vlastnosti:

  • BodyStyle – pomocí tohoto atributu jsme schopni ovlivnit styl příchozí či odchozí zprávy
    • WebMessageBodyStyle.Bare – obsah všech zpráv není zabalen do speciálních elementů (ukázka níže – obrázek 1.)
    • WebMessageBodyStyle.Wrapped – obsah všech zpráv je zabalen do speciálních elementů (ukázka níže – obrázek 1.)
    • WebMessageBodyStyle.WrappedRequest – pouze obsah příchozí zprávy je zabalen do speciálního elementu
    • WebMessageBodyStyle.WappedResponse – pouze obsah odchozí zprávy je zabalen do speciálního elementu
  • RequestFormat
    • WebMessageFormat.Xml – požadavek musí přijít ve formátu XML
    • WebMessageFormat.Json – požadavek musí přijít ve formátu Json
  • ResponseFormat
    • WebMessageFormat.Xml – odpověd bude ve formátu XML
    • WebMessageFormat.Json – odpověd bude ve formátu Json

 

Obrázek 1.

Bare Wrapped
image image

Naše služba má již vytvořenou potřebnou metodu a můžeme jí vyzkoušet. Předtím budeme ovšem muset provést pár drobných nastavení. Ve vlastnostech projektu specifikujeme port (aby jsme nemuseli pokaždé měnit kód v budoucích aplikacích na číslo portu, které se vygeneruje vždy jiné) a virtuální cestu

image

Po těchto nastaveních bude naše metoda dostupná na adrese http://localhost:8090/Eshop/Products. Poslední úprava před spuštění se musí provést v souboru Global.asax v metodě RegisterRoute z důvodu, že jsme si přejmenovali třídu Service1.cs na EshopService.cs a pevně si nastavili virtuální cestu. Upravená metoda vypadá následovně:

private void RegisterRoutes()
{
// Edit the base address of Service1 by replacing the "Service1" string below
RouteTable.Routes.Add(new ServiceRoute("", new WebServiceHostFactory(), typeof(EshopService)));
}

Nyní by mělo být vše připraveno ke spuštění aplikace. Jakmile stisknete F5, spustí se development server na adrese http://localhost:8090/Eshop/ a pokud k této adrese přidáme Products, uvidíme výsledný XML soubor obsahující seznam produktů.

Poznámka:

Když vytváříte WebHttp služby je defaultně povolena stránka s nápovědou, tzn. že když místo Products zadám help, dostanu se na stránku s informacemi o tom, jaké metody moje služba nabízí, jaké obsahují parametry či jak vypadá dotaz (odpověd) ve formátu xml (Json)

Závěr

V dnešním článku jste se mohly seznámit s novinkou, která Vás čeká a nemine ve verzi WCF 4. Kromě tohoto lehkého úvodu s velice jednoduchou ukázkou se v příštím díle podíváme na zajímavější věci např. jak

  • vystavit metody s parametry
  • vytvořit klienta a komunikovat se službou
  • provést update či vytvoření nového např. produktu
Posted 30. ledna 2010 21:44 by lukaashek | 3 Comments
Vedeno pod: ,

Ve článcích parameter inspection a message inspection jsem se věnoval možnostem rozšíření a dnes v tom budu pokračovat, protože jsem zapomněl uvést možnost rozšíření pomocí rozhraní IOperationInvoker. Toto rozhraní nám umožní přepsat metodu Invoke a plně převzít kontrolu nad vykonáním konkrétní aplikace.

Vytvořil jsem si jednoduchou službu (atributu ProductCaching si zatím nevšímejte, vysvětlím později)

[ServiceContract]
public interface IProductService
{
    [OperationContract]
    [ProductCaching]
    Product GetProduct(string name);
}

V implementaci služby pouze vrátím konkrétní produkt

public class ProductService : IProductService
{
    #region IProductService Members
    public Product GetProduct(string name)
    {
        using (VWAEntities ctx = new VWAEntities())
        {
            return (from c in ctx.Product
                    where c.Name == name
                    select c).FirstOrDefault();
 
        }
    }
    #endregion
}

To co zde napíšu jako rozšíření pomocí rozhraní IOperationInvoker by se dalo velice snadno zakomponovat do implementace metody GetProduct, ale může nastat situace, kdy nebudeme moct šahat na tento kód tak se nám možnost tohoto rozšíření může hodit.

Vytvořím si tedy třídu, která bude implementovat rozhraní IOperationInvoker

  • object[] AlocateInputs – slouří pro alokaci pole potřebného pro vsupní parametry metody, jejichž spuštění budeme chtít přepsat. V našem případě má metoda Getproduct jeden parametr name, proto vrátíme pole o velikosti 1
  • object Invoke – zde provedeme přepsání naší metody GetProduct
  • IAsyncResult BeginInvoke – stejné jako Invoke, akorát se vykonání provede asynchronně
  • object InvokeEnd – vrátí výsledek po asynchronním vykonání operace
  • IsSynchronous – nastaví zda je volání synchronní nebo asynchronní
public class ProductCacher : Attribute, IOperationInvoker
{
    private IOperationInvoker operationInvoker;
    // lokalni productCache ve ktere budu uchovavat jiz hledane produkty
    Dictionary<string, Product> productCache = new Dictionary<string, Product>();
 
    public ProductCacher(IOperationInvoker invoker)
    {
        this.operationInvoker = invoker;
    }
 
    #region IOperationInvoker Members
 
    public object[] AllocateInputs()
    {
        // moji metodu volam z jednim parametrem GetProduct(string name)
        return new object[1];
    }
 
    public object Invoke(object instance, object[] inputs, out object[] outputs)
    {
        // ziskam si hodnotu parametru name
        string name = inputs[0] as string;
        Product value;
        // zkusim najit produkt v productCache
        if (productCache.TryGetValue(name, out value))
        {
            // pokud ho najdu tak vysledny produkt vratim a vypisu textovou informaci
            outputs = new object[0];
            Console.WriteLine("-- Data loaded from productCache...");
            return value;
        }
        else
        {
            // jinak zavolam puvodni metodu GetProduct
            value = (Product)this.operationInvoker.Invoke(instance, inputs, out outputs);
            // ulozim do productCache
            productCache[ name] = value;
            Console.WriteLine("-- Data loaded from database...");
            // a vratim hodnotu
            return value;
        }
    }
 
    public IAsyncResult InvokeBegin(object instance, object[] inputs, AsyncCallback callback, object state)
    {
        throw new NotImplementedException();
    }
 
    public object InvokeEnd(object instance, out object[] outputs, IAsyncResult result)
    {
        throw new NotImplementedException();
    }
 
    public bool IsSynchronous
    {
        get { return true; }
    }
 
    #endregion
}

Na závěr se dostáváme k tomu, proč je moje metoda v definici rozhraní IProductService označená atributem ProductCaching. Tato třída implementuje již známé rozhraní IOperationBehavior, kde v implementaci metody ApplyDispatchBehavior nastavíme Invoker na instanci třídy ProductCacher

public class ProductCaching : Attribute, IOperationBehavior
 {
     #region IOperationBehavior Members
 
     public void AddBindingParameters(OperationDescription operationDescription, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
     {
     }
 
     public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation)
     {
     }
 
     public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation)
     {
         dispatchOperation.Invoker = new ProductCacher(dispatchOperation.Invoker);
     }
 
     public void Validate(OperationDescription operationDescription)
     {
     }
 
     #endregion
 }

Tím jsme dali najevo, že po označení metody atributem ProductCaching dojde při spuštění metody GetProduct k zavolání metody Invoke třídy ProductCacher, která se podívá do lokální productCache, zda se tam hledaný produkt nachází a v případě, že ne, zavolá se původní metoda GetProduct, výsledek se uloží do productCache a odešle se klientovi.

Pro otestování jsem vytvořil klientskou aplikaci, která zavolá několikrát metodu GetProduct a ověří správnou funkcionalitu našeho rozšíření

 WCF_OperationInvoker

při prvním volání metody GetProduct s parametrem name=Hokejka dojde k provedení dotazu do SQL serveru, ale při druhém volání se tento produkt již načte z productCache.

V minulém článku jsem ukazoval možnosti routování, kde jsem vše potřebné nastavil v konfiguračním souboru a dnes si ukážeme jak aplikovat nové pravidla pro směrování za běhu služby. Např. naše routovací služba může provádět v jiném vlákně kontrolu stavu jednotlivých služeb a v případě, že služba nebude schopna momentálně přijímat žádné požadavky, můžeme dynamicky změnit podmínku pro směrování zpráv a nyní přeposílat zprávy na jinou službu.

Musíme si tedy vytvořit novou routovací konfiguraci, která obsahuje směrovací tabulku s jednotlivými filtry:

private static RoutingConfiguration NewRoutingConfiguration()
{
    // vytvorim si smerovaci tabulku
    var filterTable = new MessageFilterTable<IEnumerable<ServiceEndpoint>>();
    // pridam dva filtry
    MessageFilter addFilter = new ActionMessageFilter("http://tempuri.org/ICalculator/Add");
    MessageFilter subtractFilter = new ActionMessageFilter("http://tempuri.org/ICalculator/Subtract");
    // definice endpointu na ktere se budou zpravy preposilat
    ServiceEndpoint calc1 = new ServiceEndpoint(
        ContractDescription.GetContract(typeof(IRequestReplyRouter)),
        new BasicHttpBinding(),
        new EndpointAddress("http://localhost:9000/servicemodelsamples/calcservice1"));
    ServiceEndpoint calc2 = new ServiceEndpoint(
        ContractDescription.GetContract(typeof(IRequestReplyRouter)),
        new BasicHttpBinding(),
        new EndpointAddress("http://localhost:9001/servicemodelsamples/calcservice2"));
    // filtry pridam do smerovaci tabulky
    filterTable.Add(addFilter, new List<ServiceEndpoint> { calc2 });
    filterTable.Add(subtractFilter, new List<ServiceEndpoint> { calc1 });
    // vratim nove vytvorenou konfiguraci
    return new RoutingConfiguration(filterTable, false);
}

Jedná se úplně o stejnou konfiguraci jako v minulém článku, akorát jsem provedl záměnu služeb, tzn. že se volání metody Add vykoná na službě CalculatorService2 místo CalculatorService1.

Poté si z kolekce Extensions objektu ServiceHost získáme objekt RoutingExtensions, na kterém zavoláme metodu ApplyConfiguration a jako parametr předáme naši nově vytvořenou konfiguraci

var re = serviceHost.Extensions.Find<RoutingExtension>();
if (re != null)
{
    re.ApplyConfiguration(NewRoutingConfiguration());
    Console.WriteLine("Configuration changed");
}

Zde můžete vidět výsledek po spuštění této ukázky

ServiceHost – aplikace, která hostuje služby CalculatorService1 a CalculatorService2image

RoutingServiceHost – naše routovací služba

 image

RoutingClient – klient, který volá metody Add a Subtract

image

Nejdříve spustím Clienta, který zavolá metodu Add a Subtract, poté změním konfiguraci na straně routovací služby a nakonec na straně clienta opět zavolám stejné metody a tentokrát se vykonají na jiných službách, přesně tak jak jsem to chtěl v konfiguraci

Routování nemělo ve verzi .NET 3.x oficiální podporu, ale s pomocí API jsme si tuto funkcionalitu byly schopni napsat. Ve verzi .NET 4.x už tato podpora existuje. Než se do toho pustíme, vytvoříme si pomocnou službu, na které budu požadavky od klienta posílat, Tedy naše routovací služba podle různých parametrů se rozhodne, na jakou službu tu zprávu zašle.

CalculatorService

Aby jsme si mohli ověřit správnou funkcionalitu vytvořil jsem si 2 služby, kde každá služba implementuje stejné rozhraní ICalculator:

[ServiceContract]
public interface ICalculator
{
    [OperationContract]
    double Add(double a, double b);
 
    [OperationContract]
    double Subtract(double a, double b);
 
    [OperationContract]
    double Multiply(double a, double b);
 
    [OperationContract]
    double Divide(double a, double b);
}

Kromě počítání správného výsledku mají za úkol služby vypsat do console informaci o spuštění konkrétní metody. Tyto služby spustím jednoduše pomocí objektu ServiceHost.

RoutingService

Pro spuštění naší routovací služby vytvoříme pouze instanci objektu ServiceHost, kde do konstruktoru předáme typ objektu RoutingService a spustíme. Toť vše a jak to tedy funguje? Pro důvod routování byla právě vytvořená nová třída RoutingService, kterou takto vložíme do konstruktoru ServiceHost objektu a poté vše nastavení provedeme v konfiguračním souboru

static void Main(string[] args)
{
    using (ServiceHost serviceHost = new ServiceHost(typeof(RoutingService)))
    {
        try
        {
            serviceHost.Open();
            Console.WriteLine("The Routing Service is now running.");
            Console.WriteLine("Press <ENTER> to terminate router.\n");
            // The service can now be accessed.
            Console.ReadLine();
            serviceHost.Close();
        }
        catch (CommunicationException)
        {
            serviceHost.Abort();
        }
    }
}

V konfiguračním souboru pak v elementu service uvedeme jako název služby právě název té nové třídy RoutingService ( i s názvy namespace !!) a pak stačí vytvořit libovolný počet endpointů na kterém bude naše služba naslouchat. Tzn. klient bude znát pouze adresu routovací služby, ale už se nikdy nedozví o adrese konkrétní služby, která zpracovala jeho požadavek ( v našem případě to bude buď služba CalculatorService1 nebo CalculatorService2). Při definici endpointu uvedeme klasicky adresu, binding a do contractu uvedeme název z předem určených kontraktů

  • ISimplexDatagramRouter
  • ISimplexSessionRouter
  • IRequestReplyRouter
  • IDuplexSessionRouter

My jelikož budeme komunikovat klasickým stylem request-reply využijeme contract IRequestReply. Např. pokud by naše služba nabízela metody, které jsou označeny atributem IsOneWay=true museli by jsme použít contract ISimplexDatagramRouter

<service name="System.ServiceModel.Routing.RoutingService"
           behaviorConfiguration="routingData">
    <host>
      <baseAddresses>
        <add baseAddress="http://localhost:9000/routingservice/router"/>
      </baseAddresses>
    </host>
    <endpoint name="twowayEndpointBasic"
              address="twoway-basic"
              binding="basicHttpBinding"
              contract="System.ServiceModel.Routing.IRequestReplyRouter"/>
  </service>

Nyní pokud bude chtít klient komunikovat se službou, může zaslat zprávu na adresu http://localhost:9000/routingservice/router/twoway-basic

Dále jsme v elementu service uvedli název konfigurace pro další nastavení naší služby. V této části nastavení pouze uvedeme název směrovací tabulky, která se využije pro směrování jednotlivých zpráv

<behaviors>
     <serviceBehaviors>
       <behavior name="routingData">        
         <!--NAZEV SMEROVACI TABULKY-->
         <routing filterTableName="routingTable2" routeOnHeadersOnly="false"/>
       </behavior>
     </serviceBehaviors>
   </behaviors>

Definice routovací tabulky je uvedena v elementu routing. Můžeme samozřejmě definovat více směrovacích tabulek a tu aktivní nastavíme vždy v konfiguraci služby. Každá směrovací tabulka obsahuje název filtru a název endpointu. Pokud příchozí zpráva vyhovuje filtru, přepošle se na uvedený endpoint. o filtrech a endpointech jsem se ještě nezmínil, tak pojďme to napravit

<filterTables>
   <filterTable name="routingTable">
     <add filterName="addFilter" endpointName="CalculatorService1"/>
     <add filterName="subtractFilter" endpointName="CalculatorService2"/>
   </filterTable>
   <filterTable name="routingTable2">
     <add filterName="gt5" endpointName="CalculatorService1" />
     <add filterName="lt5" endpointName="CalculatorService2" />
    </filterTable>
</filterTables>

Jelikož se naše routovací služba chová zároveň jako klient (jako služba přijímá požadavky, které potom jako klient zasílá na konkrétní služby) musíme do elementu client uvést názvy endpointů už těch opravdových služeb (CalculatorService1 a CalculatorService2)

<client>
 <endpoint name="CalculatorService1"
           address="http://localhost:9000/servicemodelsamples/calcservice1"
           binding="basicHttpBinding"
           contract="*"/>
 <endpoint name="CalculatorService2"
           address="http://localhost:9001/servicemodelsamples/calcservice2"
           binding="basicHttpBinding"
           contract="*"/>
</client>

Nyní služba ví, kam případné zprávy přeposílat a už nám chybí se podívat na jednotlivé filtry, které se rovněž definují v elementu routing. U filtru uvádíme vždy název, na který se pak odkazujeme z routovací tabulky, typ filtru (může nabývat následujících hodnot)

  • •Action
  • •EndpointAddress
  • •PrefixEndpointAddress
  • •And
  • •Custom
  • •EndpointName
  • •MatchAll
  • •XPath

a případně nastavím atribut filterData na požadovanou akci, xpath výraz či něco jiného. Pojďme se podívat na moje definované filtry:

  • addFilter – typ filtru je nastaven na action, tzn. že se bude rozhodovat podle toho jaká metoda se zavolá. Pokud se zavolá metoda Add, filtr vyhoví
  • subtractFilter – stejné jako výše, akorát filtr vyhoví při zavolání metody Subtract
  • gt5 – metody mají 2 parametry ‘a’ a ’b’. Pokud je 5 větší než 5, daný filtr vyhoví
  • lt5 – podobný jako gt5, akorát vyhoví při čísle menším než 5
<filters>
    <filter name="addFilter" filterType="Action" filterData="http://tempuri.org/ICalculator/Add"/>
    <filter name="subtractFilter" filterType="Action" filterData="http://tempuri.org/ICalculator/Subtract"/>    
    <filter name="gt5" filterType="XPath"
            filterData="boolean(//*[local-name()= 'a']/text() &gt; 5)"/>
    <filter name="lt5" filterType="XPath"
            filterData="boolean(//*[local-name()= 'a']/text() &lt; 5)"/>
    <filter name="MatchAll" filterType="MatchAll"/>
  </filters>

Naše routovací služba je nyní kopmpletně hotová a můžeme vyzkoušet klienta. Klientská část spočívá ve vytvoření proxy třídy, která poté pošle zprávu na endpoint routovací služby. Když budu chtít např. sčítat a jako číslo a zadám hodnotu menší než 5, provede se vykonání zprávy na službe CalculatorService1, při hodnotě vyšší než 5, převezme režii služba CalculatorService2.

Co ovšem nastane v případě když zadám číslo rovno 5? Výsledkem bude výjimka, proto je nutné tady tohle nějak ošetřit. Musíme do naší routovací tabulky přidat nový filter, kde jako typ filtru vybereme MatchAll (tzn. uplatní se na všechny zprávy) a nastavíme mu pripritu na 0. Ostatním filtrům v tabulce nastavíme prioritu na 1. Čím vyšší priorita, tím dříve se daný filtr vykoná. V našem případě se provedou nejdříve filtry gt5 a lt5 a jelikož ani jeden nevyhověl, provede se na závěr filtr MatchAll, který danou zprávu přepošle na např. defaultní službu (např. CalculatorService1)

Co se stane v případě, kdy služba na kterou chceme přeposlat zprávu neběží? I v této situaci jsme schopni se vyvarovat případné výjimce a to tím, že si vytvoříme tzv. backupList, do kterého uvedeme názvy endpointů, které jsou např. nějaké záložní a to že je zaručeno, že budou vždy spuštěné. Poté u jednotlivých filtrů uvedeme název takového backupListu a pokud se při uplatnění filtru bude chtít zpráva poslat na služba, která neběží, uplatní se backupList a zpráva se pošle na tuhle záložní slučbu.

Na závěr ještě kompletní nastavení v souboru App.config

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <client>
      <endpoint name="CalculatorService1"
                address="http://localhost:9000/servicemodelsamples/calcservice1"
                binding="basicHttpBinding"
                contract="*"/>
      <endpoint name="CalculatorService2"
                address="http://localhost:9001/servicemodelsamples/calcservice2"
                binding="basicHttpBinding"
                contract="*"/>
    </client>
    <services>
      <!--ROUTING SERVICE-->
      <service name="System.ServiceModel.Routing.RoutingService"
               behaviorConfiguration="routingData">
        <host>
          <baseAddresses>
            <add baseAddress="http://localhost:9000/routingservice/router"/>
          </baseAddresses>
        </host>
        <endpoint name="twowayEndpointBasic"
                  address="twoway-basic"
                  binding="basicHttpBinding"
                  contract="System.ServiceModel.Routing.IRequestReplyRouter"/>
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name="routingData">
          <serviceMetadata httpGetEnabled="true"/>
          <serviceDebug includeExceptionDetailInFaults="true"/>
          <!--NAZEV SMEROVACI TABULKY-->
          <routing filterTableName="routingTable2" routeOnHeadersOnly="false"/>
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <routing>
      <filters>
        <filter name="addFilter" filterType="Action" filterData="http://tempuri.org/ICalculator/Add"/>
        <filter name="subtractFilter" filterType="Action" filterData="http://tempuri.org/ICalculator/Subtract"/>
        <filter name="gt5" filterType="XPath"
                filterData="boolean(//*[local-name()= 'a']/text() &gt; 5)"/>
        <filter name="lt5" filterType="XPath"
                filterData="boolean(//*[local-name()= 'a']/text() &lt; 5)"/>
        <filter name="MatchAll" filterType="MatchAll"/>
      </filters>
      <filterTables>
        <filterTable name="routingTable">
          <add filterName="addFilter" endpointName="CalculatorService1"/>
          <add filterName="subtractFilter" endpointName="CalculatorService2"/>
        </filterTable>
        <filterTable name="routingTable2">
          <add filterName="gt5" endpointName="CalculatorService1" backupList="backupEndpoints" priority="1"/>
          <add filterName="lt5" endpointName="CalculatorService2" backupList="backupEndpoints" priority="1"/>
          <add filterName="MatchAll" endpointName="CalculatorService1" priority="0"/>
        </filterTable>
      </filterTables>
      <backupLists>
        <backupList name="backupEndpoints">
          <add endpointName="CalculatorService1"/>
        </backupList>
      </backupLists>
    </routing>
  </system.serviceModel>
</configuration>

Ukázkovou aplikaci můžete stáhnout zde

Metadata Extensions

Další novinkou v oblasti Discovery je možnost přidat k endpointu doplňující informace (v mém případě to bude u chatovací aplikace uživatelské jméno uživatele). Z verze WCF 3.x víte, že každý endpoint má kolekci Behaviors, do které můžeme přidat dodatečná nastavení či rozšířené vlastnosti endpointu. Stačí vytvořit třídu, která implementuje rozhraní IEndpointBehavior a upravit metody tak aby dělaly co chcete (např. kontrola příchozích zpráv v článku zde). Ve verzi 4.x můžeme využít třídu EndpointDiscoveryBehavior, která implementuje zmiňované rozhraní IEndpointBehavior a dále přidává další vlastnosti např. Scopes či Extensions.

Jakmile vytvoříme instance třídy EndpointDiscoveryBehavior tak do kolekce Extensions můžeme uložit jakékoliv informace ve formě XElementu

   1: // vytvorim si novy objekt EndpointDiscoveryBehavior
   2: var endpointDiscoveryBehavior = new EndpointDiscoveryBehavior();
   3: // do kolekce Extensions pridam doplnujici informace o aktualne prihlasenem uzivateli
   4: endpointDiscoveryBehavior.Extensions.Add(
   5:     new XElement("root",
   6:     new XElement("Name", this.UserName)));

Pokud si vzpomínáte na můj první článek o Discovery tak jsem po stisku tlačítka vytvářel instanci objektu ServiceHost, kde jsem předal typ služby a jednoznačnou adresu. Jelikož se endpoint vytvoří dynamicky na základě mojí Uri adresy, potřebuju si získat z kolekce endpointů ten můj dynamicky vytvořený, abych mu mohl toto rozšíření přidat do jeho kolekce Behaviors:

   1: var serviceEndpoint = serviceHost.Description.Endpoints.Find(typeof(GreetingService.IHello));
   2: serviceEndpoint.Behaviors.Add(endpointDiscoveryBehavior);

jakmile se nyní pokusí uživatel vyhledat ostatní uživatele (služby) bude schopen si ze získaných metadat přečíst to co jsme si uložili do kolekce Extensions. Metoda, která to v mojí aplikaci provádí vypadá následovně:

   1: private string GetPeerName(EndpointDiscoveryMetadata metadata)
   2: {
   3:     XElement peerNameElement = metadata.Extensions.Elements("Name").FirstOrDefault();
   4:     if (peerNameElement != null)
   5:         return peerNameElement.Value;
   6:     return null;
   7: }

Na závěr ještě ukázka jak bude vypadat metoda pro přihlášení uživatele po úpravách, které jsme provedli abychom přidali uživatelské jméno do kolekce Extensions:

   1: private void btnSignIn_Click(object sender, EventArgs e)
   2: {
   3:     if (string.IsNullOrWhiteSpace(this.UserName))
   4:     {
   5:         MessageBox.Show("Username");
   6:         return;
   7:     }
   8:  
   9:     localAddress = new EndpointAddress("http://localhost:8091/" + Guid.NewGuid().ToString());
  10:  
  11:     serviceHost = new ServiceHost(typeof(GreetingService.GreetingService), localAddress.Uri);
  12:  
  13:     var endpointDiscoveryBehavior = new EndpointDiscoveryBehavior();
  14:     endpointDiscoveryBehavior.Extensions.Add(
  15:         new XElement("root",
  16:         new XElement("Name", this.UserName)));
  17:     var serviceEndpoint = serviceHost.Description.Endpoints.Find(typeof(GreetingService.IHello));
  18:     serviceEndpoint.Behaviors.Add(endpointDiscoveryBehavior);
  19:  
  20:     serviceHost.BeginOpen((result) => serviceHost.EndOpen(result), null);
  21:    
  22:     btnSignIn.Enabled = false;
  23:     btnSignOut.Enabled = true;
  24:     btnDiscovery.Enabled = true;
  25:     ChangeTitle(txtUsername.Text);
  26: }

A kromě grafické vzhledu jak to bude vypadat nyní v naší aplikaci, můžete na obrázku níže vidě i strukturu SOAP zprávy z naším uživatelským jménem:

-- todo přidat obrázek kde je místo uri uživatelské jméno

   1: <s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:s="http://www.w3.org/2003/05/soap-envelope">
   2:   <s:Header>
   3:     <a:Action s:mustUnderstand="1">http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01/Hello</a:Action>
   4:     <h:AppSequence InstanceId="1259903861" MessageNumber="1" xmlns:h="http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01" />
   5:     <a:MessageID>urn:uuid:7b950263-1677-4957-ae74-dc608edc06eb</a:MessageID>
   6:   </s:Header>
   7:   <s:Body>
   8:     <Hello xmlns="http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01">
   9:       <a:EndpointReference>
  10:         <a:Address>http://localhost:8091/7fa58827-fe75-42ca-afc0-b8cad4dc37b6</a:Address>
  11:       </a:EndpointReference>
  12:       <d:Types xmlns:dp0="http://tempuri.org/" xmlns:d="http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01">dp0:IHello</d:Types>
  13:       <XAddrs>http://localhost:8091/7fa58827-fe75-42ca-afc0-b8cad4dc37b6</XAddrs>
  14:       <MetadataVersion>0</MetadataVersion>
  15:       <root xmlns="">
  16:         <Name>Lukas</Name>
  17:       </root>
  18:     </Hello>
  19:   </s:Body>
  20: </s:Envelope>

Announcements

Tak jako existuje funkcionalita u různých IM programů, že Vám oznámí když někdo přejde do stavu online, tak máte možnost tuto funkcionalitu zabudovat i do WCF služeb. Lze toho dosáhnout velice jednoduchým způsobem a to pouze přidáním konkrétního endpointu. Tak jako pro funkcionalitu hledání na síti jsme využívali udpDiscoveryEndpoint, tak pro funkcionalitu oznámení existuje udpAnnouncementEndpoint. Pokud má naše služba tento endpoint je schopna poslat notifikaci, vždy když přejde do stavu online či offline. jakmile služba odešle určitý typ oznámení, tak by jsme měli být schopni na tento typ oznámení patřičně zareagovat. Ve WCF 4 je nová třída AnnouncementService, kterou když spustíme, tak dokáže naslouchat online, či offline oznámením a vždy vyvolá potřebnou událost:

  • OnlineAnnouncementReceived
  • OfflineAnnouncementReceived

Přidáme tedy do konfiguračního souboru naší služby udpAnnouncementEndpoint, který nám zajistí funkcionalitu oznámení, tedy když se služba spustí, vyvolá online oznámení a když službu ukončím, vyvolá se offline oznámení.

   1: <serviceBehaviors>
   2:    <behavior>
   3:      <serviceDiscovery>
   4:        <announcementEndpoints>
   5:          <endpoint name="udpAnnouncement" 
   6:                    kind="udpAnnouncementEndpoint"/>
   7:        </announcementEndpoints>
   8:      </serviceDiscovery>
   9:    </behavior>
  10:  </serviceBehaviors>

Naše služba nyní umí zasílat online či offline oznámení, ale momentálně nemáme nic co by nám tyto oznámení odchytávalo. Vytvoříme si tedy instanci třídy AnnouncementService, která jak jsem uvedl výše slouží pro odchytávání těch oznámení. Provedeme obsluhu událostí OnlineAnnouncementReceived a OfflineannouncementReceived a pak takto vytvořenou instanci předáme objektu ServiceHost a spustíme. Nyní nám v naší chatovací oáplikaci běží kromě klasické služby i služba odchytávající oznámení.

   1: private void OpenAnnouncementService()
   2: {
   3:     this.announcementService = new AnnouncementService();
   4:     this.announcementService.OnlineAnnouncementReceived += OnlineAnnouncementReceived;
   5:     this.announcementService.OfflineAnnouncementReceived += OfflineAnnouncementReceived;
   6:  
   7:     announcementServiceHost = new ServiceHost(this.announcementService);
   8:     announcementServiceHost.AddServiceEndpoint(new UdpAnnouncementEndpoint());
   9:     announcementServiceHost.BeginOpen(
  10:         (result) => announcementServiceHost.EndOpen(result), null);
  11: }
  12:  
  13: void OfflineAnnouncementReceived(object sender, AnnouncementEventArgs e)
  14: {
  15:     EndpointDiscoveryMetadata metadata = e.EndpointDiscoveryMetadata;
  16:  
  17:     FindCriteria findCriteria = new FindCriteria(typeof(GreetingService.IHello));
  18:     if (findCriteria.IsMatch(metadata))
  19:     {
  20:         this.RemoveUser(new PeerUser(GetPeerName(metadata), metadata.Address.Uri));
  21:     }
  22: }
  25:  
  26: void OnlineAnnouncementReceived(object sender, AnnouncementEventArgs e)
  27: {
  28:     EndpointDiscoveryMetadata metadata = e.EndpointDiscoveryMetadata;
  29:  
  30:     FindCriteria findCriteria = new FindCriteria(typeof(GreetingService.IHello));
  31:     if (findCriteria.IsMatch(metadata))
  32:     {
  33:         this.PopulateUsersList(metadata);
  34:     }
  35: }

Jakmile se nějaký uživatel přihlásí, automaticky to zjistí i ostatní přihlášení uživatelé.

Nová verze WCF nabízí novou a velice dobře vypadající novinku “Discovery”. Tato novinka slouží pro hledání služeb, kde pro hledání využívá WS-Discovery protokol. WCF nabízí 2 módy pro hledání služeb:

  1. AD Hoc mód – funguje pouze v lokálním subnetu
  2. Managed mód – NENÍ omezen pouze na lokální subnet

Implementace druhého módu, je o něco málo komplikovanější a proto mu budu věnovat samostatný článek. Nyní si tedy ukážeme implementaci AD hoc módu.

Služba

Aby uměla naše služba reagovat na hledání ze strany klienta, musí mít endpoint UdpDiscoveryEndpoint (Klient, který provádí hledání má rovněž tento endpoint) na který přichází požadavky od klienta, tedy klient pošle zprávu na všechny služby, které mají uveden tento endpoint. Kromě tohoto endpointu je nutné v ServiceBehaviors “zapnout” viditelnost služby na síti a to pomocí elementu serviceDiscovery. Pokud by naše služba neměla uvedený element ServiceDiscovery, zprávy zaslané klientem by na takovou službu nepřišly.

   1: <system.serviceModel>
   2:     <services>
   3:       <service name="GreetingService.GreetingService">
   4:         <endpoint address=""
   5:                   binding="basicHttpBinding"
   6:                   contract="GreetingService.IHello"/>
   7:         <endpoint name="udpDiscoveryEndpoint" kind="udpDiscoveryEndpoint"/>
   8:       </service>
   9:     </services>
  10:     <behaviors>
  11:       <serviceBehaviors>
  12:         <behavior>
  13:           <serviceDiscovery />
  14:         </behavior>
  15:       </serviceBehaviors>
  16:     </behaviors>
  17:   </system.serviceModel>

Poznámka: V ukázce využívám službu, jejíž definice je následující:

   1: [ServiceContract]
   2: public interface IHello
   3: {
   4:     [OperationContract]
   5:     string SayHello(string str);
   6: }
   7:  
   8: [ServiceContract]
   9: public interface IGoodbye
  10: {
  11:     [OperationContract]
  12:     string SayGoodBye(string str);
  13: }
  14:  
  15: public class GreetingService : IHello, IGoodbye
  16: {
  17:     public string SayGoodBye(string str)
  18:     {
  19:         return string.Format("Goodbye {0}", str);
  20:     }
  21:  
  22:     public string SayHello(string str)
  23:     {
  24:         return string.Format("Hello {0}", str);
  25:     }
  26: }

Pro praktickou ukázku jsem si vytvořil chatovací aplikaci:

image

Pokud uživatel vyplní jméno a stiskne tlačítko “Sign In” vyvolá se následující metoda (důležité informace jsou v komentářích) Nedělá se zde nic zásadního, pouze se pomocí objektu ServiceHost spustí službu na adrese, která se automaticky vygeneruje. Konfigurační soubor, který jsem ukazoval o pár řádků výše je samozřejmě součástí této ukázky, bez něho by naše služba nebyla viditelná v síti a neuměla zpracovávat zpráva zaslané klientem:

   1: private void btnSignIn_Click(object sender, EventArgs e)
   2: {
   3:     if (string.IsNullOrWhiteSpace(txtUsername.Text))
   4:     {
   5:         MessageBox.Show("Username");
   6:         return;
   7:     }
   8:  
   9:     // vytvorim jedinecnou adresu, na ktere mi pobezi sluzba
  10:     localAddress = new EndpointAddress("http://localhost:8091/" + Guid.NewGuid().ToString());
  11:  
  12:     // objektu serviceHost predam do konstruktoru typ moji sluzbu, kterou chci hostovat a adresu na ktere bude dostupna
  13:     serviceHost = new ServiceHost(typeof(GreetingService.GreetingService), localAddress.Uri);
  14:     // asynchrone tuto sluzbu spustim
  15:     serviceHost.BeginOpen((result) => serviceHost.EndOpen(result), null);
  16:  
  17:     btnSignIn.Enabled = false;
  18:     btnSignOut.Enabled = true;
  19:     btnDiscovery.Enabled = true;
  20:     ChangeTitle(txtUsername.Text);
  21: }

Nyní je naše služba spuštěna a zároveň viditelná na síti. Ted se podíváme jaké máme možnosti na straně klienta, který provádí hledání takto viditelných služeb.

Klient

Pro implementaci klienta existuje nová třída nazvaná DiscoveryClient. Když vytváříme instanci této třídy, do konstruktoru uvádíme objekt typu UdpDiscoveryEndpoint aby ten klient veděl na jaké endpointy má zasílat zprávy při hledání. Dále tato nová třída obsahuje dvě užitečné události:

  • FindProgressChanged – tato událost se vyvolá, vždy když klient nalezne nějakou službu. Při odchycení této služby získáme z argumentů objekt EndpointDiscoveryMetadata, který obsahuje informace o nalezené službě
  • FindCompleted – jakmile klient dokončí hledání služeb

Ještě před samotným voláním metody Find nebo FindAsync musíme uvést nějaké kritéria, podle kterých se bude hledat. Pro definici kritérií existuje opět nová třída FindCriteria, kde do konstrukturou uvedu typ Service Contractu (řeknu, že hledám pouze ty služby, které nabízí tenhle ServiceContract). Nakonec tedy provedu volání metody Find či FindAsync kde jako parametr předám právě vytvořené kritéria

   1: private void ADHocMode()
   2: {
   3:     // vytvorim clienta
   4:     DiscoveryClient discoveryClient = new DiscoveryClient(new UdpDiscoveryEndpoint());
   5:     discoveryClient.FindProgressChanged += (sender, args) =>
   6:         PopulateUsersList(args.EndpointDiscoveryMetadata);
   7:     discoveryClient.FindCompleted += (sender, args) =>
   8:         {
   9:             btnDiscovery.Enabled = true;
  10:             btnDiscovery.Text = "Discovery Users";
  11:         };
  12:  
  13:     // zadam podle jakych kriterii se bude hledat
  14:     FindCriteria findCriteria = new FindCriteria(typeof(GreetingService.IHello));
  15:  
  16:     // budu hledat asynchronne
  17:     discoveryClient.FindAsync(findCriteria);
  18:     btnDiscovery.Enabled = false;
  19:     btnDiscovery.Text = "Discovering users...";
  20: }

V obsluze události FindProgressChanged si můžete všimnout, že volám metodu PopulateUsersList, kde ovšem nedělám nic jiného, než že si z vlastnosti Address.Uri ziskám adresu nalezené služby a zobrazím v ListBoxu. Výsledek po hledání by mohl vypadat takto:

image

Poznámka: Adresa kterou zde vidíte je adresa jiného uživatele.

Konečně jsem se odhodlal k tomu abych všechny svoje prezentace umístil na jedno místo. Prohlédnout, případně stáhnout si je můžete na adrese http://cid-443d88037e0a0331.skydrive.live.com/self.aspx/Prezentace Jedná se především o prezentace, které jsem měl na VŠB TU-Ostrava

Více článků Další stránka »