ASP.NET Core MVC 和 Entity Framework Core 入門教程 - 讀取相關數據(六)

Contoso 大學示例 Web 應用程序演示如何使用實體框架(EF)Core 2.0 和 Visual Studio 2017 創建 ASP.NET Core 2.0 MVC Web 應用程序。 如欲了解更多本教程相關信息,請參閱 一、入門
在上一教程中,您完成了學校數據模型。在本章中,您將讀取和展示相關數據 -- 即,實體框架加載到導航屬性的數據。
以下圖片展示了您即將完成的頁面。

image.png

image.png

相關數據的 Eager Loading (貪婪加載), Explicit Loading (顯式加載), 和 Lazy Loading (懶加載)

ORM (對象關系映射)框架,例如說 Entity Framework, 通常有多種方式用于加載實體的導航屬性。

  • Eager loading -- 貪婪加載。 當讀取實體的時候,也讀取實體相關的數據。這通常導致一個單一連接查詢,來取出所以需要的數據。在 Entity Framework 使用 IncludeThenInclude 方法來指定貪婪加載。
    image.png

    您可以在分離的查詢中檢索其中一些數據, 然后讓 EF “修復” 導航屬性。 也就是說, EF 會自動將分離查詢中的實體添加到之前讀取到的實體導航屬性中。 對于檢索相關數據的查詢, 您可以使用 Load 方法代替那些返回一個 listobject 的方法,比如說 ToListSingle
    image.png
  • Explicit loading -- 顯示加載。第一次讀取實體時, 相關的數據沒有被檢索。當您需要的時候,您需要寫代碼來檢索相關數據。 如同在貪婪加載中使用分離查詢一樣,顯示加載將形成多個查詢送往數據庫。不同之處在于,使用顯示加載,代碼指定的是要加載的導航屬性。在 Entity Framework 1.1 中您可以使用 Load 方法來執行顯示加載。例如:
    image.png
  • Lazy loading -- 懶加載,或延遲加載。第一次讀取實體時, 相關的數據沒有被檢索。但是,當您嘗試訪問導航屬性時,導航屬性相關的數據將會被自動檢索出。當您首次訪問導航屬性時,將有一個查詢發往數據庫。 Entity Framework Core 1.0 不支持 Lazy loading

性能注意事項

如果您事先知道,對于每個實體,需要相關的數據的話,貪婪加載通常提供了最佳性能,因為發送到數據庫的一個查詢通常比多個查詢更有效率。 例如,假設每個部門有十個相關的課程,貪婪加載方法使用了一條查詢加載一個部門的所有相關數據,只需要一次數據庫往返。而對每個部門單獨查詢課程,將導致出現十一個數據庫往返。多余的數據庫往返在延遲較高時對性能特別不利。
另一方面,在某些情況下,單獨查詢會更加高效。 貪婪加載可能會導致非常復雜的聯結查詢以至于 SQL Server 無法有效處理。 或者,您只需要對一個實體集的某個子集訪問其導航屬性,單獨查詢將可能比貪婪加載表現得更好,因為貪婪加載加載了超出您需要的數據的原因。 如果性能對您非常重要的話,最好對兩種方式都進行測試來做出最佳的選擇。

創建課程頁,其中顯示部門名稱。

Course 實體包含一個導航屬性,對應課程所分配部門的 Department 實體。 若要在 Course (課程)列表中顯示所分配 Department (部門)的名稱,您需要從 Course.Department 導航屬性連接的 Department 實體中取得 Name 屬性。
Course 實體類型創建一個控制器,命名為 CoursesController,使用前面教程中用于創建 Students 控制器時用的腳手架,相同的選項 -- “視圖使用 Entity Framework 的 MVC 控制器”。如下所示:

image.png

打開 CoursesController.cs 文件,檢查 Index 方法。腳手架已自動使用 Include 方法指定 Department 導航屬性為貪婪加載。
Index 方法替換為以下代碼, 使用一個更合適的名稱命名返回 Course 實體的 IQueryable 對象。

public async Task<IActionResult> Index()
{
    var courses = _context.Courses
        .Include(c => c.Department)
        .AsNoTracking();
    return View(await courses.ToListAsync());
}

