niedziela, 27 października 2013

JavaScript Intellisense w VS 2012

Ostatnimi czasy, miałem okazję uczestniczyć (jako widz), w dwóch wykładach prowadzonych przez Jakuba Gutowskiego o wdzięcznym tytule "Javascript dla c# developera". Pierwsza prezentacja odbyła się 12 października podczas konferencji "on-line" dotnetconf.pl (można ją obejrzeć na youtube.com). Druga odbyła się 17 października podczas 69 spotkania wg.net (Warszawska Grupa .NET). (materialy ze spotkania)
Generalnie Gutek pokazał kilka ciekawostek związanych z językiem javascript, oraz zachęcał do zapoznania się i stosowania kilku narzędzi. Zanim jednak przejdziemy do esencji tego, o czym mówił i co chciał nas nauczyć, najpierw mała przekąska, tzw. "snack", czyli repost z bloga Gutka, nt. włączenia intelisense dla języka javascript w Visual Studio 2012 link do blogu Gutka jak włączyć intelisense w VS 2012.


p.s. Gdyby kiedyś coś się stało z blogiem Gutka, to należy utworzyć w solucji katalog przeznaczony dla plików javascript i dodać tam plik plik.js, a nast. wejść do Narzędzia->Opcje->Edytor Tekstu->Javascript->IntelliSense->Odwołania-> ustawić grupę odwołań (np. Implict (Web)), a nast. wpisać ścieżkę względną do naszego pliku i go 'Dodać'.  Od tego momentu, mamy działające intellisense dla naszych plików js :)

NLog, czyli logowanie błędów w .NET

Jakiś czas temu, opisywałem na moim blogu bardzo popularny logger dla .NET, o nazwie log4net. Sam log4net jest bardzo ok, i w zasadzie całkiem nieźle się sprawdza w swojej roli, jednak jak to mówią "lepsze jest wrogiem dobrego", a tym lepszym, a przynajmniej wygodniejszym narzędziem jest nlog.

Główne różnice, wady/zalety?
- NLog jest wygodniejszy. Co musimy zrobić? Ddokładamy bibliotekę do projektu,  za pomocą Packer Menager Console pobieramy plik konfiguracyjny, odkomentowywujemy ustawienia i... działa. W przypadku log4net trzeba pamiętać o sekcji inicjalizacji w 'global.asax' oraz odpowiednim ustawianiu pliku konfiguracyjnego
- zaawansowana konfiguracja -> tutaj akurat nie miałem okazji ostro sprawdzać obu narzędzi, jednak czytając różne blogi, zazwyczaj ludzie chwalą sobie intuicyjność oraz bardziej elastyczną konfigurację NLoga
- aktualizacje -> ost. aktualizacja nlog wyszła 09.10.2013, i jest w miarę często aktualizowany (projekt żyje), natomiast odnośnie log4net spotkałem się z opiniami, że ost. aktualizacja była ponad 5 lat temu
- autor nLog'a jkowalski.com na jednym ze spotkań Warszawskiej grupy .NET wykazywał niewielkią, ale jednak wyższą wydajność swojego rozwiązania nad rozwiązaniem log4net (i zdecydowanie wyższą nad standardowym System.Diagnostics).

Osobiście uważam, że nlog jest zwyczajnie wygodniejszy, oraz "nowszy", przez co niewiele, ale jednak lepszy niż log4net.

Całkiem fajnie opisał ten temat Maciej 'procent' Aniserowicz na swoim blogu.
Dobry post z porównaniem jest też na stackoverflow.com - log4net vs nlog
Opis NLog na codeproject.com


