Krisztián's profileBátyai Krisztián[KRis]PhotosBlogListsMore ![]() | Help |
|
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… Mi köze ennek a blogbejegyzés címében előrevetített témához?!? 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! 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); }
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?!?
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) 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…. 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. 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. 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 |
|
|