打開 Views/Courses/Index.cshtl,并使用以下代碼替換模板代碼。

@model IEnumerable<ContosoUniversity.Models.Course>
@{
    ViewData["Title"] = "Courses";
}
<h2>Courses</h2>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.CourseID)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Credits)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Department)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.CourseID)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Title)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Credits)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Department.Name)
                </td>
                <td>
                    <a asp-action="Edit" asp-route-id="@item.CourseID">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.CourseID">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.CourseID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

您對腳手架生成的代碼作了如下更改:

  • 修改了課程 Index 頁面的標題
  • 添加了一列用于顯示 CourseID 屬性。 默認情況下, 主鍵不會出現在腳手架代碼中,因為它們通常對最終用戶無意義。 然而, 在這兒,主鍵是有意義的,您打算顯示出來。
  • 修改 Department 列以顯示部門名稱。 代碼顯示加載到 Department 導航屬性中 Department 實體的 Name 屬性。
@Html.DisplayFor(modelItem => item.Department.Name)

運行應用程序,選擇 Course 菜單以查看含有部門名稱的列表。

image.png

創建一個教師頁面,其中顯示課程及學生注冊情況

在本節中,您將會為 Instructor 實體創建一個控制器和視圖用于展示教師。

image.png

本頁面使用以下方法讀取并展示相關數據:

  • 教師列表展示來自 OfficeAssignment 實體的相關數據。InstructorOfficeeAssignment 實體是 一 對 零或一 關系,對 OfficeAssignment 實體將使用貪婪加載方式。 如前所述, 當您需要主表所有行的相關數據時,貪婪加載是最高效的。 在這種情況下, 你希望顯示所有教師分配的辦公室。
  • 當用戶選擇一個教師時,相關的課程實體將會顯示。 教師和課程實體是 “多對多” 關系。您將對課程及相關的部門實體使用貪婪加載。此時,單獨的查詢可能會更加高效,因為您只需要所選擇教師相關的課程。 不過,這個示例主要用于展示如何對導航屬性使用貪婪加載,以及對導航屬性內的實體進行貪婪加載。
  • 當用戶選擇一個課程時,相關的注冊實體將會顯示。 課程和注冊實體是 “一對多” 關系。 您將會使用單獨的查詢來應對注冊實體和相關的學生實體。

創建教師索引視圖的視圖模型

教師頁顯示三個不同的表中的數據。因此,你創建的視圖模型將包括三個屬性,每個屬性包含一個表的數據。
在 SchoolViewModels 文件夾中,創建 InstructorIndexData.cs 并替換為以下代碼:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Models.SchoolViewModels
{
    public class InstructorIndexData
    {
        public IEnumerable<Instructor> Instructors { get; set; }
        public IEnumerable<Course> Courses { get; set; }
        public IEnumerable<Enrollment> Enrollments { get; set; }
    }
}

創建教師控制器和視圖

使用包含 EF 讀/寫 操作的控制器模板創建一個教師控制器,如下圖所示:


image.png

打開 InstructorsController.cs 和添加 Viewmodel 命名空間引用:

using ContosoUniversity.Models.SchoolViewModels;

使用以下代碼替換 Index 方法,以達到相關數據的貪婪加載,并放入視圖模型中。

public async Task<IActionResult> Index(int? id, int? courseID)
{
    var viewModel = new InstructorIndexData();
    viewModel.Instructors = await _context.Instructors
          .Include(i => i.OfficeAssignment)
          .Include(i => i.CourseAssignments)
            .ThenInclude(i => i.Course)
                .ThenInclude(i => i.Enrollments)
                    .ThenInclude(i => i.Student)
          .Include(i => i.CourseAssignments)
            .ThenInclude(i => i.Course)
                .ThenInclude(i => i.Department)
          .AsNoTracking()
          .OrderBy(i => i.LastName)
          .ToListAsync();
    
    if (id != null)
    {
        ViewData["InstructorID"] = id.Value;
        Instructor instructor = viewModel.Instructors.Where(
            i => i.ID == id.Value).Single();
        viewModel.Courses = instructor.CourseAssignments.Select(s => s.Course);
    }

    if (courseID != null)
    {
        ViewData["CourseID"] = courseID.Value;
        viewModel.Enrollments = viewModel.Courses.Where(
            x => x.CourseID == courseID).Single().Enrollments;
    }

    return View(viewModel);
}

