Руководство. Узнайте о сложных сценариях— ASP.NET MVC с помощью EF Core

В предыдущем учебнике было реализовано наследование типа "одна таблица на иерархию". В этом учебнике описываются некоторые расширенные возможности, не относящиеся к базовой разработке веб-приложений ASP.NET Core, использующих платформу Entity Framework Core.

Изучив это руководство, вы:

  • Выполнение прямых SQL-запросов
  • Вызов запроса для получения сущностей
  • Вызов запроса для получения других типов
  • Вызов запроса на обновление
  • Изучение SQL-запросов
  • Создание уровня абстракции
  • Сведения об автоматическом обнаружении изменений
  • Сведения о планах EF Core исходного кода и разработки
  • Сведения об использовании динамических запросов LINQ для упрощения кода

Необходимые компоненты

Выполнение прямых SQL-запросов

Одним из преимуществ использования платформы Entity Framework является возможность избежать слишком тесной привязки кода к конкретному способу хранения данных. Это достигается путем автоматического создания запросов и команд SQL, что позволяет упростить написание кода. Тем не менее в редких случаях требуется выполнять созданные вручную SQL-запросы. Для таких сценариев в API Entity Framework Code First включены методы, позволяющие передавать команды SQL напрямую в базу данных. У вас есть следующие параметры в EF Core версии 1.0:

  • Использование метода DbSet.FromSql для запросов, которые возвращают типы сущностей. Возвращаемые объекты должны иметь тип, ожидаемый объектом DbSet, и автоматически отслеживаются контекстом базы данных, кроме случаев, когда отслеживание отключено.

  • Использование Database.ExecuteSqlCommand для команд, не относящихся к запросам.

Если вам необходимо выполнить запрос, который возвращает типы, не являющиеся сущностями, можно использовать ADO.NET с подключением к базе данных, предоставленным платформой EF. Возвращаемые данные не отслеживаются контекстом базы данных, даже если вы используете этот метод для извлечения типов сущностей.

Как и всегда при выполнении команд SQL в веб-приложении, необходимо принимать меры предосторожности для защиты сайта от атак путем внедрения кода SQL. Одним из способов защиты является применение параметризованных запросов, которые гарантируют, что строки, отправляемые веб-страницей, не могут быть интерпретированы как команды SQL. В рамках этого учебника вы будете использовать параметризованные запросы при интеграции вводимых пользователем данных в запрос.

Вызов запроса для получения сущностей

Класс DbSet<TEntity> предоставляет метод, который можно использовать для выполнения запроса, возвращающего сущность типа TEntity. Чтобы увидеть, как это работает, измените код в методе Details для контроллера Department.

В файле DepartmentsController.cs в методе Details замените код, извлекающий отдел, вызовом метода FromSql, как показано ниже в выделенном коде:

public async Task<IActionResult> Details(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    string query = "SELECT * FROM Department WHERE DepartmentID = {0}";
    var department = await _context.Departments
        .FromSql(query, id)
        .Include(d => d.Administrator)
        .AsNoTracking()
        .FirstOrDefaultAsync();

    if (department == null)
    {
        return NotFound();
    }

    return View(department);
}

Чтобы убедиться, что новый код работает правильно, выберите вкладку Departments (Кафедры) и щелкните Details (Сведения) для одной из кафедр.

Department Details

Вызов запроса для получения других типов

Ранее вы создали таблицу статистики учащихся на странице сведений, в которой было показано число учащихся на каждую дату регистрации. Вы получали эти данные из набора сущностей Students (_context.Students) и использовали LINQ, чтобы спроецировать результаты в список объектов модели представления EnrollmentDateGroup. Предположим, что вы хотите написать код SQL вместо использования LINQ. Для этого вам необходимо выполнить SQL-запрос, который возвращает объекты, не являющиеся сущностями. В EF Core 1.0 один из способов сделать это — написать код ADO.NET и получить подключение к базе данных из EF.

В HomeController.cs замените метод About следующим кодом:

