Consuming OData feeds from a workflow

WCF Data Services is a component of the .NET Framework that enables you to create services that use the Open Data Protocol (OData) to expose and consume data over the Web or intranet by using the semantics of representational state transfer (REST). OData exposes data as resources that are addressable by URIs. Any application can interact with an OData-based data service if it can send an HTTP request and process the OData feed that a data service returns. In addition, WCF Data Services includes client libraries that provide a richer programming experience when you consume OData feeds from .NET Framework applications. This topic provides an overview of consuming an OData feed in a workflow with and without using the client libraries.

Using the sample Northwind OData service

The examples in this topic use the sample Northwind data service located at https://services.odata.org/Northwind/Northwind.svc/. This service is provided as part of the OData SDK and provides read-only access to the sample Northwind database. If write access is desired, or if a local WCF Data Service is desired, you can follow the steps of the WCF Data Services Quickstart to create a local OData service that provides access to the Northwind database. If you follow the quickstart, substitute the local URI for the one provided in the example code in this topic.

Consuming an OData feed using the client libraries

WCF Data Services includes client libraries that enable you to more easily consume an OData feed from .NET Framework and client applications. These libraries simplify sending and receiving HTTP messages. They also translate the message payload into CLR objects that represent entity data. The client libraries feature the two core classes DataServiceContext and DataServiceQuery<TElement>. These classes enable you to query a data service and then work with the returned entity data as CLR objects. This section covers two approaches to creating activities that use the client libraries.

Adding a service reference to the WCF Data service

To generate the Northwind client libraries, you can use the Add Service Reference dialog box in Visual Studio 2012 to add a reference to the Northwind OData service.

Screenshot that shows the Add Service Reference dialog.

Note that there are no service operations exposed by the service, and in the Services list there are items representing the entities exposed by the Northwind data service. When the service reference is added, classes will be generated for these entities and they can be used in the client code. The examples in this topic use these classes and the NorthwindEntities class to perform the queries.

Using asynchronous methods

To address possible latency issues that may occur when accessing resources over the Web, we recommend accessing WCF Data Services asynchronously. The WCF Data Services client libraries include asynchronous methods for invoking queries, and Windows Workflow Foundation (WF) provides the AsyncCodeActivity class for authoring asynchronous activities. AsyncCodeActivity derived activities can be written to take advantage of .NET Framework classes that have asynchronous methods, or the code to be executed asynchronously can be put into a method and invoked by using a delegate. This section provides two examples of an AsyncCodeActivity derived activity; one that uses the asynchronous methods of the WCF Data Services client libraries and one that uses a delegate.

Using client library asynchronous methods

The DataServiceQuery<TElement> class provides BeginExecute and EndExecute methods for querying an OData service asynchronously. These methods can be called from the BeginExecute and EndExecute overrides of an AsyncCodeActivity derived class. When the AsyncCodeActivity BeginExecute override returns, the workflow can go idle (but not persist), and when the asynchronous work is completed, EndExecute is invoked by the runtime.

In the following example, an OrdersByCustomer activity is defined that has two input arguments. The CustomerId argument represents the customer who identifies which orders to return, and the ServiceUri argument represents the URI of the OData service to be queried. Because the activity derives from AsyncCodeActivity<IEnumerable<Order>> there is also a Result output argument that is used to return the results of the query. The BeginExecute override creates a LINQ query that selects all orders of the specified customer. This query is specified as the UserState of the passed AsyncCodeActivityContext, and then the query's BeginExecute method is called. Note that the callback and state that are passed into the query's BeginExecute are the ones that are passed in to the activity's BeginExecute method. When the query has finished executing, the activity's EndExecute method is invoked. The query is retrieved from the UserState, and then the query's EndExecute method is called. This method returns an IEnumerable<T> of the specified entity type; in this case Order. Since IEnumerable<Order> is the generic type of the AsyncCodeActivity<TResult>, this IEnumerable is set as the Result OutArgument<T> of the activity.

class OrdersByCustomer : AsyncCodeActivity<IEnumerable<Order>>
{
    [RequiredArgument]
    public InArgument<string> CustomerId { get; set; }

    [RequiredArgument]
    public InArgument<string> ServiceUri { get; set; }

