Доступ к данным

Обработка проверок Entity Framework в WCF Data Services

Джули Лерман

 

Julie Lerman

В этой статье обсуждается CTP-версия WCF Data Services за март 2011 г. Любая изложенная здесь информация может быть изменена.

Я пишу эту статью по горячим следам конференции Microsoft BUILD. В центре внимания этой конференции, конечно, был новый Metro UI для Windows 8, который размещается поверх новой исполняющей среды Windows Runtime (WinRT). Если вы специализируетесь на работе с данными, то, вероятно, уже знаете существующие варианты для предоставления данных приложениям в «стиле Metro». В этой ранней предварительной версии вы можете предоставлять данные из файлового хранилища или из Web. Если вам нужно взаимодействовать с реляционными данными, то варианты на основе Web включают XML или JSON поверх HTTP, сокеты и сервисы. На фронте сервисов приложения в стиле Metro будут содержать клиентские библиотеки для использования OData, т. е. существующий опыт работы с OData через Microsoft .NET Framework, Silverlight или другие клиентские библиотеки даст вам большое преимущество, когда вы будете готовы к применению OData в своих Metro-приложениях.

Учитывая это, сегодняшнюю статью я хочу посвятить работе с OData. В версии Entity Framework (EF), которая содержит Code First и DbContext, был введен новый Validation API. Я покажу, как задействовать преимущества встроенной поддержки проверок на серверной стороне, когда ваша EF-модель Code First предоставляется в формате OData через WCF Data Services.

Основы Validation API

Возможно, вы уже знакомы с атрибутами настройки, такими как Required или MaxLength, для свойств классов, использующих Data Annotations или Fluent API. Эти атрибуты можно проверять автоматически с помощью нового Validation API. Статья «Entity Framework 4.1 Validation», появившаяся в MSDN Data Developer Center (msdn.microsoft.com/data/gg193959), демонстрирует это, а также то, как применять правила через интерфейс IValidatableObject и метод ValidateEntity. Хотя вы почти наверняка уже выполняете проверки Data Annotations и IValidatableObject на клиентской стороне, их правила можно проверять и на серверной стороне с участием любой добавленной вами логики ValidateEntity. В качестве альтернативы можно также инициировать проверку по требованию в вашем серверном коде.

Вот, например, простой класс Person, который использует две аннотации данных (Data Annotations) (первая указывает, что свойство LastName обязательно, а вторая задает максимальную длину строкового поля IdentityCard):

По умолчанию EF будет выполнять проверку при вызове SaveChanges. Если любое из этих правил не будет соблюдено, EF сгенерирует исключение Sys¬tem.Da¬ta.Ent¬ity.DbEntityValidationException — это, кстати, очень интересная структура. Каждая ошибка проверки описывается в DbValidationError, а DbValidationErrors группируются по экземпляру объекта в наборы EntityValidationErrors.

Например, на рис. 1 показано DbEntityValidationException, которое было бы сгенерировано, если бы EF обнаружил проблемы с двумя разными экземплярами Person в ходе проверки. Первый объект EntityValidationErrors содержит набор DbValidationErrors для одного экземпляра Person, где было две ошибки: отсутствует LastName и слишком много символов в IdentityCard. Во втором экземпляре Person обнаружена одна проблема; поэтому во втором объекте EntityValidationErrors присутствует только один DbValidationError.

DbEntityValidationException Contains Grouped Sets of Errors
Рис. 1. DbEntityValidationException содержит сгруппированные наборы ошибок

В упомянутой статье из MSDN Data Developer Center я показала возврат исключения в приложение Model-View-¬Controller (MVC), которому известно, как обнаруживать и отображать специфические ошибки.

Однако в распределенном приложении передать ошибки клиентской стороне может оказаться далеко не так просто. Хотя исключение верхнего уровня, возможно, удастся вернуть, клиентское приложение не будет знать, как добраться до DbEntityValidationException, чтобы найти ошибки. В случае многих приложений у вас может даже не быть доступа к пространству имен System.Data.Entity, а значит, вы ничего не узнаете о DbEntityValidationException.

Еще одну проблему создает то, как WCF Data Services передает исключения по умолчанию. На клиентской стороне вы получаете только сообщение «An error occurred while processing this request» («При обработке данного запроса произошла ошибка»). Но ключевые слова здесь: «по умолчанию». Вы можете настроить WCF Data Services на разбор DbEntityValidationExceptions и возврат клиенту полезной информации об ошибке. Именно на этом я и сосредоточусь.

