Channels ▼
RSS

Parallel

More Information-Rich Programming with F#


In the first article in this series, Information-Rich Programming With F# 3.0, I provided a brief overview of one of the most interesting features of F# 3.0: the new type providers. In addition, I developed a complete example with one of the options to establish a quick connection to a SQL database. This second and final article on information-rich programming with F# 3.0 shows how to consume old but still popular WSDL services, newer OData services, and other kinds of data sources around the Web.

Consuming a WSDL Web Service with a Type Provider

Web Service Description Language (WSDL) is an XML-based standard format for describing network services. It was a part of the original implementation of Web services, prior to the wide adoption of REST. It is a bit old school, but still widely used.

If you want to make calls with a typed language (such as many .NET languages), you have to use a tool to generate code that interprets the XML to provide type information and generate .NET code to make it easy for developers to make the Web Service calls with typed requests and responses. However, F# 3.0 includes a type provider prepared to do that job (without needing to use the Add Service Reference feature that might be familiar to some .NET developers who used WSDL in the past): the WsdlService (Microsoft.FSharp.Data.TypeProviders.WsdlService) provider.

If you're working in an F# project, you must add the following references before using the WsdlService:

  • System.ServiceModel and System.Runtime.Serialization in Assemblies | Framework. In addition, if you plan to use LINQ, don't forget to add both System.Data and System.Data.Linq in Assemblies | Framework.
  • FSharp.Data.TypeProviders in Assemblies | Extensions.

If you prefer the F# script and you want to work in the FSI window, the following lines will have the same effect. Note that you will want to execute them if you want to test code in the FSI window related to the WsdlService type provider.

#r "System.Data"
#r "System.Data.Linq"
#r "System.Runtime.Serialization "
#r "System.ServiceModel "
#r "FSharp.Data.TypeProviders"

I've chosen a simple weather Web Service for this example. I just want to make a call to the GetCityForecastByZIP Web Service method. You can inspect the entire WSDL for the Web Service by visiting http://wsf.cdyne.com/WeatherWS/Weather.asmx?WSDL (see Figure 1).

[Click image to view at full size]
Figure 1: The XML WSDL service definition that the WsdlService type provider uses to build the types for F#.

If you take a look at the XML portion of the WSDL service definition that specifies the request and the response for that method, it is easy to see that the WsdlService type provider can supply the necessary information to infer types from there:

<s:element name="GetCityForecastByZIP">
  <s:complexType>
    <s:sequence>
      <s:element minOccurs="0" maxOccurs="1" name="ZIP" type="s:string"/>
    </s:sequence>
  </s:complexType>
</s:element>
<s:element name="GetCityForecastByZIPResponse">
  <s:complexType>
    <s:sequence>
      <s:element minOccurs="0" maxOccurs="1" name="GetCityForecastByZIPResult" type="tns:ForecastReturn"/>
    </s:sequence>
  </s:complexType>
</s:element>
<s:complexType name="ForecastReturn">
  <s:sequence>
    <s:element minOccurs="1" maxOccurs="1" name="Success" type="s:boolean"/>
    <s:element minOccurs="0" maxOccurs="1" name="ResponseText" type="s:string"/>
    <s:element minOccurs="0" maxOccurs="1" name="State" type="s:string"/>
    <s:element minOccurs="0" maxOccurs="1" name="City" type="s:string"/>
    <s:element minOccurs="0" maxOccurs="1" name="WeatherStationCity" type="s:string"/>
    <s:element minOccurs="0" maxOccurs="1" name="ForecastResult" type="tns:ArrayOfForecast"/>
  </s:sequence>
</s:complexType>
<s:complexType name="ArrayOfForecast">
  <s:sequence>
    <s:element minOccurs="0" maxOccurs="unbounded" name="Forecast" nillable="true" type="tns:Forecast"/>
  </s:sequence>
</s:complexType>
<s:complexType name="Forecast">
  <s:sequence>
    <s:element minOccurs="1" maxOccurs="1" name="Date" type="s:dateTime"/>
    <s:element minOccurs="1" maxOccurs="1" name="WeatherID" type="s:short"/>
    <s:element minOccurs="0" maxOccurs="1" name="Desciption" type="s:string"/>
    <s:element minOccurs="1" maxOccurs="1" name="Temperatures" type="tns:temp"/>
    <s:element minOccurs="1" maxOccurs="1" name="ProbabilityOfPrecipiation" type="tns:POP"/>
  </s:sequence>
</s:complexType>
<s:complexType name="temp">
  <s:sequence>
    <s:element minOccurs="0" maxOccurs="1" name="MorningLow" type="s:string"/>
    <s:element minOccurs="0" maxOccurs="1" name="DaytimeHigh" type="s:string"/>
  </s:sequence>