public async Task<ActionResult> About()
{
    List<EnrollmentDateGroup> groups = new List<EnrollmentDateGroup>();
    var conn = _context.Database.GetDbConnection();
    try
    {
        await conn.OpenAsync();
        using (var command = conn.CreateCommand())
        {
            string query = "SELECT EnrollmentDate, COUNT(*) AS StudentCount "
                + "FROM Person "
                + "WHERE Discriminator = 'Student' "
                + "GROUP BY EnrollmentDate";
            command.CommandText = query;
            DbDataReader reader = await command.ExecuteReaderAsync();

            if (reader.HasRows)
            {
                while (await reader.ReadAsync())
                {
                    var row = new EnrollmentDateGroup { EnrollmentDate = reader.GetDateTime(0), StudentCount = reader.GetInt32(1) };
                    groups.Add(row);
                }
            }
            reader.Dispose();
        }
    }
    finally
    {
        conn.Close();
    }
    return View(groups);
}

Добавьте инструкцию using:

using System.Data.Common;

Запустите приложение и перейдите на страницу About. На экран будут выведены те же данные, что и ранее.

About page

Вызов запроса на обновление

Предположим, что администраторам университета Suppose необходимо внести глобальные изменения в базу данных, например изменить число зачетных баллов для каждого курса. Поскольку в университете ведется множество курсов, будет неэффективно извлекать их в виде сущностей и изменять по отдельности. В этом разделе вы реализуете веб-страницу, на которой пользователь может задать множитель, который будет применен к числу зачетных баллов для каждого курса, после чего изменения будут внесены с помощью инструкции SQL UPDATE. Веб-страница должна выглядеть так, как показано на следующем рисунке:

Update Course Credits page

Добавьте CoursesController.csметоды UpdateCourseCredits для HttpGet и HttpPost:

public IActionResult UpdateCourseCredits()
{
    return View();
}
[HttpPost]
public async Task<IActionResult> UpdateCourseCredits(int? multiplier)
{
    if (multiplier != null)
    {
        ViewData["RowsAffected"] = 
            await _context.Database.ExecuteSqlCommandAsync(
                "UPDATE Course SET Credits = Credits * {0}",
                parameters: multiplier);
    }
    return View();
}

Когда контроллер обрабатывает запрос HttpGet, в ViewData["RowsAffected"] ничего не возвращается, а в представлении отображается пустое текстовое поле и кнопка отправки, как показано на предыдущем рисунке.

При нажатии кнопки Update (Обновить) вызывается метод HttpPost, а множителю присваивается значение, введенное в текстовое поле. После этого код выполняет SQL-запрос, который обновляет курсы и возвращает число затронутых строк в представление в ViewData. После того как в представление передано значение RowsAffected, оно отображает число обновленных строк.

В обозревателе решений щелкните правой кнопкой мыши папку Views/Courses и выберите Добавить > Новый элемент.

В диалоговом окне Добавление нового элемента щелкните элемент ASP.NET Core в разделе Установленные на панели слева, выберите Представление Razor и присвойте новому представлению имя UpdateCourseCredits.cshtml.

В Views/Courses/UpdateCourseCredits.cshtml замените код шаблона следующим кодом:

@{
    ViewBag.Title = "UpdateCourseCredits";
}

<h2>Update Course Credits</h2>

@if (ViewData["RowsAffected"] == null)
{
    <form asp-action="UpdateCourseCredits">
        <div class="form-actions no-color">
            <p>
                Enter a number to multiply every course's credits by: @Html.TextBox("multiplier")
            </p>
            <p>
                <input type="submit" value="Update" class="btn btn-default" />
            </p>
        </div>
    </form>
}
@if (ViewData["RowsAffected"] != null)
{
    <p>
        Number of rows updated: @ViewData["RowsAffected"]
    </p>
}
<div>
    @Html.ActionLink("Back to List", "Index")
</div>