方法接受可選路由數據(id)和一個查詢字符串參數(courseID),分別對應選擇的教師和選擇的課程。參數從頁面的超鏈接中而來。
代碼首先創建一個視圖模型的實例,并在其中加入教師列表。 代碼指定對 Instrator.OfficeAssignmentCourseAssignments 導航屬性使用貪婪加載。 在 CourseAssignments 屬性中,Course 屬性將被加載, 然后在 Course 屬性中, EnrollmentsDepartment 屬性將會被加載,同時在每個 Enrollment 實體中, Student 屬性將會被加載。

viewModel.Instructors = await _context.Instructors
      .Include(i => i.OfficeAssignment)
      .Include(i => i.CourseAssignments)
        .ThenInclude(i => i.Course)
            .ThenInclude(i => i.Enrollments)
                .ThenInclude(i => i.Student)
      .Include(i => i.CourseAssignments)
        .ThenInclude(i => i.Course)
            .ThenInclude(i => i.Department)
      .AsNoTracking()
      .OrderBy(i => i.LastName)
      .ToListAsync();

由于視圖始終需要 OfficeAssignmet 實體,因此在同一個查詢中獲取它更有效。 當在網頁中選擇教師時,課程實體是必需的,因此只有當頁面以不是沒有選擇的課程更頻繁地顯示時,單個查詢才會比多個查詢更好。
代碼中,CourseAssignmentsCourse 重復出現,因為您需要 Course 的兩個屬性。 在第一個 ThenInclude 中獲取 CourseAssignment.Course, Course.Enrollements, 及 Enrollment.Student

viewModel.Instructors = await _context.Instructors
      .Include(i => i.OfficeAssignment)
      .Include(i => i.CourseAssignments)
        .ThenInclude(i => i.Course)
            .ThenInclude(i => i.Enrollments)
                .ThenInclude(i => i.Student)
      .Include(i => i.CourseAssignments)
        .ThenInclude(i => i.Course)
            .ThenInclude(i => i.Department)
      .AsNoTracking()
      .OrderBy(i => i.LastName)
      .ToListAsync();

在代碼中的那一點,另一個 ThenInclude 將用于學生的導航屬性,您不需要它。 但是,調用 Include 是由 Instructor 屬性開始的,所以你必須重新遍歷整個鏈條,通過指定 Course.Department 而不是 Course.Enrollments

viewModel.Instructors = await _context.Instructors
      .Include(i => i.OfficeAssignment)
      .Include(i => i.CourseAssignments)
        .ThenInclude(i => i.Course)
            .ThenInclude(i => i.Enrollments)
                .ThenInclude(i => i.Student)
      .Include(i => i.CourseAssignments)
        .ThenInclude(i => i.Course)
            .ThenInclude(i => i.Department)
      .AsNoTracking()
      .OrderBy(i => i.LastName)
      .ToListAsync();

在選擇了一個教師時,將執行下面的代碼。 從教師視圖模型中的列表中檢索所選的教師。 然后視圖模型的Courses 屬性和課程實體從該教師的 CourseAssignments 導航屬性中一起被加載。

if (id != null)
{
    ViewData["InstructorID"] = id.Value;
    Instructor instructor = viewModel.Instructors.Where(
        i => i.ID == id.Value).Single();
    viewModel.Courses = instructor.CourseAssignments.Select(s => s.Course);
}

