다음을 통해 공유


연습: IQueryable LINQ 공급자 만들기

이 고급 항목에서는 사용자 지정 LINQ 공급자를 만드는 방법을 단계별로 설명합니다. 이 연습을 마친 후에는 직접 만든 공급자를 사용하여 TerraServer-USA 웹 서비스에 대한 LINQ 쿼리를 작성할 수 있게 됩니다.

TerraServer-USA 웹 서비스는 미국 항공 이미지 데이터베이스에 대한 인터페이스를 제공합니다. 또한 위치 이름 일부 또는 전체를 제공하면 미국 지역에 대한 정보를 반환하는 메서드도 제공합니다. GetPlaceList라고 하는 이 메서드가 사용자가 만든 LINQ 공급자를 통해 호출되는 메서드입니다. 공급자는 WCF(Windows Communication Foundation)를 사용하여 웹 서비스와 통신합니다. TerraServer-USA 웹 서비스에 대한 자세한 내용은 Overview of the TerraServer-USA Web Services를 참조하십시오.

이 공급자는 비교적 단순한 IQueryable 공급자로, 쿼리에 처리할 특정 정보가 있다고 가정하며 단일 형식을 노출하여 결과 데이터를 나타내는 폐쇄형 형식 시스템입니다. 이 공급자는 쿼리를 나타내는 식 트리에서 한 가지 형식의 메서드 호출 식만 검사합니다 즉, 가장 안쪽에 있는 Where 호출만 검사합니다. 이 공급자는 해당 식에서 웹 서비스를 쿼리하기 위해 필요한 데이터를 추출합니다. 그런 다음 웹 서비스를 호출하고 반환된 데이터를 식 트리의 초기 IQueryable 데이터 소스 위치에 삽입합니다. 쿼리 실행의 나머지 부분은 표준 쿼리 연산자의 Enumerable 구현으로 처리됩니다.

이 항목의 코드 예제는 C# 및 Visual Basic으로 제공됩니다.

이 연습에서는 다음 작업을 수행합니다.

  • Visual Studio에서 프로젝트 만들기

  • IQueryable LINQ 공급자에 필요한 IQueryable<T>IOrderedQueryable<T>IQueryProvider 인터페이스 구현

  • 웹 서비스의 데이터를 나타내는 사용자 지정 .NET 형식 추가

  • 쿼리 컨텍스트 클래스 및 웹 서비스에서 데이터를 가져오는 클래스 만들기

  • 가장 안쪽의 Queryable.Where 메서드 호출을 나타내는 식을 찾는 식 트리 방문자 서브클래스 만들기

  • LINQ 쿼리에서 웹 서비스 요청에 사용할 정보를 추출하는 식 트리 방문자 서브클래스 만들기

  • 완료된 LINQ 쿼리를 나타내는 식 트리를 수정하는 식 트리 방문자 서브클래스 만들기

  • 계산기 클래스를 사용하여 식 트리를 부분적으로 계산. 이 단계에서 LINQ 쿼리의 모든 지역 변수 참조가 값으로 변환되므로 이 단계는 필수적입니다.

  • 식 트리 도우미 클래스 및 새 예외 클래스 만들기

  • LINQ 쿼리를 포함하는 클라이언트 응용 프로그램에서 LINQ 공급자 테스트

  • LINQ 공급자에 더 복잡한 쿼리 기능 추가

    참고

    이 연습에서 만드는 LINQ 공급자는 샘플로 제공됩니다. 자세한 내용은 LINQ 샘플을 참조하십시오.

사전 요구 사항

이 연습을 완료하려면 다음 구성 요소가 필요합니다.

  • Visual Studio 2008

참고

다음 지침처럼 컴퓨터에서 Visual Studio 사용자 인터페이스 요소 일부에 대한 이름이나 위치를 다르게 표시할 수 있습니다. 이러한 요소는 사용하는 Visual Studio 버전 및 설정에 따라 결정됩니다. 자세한 내용은 Visual Studio 설정을 참조하십시오.

프로젝트 만들기

Visual Studio에서 프로젝트를 만들려면

  1. Visual Studio에서 새 클래스 라이브러리 응용 프로그램을 만들고 프로젝트 이름을 LinqToTerraServerProvider로 지정합니다.

  2. 솔루션 탐색기에서 Class1.cs(또는 Class1.vb) 파일을 선택하고 이름을 QueryableTerraServerData.cs(또는 QueryableTerraServerData.vb)로 바꿉니다. 대화 상자가 열리면 를 클릭하여 코드 요소에 대한 모든 참조의 이름을 바꿉니다.

    실행 가능한 클라이언트 응용 프로그램이 공급자 어셈블리를 해당 프로젝트에 대한 참조로 추가하기 때문에 Visual Studio에서는 공급자를 클래스 라이브러리 프로젝트로 만듭니다.

웹 서비스에 서비스 참조를 추가하려면

  1. 솔루션 탐색기에서 LinqToTerraServerProvider 프로젝트를 마우스 오른쪽 단추로 클릭한 다음 서비스 참조 추가를 클릭합니다.

    서비스 참조 추가 대화 상자가 열립니다.

  2. 주소 상자에 http://terraserver.microsoft.com/TerraService2.asmx를 입력합니다.

  3. 네임스페이스 상자에 TerraServerReference를 입력하고 확인을 클릭합니다.

    TerraServer-USA 웹 서비스가 서비스 참조로 추가되어 응용 프로그램이 WCF(Windows Communication Foundation)를 통해 웹 서비스와 통신할 수 있게 됩니다. 프로젝트에 서비스 참조를 추가하면 Visual Studio에서 웹 서비스에 대한 끝점과 프록시를 포함하는 app.config 파일을 생성합니다. 자세한 내용은 Windows Communication Foundation 서비스 및 Visual Studio의 WCF.NET 데이터 서비스를 참조하십시오.

이제 프로젝트에는 app.config라는 파일, QueryableTerraServerData.cs(또는 QueryableTerraServerData.vb)라는 파일 및 TerraServerReference라는 서비스 참조가 들어 있습니다.

필요한 인터페이스 구현

LINQ 공급자를 만들려면 최소한 IQueryable<T>IQueryProvider 인터페이스를 구현해야 합니다. IQueryable<T>IQueryProvider는 다른 필수 인터페이스에서 파생되므로 이 두 인터페이스를 구현하면 LINQ 공급자에 필요한 다른 인터페이스도 구현하게 됩니다.

OrderByThenBy 같은 쿼리 연산자 정렬을 지원하려면 IOrderedQueryable<T> 인터페이스도 구현해야 합니다. IOrderedQueryable<T>IQueryable<T>에서 파생되므로 이 두 인터페이스를 한 가지 형식으로 구현할 수 있습니다. 그리고 이것이 바로 이 공급자가 하는 역할입니다.

