// thinkbeforecoding

Full F# Blog - Part 3

2019-12-13T18: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:

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

The create a directory, and create the app inside

1: 
2: 
3: 
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:

1: 
2: 
dotnet add package TaskBuilder.fs 
dotnet restore

Create a timer trigger function with the following command:

1: 
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:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
<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:

1: 
2: 
3: 
4: 
<ItemGroup>
  <Compile Include="LetsEncrypt.fs" />
  <Compile Include="Renew.fs" />
</ItemGroup>

Start the file with:

1: 
module LetsEncrypt

and type inside:

  1: 
  2: 
  3: 
  4: 
  5: 
  6: 
  7: 
  8: 
  9: 
 10: 
 11: 
 12: 
 13: 
 14: 
 15: 
 16: 
 17: 
 18: 
 19: 
 20: 
 21: 
 22: 
 23: 
 24: 
 25: 
 26: 
 27: 
 28: 
 29: 
 30: 
 31: 
 32: 
 33: 
 34: 
 35: 
 36: 
 37: 
 38: 
 39: 
 40: 
 41: 
 42: 
 43: 
 44: 
 45: 
 46: 
 47: 
 48: 
 49: 
 50: 
 51: 
 52: 
 53: 
 54: 
 55: 
 56: 
 57: 
 58: 
 59: 
 60: 
 61: 
 62: 
 63: 
 64: 
 65: 
 66: 
 67: 
 68: 
 69: 
 70: 
 71: 
 72: 
 73: 
 74: 
 75: 
 76: 
 77: 
 78: 
 79: 
 80: 
 81: 
 82: 
 83: 
 84: 
 85: 
 86: 
 87: 
 88: 
 89: 
 90: 
 91: 
 92: 
 93: 
 94: 
 95: 
 96: 
 97: 
 98: 
 99: 
