// thinkbeforecoding

Full F# Blog - Part 3

2019-12-13T17:50:54 / jeremie chassaing

I promised a third part in the this FSharp.Blog series, and take the occasion of this new edition of the F# Advent Calendar to write it !

This time we'll see how to install autorenewable letsencrypt certificates on azure functions.

You probably already know letsencrypt, they provide free SSL certificates for everybody. Having a public website today without HTTPS is totally discouraged for security and privacy reasons, and most major browser issue warnings even if no form is posted. Using HTTPS on a static readonly web site is highly advise to prevent Man in the middle HTTP or script injection while on untrusted wifi.

The only requirement for letscencrypt is to prove that you own the website for which you request a certificate. For this, letsencrypt uses a challenge, a single use file that you have to serve from your web site and that will assert you have administrative rights on it.

Overview

For this, we will use the Let's encrypt extension and automate the certificate renewal with a timer trigger. On one side we'll have an azure function app with the extension and the trigger, and on the other side, your blog where you want to install the certificate.

Let's first create a function app. For this we need to install the templates first:

dotnet new -i Microsoft.Azure.WebJobs.ProjectTemplates::3.0.10379

The create a directory, and create the app inside

mkdir letsencryptadvent
cd letsencryptadvent
dotnet new func -lang F#

The only other dependency we need is the TaskBuild.fs nuget to interop with task more easily:

dotnet add package TaskBuilder.fs 
dotnet restore

Create a timer trigger function with the following command:

dotnet new TimerTrigger -lang F# -n Renew

Now open the project in your favorite editor. Set the host.json file as "Copy if newer" using the IDE, or add the following lines to the fsproj:

<ItemGroup>
  <None Update="host.json">
    <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
  </None>
  <None Update="local.settings.json">
    <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    <CopyToPublishDirectory>Never</CopyToPublishDirectory>
  </None>
</ItemGroup>

Create a LetsEncrypt.fs file and add it as well as the Renew.fs to the project:

<ItemGroup>
  <Compile Include="LetsEncrypt.fs" />
  <Compile Include="Renew.fs" />
</ItemGroup>

Start the file with:

module LetsEncrypt

and type inside:

module LetsEncrypt =
    open System
    open System.Text
    open System.Net.Http
    open Newtonsoft.Json
    open FSharp.Control.Tasks.V2.ContextInsensitive
    open Microsoft.Azure.WebJobs.Host
    open Microsoft.Extensions.Logging


    type Request = {
        AzureEnvironment: AzureEnvironment
        AcmeConfig: AcmeConfig
        CertificateSettings: CertificateSettings
        AuthorizationChallengeProviderConfig: AuthorizationChallengeProviderConfig
    }
    and AzureEnvironment = {
        WebAppName: string
        ClientId: string
        ClientSecret: string
        ResourceGroupName: string
        SubscriptionId: string
        Tenant: string
    }
    and AcmeConfig = {
        RegistrationEmail: string
        Host: string
        AlternateNames: string[]
        RSAKeyLength: int
        PFXPassword: string
        UseProduction: bool
    }
    and CertificateSettings = {
        UseIPBasedSSL: bool 
    }
    and AuthorizationChallengeProviderConfig = {
        DisableWebConfigUpdate: bool
    }

    type ServiceResponse = {
        CertificateInfo: CertificateInfo
    }
    and CertificateInfo = {
        PfxCertificate: string
        Password: string
    }

    type [<Struct>] WebApp = WebApp of string
    type [<Struct>] ResourceGroup = ResourceGroup of string
    type [<Struct>] Host = Host of string

    let env name = Environment.GetEnvironmentVariable(name)
    let basic username password = 
        sprintf "%s:%s" username password
        |> Encoding.UTF8.GetBytes
        |> Convert.ToBase64String

    let RenewCert (log: ILogger) (ResourceGroup resourceGroup) (WebApp webApp) (Host host) =
        task {
            try
                let tenant = env "Tenant"
                let subscriptionId = env "Subscription.Id"

                let publishUrl = env "Publish.Url"
                let username = env "Publish.UserName"
                let password = env "Publish.Password"

                let clientId = env "Client.Id"
                let clientSecret = env "Client.Secret"

                let registrationEmail = env "Registration.Email"
                let certificatePassword = env "Cert.Password"
                use client = new HttpClient();
                client.DefaultRequestHeaders.TryAddWithoutValidation(
                        "Authorization", 
                        "Basic " + basic username password )
                |> ignore
            
                
                let body = {
                    AzureEnvironment = 
                        {   //AzureWebSitesDefaultDomainName = "string", 
                            //    Defaults to azurewebsites.net
                            //ServicePlanResourceGroupName = "string",
                            //    Defaults to ResourceGroupName
                            //SiteSlotName = "string",
                            //    Not required if site slots isn't used
                            WebAppName = webApp
                            //AuthenticationEndpoint = "string",
                            //    Defaults to https://login.windows.net/
                            ClientId = clientId
                            ClientSecret = clientSecret
                            //ManagementEndpoint = "string", 
                            //    Defaults to https://management.azure.com
                            //Resource group of the web app 
                            ResourceGroupName = resourceGroup 
                            SubscriptionId = subscriptionId
                            Tenant = tenant //Azure AD tenant ID 
                            //TokenAudience = "string"
                            //    Defaults to https://management.core.windows.net/
                        }
                    AcmeConfig = 
                        {   RegistrationEmail = registrationEmail
                            Host = host
                            AlternateNames =  [||]
                            RSAKeyLength = 2048
                            //Replace with your own
                            PFXPassword = certificatePassword  
                            UseProduction = true 
                            // Replace with true if you want
                            // production certificate from Lets Encrypt 
                        }
                    CertificateSettings = 
                        {   UseIPBasedSSL = false }
                    AuthorizationChallengeProviderConfig = 
                        {   DisableWebConfigUpdate = false }
                }

                log.LogInformation("Post request")
                let uri = sprintf "https://%s/letsencrypt/api/certificates/challengeprovider/http/kudu/certificateinstall/azurewebapp?api-version=2017-09-01" publishUrl
                let! res = client.PostAsync(
                                uri, 
                                new StringContent(JsonConvert.SerializeObject(body), 
                                                  Encoding.UTF8, "application/json")) 

                let! value = res.Content.ReadAsStringAsync()
                let response  = value
                return Ok response

            with
            | ex ->
                log.LogError(sprintf "Error while renewing cert: %O" ex, ex)
                return Error ex
        }