    protected override IAsyncResult BeginExecute(AsyncCodeActivityContext context, AsyncCallback callback, object state)
    {
        NorthwindEntities dataContext = new NorthwindEntities(new Uri(ServiceUri.Get(context)));

        // Define a LINQ query that returns Orders and
        // Order_Details for a specific customer.
        DataServiceQuery<Order> ordersQuery = (DataServiceQuery<Order>)
            from o in dataContext.Orders.Expand("Order_Details")
            where o.Customer.CustomerID == CustomerId.Get(context)
            select o;

        // Specify the query as the UserState for the AsyncCodeActivityContext
        context.UserState = ordersQuery;

        // The callback and state used here are the ones passed into
        // the BeginExecute of this activity.
        return ordersQuery.BeginExecute(callback, state);
    }

    protected override IEnumerable<Order> EndExecute(AsyncCodeActivityContext context, IAsyncResult result)
    {
        // Get the DataServiceQuery from the context.UserState
        DataServiceQuery<Order> ordersQuery = context.UserState as DataServiceQuery<Order>;

        // Return an IEnumerable of the query results.
        return ordersQuery.EndExecute(result);
    }
}

In the following example, the OrdersByCustomer activity retrieves a list of orders for the specified customer, and then a ForEach<T> activity enumerates the returned orders and writes the date of each order to the console.

Variable<IEnumerable<Order>> orders = new Variable<IEnumerable<Order>>();
DelegateInArgument<Order> order = new DelegateInArgument<Order>();

Activity wf = new Sequence
{
    Variables = { orders },
    Activities =
    {
        new WriteLine
        {
            Text = "Calling WCF Data Service..."
        },
        new OrdersByCustomer
        {
            ServiceUri = "http://services.odata.org/Northwind/Northwind.svc/",
            CustomerId = "ALFKI",
            Result = orders
        },
        new ForEach<Order>
        {
            Values = orders,
            Body = new ActivityAction<Order>
            {
                Argument = order,
                Handler = new WriteLine
                {
                    Text = new InArgument<string>((env) => string.Format("{0:d}", order.Get(env).OrderDate))
                }
            }
        }
    }
};

WorkflowInvoker.Invoke(wf);

When this workflow is invoked, the following data is written to the console:

Calling WCF Data Service...
8/25/1997
10/3/1997
10/13/1997
1/15/1998
3/16/1998
4/9/1998

Note

If a connection to the OData server cannot be established, you will get an exception similar to the following exception:

Unhandled Exception: System.InvalidOperationException: An error occurred while processing this request. ---> System.Net.WebException: Unable to connect to the remote server ---> System.Net.Sockets.SocketException: A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond.

If any additional processing of the data returned by the query is required, it can be done in the activity's EndExecute override. Both BeginExecute and EndExecute are invoked by using the workflow thread, and any code in these overrides does not run asynchronously. If the additional processing is extensive or long-running, or the query results are paged, you should consider the approach discussed in the next section, which uses a delegate to execute the query and perform additional processing asynchronously.

Using a delegate

In addition to invoking the asynchronous method of a .NET Framework class, an AsyncCodeActivity-based activity can also define the asynchronous logic in one of its methods. This method is specified by using a delegate in the activity's BeginExecute override. When the method returns, the runtime invokes the activity's EndExecute override. When calling an OData service from a workflow, this method can be used to query the service and provide any additional processing.

In the following example, a ListCustomers activity is defined. This activity queries the sample Northwind data service and returns a List<Customer> that contains all of the customers in the Northwind database. The asynchronous work is performed by the GetCustomers method. This method queries the service for all customers, and then copies them into a List<Customer>. It then checks to see if the results are paged. If so, it queries the service for the next page of results, adds them to the list, and continues until all of the customer data has been retrieved.

Note

For more information about paging in WCF Data Services, see How to: Load Paged Results (WCF Data Services).

Once all customers are added, the list is returned. The GetCustomers method is specified in the activity's BeginExecute override. Since the method has a return value, a Func<string, List<Customer>> is created to specify the method.

Note

If the method that performs the asynchronous work does not have a return value, an Action is used instead of a Func<TResult>. For examples of creating an asynchronous example using both approaches, see Creating Asynchronous Activities.

