Thursday, April 16, 2015

Using FSharp.Data in Windows Store app

Idea to create a dev blog crossed my mind long time ago, but I didn't get to finally do it until now. Hope someone will find my thoughts and experience useful.

Today I want to share my experience of integrating FSharp.Data library into Windows Store application. It took couple of evenings to finally make it work, and few times I thought about dropping this idea and doing everything in good old C# instead. Glad that it finally worked out. So I'd like to share my findings in case someone wants to build something similar.

I started to learn F# in 2011, and the more I got to know it, the more I liked it. Since then I'm trying to use it whenever possible, but unfortunately can't say that I had a lot of success with that.

Recently I decided to rewrite part of  Window Store application I develop as a hobby project, and decided to give F# a try here. Application has HTML parsing component implemented in C# using HTML Agility Pack. Given that there is an amazing FSharp.Data library with HTML provider, it looked promising to rewrite that code in F#.

My first disappointment was that Windows Store does not support F# natively. I couldn't believe that Microsoft allows building Windows Store apps using C++ and JavaScript, but at the same time F# code needs to go into portable class library (PCL). Regardless of all statements about growing F# share and predictions of its bright future this fact makes it look like a niche language in .NET family. It looks even more strange taking into account that Apple have chosen to go functional with Swift, but that is another story. Hope that Microsoft will start taking F# more seriously in the future.

Luckily there is a great blog post describing how to integrate F# into Windows Store application: http://ps-a.blogspot.com/2013/01/windows-store-apps-with-f-part-1-make.html. It helped to setup project and understand how all pieces work together. In the first approach I tried to take maximum advantage out of F# and attempted to reimplement ViewModel. It didn't work nicely. First problem was that ViewModel concept is all about state and .NET events. All that code looks clumsy in F#. Second problem was that I have quite a bit of helper code and ViewModel base class in C# project. It couldn't be referenced from F# unless that code is moved to another C# portable library. That option didn't appeal to me much, so I decided to drop this idea. It might work better in case all ViewModels and related logic in the project are implemented in F#. In fact http://reactiveui.net/ looks like a perfect match here. Unfortunately there is no information available describing how it works with F#. I plan to try it out the next time I build Windows Store app.

So, eventually I decided to keep ViewModel in C# and only port library responsible for HTML parsing and data processing to F#. First I had to figure out how to use FSharp.Data APIs from F# interactive. I installed nuget package, and started exploring.

Install-Package FSharp.Data

It turned out that nuget package contains 2 folders: net40 and portable-net40+sl5+wp8+win8. 
net40 is used for native .NET applications, and portable-net40+sl5+wp8+win8 for platforms which work with portable class library including Windows 8. Therefore we'll refer to
packages\FSharp.Data.2.1.0\lib\net40\FSharp.Data.dll in F# interactive and add reference to packages\FSharp.Data.2.1.0\lib\portable-net40+sl5+wp8+win8\FSharp.Data.dll in Windows Store project.

 1: #r @"C:\Projects\FsharpWindowsStorePrototype\packages\FSharp.Data.2.1.0\lib\net40\FSharp.Data.dll"
 2: 
 3: open System
 4: open FSharp.Data
 5: 
 6: type SampleHtmlProvider = HtmlProvider<"http://www.weather.com/weather/today/l/98033:4:US">
 7: let data = SampleHtmlProvider.Load("http://www.weather.com/weather/today/l/98033:4:US")
 8: let info = data.Tables.Table1.Rows.[0].Column1
 9: 
10: open System
11: open FSharp.Data
12: 
13: type SampleHtmlProvider = HtmlProvider<"http://www.weather.com/weather/today/l/98033:4:US">
14: let data = SampleHtmlProvider.Load("http://www.weather.com/weather/today/l/98033:4:US")
15: let info = data.Tables.Table1.Rows.[0].Column1
namespace System
namespace FSharp
namespace FSharp.Data
type SampleHtmlProvider =
  class
  end

Full name: Snippet.SampleHtmlProvider
val data : 'a
val info : 'a

Ok, it looks like provider works, but it doesn't do very good job in providing strongly typed wrapper around HTML document structure. It exposes basic APIs for iterating over document nodes and doing matching using predicate function. This is just enough in most of cases.

