воскресенье, 28 сентября 2008 г.

Хороший дизайн должен быть SOLID: TOP-5 архитектурных принципов

Что такое хороший дизайн? По каким критериям его оценивать, и каких правил придерживаться при разработке? Как обеспечить достаточный уровень гибкости, связанности, управляемости, стабильности и понятности кода? Роберт Мартин составил список, состоящий всего из пяти правил хорошего проектирования, которые известны, как принципы SOLID.

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

SRP: Single Responsibility Principle (принцип единственной обязанности)

Не должно существовать более одного мотива для изменения данного класса.

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

Рассмотрим пример. Пусть у нас есть класс, реализующий некоторую функциональность, связанную с банковским счетом:

class Account : ActiveRecord
{
    public Guid Id{ get{ ... } }
    public string Number { get{ ... } }
    public decimal CurrentBallance { get { ... } }
    public void Deposit(decimal amount){ ... }
    public void Withdraw(decimal amount){ ... }   
    public void Transfer(decimal amount, Account recipient){ ... }
    public TaxTable CalculateTaxes(int year){ ... }
}

Как видно, класс несет ответственность за:

  1. Персистентность;
  2. Логику управление балансом;
  3. Логику расчета налогов.

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

class Account
{
    public string Number { get{ ... } }
    public decimal CurrentBallance { get { ... } }
    public void Deposit(decimal amount){ ... }
    public void Withdraw(decimal amount){ ... }
    public void Transfer(decimal amount, Account recipient){ ... }
}

class AccountRepository
{
    public Account GetByNumber(string number){ ... }
    public void Save(Account acc){ ... }
}

class TaxCalculator
{
    public TaxTable CalculateTaxes(Account acc, int year){ ... }
}

А сложность в применении данного принципа заключается в том, что прежде всего нужно научиться правильно чувствовать границы его использования. Ведь даже в приведенном примере мы превратили паттерн Active Record в антипаттерн, разом перечеркнув все примеры его успешного применения.

OCP: Open/Closed Principle (принцип открытия/закрытия)

Объекты проектирования (классы, функции, модули и т.д.) должны быть открыты для расширения, но закрыты для модификации.

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

Рассмотрим простой пример. Пусть у нас в системе есть некий класс, отвечающий за просмотр логов:

class LogViewer
{
    public IEnumerable<Transaction> GetByDate(DateTime dateTime){ ... }
}

В один прекрасный день возникла необходимость реализовать возможность выборки транзакций по имени пользователя. Класс может быть модифицирован следующим образом:

class LogViewer
{
    public IEnumerable<Transaction> GetByDate(DateTime dateTime){ ... }
    public IEnumerable<Transaction> GetByUser(string name){ ... }
    public IEnumerable<Transaction> GetByDateAndUser(DateTime dateTime, string name){ ... }
}

Подобная эволюция дизайна является типичным примером нарушения принципа открытия/закрытия. А типичным решением этой проблемы мог бы стать следующий код:

class LogViewer
{
    public IEnumerable<Transaction> GetTransaction(GetSpecification spec){ ... }
}

abstract class GetSpecification
{
    public GetSpecification CombineWith(GetSpecification nextSpec){ ... }
    // ...
}

class GetByDateSpecification : GetSpecification
{
    // ...
}

class GetByUserSpecification : GetSpecification
{
    // ...
}

// Пример использования
class Client
{
    public void ShowLog()
    {
        var viewer = new LogViewer();
        var transactions = viewer.GetTransaction(
            new GetByDateSpecification()
            .CombineWith(new GetByUserSpecification()));
    }
}

Итак, можно сказать, что объект, спроектированный по принципу открытия/закрытия обладает следующими атрибутами:

  1. "Открыт для расширения": поведение может быть расширено путем добавления новых объектов, реализующих новые аспекты поведения;
  2. "Закрыт для модификации": в результате расширения поведения исходный или двоичный код объекта не может быть изменен.

LSP: Liskov Substitution Principle (принцип замещения Лисков)

Функции, которые используют ссылки на базовые классы, должны иметь возможность использовать объекты производных классов, не зная об этом.

Впервые этот принцип был упомянут Барбарой Лисков в 1987 году на научной конференции, посвященной объектно-ориентированному программированию.