Выполните метод UpdateCourseCredits, выбрав вкладку Courses (Курсы), а затем добавив "/UpdateCourseCredits" в конец URL-адреса в адресной строке браузера (например, http://localhost:5813/Courses/UpdateCourseCredits). Введите число в текстовое поле:

Update Course Credits page

Нажмите Обновить. Отобразится число обработанных строк:

Update Course Credits page rows affected

Нажмите кнопку Back to List (Вернуться к списку), чтобы просмотреть список курсов с измененным числом зачетных баллов.

Обратите внимание, что в рабочем коде необходимо всегда проверять допустимость новых данных. В приведенном здесь упрощенном коде в результате применения множителя могут получиться значения больше 5. (Свойство Credits имеет [Range(0, 5)] атрибут.) Запрос обновления будет работать, но недопустимые данные могут вызвать непредвиденные результаты в других частях системы, предполагающих, что количество кредитов равно 5 или меньше.

Дополнительные сведения о необработанных SQL-запросах см. в разделе Необработанные SQL-запросы.

Изучение SQL-запросов

В некоторых случаях полезно иметь возможность просмотреть фактические SQL-запросы, отправляемые в базу данных. Встроенные функции ведения журнала для ASP.NET Core автоматически используются EF Core для записи журналов, содержащих SQL для запросов и обновлений. В этом разделе приводятся некоторые примеры ведения журналов кода SQL.

Откройте файл StudentsController.cs и установите в методе Details точку останова на инструкции if (student == null).

Запустите приложение в режиме отладки и перейдите на страницу Details (Сведения) для учащегося.

Перейдите в окно вывода, в котором отображаются результаты отладки. В нем будет показан запрос:

Microsoft.EntityFrameworkCore.Database.Command:Information: Executed DbCommand (56ms) [Parameters=[@__id_0='?'], CommandType='Text', CommandTimeout='30']
SELECT TOP(2) [s].[ID], [s].[Discriminator], [s].[FirstName], [s].[LastName], [s].[EnrollmentDate]
FROM [Person] AS [s]
WHERE ([s].[Discriminator] = N'Student') AND ([s].[ID] = @__id_0)
ORDER BY [s].[ID]
Microsoft.EntityFrameworkCore.Database.Command:Information: Executed DbCommand (122ms) [Parameters=[@__id_0='?'], CommandType='Text', CommandTimeout='30']
SELECT [s.Enrollments].[EnrollmentID], [s.Enrollments].[CourseID], [s.Enrollments].[Grade], [s.Enrollments].[StudentID], [e.Course].[CourseID], [e.Course].[Credits], [e.Course].[DepartmentID], [e.Course].[Title]
FROM [Enrollment] AS [s.Enrollments]
INNER JOIN [Course] AS [e.Course] ON [s.Enrollments].[CourseID] = [e.Course].[CourseID]
INNER JOIN (
    SELECT TOP(1) [s0].[ID]
    FROM [Person] AS [s0]
    WHERE ([s0].[Discriminator] = N'Student') AND ([s0].[ID] = @__id_0)
    ORDER BY [s0].[ID]
) AS [t] ON [s.Enrollments].[StudentID] = [t].[ID]
ORDER BY [t].[ID]

Вы можете заметить удивительные результаты: код SQL выбирает до 2 строк (TOP(2)) из таблицы Person. Метод SingleOrDefaultAsync не разрешается в 1 строку на сервере. Для этого есть следующие причины.

  • Если запрос возвращает несколько строк, метод возвращает значение NULL.
  • Чтобы определить, будет ли запрос возвращать несколько строк, платформа EF проверяет, возвращаются ли как минимум 2 строки.

Обратите внимание, что вам необязательно использовать режим отладки и доходить до точки останова, чтобы просмотреть содержимое журнала в окне вывода. Это просто удобный способ остановить ведение журнала в тот момент, когда вам нужно просмотреть выходные данные. Если не сделать этого, ведение журнала продолжится и вам придется прокручивать окно до нужного места.

Создание уровня абстракции

Многие разработчики пишут код, реализующий шаблоны репозитория и единиц работы, в качестве оболочки для кода, работающего с платформой Entity Framework. Эти шаблоны позволяют создать уровень абстракции между уровнями доступа к данным и бизнес-логики приложения. Реализация таких шаблонов позволяет изолировать приложение от изменений в хранилище данных и упрощает автоматическое модульное тестирование или разработку на основе тестирования. Тем не менее написание дополнительного кода для реализации этих шаблонов не всегда подходит для приложений, которые используют платформу EF. Этому есть несколько причин:

  • Класс контекста EF сам по себе изолирует код от кода хранилища данных.

  • Класс контекста EF может выступать в качестве класса единиц работы для обновления базы данных, которые выполняются с помощью EF.

  • Платформа EF предусматривает функции для реализации разработки на основе тестирования, не требующие написания кода репозитория.

Дополнительные сведения о реализации шаблонов репозитория и единиц работы см. в версии этой серии учебников для платформы Entity Framework 5.

Платформа Entity Framework Core реализует выполняющийся в памяти поставщик базы данных, который может использоваться для тестирования. Дополнительные сведения см. в разделе Тестирование с помощью InMemory.

Автоматическое обнаружение изменений

Платформа Entity Framework определяет, как была изменена сущность (и, соответственно, какие обновления требуется отправить в базу данных), сравнивая текущие значения сущности с исходными. Исходные значения сохраняются при запросе или присоединении сущности. Ниже перечислены некоторые из методов, которые приводят к автоматическому обнаружению изменений:

  • DbContext.SaveChanges

  • DbContext.Entry

  • ChangeTracker.Entries

Если вы отслеживаете большое число сущностей и многократно вызываете один из этих методов в цикле, вы сможете добиться заметного повышения производительности, отключив автоматическое обнаружение изменений с помощью свойства ChangeTracker.AutoDetectChangesEnabled. Например:

_context.ChangeTracker.AutoDetectChangesEnabled = false;

EF Core планы исходного кода и разработки

Источник Entity Framework Core расположен на странице https://github.com/dotnet/efcore. Репозиторий EF Core содержит ночные сборки, отслеживание проблем, спецификации функций, заметки о проекте и стратегию развития. Вы также можете сообщать об ошибках, находить сведения об обнаруженных проблемах и участвовать в работе сообщества.

Несмотря на открытый исходный код, платформа Entity Framework Core полностью поддерживается как продукт корпорации Майкрософт. Команда Microsoft Entity Framework контролирует предложения участников, принимает их и тестирует любые изменения кода, чтобы обеспечить максимальное качество каждого выпуска.

Реконструирование из существующей базы данных

Чтобы реконструировать модель данных, включая классы сущностей, из существующей базы данных, используйте команду scaffold-dbcontext. Ознакомьтесь с учебником по началу работы.

Использование динамических запросов LINQ для упрощения кода

В третьем учебнике этой серии демонстрируется написание кода LINQ с жестко запрограммированными именами столбцов в инструкции switch. При наличии всего двух столбцов такой подход эффективен, однако если столбцов много, код может стать слишком громоздким. Чтобы устранить эту проблему, можно использовать метод EF.Property для указания имени свойства в виде строки. Чтобы попробовать этот подход, замените метод Index в StudentsController следующим кодом.

 public async Task<IActionResult> Index(
     string sortOrder,
     string currentFilter,
     string searchString,
     int? pageNumber)
 {
     ViewData["CurrentSort"] = sortOrder;
     ViewData["NameSortParm"] = 
         String.IsNullOrEmpty(sortOrder) ? "LastName_desc" : "";
     ViewData["DateSortParm"] = 
         sortOrder == "EnrollmentDate" ? "EnrollmentDate_desc" : "EnrollmentDate";

     if (searchString != null)
     {
         pageNumber = 1;
     }
     else
     {
         searchString = currentFilter;
     }

     ViewData["CurrentFilter"] = searchString;

     var students = from s in _context.Students
                    select s;
     
     if (!String.IsNullOrEmpty(searchString))
     {
         students = students.Where(s => s.LastName.Contains(searchString)
                                || s.FirstMidName.Contains(searchString));
     }

     if (string.IsNullOrEmpty(sortOrder))
     {
         sortOrder = "LastName";
     }

     bool descending = false;
     if (sortOrder.EndsWith("_desc"))
     {
         sortOrder = sortOrder.Substring(0, sortOrder.Length - 5);
         descending = true;
     }

     if (descending)
     {
         students = students.OrderByDescending(e => EF.Property<object>(e, sortOrder));
     }
     else
     {
         students = students.OrderBy(e => EF.Property<object>(e, sortOrder));
     }

     int pageSize = 3;
     return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(), 
         pageNumber ?? 1, pageSize));
 }