Next I added sample module file, referenced F# project from C# Windows Store application and tried to compile it. It didn't work like that:

2>C:\Program Files (x86)\MSBuild\Microsoft\VisualStudio\v12.0\AppxPackage\Microsoft.AppXPackage.Targets(852,9): error MSB3816: Loading assembly "C:\Projects\FsharpWindowsStorePrototype\FsharpPortableLibrary\bin\Debug\FSharp.Data.dll" failed. System.IO.FileNotFoundException: Could not load file or assembly 'FSharp.Core, Version=2.3.5.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' or one of its dependencies. The system cannot find the file specified.

2>C:\Program Files (x86)\MSBuild\Microsoft\VisualStudio\v12.0\AppxPackage\Microsoft.AppXPackage.Targets(852,9): error MSB3816: File name: 'FSharp.Core, Version=2.3.5.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'

After some googling I found out that FSharp.Data doesn't support PCL profile 7. It turned out that there are multiple PCL profiles to target different set of platforms. I spent few hours trying to change profile type to 47. Solution was unexpected, but straightforward:

1. Use legacy Project Library template for .NET Framework 4.0


2. In project settings change target F# runtime to 3.0 (FSharp.Core 2.3.5.0)


Finally I could build solution and do HTTP requests ... almost.

It turned out that web server that hosts site that I'm scraping requires UserAgent to be set on HTTP request. There is a known issue with UserAgent header in Windows Store apps. It is just not possible to set UserAgent header on HttpWebRequest. FSharp.Data provides nice Http helper utility, and I had a hope that it would work:

1: open System
2: open FSharp.Data
3: open FSharp.Data.HttpRequestHeaders
4: 
5: // Run the HTTP web request
6: Http.RequestString
7:   ( "https://www.google.com/search?q=super",
8:     query   = [ "q", "super" ],
9:     headers = [ Accept HttpContentTypes.Any; UserAgent "Mozilla/5.0" ])
namespace System
namespace FSharp
namespace FSharp.Data

And it does work ... but only in F# interactive which uses .NET version of the binary. I thought I'm doing something wrong until I found this code in FSharp.Data souces:

1: #if FX_NO_WEBREQUEST_USERAGENT
2:             | "user-agent" -> if not (req?UserAgent <- value) then try req.Headers.[HeaderEnum.UserAgent] <- value with _ -> ()
3: #else
4:             | "user-agent" -> req.UserAgent <- value
5: #endif

This explained everything. Therefore the only option left was to use Microsoft Client HTTP library.

Here is how my helper method looks like:

 1: open System
 2: open FSharp.Data
 3: open System.Net
 4: open System.IO
 5: open System.Net.Http
 6: open System.Diagnostics
 7: open System.Text.RegularExpressions
 8: 
 9: module DownloadHelpers =
10: 
11:     type DataOrError = 
12:     | Error of Error
13:     | Data of String
14: 
15:     let buildRequest(url:String, httpMethod:String) = 
16:         let request = new HttpRequestMessage(new HttpMethod(httpMethod), url)
17:         request.Headers.UserAgent.ParseAdd("(compatible; MSIE 10.6; Windows NT 6.1; Trident/5.0; InfoPath.2; SLCC1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 2.0.50727) 3gpp-gba UNTRUSTED/1.0");
18:         request
19: 
20:     let downloadPageByRequest(request:HttpRequestMessage) = async {
21:         let handler = new HttpClientHandler()
22:         handler.UseCookies <- false
23:         let client = new HttpClient(handler)
24:         try
25:             use! response = Async.AwaitTask <| client.SendAsync(request)
26:             let resultCode = response.StatusCode.ToString();
27:             let! html = Async.AwaitTask <| response.Content.ReadAsStringAsync();
28:             let processedHtml = Regex.Replace(html, @"\</\d+\>", "") // F# data bug
29:             return Data processedHtml 
30:         with ex -> 
31:                 Debug.WriteLine(ex.ToString())
32:                 return Error (Error.Exception ex)
33:     }
34: 
35:     let downloadPage(url:System.String) = async {
36:         return! downloadPageByRequest(buildRequest(url, HttpMethod.Get))
37:     }
namespace System
namespace FSharp
namespace System.Data
namespace System.Net
namespace System.IO
namespace System.Diagnostics
namespace System.Text
namespace System.Text.RegularExpressions
module DownloadHelpers