Этот принцип является важнейшим критерием для оценки качества принимаемых решений при построении иерархий наследования. Сформулировать его можно в виде простого правила: тип S будет подтипом Т тогда и только тогда, когда каждому объекту oS типа S соответствует некий объект oT типа T таким образом, что для всех программ P, реализованных в терминах T, поведение P не будет меняться, если oT заменить на oS.

Классическим примером нарушения этого принципа является построение иерархии такого рода:

class Rectangle
{
    public virtual int Width { get; set; }
    public virtual int Height { get; set; }

    public int CalculateRectangleArea()
    {
        return Width*Height;
    }
}

class Square : Rectangle
{
    public override int Height
    {
        get{ return base.Height; }
        set
        {
            base.Height = value;
            base.Width = value;
        }
    }

    public override int Width
    {
        get{ return base.Width; }
        set
        {
            base.Width = value;
            base.Height = value;
        }
    }
}

class Program
{
    private static Rectangle CreateRecatgle()
    {
        return new Square();
    }

    static void Main()
    {
        Rectangle r = CreateRecatgle();
        r.Width = 3;
        r.Height = 2;

        Assert.AreEqual(6, r.CalculateRectangleArea());
    }
}

Этот пример заставляет задуматься о том, что такое "декларация типа" в терминах объектно-ориентированного языка программирования, который мы используем. Достаточно ли нам описать интерфейс объекта с помощью обычного абстрактного класса со списком методов, типами параметров и возвращаемого значения? Каким образом мы можем декларировать требования к значениям параметров метода и свойства, которыми будет обладать возвращаемое значение? Как нам описать исключения, которые может сгенерировать метод во время выполнения? Как нам описать изменение состояния объекта на разных этапах его жизненного цикла?

Задавая себе эти вопросы и находя ответы, можно спроектировать систему, которая действительно будет удовлетворять принципу замещения Лисков.

ISP: Interface Segregation Principle (принцип изоляции интерфейса)

Клиент не должен вынужденно зависеть от элементов интерфейса, которые он не использует.

Другими словами этот принцип можно сформулировать так: зависимость между классами должна быть ограничена как можно более узким интерфейсом.

Пример нарушения этого принципа:

abstract class ServiceClient
{
    public string ServiceUri{ get; set; }
    public abstract void SendData(object data);
    public abstract void Flush();
}

class HttpServiceClient : ServiceClient
{
    public override void SendData(object data)
    {
        var channel = OpenChannel(ServiceUri);
        channel.Send(data);
    }

    public override void Flush()
    {
        // Метод ничего не делает, но присутствует в классе
    }
}

class BufferingHttpServiceClient : ServiceClient
{
    public override void SendData(object data)
    {
        Buffer.Write(data);
    }

    public override void Flush()
    {
        var channel = OpenChannel(ServiceUri);
        channel.Send(Buffer.GetAll());
    }
}

Решение этой проблемы заключается в проектировании грамотной иерархии интерфейсов для уменьшения такой зависимости:

>abstract class ServiceClient
{
    public string ServiceUri{ get; set; }
    public abstract void SendData(object data);
}

abstract class BufferingServiceClient : ServiceClient
{
    public abstract void Flush();
}

class HttpServiceClient : ServiceClient
{
    public override void SendData(object data){ ... }
}

class BufferingHttpServiceClient : BufferingServiceClient
{
    public override void SendData(object data){ ... }
    public override void Flush(){ ... }
}

Еще одним признаком потенциального нарушения этого принципа является наличие громоздких интерфейсов. Попробуйте реализовать MembershipProvider для ASP.NET, унаследовавшись от стандартного базового класса и вы поймете о чем речь :)

DIP: Dependency Inversion Principle (принцип обращения зависимости)

Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

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

Поэтому можно сделать вывод, что основная причина, по которой проекты так быстро "стареют" и, как правило, даже умирают, заключается в том, что у разработчиков нет возможности безболезненно менять код каких-то компонентов без боязни нарушить работу других. И большинство проблем проектирования, которые выявляются на ранних этапах так и не решаются, накапливаясь лавинообразно.