Благодарности

Этот учебник написан Томом Дайкстра (Tom Dykstra) и Риком Андерсоном (Rick Anderson) (twitter @RickAndMSFT)). Помощь в проверке кода для учебника и отладке проблем, возникавших при его написании, оказывали Роуэн Миллер (Rowan Miller), Диего Вега (Diego Vega) и другие участники команды Entity Framework. Джон Парент (John Parente) и Пол Голдмен (Paul Goldman) обновили это руководство для версии ASP.NET Core 2.2.

Устранение распространенных ошибок

Библиотека ContosoUniversity.dll используется другим процессом

Сообщение об ошибке:

Не удается открыть файл "...bin\Debug\netcoreapp1.0\ContosoUniversity.dll" для записи — "Процесс не может получить доступ к файлу "...\bin\Debug\netcoreapp1.0\ContosoUniversity.dll", так как этот файл занят другим процессом.

Решение.

Остановите сайт в IIS Express. Найдите значок IIS Express в панели задач Windows, щелкните его правой кнопкой мыши, выберите сайт университета Contoso и затем выберите Остановить сайт.

Формирование шаблонов миграции без кода в методах Up и Down

Возможная причина:

Команды интерфейса командной строки EF не выполняют автоматическое закрытие и сохранение файлов кода. Если при выполнении команды migrations add у вас есть несохраненные изменения, платформа EF не сможет обнаружить их.