from Snippet
type DataOrError =
  | Error of obj
  | Data of String

Full name: Snippet.DownloadHelpers.DataOrError

  type: DataOrError
  implements: IEquatable<DataOrError>
  implements: Collections.IStructuralEquatable
union case DataOrError.Error: obj -> DataOrError
Multiple items
union case DataOrError.Data: String -> DataOrError

--------------------
namespace System.Data
type String =
  class
    new : char -> string
    new : char * int * int -> string
    new : System.SByte -> string
    new : System.SByte * int * int -> string
    new : System.SByte * int * int * System.Text.Encoding -> string
    new : char [] * int * int -> string
    new : char [] -> string
    new : char * int -> string
    member Chars : int -> char
    member Clone : unit -> obj
    member CompareTo : obj -> int
    member CompareTo : string -> int
    member Contains : string -> bool
    member CopyTo : int * char [] * int * int -> unit
    member EndsWith : string -> bool
    member EndsWith : string * System.StringComparison -> bool
    member EndsWith : string * bool * System.Globalization.CultureInfo -> bool
    member Equals : obj -> bool
    member Equals : string -> bool
    member Equals : string * System.StringComparison -> bool
    member GetEnumerator : unit -> System.CharEnumerator
    member GetHashCode : unit -> int
    member GetTypeCode : unit -> System.TypeCode
    member IndexOf : char -> int
    member IndexOf : string -> int
    member IndexOf : char * int -> int
    member IndexOf : string * int -> int
    member IndexOf : string * System.StringComparison -> int
    member IndexOf : char * int * int -> int
    member IndexOf : string * int * int -> int
    member IndexOf : string * int * System.StringComparison -> int
    member IndexOf : string * int * int * System.StringComparison -> int
    member IndexOfAny : char [] -> int
    member IndexOfAny : char [] * int -> int
    member IndexOfAny : char [] * int * int -> int
    member Insert : int * string -> string
    member IsNormalized : unit -> bool
    member IsNormalized : System.Text.NormalizationForm -> bool
    member LastIndexOf : char -> int
    member LastIndexOf : string -> int
    member LastIndexOf : char * int -> int
    member LastIndexOf : string * int -> int
    member LastIndexOf : string * System.StringComparison -> int
    member LastIndexOf : char * int * int -> int
    member LastIndexOf : string * int * int -> int
    member LastIndexOf : string * int * System.StringComparison -> int
    member LastIndexOf : string * int * int * System.StringComparison -> int
    member LastIndexOfAny : char [] -> int
    member LastIndexOfAny : char [] * int -> int
    member LastIndexOfAny : char [] * int * int -> int
    member Length : int
    member Normalize : unit -> string
    member Normalize : System.Text.NormalizationForm -> string
    member PadLeft : int -> string
    member PadLeft : int * char -> string
    member PadRight : int -> string
    member PadRight : int * char -> string
    member Remove : int -> string
    member Remove : int * int -> string
    member Replace : char * char -> string
    member Replace : string * string -> string
    member Split : char [] -> string []
    member Split : char [] * int -> string []
    member Split : char [] * System.StringSplitOptions -> string []
    member Split : string [] * System.StringSplitOptions -> string []
    member Split : char [] * int * System.StringSplitOptions -> string []
    member Split : string [] * int * System.StringSplitOptions -> string []
    member StartsWith : string -> bool
    member StartsWith : string * System.StringComparison -> bool
    member StartsWith : string * bool * System.Globalization.CultureInfo -> bool
    member Substring : int -> string
    member Substring : int * int -> string
    member ToCharArray : unit -> char []
    member ToCharArray : int * int -> char []
    member ToLower : unit -> string
    member ToLower : System.Globalization.CultureInfo -> string
    member ToLowerInvariant : unit -> string
    member ToString : unit -> string
    member ToString : System.IFormatProvider -> string
    member ToUpper : unit -> string
    member ToUpper : System.Globalization.CultureInfo -> string
    member ToUpperInvariant : unit -> string
    member Trim : unit -> string
    member Trim : char [] -> string
    member TrimEnd : char [] -> string
    member TrimStart : char [] -> string
    static val Empty : string
    static member Compare : string * string -> int
    static member Compare : string * string * bool -> int
    static member Compare : string * string * System.StringComparison -> int
    static member Compare : string * string * System.Globalization.CultureInfo * System.Globalization.CompareOptions -> int
    static member Compare : string * string * bool * System.Globalization.CultureInfo -> int
    static member Compare : string * int * string * int * int -> int
    static member Compare : string * int * string * int * int * bool -> int
    static member Compare : string * int * string * int * int * System.StringComparison -> int
    static member Compare : string * int * string * int * int * bool * System.Globalization.CultureInfo -> int
    static member Compare : string * int * string * int * int * System.Globalization.CultureInfo * System.Globalization.CompareOptions -> int
    static member CompareOrdinal : string * string -> int
    static member CompareOrdinal : string * int * string * int * int -> int
    static member Concat : obj -> string
    static member Concat : obj [] -> string
    static member Concat<'T> : System.Collections.Generic.IEnumerable<'T> -> string
    static member Concat : System.Collections.Generic.IEnumerable<string> -> string
    static member Concat : string [] -> string
    static member Concat : obj * obj -> string
    static member Concat : string * string -> string
    static member Concat : obj * obj * obj -> string
    static member Concat : string * string * string -> string
    static member Concat : obj * obj * obj * obj -> string
    static member Concat : string * string * string * string -> string
    static member Copy : string -> string
    static member Equals : string * string -> bool
    static member Equals : string * string * System.StringComparison -> bool
    static member Format : string * obj -> string
    static member Format : string * obj [] -> string
    static member Format : string * obj * obj -> string
    static member Format : System.IFormatProvider * string * obj [] -> string
    static member Format : string * obj * obj * obj -> string
    static member Intern : string -> string
    static member IsInterned : string -> string
    static member IsNullOrEmpty : string -> bool
    static member IsNullOrWhiteSpace : string -> bool
    static member Join : string * string [] -> string
    static member Join : string * obj [] -> string
    static member Join<'T> : string * System.Collections.Generic.IEnumerable<'T> -> string
    static member Join : string * System.Collections.Generic.IEnumerable<string> -> string
    static member Join : string * string [] * int * int -> string
  end