This Func<TResult> is assigned to the UserState, and then BeginInvoke is called. Since the method to be invoked does not have access to the activity's environment of arguments, the value of the ServiceUri argument is passed as the first parameter, together with the callback and state that were passed into BeginExecute. When GetCustomers returns, the runtime invokes EndExecute. The code in EndExecute retrieves the delegate from the UserState, calls EndInvoke, and returns the result, which is the list of customers returned from the GetCustomers method.

class ListCustomers : AsyncCodeActivity<List<Customer>>
{
    [RequiredArgument]
    public InArgument<string> ServiceUri { get; set; }

    protected override IAsyncResult BeginExecute(AsyncCodeActivityContext context, AsyncCallback callback, object state)
    {
        // Create a delegate that references the method that implements
        // the asynchronous work. Assign the delegate to the UserState,
        // invoke the delegate, and return the resulting IAsyncResult.
        Func<string, List<Customer>> GetCustomersDelegate = new Func<string, List<Customer>>(GetCustomers);
        context.UserState = GetCustomersDelegate;
        return GetCustomersDelegate.BeginInvoke(ServiceUri.Get(context), callback, state);
    }

    protected override List<Customer> EndExecute(AsyncCodeActivityContext context, IAsyncResult result)
    {
        // Get the delegate from the UserState and call EndInvoke
        Func<string, List<Customer>> GetCustomersDelegate = (Func<string, List<Customer>>)context.UserState;
        return (List<Customer>)GetCustomersDelegate.EndInvoke(result);
    }

    List<Customer> GetCustomers(string serviceUri)
    {
        // Get all customers here and add them to a list. This method doesn't have access to the
        // activity's environment of arguments, so the Service Uri is passed in.

        // Create the DataServiceContext using the service URI.
        NorthwindEntities context = new NorthwindEntities(new Uri(serviceUri));

        // Return all customers.
        QueryOperationResponse<Customer> response =
            context.Customers.Execute() as QueryOperationResponse<Customer>;

        // Add them to the list.
        List<Customer> customers = new List<Customer>(response);

        // Is this the complete list or are the results paged?
        DataServiceQueryContinuation<Customer> token;
        while ((token = response.GetContinuation()) != null)
        {
            // Load the next page of results.
            response = context.Execute<Customer>(token) as QueryOperationResponse<Customer>;

            // Add the next page of customers to the list.
            customers.AddRange(response);
        }

        // Return the list of customers
        return customers;
    }
}

In the following example, the ListCustomers activity retrieves a list of customers, and then a ForEach<T> activity enumerates them and writes the company name and contact name of each customer to the console.

Variable<List<Customer>> customers = new Variable<List<Customer>>();
DelegateInArgument<Customer> customer = new DelegateInArgument<Customer>();

Activity wf = new Sequence
{
    Variables = { customers },
    Activities =
    {
        new WriteLine
        {
            Text = "Calling WCF Data Service..."
        },
        new ListCustomers
        {
            ServiceUri = "http://services.odata.org/Northwind/Northwind.svc/",
            Result = customers
        },
        new ForEach<Customer>
        {
            Values = customers,
            Body = new ActivityAction<Customer>
            {
                Argument = customer,
                Handler = new WriteLine
                {
                    Text = new InArgument<string>((env) => string.Format("{0}, Contact: {1}",
                        customer.Get(env).CompanyName, customer.Get(env).ContactName))
                }
            }
        }
    }
};

WorkflowInvoker.Invoke(wf);

When this workflow is invoked, the following data is written to the console. Since this query returns many customers, only part of the output is displayed here.

Calling WCF Data Service...
Alfreds Futterkiste, Contact: Maria Anders
Ana Trujillo Emparedados y helados, Contact: Ana Trujillo
Antonio Moreno Taquería, Contact: Antonio Moreno
Around the Horn, Contact: Thomas Hardy
Berglunds snabbköp, Contact: Christina Berglund
...

Consuming an OData feed without using the client libraries