Решение.

Выполните команду migrations remove, сохраните изменения в коде и повторно выполните команду migrations add.

Ошибки во время обновления базы данных

При изменении схемы в базе, содержащей существующие данные, возможны другие ошибки. Если вы получаете ошибки миграции, которые не удается устранить, измените имя базы данных в строке подключения или удалите базу данных. В новой базе не будет данных, которые требуется перенести, в результате чего команда обновления базы данных с большей долей вероятности завершится без ошибок.

Проще всего в этом случае переименовать базу данных в файле appsettings.json. В следующий раз при выполнении команды database update будет создана новая база данных.

Чтобы удалить базу данных в средстве SSOX, щелкните ее правой кнопкой мыши, выберите Удалить, а затем в диалоговом окне Удаление базы данных выберите Закрыть существующие соединения и нажмите кнопку ОК.

Чтобы удалить базу данных с помощью интерфейса командной строки, выполните команду database drop:

dotnet ef database drop

Ошибка при обнаружении экземпляра SQL Server

Сообщение об ошибке:

При подключении к SQL Server произошла ошибка, связанная с сетью или с определенным экземпляром. Сервер не найден или недоступен. Проверьте правильность имени экземпляра и настройку сервера SQL Server для удаленных подключений. (поставщик: сетевые интерфейсы SQL, ошибка: 26 — ошибка при обнаружении указанного сервера или экземпляра)

Решение.

Проверьте строку подключения. Если вы вручную удалили файл базы данных, измените имя базы данных в строке подключения, чтобы начать работу с новой базой.

Получение кода

Скачайте или ознакомьтесь с готовым приложением.

Дополнительные ресурсы

Дополнительные сведения см EF Core. в документации Entity Framework Core. Также вы можете ознакомиться с книгой, посвященной практическому применению Entity Framework Core.

Дополнительные сведения о развертывании веб-приложения см. в разделе Размещение и развертывание ASP.NET Core.

Дополнительные сведения по другим вопросам, связанным с использованием ASP.NET Core MVC, включая способы проверки подлинности и авторизации, см. в статье Общие сведения об ASP.NET Core.

Следующие шаги

Изучив это руководство, вы:

  • Выполнение прямых SQL-запросов
  • Вызов запроса для получения сущностей
  • Вызов запроса для получения других типов
  • Вызов запроса на обновление
  • Изучение SQL-запросов
  • Создание уровня абстракции
  • Сведения об автоматическом обнаружении изменений
  • Сведения о планах EF Core разработки и исходного кода
  • Сведения об использовании динамических запросов LINQ для упрощения кода

На этом серия учебников, посвященных использованию платформы Entity Framework Core в приложении ASP.NET Core MVC, завершена. В этой серии мы работали с новой базой данных; альтернативой является реконструирование базы данных в модель.