Spis treści:

Kategoria:C#Windows Communication Foundation


Dziedziczenie w WCF

Czy da się serializować interfejsy?

Temat dzisiejszego wpisu pojawił się po przeczytaniu jednego z komentarzy pod artykułem WCF i konfiguracja połączenia TCP/IP. Pytanie nie miało wiele wspólnego z konfiguracją TCP/IP - dotyczyło WCF jako całości. Przypomnę zaprezentowany w komentarzu kod i pytanie związane z możliwością serializowania interfejsów:

public class Nosnik
{
    List<IDane> lista = new List<IDane>();
    public Nosnik()
    {
        lista.Add(new Dane1());
        lista.Add(new Dane2());
    }
}
public interface IDane
{
    string dana { get; }
}
public class Dane1 : IDane
{
    private string _dana = "Jestem obiektem klasy Dane1";
    public string dana
    {
        get { return _dana; }
    }
}
public class Dane2 : IDane
{
    private string _dana = "Jestem obiektem klasy Dane2";
    public string dana
    {
        get { return _dana; }
    }
}

Pytanie brzmiało: Czy taki twór można serializować? Zgodnie z założeniem, iż lista w obiekcie klasy Nosnik zawierała (przeniosła) obiekty dziedziczące po interfejsie.

Domyslam się, że wszyscy czekają na odpowiedź. Odpowiem więc: da się. Użyłem tu skrótu myślowego - dlaczego? O tym za chwilę.

Czy interfejs przetrzymuje dane?

Interfejs jest, oczywiście, pojęciem logicznym, pewną koncepcją. Określa on, co powinny mieć wszystkie klasy, które chcą się nim szczycić. Tak jak nie da się utworzyć insterfejsu, tak nie da się go fizycznie serializować. Tyle w kwestii wyjaśnienia wspomnianego skrótu myślowego. WCF tak prawdę mówiąc serializuje obiekty, które te interfejsy implementują. Serializacja różnych typów nie jest trudna - to samo wykonywane było przez serializatory binarne i xml w .NET jeszcze zanim pojawił sie i rozkwitł WCF. Problem leży bardziej po stronie odbiorcy usług. Skąd klient ma wiedzieć, czy IDane jest obiektem Dane1? Skąd ma wiedzieć, czy klasa Dane1 nie zawiera, oprócz wymaganej właściwości interfejsu, innych właściwości? W zaprezentowanym przykładzie nie ma, ale mieć może. Klasy Dane1 i Dane2 oprócz jednej właściwości wspólnej definiowanej przez interfejs mogą być całkowicie rozbieżne.

Klient musi zamienić dane otrzymane przez sieć na coś, co da się wyrazić przy pomocy typów CLR. Najczęściej typ da się określić bezpośrednio z kontraktu. Jeżeli wszystko się zgadza, nie ma problemu. Jeżeli przesyłamy obiekt, który dziedziczy z typu wymienionego w kontrakcie, jest problem. Problem pojawia się także w przypadku interfejsów. Interfejs nie ma danych, więc nie ma problemu, ale pod interfejsem musi się znajdować jakiś fizyczny obiekt, na który ten interfejs wskazuje (lub null, ale nie po to przesyłamy dane, żeby cieszyć się wartością null po drugiej stronie). Podobnych zależności dziedziczenia może być więcej i WCF musi sobie jakoś z nimi radzić.

Odpowiedzią na wyżej wymienione potrzeby jest atrybut KnownType.

Typy znane i nieznane

Jak wspomniałem, gdy WCF otrzymuje komunikat, próbuje zamienić przychodzące dane na obiekt. W pierwszej kolejności próbuje dopasować dane do definicji wyciągniętej z kontraktu. W przykładzie mamy interfejs, więc nic z tego. Co taki serializator robi dalej? Mógłby usiąść w kącie i zapłakać, ale jeszcze nie w tym momencie. Próbuje on znaleźć typy, które są kompatybilne z otrzymanymi wartościami. Rozsądne wydaje się, że nie próbuje on szukać tych typów wszędzie, tj. we wszystkich bibliotekach, które są wczytane do tej samej przestrzenu adresowej. Tu rzecz najważniejsza: to my podajemy typy, które deserializator powinien rozważyć. Takie typy stają się dla serializatora znane (ang. known type - znany typ). Najłatwiejszym sposobem wskazania typów-kandydatów do rozważenia przez serializator jest umieszczenie atrybutu [KnownType] nad kontraktem tej klasy, która przenosi obiekty będące w relacji dziedziczenia.