Дизайн таких систем можно охарактеризовать следующими признаками:

  • Жесткость - изменение одной части кода затрагивает слишком много других частей;
  • Хрупкость - даже незначительное изменение в коде может привести к совершенно неожиданным проблемам;
  • Неподвижность - никакая из частей приложения не может быть легко выделена и повторно использована.

Принцип обращения зависимости - это очень мощный инструмент, который в сочетании с другими SOLID-принципами позволяет разрабатывать дизайн систем так же легко, как если бы он собирался из конструктора LEGO.

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

public class OrderProcessor
{
    public decimal CalculateTotal(Order order)
    {
        decimal itemTotal = order.GetItemTotal();
        decimal discountAmount = DiscountCalculator.CalculateDiscount(order);

        decimal taxAmount = 0.0M;

        if (order.Country == "US")
            taxAmount = FindTaxAmount(order);
        else if (order.Country == "UK")
            taxAmount = FindVatAmount(order);

        decimal total = itemTotal - discountAmount + taxAmount;

        return total;
    }

    private decimal FindVatAmount(Order order)
    {
        return 10.0M;
    }

    private decimal FindTaxAmount(Order order)
    {
        return 12.0M;
    }
}

Возвращаясь к принципу единственной обязанности, перечислим все обязанности, которые выполняет класс OrderProcessor:

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

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

// Интерфейсы стратегий
public interface IDiscountCalculator
{
    decimal CalculateDiscount(Order order);
}

public interface ITaxStrategy
{
    decimal FindTaxAmount(Order order);
}

// Реализация стратегий
public class DiscountCalculatorAdapter : IDiscountCalculator
{
    public decimal CalculateDiscount(Order order)
    {
        return DiscountCalculator.CalculateDiscount(order);
    }
}

public class USTaxStrategy : ITaxStrategy
{
    public decimal FindTaxAmount(Order order){ ... }
}

public class UKTaxStrategy : ITaxStrategy
{
    public decimal FindTaxAmount(Order order){ ... }
}

// Облегченный код
public class OrderProcessor
{
    private readonly IDiscountCalculator _discountCalculator;
    private readonly ITaxStrategy _taxStrategy;

    public OrderProcessor(IDiscountCalculator discountCalculator, 
                          ITaxStrategy taxStrategy)
    {
        _taxStrategy = taxStrategy;
        _discountCalculator = discountCalculator;
    }

    public decimal CalculateTotal(Order order)
    {
        decimal itemTotal = order.GetItemTotal();
        decimal discountAmount = _discountCalculator.CalculateDiscount(order);

        decimal taxAmount = _taxStrategy.FindTaxAmount(order);

        decimal total = itemTotal - discountAmount + taxAmount;

        return total;
    }
}

Принцип обращения зависимостей лежит в основе архитектур многих каркасов приложений. Для автоматизации процесса управления зависимостями разработано множество утилит. Мартин Фаулер в своей статье рассмотрел всевозможные паттерны реализации механизма работы этого принципа.

Заключение

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

Отдельного внимания заслуживает подход к разработке через тестирование (TDD) и его связь с SOLID-принципами. Для многих именно незнание этих принципов является той самой непреодолимой стеной, которая мешает начать использовать TDD. Но во многом, это мнение является ошибочным, потому как разработка юнит-тестов сама по себе не является сложной задачей и начать писать тесты может каждый на любом этапе своего развития. Главное начать. И уже потом, в процессе поиска оптимальных способов разработки через тестирование будет достигнуто глубокое понимание всех тонкостей качественного проектирования.

среда, 10 сентября 2008 г.

Expression Trees и оптимизация Reflection

В последней версии .NET Framework среди новых возможностей было добавлено средства метапрограммирования под названием Expression Trees. На базе этой технологии, а именно основываясь на том принципе, что выражения на "обычном" языке программирования могут автоматически преобразовываться в синтаксические деревья, была разработана технология LINQ.

Но в этом посте речь пойдет о другой области применения возможности динамически собирать expression trees и компилировать их в работоспособный код. И эта область - оптимизация Reflection.

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

Но динамические Expression Trees предоставляют нам еще достаточно элегантный способ оптимизации. Суть его заключается в том, что для доступа к свойствам экземпляров известного класса мы будем генерировать соответствующий строго типизированный код в виде лямбда-функции, которая будет обращаться к ним напрямую и которую мы будем кэшировать для последующего повторного использования.

