Использование сущностей LINQ в службах WCF
Технология LINQ для SQL сама по себе представляет большой интерес, поскольку позволяет проверять схему базы данных и запросы к базе данных на этапе компиляции, избавиться от поддержки хранимых процедур в базе данных и соответствия кода слоя логики хранимым процедурам в базе данных, в результате – сохранить время уменьшив объем ручной работы. В свою очередь технология WCF позволяет создавать службы, описывая лишь данные и контракты самих служб, не вдаваясь в детали реализации того или иного протокола передачи данных и сериализации объектов, позволяя декларативно в конфигурационном файле менять адреса и протоколы, используемые службой. Таким образом, WCF позволяет сократить время разработки и уменьшить объем кода, который должен написать разработчик.
При попытке использовать сущности LINQ в качестве контрактов передаваемых данных, приходится писать дополнительный код. В этой статье представлен один из способов связать сущности LINQ и службы WCF.
Для начала представим себе стандартную архитектуру WCF службы, состоящую из слоя данных (сборка, содержащая сущности LINQ), собственно службы (сборка, содержащая код службы) и хоста службы (пусть это будет сборка, содержащая консольное приложение). Для того, чтобы сгенерировать набор сущностей LINQ, достаточно добавить .dbml файл в проект слоя данных, открыть его в дизайнере и перетащить на него таблицы из Server Explorer. В этой статье в качестве примера будет использована база данных Northwind.
После этого сущности LINQ уже можно использовать, например, так:
NorthwindDataContext dc = new
ColiseumDataContext();
var product = dc.Products.Single(p => p.ProductID == productId);
Однако, для того, чтобы использовать эти сущности в WCF, необходимо пометить соответствующие классы и свойства атрибутами DataContract и DataMember, соответственно. Для этого существует специальный параметр в свойствах дизайнера сущностей LINQ: Serialization. Чтобы установить этот атрибут, можно щелкнуть по дизайнеру, открыть свойства (обычно F4, либо щелчок правой кнопкой мыши и пункт Properties) и установить свойство Serialization = Unidirectional. В результате, будут генерироваться классы, уже размеченные необходимыми атрибутами, например:
[Table(Name="dbo.[Order Details]")]
[DataContract()]
public
partial
class
Order_Detail : INotifyPropertyChanging, INotifyPropertyChanged
{
[Column(Storage="_OrderID", DbType="Int NOT NULL", IsPrimaryKey=true)]
[DataMember(Order=1)]
public
int OrderID
{
get
{
return
this._OrderID;
}
set
{
if ((this._OrderID != value))
{
if (this._Order.HasLoadedOrAssignedValue)
{
throw
new System.Data.Linq.ForeignKeyReferenceAlreadyHasValueException();
}
this.OnOrderIDChanging(value);
this.SendPropertyChanging();
this._OrderID = value;
this.SendPropertyChanged("OrderID");
this.OnOrderIDChanged();
}
}
}
и т.д. ……………………………
Теперь эти объекты можно использовать в WCF службах. Например, создадим простую службу, которая работает с объектами Orders. Для этого определим интерфейс (контракт) службы и, собственно службу:
[ServiceContract]
public
interface
IMobileService
{
[OperationContract]
Order GetOrder(int orderId);
[OperationContract]
void UpdateOrder(Order obj);
}
public
class
MobileService : IMobileService
{
public
Order GetOrder(int orderId)
{
return
OrdersMapper.GetOrder(orderId);
}
public
void UpdateOrder(Order obj)
{
OrdersMapper.UpdateOrder(obj);
}
Однако, если с получением данных на стороне клиента проблем нет, то с обновлением на стороне сервиса возникает сложность, поскольку десериализованный на сервере объект не связан с контекстом данных LINQ. В результате, все-таки придется написать немного кода для метода UpdateOrder:
public
static
class
OrdersMapper
{
public
static
Order GetOrder(int orderId)
{
return
DataUtility.Context.Orders.Single(o => o.OrderID == orderId);
}
public
static
void UpdateOrder(Order obj)
{
var dc = DataUtility.Context;
Order oldOrder = dc.Orders.Single(o => o.OrderID == obj.OrderID);
oldOrder.CustomerID = obj.CustomerID;
oldOrder.EmployeeID = obj.EmployeeID;
oldOrder.Freight = obj.Freight;
foreach (Order_Detail od in oldOrder.Order_Details)
{
dc.Order_Details.Remove(od);
}
oldOrder.Order_Details.Clear();
foreach (Order_Detail od in obj.Order_Details)
{
Order_Detail nod = new
Order_Detail();
nod.Discount = od.Discount;
nod.OrderID = od.OrderID;
nod.ProductID = od.ProductID;
nod.Quantity = od.Quantity;
nod.UnitPrice = od.UnitPrice;
oldOrder.Order_Details.Add(nod);
}
oldOrder.OrderDate = obj.OrderDate;
oldOrder.RequiredDate = obj.RequiredDate;
oldOrder.ShipAddress = obj.ShipAddress;
oldOrder.ShipCity = obj.ShipCity;
oldOrder.ShipCountry = obj.ShipCountry;
oldOrder.ShipName = obj.ShipName;
oldOrder.ShippedDate = obj.ShippedDate;
oldOrder.ShipPostalCode = obj.ShipPostalCode;
oldOrder.ShipRegion = obj.ShipRegion;
oldOrder.ShipVia = obj.ShipVia;
dc.SubmitChanges();
}
}
Как видно из приведенного примера, необходимо заново связать данные с сущностью LINQ, более того, при наличии связей (как в связке Order – Order_Details), необходимо также обработать и их. Например, в примере выше список Order_Details полностью пересоздается, чтобы не отслеживать изменения в каждом объекте.
Собственно все. Очевидно, что пока использование сущностей LINQ и WCF сопряжено с некоторыми сложностями, однако, не так часто необходимо передавать сущности из базы данных удаленным клиентам, поэтому может быть выбрано решение с использованием дополнительных классов-контрактов данных для WCF.
Comments
Anonymous
October 03, 2007
Добрый вечер, Гайдар Все здорово, а как быть с оптимистической конкуренцией. Я вот написал код, похожий на Ваш, но собственно попытка изменить значение проперти (которое было изменнено другим пользователем)оказывается успешной. Код (101 пример LINQ) позволяет отслеживать такие ситуации, и при необходимости можно формировать подробное инфо о том, каккое проперти вызвало конфликт. Следовательно, необходимо передовать, и оригинальное значение проперти. Есть другие варианты? То есть получить сущность, отправить клиенту, клиент модифицирует, отправляет на сервер, обновляем, в случае конфликта, отправляем ФОЛТ КОНТРАКТ клиенту Фрагмент UpdateProperty() try { db.SubmitChanges(ConflictMode.ContinueOnConflict); } catch (ChangeConflictException) {...}Anonymous
October 03, 2007
Здесь все зависит от логики приложения - если ваши объекты путешествуют по сети между службами и возможны варианты одновременной записи, то, по умолчанию, все пройдет на ура - т.е. кто последний, тот и прав. Как вариант, в коде, изменяющем значения объектов, проводить валидацию по последним изменениям, для этого достаточно нагрузить объект полем TimeStamp, куда писать время последнего изменения, которое сохранять в базе в момент сохранения объекта. Дальше по этому полю можно проверять объекты перед записью в БД. Например, ушли два объекта c временем последнего изменения t, т.е. объекты содержат данные: (1)-t, (2)-t, после того, как первый объект был сохранен в БД, то время в бд стало, например p != t, тогда при попытке сохранения второго объекта наш код валидации (if(p != t) throw (new Exception("Ага!")); или какой-то другой, отобъект следующее изменение. Можно пользователя (службу) нотифицировать о проблеме, чтобы она приняла решения (перезаписывать или нет). Проблема-то вся в том, что после "отсоединения" объекта (т.е. передачи между службами), он уже на сущность LINQ, а просто объект с некоторыми полями, соответственно автоматический трекинг не работает.