Where 方法返回一個集合,但在本例中,傳遞給該方法的條件只會返回一個 Instructor 實體。 Single 方法將集合轉換為單個 Instructor 實體, 這樣一來,您就可以訪問該實體的 CourseAssignments 屬性。 CourseAssignments 包含 CourseAssignments 實體集合,從中得到相關的 Course 實體集。
當您知道集合將只有一個項目時,您可以在集合上使用 Single 方法。如果傳遞給它的集合為空,或者有多個項目, Single 方法會拋出異常。一個替代方法是 SingleOrDefault ,如果集合是空的,它返回一個默認值(在這種情況下為null)。 但是,在這種情況下,仍然會導致異常(嘗試在空引用上查找Courses屬性),并且異常消息將不太清楚地指出問題的原因。 當您調用 Single 方法時,您還可以傳遞 Where 條件,而無需單獨調用 Where 方法:

.Single(i => i.ID == id.Value)

而不是

.Where(I => i.ID == id.Value).Single()

接下來,如果選擇課程,則從視圖模型中的課程列表中檢索所選課程。 然后,視圖模型的 “Enrollments” 屬性將加載該課程的 “Enrollments” 導航屬性中的 “Enrollment” 實體。

if (courseID != null)
{
    ViewData["CourseID"] = courseID.Value;
    viewModel.Enrollments = viewModel.Courses.Where(
        x => x.CourseID == courseID).Single().Enrollments;
}

修改 “教師索引” 視圖

在 Views/Instructors/Index.cshtml 文件中,使用以下代碼替換。

@model ContosoUniversity.Models.SchoolViewModels.InstructorIndexData
@{
    ViewData["Title"] = "Instructors";
}
<h2>Instructors</h2>
<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>Last Name</th>
            <th>First Name</th>
            <th>Hire Date</th>
            <th>Office</th>
            <th>Courses</th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Instructors)
        {
            string selectedRow = "";
            if (item.ID == (int?)ViewData["InstructorID"])
            {
                selectedRow = "success";
            }
            <tr class="@selectedRow">
                <td>
                    @Html.DisplayFor(modelItem => item.LastName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.FirstMidName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.HireDate)
                </td>
                <td>
                    @if (item.OfficeAssignment != null)
                    {
                        @item.OfficeAssignment.Location
                    }
                </td>
                <td>
                    @{
                        foreach (var course in item.CourseAssignments)
                        {
                            @course.Course.CourseID @:  @course.Course.Title <br />
                        }
                    }
                </td>
                <td>
                    <a asp-action="Index" asp-route-id="@item.ID">Select</a> |
                    <a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.ID">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
                </td>
            </tr>
           }
    </tbody>
</table>

你對現有代碼做出以下更改:

  • 修改頁面 model 類為 InstructorIndexData
  • 修改頁面標題為 Instructors
  • item.OfficeAssignment 不為空的情況下,才添加一個 Office 列,顯示 item.OfficeAssignment.Location 。 (因為這是一個 "一 對 零或一" 的關系,可能沒有相關的 OfficeAssignment 實體。)
    @if (item.OfficeAssignment != null)
    {
        @item.OfficeAssignment.Location
    }
    
  • 添加了一個課程列,顯示每個教師所教授的課程。 請參閱 使用 @: 的顯式行轉換 有關此 Razor 語法的更多信息。
  • 添加的代碼動態地將 class =“success” 添加到所選教師的 tr 元素中。 這將會通過 Bootstrap 類為選擇行設置一個背景顏色。
    string selectedRow = "";
    if (item.ID == (int?)ViewData["InstructorID"])
    {
       selectedRow = "success";
    }
    <tr class="@selectedRow">
    
  • 在每行中的其他鏈接前,添加一個新的超鏈接 "Select" ,將所選教師的 ID 發送到 Index 方法。
    <a asp-action="Index" asp-route-id="@item.ID">Select</a> |
    

運行應用程序,選擇 Instructor 鏈接。 當沒有相關的 OfficeAssignment 實體時,該頁面顯示相關 OfficeAssignment 實體的 Location 屬性和一個空表單元格。

image.png

Views/Instructors/Index.cshtml 文件中,在 </table> 標簽(文件末尾)后添加以下代碼。 該代碼顯示了當教師選擇時與教練相關的課程列表。