p.s. przykładowa konfiguracja, którą stoduje w klasie Bootstrapper projektu Nancy:
        protected override void ApplicationStartup(TinyIoCContainer container, IPipelines pipelines)
        {
            base.ApplicationStartup(container, pipelines);
            #region event logger (nLog)
            FileTarget fileTarget = CreateNlogFileConfiguration();
            SimpleConfigurator.ConfigureForTargetLogging(fileTarget, LogLevel.Info);
            LogAllRequests(pipelines);
            LogAllResponseCodes(pipelines);
            LogUnhandledExceptions(pipelines);
            #endregion
        }
        private static FileTarget CreateNlogFileConfiguration()
        {
            //SimpleConfigurator.ConfigureForTargetLogging(new AsyncTargetWrapper(new EventLogTarget()));
            FileTarget fileTarget = new FileTarget();
            fileTarget.Name = "CeidgCheckLogger";
            fileTarget.CreateDirs = true;
            fileTarget.Layout = "${date} ${level} ${callsite:className=true:includeSourcePath=false:methodName=true} ${message}";
            //fileTarget.FileName = @"${basedir}/logs/CeidgCheckNancy.${date:format=yyyy.MM.dd}.log"; //for catalog in solition
            fileTarget.FileName = @"D:/logs/CeidgCheckNancy/CeidgCheckNancy.${date:format=yyyy.MM.dd}.log";
            fileTarget.KeepFileOpen = false;
            fileTarget.Encoding = Encoding.UTF8;
            return fileTarget;
        }
        private void LogAllRequests(IPipelines pipelines)
        {
            pipelines.BeforeRequest += ctx =>
            {
                log.Info("Handling request {0} \"{1}\"",
                ctx.Request.Method, ctx.Request.Path);
                return null;
            };
        }
        private void LogAllResponseCodes(IPipelines pipelines)
        {
            pipelines.AfterRequest += ctx =>
            log.Info("Responding {0} to {1} \"{2}\"",
            ctx.Response.StatusCode, ctx.Request.Method,
            ctx.Request.Path);
        }
        private void LogUnhandledExceptions(IPipelines pipelines)
        {
            pipelines.OnError.AddItemToStartOfPipeline
            ((ctx, err) =>
            {
            log.ErrorException(string.Format("Request {0}\"{1}\" failed", ctx.Request.Method, ctx.Request.Path), err);
            return null;
            });
        }

czwartek, 24 października 2013

.NET Client consuming Java IBM WebSphere Web Service

W dwóch moich poprzednich postach opisywałem sposób podłączenia się aplikacji .NET do web serwisów napisanych w PHP z pomocą Zend Framework (link1 link 2 ) i związanymi z tym problemami. W tym wpisie skupie się na podłączeniu się do web service utworzonego za pomocą IBM WebSphere wykorzystującym standardy technologii web services napisanych dla środowiska J2EE (tzw. "Java korporacyjna").

Do poprawnego 'skonsumowania' serwisu używamy standardowego mechanizmu WCF poprzez "Add Service Reference". Po wpisaniu adresu serwisu (z końcówką ?wsdl) powinniśmy wykryć serwis, a nast. utworzyć na jego podstawie klasę proxy. Gdyby automat nie wykrył usługi, to należy wpisać adres usługi do przeglądarki, a nast. zapisać plik na dysk z rozszerzeniem .xml (i w Add Service Reference podać ścieżkę na dysku gdzie zapisaliśmy ten plik). W przypadku mojej usługi, konieczne było jeszcze zgranie pliku .xsd (na szczęście znajdował się pod tym samym adresem co serwis, tylko z innym rozszerzeniem).

W momencie, gdy mamy utworzoną klasę proxy, możemy spróbować ją wywołać. Niestety w moim przypadku, standardowe wywołanie klasy proxy oznaczało błąd:
„Atrybut System.Xml.Serialization.XmlAttributeAttribute obiektu XmlSerializer nie jest prawidłowy w parametrXYZ. Kiedy atrybut IsWrapped ma wartość true, obsługiwane są tylko atrybuty XmlElement, XmlArray, XmlArrayItem i XmlAnyElement.”
Poprosiłem więc o pomoc bardziej doświadczone osoby i one zasugerowały mi aby,  w miejscu, w którym chcemy wywołać usługę dodać przestrzeń nazw:
using System.ServiceModel
 a w kodzie aplikacji EndPoint:
EndpointAddress ea = new EndpointAddress("https://abcd/hash/ws/xyz");
WS ws = new WSClient(new BasicHttpBinding(BasicHttpSecurityMode.Transport), ea);
Posiadając tak wygenerowaną klasę proxy, zasugerowano mi wejść w obiekt "WS" ("go to definition") i tam sprawdzić klasy, które wykorzystują 'parametrXYZ'.
Okazało się, że faktycznie, były tam dwie klasy, które wykorzystywały taki parametr. Rozwiązaniem tego problemu okazało się przemodelowanie tych klas, poprzez implementację System.ComponentModel.INotifyPropertyChanged czyli dodanie i obsługę zdarzenia  
 public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
