Komunikacja PHP i C Sharp

Ostatnio musiałem napisać aplikację w której aplikacja .Net współpracuje z aplikacją napisaną w PHP za pośrednictwem gołego http.

Sprawa nie jest szczególnie złożona jednak jak się później okazało nie jest taka prosta jak myślałem. Fragmenty kodu zawarte w Blogu pochodzą z przykładowego programu dostępnego do pobrania na końcu postu.

To co było proste:

Komunikacja PHP z dowolną inną platformą odbywa się za pomocą protokołu HTTP który jest ogólnie dobrze opisany np. tutaj. Dodatkowo w większości platform istnieją dedykowane biblioteki obsługujące w mniejszym lub większym stopniu komunikację przez http, a biblioteka standardowa .Net nie jest pod tym względem wyjątkiem(tfu!).

Komunikację za pomocą http ułatwiają przynajmniej dwie klasy HttpWebRequest i WebClient. Klasa WebClient ma w moim mniemaniu znacznie bardziej intuicyjny interfejs i to jej będę się starał używać.

Wywołanie GET:

Wywołanie GET jest odwołaniem się do strony www równoważnym z wpisaniem adresu w przeglądarce internetowej, parametry do serwera wysyła się dopisując po adresie strony znak zapytania i wartości tych parametrów w formacie parametr1=wartosc1&parametr2=wartosc2&parametr3=wartosc3. Ograniczeniem tej metody jest fakt iż długość adresu strony wraz z wartościami parametrów nie może przekraczać 256znaków.

Metody tej nie powinno się także stosować na stronach w celu przekazywania danych wrażliwych (jak np. login i hasło użytkownika).

Do zbudowania łańcucha połączeniowego można użyć klasy UriBuilder, jest to klasa która na podstawie fragmentów adresu potrafi złożyć pełną ścieżkę do pliku/katalogu także na serwerze www.

Klasą która się przydaje jest także HttpUtility posiadająca metodę UrlEncode zwalniającą programistę z wstawiania odpowiednich znaków ucieczki w wysyłanym zapytaniu do serwera, niestety klasa ta znajduje się w przestrzeni nazw System.Web w bibliotece o tej samej nazwie, biblioteka ta zaś nie znajduje się w Wersji Client Profile platformy .Net. Tak więc jeżeli klasy tej chcemy użyć w projekcie Windows Forms lub WPF musimy dokonać następujących zmian w projekcie:

1) Zapisanie wszystkich niezapisanych plików.

2) W Solution Explorze klikamy prawym przyciskiem myszy na projekt i wybieramy Properties

3) W zakładce Application znajdujemy Combobox Target platform i wybieramy w nim .Net Framework 4.0

4) Zapisujemy wszysto i zamykamy właściwości projektu

5) Klikamy prawym przyciskiem w katalog refereces w projekcie i wybieramy AddReference

6) Przechodzimy na zakładkę .Net i wybieramy System.Web

7) Zatwierdzamy dialog i możemy używać klasy System.Web.HttpUtility

Najprostszy kod pobierający treść strony www może wyglądać tak.

WebClient client = new WebClient();
// Przy niektórych serwerach aplikacja powinna się przedstawić 
//client.Headers.Add("user-agent", "PHP and dotNet");

UriBuilder queryBuilder = new UriBuilder(tbServer.Text + "helloGet.php");
queryBuilder.Query = string.Format("name={0}", HttpUtility.UrlEncode(tbName.Text));

string result = client.DownloadString(queryBuilder.Uri);
MessageBox.Show(this, result);

plik helloGet.php zawiera chyba najprostrzą możliwą obsługę zapytania GET:

<?php
echo 'Witaj '.$_GET['name'];
?>

Pobranie plików z serwera:

Zadanie trochę mniej związane z PHP, aczkolwiek pojawiające się w pracy z http to pobranie pliku z serwera http. Tutaj klasa WebClient oferuje dwie możliwości.

Metoda DownloadFile podaje się adres pliku i nazwę pliku nic prostszego

