Krisztián's profileBátyai Krisztián[KRis]PhotosBlogListsMore Tools Help

Blog


    January 22

    Saját LINQ provider készítése (LINQ 2 MyWebService)

    Szerény LINQ tapasztalataim azt mutatják (nem reprezentatív, meg persze a kivételek…), hogy az emberek nagy többsége nem úgy közelíti meg a LINQ-t ahogy azt kellene (szerintem).

    A legtöbb ember fejében a következő egyenlőség él :

    LINQ == LINQ 2 Sql

    Máshogy fogalmazva a LINQ 2 Sql maga a LINQ. Hogy ez miért van így az egy másik érdekes kérdés, részben mert azzal találkozik az ember először, mert ezzel demóznak mindenhol, és mert legtöbbször adatbázissal dolgozunk és ekkor adódik ugye maga LINQ 2 SQL.

    Furcsállják is a tanfolyam-hallgatók, amikor a 2 napos LINQ tanfolyam első felében LINQ 2 Object-ről beszélünk, saját providert írunk, stb…
    A végére persze mindenki megtanulja,  hogy a LINQ 2 Object+c#3.0 maga a LINQ , és ezeknek a következménye, pontosabban a providerek következménye, az hogy van olyan is hogy LINQ 2 Sql.

    Mi köze ennek a blogbejegyzés címében előrevetített témához?!?
    Hát mindösszesen annyi, hogy ahhoz hogy valaki saját providert írjon viszonylag mélyen kell ismerni a LINQ-t, azaz a LINQ 2 Object-et és a c# 3.0 újdonságait.

    Tehát mielőtt tovább olvasnád, javaslom ezen témakörök átbogarászását:

    Ha ez megvolt… akkor bele is vághatunk.

    LINQ 2 akármiből tízesével lehet találni a google-ön. De hogy?!?

    Első gyors megoldás, hogy kiterjesztjük az osztályunkat IEnumberable- interface-el, és máris LINQ alatt felhasználhatóvá vált az osztály query irására. Előnyei : mindösszesen 2 perc szükséges fejlesztés. Hátrányai : látszólagos, és sok esetben nem skálázható, nem finomítható… Persze ettől még sok esetben jó megoldás lehet.

    Ha pl. az adatforrás, ahonnan kapjuk az adatokat egy hagyományos WS, ami XML-ben adja vissza az adatokat, és a feladat az, hogy mi ne “lássuk” a WS-t, csak egy halom(List<>) adatot kapjunk, akkor adódik a megoldás hogy készítünk egy wrapper osztályt, ami megvalósítja a fent említett interface-t. Mégpedig úgy hogy lekéri az összes terméket az összes tulajdonságával… mindig. Aztán majd a LINQ query elmolyol rajta: leszűri amire kell, és kiválogatja a megfelelő tulajdonságokat…

    public class ProductSearch_bad : IEnumerable<Product>
       {
           #region IEnumerable<Product> Members
    
           public IEnumerator<Product> GetEnumerator()
           {
               return (IEnumerator<Product>)((IEnumerable)this).GetEnumerator();
           }
    
           #endregion
    
           #region IEnumerable Members
    
           IEnumerator IEnumerable.GetEnumerator()
           {
               ProductWS.Service1SoapClient client = new Linq2WebService.ProductWS.Service1SoapClient();
    
               string ret = "";
               try
               {
                   //fontos hogy mindig az összes lejön!!!!
                   ret = client.GetAll();
               }
               catch (Exception ex)
               {
    
               }
    
               XDocument xdoc = XDocument.Parse(ret);
    
               var q = from x in xdoc.Descendants("product")
                       select
                       new Product()
                       {
                           ProductID = int.Parse(x.Attribute("ProductID").Value),
                           Name = x.Element("Name").Value,
                           ProductNumber = x.Element("ProductNumber").Value,
                           ListPrice = x.Element("ListPrice").Value
                       };
               Console.WriteLine("WS : letöltöttem {0} db terméket",q.Count() );
               return q.GetEnumerator();
           }
    
           #endregion
       }
    
    Használata:
    Console.WriteLine("Most jól elkérjük a WS-től az összeset:");
               ProductSearch_bad search_bad = new ProductSearch_bad();
    
               var qq = from pp in search_bad
                         where pp.Name.Contains("b")
                        select pp;
    
               foreach (var item in qq)
               {
                   Console.WriteLine(item.Name);
               }

    Ugye mindenki látja hogy miért nem jó (2 kell de 20 jön át) a megoldás? Annak ellenére hogy a (kódot) “felhasználó” programozó örül majd mind majom a fa…nak, mert sose volt még ilyen egyszerű adatelérése.

    A megoldás adódik: valahogy át kell juttatni a feltételeket, és a kért mezőket, a “túloldalra”, ami ezek után csak tényleges adatokat adja majd vissza.

    Ehhez elég lesz néhány osztály és pár 10 sor kód. A kódolást több oldalról el lehet kezdeni (TOP-Down, Down-TOP), én az elejéről kezdem.

    1.Kell egy WS ami tud feltételeket kezelni, és mezőket válogatni:

    [WebMethod]
          public string GetProducts(string name,string productnumber,string cols)
          {
              AdventureWorksDataContext dc = new AdventureWorksDataContext();
    
    
              XDocument xdoc;
              xdoc = new XDocument(new XElement("Products",
                  (from p in dc.Products
                   where (String.IsNullOrEmpty(name) || p.Name.ToLower().Contains(name.ToLower())) &&
                          (String.IsNullOrEmpty(productnumber) || p.ProductNumber.ToLower().Contains(productnumber.ToLower()))
                   select ProductSelector(cols,p)
                  )
                  ));
    
              return xdoc.ToString();
          }
    
          private object ProductSelector(string cols, Product p)
          {
              XElement ret = new XElement("product",
                       new XAttribute("ProductID", p.ProductID)
                       );
              if (cols.Contains("Name")) ret.Add(new XElement("Name", p.Name));
              if (cols.Contains("ProductNumber")) ret.Add(new XElement("ProductNumber", p.ProductNumber));
              if (cols.Contains("ListPrice")) ret.Add(new XElement("ListPrice", p.ListPrice));
              return ret;
          }

    Látható hogy a WS a feltételeknek megfelelően fog csak adatokat visszadni, sőt! csak az adott oszlopokat fogja visszaadni!
    Itt jegyezném meg hogy a példa moricka, azaz nem kezel hibákat, sok mindent nem tud… és bele lehet kötni 5 helyen… természetesen éles szituációban tovább kell gondolni

    2/1. Kliens oldal

    Így akarjuk használni :

    var q = from p in new ProductSerach()
                      where p.Name == "Race" && p.ProductNumber == "1"
                      select new
                      {
                          p.Name            
                      };
    
              foreach (var item in q)
              {
                  Console.WriteLine(item.Name);
              }

    Tehát kell egy segédosztály, ami IEnumerable<Product>, ez tuti :

    2/2. Segéd osztály1

    public class Product
       {
           public int ProductID { get; set; }
    
           public string Name { get; set; }
    
           public string ProductNumber { get; set; }
    
           public string ListPrice { get; set; }
       }

    2/3 Segéd osztály2

    public class ProductSerach : IEnumerable<Product>
       {
           private ProductQueryCriteria _criteria;
           //default
           private string cols = "Name|ProductNumber|ListPrice";
    
           public ProductSerach Where(Expression<Func<Product, Boolean>> predicate)
           {
               _criteria = new ProductSearchExpressionVisitor().ProcessExpression(predicate);
               return this;
           }
    
           public ProductSerach Select<TResult>(
             Expression<Func<Product, TResult>> selector)
           {
               cols = new ProductSelectExpressionVisitor().ProcessExpression(selector);
               return this;
           }
    
    
           #region IEnumerable<Product> Members
    
           public IEnumerator<Product> GetEnumerator()
           {
               return (IEnumerator<Product>)((IEnumerable)this).GetEnumerator();
           }
    
           #endregion
    
           #region IEnumerable Members
    
           System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
           {
              
               IEnumerable<Product> prods = ProductHelper.PerformQuery(_criteria,cols);
    
               return prods.GetEnumerator();
           }
    
           #endregion
       }
    public class ProductQueryCriteria
       {
           public string ProductNumber { get; set; }
    
           public string Name { get; set; }
       }

    Mi van ebben az osztályban?!?

    • 2 segédváltozó, amelybe gyűjtjük a szükséges adatokat : where feltételek, select feltételek
    • IEnumberable megvalósítás, amely szükség esetén elvégzi a tényleges adatelérést. lsd később…
    • Select és a Where metódus
      Nos ez már érdekes. Nagyon.

      Mit tud? Azt hogy ez nem egy Statikus Extension Method, MÉGIS meghívódik, még mielőtt a GetEnumerator() lefutna, így fel tudunk készülni.
      Hogy hogy?!? Ez a query expression pattern

      Másik fontos dolog, hogy ez nem magát a lambda kifejezést várja, hanem csak az expression tree-t!

    Tehát ez az osztály bitosítja az enumerálhatóságot, szép magyarosan.

    2/4. Segéd osztály3

    Fel kellene dolgozni magát a Where részt:

    public class ProductSearchExpressionVisitor
      {
          ProductQueryCriteria _Criteria;
        
    
          public ProductQueryCriteria ProcessExpression(Expression expression)
          {
              _Criteria = new ProductQueryCriteria();
              VisitExpression(expression);
              return _Criteria;
          }
    
          private void VisitExpression(Expression expression)
          {
              if (expression.NodeType == ExpressionType.AndAlso)
              {
                  VisitAndAlso((BinaryExpression)expression);
              }
              else if (expression.NodeType == ExpressionType.Equal)
              {
                  VisitEqual((BinaryExpression)expression);
              }          
              else if (expression is LambdaExpression)
              {
                  VisitExpression(((LambdaExpression)expression).Body);
              }
          }
    
          private void VisitAndAlso(BinaryExpression andAlso)
          {
              VisitExpression(andAlso.Left);
              VisitExpression(andAlso.Right);
          }
    
          private void VisitEqual(BinaryExpression expression)
          {
                     if ((expression.Left.NodeType == ExpressionType.MemberAccess) &&
                (((MemberExpression)expression.Left).Member.Name == "ProductNumber"))
              {
                  if (expression.Right.NodeType == ExpressionType.Constant)
                      _Criteria.ProductNumber = (String)((ConstantExpression)expression.Right).Value;
                  else if (expression.Right.NodeType == ExpressionType.MemberAccess)
                      _Criteria.ProductNumber = (String)GetMemberValue((MemberExpression)expression.Right);
                  else
                      throw new NotSupportedException("Expression type not supported for ProductNumber: " + expression.Right.NodeType.ToString());
              }
              else if ((expression.Left.NodeType == ExpressionType.MemberAccess) &&
                (((MemberExpression)expression.Left).Member.Name == "Name"))
              {
                  if (expression.Right.NodeType == ExpressionType.Constant)
                      _Criteria.Name = (String)((ConstantExpression)expression.Right).Value;
                  else if (expression.Right.NodeType == ExpressionType.MemberAccess)
                      _Criteria.Name = (String)GetMemberValue((MemberExpression)expression.Right);
                  else
                      throw new NotSupportedException("Expression type not supported for Name: " + expression.Right.NodeType.ToString());
              }
            
          }    
    
      }

    (fontos dolgok kiemelve)

    Elsőre bonyolult, másodikra még inkább. Sajna át kell látni az Expression-t A-Z-ig. (Albert István)
    Tehát ha megtaláljuk a megfelelő hivatkozást pl. egy mezőre való szűrést, ami jelen esetben egy Contains lesz tulajdonképpen (lehetett volna metódushívás vizsgálat a contains-re, de én direkt az == operandust használom erre), akkor ezt megjegyezzük (segédosztály), és megyünk tovább rekurzívan…

    Ezt az igények szerint el lehet bonyolítani…

    2/5. Segéd osztály4

    Select feldolgozása:

    public class ProductSelectExpressionVisitor
      {
          string cols;
    
          public string ProcessExpression(Expression expression)
          {
              cols = "";
              VisitExpression(expression);
              return cols;
          }
    
          private void VisitExpression(Expression expression)
          {
              if (expression is LambdaExpression)
              {
                  VisitExpression(((LambdaExpression)expression).Body);
              }
              if (expression is NewExpression)
              {
                  VisitNewExpression((NewExpression)expression);
              }
          }
    
          private void VisitNewExpression(NewExpression newExpression)
          {
              //EZ NEM A TELJES MEGOLDÁS, CSAK EBBEN AZ ESETBEN működik
    
              //ez csak abban az esetben működik, ha az anonymous tipusban CSAK a product osztály 
              // tulajdonságai vannak, azonos névvel!!!!
    
              //amilyen bonyolult Select-et akarunk írni, úgy kell "elbonyolítani" a bejárást...
              foreach (var item in newExpression.Constructor.ReflectedType.GetProperties())
              {
                  cols += "|" + item.Name;
              }
          }
    
          private void VisitAndAlso(BinaryExpression andAlso)
          {
              VisitExpression(andAlso.Left);
              VisitExpression(andAlso.Right);
          }
    
      }

    Ez CSAK abban az esetben működik, ha anonymous tipust akarunk csinálni belőle, a megfelelő mezőket válogatva….
    Ezt is lehet tovább fokozni, hogy ha az Anonymous type nem ilyen egyszerű.

    2/6. Lekérdezés elvégzése:

    public static class ProductHelper
        {
    
            static internal IEnumerable<Product> PerformQuery(ProductQueryCriteria criteria, string cols)
            {
                ProductWS.Service1SoapClient client = new Linq2WebService.ProductWS.Service1SoapClient();
    
                string ret = "";
                try
                {
                    ret = client.GetProducts(criteria.Name, criteria.ProductNumber, cols);
                }
                catch (Exception ex)
                {
                                    
                }            
    
                XDocument xdoc = XDocument.Parse(ret);
    
                var q = from x in xdoc.Descendants("product")
                        select
                        CreateProductFromCols(cols,x);
                return q;
            }
    
            private static Product CreateProductFromCols(string cols,XElement x)
            {
                Product ret = new Product() { ProductID = int.Parse(x.Attribute("ProductID").Value) };
    
                if (cols.Contains("Name")) ret.Name = x.Element("Name").Value;
                if (cols.Contains("ProductNumber")) ret.ProductNumber = x.Element("ProductNumber").Value;
                if (cols.Contains("ListPrice")) ret.ListPrice = x.Element("ListPrice").Value;
    
                return ret;
            }
        }

    Miután megvan a criteria és a cols változó, ami kell a lekéréshez, meg tudjuk hívni a megfelelő WS-t.
    (itt újra megyjegyzem hogy a Where és a Select hamarabb fog lefutni, mint GetEnumerator)

    Ha megvannak a SZÜKSÉGES adatok (nem az összes!!), akkor már csak Productot kell belőle varázsolni.
    (direkt XML-el dolgozom, hogy minél “szabványosabb” legyen, lehetne direkt objektumokat ráncigálni…)

    3. Testre szabás…

    Az igények szerint :)

     

    Összefoglalva: a lényeg hogy a kliens oldali felhasználás egyszerű, és mindig csak a megfelelő adatokon dolgozik, egy hidat képeztünk a rétegek között a LINQ kiterjesztésével.

    Persze ez még mindig csak IEnumberable, és ne IQueryable, ami között az a nagy különbség, hogy ha az készítünk egy query-t majd azt felhasználjuk egy másikban, akkor késleltetett végrehajtás ide vagy oda, az első query le fog futni önmagában… függetlenül a másodiktól.
    Erre megoldás az IQueryable irányába való továbbfejlesztés, ami a következő blogbejegyzés témája lesz…

    Alapötlet : LINQ 2 Amazon

    Forrás : http://devportal.hu/groups/linq/media/p/5855.aspx

    demo adatbázis : Microsoft AdventureWorks

    http://www.codeplex.com/SqlServerSamples
    http://www.codeplex.com/MSFTDBProdSamples/Release/ProjectReleases.aspx?ReleaseId=4004