System.Linq.IQueryable`1 및 System.Linq.IOrderedQueryable`1을 구현하려면

  • QueryableTerraServerData.cs(또는 QueryableTerraServerData.vb) 파일에 다음 코드를 추가합니다.

    Imports System.Linq.Expressions
    
    Public Class QueryableTerraServerData(Of TData)
        Implements IOrderedQueryable(Of TData)
    
    #Region "Private members"
    
        Private _provider As TerraServerQueryProvider
        Private _expression As Expression
    
    #End Region
    
    #Region "Constructors"
    
        ''' <summary>
        ''' This constructor is called by the client to create the data source.
        ''' </summary>
        Public Sub New()
            Me._provider = New TerraServerQueryProvider()
            Me._expression = Expression.Constant(Me)
        End Sub
    
        ''' <summary>
        ''' This constructor is called by Provider.CreateQuery().
        ''' </summary>
        ''' <param name="_expression"></param>
        Public Sub New(ByVal _provider As TerraServerQueryProvider, ByVal _expression As Expression)
    
            If _provider Is Nothing Then
                Throw New ArgumentNullException("provider")
            End If
    
            If _expression Is Nothing Then
                Throw New ArgumentNullException("expression")
            End If
    
            If Not GetType(IQueryable(Of TData)).IsAssignableFrom(_expression.Type) Then
                Throw New ArgumentOutOfRangeException("expression")
            End If
    
            Me._provider = _provider
            Me._expression = _expression
        End Sub
    
    #End Region
    
    #Region "Properties"
    
        Public ReadOnly Property ElementType(
            ) As Type Implements IQueryable(Of TData).ElementType
    
            Get
                Return GetType(TData)
            End Get
        End Property
    
        Public ReadOnly Property Expression(
            ) As Expression Implements IQueryable(Of TData).Expression
    
            Get
                Return _expression
            End Get
        End Property
    
        Public ReadOnly Property Provider(
            ) As IQueryProvider Implements IQueryable(Of TData).Provider
    
            Get
                Return _provider
            End Get
        End Property
    
    #End Region
    
    #Region "Enumerators"
    
        Public Function GetGenericEnumerator(
            ) As IEnumerator(Of TData) Implements IEnumerable(Of TData).GetEnumerator
    
            Return (Me.Provider.
                    Execute(Of IEnumerable(Of TData))(Me._expression)).GetEnumerator()
        End Function
    
        Public Function GetEnumerator(
            ) As IEnumerator Implements IEnumerable.GetEnumerator
    
            Return (Me.Provider.
                    Execute(Of IEnumerable)(Me._expression)).GetEnumerator()
        End Function
    
    #End Region
    
    End Class
    
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Linq.Expressions;
    
    namespace LinqToTerraServerProvider
    {
        public class QueryableTerraServerData<TData> : IOrderedQueryable<TData>
        {
            #region Constructors
            /// <summary>
            /// This constructor is called by the client to create the data source.
            /// </summary>
            public QueryableTerraServerData()
            {
                Provider = new TerraServerQueryProvider();
                Expression = Expression.Constant(this);
            }
    
            /// <summary>
            /// This constructor is called by Provider.CreateQuery().
            /// </summary>
            /// <param name="expression"></param>
            public QueryableTerraServerData(TerraServerQueryProvider provider, Expression expression)
            {
                if (provider == null)
                {
                    throw new ArgumentNullException("provider");
                }
    
                if (expression == null)
                {
                    throw new ArgumentNullException("expression");
                }
    
                if (!typeof(IQueryable<TData>).IsAssignableFrom(expression.Type))
                {
                    throw new ArgumentOutOfRangeException("expression");
                }
    
                Provider = provider;
                Expression = expression;
            }
            #endregion
    
            #region Properties
    
            public IQueryProvider Provider { get; private set; }
            public Expression Expression { get; private set; }
    
            public Type ElementType
            {
                get { return typeof(TData); }
            }
    
            #endregion
    
            #region Enumerators
            public IEnumerator<TData> GetEnumerator()
            {
                return (Provider.Execute<IEnumerable<TData>>(Expression)).GetEnumerator();
            }
    
            System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
            {
                return (Provider.Execute<System.Collections.IEnumerable>(Expression)).GetEnumerator();
            }
            #endregion
        }
    }
    

    QueryableTerraServerData 클래스에 의한 IOrderedQueryable<T> 구현은 IQueryable에서 선언된 세 가지 속성과 IEnumerableIEnumerable<T>에서 선언된 두 가지 열거형 메서드를 구현합니다.

    이 클래스에는 두 가지 생성자가 있습니다. 첫 번째 생성자는 클라이언트 응용 프로그램에서 호출되어 LINQ 쿼리를 작성하는 개체를 만듭니다. 두 번째 생성자는 IQueryProvider 구현의 코드에 의해 공급자 라이브러리 내부적으로 호출됩니다.

    QueryableTerraServerData 형식의 개체에서 GetEnumerator 메서드를 호출하면 이 메서드가 나타내는 쿼리가 실행되고 쿼리 결과가 열거됩니다.

    클래스 이름을 제외하면 이 코드는 TerraServer-USA 웹 서비스 공급자에 한정되지 않으므로 모든 LINQ 공급자에 재사용할 수 있습니다.

System.Linq.IQueryProvider를 구현하려면

  • 프로젝트에 TerraServerQueryProvider 클래스를 추가합니다.

    Imports System.Linq.Expressions
    Imports System.Reflection
    
    Public Class TerraServerQueryProvider
        Implements IQueryProvider
    
        Public Function CreateQuery(
            ByVal expression As Expression
            ) As IQueryable Implements IQueryProvider.CreateQuery
    
            Dim elementType As Type = TypeSystem.GetElementType(expression.Type)
    
            Try
                Dim qType = GetType(QueryableTerraServerData(Of )).MakeGenericType(elementType)
                Dim args = New Object() {Me, expression}
                Dim instance = Activator.CreateInstance(qType, args)
    
                Return CType(instance, IQueryable)
            Catch tie As TargetInvocationException
                Throw tie.InnerException
            End Try
        End Function
    
        ' Queryable's collection-returning standard query operators call this method.
        Public Function CreateQuery(Of TResult)(
            ByVal expression As Expression
            ) As IQueryable(Of TResult) Implements IQueryProvider.CreateQuery
    
            Return New QueryableTerraServerData(Of TResult)(Me, expression)
        End Function
    
        Public Function Execute(
            ByVal expression As Expression
            ) As Object Implements IQueryProvider.Execute
    
            Return TerraServerQueryContext.Execute(expression, False)
        End Function
    
        ' Queryable's "single value" standard query operators call this method.
        ' It is also called from QueryableTerraServerData.GetEnumerator().
        Public Function Execute(Of TResult)(
            ByVal expression As Expression
            ) As TResult Implements IQueryProvider.Execute
    
            Dim IsEnumerable As Boolean = (GetType(TResult).Name = "IEnumerable`1")
    
            Dim result = TerraServerQueryContext.Execute(expression, IsEnumerable)
            Return CType(result, TResult)
        End Function
    End Class
    
    using System;
    using System.Linq;
    using System.Linq.Expressions;
    
    namespace LinqToTerraServerProvider
    {
        public class TerraServerQueryProvider : IQueryProvider
        {
            public IQueryable CreateQuery(Expression expression)
            {
                Type elementType = TypeSystem.GetElementType(expression.Type);
                try
                {
                    return (IQueryable)Activator.CreateInstance(typeof(QueryableTerraServerData<>).MakeGenericType(elementType), new object[] { this, expression });
                }
                catch (System.Reflection.TargetInvocationException tie)
                {
                    throw tie.InnerException;
                }
            }
    
            // Queryable's collection-returning standard query operators call this method.
            public IQueryable<TResult> CreateQuery<TResult>(Expression expression)
            {
                return new QueryableTerraServerData<TResult>(this, expression);
            }
    
            public object Execute(Expression expression)
            {
                return TerraServerQueryContext.Execute(expression, false);
            }
    
            // Queryable's "single value" standard query operators call this method.
            // It is also called from QueryableTerraServerData.GetEnumerator().
            public TResult Execute<TResult>(Expression expression)
            {
                bool IsEnumerable = (typeof(TResult).Name == "IEnumerable`1");
    
                return (TResult)TerraServerQueryContext.Execute(expression, IsEnumerable);
            }
        }
    }
    

    이 클래스의 쿼리 공급자 코드는 IQueryProvider 인터페이스를 구현하는 데 필요한 네 가지 메서드를 구현합니다. 두 CreateQuery 메서드는 데이터 소스와 관련된 쿼리를 만들고 두 Execute 메서드는 이러한 쿼리를 전송하여 실행합니다.

    제네릭이 아닌 CreateQuery 메서드는 리플렉션을 사용하여, 메서드가 실행될 경우 그 메서드로 생성된 쿼리가 반환할 시퀀스의 요소 형식을 가져옵니다. 그런 다음 Activator 클래스를 사용하여 요소 형식으로 생성된 새 QueryableTerraServerData 인스턴스를 제네릭 형식 인수로 생성합니다. 따라서 제네릭이 아닌 CreateQuery 메서드를 호출한 결과는 올바른 형식 인수를 사용하여 제네릭 CreateQuery 메서드를 호출한 결과와 동일합니다.

    대부분의 쿼리 실행 논리는 이후에 추가할 다른 클래스에서 처리됩니다. 이러한 기능은 쿼리 대상인 데이터 소스에 한정되므로 따로 처리되지만 이 클래스의 코드는 모든 LINQ 공급자에 대해 제네릭입니다. 다른 공급자에 이 코드를 사용하려면 메서드 중 두 개에서 참조되는 클래스 이름과 쿼리 컨텍스트 형식 이름을 변경해야 합니다.

결과 데이터를 나타내는 사용자 지정 형식 추가

웹 서비스에서 가져오는 데이터를 나타내기 위해서는 .NET 형식이 필요합니다. 이 형식은 클라이언트 LINQ 쿼리에서 필요한 결과를 정의하는 데 사용됩니다. 다음 절차에서는 이러한 형식을 만듭니다. Place라는 이름의 이 형식은 도시, 공원, 호수 등과 같은 하나의 지리적 위치 정보를 포함합니다.

이 코드는 다양한 형식의 지리적 위치를 정의하고 Place 클래스에서 사용되는 PlaceType이라는 열거형 형식도 포함합니다.

사용자 지정 결과 형식을 만들려면

  • 프로젝트에 Place 클래스와 PlaceType 열거형을 추가합니다.

    Public Class Place
        ' Properties.
        Public Property Name As String
        Public Property State As String
        Public Property PlaceType As PlaceType
    
        ' Constructor.
        Friend Sub New(ByVal name As String, 
                       ByVal state As String, 
                       ByVal placeType As TerraServerReference.PlaceType)
    
            Me.Name = name
            Me.State = state
            Me.PlaceType = CType(placeType, PlaceType)
        End Sub
    End Class
    
    Public Enum PlaceType
        Unknown
        AirRailStation
        BayGulf
        CapePeninsula
        CityTown
        HillMountain
        Island
        Lake
        OtherLandFeature
        OtherWaterFeature
        ParkBeach
        PointOfInterest
        River
    End Enum
    
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    
    namespace LinqToTerraServerProvider
    {
        public class Place
        {
            // Properties.
            public string Name { get; private set; }
            public string State { get; private set; }
            public PlaceType PlaceType { get; private set; }
    
            // Constructor.
            internal Place(string name,
                            string state,
                            LinqToTerraServerProvider.TerraServerReference.PlaceType placeType)
            {
                Name = name;
                State = state;
                PlaceType = (PlaceType)placeType;
            }
        }
    
        public enum PlaceType
        {
            Unknown,
            AirRailStation,
            BayGulf,
            CapePeninsula,
            CityTown,
            HillMountain,
            Island,
            Lake,
            OtherLandFeature,
            OtherWaterFeature,
            ParkBeach,
            PointOfInterest,
            River
        }
    }
    

    Place 형식에 대한 생성자를 사용하면 웹 서비스에서 반환되는 형식에서 결과 개체를 쉽게 만들 수 있습니다. 공급자는 웹 서비스 API로 직접 정의된 결과 형식을 반환할 수 있지만 그러려면 클라이언트 응용 프로그램에서 웹 서비스에 참조를 추가해야 합니다. 공급자 라이브러리의 일부로 새 형식을 만들면 웹 서비스가 노출하는 형식과 메서드를 클라이언트가 알 필요가 없습니다.

데이터 소스에서 데이터를 가져오는 기능 추가

이 공급자 구현에서는 Queryable.Where에 대한 가장 안쪽 호출에 웹 서비스를 쿼리하는 데 사용할 위치 정보가 들어 있다고 가정합니다. 가장 안쪽에 있는 Queryable.Where 호출은 LINQ 쿼리에서 처음 발생하는 where 절(Visual Basic의 경우 Where 절) 또는 Queryable.Where 메서드 호출이거나 쿼리를 나타내는 식 트리의 "가장 아래쪽"에 있는 호출입니다.

쿼리 컨텍스트 클래스를 만들려면

  • 프로젝트에 TerraServerQueryContext 클래스를 추가합니다.

    Imports System.Linq.Expressions
    
    Public Class TerraServerQueryContext
    
        ' Executes the expression tree that is passed to it.
        Friend Shared Function Execute(ByVal expr As Expression, 
                                       ByVal IsEnumerable As Boolean) As Object
    
            ' The expression must represent a query over the data source.
            If Not IsQueryOverDataSource(expr) Then
                Throw New InvalidProgramException("No query over the data source was specified.")
            End If
    
            ' Find the call to Where() and get the lambda expression predicate.
            Dim whereFinder As New InnermostWhereFinder()
            Dim whereExpression As MethodCallExpression = 
                whereFinder.GetInnermostWhere(expr)
            Dim lambdaExpr As LambdaExpression
            lambdaExpr = CType(CType(whereExpression.Arguments(1), UnaryExpression).Operand, LambdaExpression)
    
            ' Send the lambda expression through the partial evaluator.
            lambdaExpr = CType(Evaluator.PartialEval(lambdaExpr), LambdaExpression)
    
            ' Get the place name(s) to query the Web service with.
            Dim lf As New LocationFinder(lambdaExpr.Body)
            Dim locations As List(Of String) = lf.Locations
            If locations.Count = 0 Then
                Dim s = "You must specify at least one place name in your query."
                Throw New InvalidQueryException(s)
            End If
    
            ' Call the Web service and get the results.
            Dim places() = WebServiceHelper.GetPlacesFromTerraServer(locations)
    
            ' Copy the IEnumerable places to an IQueryable.
            Dim queryablePlaces = places.AsQueryable()
    
            ' Copy the expression tree that was passed in, changing only the first
            ' argument of the innermost MethodCallExpression.
            Dim treeCopier As New ExpressionTreeModifier(queryablePlaces)
            Dim newExpressionTree = treeCopier.Visit(expr)
    
            ' This step creates an IQueryable that executes by replacing 
            ' Queryable methods with Enumerable methods.
            If (IsEnumerable) Then
                Return queryablePlaces.Provider.CreateQuery(newExpressionTree)
            Else
                Return queryablePlaces.Provider.Execute(newExpressionTree)
            End If
        End Function
    
        Private Shared Function IsQueryOverDataSource(ByVal expression As Expression) As Boolean
            ' If expression represents an unqueried IQueryable data source instance,
            ' expression is of type ConstantExpression, not MethodCallExpression.
            Return (TypeOf expression Is MethodCallExpression)
        End Function
    End Class
    
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Linq.Expressions;
    
    namespace LinqToTerraServerProvider
    {
        class TerraServerQueryContext
        {
            // Executes the expression tree that is passed to it.
            internal static object Execute(Expression expression, bool IsEnumerable)
            {
                // The expression must represent a query over the data source.
                if (!IsQueryOverDataSource(expression))
                    throw new InvalidProgramException("No query over the data source was specified.");
    
                // Find the call to Where() and get the lambda expression predicate.
                InnermostWhereFinder whereFinder = new InnermostWhereFinder();
                MethodCallExpression whereExpression = whereFinder.GetInnermostWhere(expression);
                LambdaExpression lambdaExpression = (LambdaExpression)((UnaryExpression)(whereExpression.Arguments[1])).Operand;
    
                // Send the lambda expression through the partial evaluator.
                lambdaExpression = (LambdaExpression)Evaluator.PartialEval(lambdaExpression);
    
                // Get the place name(s) to query the Web service with.
                LocationFinder lf = new LocationFinder(lambdaExpression.Body);
                List<string> locations = lf.Locations;
                if (locations.Count == 0)
                    throw new InvalidQueryException("You must specify at least one place name in your query.");
    
                // Call the Web service and get the results.
                Place[] places = WebServiceHelper.GetPlacesFromTerraServer(locations);
    
                // Copy the IEnumerable places to an IQueryable.
                IQueryable<Place> queryablePlaces = places.AsQueryable<Place>();
    
                // Copy the expression tree that was passed in, changing only the first
                // argument of the innermost MethodCallExpression.
                ExpressionTreeModifier treeCopier = new ExpressionTreeModifier(queryablePlaces);
                Expression newExpressionTree = treeCopier.Visit(expression);
    
                // This step creates an IQueryable that executes by replacing Queryable methods with Enumerable methods.
                if (IsEnumerable)
                    return queryablePlaces.Provider.CreateQuery(newExpressionTree);
                else
                    return queryablePlaces.Provider.Execute(newExpressionTree);
            }
    
            private static bool IsQueryOverDataSource(Expression expression)
            {
                // If expression represents an unqueried IQueryable data source instance,
                // expression is of type ConstantExpression, not MethodCallExpression.
                return (expression is MethodCallExpression);
            }
        }
    }
    

    이 클래스는 쿼리 실행 작업을 구성합니다. 이 코드는 가장 안쪽에 있는 Queryable.Where 호출을 나타내는 식을 찾은 후 Queryable.Where로 전달된 조건자를 나타내는 람다 식을 검색합니다. 그런 다음 조건자 식을 메서드로 전달하여 부분적으로 계산하므로 지역 변수에 대한 모든 참조가 값으로 변환됩니다. 계속해서 이 코드는 요청된 위치를 조건자에서 추출하는 메서드를 호출하고 웹 서비스에서 결과 데이터를 가져오는 또 다른 메서드를 호출합니다.

    다음 단계에서 이 코드는 LINQ 쿼리를 나타내는 식 트리를 복사하여 한 가지를 수정합니다. 즉, 코드는 식 트리 방문자 서브클래스를 사용하여 가장 안쪽에 있는 쿼리 연산자 호출이 적용되는 데이터 소스를 웹 서비스에서 가져온 Place 개체의 구체적인 목록으로 대체합니다.

    Place 개체 목록을 식 트리에 삽입하기 전에 AsQueryable을 호출하여 개체 목록 형식을 IEnumerable에서 IQueryable로 변경합니다. 이러한 형식 변경이 필요한 이유는 식 트리를 다시 작성할 때 가장 안쪽에 있는 쿼리 연산자 메서드에 대한 메서드 호출을 나타내는 노드를 다시 구성해야 하기 때문입니다. 노드를 다시 구성하는 이유는 인수 중 하나(즉, 인수가 적용되는 데이터 소스)가 변경되었기 때문입니다. 노드를 다시 구성하는 데 사용되는 Call(Expression, MethodInfo, IEnumerable<Expression>) 메서드는 인수가 전달될 메서드의 해당 매개 변수에 인수를 할당할 수 없는 경우 예외를 throw합니다. 이 경우 Place 개체의 IEnumerable 목록을 Queryable.WhereIQueryable 매개 변수에 할당할 수 없으므로 해당 형식이 IQueryable로 변경됩니다.

    형식을 IQueryable로 변경하면 컬렉션에서 Provider 속성을 통해 액세스되는 IQueryProvider 멤버를 가져와 쿼리를 만들거나 실행할 수 있습니다. IQueryable°Place 컬렉션의 동적 형식은 EnumerableQuery로, 이것은 System.Linq API 내부의 형식입니다. 이 형식과 연결된 쿼리 공급자는 Queryable 표준 쿼리 연산자 호출을 그와 동등한 Enumerable 연산자로 대체하여 쿼리를 실행하므로 쿼리가 효과적으로 LINQ LINQ to Objects 쿼리가 됩니다.

    TerraServerQueryContext 클래스의 마지막 코드는 Place 개체의 IQueryable 목록에서 두 메서드 중 하나를 호출합니다. 즉, 클라이언트 쿼리가 열거 가능한 결과를 반환하면 CreateQuery를 호출하고 클라이언트 쿼리가 열거할 수 없는 결과를 반환하면 Execute를 호출합니다.

    이 클래스의 코드는 이 TerraServer-USA 공급자에 매우 한정적입니다. 따라서 보다 제네릭한 IQueryProvider 구현에 직접 삽입되지 않고 TerraServerQueryContext 클래스에서 캡슐화됩니다.

여기에서 만들어지는 공급자는 Queryable.Where 조건자에서 웹 서비스를 쿼리하는 정보만 요구합니다. 따라서 공급자는 LINQ to Objects를 통해 내부 EnumerableQuery 형식을 사용하여 LINQ 쿼리 실행 작업을 처리합니다. LINQ to Objects를 사용하여 쿼리를 실행하는 다른 방법은 클라이언트가 LINQ to Objects 쿼리에서 LINQ to Objects에 의해 실행될 쿼리 부분을 래핑하게 하는 것입니다. 이러한 래핑은 쿼리의 나머지 부분에서 AsEnumerable<TSource>을 호출하여 수행할 수 있습니다. 쿼리의 나머지 부분이란 공급자가 특정 용도를 위해 다시 쿼리하는 부분입니다. 이런 종류의 구현이 좋은 점 사용자 지정 공급자와 LINQ to Objects 간의 작업 분리가 더 분명하다는 것입니다.

참고

이 항목에 제시된 공급자는 최소한의 자체 쿼리 지원만 갖춘 단순한 공급자입니다. 따라서 이 공급자는 쿼리를 실행하기 위해 LINQ to Objects에 크게 의존합니다. LINQ to SQL과 같은 복잡한 LINQ 공급자는 LINQ to Objects에 어떠한 작업 처리도 의존하지 않고 전체 쿼리를 지원할 수 있습니다.

웹 서비스에서 데이터를 가져오는 클래스를 만들려면

  • 프로젝트에 WebServiceHelper 클래스(또는 Visual Basic의 모듈)를 추가합니다.

    Imports System.Collections.Generic
    Imports LinqToTerraServerProvider.TerraServerReference
    
    Friend Module WebServiceHelper
        Private numResults As Integer = 200
        Private mustHaveImage As Boolean = False
    
        Friend Function GetPlacesFromTerraServer(ByVal locations As List(Of String)) As Place()
            ' Limit the total number of Web service calls.
            If locations.Count > 5 Then
                Dim s = "This query requires more than five separate calls to the Web service. Please decrease the number of places."
                Throw New InvalidQueryException(s)
            End If
    
            Dim allPlaces As New List(Of Place)
    
            ' For each location, call the Web service method to get data.
            For Each location In locations
                Dim places = CallGetPlaceListMethod(location)
                allPlaces.AddRange(places)
            Next
    
            Return allPlaces.ToArray()
        End Function
    
        Private Function CallGetPlaceListMethod(ByVal location As String) As Place()
    
            Dim client As New TerraServiceSoapClient()
            Dim placeFacts() As PlaceFacts
    
            Try
                ' Call the Web service method "GetPlaceList".
                placeFacts = client.GetPlaceList(location, numResults, mustHaveImage)
    
                ' If we get exactly 'numResults' results, they are probably truncated.
                If (placeFacts.Length = numResults) Then
                    Dim s = "The results have been truncated by the Web service and would not be complete. Please try a different query."
                    Throw New Exception(s)
                End If
    
                ' Create Place objects from the PlaceFacts objects returned by the Web service.
                Dim places(placeFacts.Length - 1) As Place
                For i = 0 To placeFacts.Length - 1
                    places(i) = New Place(placeFacts(i).Place.City, 
                                          placeFacts(i).Place.State, 
                                          placeFacts(i).PlaceTypeId)
                Next
    
                ' Close the WCF client.
                client.Close()
    
                Return places
            Catch timeoutException As TimeoutException
                client.Abort()
                Throw
            Catch communicationException As System.ServiceModel.CommunicationException
                client.Abort()
                Throw
            End Try
        End Function
    End Module
    
    using System;
    using System.Collections.Generic;
    using LinqToTerraServerProvider.TerraServerReference;
    
    namespace LinqToTerraServerProvider
    {
        internal static class WebServiceHelper
        {
            private static int numResults = 200;
            private static bool mustHaveImage = false;
    
            internal static Place[] GetPlacesFromTerraServer(List<string> locations)
            {
                // Limit the total number of Web service calls.
                if (locations.Count > 5)
                    throw new InvalidQueryException("This query requires more than five separate calls to the Web service. Please decrease the number of locations in your query.");
    
                List<Place> allPlaces = new List<Place>();
    
                // For each location, call the Web service method to get data.
                foreach (string location in locations)
                {
                    Place[] places = CallGetPlaceListMethod(location);
                    allPlaces.AddRange(places);
                }
    
                return allPlaces.ToArray();
            }
    
            private static Place[] CallGetPlaceListMethod(string location)
            {
                TerraServiceSoapClient client = new TerraServiceSoapClient();
                PlaceFacts[] placeFacts = null;
    
                try
                {
                    // Call the Web service method "GetPlaceList".
                    placeFacts = client.GetPlaceList(location, numResults, mustHaveImage);
    
                    // If there are exactly 'numResults' results, they are probably truncated.
                    if (placeFacts.Length == numResults)
                        throw new Exception("The results have been truncated by the Web service and would not be complete. Please try a different query.");
    
                    // Create Place objects from the PlaceFacts objects returned by the Web service.
                    Place[] places = new Place[placeFacts.Length];
                    for (int i = 0; i < placeFacts.Length; i++)
                    {
                        places[i] = new Place(
                            placeFacts[i].Place.City,
                            placeFacts[i].Place.State,
                            placeFacts[i].PlaceTypeId);
                    }
    
                    // Close the WCF client.
                    client.Close();
    
                    return places;
                }
                catch (TimeoutException timeoutException)
                {
                    client.Abort();
                    throw;
                }
                catch (System.ServiceModel.CommunicationException communicationException)
                {
                    client.Abort();
                    throw;
                }
            }
        }
    }
    

    이 클래스에는 웹 서비스에서 데이터를 가져오는 기능이 들어 있습니다. 이 코드에서는 WCF(Windows Communication Foundation)에서 프로젝트용으로 자동 생성한 TerraServiceSoapClient라는 형식을 사용하여 웹 서비스 메서드 GetPlaceList를 호출합니다. 그런 다음 각 결과를 웹 서비스 메서드의 반환 형식에서 공급자가 데이터에 대해 정의한 .NET 형식으로 변환합니다.

    이 코드에는 공급자 라이브러리의 유용성을 향상시키는 두 가지 검사가 포함됩니다. 첫 번째 검사에서는 웹 서비스를 대상으로 이루어지는 총 호출 수를 쿼리 당 5번으로 제한함으로써 클라이언트 응용 프로그램이 응답을 기다리는 최대 시간을 제한합니다. 클라이언트 쿼리에 지정된 각 위치마다 하나의 웹 서비스 요청이 생성되므로 쿼리에 6개 이상의 위치가 들어 있으면 공급자가 예외를 throw합니다.

    두 번째 검사에서는 웹 서비스가 반환하는 결과 수가 웹 서비스가 반환할 수 있는 최대 결과 수와 동일한지 확인합니다. 결과 수가 최대 수와 같으면 웹 서비스의 결과가 잘린 것일 수도 있습니다. 따라서 공급자는 클라이언트로 불완전한 목록을 반환하는 대신 예외를 throw합니다.

식 트리 방문자 클래스 추가

가장 안쪽에 있는 Where 메서드 호출 식을 찾는 방문자를 만들려면

  1. 프로젝트에 ExpressionVisitor 클래스를 상속하는 InnermostWhereFinder 클래스를 추가합니다.

    Imports System.Linq.Expressions
    
    Class InnermostWhereFinder
        Inherits ExpressionVisitor
    
        Private innermostWhereExpression As MethodCallExpression
    
        Public Function GetInnermostWhere(ByVal expr As Expression) As MethodCallExpression
            Me.Visit(expr)
            Return innermostWhereExpression
        End Function
    
        Protected Overrides Function VisitMethodCall(ByVal expr As MethodCallExpression) As Expression
            If expr.Method.Name = "Where" Then
                innermostWhereExpression = expr
            End If
    
            Me.Visit(expr.Arguments(0))
    
            Return expr
        End Function
    End Class
    
    using System;
    using System.Linq.Expressions;
    
    namespace LinqToTerraServerProvider
    {
        internal class InnermostWhereFinder : ExpressionVisitor
        {
            private MethodCallExpression innermostWhereExpression;
    
            public MethodCallExpression GetInnermostWhere(Expression expression)
            {
                Visit(expression);
                return innermostWhereExpression;
            }
    
            protected override Expression VisitMethodCall(MethodCallExpression expression)
            {
                if (expression.Method.Name == "Where")
                    innermostWhereExpression = expression;
    
                Visit(expression.Arguments[0]);
    
                return expression;
            }
        }
    }
    

    이 클래스는 특정 식을 찾는 기능을 수행하는 기본 식 트리 방문자 클래스를 상속합니다. 이 기본 식 트리 방문자 클래스는 상속이 가능하도록 디자인되었으며 식 트리 이동과 같은 특정 작업에 적합하도록 만들어졌습니다. 파생된 클래스는 VisitMethodCall 메서드가 클라이언트 쿼리를 나타내는 식 트리에서 가장 안쪽 Where 호출을 나타내는 식을 찾도록 재정의합니다. 이 가장 안쪽 식이 공급자가 검색 위치를 추출하는 식입니다.

  2. System.Collections.Generic, System.Collections.ObjectModel 및 System.Linq.Expressions 네임스페이스에 대한 using 지시문(Visual Basic의 경우 Imports 문)을 파일에 추가합니다.

데이터를 추출하여 웹 서비스를 쿼리하는 방문자를 만들려면

  • 프로젝트에 LocationFinder 클래스를 추가합니다.

    Imports System.Linq.Expressions
    Imports ETH = LinqToTerraServerProvider.ExpressionTreeHelpers
    
    Friend Class LocationFinder
        Inherits ExpressionVisitor
    
        Private _expression As Expression
        Private _locations As List(Of String)
    
        Public Sub New(ByVal exp As Expression)
            Me._expression = exp
        End Sub
    
        Public ReadOnly Property Locations() As List(Of String)
            Get
                If _locations Is Nothing Then
                    _locations = New List(Of String)()
                    Me.Visit(Me._expression)
                End If
                Return Me._locations
            End Get
        End Property
    
        Protected Overrides Function VisitBinary(ByVal be As BinaryExpression) As Expression
            ' Handles Visual Basic String semantics.
            be = ETH.ConvertVBStringCompare(be)
    
            If be.NodeType = ExpressionType.Equal Then
                If (ETH.IsMemberEqualsValueExpression(be, GetType(Place), "Name")) Then
                    _locations.Add(ETH.GetValueFromEqualsExpression(be, GetType(Place), "Name"))
                    Return be
                ElseIf (ETH.IsMemberEqualsValueExpression(be, GetType(Place), "State")) Then
                    _locations.Add(ETH.GetValueFromEqualsExpression(be, GetType(Place), "State"))
                    Return be
                Else
                    Return MyBase.VisitBinary(be)
                End If
            Else
                Return MyBase.VisitBinary(be)
            End If
        End Function
    End Class
    
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Linq.Expressions;
    
    namespace LinqToTerraServerProvider
    {
        internal class LocationFinder : ExpressionVisitor
        {
            private Expression expression;
            private List<string> locations;
    
            public LocationFinder(Expression exp)
            {
                this.expression = exp;
            }
    
            public List<string> Locations
            {
                get
                {
                    if (locations == null)
                    {
                        locations = new List<string>();
                        this.Visit(this.expression);
                    }
                    return this.locations;
                }
            }
    
            protected override Expression VisitBinary(BinaryExpression be)
            {
                if (be.NodeType == ExpressionType.Equal)
                {
                    if (ExpressionTreeHelpers.IsMemberEqualsValueExpression(be, typeof(Place), "Name"))
                    {
                        locations.Add(ExpressionTreeHelpers.GetValueFromEqualsExpression(be, typeof(Place), "Name"));
                        return be;
                    }
                    else if (ExpressionTreeHelpers.IsMemberEqualsValueExpression(be, typeof(Place), "State"))
                    {
                        locations.Add(ExpressionTreeHelpers.GetValueFromEqualsExpression(be, typeof(Place), "State"));
                        return be;
                    }
                    else
                        return base.VisitBinary(be);
                }
                else
                    return base.VisitBinary(be);
            }
        }
    }
    

    이 클래스는 클라이언트가 Queryable.Where로 전달하는 조건자에서 위치 정보를 추출하는 데 사용되며, ExpressionVisitor 클래스에서 파생되어 VisitBinary 메서드만 재정의합니다.

    ExpressionVisitor 클래스는 place.Name == "Seattle" (Visual Basic의 경우 place.Name = "Seattle") 같은 동등 식 형태의 이항 식을 VisitBinary 메서드로 전송합니다. 이러한 재정의 VisitBinary 메서드에서 식이 위치 정보를 제공할 수 있는 동등 식 패턴과 일치하는 경우 해당 정보가 추출되고 위치 목록에 저장됩니다.

    방문자는 식 트리를 이동하고 검사하도록 디자인되어 있으므로 이 클래스는 식 트리 방문자를 사용하여 식 트리에서 위치 정보를 찾습니다. 방문자를 사용하지 않고 코드를 구현할 경우 결과 코드가 더 간단해지고 오류가 발생할 가능성도 줄어듭니다.

    이 연습 단계에서는 공급자가 제한적인 방법으로만 쿼리의 위치 정보를 제공합니다. 이 항목의 뒷부분에서는 더 다양한 방법으로 위치 정보를 제공하는 기능을 추가합니다.

식 트리를 수정하는 방문자를 만들려면

  • 프로젝트에 ExpressionTreeModifier 클래스를 추가합니다.

    Imports System.Linq.Expressions
    
    Friend Class ExpressionTreeModifier
        Inherits ExpressionVisitor
    
        Private queryablePlaces As IQueryable(Of Place)
    
        Friend Sub New(ByVal places As IQueryable(Of Place))
            Me.queryablePlaces = places
        End Sub
    
        Protected Overrides Function VisitConstant(ByVal c As ConstantExpression) As Expression
            ' Replace the constant QueryableTerraServerData arg with the queryable Place collection.
            If c.Type Is GetType(QueryableTerraServerData(Of Place)) Then
                Return Expression.Constant(Me.queryablePlaces)
            Else
                Return c
            End If
        End Function
    End Class
    
    using System;
    using System.Linq;
    using System.Linq.Expressions;
    
    namespace LinqToTerraServerProvider
    {
        internal class ExpressionTreeModifier : ExpressionVisitor
        {
            private IQueryable<Place> queryablePlaces;
    
            internal ExpressionTreeModifier(IQueryable<Place> places)
            {
                this.queryablePlaces = places;
            }
    
            protected override Expression VisitConstant(ConstantExpression c)
            {
                // Replace the constant QueryableTerraServerData arg with the queryable Place collection.
                if (c.Type == typeof(QueryableTerraServerData<Place>))
                    return Expression.Constant(this.queryablePlaces);
                else
                    return c;
            }
        }
    }
    

    이 클래스는 ExpressionVisitor 클래스에서 파생되며 VisitConstant 메서드만 재정의합니다. 이 메서드에서는 가장 안쪽에 있는 표준 쿼리 연산자 호출이 적용되는 개체가 Place 개체의 구체적인 목록으로 대체됩니다.

    식 트리 방문자는 식 트리를 이동, 검사 및 복사하도록 디자인되어 있으므로 이 식 트리 수정자 클래스는 식 트리 방문자를 사용합니다. 이 클래스는 기본 식 트리 방문자 클래스에서 파생되므로 기능 수행에 최소한의 코드만 필요합니다.

식 계산기 추가

클라이언트 쿼리에서 Queryable.Where 메서드로 전달되는 조건자는 람다 식의 매개 변수에 의존하지 않는 하위 식을 포함할 수 있습니다. 이처럼 격리된 하위 식은 즉시 계산할 수 있으며 그렇게 해야 합니다. 그 이유는 이러한 식이 값으로 변환되어야 하는 지역 변수나 멤버 변수에서 참조될 수 있기 때문입니다.

다음 클래스는 식에 하위 트리가 있는 경우 즉시 계산할 수 있는지 여부를 결정하는 PartialEval(Expression) 메서드를 노출합니다. 그런 다음 람다 식을 만들고 컴파일한 후 반환된 대리자를 호출하여 해당 식을 계산합니다. 마지막으로 하위 트리를 상수 값을 나타내는 새 노드로 대체합니다. 이것을 부분 계산이라고 합니다.

식 트리의 부분 계산을 수행하는 클래스를 추가하려면

  • 프로젝트에 Evaluator 클래스를 추가합니다.

    Imports System.Linq.Expressions
    
    Public Module Evaluator
        ''' <summary>Performs evaluation and replacement of independent sub-trees</summary>
        ''' <param name="expr">The root of the expression tree.</param>
        ''' <param name="fnCanBeEvaluated">A function that decides whether a given expression node can be part of the local function.</param>
        ''' <returns>A new tree with sub-trees evaluated and replaced.</returns>
        Public Function PartialEval(
            ByVal expr As Expression, 
            ByVal fnCanBeEvaluated As Func(Of Expression, Boolean)
            )  As Expression
    
            Return New SubtreeEvaluator(New Nominator(fnCanBeEvaluated).Nominate(expr)).Eval(expr)
        End Function
    
        ''' <summary>
        ''' Performs evaluation and replacement of independent sub-trees
        ''' </summary>
        ''' <param name="expression">The root of the expression tree.</param>
        ''' <returns>A new tree with sub-trees evaluated and replaced.</returns>
        Public Function PartialEval(ByVal expression As Expression) As Expression
            Return PartialEval(expression, AddressOf Evaluator.CanBeEvaluatedLocally)
        End Function
    
        Private Function CanBeEvaluatedLocally(ByVal expression As Expression) As Boolean
            Return expression.NodeType <> ExpressionType.Parameter
        End Function
    
        ''' <summary>
        ''' Evaluates and replaces sub-trees when first candidate is reached (top-down)
        ''' </summary>
        Class SubtreeEvaluator
            Inherits ExpressionVisitor
    
            Private candidates As HashSet(Of Expression)
    
            Friend Sub New(ByVal candidates As HashSet(Of Expression))
                Me.candidates = candidates
            End Sub
    
            Friend Function Eval(ByVal exp As Expression) As Expression
                Return Me.Visit(exp)
            End Function
    
            Public Overrides Function Visit(ByVal exp As Expression) As Expression
                If exp Is Nothing Then
                    Return Nothing
                ElseIf Me.candidates.Contains(exp) Then
                    Return Me.Evaluate(exp)
                End If
    
                Return MyBase.Visit(exp)
            End Function
    
            Private Function Evaluate(ByVal e As Expression) As Expression
                If e.NodeType = ExpressionType.Constant Then
                    Return e
                End If
    
                Dim lambda = Expression.Lambda(e)
                Dim fn As [Delegate] = lambda.Compile()
    
                Return Expression.Constant(fn.DynamicInvoke(Nothing), e.Type)
            End Function
        End Class
    
    
        ''' <summary>
        ''' Performs bottom-up analysis to determine which nodes can possibly
        ''' be part of an evaluated sub-tree.
        ''' </summary>
        Class Nominator
            Inherits ExpressionVisitor
    
            Private fnCanBeEvaluated As Func(Of Expression, Boolean)
            Private candidates As HashSet(Of Expression)
            Private cannotBeEvaluated As Boolean
    
            Friend Sub New(ByVal fnCanBeEvaluated As Func(Of Expression, Boolean))
                Me.fnCanBeEvaluated = fnCanBeEvaluated
            End Sub
    
            Friend Function Nominate(ByVal expr As Expression) As HashSet(Of Expression)
                Me.candidates = New HashSet(Of Expression)()
                Me.Visit(expr)
    
                Return Me.candidates
            End Function
    
            Public Overrides Function Visit(ByVal expr As Expression) As Expression
                If expr IsNot Nothing Then
    
                    Dim saveCannotBeEvaluated = Me.cannotBeEvaluated
                    Me.cannotBeEvaluated = False
    
                    MyBase.Visit(expr)
    
                    If Not Me.cannotBeEvaluated Then
                        If Me.fnCanBeEvaluated(expr) Then
                            Me.candidates.Add(expr)
                        Else
                            Me.cannotBeEvaluated = True
                        End If
                    End If
    
                    Me.cannotBeEvaluated = Me.cannotBeEvaluated Or 
                                           saveCannotBeEvaluated
                End If
    
                Return expr
            End Function
        End Class
    End Module
    
    using System;
    using System.Collections.Generic;
    using System.Linq.Expressions;
    
    namespace LinqToTerraServerProvider
    {
        public static class Evaluator
        {
            /// <summary>
            /// Performs evaluation & replacement of independent sub-trees
            /// </summary>
            /// <param name="expression">The root of the expression tree.</param>
            /// <param name="fnCanBeEvaluated">A function that decides whether a given expression node can be part of the local function.</param>
            /// <returns>A new tree with sub-trees evaluated and replaced.</returns>
            public static Expression PartialEval(Expression expression, Func<Expression, bool> fnCanBeEvaluated)
            {
                return new SubtreeEvaluator(new Nominator(fnCanBeEvaluated).Nominate(expression)).Eval(expression);
            }
    
            /// <summary>
            /// Performs evaluation & replacement of independent sub-trees
            /// </summary>
            /// <param name="expression">The root of the expression tree.</param>
            /// <returns>A new tree with sub-trees evaluated and replaced.</returns>
            public static Expression PartialEval(Expression expression)
            {
                return PartialEval(expression, Evaluator.CanBeEvaluatedLocally);
            }
    
            private static bool CanBeEvaluatedLocally(Expression expression)
            {
                return expression.NodeType != ExpressionType.Parameter;
            }
    
            /// <summary>
            /// Evaluates & replaces sub-trees when first candidate is reached (top-down)
            /// </summary>
            class SubtreeEvaluator : ExpressionVisitor
            {
                HashSet<Expression> candidates;
    
                internal SubtreeEvaluator(HashSet<Expression> candidates)
                {
                    this.candidates = candidates;
                }
    
                internal Expression Eval(Expression exp)
                {
                    return this.Visit(exp);
                }
    
                public override Expression Visit(Expression exp)
                {
                    if (exp == null)
                    {
                        return null;
                    }
                    if (this.candidates.Contains(exp))
                    {
                        return this.Evaluate(exp);
                    }
                    return base.Visit(exp);
                }
    
                private Expression Evaluate(Expression e)
                {
                    if (e.NodeType == ExpressionType.Constant)
                    {
                        return e;
                    }
                    LambdaExpression lambda = Expression.Lambda(e);
                    Delegate fn = lambda.Compile();
                    return Expression.Constant(fn.DynamicInvoke(null), e.Type);
                }
            }
    
            /// <summary>
            /// Performs bottom-up analysis to determine which nodes can possibly
            /// be part of an evaluated sub-tree.
            /// </summary>
            class Nominator : ExpressionVisitor
            {
                Func<Expression, bool> fnCanBeEvaluated;
                HashSet<Expression> candidates;
                bool cannotBeEvaluated;
    
                internal Nominator(Func<Expression, bool> fnCanBeEvaluated)
                {
                    this.fnCanBeEvaluated = fnCanBeEvaluated;
                }
    
                internal HashSet<Expression> Nominate(Expression expression)
                {
                    this.candidates = new HashSet<Expression>();
                    this.Visit(expression);
                    return this.candidates;
                }
    
                public override Expression Visit(Expression expression)
                {
                    if (expression != null)
                    {
                        bool saveCannotBeEvaluated = this.cannotBeEvaluated;
                        this.cannotBeEvaluated = false;
                        base.Visit(expression);
                        if (!this.cannotBeEvaluated)
                        {
                            if (this.fnCanBeEvaluated(expression))
                            {
                                this.candidates.Add(expression);
                            }
                            else
                            {
                                this.cannotBeEvaluated = true;
                            }
                        }
                        this.cannotBeEvaluated |= saveCannotBeEvaluated;
                    }
                    return expression;
                }
            }
        }
    }
    

도우미 클래스 추가

이 단원에는 공급자에 사용할 세 가지 도우미 클래스에 대한 코드가 있습니다.

System.Linq.IQueryProvider 구현에서 사용되는 도우미 클래스를 추가하려면

  • 프로젝트에 TypeSystem 클래스(또는 Visual Basic의 모듈)를 추가합니다.

    Imports System.Collections.Generic
    
    Friend Module TypeSystem
    
        Friend Function GetElementType(ByVal seqType As Type) As Type
            Dim ienum As Type = FindIEnumerable(seqType)
    
            If ienum Is Nothing Then
                Return seqType
            End If
    
            Return ienum.GetGenericArguments()(0)
        End Function
    
        Private Function FindIEnumerable(ByVal seqType As Type) As Type
    
            If seqType Is Nothing Or seqType Is GetType(String) Then
                Return Nothing
            End If
    
            If (seqType.IsArray) Then
                Return GetType(IEnumerable(Of )).MakeGenericType(seqType.GetElementType())
            End If
    
            If (seqType.IsGenericType) Then
                For Each arg As Type In seqType.GetGenericArguments()
                    Dim ienum As Type = GetType(IEnumerable(Of )).MakeGenericType(arg)
    
                    If (ienum.IsAssignableFrom(seqType)) Then
                        Return ienum
                    End If
                Next
            End If
    
            Dim ifaces As Type() = seqType.GetInterfaces()
    
            If ifaces IsNot Nothing And ifaces.Length > 0 Then
                For Each iface As Type In ifaces
                    Dim ienum As Type = FindIEnumerable(iface)
    
                    If (ienum IsNot Nothing) Then
                        Return ienum
                    End If
                Next
            End If
    
            If seqType.BaseType IsNot Nothing AndAlso
               seqType.BaseType IsNot GetType(Object) Then
    
                Return FindIEnumerable(seqType.BaseType)
            End If
    
            Return Nothing
        End Function
    End Module
    
    using System;
    using System.Collections.Generic;
    
    namespace LinqToTerraServerProvider
    {
        internal static class TypeSystem
        {
            internal static Type GetElementType(Type seqType)
            {
                Type ienum = FindIEnumerable(seqType);
                if (ienum == null) return seqType;
                return ienum.GetGenericArguments()[0];
            }
    
            private static Type FindIEnumerable(Type seqType)
            {
                if (seqType == null || seqType == typeof(string))
                    return null;
    
                if (seqType.IsArray)
                    return typeof(IEnumerable<>).MakeGenericType(seqType.GetElementType());
    
                if (seqType.IsGenericType)
                {
                    foreach (Type arg in seqType.GetGenericArguments())
                    {
                        Type ienum = typeof(IEnumerable<>).MakeGenericType(arg);
                        if (ienum.IsAssignableFrom(seqType))
                        {
                            return ienum;
                        }
                    }
                }
    
                Type[] ifaces = seqType.GetInterfaces();
                if (ifaces != null && ifaces.Length > 0)
                {
                    foreach (Type iface in ifaces)
                    {
                        Type ienum = FindIEnumerable(iface);
                        if (ienum != null) return ienum;
                    }
                }
    
                if (seqType.BaseType != null && seqType.BaseType != typeof(object))
                {
                    return FindIEnumerable(seqType.BaseType);
                }
    
                return null;
            }
        }
    }
    

    이전에 추가한 IQueryProvider 구현에서는 이 도우미 클래스를 사용합니다.

    TypeSystem.GetElementType은 리플렉션을 사용하여 IEnumerable<T>(Visual Basic의 경우 IEnumerable(Of T)) 컬렉션의 제네릭 형식 인수를 가져옵니다. 이 메서드는 쿼리 공급자 구현의 제네릭이 아닌 CreateQuery 메서드에서 호출되어 쿼리 결과 컬렉션의 요소 형식을 제공합니다.

    이 도우미 클래스는 이 TerraServer-USA 웹 서비스 공급자에 한정되지 않으므로 모든 LINQ 공급자에 재사용할 수 있습니다.

식 트리 도우미 클래스를 만들려면

  • 프로젝트에 ExpressionTreeHelpers 클래스를 추가합니다.

    Imports System.Linq.Expressions
    
    Friend Class ExpressionTreeHelpers
        ' Visual Basic encodes string comparisons as a method call to
        ' Microsoft.VisualBasic.CompilerServices.Operators.CompareString.
        ' This method will convert the method call into a binary operation instead.
        ' Note that this makes the string comparison case sensitive.
        Friend Shared Function ConvertVBStringCompare(ByVal exp As BinaryExpression) As BinaryExpression
    
            If exp.Left.NodeType = ExpressionType.Call Then
                Dim compareStringCall = CType(exp.Left, MethodCallExpression)
    
                If compareStringCall.Method.DeclaringType.FullName = 
                    "Microsoft.VisualBasic.CompilerServices.Operators" AndAlso 
                    compareStringCall.Method.Name = "CompareString" Then
    
                    Dim arg1 = compareStringCall.Arguments(0)
                    Dim arg2 = compareStringCall.Arguments(1)
    
                    Select Case exp.NodeType
                        Case ExpressionType.LessThan
                            Return Expression.LessThan(arg1, arg2)
                        Case ExpressionType.LessThanOrEqual
                            Return Expression.GreaterThan(arg1, arg2)
                        Case ExpressionType.GreaterThan
                            Return Expression.GreaterThan(arg1, arg2)
                        Case ExpressionType.GreaterThanOrEqual
                            Return Expression.GreaterThanOrEqual(arg1, arg2)
                        Case Else
                            Return Expression.Equal(arg1, arg2)
                    End Select
                End If
            End If
            Return exp
        End Function
    
        Friend Shared Function IsMemberEqualsValueExpression(
            ByVal exp As Expression, 
            ByVal declaringType As Type, 
            ByVal memberName As String) As Boolean
    
            If exp.NodeType <> ExpressionType.Equal Then
                Return False
            End If
    
            Dim be = CType(exp, BinaryExpression)
    
            ' Assert.
            If IsSpecificMemberExpression(be.Left, declaringType, memberName) AndAlso 
               IsSpecificMemberExpression(be.Right, declaringType, memberName) Then
    
                Throw New Exception("Cannot have 'member' = 'member' in an expression!")
            End If
    
            Return IsSpecificMemberExpression(be.Left, declaringType, memberName) OrElse 
                   IsSpecificMemberExpression(be.Right, declaringType, memberName)
        End Function
    
    
        Friend Shared Function IsSpecificMemberExpression(
            ByVal exp As Expression, 
            ByVal declaringType As Type, 
            ByVal memberName As String) As Boolean
    
            Return (TypeOf exp Is MemberExpression) AndAlso 
                   (CType(exp, MemberExpression).Member.DeclaringType Is declaringType) AndAlso 
                   (CType(exp, MemberExpression).Member.Name = memberName)
        End Function
    
    
        Friend Shared Function GetValueFromEqualsExpression(
            ByVal be As BinaryExpression, 
            ByVal memberDeclaringType As Type, 
            ByVal memberName As String) As String
    
            If be.NodeType <> ExpressionType.Equal Then
                Throw New Exception("There is a bug in this program.")
            End If
    
            If be.Left.NodeType = ExpressionType.MemberAccess Then
                Dim mEx = CType(be.Left, MemberExpression)
    
                If mEx.Member.DeclaringType Is memberDeclaringType AndAlso 
                   mEx.Member.Name = memberName Then
                    Return GetValueFromExpression(be.Right)
                End If
            ElseIf be.Right.NodeType = ExpressionType.MemberAccess Then
                Dim mEx = CType(be.Right, MemberExpression)
    
                If mEx.Member.DeclaringType Is memberDeclaringType AndAlso 
                   mEx.Member.Name = memberName Then
                    Return GetValueFromExpression(be.Left)
                End If
            End If
    
            ' We should have returned by now.
            Throw New Exception("There is a bug in this program.")
        End Function
    
        Friend Shared Function GetValueFromExpression(ByVal expr As expression) As String
            If expr.NodeType = ExpressionType.Constant Then
                Return CStr(CType(expr, ConstantExpression).Value)
            Else
                Dim s = "The expression type {0} is not supported to obtain a value."
                Throw New InvalidQueryException(String.Format(s, expr.NodeType))
            End If
        End Function
    End Class
    
    using System;
    using System.Linq.Expressions;
    
    namespace LinqToTerraServerProvider
    {
        internal class ExpressionTreeHelpers
        {
            internal static bool IsMemberEqualsValueExpression(Expression exp, Type declaringType, string memberName)
            {
                if (exp.NodeType != ExpressionType.Equal)
                    return false;
    
                BinaryExpression be = (BinaryExpression)exp;
    
                // Assert.
                if (ExpressionTreeHelpers.IsSpecificMemberExpression(be.Left, declaringType, memberName) &&
                    ExpressionTreeHelpers.IsSpecificMemberExpression(be.Right, declaringType, memberName))
                    throw new Exception("Cannot have 'member' == 'member' in an expression!");
    
                return (ExpressionTreeHelpers.IsSpecificMemberExpression(be.Left, declaringType, memberName) ||
                    ExpressionTreeHelpers.IsSpecificMemberExpression(be.Right, declaringType, memberName));
            }
    
            internal static bool IsSpecificMemberExpression(Expression exp, Type declaringType, string memberName)
            {
                return ((exp is MemberExpression) &&
                    (((MemberExpression)exp).Member.DeclaringType == declaringType) &&
                    (((MemberExpression)exp).Member.Name == memberName));
            }
    
            internal static string GetValueFromEqualsExpression(BinaryExpression be, Type memberDeclaringType, string memberName)
            {
                if (be.NodeType != ExpressionType.Equal)
                    throw new Exception("There is a bug in this program.");
    
                if (be.Left.NodeType == ExpressionType.MemberAccess)
                {
                    MemberExpression me = (MemberExpression)be.Left;
    
                    if (me.Member.DeclaringType == memberDeclaringType && me.Member.Name == memberName)
                    {
                        return GetValueFromExpression(be.Right);
                    }
                }
                else if (be.Right.NodeType == ExpressionType.MemberAccess)
                {
                    MemberExpression me = (MemberExpression)be.Right;
    
                    if (me.Member.DeclaringType == memberDeclaringType && me.Member.Name == memberName)
                    {
                        return GetValueFromExpression(be.Left);
                    }
                }
    
                // We should have returned by now.
                throw new Exception("There is a bug in this program.");
            }
    
            internal static string GetValueFromExpression(Expression expression)
            {
                if (expression.NodeType == ExpressionType.Constant)
                    return (string)(((ConstantExpression)expression).Value);
                else
                    throw new InvalidQueryException(
                        String.Format("The expression type {0} is not supported to obtain a value.", expression.NodeType));
            }
        }
    }
    

    이 클래스에는 식 트리의 특정 형식에서 데이터에 대한 정보를 확인하고 데이터를 추출하는 데 사용할 수 있는 메서드가 있습니다. 이 공급자에서는 이러한 메서드가 LocationFinder 클래스에서 사용되어 쿼리를 나타내는 식 트리에서 위치 정보를 추출합니다.

잘못된 쿼리에 대한 예외 형식을 추가하려면

  • 프로젝트에 InvalidQueryException 클래스를 추가합니다.

    Public Class InvalidQueryException
        Inherits Exception
    
        Private _message As String
    
        Public Sub New(ByVal message As String)
            Me._message = message & " "
        End Sub
    
        Public Overrides ReadOnly Property Message() As String
            Get
                Return "The client query is invalid: " & _message
            End Get
        End Property
    End Class
    
    using System;
    
    namespace LinqToTerraServerProvider
    {
        class InvalidQueryException : System.Exception
        {
            private string message;
    
            public InvalidQueryException(string message)
            {
                this.message = message + " ";
            }
    
            public override string Message
            {
                get
                {
                    return "The client query is invalid: " + message;
                }
            }
        }
    }
    

    이 클래스는 공급자가 클라이언트의 LINQ 쿼리를 이해하지 못하는 경우 throw할 수 있는 Exception 형식을 정의합니다. 이처럼 잘못된 쿼리에 대한 예외 형식을 정의하면 코드의 다양한 위치에서 공급자가 단순한 Exception보다 더 구체적인 예외를 throw할 수 있습니다.

이제 공급자를 컴파일하는 데 필요한 모든 코드 조각을 추가했습니다. LinqToTerraServerProvider 프로젝트를 빌드하고 컴파일 오류가 없는지 확인합니다.

LINQ 공급자 테스트

데이터 소스에 대한 LINQ 쿼리를 포함하는 클라이언트 응용 프로그램을 만들어 LINQ 공급자를 테스트할 수 있습니다.

공급자를 테스트하는 클라이언트 응용 프로그램을 만들려면

  1. 콘솔 응용 프로그램 프로젝트를 솔루션에 추가하고 이름을 ClientApp로 지정합니다.

  2. 새 프로젝트에서 공급자 어셈블리에 참조를 추가합니다.

  3. app.config 파일을 공급자 프로젝트에서 클라이언트 프로젝트로 끌어 놓습니다. 웹 서비스와 통신하려면 이 파일이 필요합니다.

    참고

    Visual Basic의 솔루션 탐색기에서 app.config 파일을 표시하려면 모든 파일 표시 단추를 클릭해야 할 수도 있습니다.

  4. 다음 using 문(Visual Basic의 경우 Imports 문)을 Program.cs(Visual Basic의 경우 Module1.vb) 파일에 추가합니다.

    using System;
    using System.Linq;
    using LinqToTerraServerProvider;
    
    Imports LinqToTerraServerProvider
    
  5. Program.cs(Visual Basic의 경우 Module1.vb) 파일의 Main 메서드에 다음 코드를 삽입합니다.

    QueryableTerraServerData<Place> terraPlaces = new QueryableTerraServerData<Place>();
    
    var query = from place in terraPlaces
                where place.Name == "Johannesburg"
                select place.PlaceType;
    
    foreach (PlaceType placeType in query)
        Console.WriteLine(placeType);
    
    Dim terraPlaces As New QueryableTerraServerData(Of Place)
    
    Dim query = From place In terraPlaces 
                Where place.Name = "Johannesburg" 
                Select place.PlaceType
    
    For Each placeType In query
        Console.WriteLine(placeType.ToString())
    Next
    

    이 코드에서는 공급자에서 정의한 IQueryable<T> 형식의 새 인스턴스를 만든 다음 LINQ를 사용하여 해당 개체를 쿼리합니다. 쿼리에서는 동등 식을 사용하여 데이터를 가져올 위치를 지정합니다. 데이터 소스가 IQueryable을 구현하기 때문에 컴파일러는 쿼리 식 구문을 Queryable에 정의된 표준 쿼리 연산자에 대한 호출로 변환합니다. 내부적으로 이러한 표준 쿼리 연산자 메서드는 식 트리를 빌드하고 IQueryProvider 구현의 일부로 구현한 Execute 또는 CreateQuery 메서드를 호출합니다.

  6. ClientApp를 빌드합니다.

  7. 이 클라이언트 응용 프로그램을 솔루션의 "시작" 프로젝트로 설정합니다. 솔루션 탐색기에서 ClientApp 프로젝트를 마우스 오른쪽 단추로 클릭하고 시작 프로젝트로 설정을 선택합니다.

  8. 프로그램을 실행하고 결과를 확인합니다. 약 세 가지 결과가 나타나야 합니다.

더 복잡한 쿼리 기능 추가

지금까지 작업한 공급자로는 클라이언트가 매우 제한적인 방법으로만 LINQ 쿼리의 위치 정보를 지정할 수 있습니다. 즉, 공급자가 Place.Name == "Seattle" 또는 Place.State == "Alaska"(Visual Basic의 경우 Place.Name = "Seattle" 또는 Place.State = "Alaska") 같은 동등 식의 위치 정보만 가져올 수 있습니다.

다음 절차에서는 위치 정보를 지정하는 다른 방법에 대한 지원을 추가하는 방법에 대해 설명합니다. 이 코드를 추가했다면 공급자가 place.Name.StartsWith("Seat") 같은 메서드 호출 식에서 위치 정보를 추출할 수 있습니다.

String.StartsWith를 포함하는 조건자에 대한 지원을 추가하려면

  1. LinqToTerraServerProvider 프로젝트에서 VisitMethodCall 메서드를 LocationFinder 클래스 정의에 추가합니다.

    Protected Overrides Function VisitMethodCall(ByVal m As MethodCallExpression) As Expression
        If m.Method.DeclaringType Is GetType(String) And m.Method.Name = "StartsWith" Then
            If ETH.IsSpecificMemberExpression(m.Object, GetType(Place), "Name") OrElse
               ETH.IsSpecificMemberExpression(m.Object, GetType(Place), "State") Then
                _locations.Add(ETH.GetValueFromExpression(m.Arguments(0)))
                Return m
            End If
        End If
    
        Return MyBase.VisitMethodCall(m)
    End Function
    
    protected override Expression VisitMethodCall(MethodCallExpression m)
    {
        if (m.Method.DeclaringType == typeof(String) && m.Method.Name == "StartsWith")
        {
            if (ExpressionTreeHelpers.IsSpecificMemberExpression(m.Object, typeof(Place), "Name") ||
            ExpressionTreeHelpers.IsSpecificMemberExpression(m.Object, typeof(Place), "State"))
            {
                locations.Add(ExpressionTreeHelpers.GetValueFromExpression(m.Arguments[0]));
                return m;
            }
        }
    
        return base.VisitMethodCall(m);
    }
    
  2. LinqToTerraServerProvider 프로젝트를 다시 컴파일합니다.

  3. 공급자의 새 기능을 테스트하려면 ClientApp 프로젝트에서 Program.cs(Visual Basic의 경우 Module1.vb) 파일을 엽니다. Main 메서드의 코드를 다음 코드로 바꿉니다.

    QueryableTerraServerData<Place> terraPlaces = new QueryableTerraServerData<Place>();
    
    var query = from place in terraPlaces
                where place.Name.StartsWith("Lond")
                select new { place.Name, place.State };
    
    foreach (var obj in query)
        Console.WriteLine(obj);
    
    Dim terraPlaces As New QueryableTerraServerData(Of Place)
    
    Dim query = From place In terraPlaces 
                Where place.Name.StartsWith("Lond") 
                Select place.Name, place.State
    
    For Each obj In query
        Console.WriteLine(obj)
    Next
    
  4. 프로그램을 실행하고 결과를 확인합니다. 약 29개의 결과가 나타나야 합니다.

다음 절차에서는 Enumerable.ContainsList<T>.Contains라는 두 가지 추가 메서드를 사용하여 클라이언트 쿼리를 통해 위치 정보를 지정할 수 있는 기능을 공급자에 추가하는 방법을 설명합니다. 이 코드를 추가했다면 공급자가 placeList 컬렉션이 클라이언트가 지원하는 구체적인 목록인 placeList.Contains(place.Name)와 같은 클라이언트 쿼리에서 메서드 호출 식으로부터 위치 정보를 추출할 수 있습니다. 클라이언트가 Contains 메서드를 사용할 수 있게 만들면 placeList에 위치를 추가하는 것만으로도 수에 제한 없이 위치를 지정할 수 있다는 장점이 있습니다. 즉, 위치 수가 달라져도 쿼리 구문은 변경되지 않습니다.

'where' 절에 Contains 메서드가 있는 쿼리에 대한 지원을 추가하려면

  1. LinqToTerraServerProvider 프로젝트의 LocationFinder 클래스 정의에서 VisitMethodCall 메서드를 다음 코드로 바꿉니다.

    Protected Overrides Function VisitMethodCall(ByVal m As MethodCallExpression) As Expression
        If m.Method.DeclaringType Is GetType(String) And m.Method.Name = "StartsWith" Then
            If ETH.IsSpecificMemberExpression(m.Object, GetType(Place), "Name") OrElse
               ETH.IsSpecificMemberExpression(m.Object, GetType(Place), "State") Then
                _locations.Add(ETH.GetValueFromExpression(m.Arguments(0)))
                Return m
            End If
        ElseIf m.Method.Name = "Contains" Then
            Dim valuesExpression As Expression = Nothing
    
            If m.Method.DeclaringType Is GetType(Enumerable) Then
                If ETH.IsSpecificMemberExpression(m.Arguments(1), GetType(Place), "Name") OrElse
                   ETH.IsSpecificMemberExpression(m.Arguments(1), GetType(Place), "State") Then
                    valuesExpression = m.Arguments(0)
                End If
    
            ElseIf m.Method.DeclaringType Is GetType(List(Of String)) Then
                If ETH.IsSpecificMemberExpression(m.Arguments(0), GetType(Place), "Name") OrElse
                   ETH.IsSpecificMemberExpression(m.Arguments(0), GetType(Place), "State") Then
                    valuesExpression = m.Object
                End If
            End If
    
            If valuesExpression Is Nothing OrElse valuesExpression.NodeType <> ExpressionType.Constant Then
                Throw New Exception("Could not find the location values.")
            End If
    
            Dim ce = CType(valuesExpression, ConstantExpression)
    
            Dim placeStrings = CType(ce.Value, IEnumerable(Of String))
            ' Add each string in the collection to the list of locations to obtain data about.
            For Each place In placeStrings
                _locations.Add(place)
            Next
    
            Return m
        End If
    
        Return MyBase.VisitMethodCall(m)
    End Function
    
    protected override Expression VisitMethodCall(MethodCallExpression m)
    {
        if (m.Method.DeclaringType == typeof(String) && m.Method.Name == "StartsWith")
        {
            if (ExpressionTreeHelpers.IsSpecificMemberExpression(m.Object, typeof(Place), "Name") ||
            ExpressionTreeHelpers.IsSpecificMemberExpression(m.Object, typeof(Place), "State"))
            {
                locations.Add(ExpressionTreeHelpers.GetValueFromExpression(m.Arguments[0]));
                return m;
            }
    
        }
        else if (m.Method.Name == "Contains")
        {
            Expression valuesExpression = null;
    
            if (m.Method.DeclaringType == typeof(Enumerable))
            {
                if (ExpressionTreeHelpers.IsSpecificMemberExpression(m.Arguments[1], typeof(Place), "Name") ||
                ExpressionTreeHelpers.IsSpecificMemberExpression(m.Arguments[1], typeof(Place), "State"))
                {
                    valuesExpression = m.Arguments[0];
                }
            }
            else if (m.Method.DeclaringType == typeof(List<string>))
            {
                if (ExpressionTreeHelpers.IsSpecificMemberExpression(m.Arguments[0], typeof(Place), "Name") ||
                ExpressionTreeHelpers.IsSpecificMemberExpression(m.Arguments[0], typeof(Place), "State"))
                {
                    valuesExpression = m.Object;
                }
            }
    
            if (valuesExpression == null || valuesExpression.NodeType != ExpressionType.Constant)
                throw new Exception("Could not find the location values.");
    
            ConstantExpression ce = (ConstantExpression)valuesExpression;
    
            IEnumerable<string> placeStrings = (IEnumerable<string>)ce.Value;
            // Add each string in the collection to the list of locations to obtain data about.
            foreach (string place in placeStrings)
                locations.Add(place);
    
            return m;
        }
    
        return base.VisitMethodCall(m);
    }
    

    이 메서드는 Contains가 적용되는 컬렉션의 각 문자열을 웹 서비스를 쿼리할 위치 목록에 추가합니다. Contains 메서드는 EnumerableList<T>에 모두 정의되어 있습니다. 따라서 VisitMethodCall 메서드는 두 클래스 모두에서 이러한 선언 형식을 확인해야 합니다. Enumerable.Contains는 확장 메서드로 정의되어 있으므로 메서드가 적용되는 컬렉션이 실제로 메서드에 대한 첫 번째 인수입니다. List.Contains는 인스턴스 메서드로 정의되어 있으므로 메서드가 적용되는 컬렉션이 메서드의 수신 개체입니다.

  2. LinqToTerraServerProvider 프로젝트를 다시 컴파일합니다.

  3. 공급자의 새 기능을 테스트하려면 ClientApp 프로젝트에서 Program.cs(Visual Basic의 경우 Module1.vb) 파일을 엽니다. Main 메서드의 코드를 다음 코드로 바꿉니다.

    QueryableTerraServerData<Place> terraPlaces = new QueryableTerraServerData<Place>();
    
    string[] places = { "Johannesburg", "Yachats", "Seattle" };
    
    var query = from place in terraPlaces
                where places.Contains(place.Name)
                orderby place.State
                select new { place.Name, place.State };
    
    foreach (var obj in query)
        Console.WriteLine(obj);
    
    Dim terraPlaces As New QueryableTerraServerData(Of Place)
    
    Dim places = New String() {"Johannesburg", "Yachats", "Seattle"}
    
    Dim query = From place In terraPlaces 
                Where places.Contains(place.Name) 
                Order By place.State 
                Select place.Name, place.State
    
    For Each obj In query
        Console.WriteLine(obj)
    Next
    
  4. 프로그램을 실행하고 결과를 확인합니다. 약 5개의 결과가 나타나야 합니다.

다음 단계

이 연습에서는 웹 서비스의 한 메서드에 대한 LINQ 공급자를 만드는 방법에 대해 설명했습니다. 더욱 수준 높은 LINQ 공급자를 만들고 싶다면 다음과 같은 방법을 고려해 보십시오.

  • LINQ 공급자가 다른 방법을 통해 클라이언트 쿼리에서 위치를 지정할 수 있게 합니다.

  • TerraServer-USA 웹 서비스가 노출하는 다른 메서드를 검토하여 그러한 메서드 중 하나의 인터페이스 역할을 하는 LINQ 공급자를 만듭니다.

  • 관심 있는 다른 웹 서비스를 찾아 그 웹 서비스에 대한 LINQ 공급자를 만듭니다.

  • 웹 서비스가 아닌 데이터 소스에 대한 LINQ 공급자를 만듭니다.

고유한 LINQ 공급자를 만드는 방법에 대한 자세한 내용은 MSDN 블로그에서 LINQ: Building an IQueryable Provider를 참조하십시오.

참고 항목

작업

LINQ 샘플

방법: 식 트리 수정(C# 및 Visual Basic)

참조

IQueryable<T>

IOrderedQueryable<T>

개념

LINQ 쿼리에 대한 데이터 소스 활성화

Windows Communication Foundation 서비스 및 Visual Studio의 WCF.NET 데이터 서비스