Сам по себе метод создания нужной лямбда-функции достаточно прост:

private static Func<object, object> CreateGetter(object entity, string propertyName)
{
    var param = Expression.Parameter(typeof (object), "e");
    Expression body = Expression.PropertyOrField(Expression.TypeAs(param, entity.GetType()), propertyName);
    var getterExpression = Expression.Lambda<Func<object, object>>(body, param);
    return getterExpression.Compile();
}

Если поставить в этом методе точку останова и посмотреть на строковое представление переменной getterExpression, то мы увидим, во что оно будет скомпилировано:

getterExpression

Обернем всю логику доступа к свойству класса в некий ReflectionHelper, который в дальнейшем можно будет расширить методами для вызова методов, инициализации свойств и т.д. Этот класс будет реализовывать метод GetPropertyValue следующим образом:

readonly Dictionary<PropertyGetterKey, Func<object, object>> propertyGetters = new Dictionary<PropertyGetterKey, Func<object, object>>();

public object GetPropertyValue(object entity, string propertyName)
{
    Func<object, object> getter;

    var key = new PropertyGetterKey {Type = entity.GetType(), PropertyName = propertyName};

    if (propertyGetters.ContainsKey(key))
        getter = propertyGetters[key];
    else
    {
        getter = CreateGetter(entity, propertyName);
        propertyGetters.Add(key, getter);
    }

    return getter(entity);
}

Для проверки того, насколько эта логика эффективна, разработаем небольшой тест:

var entities = new List<Class1>();

for (var i = 0; i < 20; i++)
    entities.Add(new Class1 { Property1 = "Value" + i });

foreach (var entity in entities)
{
    var start = DateTime.Now.Millisecond;
    var val = ReflectionHelper.Instance.GetPropertyValue(entity, "Property1");
    Console.WriteLine("{0} - {1}", val, (DateTime.Now.Millisecond - start));
}

Ну и результаты говорят сами за себя:

screenshot002

Как видим, такой способ оптимизации более чем жизнеспособен :)

воскресенье, 7 сентября 2008 г.

Интеграция данных: REST + LINQ = ADO.NET Data Services

Одним из интересных аспектов SOA является интеграция данных. Выдержка из Wikipedia:

Интеграция данных – это процесс комбинирования данных из разнообразных источников и предоставление их пользователи в неком унифицированном виде. Необходимость в разработке этого процесса появляется в различных областях. Например, в коммерческой (когда двум схожим компаниям необходимо объединить свои базы данных) и в научной (объединение данных из нескольких разных хранилищ биометрической информации).

Необходимость в таком виде интеграции возникла в одном из текущих проектов. Со стороны разработчика хотелось получить максимально прозрачный API, не зависящий от способа реализации и предоставляющий удобный и максимально "родной" механизм работы с данными.

Введение

Главный вопрос, на который пришлось найти ответ - это технология открытия доступа к удаленной базе данных. Выбор пал на технологию REST. Вкратце, она позволяет нам получать нужные данные, формируя запросы в виде GET-параметров к сервису. Дополнительно к этому хотелось бы получить транслятор с LINQ к REST, используя LINQ как раз как тот самый унифицированный и "родной" механизм, который позволит связать воедино данные из любых источников, для которых реализована соответствующая библиотека Linq2***.

Поиски соответствующей библиотеки были недолгими: не так давно Microsoft выпустило в составе .NET 3.5 SP1 свой проект Astoria, назвав его ADO.NET Data Services. С одной стороны эта библиотека позволяет легко создавать сервисы данных, открывающие доступ к данным на сервере посредством протокола REST. А с другой стороны, предоставляет удобные средства интеграции этих сервисов с клиентскими приложениями, оборачивая всю рутину по доступу к сервису в автосгенерированные прокси-классы. Ну и конечно же эти клиентские прокси-классы реализуют LINQ-транслятор. Кроме того, сервисы данных реализуются на базе стека WCF, предоставляя широкие возможности по тонкой низкоуровневой настройке.

Реализация сервиса

Для начала добавим в веб-проект сервис данных:

Add New Item - ADO.NET Data Service

Исходный код сервиса будет таким:

