<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          深入LINQ | 動(dòng)態(tài)構(gòu)建LINQ表達(dá)式

          共 12013字,需瀏覽 25分鐘

           ·

          2021-05-26 01:19


          原文:bit.ly/3fwlKQJ
          作者:Jeremy Likness
          譯者:精致碼農(nóng)-王亮

          LINQ 是 Language Integrated Query(語(yǔ)言集成查詢)的縮寫,是我最喜歡的 .NET 和 C# 技術(shù)之一。使用 LINQ,開發(fā)者可以直接在強(qiáng)類型代碼中編寫查詢。LINQ 提供了一種標(biāo)準(zhǔn)的語(yǔ)言和語(yǔ)法,使不同的數(shù)據(jù)源的查詢編碼方法一致。

          1一些基礎(chǔ)

          考慮如下這個(gè) LINQ 查詢(你可以把它粘貼到一個(gè)控制臺(tái)應(yīng)用程序中運(yùn)行)。

          using System;
          using System.Linq;

          public class Program
          {
          public static void Main()
          {
          var someNumbers = new int[]{4, 8, 15, 16, 23, 42};
          var query =
          from num in someNumbers
          where num > 10
          orderby num descending
          select num.ToString();
          Console.WriteLine(string.Join('-', query.ToArray()));
          // 42-23-16-15
          }
          }

          因?yàn)?nbsp;someNumbers 是一個(gè) IEnumerable<int>,該查詢是被 LINQ to Objects 解析的。同樣的查詢語(yǔ)法可用于像 Entity Framework Core 這樣的工具,生成針對(duì)關(guān)系型數(shù)據(jù)庫(kù)運(yùn)行的 T-SQL。LINQ 可以使用兩種語(yǔ)法來(lái)編寫:查詢語(yǔ)法(如上所示)和(擴(kuò)展)方法語(yǔ)法。這兩種語(yǔ)法在語(yǔ)義上是相同的,你使用哪一種語(yǔ)法取決于你的偏好。上面同樣的查詢可以用方法語(yǔ)法寫成這樣:

          var secondQuery = someNumbers.Where(n => n > 10)
          .OrderByDescending(n => n)
          .Select(n => n.ToString());

          每個(gè) LINQ 查詢都有三個(gè)階段:

          1. 設(shè)置一個(gè)數(shù)據(jù)源,稱為提供者(provider),供查詢時(shí)使用。例如,到目前為止的代碼使用了內(nèi)置的 LINQ to Objects 提供者。你的 EF Core 項(xiàng)目使用的是 EF Core 提供者,它映射到你的數(shù)據(jù)庫(kù)。

          2. 查詢被定義并轉(zhuǎn)變成一個(gè)表達(dá)式樹(expression tree),我將在稍后介紹。

          3. 查詢被執(zhí)行,數(shù)據(jù)被返回。

          第 3 步很重要,因?yàn)?LINQ 使用了所謂的延遲執(zhí)行(deferred execution)。在上面的例子中,secondQuery 定義了一個(gè)表達(dá)式樹,但還沒有返回任何數(shù)據(jù)。事實(shí)上,在你開始迭代數(shù)據(jù)之前,實(shí)際上什么都沒有發(fā)生。這很重要,因?yàn)樗试S提供者通過(guò)只提供所要求的數(shù)據(jù)。例如,假設(shè)你想用 secondQuery 找到一個(gè)特定的字符串,所以你做了這樣的事情:

          var found = false;
          foreach(var item in secondQuery.AsEnumerable())
          {
          if (item == "23")
          {
          found = true;
          break;
          }
          }

          一個(gè)提供者通過(guò)枚舉器(enumerator)訪問(wèn),這樣它就可以一次輸入一個(gè)元素的數(shù)據(jù)。如果你在第三次迭代時(shí)得到了想要的值,可能實(shí)際上只有三條數(shù)據(jù)從數(shù)據(jù)庫(kù)中返回。另一方面,當(dāng)你使用 .ToList() 擴(kuò)展方法時(shí),所有的數(shù)據(jù)都會(huì)立即被取出并填充到列表中。

          2難題

          我作為我們公司的 .NET 項(xiàng)目經(jīng)理,我經(jīng)常與客戶交談,了解他們的需求。最近,我與一位客戶進(jìn)行了討論,他想在他們的網(wǎng)站上使用第三方控件來(lái)建立業(yè)務(wù)規(guī)則。更具體地說(shuō),業(yè)務(wù)規(guī)則是“謂詞”(predicates,譯注:也可以翻譯成判斷語(yǔ)句)或一組條件,可解析為 true 或 false。該工具可以用 JSON 或 SQL 格式生成規(guī)則。SQL 很香,可以持久化到給數(shù)據(jù)庫(kù),但他們的要求是將“謂詞”應(yīng)用于內(nèi)存對(duì)象,作為服務(wù)器上的一個(gè)過(guò)濾器。他們正在考慮使用一種工具,將 SQL 翻譯成表達(dá)式(其實(shí)就是動(dòng)態(tài)生成 LINQ)。我建議使用 JSON 格式,因?yàn)樗梢员唤馕龀?LINQ 表達(dá)式,針對(duì)內(nèi)存中的對(duì)象運(yùn)行,或者很容易應(yīng)用到 Entity Framework Core 集合,相對(duì) SQL 數(shù)據(jù)庫(kù)是更好的選擇。

          我只要處理工具產(chǎn)生的 JSON:

          {
          "condition": "and",
          "rules": [
          {
          "label": "Category",
          "field": "Category",
          "operator": "in",
          "type": "string",
          "value": ["Clothing"]
          },
          {
          "condition": "or",
          "rules": [
          {
          "label": "TransactionType",
          "field": "TransactionType",
          "operator": "equal",
          "type": "boolean",
          "value": "income"
          },
          {
          "label": "PaymentMode",
          "field": "PaymentMode",
          "operator": "equal",
          "type": "string",
          "value": "Cash"
          }
          ]
          },
          {
          "label": "Amount",
          "field": "Amount",
          "operator": "equal",
          "type": "number",
          "value": 10
          }
          ]
          }

          結(jié)構(gòu)很簡(jiǎn)單:有一個(gè) AND 或 OR 條件,包含一組規(guī)則,要么是比較,要么是嵌套條件。我的目標(biāo)有兩個(gè):學(xué)習(xí)更多關(guān)于 LINQ 表達(dá)式的知識(shí),以便更好地了解 EF Core 和相關(guān)技術(shù);提供一個(gè)簡(jiǎn)單的例子,說(shuō)明如何在不依賴第三方工具的情況下使用 JSON。

          3動(dòng)態(tài)表達(dá)式

          我創(chuàng)建了一個(gè)簡(jiǎn)單的控制臺(tái)應(yīng)用程序來(lái)測(cè)試我的假設(shè),即解析 JSON 信息直接生成 LINQ 查詢。

          https://github.com/JeremyLikness/ExpressionGenerator

          譯注:建議參照此 GitHub 源代碼閱讀本文,方便理解。

          在本文的第一部分,將啟動(dòng)項(xiàng)目設(shè)置為 ExpressionGenerator。如果你從命令行運(yùn)行它,請(qǐng)確保 rules.json 在你的當(dāng)前目錄中。

          我將樣本 JSON 嵌入為 rules.json。使用 System.Text.Json 來(lái)解析文件,就是這么簡(jiǎn)單:

          var jsonStr = File.ReadAllText("rules.json");
          var jsonDocument = JsonDocument.Parse(jsonStr);

          然后我創(chuàng)建了一個(gè) JsonExpressionParser 來(lái)解析 JSON 并創(chuàng)建一個(gè)表達(dá)式樹。因?yàn)閯?dòng)態(tài)表達(dá)式是一個(gè)謂詞,所以表達(dá)式樹是由二元表達(dá)式 BinaryExpression 的實(shí)例構(gòu)成的,這些實(shí)例計(jì)算一個(gè)左表達(dá)式和一個(gè)右表達(dá)式。這個(gè)計(jì)算可能是一個(gè)邏輯門(AND 或 OR),或一個(gè)比較(equal 或 greaterThan),或一個(gè)方法調(diào)用。對(duì)于 In 的情況,即我們想讓屬性 Category 出現(xiàn)在一個(gè)列表中,我使用 Contains。從概念上講,引用的 JSON 看起來(lái)像這樣:

                                   /-----------AND-----------\
          | |
          /-AND-\ |
          Category IN ['Clothing'] Amount eq 10.0 /-OR-\
          TransactionType EQ 'income' PaymentMode EQ 'Cash'

          注意,每個(gè)節(jié)點(diǎn)都是二元的。讓我們開始解析吧!

          4引入 Transaction

          注意,這不是 System.Transaction(這里的 Transaction 不是指事務(wù),而是指交易)。這是示例項(xiàng)目中使用的一個(gè)自定義類。我沒有在供應(yīng)商的網(wǎng)站上花很多時(shí)間,所以我根據(jù)規(guī)則猜測(cè)實(shí)體可能的樣子。我想出了這個(gè):

          public class Transaction
          {
          public int Id { get; set; }
          public string Category { get; set; }
          public string TransactionType { get; set; }
          public string PaymentMode { get; set; }
          public decimal Amount { get; set; }
          }

          我還添加了一些額外的方法,以使其易于生成隨機(jī)實(shí)例。你可以自己在 GitHub 代碼中看到這些。

          5參數(shù)表達(dá)式

          主要方法返回一個(gè)謂詞(predicate)函數(shù)。下面是該方法開始部分的代碼:

          public Func<T, bool> ParsePredicateOf<T>(JsonDocument doc)
          {
          var itemExpression = Expression.Parameter(typeof(T));
          var conditions = ParseTree<T>(doc.RootElement, itemExpression);
          }

          第一步是創(chuàng)建謂詞參數(shù)。謂詞可以傳遞給 Where 子句,如果我們自己寫的話,它看起來(lái)就像這樣:

          var query = ListOfThings.Where(t => t.Id > 2);

          t => 是一個(gè)參數(shù),代表列表中一個(gè)條目的類型。因此,我們?yōu)樵擃愋蛣?chuàng)建一個(gè)參數(shù)。然后我們遞歸地遍歷 JSON 節(jié)點(diǎn)來(lái)建立樹。

          6邏輯表達(dá)式

          解析器的開頭看起來(lái)像這樣:

          private Expression ParseTree<T>(
          JsonElement condition,
          ParameterExpression parm)
          {
          Expression left = null;
          var gate = condition.GetProperty(nameof(condition)).GetString();

          JsonElement rules = condition.GetProperty(nameof(rules));

          Binder binder = gate == And ? (Binder)Expression.And : Expression.Or;

          Expression bind(Expression left, Expression right) =>
          left == null ? right : binder(left, right);

          gate 變量是條件,即“and”或“or”。規(guī)則語(yǔ)句得到一個(gè)節(jié)點(diǎn),是相關(guān)規(guī)則的列表。我們正在跟蹤表達(dá)式的左邊和右邊。Binder 簽名是二元表達(dá)式的簡(jiǎn)寫,定義如下:

          private delegate Expression Binder(Expression left, Expression right);

          binder 變量簡(jiǎn)單地設(shè)置了頂層表達(dá)式:Expression.And 或 Expression.Or。兩者都使用左邊和右邊表達(dá)式來(lái)計(jì)算。

          bind 函數(shù)更有趣一點(diǎn)。當(dāng)我們遍歷樹時(shí),我們需要建立各種節(jié)點(diǎn)。如果我們還沒有創(chuàng)建一個(gè)表達(dá)式(left 是 null),我們就從創(chuàng)建的第一個(gè)表達(dá)式開始。如果我們有一個(gè)現(xiàn)有的表達(dá)式,我們就用這個(gè)表達(dá)式來(lái)合并兩邊的內(nèi)容。

          現(xiàn)在,left 是 null,然后我們開始列舉屬于這個(gè)條件的規(guī)則:

          foreach (var rule in rules.EnumerateArray())

          7屬性表達(dá)式

          第一條規(guī)則是一個(gè)相等規(guī)則,所以我現(xiàn)在跳過(guò)條件部分。大致情況是下面這樣的:

          string @operator = rule.GetProperty(nameof(@operator)).GetString();
          string type = rule.GetProperty(nameof(type)).GetString();
          string field = rule.GetProperty(nameof(field)).GetString();
          JsonElement value = rule.GetProperty(nameof(value));
          var property = Expression.Property(parm, field);

          首先,我們得到運(yùn)算符(in)、類型(string)、字段(Category)和值(一個(gè)以Clothing為唯一元素的數(shù)組)。注意對(duì) Expression.Property 的調(diào)用。這個(gè)規(guī)則的 LINQ 看起來(lái)是這樣的:

          var filter = new List<string> { "Clothing" };
          Transactions.Where(t => filter.Contains(t.Category));

          該屬性是 t.Category,所以我們根據(jù)父屬性(t)和字段名來(lái)創(chuàng)建它。

          8常量和調(diào)用表達(dá)式

          接下來(lái),我們需要建立對(duì) Contains 的調(diào)用。為了簡(jiǎn)化,我在這里創(chuàng)建了一個(gè)對(duì)該方法的引用:

          private readonly MethodInfo MethodContains = typeof(Enumerable).GetMethods(
          BindingFlags.Static | BindingFlags.Public)
          .Single(m => m.Name == nameof(Enumerable.Contains)
          && m.GetParameters().Length == 2);

          這就動(dòng)態(tài)提取了 Enumerable 的 Contains 方法,該方法需要兩個(gè)參數(shù):要使用的集合和要檢查的值。接下來(lái)的邏輯看起來(lái)像這樣:

          if (@operator == In)
          {
          var contains = MethodContains.MakeGenericMethod(typeof(string));
          object val = value.EnumerateArray().Select(e => e.GetString())
          .ToList();
          var right = Expression.Call(
          contains,
          Expression.Constant(val),
          property);
          left = bind(left, right);
          }

          首先,我們使用 Enumerable.Contains 模板來(lái)創(chuàng)建一個(gè) Enumerable<string>。接下來(lái),我們獲取值的列表,把它變成一個(gè) List<string>。最后,建立我們的調(diào)用,需要傳遞:

          • 要調(diào)用的方法(contains

          • 要檢查的參數(shù)的值(帶有 Clothing 的列表,或者 Expression.Constant(val)

          • 要對(duì)其進(jìn)行檢查的屬性(t.Category)。

          我們的表達(dá)式樹已經(jīng)相當(dāng)深了,有參數(shù)、屬性、調(diào)用和常量。記住,left 仍然是空的,所以對(duì) bind 的調(diào)用只是將 left 設(shè)置為我們剛剛創(chuàng)建的調(diào)用表達(dá)式。到目前為止,看起來(lái)像這樣:

          Transactions.Where(t => (new List<string> { "Clothing" }).Contains(t.Category));

          循環(huán)往復(fù),下一個(gè)規(guī)則是一個(gè)嵌套條件。關(guān)鍵代碼如下:

          if (rule.TryGetProperty(nameof(condition), out JsonElement check))
          {
          var right = ParseTree<T>(rule, parm);
          left = bind(left, right);
          continue;
          }

          目前,left 被分配給 in 表達(dá)式。right 將被分配為解析新條件的結(jié)果。現(xiàn)在,我們的 binder 被設(shè)置為 Expression.And,所以當(dāng)函數(shù)返回時(shí),bind 的調(diào)用結(jié)果是這樣的:

          Transactions.Where(t => (new List<string> { "Clothing" }).Contains(t.Category) && <something>);

          我們?cè)賮?lái)看看這里的“something”。

          9比較表達(dá)式

          首先,遞歸調(diào)用確定了一個(gè)新的條件存在,這次是一個(gè)邏輯 OR。binder 被設(shè)置為 Expression.Or,規(guī)則開始運(yùn)算。第一條規(guī)則是關(guān)于 TransactionType 的。它被設(shè)置為布爾值,但根據(jù)我的推斷,它意味著用戶在界面中可以選擇一個(gè)值或切換到另一個(gè)值。因此,我把它實(shí)現(xiàn)為一個(gè)簡(jiǎn)單的字符串比較。下面是建立比較的代碼:

          object val = (type == StringStr || type == BooleanStr) ?
          (object)value.GetString() : value.GetDecimal();
          var toCompare = Expression.Constant(val);
          var right = Expression.Equal(property, toCompare);
          left = bind(left, right);

          該值被解析為字符串或小數(shù)(后面的規(guī)則將使用小數(shù)格式)。然后,該值被轉(zhuǎn)換成一個(gè)常數(shù),然后創(chuàng)建比較。注意它是通過(guò)屬性比較的。現(xiàn)在的變量看起來(lái)像這樣:

          Transactions.Where(t => t.TransactionType == "income");

          在這個(gè)嵌套循環(huán)中,left 仍然是空的。解析器計(jì)算了下一條規(guī)則,即 PaymentModebind 函數(shù)把它變成了這個(gè)“或”語(yǔ)句:

          Transactions.Where(t => t.TransactionType == "income" || t.PaymentMode == "Cash");

          其余的應(yīng)該是不言自明的。表達(dá)式的一個(gè)很好的特點(diǎn)是它們可以重載 ToString() 來(lái)展現(xiàn)輸出。下面就是我們的表達(dá)式的樣子(為了方便查看,我手動(dòng)進(jìn)行了格式化):

          (
          (value(System.Collections.Generic.List`1[System.String]).Contains(Param_0.Category)
          And (
          (Param_0.TransactionType == "income")
          Or
          (Param_0.PaymentMode == "Cash"))
          )
          And
          (Param_0.Amount == 10)
          )

          它看起來(lái)不錯(cuò)......但我們還沒有完成!

          10Lambda 表達(dá)式和編譯

          接下來(lái),我創(chuàng)建一個(gè) lambda 表達(dá)式。這里定義了解析后的表達(dá)式的形狀,它將是一個(gè)謂詞(Func<T,bool>)。最后,返回編譯后的委托:

          var conditions = ParseTree<T>(doc.RootElement, itemExpression);
          if (conditions.CanReduce)
          {
          conditions = conditions.ReduceAndCheck();
          }
          var query = Expression.Lambda<Func<T, bool>>(conditions, itemExpression);
          return query.Compile();

          為了測(cè)試,我生成了 1000 個(gè) Transaction。然后我應(yīng)用過(guò)濾器并迭代結(jié)果,這樣我就可以手動(dòng)測(cè)試條件是否滿足:

          var predicate = jsonExpressionParser
          .ParsePredicateOf<Transaction>(jsonDocument);
          var transactionList = Transaction.GetList(1000);
          var filteredTransactions = transactionList.Where(predicate).ToList();
          filteredTransactions.ForEach(Console.WriteLine);

          正如你所看到的,結(jié)果出來(lái)了(我平均每次運(yùn)行約 70 次“命中”)。

          11從內(nèi)存到數(shù)據(jù)庫(kù)

          生成的委托并不只是用于對(duì)象。我們也可以用它來(lái)訪問(wèn)數(shù)據(jù)庫(kù)。

          在這篇文章的其余部分,將啟動(dòng)項(xiàng)目設(shè)置為 DatabaseTest。如果你從命令行運(yùn)行它,要確保 databaseRules.json 在你的當(dāng)前目錄中。

          首先,我重構(gòu)了代碼。還記得表達(dá)式是如何要求一個(gè)數(shù)據(jù)源的嗎?在前面的例子中,我們編譯了表達(dá)式,最后得到了一個(gè)對(duì)對(duì)象工作的委托。為了使用不同的數(shù)據(jù)源,我們需要在編譯表達(dá)式之前將其傳遞給它。這允許數(shù)據(jù)源對(duì)其進(jìn)行編譯。如果我們傳遞已編譯的數(shù)據(jù)源,數(shù)據(jù)庫(kù)提供者將被迫從數(shù)據(jù)庫(kù)中獲取所有行,然后解析返回的列表。我們希望數(shù)據(jù)庫(kù)來(lái)做這些工作。我把大部分代碼移到一個(gè)名為 ParseExpressionOf<T> 的方法中,該方法返回 lambda。我把原來(lái)的方法重構(gòu)成這樣:

          public Func<T, bool> ParsePredicateOf<T>(JsonDocument doc)
          {
          var query = ParseExpressionOf<T>(doc);
          return query.Compile();
          }

          ExpressionGenerator 程序使用編譯后的查詢,DatabaseTest 使用原始的 lambda 表達(dá)式。它將其應(yīng)用于一個(gè)本地的 SQLite 數(shù)據(jù)庫(kù),以演示 EF Core 是如何解析表達(dá)式的。在數(shù)據(jù)庫(kù)中創(chuàng)建并插入 1000 條 Transaction 后,通過(guò)下面代碼查詢總數(shù):

          var count = await context.DbTransactions.CountAsync();
          Console.WriteLine($"Verified insert count: {count}.");

          這會(huì)生成以下 SQL 語(yǔ)句:

          SELECT COUNT(*)
          FROM "DbTransactions" AS "d"

          謂詞被解析(這次是來(lái)自 databaseRules.json 中的一組新規(guī)則)并傳遞給 Entity Framework Core 提供者。

          var parser = new JsonExpressionParser();
          var predicate = parser.ParseExpressionOf<Transaction>(
          JsonDocument.Parse(
          await File.ReadAllTextAsync("databaseRules.json")));
          var query = context.DbTransactions.Where(predicate)
          .OrderBy(t => t.Id);
          var results = await query.ToListAsync();

          打開 Entity Framework Core 日志記錄開關(guān),我們能夠檢索到生成的 SQL,看到數(shù)據(jù)條目是如何被一次性獲取和在數(shù)據(jù)庫(kù)引擎中如何計(jì)算的。注意 PaymentMode 被檢查為“Credit”而不是“Cash”。

          SELECT "d"."Id", "d"."Amount", "d"."Category", "d"."PaymentMode", "d"."TransactionType"
          FROM "DbTransactions" AS "d"
          WHERE ("d"."Category" IN ('Clothing') &
          ((("d"."TransactionType" = 'income') AND "d"."TransactionType" IS NOT NULL) |
          (("d"."PaymentMode" = 'Credit') AND "d"."PaymentMode" IS NOT NULL))) &
          ("d"."Amount" = '10.0')
          ORDER BY "d"."Id"

          該示例應(yīng)用程序還打印了一個(gè)實(shí)體,以進(jìn)行抽查。

          12總結(jié)

          LINQ 表達(dá)式是一個(gè)非常強(qiáng)大的工具,可以過(guò)濾和轉(zhuǎn)換數(shù)據(jù)。我希望這個(gè)例子有助于理解表達(dá)式樹是如何構(gòu)建的。當(dāng)然,解析表達(dá)式樹感覺有點(diǎn)像魔術(shù)。Entity Framework Core 是如何在表達(dá)式樹上行走以產(chǎn)生有意義的 SQL?我正在自己探索這個(gè)問(wèn)題,并得到了 ExpressionVisitor 類的幫助。我將陸續(xù)發(fā)表更多關(guān)于這個(gè)問(wèn)題的文章。


          往期精彩回顧




          【推薦】.NET Core開發(fā)實(shí)戰(zhàn)視頻課程 ★★★

          .NET Core實(shí)戰(zhàn)項(xiàng)目之CMS 第一章 入門篇-開篇及總體規(guī)劃

          【.NET Core微服務(wù)實(shí)戰(zhàn)-統(tǒng)一身份認(rèn)證】開篇及目錄索引

          Redis基本使用及百億數(shù)據(jù)量中的使用技巧分享(附視頻地址及觀看指南)

          .NET Core中的一個(gè)接口多種實(shí)現(xiàn)的依賴注入與動(dòng)態(tài)選擇看這篇就夠了

          10個(gè)小技巧助您寫出高性能的ASP.NET Core代碼

          用abp vNext快速開發(fā)Quartz.NET定時(shí)任務(wù)管理界面

          在ASP.NET Core中創(chuàng)建基于Quartz.NET托管服務(wù)輕松實(shí)現(xiàn)作業(yè)調(diào)度

          現(xiàn)身說(shuō)法:實(shí)際業(yè)務(wù)出發(fā)分析百億數(shù)據(jù)量下的多表查詢優(yōu)化

          關(guān)于C#異步編程你應(yīng)該了解的幾點(diǎn)建議

          C#異步編程看這篇就夠了


          瀏覽 47
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  激情丁香五月婷婷 | 日本黄色电影一区二区三区 | 韩国一级黄色毛片 | 中文字幕无码日韩 | 人人肏人人摸人人操 |