With the release of the dotnet 2.0 framework things are really getting interesting. So I tried to combine dotnet core, F# and Azure service bus. Seems reasonable at first, but as F# is quite new to me I struggled to find any tutorial. Apparently everyone is assuming that when you work in F# you'll be able to figure things out from the C# tutorials. With this tutorial that's about to change.
Create an Azure Service bus
First we need our Service Bus. Go to the azure portal
Click on the new button and search for "Service Bus" and click "Create"
I'll assume you know the drill to complete the wizard. Here is what I took:
Once the Service bus is created you'll find it under the resource group you defined.
Before we can start using the service bus, you need to create a queue. I named the queue "demoqueue". You can leave the other default settings.
The last thing to do is to get the Service bus connectionstring. Click on the "Shared access policies" of the service bus (make sure you're on the service bus pane and not the queue pane). Click on the policy "RootManageSharedAccessKey" to reveal the connection string.
The Azure side is all set up.
Create a dotnet core solution
You can use Visual Studio to create .NET Core solutions, but an editor as VS Code is cross platorm and is quite fast to work with. I used VS Code to get the job done, but for the Visual Studio users we'll create a solution file as well.
Create a new directory "SbDemo"
Next we'll create a solution file via the dotnet CLI:
Next we'll create a solution file via the dotnet CLI:
dotnet new sln --name DevProtocol.Azure.SbDemo
Create 2 applications. The SbPush will push messages on our service bus, the SbReceive will receive them.
dotnet new console -lang F# --name DevProtocol.Azure.SbDemo.SbPush
dotnet new console -lang F# --name DevProtocol.Azure.SbDemo.SbReceive
Add the projects to your solution file
dotnet sln add DevProtocol.Azure.SbDemo.SbPush\DevProtocol.Azure.SbDemo.SbPush.fsproj
dotnet sln add DevProtocol.Azure.SbDemo.SbReceive\DevProtocol.Azure.SbDemo.SbReceive.fsproj
Push messages to your Service bus
We'll start off with pushing messages to the queue.
Open up the project with VSCode
Open up the project with VSCode
code .
Add the Service bus Nuget
Open your project file DevProtocol.Azure.SbDemo.SbPush.fsproj. You'll notice an ItemGroup that contains the files in your project (currently only Program.fs). We'll add a NuGet reference by adding a second ItemGroup
<ItemGroup>
<PackageReference Include="Microsoft.Azure.ServiceBus" Version="2.0.0" />
</ItemGroup>
Now you can open up the build in terminal in VSCode (shortcut: CTRL+`) and restore the package(s). Once the package is restored you can get intellisense.
dotnet restore
Adding files to the project
Add 2 new files (Sb.fs and Data.fs) to the SbPush project and register them in the DevProtocol.Azure.SbDemo.SbPush.fsproj file. Add them in the existing ItemGroup where the Program.fs file is already included. Make sure you have the same file order as defined below as he file order in F# is important:
<ItemGroup>
<Compile Include="Data.fs" />
<Compile Include="Sb.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
The data
The Data.fs file is simple. We'll use a list of integers to push to the service bus
namespace DevProtocol.Azure.SbDemo.SbPush
module Data =
let getData() =
let myData = [1..10]
myData |>
List.map (fun x -> x + 10)
Sending messages
The Sb.fs file is a bit more complex. First we need to add the Microsoft.Azure.ServiceBus namespace
open Microsoft.Azure.ServiceBus
In the Sb module we define our connectionstring we got from the Azure portal earlier on and the queuename
namespace DevProtocol.Azure.SbDemo.SbPush
open System.Text
open Microsoft.Azure.ServiceBus
module Sb =
let connectionstring = "<your connectionstring>"
let queuePath = "<your queuename>"
In the same Sb module we now can create a sendData method that has a list of strings as input (defined by the parameter data). First we create a queueclient with our connectionstring and queuepath.
let sendData data =
let queueClient = QueueClient(connectionstring, queuePath)
The queueClient has a SendAsync method. So we'll define an async sendMessage that will print out our message to the console and then send it via the queueclient. Unlike C#, the F# methods don't have the async keyword in the method declaration, but around the async workflow. We capture the result of the async task with a "let!" instead of a normal "let". Note that an async workflow contains the return keyword.
let sendMessage (queueClient:IQueueClient) (messageText:string) =
async {
printfn "%s" messageText
let message = Message(Encoding.UTF8.GetBytes messageText)
let! result = queueClient.SendAsync(message) |> Async.AwaitTask
return result
}
The only thing we need to do now is loop over our input list (named data) and send every string via the sendMessage method and close the connection of the queueClient:
async {
let result = List.iter(fun x -> sendMessage queueClient x |> Async.RunSynchronously) data
queueClient.CloseAsync() |> Async.AwaitTask
|> ignore
return result
}
So the Sb.fs file looks like:
namespace DevProtocol.Azure.SbDemo.SbPush
open System.Text
open Microsoft.Azure.ServiceBus
module Sb =
let connectionstring = "<your connectionstring>"
let queuePath = "<your queuename>"
let sendData data =
let queueClient = QueueClient(connectionstring, queuePath)
let sendMessage (queueClient:IQueueClient) (messageText:string) =
async {
printfn "%s" messageText
let message = Message(Encoding.UTF8.GetBytes messageText)
let! result = queueClient.SendAsync(message) |> Async.AwaitTask
return result
}
async {
let result = List.iter(fun x -> sendMessage queueClient x |> Async.RunSynchronously) data
queueClient.CloseAsync() |> Async.AwaitTask
|> ignore
return result
}
Note that the sendMessage method is a function defined within the sendData function. As functions in F# are first-class citizens, we can use it to restrict the scope of a function.
Put it together
In the Program.fs file we send our data via the service bus
open System
open DevProtocol.Azure.SbDemo.SbPush.Sb
open DevProtocol.Azure.SbDemo.SbPush.Data
[<EntryPoint>]
let main argv =
printfn "Start sending messages"
sendData (getData() |> List.map(fun x -> sprintf "%s%A" "Message" x )) |> Async.RunSynchronously
printfn "Ended sending messages"
0
Verify the result
Now build the DevProtocol.Azure.SbDemo.SbPush project. cd into the project folder and execute
dotnet build
dotnet run
The console will output Message11 till Message20. Now we can already verify if our messages arrived on the queue. Open up the azure portal and go to the queue overview. You'll notice that 10 messages arrived
Receive messages from your Service bus
Now that we can push messages on our Service bus, you might get excited to get them back.
Add the Service bus Nuget
Open up the DevProtocol.Azure.SbDemo.SbReceive.fsproj file and add the Service bus reference:
<ItemGroup>
<PackageReference Include="Microsoft.Azure.ServiceBus" Version="2.0.0" />
</ItemGroup>
Add a file to the project
Add a new file (Sb.fs) to the SbPush project and register it in the DevProtocol.Azure.SbDemo.SbReceive.fsproj file. Add it in the existing ItemGroup where the Program.fs file is already included. Make sure you have the same file order as defined below as he file order in F# is important:
<ItemGroup>
<Compile Include="Sb.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
Receiving messages
To receive messages we first add the Microsoft.Azure.ServiceBus namespace
open Microsoft.Azure.ServiceBus
In the Sb module we define jus like we dit for pushing messages our connectionstring we got from the Azure portal earlier on and the queuename
namespace DevProtocol.Azure.SbDemo.SbReceive
open Microsoft.Azure.ServiceBus
open System.Threading
open System.Text
open System.Threading.Tasks
module Sb =
let connectionstring = "<your connectionstring>"
let queuePath = "<your queuename>"
Now we define the method to receive the data
let receiveData() =
let queueClient = QueueClient(connectionstring, queuePath)
let exceptionReceivedHandler (args:ExceptionReceivedEventArgs) =
printfn "Got an exception: %A" args.Exception
Task.CompletedTask
let processMessage (message:Message) (token:CancellationToken) =
printfn "Received message: %s" (Encoding.UTF8.GetString(message.Body))
queueClient.CompleteAsync(message.SystemProperties.LockToken)|> Async.AwaitTask |> ignore
Task.CompletedTask
let msgOptions = new MessageHandlerOptions(fun x -> exceptionReceivedHandler(x))
msgOptions.AutoComplete <- false
let messagehandler = queueClient.RegisterMessageHandler(processMessage, msgOptions)
messagehandler |> ignore
Put it together
From the main method in the Program.fs file we'll start listening for message.
open System
open DevProtocol.Azure.SbDemo.SbReceive
[<EntryPoint>]
let main argv =
printfn "Start receiving messages"
Sb.receiveData()
Console.ReadLine() |> ignore
0
Verify the result
Now build the DevProtocol.Azure.SbDemo.SbReceive project. cd into the project folder and execute
dotnet build
dotnet run
Now you'll receive all your messages back in the console. Go back to the portal and verify your queue messages. The count will be 0 again!
Final Thoughts
Setting up a Service bus in Azure is really a breeze. With the knowledge of some dotnet CLI commands you can quickly setup a new dotnet core project even in F#. Now there's at least one more tutorial to get you started with F# and off course dotnet core and Azure as well.
Next steps
As Andrii Chebukin pointed out in a comment to this post, you can optimise the code by using FusionTasks.
First add a packate reference in the DevProtocol.Azure.SbDemo.SbReceive.fsproj file and the DevProtocol.Azure.SbDemo.SbPush.fsproj file
First add a packate reference in the DevProtocol.Azure.SbDemo.SbReceive.fsproj file and the DevProtocol.Azure.SbDemo.SbPush.fsproj file
<ItemGroup>
<PackageReference Include="FSharp.Control.FusionTasks.FS41" Version="1.1.0" />
<PackageReference Include="Microsoft.Azure.ServiceBus" Version="2.0.0" />
</ItemGroup>
Now we basically can get rid of the "Async.AwaitTask" statements.
As a result a code block like
async {
let result = List.iter(fun x -> sendMessage queueClient x |> Async.RunSynchronously) data
queueClient.CloseAsync() |> Async.AwaitTask
|> ignore
return result
}
becomes
async {
let tasks = List.map(fun x -> sendMessage queueClient x) data
let! result = Async.Parallel tasks
do! queueClient.CloseAsync()
return result
}
Andril was kind enough to send me a pull request with a code update to use FusionTasks. So, definitely check out the code on GitHub.
Download
Download the code from my github.
No comments:
Post a Comment