It's a bit long but actually really simple. It crafts a json http request on the extension and get the result.

The three parameters resourceGroup, webApp and host are the one of your blog azure function app. The host is the host name you need a certificate for. In the case of this blog, this is simply thinkbeforecoding.com.

The function also use a lot of environment variables.

Tenant and Subscription.Id are your azure account information. You can find them when you switch account/subscription. The Tenant ends with .onmicrosoft.com and the subscription id is a guid.

The Publish.Url, Publish.UserName and Publish.Password are Kudu publish profile credentials of your timer trigger functions used to call the extension.

The Client.Id and Client.Secret are the appId and secret of an Active Directory account that has Contributor access to your blog functions resource group. We'll see how to configure this later.

The Registration.Email is the email sent to Let's Encrypt. They use it only to notify you when the certificate is about to expire.

Finally Cert.Password is a password used to protect generated pfx. You may have to use it if you wan't to reuse an existing certificate.

When called with the Publish info, the extension checks that it can access your blog function app using provided Client account. Then it calls Let's encrypt to request a certificate. Let's encrypt responds with a challenge. The extension then takes the challenge and creates a file in a .well-known directory in your blog function. Let's encrypt calls your blog to check the presence of the challenge, and in case of success, creates the certificate. The extension finally installs this certificate in your blog SSL configuration.

Let's call this RenewCert function from the trigger. In the Renew.fs file, type:

open System
open Microsoft.Azure.WebJobs
open Microsoft.Extensions.Logging
open System.Threading.Tasks
open FSharp.Control.Tasks.V2.ContextSensitive

open LetsEncrypt

module Renew =
    [<FunctionName("Renew")>]
    let Run([<TimerTrigger("0 3 2 24 * *") >] myTimer: TimerInfo,  log : ILogger) =
        task {
            sprintf "Timer function to renew myblog.com executed at: %O" DateTime.Now
            |> log.LogInformation

            let! result = 
                LetsEncrypt.RenewCert 
                            log 
                            (ResourceGroup "blog-resourcegroup") 
                            (WebApp "blog-func") 
                            (Host "myblog.com")

            log.LogInformation "Cert for myblog.com renewed"

            match result with
            | Ok success -> log.LogInformation(string success)
            | Error err -> log.LogError(string err)
        } :> Task

The cron expresion is meant to run on each 24th of the month a 02:03. It's a good idea to not run the trigger at midnight to avoid renewing certificates at the same time as everybody else...

Deployment

Our Renew function is ready, use the az cli to deploy it.

First login and select the subscription

az login
az account  set --subscription yoursubscription

Create a resource group and a storage account. You can of course use a different region

az group create --name letsencryptadvent --location NorthEurope
az storage account create --name letsencryptadvent -g letsencryptadvent  --sku Standard_LRS

We can now proceed to the functions creation:

az functionapp create --name letsencryptadvent -g letsencryptadvent --storage-account letsencryptadvent --consumption-plan-location NorthEurope --os-type Windows --runtime dotnet

Next step is to deploy our code:

dotnet publish -c release -o deploy
ls ./deploy/* | Compress-Archive  -DestinationPath deploy.zip
az functionapp deployment source config-zip --src deploy.zip --name letsencryptadvent -g letsencryptadvent

Extension installation

On the portal, in the letsencryptadvent functions, go to the extensions tab, and click Add. Find and select the "Let's Encrypt (No web Jobs)" extensions. Accept the legal terms and OK.

Access Rights

We need to create a client user and give it appropriate access rights on the blog:

az ad app create --display-name letsencryptadvent --password '$ecureP@ssw0rd'

I've not found yet how to fully configure this account using the cli... So login to azure portal and go to the Active Directory settings. Click on the App registrations tab and select the letsencryptadvent application.

In the overview tab, the Managed application in local directory is not set, just click on the link. It will create the user. Don't forget to note the account App Id for later use.

Now go to your blog resource group, and open the Access Control (IAM) tab. Once there, add a role assignment. Select the Contributor role, and select the letencryptadvent account, and save. Alternatively, you can use the following command:

az role assignment create --role Contributor --assignee 'the account appid' -g yourblog-resourcegroup

The account we created now has access to your blog to upload the challenge and install the certificate.

We can set the environment variables for the client:

az functionapp config appsettings set --settings 'Client.Id=theappid' \
    --name letsencryptadvent -g letsencryptadvent
az functionapp config appsettings set --settings 'Client.Secret=$ecureP@ssw0rd' \
    --name letsencryptadvent -g letsencryptadvent

Publish information

To get the publish profile information, use the azure cli:

az webapp deployment list-publishing-profiles -n letsencryptadvent -g letsencryptadvent

get the publish url, the username and password, and set the environment variables:

az functionapp config appsettings set --settings 'Publish.Url=letsencryptadvent.scm.azurewebsites.net' \
    --name letsencryptadvent -g letsencryptadvent
az functionapp config appsettings set --settings 'Publish.UserName=$letsencryptadvent' \
    --name letsencryptadvent -g letsencryptadvent
az functionapp config appsettings set --settings 'Publish.Password=...' \
    --name letsencryptadvent -g letsencryptadvent

Don't forget to set the Subscription.Id and Tenant, as well as the Registration.Email and Cert.Password with your own values.

Responding to the challenge

Let's encrypt will call your blog app to check the challenge. The extension puts the challenge in a file on the local disk, and we have to serve it.

For this we'll create a function !

In your blog functions code, add a 'challenge' http trigger:

open System
open System.IO
open Microsoft.AspNetCore.Mvc
open Microsoft.Azure.WebJobs
open Microsoft.Azure.WebJobs.Extensions.Http
open Microsoft.AspNetCore.Http
open Newtonsoft.Json
open Microsoft.Extensions.Logging
open FSharp.Control.Tasks.V2.ContextSensitive
open System.Net.Http
open System.Net

module challenge =
    [<FunctionName("challenge")>]
    let Run([<HttpTrigger(AuthorizationLevel.Anonymous, [| "get" |],Route="acme-challenge/{code}")>]req: HttpRequestMessage, code: string, log: ILogger) =
        task {
            try
                log.LogInformation (sprintf "chalenge for: %s" code)
                let content = File.ReadAllText(@"D:\home\site\wwwroot\.well-known\acme-challenge\"+code);
                log.LogInformation("Challenge found")
                return new HttpResponseMessage(
                        Content = new StringContent(content, Text.Encoding.UTF8, "text/plain") )
            with
            | ex ->
                log.LogError(ex.Message)
                return raise (AggregateException ex)
        } 

It just loads the challenge file from local disk and returns its content.

We also have to add a proxy rule to serve it on the expected path. For this create a proxies.json file or edit existing one:

{
  "$schema": "http://json.schemastore.org/proxies",
  "proxies": {
    "acmechallenge": {
      "matchCondition": {
        "route": "/.well-known/acme-challenge/{*code}"
      },
      "backendUri": "https://localhost/api/acme-challenge/{code}"
    }
  }
}

Place it as the first rule to be sure the path is not overloaded by a less specific one.

You can now republish you blog with this function.

Test

To generate your first certificate, go to the letsencryptadvent function, and trigger the timer manually.

You'll see the output in the log console. You can also follow the activity with the following command:

func azure functionapp logstream letsencryptadvent

Once exectued, you should be able to go to your blog with HTTPS. You can check that the certificate is correctly installed in the SSL tab.

Errors are logged, so you should be able to troubleshoot problems as they occure.

Improvements

Storing the passwords as plain text in the configuration is highly discouraged.

Hopefully, you can store them in a keyvault and use a key vault reference as a parameter value:

@Microsoft.KeyVault(SecretUri=https://your-keyvault.vault.azure.net/secrets/secret/guid)

Don't forget to give read access to your function app to the key vault.

When the function runtime finds parameters like this, it loads them from keyvault, decrypts them and pass them as environment variable to the function.

Now, enjoy your auto renewed certificates every month !!

Happy xmas

namespace System
namespace System.Text
namespace System.Net
namespace System.Net.Http
namespace Newtonsoft
namespace Newtonsoft.Json
Multiple items
namespace FSharp

--------------------
namespace Microsoft.FSharp
Multiple items
namespace FSharp.Control

--------------------
namespace Microsoft.FSharp.Control
namespace FSharp.Control.Tasks
module V2 from FSharp.Control.Tasks
module ContextInsensitive from FSharp.Control.Tasks.V2
namespace Microsoft
namespace Microsoft.Azure
namespace Microsoft.Azure.WebJobs
namespace Microsoft.Azure.WebJobs.Host
namespace Microsoft.Extensions
namespace Microsoft.Extensions.Logging
type Request = { AzureEnvironment: AzureEnvironment AcmeConfig: AcmeConfig CertificateSettings: CertificateSettings AuthorizationChallengeProviderConfig: AuthorizationChallengeProviderConfig }
Multiple items
Request.AzureEnvironment: AzureEnvironment

--------------------
type AzureEnvironment = { WebAppName: string ClientId: string ClientSecret: string ResourceGroupName: string SubscriptionId: string Tenant: string }
type AzureEnvironment = { WebAppName: string ClientId: string ClientSecret: string ResourceGroupName: string SubscriptionId: string Tenant: string }
Multiple items
Request.AcmeConfig: AcmeConfig

--------------------
type AcmeConfig = { RegistrationEmail: string Host: string AlternateNames: string[] RSAKeyLength: int PFXPassword: string UseProduction: bool }
type AcmeConfig = { RegistrationEmail: string Host: string AlternateNames: string[] RSAKeyLength: int PFXPassword: string UseProduction: bool }
Multiple items
Request.CertificateSettings: CertificateSettings

--------------------
type CertificateSettings = { UseIPBasedSSL: bool }
type CertificateSettings = { UseIPBasedSSL: bool }
Multiple items
Request.AuthorizationChallengeProviderConfig: AuthorizationChallengeProviderConfig

--------------------
type AuthorizationChallengeProviderConfig = { DisableWebConfigUpdate: bool }
type AuthorizationChallengeProviderConfig = { DisableWebConfigUpdate: bool }
AzureEnvironment.WebAppName: string
Multiple items
val string: value: 'T -> string
<summary>Converts the argument to a string using <c>ToString</c>.</summary>
<remarks>For standard integer and floating point values the and any type that implements <c>IFormattable</c><c>ToString</c> conversion uses <c>CultureInfo.InvariantCulture</c>. </remarks>
<param name="value">The input value.</param>
<returns>The converted string.</returns>


--------------------
type string = String
<summary>An abbreviation for the CLI type <see cref="T:System.String" />.</summary>
<category>Basic Types</category>
AzureEnvironment.ClientId: string
AzureEnvironment.ClientSecret: string
AzureEnvironment.ResourceGroupName: string
AzureEnvironment.SubscriptionId: string
AzureEnvironment.Tenant: string
AcmeConfig.RegistrationEmail: string
AcmeConfig.Host: string
AcmeConfig.AlternateNames: string[]
AcmeConfig.RSAKeyLength: int
Multiple items
val int: value: 'T -> int (requires member op_Explicit)
<summary>Converts the argument to signed 32-bit integer. This is a direct conversion for all primitive numeric types. For strings, the input is converted using <c>Int32.Parse()</c> with InvariantCulture settings. Otherwise the operation requires an appropriate static conversion method on the input type.</summary>
<param name="value">The input value.</param>
<returns>The converted int</returns>


--------------------
[<Struct>] type int = int32
<summary>An abbreviation for the CLI type <see cref="T:System.Int32" />.</summary>
<category>Basic Types</category>


--------------------
type int<'Measure> = int
<summary>The type of 32-bit signed integer numbers, annotated with a unit of measure. The unit of measure is erased in compiled code and when values of this type are analyzed using reflection. The type is representationally equivalent to <see cref="T:System.Int32" />.</summary>
<category>Basic Types with Units of Measure</category>
AcmeConfig.PFXPassword: string
AcmeConfig.UseProduction: bool
[<Struct>] type bool = Boolean
<summary>An abbreviation for the CLI type <see cref="T:System.Boolean" />.</summary>
<category>Basic Types</category>
CertificateSettings.UseIPBasedSSL: bool
AuthorizationChallengeProviderConfig.DisableWebConfigUpdate: bool
type ServiceResponse = { CertificateInfo: CertificateInfo }
Multiple items
ServiceResponse.CertificateInfo: CertificateInfo

--------------------
type CertificateInfo = { PfxCertificate: string Password: string }
type CertificateInfo = { PfxCertificate: string Password: string }
CertificateInfo.PfxCertificate: string
CertificateInfo.Password: string
Multiple items
type StructAttribute = inherit Attribute new: unit -> StructAttribute
<summary>Adding this attribute to a type causes it to be represented using a CLI struct.</summary>
<category>Attributes</category>


--------------------
new: unit -> StructAttribute
Multiple items
union case WebApp.WebApp: string -> WebApp

--------------------
[<Struct>] type WebApp = | WebApp of string
Multiple items
union case ResourceGroup.ResourceGroup: string -> ResourceGroup

--------------------
[<Struct>] type ResourceGroup = | ResourceGroup of string
Multiple items
union case Host.Host: string -> Host

--------------------
[<Struct>] type Host = | Host of string
val env: name: string -> string
val name: string
type Environment = static member Exit: exitCode: int -> unit static member ExpandEnvironmentVariables: name: string -> string static member FailFast: message: string -> unit + 1 overload static member GetCommandLineArgs: unit -> string[] static member GetEnvironmentVariable: variable: string -> string + 1 overload static member GetEnvironmentVariables: unit -> IDictionary + 1 overload static member GetFolderPath: folder: SpecialFolder -> string + 1 overload static member GetLogicalDrives: unit -> string[] static member SetEnvironmentVariable: variable: string * value: string -> unit + 1 overload static member CommandLine: string ...
<summary>Provides information about, and means to manipulate, the current environment and platform. This class cannot be inherited.</summary>
Environment.GetEnvironmentVariable(variable: string) : string
Environment.GetEnvironmentVariable(variable: string, target: EnvironmentVariableTarget) : string
val basic: username: string -> password: string -> string
val username: string
val password: string
val sprintf: format: Printf.StringFormat<'T> -> 'T
<summary>Print to a string using the given format.</summary>
<param name="format">The formatter.</param>
<returns>The formatted result.</returns>
type Encoding = interface ICloneable member Clone: unit -> obj member Equals: value: obj -> bool member GetByteCount: chars: nativeptr<char> * count: int -> int + 5 overloads member GetBytes: chars: nativeptr<char> * charCount: int * bytes: nativeptr<byte> * byteCount: int -> int + 7 overloads member GetCharCount: bytes: nativeptr<byte> * count: int -> int + 3 overloads member GetChars: bytes: nativeptr<byte> * byteCount: int * chars: nativeptr<char> * charCount: int -> int + 4 overloads member GetDecoder: unit -> Decoder member GetEncoder: unit -> Encoder member GetHashCode: unit -> int ...
<summary>Represents a character encoding.</summary>
property Encoding.UTF8: Encoding with get
<summary>Gets an encoding for the UTF-8 format.</summary>
<returns>An encoding for the UTF-8 format.</returns>
(extension) Encoding.GetBytes(chars: inref<Buffers.ReadOnlySequence<char>>) : byte[]
   (+0 other overloads)
Encoding.GetBytes(s: string) : byte[]
   (+0 other overloads)
Encoding.GetBytes(chars: char[]) : byte[]
   (+0 other overloads)
(extension) Encoding.GetBytes(chars: inref<Buffers.ReadOnlySequence<char>>, writer: Buffers.IBufferWriter<byte>) : int64
   (+0 other overloads)
(extension) Encoding.GetBytes(chars: inref<Buffers.ReadOnlySequence<char>>, bytes: Span<byte>) : int
   (+0 other overloads)
(extension) Encoding.GetBytes(chars: ReadOnlySpan<char>, writer: Buffers.IBufferWriter<byte>) : int64
   (+0 other overloads)
Encoding.GetBytes(chars: ReadOnlySpan<char>, bytes: Span<byte>) : int
   (+0 other overloads)
Encoding.GetBytes(s: string, index: int, count: int) : byte[]
   (+0 other overloads)
Encoding.GetBytes(chars: char[], index: int, count: int) : byte[]
   (+0 other overloads)
Encoding.GetBytes(chars: nativeptr<char>, charCount: int, bytes: nativeptr<byte>, byteCount: int) : int
   (+0 other overloads)
type Convert = static member ChangeType: value: obj * conversionType: Type -> obj + 3 overloads static member FromBase64CharArray: inArray: char[] * offset: int * length: int -> byte[] static member FromBase64String: s: string -> byte[] static member FromHexString: s: string -> byte[] + 1 overload static member GetTypeCode: value: obj -> TypeCode static member IsDBNull: value: obj -> bool static member ToBase64CharArray: inArray: byte[] * offsetIn: int * length: int * outArray: char[] * offsetOut: int -> int + 1 overload static member ToBase64String: inArray: byte[] -> string + 4 overloads static member ToBoolean: value: bool -> bool + 17 overloads static member ToByte: value: bool -> byte + 18 overloads ...
<summary>Converts a base data type to another base data type.</summary>
Convert.ToBase64String(inArray: byte[]) : string
Convert.ToBase64String(bytes: ReadOnlySpan<byte>, ?options: Base64FormattingOptions) : string
Convert.ToBase64String(inArray: byte[], options: Base64FormattingOptions) : string
Convert.ToBase64String(inArray: byte[], offset: int, length: int) : string
Convert.ToBase64String(inArray: byte[], offset: int, length: int, options: Base64FormattingOptions) : string
val RenewCert: log: ILogger -> ResourceGroup -> WebApp -> Host -> Threading.Tasks.Task<Result<string,exn>>
val log: ILogger
Multiple items
type ILogger = member BeginScope<'TState> : state: 'TState -> IDisposable member IsEnabled: logLevel: LogLevel -> bool member Log<'TState> : logLevel: LogLevel * eventId: EventId * state: 'TState * ``exception`` : exn * formatter: Func<'TState,exn,string> -> unit
<summary>Represents a type used to perform logging.</summary>

--------------------
type ILogger<'TCategoryName> = inherit ILogger
<summary>A generic interface for logging where the category name is derived from the specified <typeparamref name="TCategoryName" /> type name. Generally used to enable activation of a named <see cref="T:Microsoft.Extensions.Logging.ILogger" /> from dependency injection.</summary>
<typeparam name="TCategoryName">The type who's name is used for the logger category name.</typeparam>
val resourceGroup: string
val webApp: string
val host: string
val task: FSharp.Control.Tasks.TaskBuilder.TaskBuilderV2
val tenant: string
val subscriptionId: string
val publishUrl: string
val clientId: string
val clientSecret: string
val registrationEmail: string
val certificatePassword: string
val client: HttpClient
Multiple items
type HttpClient = inherit HttpMessageInvoker new: unit -> unit + 2 overloads member CancelPendingRequests: unit -> unit member DeleteAsync: requestUri: string -> Task<HttpResponseMessage> + 3 overloads member GetAsync: requestUri: string -> Task<HttpResponseMessage> + 7 overloads member GetByteArrayAsync: requestUri: string -> Task<byte[]> + 3 overloads member GetStreamAsync: requestUri: string -> Task<Stream> + 3 overloads member GetStringAsync: requestUri: string -> Task<string> + 3 overloads member PatchAsync: requestUri: string * content: HttpContent -> Task<HttpResponseMessage> + 3 overloads member PostAsync: requestUri: string * content: HttpContent -> Task<HttpResponseMessage> + 3 overloads ...
<summary>Provides a class for sending HTTP requests and receiving HTTP responses from a resource identified by a URI.</summary>

--------------------
HttpClient() : HttpClient
HttpClient(handler: HttpMessageHandler) : HttpClient
HttpClient(handler: HttpMessageHandler, disposeHandler: bool) : HttpClient
property HttpClient.DefaultRequestHeaders: Headers.HttpRequestHeaders with get
<summary>Gets the headers which should be sent with each request.</summary>
<returns>The headers which should be sent with each request.</returns>
Headers.HttpHeaders.TryAddWithoutValidation(name: string, value: string) : bool
Headers.HttpHeaders.TryAddWithoutValidation(name: string, values: Collections.Generic.IEnumerable<string>) : bool
val ignore: value: 'T -> unit
<summary>Ignore the passed value. This is often used to throw away results of a computation.</summary>
<param name="value">The value to ignore.</param>
val body: Request
(extension) ILogger.LogInformation(message: string, [<ParamArray>] args: obj[]) : unit
(extension) ILogger.LogInformation(eventId: EventId, message: string, [<ParamArray>] args: obj[]) : unit
(extension) ILogger.LogInformation(``exception`` : exn, message: string, [<ParamArray>] args: obj[]) : unit
(extension) ILogger.LogInformation(eventId: EventId, ``exception`` : exn, message: string, [<ParamArray>] args: obj[]) : unit
val uri: string
val res: HttpResponseMessage
HttpClient.PostAsync(requestUri: Uri, content: HttpContent) : Threading.Tasks.Task<HttpResponseMessage>
   (+0 other overloads)
HttpClient.PostAsync(requestUri: string, content: HttpContent) : Threading.Tasks.Task<HttpResponseMessage>
   (+0 other overloads)
(extension) HttpClient.PostAsync<'T>(requestUri: string, value: 'T, formatter: Formatting.MediaTypeFormatter) : Threading.Tasks.Task<HttpResponseMessage>
   (+0 other overloads)
(extension) HttpClient.PostAsync<'T>(requestUri: Uri, value: 'T, formatter: Formatting.MediaTypeFormatter) : Threading.Tasks.Task<HttpResponseMessage>
   (+0 other overloads)
HttpClient.PostAsync(requestUri: Uri, content: HttpContent, cancellationToken: Threading.CancellationToken) : Threading.Tasks.Task<HttpResponseMessage>
   (+0 other overloads)
HttpClient.PostAsync(requestUri: string, content: HttpContent, cancellationToken: Threading.CancellationToken) : Threading.Tasks.Task<HttpResponseMessage>
   (+0 other overloads)
(extension) HttpClient.PostAsync<'T>(requestUri: string, value: 'T, formatter: Formatting.MediaTypeFormatter, cancellationToken: Threading.CancellationToken) : Threading.Tasks.Task<HttpResponseMessage>
   (+0 other overloads)
(extension) HttpClient.PostAsync<'T>(requestUri: string, value: 'T, formatter: Formatting.MediaTypeFormatter, mediaType: string) : Threading.Tasks.Task<HttpResponseMessage>
   (+0 other overloads)
(extension) HttpClient.PostAsync<'T>(requestUri: Uri, value: 'T, formatter: Formatting.MediaTypeFormatter, cancellationToken: Threading.CancellationToken) : Threading.Tasks.Task<HttpResponseMessage>
   (+0 other overloads)
(extension) HttpClient.PostAsync<'T>(requestUri: Uri, value: 'T, formatter: Formatting.MediaTypeFormatter, mediaType: string) : Threading.Tasks.Task<HttpResponseMessage>
   (+0 other overloads)
Multiple items
type StringContent = inherit ByteArrayContent new: content: string -> unit + 2 overloads
<summary>Provides HTTP content based on a string.</summary>

--------------------
StringContent(content: string) : StringContent
StringContent(content: string, encoding: Encoding) : StringContent
StringContent(content: string, encoding: Encoding, mediaType: string) : StringContent
type JsonConvert = static member DeserializeAnonymousType<'T> : value: string * anonymousTypeObject: 'T -> 'T + 1 overload static member DeserializeObject: value: string -> obj + 7 overloads static member DeserializeXNode: value: string -> XDocument + 3 overloads static member DeserializeXmlNode: value: string -> XmlDocument + 3 overloads static member PopulateObject: value: string * target: obj -> unit + 1 overload static member SerializeObject: value: obj -> string + 7 overloads static member SerializeXNode: node: XObject -> string + 2 overloads static member SerializeXmlNode: node: XmlNode -> string + 2 overloads static member ToString: value: DateTime -> string + 24 overloads static val False: string ...
JsonConvert.SerializeObject(value: obj) : string
JsonConvert.SerializeObject(value: obj, settings: JsonSerializerSettings) : string
JsonConvert.SerializeObject(value: obj, [<ParamArray>] converters: JsonConverter[]) : string
JsonConvert.SerializeObject(value: obj, formatting: Formatting) : string
JsonConvert.SerializeObject(value: obj, formatting: Formatting, settings: JsonSerializerSettings) : string
JsonConvert.SerializeObject(value: obj, ``type`` : Type, settings: JsonSerializerSettings) : string
JsonConvert.SerializeObject(value: obj, formatting: Formatting, [<ParamArray>] converters: JsonConverter[]) : string
JsonConvert.SerializeObject(value: obj, ``type`` : Type, formatting: Formatting, settings: JsonSerializerSettings) : string
val value: string
property HttpResponseMessage.Content: HttpContent with get, set
<summary>Gets or sets the content of a HTTP response message.</summary>
<returns>The content of the HTTP response message.</returns>
HttpContent.ReadAsStringAsync() : Threading.Tasks.Task<string>
HttpContent.ReadAsStringAsync(cancellationToken: Threading.CancellationToken) : Threading.Tasks.Task<string>
val response: string
union case Result.Ok: ResultValue: 'T -> Result<'T,'TError>
<summary> Represents an OK or a Successful result. The code succeeded with a value of 'T. </summary>
val ex: exn
(extension) ILogger.LogError(message: string, [<ParamArray>] args: obj[]) : unit
(extension) ILogger.LogError(eventId: EventId, message: string, [<ParamArray>] args: obj[]) : unit
(extension) ILogger.LogError(``exception`` : exn, message: string, [<ParamArray>] args: obj[]) : unit
(extension) ILogger.LogError(eventId: EventId, ``exception`` : exn, message: string, [<ParamArray>] args: obj[]) : unit
union case Result.Error: ErrorValue: 'TError -> Result<'T,'TError>
<summary> Represents an Error or a Failure. The code failed with a value of 'TError representing what went wrong. </summary>
namespace System.Threading
namespace System.Threading.Tasks
module ContextSensitive from FSharp.Control.Tasks.V2
module LetsEncrypt from 2019-12-13-full-fsharp-blog-3
Multiple items
type FunctionNameAttribute = inherit Attribute new: name: string -> unit static val FunctionNameValidationRegex: Regex member Name: string

--------------------
FunctionNameAttribute(name: string) : FunctionNameAttribute
val Run: myTimer: TimerInfo * log: ILogger -> Task
Multiple items
type TimerTriggerAttribute = inherit Attribute new: scheduleExpression: string -> unit + 1 overload member RunOnStartup: bool member ScheduleExpression: string member ScheduleType: Type member UseMonitor: bool

--------------------
TimerTriggerAttribute(scheduleExpression: string) : TimerTriggerAttribute
TimerTriggerAttribute(scheduleType: Type) : TimerTriggerAttribute
val myTimer: TimerInfo
Multiple items
type TimerInfo = new: schedule: TimerSchedule * status: ScheduleStatus * ?isPastDue: bool -> unit member FormatNextOccurrences: count: int * ?now: Nullable<DateTime> -> string member IsPastDue: bool member Schedule: TimerSchedule member ScheduleStatus: ScheduleStatus

--------------------
TimerInfo(schedule: Extensions.Timers.TimerSchedule, status: Extensions.Timers.ScheduleStatus, ?isPastDue: bool) : TimerInfo
Multiple items
[<Struct>] type DateTime = new: year: int * month: int * day: int -> unit + 10 overloads member Add: value: TimeSpan -> DateTime member AddDays: value: float -> DateTime member AddHours: value: float -> DateTime member AddMilliseconds: value: float -> DateTime member AddMinutes: value: float -> DateTime member AddMonths: months: int -> DateTime member AddSeconds: value: float -> DateTime member AddTicks: value: int64 -> DateTime member AddYears: value: int -> DateTime ...
<summary>Represents an instant in time, typically expressed as a date and time of day.</summary>

--------------------
DateTime ()
   (+0 other overloads)
DateTime(ticks: int64) : DateTime
   (+0 other overloads)
DateTime(ticks: int64, kind: DateTimeKind) : DateTime
   (+0 other overloads)
DateTime(year: int, month: int, day: int) : DateTime
   (+0 other overloads)
DateTime(year: int, month: int, day: int, calendar: Globalization.Calendar) : DateTime
   (+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int) : DateTime
   (+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, kind: DateTimeKind) : DateTime
   (+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, calendar: Globalization.Calendar) : DateTime
   (+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int) : DateTime
   (+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int, kind: DateTimeKind) : DateTime
   (+0 other overloads)
property DateTime.Now: DateTime with get
<summary>Gets a <see cref="T:System.DateTime" /> object that is set to the current date and time on this computer, expressed as the local time.</summary>
<returns>An object whose value is the current local date and time.</returns>
val result: Result<string,exn>
val RenewCert: log: ILogger -> ResourceGroup -> WebApp -> Host -> Task<Result<string,exn>>
Multiple items
union case Host.Host: string -> Host

--------------------
namespace Microsoft.Azure.WebJobs.Host

--------------------
[<Struct>] type Host = | Host of string
val success: string
val err: exn
Multiple items
type Task = interface IAsyncResult interface IDisposable new: action: Action -> unit + 7 overloads member ConfigureAwait: continueOnCapturedContext: bool -> ConfiguredTaskAwaitable member ContinueWith: continuationAction: Action<Task,obj> * state: obj -> Task + 19 overloads member Dispose: unit -> unit member GetAwaiter: unit -> TaskAwaiter member RunSynchronously: unit -> unit + 1 overload member Start: unit -> unit + 1 overload member Wait: unit -> unit + 4 overloads ...
<summary>Represents an asynchronous operation.</summary>

--------------------
type Task<'TResult> = inherit Task new: ``function`` : Func<obj,'TResult> * state: obj -> unit + 7 overloads member ConfigureAwait: continueOnCapturedContext: bool -> ConfiguredTaskAwaitable<'TResult> member ContinueWith: continuationAction: Action<Task<'TResult>,obj> * state: obj -> Task + 19 overloads member GetAwaiter: unit -> TaskAwaiter<'TResult> member WaitAsync: timeout: TimeSpan -> Task<'TResult> + 2 overloads member Result: 'TResult static member Factory: TaskFactory<'TResult>
<summary>Represents an asynchronous operation that can return a value.</summary>
<typeparam name="TResult">The type of the result produced by this <see cref="T:System.Threading.Tasks.Task`1" />.</typeparam>


--------------------
Task(action: Action) : Task
Task(action: Action, cancellationToken: Threading.CancellationToken) : Task
Task(action: Action, creationOptions: TaskCreationOptions) : Task
Task(action: Action<obj>, state: obj) : Task
Task(action: Action, cancellationToken: Threading.CancellationToken, creationOptions: TaskCreationOptions) : Task
Task(action: Action<obj>, state: obj, cancellationToken: Threading.CancellationToken) : Task
Task(action: Action<obj>, state: obj, creationOptions: TaskCreationOptions) : Task
Task(action: Action<obj>, state: obj, cancellationToken: Threading.CancellationToken, creationOptions: TaskCreationOptions) : Task

--------------------
Task(``function`` : Func<'TResult>) : Task<'TResult>
Task(``function`` : Func<obj,'TResult>, state: obj) : Task<'TResult>
Task(``function`` : Func<'TResult>, cancellationToken: Threading.CancellationToken) : Task<'TResult>
Task(``function`` : Func<'TResult>, creationOptions: TaskCreationOptions) : Task<'TResult>
Task(``function`` : Func<obj,'TResult>, state: obj, cancellationToken: Threading.CancellationToken) : Task<'TResult>
Task(``function`` : Func<obj,'TResult>, state: obj, creationOptions: TaskCreationOptions) : Task<'TResult>
Task(``function`` : Func<'TResult>, cancellationToken: Threading.CancellationToken, creationOptions: TaskCreationOptions) : Task<'TResult>
Task(``function`` : Func<obj,'TResult>, state: obj, cancellationToken: Threading.CancellationToken, creationOptions: TaskCreationOptions) : Task<'TResult>
namespace System.IO
namespace Microsoft.AspNetCore
namespace Microsoft.AspNetCore.Mvc
namespace Microsoft.Azure.WebJobs.Extensions
namespace Microsoft.Azure.WebJobs.Extensions.Http
namespace Microsoft.AspNetCore.Http
module challenge from 2019-12-13-full-fsharp-blog-3
val Run: req: HttpRequestMessage * code: string * log: ILogger -> Task<HttpResponseMessage>
Multiple items
type HttpTriggerAttribute = inherit Attribute new: unit -> unit + 3 overloads member AuthLevel: AuthorizationLevel member Methods: string[] member Route: string

--------------------
HttpTriggerAttribute() : HttpTriggerAttribute
HttpTriggerAttribute([<ParamArray>] methods: string[]) : HttpTriggerAttribute
HttpTriggerAttribute(authLevel: AuthorizationLevel) : HttpTriggerAttribute
HttpTriggerAttribute(authLevel: AuthorizationLevel, [<ParamArray>] methods: string[]) : HttpTriggerAttribute
[<Struct>] type AuthorizationLevel = | Anonymous = 0 | User = 1 | Function = 2 | System = 3 | Admin = 4
field AuthorizationLevel.Anonymous: AuthorizationLevel = 0
Multiple items
type RouteAttribute = inherit Attribute interface IRouteTemplateProvider new: template: string -> unit member Name: string member Order: int member Template: string

--------------------
RouteAttribute(template: string) : RouteAttribute
val req: HttpRequestMessage
Multiple items
type HttpRequestMessage = interface IDisposable new: unit -> unit + 2 overloads member Dispose: unit -> unit member ToString: unit -> string member Content: HttpContent member Headers: HttpRequestHeaders member Method: HttpMethod member Options: HttpRequestOptions member Properties: IDictionary<string,obj> member RequestUri: Uri ...
<summary>Represents a HTTP request message.</summary>

--------------------
HttpRequestMessage() : HttpRequestMessage
HttpRequestMessage(method: HttpMethod, requestUri: string) : HttpRequestMessage
HttpRequestMessage(method: HttpMethod, requestUri: Uri) : HttpRequestMessage
val code: string
val content: string
Multiple items
type File = static member AppendAllLines: path: string * contents: IEnumerable<string> -> unit + 1 overload static member AppendAllLinesAsync: path: string * contents: IEnumerable<string> * encoding: Encoding * ?cancellationToken: CancellationToken -> Task + 1 overload static member AppendAllText: path: string * contents: string -> unit + 1 overload static member AppendAllTextAsync: path: string * contents: string * encoding: Encoding * ?cancellationToken: CancellationToken -> Task + 1 overload static member AppendText: path: string -> StreamWriter static member Copy: sourceFileName: string * destFileName: string -> unit + 1 overload static member Create: path: string -> FileStream + 2 overloads static member CreateSymbolicLink: path: string * pathToTarget: string -> FileSystemInfo static member CreateText: path: string -> StreamWriter static member Decrypt: path: string -> unit ...
<summary>Provides static methods for the creation, copying, deletion, moving, and opening of a single file, and aids in the creation of <see cref="T:System.IO.FileStream" /> objects.</summary>

--------------------
type FileAttribute = inherit Attribute new: path: string * ?access: FileAccess -> unit + 1 overload member Access: FileAccess member Mode: FileMode member Path: string

--------------------
FileAttribute(path: string, ?access: FileAccess) : FileAttribute
FileAttribute(path: string, access: FileAccess, mode: FileMode) : FileAttribute
File.ReadAllText(path: string) : string
File.ReadAllText(path: string, encoding: Text.Encoding) : string
Multiple items
type HttpResponseMessage = interface IDisposable new: unit -> unit + 1 overload member Dispose: unit -> unit member EnsureSuccessStatusCode: unit -> HttpResponseMessage member ToString: unit -> string member Content: HttpContent member Headers: HttpResponseHeaders member IsSuccessStatusCode: bool member ReasonPhrase: string member RequestMessage: HttpRequestMessage ...
<summary>Represents a HTTP response message including the status code and data.</summary>

--------------------
HttpResponseMessage() : HttpResponseMessage
HttpResponseMessage(statusCode: HttpStatusCode) : HttpResponseMessage
Multiple items
type StringContent = inherit ByteArrayContent new: content: string -> unit + 2 overloads
<summary>Provides HTTP content based on a string.</summary>

--------------------
StringContent(content: string) : StringContent
StringContent(content: string, encoding: Text.Encoding) : StringContent
StringContent(content: string, encoding: Text.Encoding, mediaType: string) : StringContent
property Text.Encoding.UTF8: Text.Encoding with get
<summary>Gets an encoding for the UTF-8 format.</summary>
<returns>An encoding for the UTF-8 format.</returns>
property Exception.Message: string with get
val raise: exn: Exception -> 'T
<summary>Raises an exception</summary>
<param name="exn">The exception to raise.</param>
<returns>The result value.</returns>
Multiple items
type AggregateException = inherit exn new: unit -> unit + 6 overloads member Flatten: unit -> AggregateException member GetBaseException: unit -> exn member GetObjectData: info: SerializationInfo * context: StreamingContext -> unit member Handle: predicate: Func<exn,bool> -> unit member ToString: unit -> string member InnerExceptions: ReadOnlyCollection<exn> member Message: string
<summary>Represents one or more errors that occur during application execution.</summary>

--------------------
AggregateException() : AggregateException
AggregateException(innerExceptions: Collections.Generic.IEnumerable<exn>) : AggregateException
AggregateException([<ParamArray>] innerExceptions: exn[]) : AggregateException
AggregateException(message: string) : AggregateException
AggregateException(message: string, innerExceptions: Collections.Generic.IEnumerable<exn>) : AggregateException
AggregateException(message: string, innerException: exn) : AggregateException
AggregateException(message: string, [<ParamArray>] innerExceptions: exn[]) : AggregateException