using (SaveFileDialog sfd = new SaveFileDialog())
{
    sfd.Filter = "Pliki jpeg|*.jpg";
    if (sfd.ShowDialog() == System.Windows.Forms.DialogResult.OK)
    {
        WebClient client = new WebClient();
        client.DownloadFile("http://www.programy.witraze-plocin.pl/wp-content/uploads/2010/04/cycki226jo5.jpg", sfd.FileName);
    }
}

Metoda DownloadData zwraca tablicę bajtów składających się na plik metoda może być przydatna gdy pobrane dane chcemy tylko obrobić bez zapisywania ich na dysku. Np. wyświetlić obrazek w pictureBoxie.

WebClient client = new WebClient();
byte[] buffer = client.DownloadData("http://www.programy.witraze-plocin.pl/wp-content/uploads/2010/04/cycki226jo5.jpg");

MemoryStream stream = new MemoryStream(buffer);

// Kwestie techniczne
if (pbCycki.Image != null)
{
    Image oldImage = pbCycki.Image;
    pbCycki.Image = null;
    oldImage.Dispose();
}

pbCycki.Image = Image.FromStream(stream);

Wadą tej metody jest spore zajęcie pamięci ponieważ cały plik jest wgrywany do RAMu, dodatkowo aplikacja musi pobrać cały plik i dopiero wtedy go obrobić. Przypadkiem gdy żadna z powyższych metod nie zastosowania jest pobranie dużego pliku z serwera i zapisanie go do strumienia nie będącego plikiem(np. wysłać portem COM do jakiegoś urządzenia). W tym przypadku należy wykorzystać metodę WebRequest.

private void FromHttpToStream(Stream outStream, string url)
{
    WebClient client = new WebClient();
    client.OpenRead(url).CopyTo(outStream);
}

Wywołanie POST:

Wysłanie zapytania POST do serwera http przy pomocy klasy HttpClient jest dosyć niskopoziomowe. Ponieważ należy wykonać następujące kroki:

1) Dodać do nagłówka wiadomości informację o rodzaju przesyłanej treści "content-type: application/x-www-form-urlencoded"

2) Sformatować treść wysyłanej wiadomości przy UrlEncode

3) Zakodować informację przy pomocy kodowania ASCII i wysłać na serwer.

4) Rozkodować przesłaną wiadomość przy pomocy kodowania użytego na stronie.

WebClient client = new WebClient();
// Przy niektórych serwerach aplikacja powinna się przedstawić 
//client.Headers.Add("user-agent", "PHP and dotNet");

client.Headers.Add("Content-Type", "application/x-www-form-urlencoded");

byte[] buffer = Encoding.ASCII.GetBytes(string.Format("name={0}", HttpUtility.UrlEncode(tbName.Text)));
buffer = client.UploadData(tbServer.Text + "helloPost.php", "POST", buffer);
string result = Encoding.UTF8.GetString(buffer, 0, buffer.Length);
MessageBox.Show(this, result);

Upload plików:

Upload plików też jest dość prosty klasa WebClient posiada metodę UploadFile, która jako parametry pobiera nazwę wysyłanego pliku i adres pliku do którego wysyłana jest zawartość pliku.

using (OpenFileDialog ofd = new OpenFileDialog())
{
    ofd.Filter = "Pliki jpeg|*jpg";
    if (ofd.ShowDialog() == System.Windows.Forms.DialogResult.OK)
    {
        WebClient client = new WebClient();
        client.UploadFile(tbServer.Text + "uploadFile.php", ofd.FileName);
    }
}

Po stronie serwera wysyłany plik jest opisany w tabeli $_FILES w polu 'file' obsługa pliku. Przykładowa obsługa może wyglądać następująco:

<?php
$tempFile = $_FILES['file']['tmp_name'];
copy($tempFile, 'uploaded.jpg');
unlink($tempFile);
?>

Pliki z kodem przykładowego programu są do pobrania tutaj.

Zastosowanie LambdaEqualityComparer

Jakiś czas temu Maciej Aniserowicz na swoim blogu opublikował kod klasy LambdaEqualityComarer