OData exposes data as resources that are addressable by URIs. When you use the client libraries these URIs are created for you, but you do not have to use the client libraries. If desired, OData services can be accessed directly without using the client libraries. When not using the client libraries the location of the service and the desired data are specified by the URI and the results are returned in the response to the HTTP request. This raw data can then be processed or manipulated in the desired manner. One way to retrieve the results of an OData query is by using the WebClient class. In this example, the contact name for the customer represented by the key ALFKI is retrieved.

string uri = "http://services.odata.org/Northwind/Northwind.svc/Customers('ALFKI')/ContactName";
WebClient client = new WebClient();
string data = client.DownloadString(uri);
Console.WriteLine("Raw data returned:\n{0}", data);

When this code is run, the following output is displayed to the console:

Raw data returned:
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<ContactName xmlns="http://schemas.microsoft.com/ado/2007/08/dataservices">Maria Anders</ContactName>

In a workflow, the code from this example could be incorporated into the Execute override of a CodeActivity-based custom activity, but the same functionality can also be accomplished by using the InvokeMethod<TResult> activity. The InvokeMethod<TResult> activity enables workflow authors to invoke static and instance methods of a class, and also has an option to invoke the specified method asynchronously. In the following example, an InvokeMethod<TResult> activity is configured to call the DownloadString method of the WebClient class and return a list of customers.

new InvokeMethod<string>
{
    TargetObject = new InArgument<WebClient>(new VisualBasicValue<WebClient>("New System.Net.WebClient()")),
    MethodName = "DownloadString",
    Parameters =
    {
        new InArgument<string>("http://services.odata.org/Northwind/Northwind.svc/Customers('ALFKI')/Orders")
    },
    Result = data,
    RunAsynchronously = true
},

InvokeMethod<TResult> can call both static and instance methods of a class. Since DownloadString is an instance method of the WebClient class, a new instance of the WebClient class is specified for the TargetObject. DownloadString is specified as the MethodName, the URI that contains the query is specified in the Parameters collection, and the return value is assigned to the Result value. The RunAsynchronously value is set to true, which means that the method invocation will run asynchronously with regard to the workflow. In the following example, a workflow is constructed that uses the InvokeMethod<TResult> activity to query the sample Northwind data service for a list of orders for a specific customer, and then the returned data is written to the console.

Variable<string> data = new Variable<string>();

Activity wf = new Sequence
{
    Variables = { data },
    Activities =
    {
        new WriteLine
        {
            Text = "Calling WCF Data Service..."
        },
        new InvokeMethod<string>
        {
            TargetObject = new InArgument<WebClient>(new VisualBasicValue<WebClient>("New System.Net.WebClient()")),
            MethodName = "DownloadString",
            Parameters =
            {
                new InArgument<string>("http://services.odata.org/Northwind/Northwind.svc/Customers('ALFKI')/Orders")
            },
            Result = data,
            RunAsynchronously = true
        },
        new WriteLine
        {
            Text = new InArgument<string>(env => string.Format("Raw data returned:\n{0}", data.Get(env)))
        }
    }
};

WorkflowInvoker.Invoke(wf);

When this workflow is invoked, the following output is displayed to the console. Since this query returns several orders, only part of the output is displayed here.

Calling WCF Data Service...
Raw data returned:

<?xml version="1.0" encoding="utf-8" standalone="yes"?>*
<feed
xml:base="http://services.odata.org/Northwind/Northwind.svc/"
xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices"
xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"
xmlns="http://www.w3.org/2005/Atom">
<title type="text">Orders\</title>
<id>http://services.odata.org/Northwind/Northwind.svc/Customers('ALFKI')/Orders\</id>
<updated>2010-05-19T19:37:07Z\</updated>
<link rel="self" title="Orders" href="Orders" />
<entry>
<id>http://services.odata.org/Northwind/Northwind.svc/Orders(10643)\</id>
<title type="text">\</title>
<updated>2010-05-19T19:37:07Z\</updated>
<author>
<name />
</author>
<link rel="edit" title="Order" href="Orders(10643)" />
<link rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/Customer" type="application/atom+xml;type=entry" title="Customer" href="Orders(10643)/Customer" />
...

This example provides one method that workflow application authors can use to consume the raw data returned from an OData service. For more information about accessing WCF Data Services using URIs, see Accessing Data Service Resources (WCF Data Services) and OData: URI Conventions.