Przejdźmy zatem do najważniejszej części, do kodu.

Struktura danych WCF z relacją dziedziczenia

Napisałem już dużo, więc przejdźmy od razu do używanych w przykładzie klas. Pokazano je na poniższym listingu:

//Klasy interfejsu WCF public interface IData
{
    string Name { getset; }
}

[DataContract]
public class Data1 : IData
{
    private string _name = "Hello from Data1";
    [DataMember]
    public string Name
    {
        get { return _name; }
        set { _name = value; }
    }
}

[DataContract]
public class Data2 : IData
{
    private string _name = "Hello from Data2";
    [DataMember]
    public string Name
    {
        get { return _name; }
        set { _name = value; }
    }
}

[DataContract]
[KnownType(typeof(Data1))]
[KnownType(typeof(Data2))]
public class Container
{
    [DataMember]
    public List<IData> Items { getset; }

    public Container()
    {
        Items = new List<IData>();
        Items.Add(new Data1());
        Items.Add(new Data2());
    }
}

[ServiceContract]
public interface IContainerSvc
{
    [OperationContract]
    Container GetSomeData();
}

Powyżej przedstawiono prostą hierarchię dziedziczenia. Gdyby nie WCF, to nie byłoby o czym rozmawiać. Tu natomiast warto zwrócić uwagę na umieszczenie atrybutów KnownType. Serializator podczas pobierania struktury Container, a w szczególności elementów listy tej struktury, będzie podejmował próbę dopasowania danych do typów Data1 i Data2.

Implementacja usługi i serwera

Przyjrzyjmy się teraz implementacji usługi (ang. service) oraz serwera:

//Implementacja metod interfejsu WCF
public class ContainerSvc : IContainerSvc
{
    public Container GetSomeData()
    {
        return new Container();
    }
}

//Program serwera
static void Main(string[] args)
{
    ServiceHost host = new ServiceHost(typeof(ContainerSvc));
    var wsBinding = new BasicHttpBinding();
    host.AddServiceEndpoint(typeof(IContainerSvc),
        wsBinding, "http://localhost:8000/ContainerSvc");
    host.Open();
    Console.WriteLine("Listening...");
    Console.ReadLine();
    host.Close();
}

Nie ma żadnego zaskoczenia - dla tych, którzy czytali poprzednie artykuły wszystko powinno być jasne. Jeżeli ktoś poćwiczył z prostymi konfiguracjami WCF, zobaczy, że nie ma tutaj niczego nowego. Można, co naturalne, konfigurować WCF przy pomocy plików konfiguracyjnych (), ale dla czytelności przykładu zastosowałem sztywną konfigurację w kodzie. Przejdźmy zatem dalej.

Kod klienta

Popatrzmy jeszcze na końcowy fragment zaprezentowany na poniższym listingu:

static void Main(string[] args)
{
    var binding = new BasicHttpBinding();
    ChannelFactory<IContainerSvc> factory = new ChannelFactory<IContainerSvc>(binding);
    EndpointAddress address = new EndpointAddress("http://localhost:8000/ContainerSvc");
    IContainerSvc container = factory.CreateChannel(address);
    var list = container.GetSomeData();
    foreach (var item in list.Items)
        Console.WriteLine(item.Name);
}

Wykonanie kodu klienta spowoduje wypisanie na ekranie dwóch wartości:

Hello from Data1
Hello from Data2

To jest z pewnością to, o co chodziło. Podobnie można postępować w przypadku klasy bazowej. Jeżeli w definicji interfejsu WCF znajduje się klasa bazowa, a przesyłamy klasy pochodne, to należy umieścić atrybuty [KnownType] przy klasie bazowej, wskazując wszystkie klasy dziedziczące, które mogą być przesyłane.

Jak zwykle zachęcam do eksperymentowania - po jakimś czasie wszystko wyda się jasne i oczywiste.

Kategoria:C#Windows Communication Foundation

, 2013-12-20

Brak komentarzy - bądź pierwszy

Dodaj komentarz
Wyślij
Ostatnie komentarze
Dzieki za te informacje. były kluczowe
Dobrze wyjaśnione, dzięki !
a z innej strony - co gdybym ciąg znaków chciał mieć rozbity nie na wiersze a na kolumny? Czyli ciąg ABCD: 1. kolumna: A, 2. kolumna: B, 3. kolumna: C, 4 kolumna: D?
Ciekawy artykuł.
Czy można za pomocą EF wysłać swoje zapytanie?
Czy lepiej do tego użyć ADO.net i DataTable?