WCF-сервис данных по умолчанию скрывает ошибки проверки

Моя модель размещается на уровне данных DbContext, который я назвала PersonModelContext:

У меня есть простой сервис данных, который предоставляет тип Person из этого контекста для чтения и записи:

Поскольку я использую Code First, то сначала должна была бы внести ряд изменений, чтобы заставить WCF Data Services работать с этой моделью. Но вместо этого я заменила System.Data.Services и System.Data.ClientServices из Microsoft .NET Framework 4 на библиотеки Microsoft.Data.Services и Microsoft.Data.ClientServices из WCF Data Services CTP за март 2011 г. (bit.ly/mTI69m), в которую уже внесены необходимые изменения. Вот почему DataServiceProtocolVersion устанавливается равным V3.

Наконец, я использую сервис в простом консольном приложении, в котором для вставки Person применяется следующий метод:

Заметьте, что я пренебрегла настройкой свойства LastName. Поскольку LastName сконфигурировано как обязательное, EF сгенерирует исключение для сервиса данных, но консольное приложение просто получит DataServiceRequestException с приведенным ранее сообщением («An error occurred while processing this request»). Если вы доберетесь до вложенного исключения, то обнаружите, что оно содержит то же сообщение и никаких дополнительных сведений в нем нет.

В WCF Data Services есть возможность возвращать более детализированные сообщения об ошибках. Для этого добавьте в метод InitializeService следующее:

Теперь вложенное сообщение (содержащееся в XML-ответе от сервиса) гласит: «Validation failed for one or more entities. See 'EntityValidationErrors' property for more details» («Проверка для одной или более сущностей завершилась неудачно. Детали см. в свойстве EntityValidationErrors»). Но, к сожалению, EntityValidationErrors не передается с исключением. Поэтому вы узнаете, что Validation API обнаружил одну или более проблем, но больше ничего выяснить об ошибке вам не удастся. Заметьте, что я обернула UseVerboseErrors в директиву компилятора. UseVerboseErrors следует использовать только для отладки — в производственном коде этого не должно быть.

Переопределение метода HandleException

WCF Data Services предоставляет виртуальный (переопределяемый) метод HandleException. Это позволяет захватывать любое исключение, возникающее в сервисе, анализировать его и конструировать собственное исключение DataServiceException для возврата вызвавшему коду. Именно в этом методе вы можете разобрать любые ошибки проверки и передать более осмысленную информацию клиентскому приложению. Сигнатура этого метода выглядит так:

Тип HandleExceptionArgs имеет ряд свойств: Exception, ResponseContentType, ResponseStatusCode, ResponseWritten, UseVerboseErrors. Меня интересует свойство Exception. Оно позволяет захватывать и идентифицировать исключения, генерируемые Validation API, — DbEntityValidationException. Здесь можно обрабатывать и любые другие типы ошибок, но я сейчас говорю только о распознавании и разборе исключений проверки. В начале класса в моих выражениях using присутствует пространство имен System.Data.Entity.Validation, поэтому мне не нужна строгая типизация исключения.

Я начну с предположения, что проверяется только одна сущность, поэтому я буду запрашивать лишь первый Entity¬ValidationErrors, содержащийся в исключении, как показано на рис. 2. Если вам нужно, чтобы сервис проверял несколько объектов, не забудьте о передаче параметра SaveChangesOptions.Batch при вызове SaveChanges. Иначе в каждый момент времени будет сохраняться и проверяться только один объект, а когда возникнет ошибка, никакие объекты больше не будут ни сохраняться, ни проверяться.

Рис. 2. Формирование более полезного сообщения для исключения

В этом методе я сначала проверяю, относится ли исключение к тому типу, который генерируется Validation API. Если да, я извлекаю исключение в переменную ex. Затем я запрашиваю список всех DbValidationErrors, содержащихся в первом наборе EntityValidationErrors в исключении. Далее я формирую новую строку текста об ошибке, используя свойство ErrorMessage каждого EntityValidationError и передаю эту строку вызвавшему приложению в новом DataServiceException. В EntityValidationError есть и другие свойства, но он составляет полное сообщение об ошибке, используя имя свойства и описание проблемы при проверке, а потом записывает его в ErrorMessage. В случае Code First вы можете задать собственное сообщение об ошибке, но для данной демонстрации меня вполне устраивает вариант по умолчанию. В этом примере сообщение выглядит так: «The LastName field is required» («Поле LastName является обязательным»). Заметьте, что у конструктора DataServiceException есть целый ряд перегруженных версий. Я стараюсь не усложнять картину и просто передаю код внутренней ошибки сервера 500 вместе со строкой, которая содержит необходимое сообщение.