oraz dodanie zmiennych prywatnych każdemu odpowiadającemu mu publicznemu propertisowi, a także usunięcie atrybutu [System.ServiceModel.MessageBodyMemberAttribute] dla tych propetrtisów.
Przykład poprawnie poprawionej klasy:

    [System.Diagnostics.DebuggerStepThroughAttribute()]
    [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")]
    [System.ServiceModel.MessageContractAttribute(WrapperName="parametr-XYZ", WrapperNamespace="http://xyz.pl/ab/xyz", IsWrapped=true)]
    public partial class xyzRequest : object, System.ComponentModel.INotifyPropertyChanged {

        private string parametrXYZField;
        [System.Xml.Serialization.XmlAttributeAttribute("parametr-XYZ")]
        public string parametrXYZ
        {
            get
            {
                return this.parametrXYZField;
            }
            set
            {
                this.parametrXYZField= value;
                this.RaisePropertyChanged("parametrXYZ");
            }
        }
       
        public xyzRequest()
        {
        }

        public xyzRequest(string parametrXYZ)
        {
            this.parametrXYZField = parametrXYZ;
        }
        public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
        protected void RaisePropertyChanged(string propertyName)
        {
            System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged;
            if ((propertyChanged != null))
            {
                propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
            }
        }
    }
Czynność taką powtarzamy dla każdej klasy (i propertisu w tej klasie), w której występuje problematyczny dla nas parametr "parametrXYZ". W moim konkretnym przypadku to wystarczyło, a wywołanie usługi nast. poprzez:
string fuu = "Bar";
var raport = ws.pobranieraportu(fuu);

Korzystanie z web serwisów napisanych dla J2EE w .NET może nieść za sobą inne problemy np:
„Element XML 'rezultat-xyz' z obszaru nazw 'http://xyz.pl/ab/xyz' odwołuje się do metody i typu. Zmień nazwę komunikatu metody, używając atrybutu WebMethodAttribute, lub zmień element główny typu, używając atrybutu XmlRootAttribute.”
Powodem błędu jest zastosowanie dla kilku operacji: takiej samej nazwy typu zwracanego, którą jest „rezultat-xyz”. Technologia C#/.NET przewiduje obecność tylko unikalnych nazw elementów zwracanych przez poszczególne operacje. 
W celu wyeliminowania błędu niezbędna jest ręczna modyfikacja automatycznie wygenerowanej klasy Proxy. Modyfikacja polega na nadaniu unikalnych nazw elementów zwracanych przez wszystkie operacje dostępne w WebService, których nazwy elementów zwracanych kolidują ze sobą.

Inne problemy mogą wynikać z niestarannie napisanego XSD, lub innych, trudnych do zidentyfikowania powodów.



środa, 23 października 2013

SQL DateTime i DateTime2

Całkiem niedawno, tworząc insert do bazy danych wygenerowanej przez EF spotkałem się z błędem odnośnie parsowania dat z 'datetime2' na 'datetime'. Z tego co zrozumiałem, chodziło o brak inicjacji zmiennej (bo jak wiadomo, .NET Datetime ma domyślą wartość roku 0, a T-SQL Datetime zaczyna się od 1753 roku). Zainicjowanie tej zmiennej na "DateTime.Now();" rozwiązało ten problem, za to, krótkie spojrzenie na forum stackoverflow.com wyjaśnia nam dodatkowe niuanse pomiędzy tymi typami, taki jak np. precyzja dokładności "3 1/3 milliseconds" do "100ns".

Link do do zgłoszenia na stackoverflow

piątek, 18 października 2013

Rozszyfrowywanie CAPTCHA z pomocą zewnętrznego (płatnego) klienta

CAPTCHA, to rodzaj techniki stosowanej jako zabezpieczenie na stronach www, celem której jest dopuszczenie do przesłania danych tylko wypełnionych przez człowieka link do wikipedii.

Najczęściej jest to rysunek, który wymaga od użytkownika własnoręcznego wpisania, tworzony w taki sposób, aby rozwiązanie tego sposobem mechanicznym było maksymalnie skomplikowane. Same algorytmy rozwiązywania captcha są skomplikowane i bazują na algorytmach szyfrowania i deszyfrowania. Na szczęście, nie musimy się znać na wszystkim i można te zadanie zlecić komuś innemu, kto się na tym zna dużo lepiej niż my. Przykładem firmy, która się specjalizuje w rozwiązywania zabezpieczeń typu CAPTCHA jest deathbycaptcha.

Najnowsza wersja (4.2) posiada bibliotekę przygotowaną dla języków:
-.NET (C#, Visual Basic)
- AutoIt3
- iMacros
- C (client and libs' source code)
- Java
- Perl 5+
- PHP v5+
- Python v2.5+ and v3.0+
- Command-line tool for Windows, Linux (i386 and x86-64) (see usage note below)
- Plugin for MyAdTools bots

oraz klienta firm 3 (nie wspierane przez producenta)
- Ruby

Producent udostępnia przykład wykorzystania API w językach:
- PHP
- C#

Osobiście wybrałem przykład działający w C# i działa tak, jak należy, tzn. do projektu dodajemy bibliotekę .dll, w wykonywanym pliku .cs dokładamy nową przestrzeń nazw, za pomocą której tworzymy obiekt. Na obiekcie wywołujemy przeciążoną metodę .Decode (captcha możemy podać, np. jako tablicę bajtów) i... to wszystko :)
Dodatkowo mamy metodę GetBalance(), która sprawdza bilans naszego konta (czyli w razie potrzeby możemy napisać automat przypominający nam o tym, że kończą się pieniądze przeznaczone na rozwiązywanie captcha).


Ile to kosztuje?
1k Captcha - 1.390 dolara, co przy aktualnym kursie dolara daje nam ~ 4.22zł za każde rozwiązane 1k CAPTCHA, czyli nie jest to jakiś nie wiadomo jaki majątek.

poniedziałek, 14 października 2013

.NET Client consuming PHP ZendFramework Web Service - solution no.2 (HttpWebRequest)

Dostałem za zadanie zintegrowanie się z kilkoma web serwisami. Naturalnym rozwiązaniem było wybranie do tego celu technologii WPF, co opisałem we wcześniejszym wpisie. Na szczęście, platforma .NET pozwala wykonywać wiele zdań na wiele, różnorodnych sposobów. W tym wpisie, przedstawię inny sposób odpytania web serwisu, tj. za pomocą klasy HttpWebRequest

W przypadku korzystania z klasy HttpWebRequest, całość zapytania SOAP, jakie wykonamy musimy napisać samemu w stringu, a nast. odpytać serwis za pomocą metody POST.
Odpowiedź dostajemy w strumieniu bajtów, który następnie zamieniamy na XmlDocument

        public string GetMigDZiDByPost()
        {
            try
            {
                WebRequest webRequest = WebRequest.Create("http://xxx.yyy.zzz/Dzsoapserver/queryko");

                HttpWebRequest httpRequest = (HttpWebRequest)webRequest;
                httpRequest.Method = "POST";
                httpRequest.ContentType = "text/xml";
                httpRequest.Headers.Add("SOAPAction: http://xxx.yyy.zzz/Dzsoapserver/queryko");
                Stream requestStream = httpRequest.GetRequestStream();

                //Create Stream and Complete Request
                StreamWriter streamWriter = new StreamWriter(requestStream);
                streamWriter.Write(this.GetSoapString());
                streamWriter.Close();

                //Get the Response
                WebResponse webResponse = httpRequest.GetResponse();
                Stream responseStream = webResponse.GetResponseStream();
                StreamReader streamReader = new StreamReader(responseStream);

                //Read the response into an xml document
                System.Xml.XmlDocument soapResonseXMLDocument = new System.Xml.XmlDocument();
                soapResonseXMLDocument.LoadXml(streamReader.ReadToEnd());

                //return only the xml representing the response details (inner request)
                string test = soapResonseXMLDocument.GetElementsByTagName("organisation")[0].InnerXml;
                return test;
            }
            catch (Exception ex)
            {
                //error 500 mean rekord not found
                return null;
            }
        }
        private string GetSoapString()
        {
            string hash = Constraints.MIGDZ.MigDzHashBase64;
            StringBuilder sb = new StringBuilder();
            sb.Append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
            sb.Append("<SOAP-ENV:Envelope SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\" xmlns:SOAP-ENC=\"http://schemas.xmlsoap.org/soap/encoding/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:ns1=\"http://xxx.yyy.zzz/Dzsoapserver/queryko\" xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\"><SOAP-ENV:Body>");
            sb.Append("<ns1:getParcel><parameters xsi:type=\"ns1:Logic_DzSoapServer_Request_BodyKo\"><sy xsi:type=\"xsd:string\">12345678</sy></parameters><hashCode xsi:type=\"xsd:string\">abcdefg0123456VUZ123ABCD01a=</hashCode></ns1:getParcel></SOAP-ENV:Body></SOAP-ENV:Envelope>");

            return sb.ToString();
        }

piątek, 11 października 2013

.NET Client consuming PHP ZendFramework Web Service - solution no.1 (WPF)

 Dostałem za zadanie zintegrowanie się z kilkoma web servisami. Naturalnym rozwiązaniem było wybranie do tego celu technologii WPF, szczególnie, że korzystanie z jego poprzednika zwanego potocznie "(SOAP) Web Services" jest niezalecane przez firmę Microsoft od 2009r:
 W książce "Professional-ASP-NET-4-5-in-C-and-VB.productCd-1118311825.html" jest napisane:

In 2009, ASMX Web Services were marked as legacy. Therefore, the code found within will notbe updated with future ASP.NET releases (the exception would be a security update).

 O ile zintegrowanie się z serwisami wystawionymi przez inne WCF poszło gładko, to pewne problemy napotkałem podczas integracji z web serwisem napisanym w PHP przy pomocy Zend Framework. Początkowo chciałem wykonać standardową operację połączenia, która powinna wyglądać tak samo, jak w przypadku korzystania z serwisu napisanego w WCF (link do artykułu, w którym programista łączy się z php za pomocą wcf). Niestety, w moim przypadku, WCF wykrył usługę, ale wyrzucił błąd:

There was an error downloading 'http://xxx.yyy.zzz.pl/Swdsoapserver/query?wsdl/_vti_bin/ListData.svc/$metadata'.
The request failed with the error message:
--
<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Body><SOAP-ENV:Fault><faultcode>Sender</faultcode><faultstring>Invalid XML</faultstring></SOAP-ENV:Fault></SOAP-ENV:Body></SOAP-ENV:Envelope>

--.
Metadata contains a reference that cannot be resolved: 'http://xxx.yyy.zzz.pl/Swdsoapserver/query?wsdl'.
The content type text/html; charset=UTF-8 of the response message does not match the content type of the binding (application/soap+xml; charset=utf-8). If using a custom encoder, be sure that the IsContentTypeSupported method is implemented properly. The first 1024 bytes of the response were: '<?xml version="1.0" encoding="UTF-8"?>
<wsdl:definitions xmlns="http://schemas.xmlsoap.org/wsdl/"
    xmlns:tns="http://xxx.yyy.zzz.pl/Swdsoapserver/query" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap-enc="http://schemas.xmlsoap.org/soap/encoding/"
    xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" name="SwdQuerySoap"
    targetNamespace="http://xxx.yyy.zzz.pl/Swdsoapserver/query">
    <wsdl:types>
        <xsd:schema 
 Jak zwykle w takich przypadkach nieodzowne okazało się forum stackoverflow, gdzie zadałem pytanie: "consuming-php-web-service-by-wcf-client".
 Dzięki otrzymanej tam pomocy, zapisałem plik .wsdl na dysku i za jego pomocą utworzyłem klasy proxy (Add Service Reference, tylko zamiast linku, wskazujemy lokalizację pliku na dysku).

Tak wygenerowane klasy proxy potrafiły się połączyć i wykonać zapytanie, ale miały problemy z odczytaniem odpowiedzi. Błędy były dwa:

a) Wielkość przesyłanej odpowiedzi była większa, niż długość standardowej odpowiedzi
"Maksymalny przydział rozmiaru wiadomości dla wiadomości przychodzących (65536) został przekroczony. Aby zwiększyć przydział, użyj właściwości MaxReceivedMessageSize we właściwym elemencie wiązania."
Obejście tego problemu jest stosunkowo proste i polega na modyfikacji ustawień powiązania (ang. binding) wewnątrz pliku web.config/app.config naszej aplikacji:

        <binding name="Logic_DzSoapServer_QueryKoBinding" maxBufferSize="2147483647"
          maxReceivedMessageSize="2147483647">
          <readerQuotas maxDepth="2147483647" maxStringContentLength="2147483647"
            maxArrayLength="2147483647" maxBytesPerRead="2147483647" maxNameTableCharCount="2147483647" />
        </binding>
b) Problem mappera z parsowaniem odpowiedzi wysyłanej przez PHP na obiekty .NET
"Nie można przypisać obiektu typu System.Int32 do obiektu typu System.Int32[]."
Sposobem aby to obejść okazała się ręczna modyfikacja wygenerowanych klas proxy, aby zamiast spodziewanego typu odpowiedzi, odpowiadały typem 'object':

        public object getParcel(CreditRiskEngine.ko.Logic_DzSoapServer_Request_BodyKo parameters, string hashCode) {
            return base.Channel.getParcel(parameters, hashCode);
        }

        public object getParcelAsync(CreditRiskEngine.ko.Logic_DzSoapServer_Request_BodyKo parameters, string hashCode)
        {
            return base.Channel.getParcelAsync(parameters, hashCode);
        }

     [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")]
    [System.ServiceModel.ServiceContractAttribute(Namespace="http://xxx.yyy.zzz.pl/Dzsoapserver/queryko", ConfigurationName="ko.Logic_DzSoapServer_QueryKoPort")]
    public interface Logic_DzSoapServer_QueryKoPort {
      
        [System.ServiceModel.OperationContractAttribute(Action="http://xxx.yyy.zzz.pl/Dzsoapserver/queryko#getParcel", ReplyAction="*")]
    [System.ServiceModel.XmlSerializerFormatAttribute(Style=System.ServiceModel.OperationFormatStyle.Rpc, SupportFaults=true, Use=System.ServiceModel.OperationFormatUse.Encoded)]
        [return: System.ServiceModel.MessageParameterAttribute(Name="return")]
        object getParcel(CreditRiskEngine.ko.Logic_DzSoapServer_Request_BodyKo parameters, string hashCode);
      
        [System.ServiceModel.OperationContractAttribute(Action="http://xxx.yyy.zzz.pl/Dzsoapserver/queryko#getParcel", ReplyAction="*")]
        [return: System.ServiceModel.MessageParameterAttribute(Name="return")]
        object getParcelAsync(CreditRiskEngine.ko.Logic_DzSoapServer_Request_BodyKo parameters, string hashCode);
    }

 Co spowodowało, że wywołanie odpowiedzi było stosunkowo proste:

            List<MigDzKo> migDzKoList = new List<MigDzKo>();
            try
            {
                ko.Logic_DzSoapServer_QueryKoPortClient client = new ko.Logic_DzSoapServer_QueryKoPortClient();
                ko.Logic_DzSoapServer_Request_BodyKo request = new ko.Logic_DzSoapServer_Request_BodyKo();
                request.sy = "12345678";
                client.Open();
                var response = client.getParcel(request, Constraints.MIGDZ.MigDzHashBase64);
                XmlNode[] xmlNodes = (XmlNode[])response;
                for (int i = 0; i < xmlNodes.Length; i++)
                {
                    try
                    {
                        string organization = string.Empty;
                        if (xmlNodes[i].ChildNodes[0].HasChildNodes)
                        {
                            MigDzKo migDzKo = new MigDzKo();
                            migDzKo.operation_id = Convert.ToInt32(xmlNodes[i].ChildNodes[0]["operation_id"].InnerXml);
                            migDzKo.hashpublic = xmlNodes[i].ChildNodes[0]["hashpublic"].InnerXml;
                            migDzKoList.Add(migDzKo);
                        }
                    }
                    catch (Exception ex)
                    {
                        log.Error(ex.Message.ToString());
                    }
                }
                client.Close();
            }
            catch (Exception ex)
            {
                log.Error(ex.Message.ToString());
            }
            return migDzKoList;


Dla spójności podaję też zawartość klasy MigDzKo

    public class MigDzKo
    {
        public int operation_id { get; set; }
        public string hashpublic { get; set; }
    }