</s:complexType>
<s:complexType name="POP">
  <s:sequence>
    <s:element minOccurs="0" maxOccurs="1" name="Nighttime" type="s:string"/>
    <s:element minOccurs="0" maxOccurs="1" name="Daytime" type="s:string"/>
  </s:sequence>
</s:complexType>

The following lines show the basic usage of the WsdlService type provider to make a synchronous call to the GetCityForecastByZIP method and display the description for the weather forecast (for example, Mostly Cloudy). Notice that in this case, it isn't necessary to make additional configurations for security, endpoints, and channels.

module WsdlConnectivity = 
    open System
    open System.Runtime.Serialization
    open System.ServiceModel
    open Microsoft.FSharp.Data.TypeProviders

    [<Literal>]
    let WeatherServiceAddress = "http://wsf.cdyne.com/WeatherWS/Weather.asmx?WSDL"
    
    type WeatherService = WsdlService<WeatherServiceAddress>

    let weatherServiceClient = WeatherService.GetWeatherSoap()

    let zipCode = "89509"

    let forecastResult = weatherServiceClient.GetCityForecastByZIP zipCode
    // It's OK, it's Desciption instead of Description (a bug in the WSDL)
    printfn "%s: %s" zipCode forecastResult.ForecastResult.[0].Desciption

    let ReadAnyKey() = 
        Console.WriteLine("Press any key to continue")
        Console.ReadKey() |> ignore

    ReadAnyKey()

This code is easy to understand. WeatherService is the type abbreviation (or alias) for the WsdlService type provider with the specified Web service WSDL definition address. In this case, the Web Service WSDL definition address is defined as a literal constant (notice the literal attribute).

The GetWeatherSoap static method gets a simplified data context for the WSDL service. weatherServiceClient is of type WeatherService.ServiceTypes.SimpleDataContextTypes.WeatherSoapClient. Because the type provider already established the connection and generated the necessary types, when you enter "WeatherService," IntelliSense will display the available methods, such as GetWeatherSoap and the ServiceTypes with all the types required for the service as nested types (see Figure 2).

[Click image to view at full size]
Figure 2: IntelliSense displaying a summary of all the nested types.

If you enter the following line:

  let forecastResult = weatherServiceClient.

you will notice how IntelliSense displays all the Web service methods that you can call in two versions: synchronous and asynchronous (see Figure 3):

  • GetCityForecastByZIP
  • GetCityForecastByZIPAsync
  • GetCityWeatherByZIP
  • GetCityWeatherByZIPAsync
  • GetWeatherInformation
  • GetWeatherInformationAsync
[Click image to view at full size]
Figure 3: IntelliSense displaying the DataContext property and the methods with their synchronous and asynchronous versions.

In the previous example, the code calls the GetCityForecastByZIP Web service method with a synchronous execution and displays the ZIP code and the returned weather forecast description with printfn. Once you write the line that defines forecastResult, the type inference mechanism knows that forecastResult is of type

WeatherService.ServiceTypes.ws.cdyne.com.WeatherWS.ForecastReturn: 
let forecastResult = weatherServiceClient.GetCityForecastByZIP zipCode

Thus, after you write "forecastResult," IntelliSense will display the different fields available for the response. To keep the example simple, I haven't added exception handling and I assume I'll have a successful response from the Web Service. The response returns an array of forecast results. I also assume there is at least one item (see Figure 4). This way, you focus on the code and you can consume a complex Web Service without having to use additional tools or generate new code for your project. You focus on creating your F# algorithms with the results of the Web service calls.

[Click image to view at full size]
Figure 4: F# type inference + IntelliSense provide you with the field names for the array of forecast results.

If you execute the code, you will see the ZIP code with the description of the weather forecast displayed on the console after the Web service call is successful:

  89509: Mostly Cloudy
  Press any key to continue

Making Asynchronous Calls to a WSDL Web Service with a Type Provider

The previous example made calls to the GetCityForecastByZIP Web service method with synchronous execution. If you want to retrieve the weather forecast description for dozens of ZIP codes, it is a good idea to take advantage of the asynchronous Web service methods that the WsdlService type provider generates (the asynchronous methods end with the Async suffix).

The following lines show a more complex usage of the WsdlService type provider to launch multiple asynchronous calls to the GetCityForecastByZIPAsync method in parallel, and display the description for the weather forecasts for all the ZIP codes defined in the zipCodes string list.

