Создание модели с проверками бизнес-правил
от Майкрософт
Это шаг 3 бесплатного руководства по приложению "NerdDinner" , в которых показано, как создать небольшое, но готовое веб-приложение с помощью ASP.NET MVC 1.
На шаге 3 показано, как создать модель, которую можно использовать для запроса и обновления базы данных для приложения NerdDinner.
Если вы используете ASP.NET MVC 3, рекомендуется следовать руководствам по начало работы С MVC 3 или MVC Music Store.
NerdDinner, шаг 3. Построение модели
На платформе model-view-controller термин "модель" относится к объектам, представляющим данные приложения, а также к соответствующей логике предметной области, которая интегрирует с ним проверку и бизнес-правила. Модель во многих отношениях является "сердцем" приложения на основе MVC и, как мы увидим позже, фундаментально управляет ее поведением.
Платформа ASP.NET MVC поддерживает использование любой технологии доступа к данным, и разработчики могут выбрать из множества многофункциональных вариантов данных .NET для реализации своих моделей, включая: LINQ to Entities, LINQ to SQL, NHibernate, LLBLGen Pro, SubSonic, WilsonORM или просто необработанные ADO.NET DataReaders или DataSets.
Для нашего приложения NerdDinner мы будем использовать LINQ to SQL для создания простой модели, которая довольно точно соответствует нашей структуре базы данных, и добавим некоторые пользовательские логики проверки и бизнес-правила. Затем мы реализуем класс репозитория, который помогает абстрагировать реализацию сохраняемости данных от остальной части приложения и позволяет легко выполнить модульное тестирование.
LINQ to SQL
LINQ to SQL — это ORM (объектно-реляционный сопоставителя), который поставляется в составе .NET 3.5.
LINQ to SQL предоставляет простой способ сопоставления таблиц базы данных с классами .NET, которые можно использовать в коде. Для нашего приложения NerdDinner мы будем использовать его для сопоставления таблиц Dinners и RSVP в нашей базе данных с классами Dinner и RSVP. Столбцы таблиц Dinners и RSVP будут соответствовать свойствам классов Dinner и RSVP. Каждый объект Dinner и RSVP представляет отдельную строку в таблицах Dinners или RSVP в базе данных.
LINQ to SQL позволяет избежать необходимости вручную создавать инструкции SQL для получения и обновления объектов Dinner и RSVP с данными базы данных. Вместо этого мы определим классы Dinner и RSVP, способ их сопоставления с базой данных и из нее, а также связи между ними. LINQ to SQL затем позаботится о создании соответствующей логики выполнения SQL, которая будет использоваться во время выполнения при взаимодействии с ними.
Мы можем использовать поддержку языка LINQ в VB и C# для написания выразительных запросов, которые извлекают объекты Dinner и RSVP из базы данных. Это сводит к минимуму объем кода данных, необходимого для написания, и позволяет нам создавать действительно чистые приложения.
Добавление классов LINQ to SQL в проект
Начнем с щелчка правой кнопкой мыши в папке Models в проекте и выберите команду меню Добавить новый> элемент :
Откроется диалоговое окно "Добавление нового элемента". Мы отфильтруем по категории "Данные" и выберем в ней шаблон "LINQ to SQL Classes" (Классы LINQ to SQL):
Мы присвоим элементу имя "NerdDinner" и нажмите кнопку "Добавить". Visual Studio добавит файл NerdDinner.dbml в каталог \Models, а затем откроет реляционный конструктор объектов LINQ to SQL:
Создание классов модели данных с помощью LINQ to SQL
LINQ to SQL позволяет быстро создавать классы модели данных из существующей схемы базы данных. Для этого мы откроем базу данных NerdDinner в Обозреватель сервера и выберите в ней таблицы, которые нужно моделировать:
Затем можно перетащить таблицы в область конструктора LINQ to SQL. При этом LINQ to SQL автоматически создает классы Dinner и RSVP, используя схему таблиц (со свойствами классов, которые сопоставляют со столбцами таблицы базы данных):
По умолчанию конструктор LINQ to SQL автоматически "во множественном числе" имен таблиц и столбцов при создании классов на основе схемы базы данных. Например, таблица "Dinners" в приведенном выше примере привела к созданию класса "Dinner". Такое именование классов помогает обеспечить согласованность моделей с соглашениями об именовании .NET, и я обычно считаю, что это удобно исправить в конструкторе (особенно при добавлении большого количества таблиц). Если вам не нравится имя класса или свойства, создаваемого конструктором, вы всегда можете переопределить их и изменить на любое имя. Это можно сделать, изменив имя сущности или свойства в конструкторе или изменив его с помощью сетки свойств.
По умолчанию конструктор LINQ to SQL также проверяет связи между первичным ключом и внешним ключом таблиц и на их основе автоматически создает связи по умолчанию между различными классами моделей, которые он создает. Например, при перетаскивании таблиц Dinners и RSVP в конструктор LINQ to SQL связь "один ко многим" между ними была выведена на основе того факта, что таблица RSVP имеет внешний ключ к таблице Dinners (это указывает стрелка в конструкторе):
Приведенная выше связь приведет к тому, что LINQ to SQL добавит строго типизированное свойство "Dinner" в класс RSVP, которое разработчики могут использовать для доступа к Dinner, связанному с заданным RSVP. Это также приведет к тому, что класс Dinner будет иметь свойство коллекции "RSVPs", которое позволяет разработчикам получать и обновлять объекты RSVP, связанные с конкретным Dinner.
Ниже приведен пример intellisense в Visual Studio при создании нового объекта RSVP и его добавлении в коллекцию RSVP Dinner. Обратите внимание, что LINQ to SQL автоматически добавил коллекцию RSVPs в объект Dinner:
Добавляя объект RSVP в коллекцию RSVPs Dinner, мы сообщаем LINQ to SQL связать связь внешнего ключа между Dinner и строкой RSVP в нашей базе данных:
Если вам не нравится, как конструктор смоделировал или назвал связь таблицы, ее можно переопределить. Просто щелкните стрелку связи в конструкторе и получите доступ к ее свойствам через сетку свойств, чтобы переименовать, удалить или изменить ее. Однако для нашего приложения NerdDinner правила ассоциации по умолчанию хорошо работают для классов модели данных, которые мы создаем, и мы можем просто использовать поведение по умолчанию.
Класс NerdDinnerDataContext
Visual Studio автоматически создает классы .NET, представляющие модели и связи баз данных, определенные с помощью конструктора LINQ to SQL. Класс DataContext LINQ to SQL также создается для каждого файла конструктора LINQ to SQL, добавленного в решение. Так как мы назвали элемент класса LINQ to SQL "NerdDinner", созданный класс DataContext будет называться "NerdDinnerDataContext". Этот класс NerdDinnerDataContext является основным способом взаимодействия с базой данных.
Наш класс NerdDinnerDataContext предоставляет два свойства — "Dinners" и "RSVPs", которые представляют две таблицы, которые мы моделировали в базе данных. Мы можем использовать C# для написания запросов LINQ к этим свойствам, чтобы запрашивать и извлекать объекты Dinner и RSVP из базы данных.
В следующем коде показано, как создать экземпляр объекта NerdDinnerDataContext и выполнить к нему запрос LINQ, чтобы получить последовательность ужинов, которые произойдут в будущем. Visual Studio предоставляет полную intellisense при написании запроса LINQ, а возвращаемые из него объекты строго типизированы и поддерживают intellisense:
Помимо возможности запрашивать объекты Dinner и RSVP, NerdDinnerDataContext также автоматически отслеживает любые изменения, которые мы впоследствии внося в объекты Dinner и RSVP, которые мы получаем через него. Эту функцию можно использовать для простого сохранения изменений в базе данных без необходимости писать явный код обновления SQL.
Например, в приведенном ниже коде показано, как использовать запрос LINQ для получения одного объекта Dinner из базы данных, обновления двух свойств Dinner, а затем сохранения изменений в базе данных:
NerdDinnerDataContext db = new NerdDinnerDataContext();
// Retrieve Dinner object that reprents row with DinnerID of 1
Dinner dinner = db.Dinners.Single(d => d.DinnerID == 1);
// Update two properties on Dinner
dinner.Title = "Changed Title";
dinner.Description = "This dinner will be fun";
// Persist changes to database
db.SubmitChanges();
Объект NerdDinnerDataContext в приведенном выше коде автоматически отслеживал изменения свойств, внесенные в объект Dinner, который мы извлекли из него. При вызове метода SubmitChanges()он выполняет соответствующую инструкцию SQL UPDATE для базы данных, чтобы сохранить обновленные значения.
Создание класса DinnerRepository
Для небольших приложений иногда бывает хорошо, чтобы контроллеры работали непосредственно с классом DataContext LINQ to SQL и внедряли запросы LINQ в контроллеры. Однако по мере того как приложения становятся больше, этот подход становится громоздким для обслуживания и тестирования. Это также может привести к дублированию одних и того же запроса LINQ в нескольких местах.
Один из подходов, упрощающих обслуживание и тестирование приложений, заключается в использовании шаблона "репозитория". Класс репозитория помогает инкапсулировать логику запросов и сохраняемости данных, а также абстрагирует сведения о реализации сохраняемости данных из приложения. Помимо очистки кода приложения, использование шаблона репозитория может упростить изменение реализаций хранилища данных в будущем и упростить модульное тестирование приложения без реальной базы данных.
Для нашего приложения NerdDinner мы определим класс DinnerRepository со следующей сигнатурой:
public class DinnerRepository {
// Query Methods
public IQueryable<Dinner> FindAllDinners();
public IQueryable<Dinner> FindUpcomingDinners();
public Dinner GetDinner(int id);
// Insert/Delete
public void Add(Dinner dinner);
public void Delete(Dinner dinner);
// Persistence
public void Save();
}
Примечание. Далее в этой главе мы извлекаем интерфейс IDinnerRepository из этого класса и включим с ним внедрение зависимостей на контроллерах. Для начала, однако, мы собираемся начать просто и просто работать непосредственно с классом DinnerRepository.
Чтобы реализовать этот класс, щелкните правой кнопкой мыши папку Models и выберите команду меню Добавить новый> элемент . В диалоговом окне "Добавление нового элемента" мы выберем шаблон "Класс" и назовем файл "DinnerRepository.cs":
Затем мы можем реализовать класс DinnerRepository, используя следующий код:
public class DinnerRepository {
private NerdDinnerDataContext db = new NerdDinnerDataContext();
//
// Query Methods
public IQueryable<Dinner> FindAllDinners() {
return db.Dinners;
}
public IQueryable<Dinner> FindUpcomingDinners() {
return from dinner in db.Dinners
where dinner.EventDate > DateTime.Now
orderby dinner.EventDate
select dinner;
}
public Dinner GetDinner(int id) {
return db.Dinners.SingleOrDefault(d => d.DinnerID == id);
}
//
// Insert/Delete Methods
public void Add(Dinner dinner) {
db.Dinners.InsertOnSubmit(dinner);
}
public void Delete(Dinner dinner) {
db.RSVPs.DeleteAllOnSubmit(dinner.RSVPs);
db.Dinners.DeleteOnSubmit(dinner);
}
//
// Persistence
public void Save() {
db.SubmitChanges();
}
}
Получение, обновление, вставка и удаление с помощью класса DinnerRepository
Теперь, когда мы создали класс DinnerRepository, давайте рассмотрим несколько примеров кода, демонстрирующих типичные задачи, которые можно выполнить с ним:
Примеры запросов
Приведенный ниже код извлекает один dinner с помощью значения DinnerID:
DinnerRepository dinnerRepository = new DinnerRepository();
// Retrieve specific dinner by its DinnerID
Dinner dinner = dinnerRepository.GetDinner(5);
Приведенный ниже код извлекает все предстоящие ужины и циклы по ним:
DinnerRepository dinnerRepository = new DinnerRepository();
// Retrieve all upcoming Dinners
var upcomingDinners = dinnerRepository.FindUpcomingDinners();
// Loop over each upcoming Dinner and print out its Title
foreach (Dinner dinner in upcomingDinners) {
Response.Write("Title" + dinner.Title);
}
Примеры вставки и обновления
В приведенном ниже коде показано добавление двух новых ужинов. Добавление и изменение репозитория не фиксируется в базе данных, пока в ней не будет вызван метод Save(). LINQ to SQL автоматически создает оболочку для всех изменений в транзакции базы данных, поэтому либо все изменения происходят, либо ни один из них не выполняется при сохранении репозитория:
DinnerRepository dinnerRepository = new DinnerRepository();
// Create First Dinner
Dinner newDinner1 = new Dinner();
newDinner1.Title = "Dinner with Scott";
newDinner1.HostedBy = "ScotGu";
newDinner1.ContactPhone = "425-703-8072";
// Create Second Dinner
Dinner newDinner2 = new Dinner();
newDinner2.Title = "Dinner with Bill";
newDinner2.HostedBy = "BillG";
newDinner2.ContactPhone = "425-555-5151";
// Add Dinners to Repository
dinnerRepository.Add(newDinner1);
dinnerRepository.Add(newDinner2);
// Persist Changes
dinnerRepository.Save();
Приведенный ниже код извлекает существующий объект Dinner и изменяет два свойства в нем. Изменения фиксируются обратно в базу данных при вызове метода Save()в нашем репозитории:
DinnerRepository dinnerRepository = new DinnerRepository();
// Retrieve specific dinner by its DinnerID
Dinner dinner = dinnerRepository.GetDinner(5);
// Update Dinner properties
dinner.Title = "Update Title";
dinner.HostedBy = "New Owner";
// Persist changes
dinnerRepository.Save();
Приведенный ниже код извлекает ужин, а затем добавляет в него RSVP. Для этого используется коллекция RSVP в объекте Dinner, который LINQ to SQL создан для нас (так как между ними в базе данных существует связь первичный и внешний ключ). Это изменение сохраняется в базе данных в виде новой строки таблицы RSVP при вызове метода Save()в репозитории:
DinnerRepository dinnerRepository = new DinnerRepository();
// Retrieve specific dinner by its DinnerID
Dinner dinner = dinnerRepository.GetDinner(5);
// Create a new RSVP object
RSVP myRSVP = new RSVP();
myRSVP.AttendeeName = "ScottGu";
// Add RSVP to Dinner's RSVP Collection
dinner.RSVPs.Add(myRSVP);
// Persist changes
dinnerRepository.Save();
Пример удаления
Приведенный ниже код извлекает существующий объект Dinner, а затем помечает его для удаления. При вызове метода Save()в репозитории выполняется фиксация удаления обратно в базу данных:
DinnerRepository dinnerRepository = new DinnerRepository();
// Retrieve specific dinner by its DinnerID
Dinner dinner = dinnerRepository.GetDinner(5);
// Mark dinner to be deleted
dinnerRepository.Delete(dinner);
// Persist changes
dinnerRepository.Save();
Интеграция логики проверки и бизнес-правил с классами моделей
Интеграция логики проверки и бизнес-правил является ключевой частью любого приложения, которое работает с данными.
Проверка схемы
Если классы модели определяются с помощью конструктора LINQ to SQL, типы данных свойств в классах модели данных соответствуют типам данных таблицы базы данных. Например, если столбец EventDate в таблице Dinners является datetime, класс модели данных, созданный LINQ to SQL, будет иметь тип DateTime (встроенный тип данных .NET). Это означает, что при попытке назначить ему целочисленное или логическое значение из кода будут возникать ошибки компиляции. При попытке неявного преобразования недопустимого типа строки в него во время выполнения автоматически возникает ошибка.
LINQ to SQL также автоматически обрабатывает экранирование значений SQL при использовании строк, что помогает защититься от атак путем внедрения кода SQL.
Проверка и логика бизнес-правил
Проверка схемы полезна в качестве первого шага, но редко бывает достаточной. В большинстве реальных сценариев требуется возможность указать более расширенную логику проверки, которая может охватывать несколько свойств, выполнять код и часто иметь представление о состоянии модели (например, создается ли она /обновляется/удаляется или находится в состоянии конкретного домена, например "archived"). Существует множество различных шаблонов и платформ, которые можно использовать для определения и применения правил проверки к классам моделей, и существует несколько платформ на основе .NET, которые можно использовать для этого. Вы можете использовать практически любой из них в ASP.NET приложений MVC.
В целях нашего приложения NerdDinner мы будем использовать относительно простой и прямой шаблон, в котором мы будем предоставлять свойство IsValid и метод GetRuleViolations() в объекте модели Dinner. Свойство IsValid возвращает значение true или false в зависимости от того, допустимы ли все правила проверки и бизнес-правила. Метод GetRuleViolations() возвращает список ошибок правил.
Мы реализуем IsValid и GetRuleViolations() для модели Dinner, добавив в проект разделяемый класс. Разделяемые классы можно использовать для добавления методов, свойств и событий в классы, поддерживаемые конструктором VS (например, класс Dinner, созданный конструктором LINQ to SQL), чтобы избежать проблем с нашим кодом. Мы можем добавить в проект новый разделяемый класс, щелкнув правой кнопкой мыши папку \Models и выбрав команду меню "Добавить новый элемент". Затем можно выбрать шаблон "Класс" в диалоговом окне "Добавление нового элемента" и присвоить ему имя Dinner.cs.
Нажатие кнопки "Добавить" добавит файл Dinner.cs в наш проект и откроет его в интегрированной среде разработки. Затем мы можем реализовать базовую платформу применения правил и проверки, используя следующий код:
public partial class Dinner {
public bool IsValid {
get { return (GetRuleViolations().Count() == 0); }
}
public IEnumerable<RuleViolation> GetRuleViolations() {
yield break;
}
partial void OnValidate(ChangeAction action) {
if (!IsValid)
throw new ApplicationException("Rule violations prevent saving");
}
}
public class RuleViolation {
public string ErrorMessage { get; private set; }
public string PropertyName { get; private set; }
public RuleViolation(string errorMessage, string propertyName) {
ErrorMessage = errorMessage;
PropertyName = propertyName;
}
}
Несколько примечаний к приведенному выше коду:
- Класс Dinner предваряется "частичным" ключевое слово. Это означает, что код, содержащийся в нем, будет объединен с классом, созданным или поддерживаемым конструктором LINQ to SQL, и скомпилирован в один класс.
- Класс RuleViolation — это вспомогательный класс, который мы добавим в проект, который позволяет предоставить дополнительные сведения о нарушении правила.
- Метод Dinner.GetRuleViolations() приводит к оценке нашей проверки и бизнес-правил (мы реализуем их в ближайшее время). Затем он возвращает последовательность объектов RuleViolation, которые предоставляют дополнительные сведения об ошибках правил.
- Свойство Dinner.IsValid предоставляет удобное вспомогательное свойство, указывающее, имеет ли объект Dinner какие-либо активные свойства RuleViolation. Он может быть предварительно проверен разработчиком с помощью объекта Dinner в любое время (и не вызывает исключения).
- Частичный метод Dinner.OnValidate() — это перехватчик, который предоставляет LINQ to SQL, который позволяет получать уведомления в любое время, когда объект Dinner будет сохранен в базе данных. Приведенная выше реализация OnValidate() гарантирует, что у dinner не будет правил RuleViolation перед сохранением. Если он находится в недопустимом состоянии, возникает исключение, которое приведет к прерыванию транзакции LINQ to SQL.
Этот подход предоставляет простую платформу, в которую можно интегрировать проверку и бизнес-правила. Теперь добавим приведенные ниже правила в метод GetRuleViolations().
public IEnumerable<RuleViolation> GetRuleViolations() {
if (String.IsNullOrEmpty(Title))
yield return new RuleViolation("Title required","Title");
if (String.IsNullOrEmpty(Description))
yield return new RuleViolation("Description required","Description");
if (String.IsNullOrEmpty(HostedBy))
yield return new RuleViolation("HostedBy required", "HostedBy");
if (String.IsNullOrEmpty(Address))
yield return new RuleViolation("Address required", "Address");
if (String.IsNullOrEmpty(Country))
yield return new RuleViolation("Country required", "Country");
if (String.IsNullOrEmpty(ContactPhone))
yield return new RuleViolation("Phone# required", "ContactPhone");
if (!PhoneValidator.IsValidNumber(ContactPhone, Country))
yield return new RuleViolation("Phone# does not match country", "ContactPhone");
yield break;
}
Мы используем функцию возврата доходности В# для возврата последовательности любых RuleViolations. Первые шесть проверок правил, приведенных выше, просто гарантируют, что строковые свойства в нашем Dinner не могут быть пустыми или пустыми. Последнее правило немного интереснее и вызывает вспомогательный метод PhoneValidator.IsValidNumber(), который можно добавить в проект, чтобы убедиться, что формат номера ContactPhone соответствует стране или региону ужина.
Мы можем использовать . Поддержка регулярных выражений NET для реализации этой поддержки проверки телефона. Ниже приведена простая реализация PhoneValidator, которую можно добавить в наш проект, которая позволяет добавлять проверки шаблонов регулярных выражений для конкретных стран или регионов:
public class PhoneValidator {
static IDictionary<string, Regex> countryRegex = new Dictionary<string, Regex>() {
{ "USA", new Regex("^[2-9]\\d{2}-\\d{3}-\\d{4}$")},
{ "UK", new Regex("(^1300\\d{6}$)|(^1800|1900|1902\\d{6}$)|(^0[2|3|7|8]{1}[0-9]{8}$)|(^13\\d{4}$)|(^04\\d{2,3}\\d{6}$)")},
{ "Netherlands", new Regex("(^\\+[0-9]{2}|^\\+[0-9]{2}\\(0\\)|^\\(\\+[0-9]{2}\\)\\(0\\)|^00[0-9]{2}|^0)([0-9]{9}$|[0-9\\-\\s]{10}$)")},
};
public static bool IsValidNumber(string phoneNumber, string country) {
if (country != null && countryRegex.ContainsKey(country))
return countryRegex[country].IsMatch(phoneNumber);
else
return false;
}
public static IEnumerable<string> Countries {
get {
return countryRegex.Keys;
}
}
}
Обработка нарушений проверки и бизнес-логики
Теперь, когда мы добавили приведенный выше код проверки и бизнес-правил, каждый раз, когда мы пытаемся создать или обновить Dinner, наши правила логики проверки будут оцениваться и применяться.
Разработчики могут написать код, как показано ниже, чтобы заранее определить, является ли объект Dinner допустимым, и получить список всех нарушений в нем без каких-либо исключений:
Dinner dinner = dinnerRepository.GetDinner(5);
dinner.Country = "USA";
dinner.ContactPhone = "425-555-BOGUS";
if (!dinner.IsValid) {
var errors = dinner.GetRuleViolations();
// do something to fix the errors
}
Если мы попытаемся сохранить Dinner в недопустимом состоянии, при вызове метода Save() в DinnerRepository возникнет исключение. Это происходит потому, что LINQ to SQL автоматически вызывает разделяемый метод Dinner.OnValidate() перед сохранением изменений, внесенных в Dinner.OnValidate(), и мы добавили код в Dinner.OnValidate(), чтобы вызвать исключение, если в Dinner имеются нарушения правил. Мы можем перехватывать это исключение и реактивно получать список нарушений, которые необходимо исправить:
Dinner dinner = dinnerRepository.GetDinner(5);
try {
dinner.Country = "USA";
dinner.ContactPhone = "425-555-BOGUS";
dinnerRepository.Save();
}
catch {
var errors = dinner.GetRuleViolations();
// do something to fix errors
}
Так как наши правила проверки и бизнес-правила реализуются на уровне модели, а не на уровне пользовательского интерфейса, они будут применяться и использоваться во всех сценариях в приложении. Позже мы можем изменить или добавить бизнес-правила, чтобы весь код, который работает с нашими объектами Dinner, соблюдал их.
Гибкость для изменения бизнес-правил в одном месте без изменения этих изменений в приложении и логике пользовательского интерфейса является признаком хорошо написанного приложения и преимущества, которое платформа MVC помогает поощрять.
Следующий шаг
Теперь у нас есть модель, которую можно использовать как для запроса, так и для обновления базы данных.
Теперь добавим в проект несколько контроллеров и представлений, которые можно использовать для создания интерфейса пользовательского интерфейса HTML.