Разбор нового исключения на клиентской стороне

Теперь на клиентской стороне вы все равно получите исключение с сообщением «An error occurred while processing this request», но на этот раз вложенное исключение содержит сообщение «The LastName field is required».

Но это не простая строка. Сообщение DataServiceRequestException форматируется как HTTP Response, поскольку запрос осуществлялся по HTTP:

Одна из перегруженных версий для DataServiceException, который я конструирую в сервисе, позволяет вставлять собственные коды ошибок. Если бы я воспользовалась этим, собственный код появился бы в элементе <code> ошибки. Если вы вызываете сервис из веб-приложения, то сможете вывести HTTP-ответ непосредственно в своем UI. В ином случае вы скорее всего предпочтете разобрать его, чтобы иметь возможность обработать исключение по любому шаблону, применяемому в вашем приложении.

Для получения сообщения я использую LINQ to XML, а затем могу вывести его в своем консольном приложении. Я вызываю SaveChanges в блоке try/catch, разбирая и отображая сообщение об ошибке (рис. 3). На рис. 4 показаны результаты исключения на клиентской стороне.

Рис. 3. Разбор и отображение сообщения об ошибке, возвращенного сервисом

Parsed Error Message Displayed in the Client
Рис. 4. Разобранное сообщение об ошибке, отображаемое на клиенте

Теперь подбросим методу InsertPerson другую ошибку. Помимо игнорирования свойства LastName, я помещу в свойство Identity¬Card слишком много символов. Вспомните, что это свойство было сконфигурировано со значением MaxLength, равным 10:

Теперь метод HandleException обнаружит два DataValidation¬Error для экземпляра, который сервис пытался обновить. StringBuilder будет содержать двухстрочное сообщение: в одной строке описывается проблема со свойством LastName, а в другой — поясняется проблема со свойством IdentityCard.

В консольном приложении эти две строки будут видны как одно сообщение в исключении:

Средство разбора LINQ to XML затем передаст сообщение в консоль, как показано на рис. 5.

Console App Displaying Multiple Errors for a Single Entity
Рис. 5. Консольное приложение, показывающее несколько ошибок для одной сущности

Преимущества проверки даже в отсутствие соединения с сервером

Теперь вы знаете, как с помощью простого набора требований, применяемых через Code First Data Annotations, можно захватывать и разбирать исключения проверки в EF, возвращать их в клиент, а затем на клиентской стороне разбирать исключение, переданное по HTTP. Если вы имеете дело с правилами проверки, применяемыми через конфигурации свойств, или с более сложными правилами, задаваемыми с помощью IValidationObject или переопределением метода ValidateEntity, то EF будет всегда возвращать DbEntityValidationExceptions. Вы видели, как осуществляется разбор и в таком случае, и сможете расширить логику для обработки нескольких объектов, предоставлять сообщения об ошибках, содержащие более подробные описания, и обрабатывать их на сервере или на клиенте в зависимости от требований вашего приложения.

Поскольку WCF Data Services возвращает данные в формате OData, вы можете использовать WCF-сервисы данных и применять проверки уже сегодня, чтобы во всеоружии встретить будущие технологии в стиле Metro.

Исходный код можно скачать по ссылке code.msdn.microsoft.com/mag201112DataPoints.


Джули Лерман (Julie Lerman) — Microsoft MVP, преподаватель и консультант по .NET, живет в Вермонте. Часто выступает на конференциях по всему миру и в группах пользователей по тематике, связанной с доступом к данным и другими технологиями Microsoft .NET. Ведет блог thedatafarm.com/blog и является автором очень популярной книги «Programming Entity Framework» (O’Reilly Media, 2010). Вы также можете читать ее заметки в twitter.com/julielerman.

Выражаю благодарность за рецензирование статьи эксперту Майку Фласко (Mike Flasko).