public class WebDataService : DataService< /* TODO: put your data source class name here */ >
{
    // This method is called only once to initialize service-wide policies.
    public static void InitializeService(IDataServiceConfiguration config)
    {
        // TODO: set rules to indicate which entity sets and service operations are visible, updatable, etc.
        // Examples:
        // config.SetEntitySetAccessRule("MyEntityset", EntitySetRights.AllRead);
        // config.SetServiceOperationAccessRule("MyServiceOperation", ServiceOperationRights.All);
    }
}

Если вы обратитесь к спецификации класса DataService<T>, то увидите, что к типу Т не предоставляются такие требования, как реализация какого-либо инетрфейса. Все дело в том, что необходимые требования невозможно описать в виде какой-либо простой модели, которая бы не усложнила разработку этих сервисов. Но требования, тем не менее, есть:

  1. Сервис данных может опубликовывать все IQueryable-свойства данного источника данных.
    Наш источник данных может выглядеть таким образом:
    public class DomainDataContext
    {
        private readonly Supplier[] _suppliers;
        private readonly Product[] _products;
    
        public DomainDataContext()
        {
            // Инициализация _suppliers и _products тестовыми данными
        }
    
        public IQueryable<Supplier> Suppliers
        {
            get { return _suppliers.AsQueryable(); }
        }
    
        public IQueryable<Product> Products
        {
            get { return _products.AsQueryable(); }
        }
    }
  2. Классы-сущности должны быть "идентифицируемыми". 
    Это правило в данном случае означает, что в классах обязательно должны быть свойства, заканчивающиеся на ID:
    public class Product
    {
        public string ProductID { get; set; }
        public string SupplierID { get; set; }
        public string ProductName { get; set; }
    }
    
    public class Supplier
    {
        public string SupplierID { get; set; }
        public string CompanyName { get; set; }
        public string ContactName { get; set; }
        public string Phone { get; set; }
    } 

И это не удивительно, что в качестве источников данных можно использовать модели Linq2Sql и ADO.NET Entity Framework.

Посмотрев на наш сервис в браузере, мы увидим такой XML:

<?xml version="1.0" encoding="utf-8" standalone="yes" ?> 
<service xml:base="http://localhost:7593/WebDataService.svc/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:app="http://www.w3.org/2007/app" xmlns="http://www.w3.org/2007/app">
   <workspace>
      <atom:title>Default</atom:title> 
   </workspace>
</service>

Никаких упоминаний о наших Products и Suppliers. Мы забыли сконфигурировать наш сервис, указав доступ к коллекциям объектов и операциям. Но так как наш сервис не предоставляет никаких методов для манипуляции данными, нам нужно сконфигурировать только доступ к коллекциям в источнике данных:

public static void InitializeService(IDataServiceConfiguration config)
{
    config.SetEntitySetAccessRule("*", EntitySetRights.AllRead);
}

Открываем сервис в браузере и видим наши коллекции:

<?xml version="1.0" encoding="windows-1251" standalone="yes"?>
<service xml:base="http://localhost:7593/WebDataService.svc/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:app="http://www.w3.org/2007/app" xmlns="http://www.w3.org/2007/app">
  <workspace>
    <atom:title>Default</atom:title>
    <collection href="Suppliers">
      <atom:title>Suppliers</atom:title>
    </collection>
    <collection href="Products">
      <atom:title>Products</atom:title>
    </collection>
  </workspace>
</service>

Итак, наш сервис готов к использованию.

Реализация клиента

Так как наш сервис данных построен на основе WCF, то подключение сервиса ничем не отличается от подключения любого другого WCF-сервиса. Самое же интересное начинается во время его использования.

В качестве примера реализуем логику поиска поставщиков по названию товара. Локально поиск будет проводиться по базе Northwind с использованием Linq2Sql, а потом результат локального поиска будет объединяться с поиском через сервис данных.

Локальная схема данных будет выглядеть следующим образом:

Local data schema

Поисковый запрос к локальной базе достаточно тривиален:

var localData = new LocalDataDataContext();

var localSuppliers = from p in localData.Products
                     join s in localData.Suppliers on p.SupplierID equals s.SupplierID
                     where p.ProductName == productName
                     select
                         new 
                         {
                             Company = s.CompanyName,
                             PhoneNumber = s.Phone
                         };