module WsdlConnectivity = 
    open System
    open System.ServiceModel
    open System.Runtime.Serialization
    open Microsoft.FSharp.Data.TypeProviders

    [<Literal>]
    let WeatherServiceAddress = "http://wsf.cdyne.com/WeatherWS/Weather.asmx?WSDL"
    
    type WeatherService = WsdlService<WeatherServiceAddress>

    let weatherServiceClient = WeatherService.GetWeatherSoap()

    let zipCodes = [ "89509"; "90042"; "90404"; "91101";
                     "89510"; "90043"; "90405"; "91103";
                     "89511"; "90044"; "90501"; "91104";
                     "89512"; "90045"; "90502"; "91105";
                     "89523"; "90046"; "90503"; "91106";
                     "89701"; "90047"; "90504"; "91107";
                     "89703"; "90048"; "90505"; "91108";
                     "89704"; "90049"; "90601"; "91201";
                     "89705"; "90056"; "90602"; "91202";
                     "89706"; "90057"; "90603"; "91203";
                     "89801"; "90058"; "90604"; "91204";
                     "89803"; "90059"; "90605"; "91205";
                     "89815"; "90061"; "90606"; "91206";
                     "89820"; "90062"; "90620"; "91207";
                     "89821"; "90063"; "90621"; "91208";
                     "89822"; "90064"; "90623"; "91214";
                     "89823"; "90065"; "90630"; "91301";
                     "89824"; "90066"; "90631"; "91302";
                     "89825"; "90067"; "90638"; "91303";
                     "89826"; "90068"; "90640"; "91304";
                     "89828"; "90069"; "90650"; "91306";
                     "89830"; "90071"; "90660"; "91307";
                     "89831"; "90077"; "90670"; "91311";
                     "89832"; "90201"; "90680"; "91316";
                     "89833"; "90210"; "90701"; "91320";
                     "89834"; "90211"; "90703"; "91321";
                     "89835"; "90212"; "90704"; "89883"; 
                     "90220"; "90706" ]

    zipCodes
        |> List.map (fun zipCode ->
            async {
                let! forecastResult = Async.AwaitTask <| weatherServiceClient.GetCityForecastByZIPAsync zipCode
                // It's OK, it's Desciption instead of Description (a bug in the WSDL)
                do Console.WriteLine("{0}: {1}", zipCode,
          forecastResult.Body.GetCityForecastByZIPResult.ForecastResult.[0].Desciption)
            })
        |> Async.Parallel
        |> Async.Ignore
        |> Async.RunSynchronously

    let ReadAnyKey() = 
        Console.WriteLine("Press any key to continue")
        Console.ReadKey() |> ignore

    ReadAnyKey()

The code is easy to understand. The zipCodes feed List.map, which launches an asynchronous block that uses Async.AwaitTask to wait for the asynchronous operation before going on with the next statement. Thus, the results will appear on the console after the service call returns with the result. However, the key advantage is that Async.Parallel creates an asynchronous computation that queues and executes all the provided asynchronous computations with a fork/join pattern. Thus, the code maximizes throughput in Web service calls that have some latency and provides results as soon as each Web service call finishes executing (See Figure 5).

[Click image to view at full size]
Figure 5: The final results of executing the code that launches many asynchronous Web service calls in parallel.

The Web service methods generated by the WsdlService type provider that end with the Async suffix simplify the generation of asynchronous computations in F#, and make it easy to execute many Web service methods in parallel. Of course, it is necessary to add exception handling and to check the results of the Web service call before displaying them. In this example, I simply want to ensure you understand why the WsdlService type provider provides the methods with the Async suffix and how you can use it with asynchronous computations.

Consuming an OData Web Service with a Type Provider

Many modern Web Services use the Open Data Protocol (OData) Web protocol to expose information. OData is ideal when you need to query and update data, and therefore, F# 3.0 also includes a type provider prepared to interact with an OData Web Service, the ODataService (Microsoft.FSharp.Data.TypeProviders.ODataService) provider.

If you're working in an F# project, you must add the following references before using the ODataService:

  • FSharp.Data.TypeProviders in Assemblies | Extensions.
  • If you plan to use LINQ, don't forget to add both System.Data and System.Data.Linq in Assemblies | Framework.

If you prefer the F# script and you want to work in the FSI window, the following lines will have the same effect. Take into account that you will have to execute them if you want to test code in either the FSI window or any F# script related to the ODataService type provider.

#r "System.Data"
#r "System.Data.Linq"
#r "FSharp.Data.TypeProviders"

I've chosen a simple OData service from the International .NET Association (INETA) for this example. I simply want to make a call to query the presentations to find the first one that contains a specific word in the abstract. You can inspect the exposed collections by visiting http://live.ineta.org/InetaLiveService.svc/ (see Figure 6).

[Click image to view at full size]
Figure 6: The XML with the collections that the ODataService type provider uses to build the types for F#.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video