Bardzo często chcielibyśmy aby równość elementów jakiejś klasy zależała tylko od jakiegoś pola (najczęściej od ID), aby to osiągnąć należy nadpisać metody GetHashcode i Equals tej klasy jest to jednak praca dosyć żmudna i powtarzalna, dodatkowo okazuje się że kod ten należy często pisać ponieważ wiele klas wywołuje te metody w celu porównania elementów, przykładem takich klas są kontrolki WPF zawierające Listy jak np. ComboBox. Aby pracę tą trochę skrócić można użyć następującego fragmentu kodu:

    class User
    {
        /// <summary>
        /// Identyfikator
        /// </summary>
        public int ID { get; set; }

        // Inne pola

        private LambdaEqualityComparer<User, int> _comparer = new LambdaEqualityComparer<User, int>(u => u.ID);

        public override int GetHashCode()
        {
            return _comparer.GetHashCode(this);
        }

        public override bool Equals(object obj)
        {
            return _comparer.Equals(this, obj as User);
        }
    }

Aby ten kod działał w 100% poprawnie należy jeszcze zmodyfikować kod Maćka tak aby uwzględniała fakt iż któryś z parametrów metody Equals może być nullem.

class LambdaEqualityComparer<T, TValue> : IEqualityComparer<T> 
        { 
            #region Private members 
            private Func<T, TValue> converter; 
            #endregion Private members 

            #region Constructors 
            public LambdaEqualityComparer(Func<T, TValue> converter) 
            { 
                this.converter = converter; 
            } 
            #endregion Constructors 

            #region IEqualityComparer<T> Members 
            public bool Equals(T x, T y) 
            { 
                if (x == null && y == null)
                {
                      return true;
                 } 
                  else if (x == null || y == null)
                 {
                         return false;
                 }
                  else
                  {
                         return converter(x).Equals(converter(y)); 
                  }
            }
            public int GetHashCode(T obj) 
            {
                if (obj == null)
               {
                       return 0;
                }
                else
                { 
                        return converter(obj).GetHashCode(); 
                 }
            }
            #endregion 
        } 

Na koniec dodam że implementację tą można rozbudować zgodnie z wzorcem dekoratora, co daje nam możliwość wykorzystania istniejących implementacji IEqualityComparer'a.

class LambdaEqualityComparer<T, TValue> : IEqualityComparer<T> 
        { 
            #region Private members 
            private Func<T, TValue> converter; 
            private IEqualityComparer<TValue> realComparer;
            #endregion Private members 

            #region Constructors 
           public LambdaEqualityComparer(Func<T, TValue> converter) : this(converter, null) { }

            public LambdaEqualityComparer(Func<T, TValue> converter, IEqualityComarer<TValue> realComparer) 
            { 
                this.converter = converter; 
                this.realComparer = realComparer; 
            } 
            #endregion Constructors 

            #region IEqualityComparer<T> Members 
            public bool Equals(T x, T y) 
            { 
                if (x == null && y == null)
                {
                      return true;
                 } 
                  else if (x == null || y == null)
                 {
                         return false;
                 }
                  else
                  {
                         if  (realComparer == null)
                         {
                                  return converter(x).Equals(converter(y)); 
                          }
                          else
                          {
                                   return realComparer.Equals(converter(x), converter(y));
                           }
                  }
            }
            public int GetHashCode(T obj) 
            {
                if (obj == null)
               {
                       return 0;
                }
                else
                {
                       if (realComparer == null)
                       { 
                              return converter(obj).GetHashCode(); 
                        }
                         else
                         {
                                  return realComparer.GetHashCode(converter(obj));
                          }
                 }
            }
            #endregion 
        } 

Niestandardowe generowanie kluczy głównych w NHibernate

Robiąc ostatnio mały projekt postanowiłem w końcu wykorzystać NHibernate, na temat tego ORM'a jest w necie masa informacji więc nie będę się na jego temat rozpisywał. Podobnie nie będę opisywał konfiguratora Fluent NHibernate, który jest znacznie bardziej intuicyjny niż tradycyjne pliki hbm (oparte na XML).

Problemem przed jakim stanąłem był fakt że moja aplikacja miała współpracować z bazą na której działała inna aplikacja, posiadająca dosyć charakterystyczną politykę generowania kluczy głównych. Która nijak nie pasowała do żadnej standardowej polityki zawartej standardowo w Bibliotece hibernate'a.