Full name: System.String

  type: String
  implements: IComparable
  implements: ICloneable
  implements: IConvertible
  implements: IComparable<string>
  implements: seq<char>
  implements: Collections.IEnumerable
  implements: IEquatable<string>
val buildRequest : String * String -> 'a

Full name: Snippet.DownloadHelpers.buildRequest
val url : String

  type: String
  implements: IComparable
  implements: ICloneable
  implements: IConvertible
  implements: IComparable<string>
  implements: seq<char>
  implements: Collections.IEnumerable
  implements: IEquatable<string>
val httpMethod : String

  type: String
  implements: IComparable
  implements: ICloneable
  implements: IConvertible
  implements: IComparable<string>
  implements: seq<char>
  implements: Collections.IEnumerable
  implements: IEquatable<string>
val request : 'a
val downloadPageByRequest : 'a -> Async<DataOrError>

Full name: Snippet.DownloadHelpers.downloadPageByRequest
val async : AsyncBuilder

Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.async
val handler : 'a
val client : 'a
val response : 'a (requires 'a :> IDisposable)

  type: 'a
  implements: IDisposable
Multiple items
type Async<'T>

Full name: Microsoft.FSharp.Control.Async<_>

--------------------
type Async
with
  static member AsBeginEnd : computation:('Arg -> Async<'T>) -> ('Arg * AsyncCallback * obj -> IAsyncResult) * (IAsyncResult -> 'T) * (IAsyncResult -> unit)
  static member AwaitEvent : event:IEvent<'Del,'T> * ?cancelAction:(unit -> unit) -> Async<'T> (requires delegate and 'Del :> Delegate)
  static member AwaitIAsyncResult : iar:IAsyncResult * ?millisecondsTimeout:int -> Async<bool>
  static member AwaitTask : task:Threading.Tasks.Task<'T> -> Async<'T>
  static member AwaitWaitHandle : waitHandle:Threading.WaitHandle * ?millisecondsTimeout:int -> Async<bool>
  static member CancelDefaultToken : unit -> unit
  static member Catch : computation:Async<'T> -> Async<Choice<'T,exn>>
  static member FromBeginEnd : beginAction:(AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
  static member FromBeginEnd : arg:'Arg1 * beginAction:('Arg1 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
  static member FromBeginEnd : arg1:'Arg1 * arg2:'Arg2 * beginAction:('Arg1 * 'Arg2 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
  static member FromBeginEnd : arg1:'Arg1 * arg2:'Arg2 * arg3:'Arg3 * beginAction:('Arg1 * 'Arg2 * 'Arg3 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
  static member FromContinuations : callback:(('T -> unit) * (exn -> unit) * (OperationCanceledException -> unit) -> unit) -> Async<'T>
  static member Ignore : computation:Async<'T> -> Async<unit>
  static member OnCancel : interruption:(unit -> unit) -> Async<IDisposable>
  static member Parallel : computations:seq<Async<'T>> -> Async<'T []>
  static member RunSynchronously : computation:Async<'T> * ?timeout:int * ?cancellationToken:Threading.CancellationToken -> 'T
  static member Sleep : millisecondsDueTime:int -> Async<unit>
  static member Start : computation:Async<unit> * ?cancellationToken:Threading.CancellationToken -> unit
  static member StartAsTask : computation:Async<'T> * ?taskCreationOptions:Threading.Tasks.TaskCreationOptions * ?cancellationToken:Threading.CancellationToken -> Threading.Tasks.Task<'T>
  static member StartChild : computation:Async<'T> * ?millisecondsTimeout:int -> Async<Async<'T>>
  static member StartChildAsTask : computation:Async<'T> * ?taskCreationOptions:Threading.Tasks.TaskCreationOptions -> Async<Threading.Tasks.Task<'T>>
  static member StartImmediate : computation:Async<unit> * ?cancellationToken:Threading.CancellationToken -> unit
  static member StartWithContinuations : computation:Async<'T> * continuation:('T -> unit) * exceptionContinuation:(exn -> unit) * cancellationContinuation:(OperationCanceledException -> unit) * ?cancellationToken:Threading.CancellationToken -> unit
  static member SwitchToContext : syncContext:Threading.SynchronizationContext -> Async<unit>
  static member SwitchToNewThread : unit -> Async<unit>
  static member SwitchToThreadPool : unit -> Async<unit>
  static member TryCancelled : computation:Async<'T> * compensation:(OperationCanceledException -> unit) -> Async<'T>
  static member CancellationToken : Async<Threading.CancellationToken>
  static member DefaultCancellationToken : Threading.CancellationToken
end

Full name: Microsoft.FSharp.Control.Async
static member Async.AwaitTask : task:Threading.Tasks.Task<'T> -> Async<'T>
val resultCode : 'a
val html : string

  type: string
  implements: IComparable
  implements: ICloneable
  implements: IConvertible
  implements: IComparable<string>
  implements: seq<char>
  implements: Collections.IEnumerable
  implements: IEquatable<string>
val processedHtml : string

  type: string
  implements: IComparable
  implements: ICloneable
  implements: IConvertible
  implements: IComparable<string>
  implements: seq<char>
  implements: Collections.IEnumerable
  implements: IEquatable<string>
type Regex =
  class
    new : string -> System.Text.RegularExpressions.Regex
    new : string * System.Text.RegularExpressions.RegexOptions -> System.Text.RegularExpressions.Regex
    new : string * System.Text.RegularExpressions.RegexOptions * System.TimeSpan -> System.Text.RegularExpressions.Regex
    member GetGroupNames : unit -> string []
    member GetGroupNumbers : unit -> int []
    member GroupNameFromNumber : int -> string
    member GroupNumberFromName : string -> int
    member IsMatch : string -> bool
    member IsMatch : string * int -> bool
    member Match : string -> System.Text.RegularExpressions.Match
    member Match : string * int -> System.Text.RegularExpressions.Match
    member Match : string * int * int -> System.Text.RegularExpressions.Match
    member MatchTimeout : System.TimeSpan
    member Matches : string -> System.Text.RegularExpressions.MatchCollection
    member Matches : string * int -> System.Text.RegularExpressions.MatchCollection
    member Options : System.Text.RegularExpressions.RegexOptions
    member Replace : string * string -> string
    member Replace : string * System.Text.RegularExpressions.MatchEvaluator -> string
    member Replace : string * string * int -> string
    member Replace : string * System.Text.RegularExpressions.MatchEvaluator * int -> string
    member Replace : string * string * int * int -> string
    member Replace : string * System.Text.RegularExpressions.MatchEvaluator * int * int -> string
    member RightToLeft : bool
    member Split : string -> string []
    member Split : string * int -> string []
    member Split : string * int * int -> string []
    member ToString : unit -> string
    static val InfiniteMatchTimeout : System.TimeSpan
    static member CacheSize : int with get, set
    static member CompileToAssembly : System.Text.RegularExpressions.RegexCompilationInfo [] * System.Reflection.AssemblyName -> unit
    static member CompileToAssembly : System.Text.RegularExpressions.RegexCompilationInfo [] * System.Reflection.AssemblyName * System.Reflection.Emit.CustomAttributeBuilder [] -> unit
    static member CompileToAssembly : System.Text.RegularExpressions.RegexCompilationInfo [] * System.Reflection.AssemblyName * System.Reflection.Emit.CustomAttributeBuilder [] * string -> unit
    static member Escape : string -> string
    static member IsMatch : string * string -> bool
    static member IsMatch : string * string * System.Text.RegularExpressions.RegexOptions -> bool
    static member IsMatch : string * string * System.Text.RegularExpressions.RegexOptions * System.TimeSpan -> bool
    static member Match : string * string -> System.Text.RegularExpressions.Match
    static member Match : string * string * System.Text.RegularExpressions.RegexOptions -> System.Text.RegularExpressions.Match
    static member Match : string * string * System.Text.RegularExpressions.RegexOptions * System.TimeSpan -> System.Text.RegularExpressions.Match
    static member Matches : string * string -> System.Text.RegularExpressions.MatchCollection
    static member Matches : string * string * System.Text.RegularExpressions.RegexOptions -> System.Text.RegularExpressions.MatchCollection
    static member Matches : string * string * System.Text.RegularExpressions.RegexOptions * System.TimeSpan -> System.Text.RegularExpressions.MatchCollection
    static member Replace : string * string * string -> string
    static member Replace : string * string * System.Text.RegularExpressions.MatchEvaluator -> string
    static member Replace : string * string * string * System.Text.RegularExpressions.RegexOptions -> string
    static member Replace : string * string * System.Text.RegularExpressions.MatchEvaluator * System.Text.RegularExpressions.RegexOptions -> string
    static member Replace : string * string * string * System.Text.RegularExpressions.RegexOptions * System.TimeSpan -> string
    static member Replace : string * string * System.Text.RegularExpressions.MatchEvaluator * System.Text.RegularExpressions.RegexOptions * System.TimeSpan -> string
    static member Split : string * string -> string []
    static member Split : string * string * System.Text.RegularExpressions.RegexOptions -> string []
    static member Split : string * string * System.Text.RegularExpressions.RegexOptions * System.TimeSpan -> string []
    static member Unescape : string -> string
  end

Full name: System.Text.RegularExpressions.Regex

  type: Regex
  implements: Runtime.Serialization.ISerializable
Regex.Replace(input: string, pattern: string, evaluator: MatchEvaluator) : string
Regex.Replace(input: string, pattern: string, replacement: string) : string
Regex.Replace(input: string, pattern: string, evaluator: MatchEvaluator, options: RegexOptions) : string
Regex.Replace(input: string, pattern: string, replacement: string, options: RegexOptions) : string
Regex.Replace(input: string, pattern: string, evaluator: MatchEvaluator, options: RegexOptions, matchTimeout: TimeSpan) : string
Regex.Replace(input: string, pattern: string, replacement: string, options: RegexOptions, matchTimeout: TimeSpan) : string
val ex : exn

  type: exn
  implements: Runtime.Serialization.ISerializable
  implements: Runtime.InteropServices._Exception
type Debug =
  class
    static member Assert : bool -> unit
    static member Assert : bool * string -> unit
    static member Assert : bool * string * string -> unit
    static member Assert : bool * string * string * obj [] -> unit
    static member AutoFlush : bool with get, set
    static member Close : unit -> unit
    static member Fail : string -> unit
    static member Fail : string * string -> unit
    static member Flush : unit -> unit
    static member Indent : unit -> unit
    static member IndentLevel : int with get, set
    static member IndentSize : int with get, set
    static member Listeners : System.Diagnostics.TraceListenerCollection
    static member Print : string -> unit
    static member Print : string * obj [] -> unit
    static member Unindent : unit -> unit
    static member Write : string -> unit
    static member Write : obj -> unit
    static member Write : string * string -> unit
    static member Write : obj * string -> unit
    static member WriteIf : bool * string -> unit
    static member WriteIf : bool * obj -> unit
    static member WriteIf : bool * string * string -> unit
    static member WriteIf : bool * obj * string -> unit
    static member WriteLine : string -> unit
    static member WriteLine : obj -> unit
    static member WriteLine : string * string -> unit
    static member WriteLine : obj * string -> unit
    static member WriteLine : string * obj [] -> unit
    static member WriteLineIf : bool * string -> unit
    static member WriteLineIf : bool * obj -> unit
    static member WriteLineIf : bool * string * string -> unit
    static member WriteLineIf : bool * obj * string -> unit
  end

Full name: System.Diagnostics.Debug
Debug.WriteLine(value: obj) : unit
Debug.WriteLine(message: string) : unit
Debug.WriteLine(format: string, args: obj []) : unit
Debug.WriteLine(value: obj, category: string) : unit
Debug.WriteLine(message: string, category: string) : unit
Object.ToString() : string
type Exception =
  class
    new : unit -> System.Exception
    new : string -> System.Exception
    new : string * System.Exception -> System.Exception
    member Data : System.Collections.IDictionary
    member GetBaseException : unit -> System.Exception
    member GetObjectData : System.Runtime.Serialization.SerializationInfo * System.Runtime.Serialization.StreamingContext -> unit
    member GetType : unit -> System.Type
    member HResult : int with get, set
    member HelpLink : string with get, set
    member InnerException : System.Exception
    member Message : string
    member Source : string with get, set
    member StackTrace : string
    member TargetSite : System.Reflection.MethodBase
    member ToString : unit -> string
  end

Full name: System.Exception

  type: Exception
  implements: Runtime.Serialization.ISerializable
  implements: Runtime.InteropServices._Exception
val downloadPage : String -> Async<DataOrError>

Full name: Snippet.DownloadHelpers.downloadPage

Note that there is an bug in FSharp.Data 2.1.0, so that any closing tag with number like </1> breaks the parser.

Finally everything worked, but can't say that it was the end of the story.
When I tried to pull project from Git to another computer it refused to build. For some reason F# project was broken. Nuget pulled all dependencies, but for build didn't work.
Googling and experimenting proved that there is an issue with Microsoft.Bcl dependency which is required by Microsoft.Net.Http. By default Nuget tries to pull older version of this library, while Microsoft.Net.Http works only with the most recent one. The solution which worked for me was to clean Nuget cache, remove Microsoft.Bcl and Microsoft.Bcl.Build projects from packages folder, and request Nuget to install those projects manually before installing Microsoft.Net.Http.

Install-Package Microsoft.Bcl

It helped to solve dependency issues.

When I did that recently, I also updated FSharp.Data to version 2.2.0. This update broke my application, and now I have to investigate what changed.

Was it worth messing up with F#? Definitely I've spent way more time trying to fix all issues on the way than I'd spend writing same logic in C#. But it was fun and I'm glad that finally everything worked out.