100: 
101: 
102: 
103: 
104: 
105: 
106: 
107: 
108: 
109: 
110: 
111: 
112: 
113: 
114: 
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 RenewCert (log: ILogger)  (ResourceGroup resourceGroup) (WebApp webApp) (Host host) =
        task {
            try
                let tenant = Environment.GetEnvironmentVariable("Tenant")
                let subscriptionId = Environment.GetEnvironmentVariable("Subscription.Id")

                let publishUrl = Environment.GetEnvironmentVariable("Publish.Url")
                let username = Environment.GetEnvironmentVariable("Publish.UserName")
                let password = Environment.GetEnvironmentVariable("Publish.Password")

                let clientId = Environment.GetEnvironmentVariable("Client.Id")
                let clientSecret = Environment.GetEnvironmentVariable("Client.Secret")

                let registrationEmail = Environment.GetEnvironmentVariable("Registration.Email")
                let certificatePassword = Environment.GetEnvironmentVariable("Cert.Password")
                use client = new HttpClient();
                client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes(sprintf "%s:%s" 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
                            ResourceGroupName = resourceGroup //Resource group of the web app 
                            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
                            PFXPassword = certificatePassword //Replace with your own 
                            UseProduction = true // Replace with true if you want production certificate from Lets Encrypt 
                        }
                    CertificateSettings = 
                        {   UseIPBasedSSL = false }
                    AuthorizationChallengeProviderConfig = 
                        {   DisableWebConfigUpdate = false }
                }

                log.LogInformation("Post request")
                let! res = client.PostAsync(
                                sprintf "https://%s/letsencrypt/api/certificates/challengeprovider/http/kudu/certificateinstall/azurewebapp?api-version=2017-09-01" publishUrl, 
                                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:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
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 {
            log.LogInformation(sprintf "Timer trigger function to renew myblog.com executed at: %O" DateTime.Now)
            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

1: 
2: 
az login
az account  set --subscription yoursubscription

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

1: 
2: 
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:

1: 
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:

1: 
2: 
3: 
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:

1: 
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:

1: 
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:

1: 
2: 
3: 
4: 
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:

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

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

1: 
2: 
3: 
4: 
5: 
6: 
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:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
25: 
26: 
27: 
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:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
{
  "$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:

1: 
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:

1: 
@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
namespace FSharp
namespace 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: obj;
   ClientId: obj;
   ClientSecret: obj;
   ResourceGroupName: obj;
   SubscriptionId: obj;
   Tenant: obj;}
type AzureEnvironment =
  {WebAppName: obj;
   ClientId: obj;
   ClientSecret: obj;
   ResourceGroupName: obj;
   SubscriptionId: obj;
   Tenant: obj;}
Multiple items
Request.AcmeConfig: AcmeConfig

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

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

--------------------
type AuthorizationChallengeProviderConfig =
  {DisableWebConfigUpdate: obj;}
type AuthorizationChallengeProviderConfig =
  {DisableWebConfigUpdate: obj;}
AzureEnvironment.WebAppName: obj
AzureEnvironment.ClientId: obj
AzureEnvironment.ClientSecret: obj
AzureEnvironment.ResourceGroupName: obj
AzureEnvironment.SubscriptionId: obj
AzureEnvironment.Tenant: obj
AcmeConfig.RegistrationEmail: obj
AcmeConfig.Host: obj
AcmeConfig.AlternateNames: obj Microsoft.FSharp.Core.[]
AcmeConfig.RSAKeyLength: obj
AcmeConfig.PFXPassword: obj
AcmeConfig.UseProduction: obj
CertificateSettings.UseIPBasedSSL: obj
AuthorizationChallengeProviderConfig.DisableWebConfigUpdate: obj
type ServiceResponse =
  {CertificateInfo: CertificateInfo;}
Multiple items
ServiceResponse.CertificateInfo: CertificateInfo

--------------------
type CertificateInfo =
  {PfxCertificate: obj;
   Password: obj;}
type CertificateInfo =
  {PfxCertificate: obj;
   Password: obj;}
CertificateInfo.PfxCertificate: obj
CertificateInfo.Password: obj
Multiple items
union case WebApp.WebApp: obj -> WebApp

--------------------
type WebApp = | WebApp of obj
Multiple items
union case ResourceGroup.ResourceGroup: obj -> ResourceGroup

--------------------
type ResourceGroup = | ResourceGroup of obj
Multiple items
union case Host.Host: obj -> Host

--------------------
type Host = | Host of obj
val RenewCert : log:ILogger -> ResourceGroup -> WebApp -> Host -> 'a
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:Exception * formatter:Func<'TState, Exception, string> -> unit

--------------------
type ILogger<'TCategoryName> =
  inherit ILogger
val resourceGroup : obj
val webApp : obj
val host : obj
val task : FSharp.Control.Tasks.TaskBuilder.TaskBuilderV2
type Environment =
  static member CommandLine : string
  static member CurrentDirectory : string with get, set
  static member CurrentManagedThreadId : int
  static member Exit : exitCode:int -> unit
  static member ExitCode : int with get, set
  static member ExpandEnvironmentVariables : name:string -> string
  static member FailFast : message:string -> unit + 2 overloads
  static member GetCommandLineArgs : unit -> string[]
  static member GetEnvironmentVariable : variable:string -> string + 1 overload
  static member GetEnvironmentVariables : unit -> IDictionary + 1 overload
  ...
  nested type SpecialFolder
  nested type SpecialFolderOption
Environment.GetEnvironmentVariable(variable: string) : string
Environment.GetEnvironmentVariable(variable: string, target: EnvironmentVariableTarget) : string
Multiple items
type HttpClient =
  inherit HttpMessageInvoker
  new : unit -> HttpClient + 2 overloads
  member BaseAddress : Uri with get, set
  member CancelPendingRequests : unit -> unit
  member DefaultRequestHeaders : HttpRequestHeaders
  member DefaultRequestVersion : Version with get, set
  member DeleteAsync : requestUri:string -> Task<HttpResponseMessage> + 3 overloads
  member GetAsync : requestUri:string -> Task<HttpResponseMessage> + 7 overloads
  member GetByteArrayAsync : requestUri:string -> Task<byte[]> + 1 overload
  member GetStreamAsync : requestUri:string -> Task<Stream> + 1 overload
  member GetStringAsync : requestUri:string -> Task<string> + 1 overload
  ...

--------------------
HttpClient() : HttpClient
HttpClient(handler: HttpMessageHandler) : HttpClient
HttpClient(handler: HttpMessageHandler, disposeHandler: bool) : HttpClient
type Convert =
  static val DBNull : obj
  static member ChangeType : value:obj * typeCode:TypeCode -> obj + 3 overloads
  static member FromBase64CharArray : inArray:char[] * offset:int * length:int -> byte[]
  static member FromBase64String : s:string -> byte[]
  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:obj -> bool + 17 overloads
  static member ToByte : value:obj -> byte + 18 overloads
  ...
Convert.ToBase64String(inArray: byte Microsoft.FSharp.Core.[]) : string
Convert.ToBase64String(bytes: ReadOnlySpan<byte>,?options: Base64FormattingOptions) : string
Convert.ToBase64String(inArray: byte Microsoft.FSharp.Core.[], options: Base64FormattingOptions) : string
Convert.ToBase64String(inArray: byte Microsoft.FSharp.Core.[], offset: int, length: int) : string
Convert.ToBase64String(inArray: byte Microsoft.FSharp.Core.[], offset: int, length: int, options: Base64FormattingOptions) : string
type Encoding =
  member BodyName : string
  member Clone : unit -> obj
  member CodePage : int
  member DecoderFallback : DecoderFallback with get, set
  member EncoderFallback : EncoderFallback with get, set
  member EncodingName : string
  member Equals : value:obj -> bool
  member GetByteCount : chars:char[] -> int + 5 overloads
  member GetBytes : chars:char[] -> byte[] + 7 overloads
  member GetCharCount : bytes:byte[] -> int + 3 overloads
  ...
property Encoding.UTF8: Encoding
Encoding.GetBytes(s: string) : byte Microsoft.FSharp.Core.[]
Encoding.GetBytes(chars: char Microsoft.FSharp.Core.[]) : byte Microsoft.FSharp.Core.[]
Encoding.GetBytes(chars: ReadOnlySpan<char>, bytes: Span<byte>) : int
Encoding.GetBytes(s: string, index: int, count: int) : byte Microsoft.FSharp.Core.[]
Encoding.GetBytes(chars: char Microsoft.FSharp.Core.[], index: int, count: int) : byte Microsoft.FSharp.Core.[]
Encoding.GetBytes(chars: nativeptr<char>, charCount: int, bytes: nativeptr<byte>, byteCount: int) : int
Encoding.GetBytes(s: string, charIndex: int, charCount: int, bytes: byte Microsoft.FSharp.Core.[], byteIndex: int) : int
Encoding.GetBytes(chars: char Microsoft.FSharp.Core.[], charIndex: int, charCount: int, bytes: byte Microsoft.FSharp.Core.[], byteIndex: int) : int
(extension) ILogger.LogInformation(message: string, [<ParamArray>] args: obj Microsoft.FSharp.Core.[]) : unit
(extension) ILogger.LogInformation(eventId: EventId, message: string, [<ParamArray>] args: obj Microsoft.FSharp.Core.[]) : unit
(extension) ILogger.LogInformation(exception: exn, message: string, [<ParamArray>] args: obj Microsoft.FSharp.Core.[]) : unit
(extension) ILogger.LogInformation(eventId: EventId, exception: exn, message: string, [<ParamArray>] args: obj Microsoft.FSharp.Core.[]) : unit
Multiple items
type StringContent =
  inherit ByteArrayContent
  new : content:string -> StringContent + 2 overloads

--------------------
StringContent(content: string) : StringContent
StringContent(content: string, encoding: Encoding) : StringContent
StringContent(content: string, encoding: Encoding, mediaType: string) : StringContent
type JsonConvert =
  static val True : string
  static val False : string
  static val Null : string
  static val Undefined : string
  static val PositiveInfinity : string
  static val NegativeInfinity : string
  static val NaN : string
  static member DefaultSettings : Func<JsonSerializerSettings> with get, set
  static member DeserializeAnonymousType<'T> : value:string * anonymousTypeObject:'T -> 'T + 1 overload
  static member DeserializeObject : value:string -> obj + 7 overloads
  ...
(extension) ILogger.LogError(message: string, [<ParamArray>] args: obj Microsoft.FSharp.Core.[]) : unit
(extension) ILogger.LogError(eventId: EventId, message: string, [<ParamArray>] args: obj Microsoft.FSharp.Core.[]) : unit
(extension) ILogger.LogError(exception: exn, message: string, [<ParamArray>] args: obj Microsoft.FSharp.Core.[]) : unit
(extension) ILogger.LogError(eventId: EventId, exception: exn, message: string, [<ParamArray>] args: obj Microsoft.FSharp.Core.[]) : unit
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 -> FunctionNameAttribute
  member Name : string
  static val FunctionNameValidationRegex : Regex

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

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

--------------------
TimerInfo(schedule: Extensions.Timers.TimerSchedule, status: Extensions.Timers.ScheduleStatus,?isPastDue: bool) : TimerInfo
Multiple items
type DateTime =
  struct
    new : ticks:int64 -> DateTime + 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
    ...
  end

--------------------
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
Multiple items
union case Host.Host: obj -> Host

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

--------------------
type Host = | Host of obj
Multiple items
type Task =
  new : action:Action -> Task + 7 overloads
  member AsyncState : obj
  member ConfigureAwait : continueOnCapturedContext:bool -> ConfiguredTaskAwaitable
  member ContinueWith : continuationAction:Action<Task> -> Task + 19 overloads
  member CreationOptions : TaskCreationOptions
  member Dispose : unit -> unit
  member Exception : AggregateException
  member GetAwaiter : unit -> TaskAwaiter
  member Id : int
  member IsCanceled : bool
  ...

--------------------
type Task<'TResult> =
  inherit Task
  new : function:Func<'TResult> -> Task<'TResult> + 7 overloads
  member ConfigureAwait : continueOnCapturedContext:bool -> ConfiguredTaskAwaitable<'TResult>
  member ContinueWith : continuationAction:Action<Task<'TResult>> -> Task + 19 overloads
  member GetAwaiter : unit -> TaskAwaiter<'TResult>
  member Result : 'TResult
  static member Factory : TaskFactory<'TResult>

--------------------
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<'TResult>, cancellationToken: Threading.CancellationToken) : Task<'TResult>
Task(function: Func<'TResult>, creationOptions: TaskCreationOptions) : Task<'TResult>
Task(function: Func<obj,'TResult>, state: obj) : Task<'TResult>
Task(function: Func<'TResult>, cancellationToken: Threading.CancellationToken, 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<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:'a * log:ILogger -> 'b
Multiple items
type HttpTriggerAttribute =
  inherit Attribute
  new : unit -> HttpTriggerAttribute + 3 overloads
  member AuthLevel : AuthorizationLevel with get, set
  member Methods : string[] with get, set
  member Route : string with get, set

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

--------------------
RouteAttribute(template: string) : RouteAttribute
val req : HttpRequestMessage
Multiple items
type HttpRequestMessage =
  new : unit -> HttpRequestMessage + 2 overloads
  member Content : HttpContent with get, set
  member Dispose : unit -> unit
  member Headers : HttpRequestHeaders
  member Method : HttpMethod with get, set
  member Properties : IDictionary<string, obj>
  member RequestUri : Uri with get, set
  member ToString : unit -> string
  member Version : Version with get, set

--------------------
val code : 'a
Multiple items
type File =
  static member AppendAllLines : path:string * contents:IEnumerable<string> -> unit + 1 overload
  static member AppendAllLinesAsync : path:string * contents:IEnumerable<string> * ?cancellationToken:CancellationToken -> Task + 1 overload
  static member AppendAllText : path:string * contents:string -> unit + 1 overload
  static member AppendAllTextAsync : path:string * contents:string * ?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 CreateText : path:string -> StreamWriter
  static member Decrypt : path:string -> unit
  static member Delete : path:string -> unit
  ...

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

--------------------
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 =
  new : unit -> HttpResponseMessage + 1 overload
  member Content : HttpContent with get, set
  member Dispose : unit -> unit
  member EnsureSuccessStatusCode : unit -> HttpResponseMessage
  member Headers : HttpResponseHeaders
  member IsSuccessStatusCode : bool
  member ReasonPhrase : string with get, set
  member RequestMessage : HttpRequestMessage with get, set
  member StatusCode : HttpStatusCode with get, set
  member ToString : unit -> string
  ...

--------------------
Multiple items
type StringContent =
  inherit ByteArrayContent
  new : content:string -> StringContent + 2 overloads

--------------------
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
Multiple items
type AggregateException =
  inherit Exception
  new : unit -> AggregateException + 6 overloads
  member Flatten : unit -> AggregateException
  member GetBaseException : unit -> Exception
  member GetObjectData : info:SerializationInfo * context:StreamingContext -> unit
  member Handle : predicate:Func<Exception, bool> -> unit
  member InnerExceptions : ReadOnlyCollection<Exception>
  member Message : string
  member ToString : unit -> string

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