Попробуем сделать то же самое для поиска по удаленному сервису данных:

var serviceData = new DomainDataContext(new Uri("http://localhost:7593/WebDataService.svc"));

var dataServiceSuppliers = from p in serviceData.Products
                           join s in serviceData.Suppliers on p.SupplierID equals s.SupplierID
                           where p.ProductName == productName
                           select
                               new
                                   {
                                       Company = s.CompanyName,
                                       PhoneNumber = s.Phone
                                   };

При попытке получить данные по этому запросу мы получим исключение "The method 'Join' is not supported.". По сути, это Законы дырявых асбстракций в действии :) Все дело в том, что не вся функциональность Linq однозначно транслируется в REST-запросы. REST-сервисы предоставляют лишь несколько параметров запросов: expand, orderby, skip, top, filter. Как видно, никаких inner join и даже агрегатов, типа банального count тут нет.

Для решения этой проблемы разобьем наш запрос на несколько подзапросов: сначала найдем все товары, удовлетворяющие данному, а потом получим список поставщиков этих товаров.

var serviceData = new DomainDataContext(new Uri("http://localhost:7593/WebDataService.svc"));

var matchedProducts = from p in serviceData.Products
                                        where p.ProductName == productName
                                        select p;

Следующий фрагмент кода динамически соберет условие для параметра filter так, чтобы получилось что-то в таком духе:

Func<ServiceReference1.Supplier, bool> filterCondition =
    s => s.SupplierID == "001" || s.SupplierID == "002";

Такой вариант фильтра однозначно транслируется в строку для параметра запроса filter.

var param = Expression.Parameter(typeof(ServiceReference1.Supplier), "s");

Func<string, IEnumerable<string>, Expression> buildOrElseExpression = null;

Func<string, Expression> buildEqualExpression =
    val => Expression.Equal(Expression.Property(param, "SupplierID"), Expression.Constant(val));

Func<IEnumerable<string>, Expression> buildTailExpression = tail => tail.Count() == 1
                                       ? buildEqualExpression(tail.First())
                                       : buildOrElseExpression(tail.First(), tail.Skip(1));

buildOrElseExpression =
    (head, tail) => Expression.OrElse(
                        buildEqualExpression(head),
                        buildTailExpression(tail));

// Вычитываем данные из сервиса и получаем отключенный список идентификаторов поставщиков
var serviceSupplierIDs = matchedProducts.ToArray().Select(p => p.SupplierID);
var filterCondition =
    Expression.Lambda<Func<ServiceReference1.Supplier, bool>>(
    buildTailExpression(serviceSupplierIDs), param);

// Получаем отключенную копию результатов поиска для дальнейших манипуляций
var serviceSuppliers = serviceData.Suppliers.
    Where(filterCondition).
    ToArray().
    Select(s => new
                    {
                        Company = s.CompanyName,
                        PhoneNumber = s.Phone
                    });

 

Ну и наконец-то финальное объединение:

// Перед объединением получаем отключенную копию данных.
// В противном случае Linq2Sql попытается транслировать этот метод в sql-инструкцию UNION
var allSuppliers = localSuppliers.ToArray().Union(serviceSuppliers);

Выводы

Конечно же, ADO.NET Data Services - это решение многих проблем интеграции данных внутри системы. Но использование этой системы накладывает и ряд требований:

  1. Модель данных для таких сервисов должна быть подготовлена специальным образом так, чтобы избежать множества повторяющихся простых запросов из-за ограничения протокола REST. В общем случае, эта подготовка сведется к денормализации исходной модели данных, к укрупнению классов сущностей и введению всяких агрегатных полей с данными из дочерних сущностей.
  2. Для вычисления агрегатных функций (таких, как count(), sum() и т.д.) нужно будет реализовать специальные методы вне контекста сервисов данных.
  3. Все операции проекции должны проводиться на отсоединенной выборке. Таким образом, с учетом требований в п.1 такими запросами не стоит злоупотреблять по причиные избыточности данных, передаваемых через транспортный уровень. Для решения этой проблемы можно было бы расширить нашу модель одинаковыми сущностями с различными уровнями детализации.

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

Исходный код тестового приложения