Na szczęście NHibernate jest przygotowany na taką ewentualność, jego API zawiera interfejs IIDentifierGenerator, który umożliwia pisanie własnych polityk generowania kluczy. Interfejs ten zawiera jedną metodę Generate.

public class MyGenerator:IIDentiferGenerator
{
    public object Generate(ISessionImplementor session, Object obj)
   {
        return Guid.NewGuid()
    }
}

Pierwszy parametr zapewnia dostęp do obiektów związanych z konfiguracją połączenia z bazą danych oraz stanem tego połączenia, drugim parametrem jest obiekt dla którego generowany jest klucz. Przykład powyżej dla każdego obiektu zwróci wartość statycznej metody NewGuid która nadaje się na klucz główny ponieważ z definicji jest to Global Unique Identifier. Jednak generowanie nowego Guida nie jest niczym nadzwyczajnym po za tym biblioteka NHibernate'a już ją zawiera.

Częstym sposobem generowania kluczy głównych jest procedura składowana (lub fragment zapytania SQL) która zwraca najniższą nieużywaną wartość którą aplikacja może wykorzystać jako klucz dla danej tabeli.

Aby uruchomić takie zapytanie pod kontrolą NHibernate należy wykorzystać właściwości Connection i Batcher parametru session przekazanego do metody generate.

public class MyGenerator:IIDentiferGenerator
{
    public object Generate(ISessionImplementor session, Object obj)
   {
        using(IDbCommand command = session.Connection.GenerateCommand())
        {
            command.CommandText = "exec GetNextId()";
            using(IDataReader  reader = session.Batcher.ExecuteReader())
            {
                if (reader.Read())
                {
                     return reader.GetInt(0);
                }
                else
                {
                      throw new Exception ("Nie da się wygenerować klucza.");
                }
            }
        }
    }
}

Klasa przedstawiona powyżej jedną poważną wadę: jest bardzo sztywna tj. trudno byłoby ją wykorzystać do generowania klucza dla dowolnej encji. Informację na temat tabeli w której należy sprawdzać "Zajętość" danego identyfikatora można uzyskać tylko przez weryfikację typu parametru obj metody Generate. Wykorzystanie tego faktu sprawiłoby to że programista musiałby dla każdego obsługiwanego typu dopisywać kilka linijek kodu w tej metodzie co generowałoby masę powtarzalnego kodu.

Znacznie lepszym rozwiązaniem jest Implementacja Interfejsu IConfigurable zawierającego metodę Configure która przyjmuje jako parametry następujące wartości:

type – typ dla jakiego jest konfigurowana klasa

params – Słownik klucz <=> wartość przechowujący dodatkowe dane konfiguracyjne.

d – informacja na temat silnika bazodanowego przechowującego dane.

Parametrem nas interesującym jest params zawiera on dane na temat tabeli w której zapisywany jest dany typ oraz kolumny będącej kluczem głównym. Nie jestem pewien czy dla każdego silnika bazodanowego nazwy kluczy są takie same dlatego nie podaję tutaj konkretnego fragmentu kodu.

Pozostaje jeszcze tylko kwestia jak powiedzieć NHibernate'owi żeby dla danej klasy używał naszego generatora. We FluentHibernate wymaga to wpisania jednej linijki w klasie Mapującej:

Id(obj =&gt; obj.ID).GeneratedBy().Custom&lt;MyGenerator&gt;()

Edit: 02-10-2010

Podczas prac w NHibernate zauważyłem że zachowuje się on dosyć specyficznie podczas zapisu więcej niż jednego obiektu jedego typu w ciągu jednej tranzakcji. Otóż najpierw generuje on klucze główne dla wszystkich nowych obiektów, a następnie dokonuje ich zapisu. Jeżeli teraz oprzemy nasz mechanizm zapisu na zapytaniu do bazy to musimy się liczyć z faktem iż za każdym razem generowanie klucza będzie się odbywać na niezmienionej bazie. Więc zapytanie wywołane za każdym razem zwróci tą samą wartość. Należy to uwzględnić w kodzie, najczęściej przez duplikację części logiki zapytania, co jest oczywistą wadą tego rozwiązania, zaletą jego zaś jest mniejsza ilość zapytań co zazwyczaj poprawia aplikację pod względem wydajnosciowym.