@if (Model.Courses != null)
{
    <h3>Courses Taught by Selected Instructor</h3>
    <table class="table">
        <tr>
            <th></th>
            <th>Number</th>
            <th>Title</th>
            <th>Department</th>
        </tr>

        @foreach (var item in Model.Courses)
        {
            string selectedRow = "";
            if (item.CourseID == (int?)ViewData["CourseID"])
            {
                selectedRow = "success";
            }
            <tr class="@selectedRow">
                <td>
                    @Html.ActionLink("Select", "Index", new { courseID = item.CourseID })
                </td>
                <td>
                    @item.CourseID
                </td>
                <td>
                    @item.Title
                </td>
                <td>
                    @item.Department.Name
                </td>
            </tr>
        }

    </table>
}

此代碼讀取視圖模型的 Courses 屬性以顯示課程列表。它還提供一個選擇超鏈接,將所選課程的 ID 發送到 Index 操作方法。
刷新頁面并選擇一個教練。 現在,您看到一個網格,顯示分配給所選教師的課程,并且每個課程都會看到所分配部門的名稱。

image.png

在您剛剛添加的代碼塊之后,添加以下代碼。 這將顯示在選擇課程時注冊該課程的學生列表。

@if (Model.Enrollments != null)
{
    <h3>
        Students Enrolled in Selected Course
    </h3>
    <table class="table">
        <tr>
            <th>Name</th>
            <th>Grade</th>
        </tr>
        @foreach (var item in Model.Enrollments)
        {
            <tr>
                <td>
                    @item.Student.FullName
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Grade)
                </td>
            </tr>
        }
    </table>
}

該代碼讀取視圖模型的 Enrollments 屬性,以顯示在課程中注冊的學生列表。
再次刷新頁面并選擇一個教師。 然后選擇一個課程,查看注冊學生及其成績的列表。

image.png

顯式加載

當您在 InstructorsController.cs 中檢索到教師列表時,您為 CourseAssignments 導航屬性指定了貪婪加載。
假設您期望用戶很少想要在選定的教練和課程中看到注冊。 在這種情況下,您可能只需要請求加載注冊數據。 要查看如何進行顯式加載的示例,請使用以下代碼替換 Index 方法,這將刪除 Enrollments 的貪婪加載然后顯式加載該屬性。

public async Task<IActionResult> Index(int? id, int? courseID)
{
    var viewModel = new InstructorIndexData();
    viewModel.Instructors = await _context.Instructors
          .Include(i => i.OfficeAssignment)
          .Include(i => i.CourseAssignments)
            .ThenInclude(i => i.Course)
                .ThenInclude(i => i.Department)
          .OrderBy(i => i.LastName)
          .ToListAsync();
    if (id != null)
    {
        ViewData["InstructorID"] = id.Value;
        Instructor instructor = viewModel.Instructors.Where(
            i => i.ID == id.Value).Single();
        viewModel.Courses = instructor.CourseAssignments.Select(s => s.Course);
    }
    if (courseID != null)
    {
        ViewData["CourseID"] = courseID.Value;
        var selectedCourse = viewModel.Courses.Where(x => x.CourseID == courseID).Single();
        await _context.Entry(selectedCourse).Collection(x => x.Enrollments).LoadAsync();
        foreach (Enrollment enrollment in selectedCourse.Enrollments)
        {
            await _context.Entry(enrollment).Reference(x => x.Student).LoadAsync();
        }
        viewModel.Enrollments = selectedCourse.Enrollments;
    }
    return View(viewModel);
}

新代碼從用于檢索教師實體的代碼中刪除 Enrollment 數據的 ThenInclude 方法調用。如果選擇了教員和課程,高亮部分的代碼將檢索所選課程的注冊實體,以及每個注冊的學生實體。
運行應用程序,選擇 Instructor 鏈接。 可以看到,雖然您已經更改了數據的檢索方式, 頁面上顯示的內容并沒有任何區別于之前的。

小結

您現在已經使用貪婪加載,在一個查詢及多個查詢中用于讀取相關數據到導航屬性。 在下一個教程中,您將學習如何更新相關數據。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,646評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,595評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,560評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,035評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,814評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,224評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,301評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,444評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,988評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,804評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,998評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,544評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,237評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,665評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,927評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,706評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,993評論 2 374

推薦閱讀更多精彩內容