(.NET) How to Create a Chatbot
What's a chatbot? It's a Space application that communicates with a Space user in its own Chats channel. A minimum viable bot must:
Respond with a list of available commands when a user types
/
(slash) in the channel.Provide at least one command: After a user sends this command to the channel, the bot must do something and then respond with a message.
Let's create a first chatbot! Please welcome – the 'Remind me' bot!
Our bot will send a reminder to a user after a given amount of time. For example, if a user sends the remind 60 take a nap
command to the bot, after 60 seconds, the bot will respond with the take a nap
message. Also, our bot will have a command that provides help
when requested.

In this tutorial, we'll go through the entire process of creating a chatbot. You can also download the resulting source code.
What will we need along our journey?
![]() | We'll write our bot in C# and .NET. There are many IDE's available for building .NET applications, so you can use your preferred IDE. In this tutorial, we'll use JetBrains Rider. |
![]() | As we have learned in the Develop Applications topic, any application must communicate with Space using the Space HTTP API. To make developing Space apps easier, we provide Space SDK for Kotlin and .NET. The SDK contains the HTTP API client that lets you easily authenticate in and communicate with Space. We'll use the Space SDK by adding a NuGet package dependency to our project. |
![]() | A tunnelling service Such a service exposes local servers to the public internet. It will let us run our chatbot locally and access it from Space via a public URL (we'll specify it as the chatbot's endpoint). For example, you can use the ngrok tunnelling service for this purpose. To start working with ngrok, you should download the ngrok client. For our purposes, the free plan for ngrok is enough.
|
Open JetBrains Rider.
In the welcome screen, start creating a new solution with New Solution.
In the list of templates, select ASP.NET Core Web Application.
In Type, select Empty. This template will set up a project with the minimal required configuration.
Specify a solution name and project name, for example
RemindMeBot
, and click Create.That's it! We now have a web application where we can start building our bot.
Use the context menu on the RemindMeBot project, and select Manage NuGet Packages. This will open the NuGet tool window in Rider.
Search for the
JetBrains.Space.AspNetCore
package. This package contains helpers to create Space applications, such as the chatbot we are building.tip
To get more information about the NuGet packages available in the SDK, consult the .NET SDK for JetBrains Space project on GitHub.
Done! Now, we have the Space SDK in our project, and we can start building the 'Remind me' bot.
warning
We strongly recommend that you use a tunneling service only for testing purposes during development. Do not use it to host the chatbot on your computer on a regular basis. An open and publicly-available port on your computer poses a severe security threat.
When adding new endpoints to your application, make sure to verify incoming requests as we do later in this tutorial.
Before we register our chatbot in Space, we have to get a publicly available URL for it. As your development environment is probably located behind NAT, the easiest way to get the URL is to use a tunneling service. In our case, we'll use ngrok.
Download and unzip the ngrok client.
In terminal (on macOS or Linux) or in the command line (on Windows), open the ngrok directory.
By default, our project is configured to run the HTTP server on the port 5001 (you can check this in the
Properties/launchSettings.json
file). Run tunnelling for this port:./ngrok http 5001
The ngrok service will start. It will look similar to:
Session Status online Account user@example.com (Plan: Free) Version 3.0.6 Region United States (us) Latency - Web Interface http://127.0.0.1:4040 Forwarding https://98af-94-158-242-146.ngrok.io -> http://localhost:8080 Connections ttl opn rt1 rt5 p50 p90 0 0 0.00 0.00 0.00 0.00
Here we are interested in the
Forwarding
line – it contains the public URL. ngrok redirects requests from this URL to our localhost using its tunnelling service. In the example above, the address ishttps://98af-94-158-242-146.ngrok.io
but in your case it will be something else as ngrok generates these random URLs dynamically.Great job! Now, we have a running tunneling service and the public URL of our future chatbot.
In order Space and chatbot could communicate with each other, we must register the bot in Space.
When developing an application, you must decide on two important things:
Single-org applications are intended only for a single Space organization. A Space user registers and configures a single-org application manually in the Space UI.
Multi-org applications are intended for multiple Space organizations. A multi-org application registers and configures itself in a particular Space instance using API calls.
Since we are just practicing, there is no point in being distracted by the complexities of configuring a multi-org application. Instead, we will register and configure our application using the Space UI. So, we are going to create a single-org application.
Authorization subject: decide how your application should act in Space – on behalf of itself, on behalf of a particular Space user, or both. This determines which authorization flows the application will use.
In our case, a chatbot will send notifications in its own chat channel on befalf of itself. As an OAuth 2.0 flow, we will use the Client Credentials flow. It lets the application authorize in Space using a client ID and a client secret.
To summarize, our chatbot is a single-org application that uses the Client Credentials authorization flow. Now, let's register our bot in Space.
Open your Space instance.
On the main menu, click
Extensions and choose Installed.
Click New application.
Give the application a unique name, say,
remind-me-bot
and click Create.On the Overview tab, select Chat bot. This will enable a chat channel for our application.
Open the Authorization tab. We will not change anything on this tab – as our bot is simple and doesn't get any data from Space.
We're on this tab just for you to note how important it is – if your app is supposed to access various Space modules, you should provide it corresponding permissions. Learn more about requesting permissions.
Open the Authentication tab. Note that the Client Credentials Flow is enabled for all applications by default. We need to get the application's Client ID and Client secret. Our chatbot will use them to get a Space access token.
When a user types anything in the chatbot channel, Space sends the user input to the application. So, our next step is to specify the URL of our application endpoint and choose how we will verify requests from Space.
Open the Endpoint tab.
In the Endpoint URI, specify the public URL generated by the tunnelling service for our bot. Let's make this endpoint less generic and add a postfix to the URL, for example
api/space
. So, the final endpoint would behttps://{random_string_from_ngrok}.ngrok.io/api/space
By default, Space recommends using the Public key verification method. Let's leave the default and click Save.
Great job! Now, our bot is registered in Space, we have all required authentication data, and we're ready to start developing our bot.
Ready, set, code! We'll start by updating the configuration file in our application. Then, we'll create and register a RemindMeBotHandler
class that can respond to incoming messages from Space, and send responses back.
Edit the
appsettings.json
file in your project.Add a
Space
element to it. The file should look like this:You will have to make some updates to the values of elements:{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*", "Space": { "ServerUrl": "https://organization.jetbrains.space", "ClientId": "value-of-client-id", "ClientSecret": "value-of-client-secret", "VerifySigningKey": { "IsEnabled": false, "EndpointSigningKey": "value-of-endpoint-signing-key" }, "VerifyHttpBearerToken": { "IsEnabled": false, "BearerToken": "value-of-bearer-token}" }, "VerifyHttpBasicAuthentication": { "IsEnabled": false, "Username": "value-of-username", "Password": "value-of-password" }, "VerifyVerificationToken": { "IsEnabled": false, "EndpointVerificationToken": "value-of-endpoint-verification-token" } } }
ServerUrl
is the URL of your Space instance.ClientId
,ClientSecret
are the ID and secret we obtained during application registration.Set the
IsEnabled
option totrue
for the selected authentication method during application registration. Make sure to update other options as well, for example set theEndpointSigningKey
orEndpointVerificationToken
when needed.
tip
Typically, these values will be stored outside your application code base, for example using environment variables or the Secret Manager in ASP.NET Core. The .NET Core User Secrets plugin for JetBrains Rider can help you locate and edit these secrets on your machine.
Create the
RemindMeBotHandler
class in your project, and add the code:Note that theusing JetBrains.Space.AspNetCore.Experimental.WebHooks; namespace RemindMeBot; public class RemindMeBotHandler : SpaceWebHookHandler { }
RemindMeBotHandler
class inherits from the Space SDK'sSpaceWebHookHandler
class. It handles a number of things for us:It verifies the Space instance from which our bot receives a request, by taking the message payload and comparing the incoming token with the EndpointVerificationToken configuration value.
It ensures the message payload has not been tampered with, by using the EndpointSigningKey, and calculating the message signature.
Now, let's wire things up. We will update code in
Program.cs
, and register the application helpers for Space.Right after the line that reads
var builder = WebApplication.CreateBuilder(args);
, add the following code:// Space client API builder.Services.AddSingleton<Connection>(provider => new ClientCredentialsConnection( new Uri(builder.Configuration["Space:ServerUrl"]), builder.Configuration["Space:ClientId"], builder.Configuration["Space:ClientSecret"], provider.GetService<IHttpClientFactory>().CreateClient())); builder.Services.AddSpaceClientApi(); // Space webhook handler builder.Services.AddSpaceWebHookHandler<RemindMeBotHandler>(options => builder.Configuration.Bind("Space", options));
Some things to note:
AddHttpClient()
registers the .NETHttpClient
that is used by the Space SDK and makes it available as a service.AddSingleton<Connection>(...)
registers a connection to work with Space, using the configuration we created earlier.AddSpaceClientApi()
registers the Space API client and makes it available as a service. Later in this tutorial, we can make use of it to send a chat message to our bot's users.AddSpaceWebHookHandler<RemindMeBotHandler>(...)
registers aRemindMeBotHandler
class as a web hook handler. This class does not exist yet: it will contain the logic of the 'Remind me' bot, which we still have to implement.
Further in the
Program.cs
file, right after the line that readsvar app = builder.Build();
, add the following code:app.MapSpaceWebHookHandler<RemindMeBotHandler>("/api/myapp"); app.MapGet("/", () => "Space app is running.");
Things to note:
app.MapSpaceWebHookHandler<RemindMeBotHandler>
makes the 'Remind me' bot available on the web server, on the/api/myapp
path. This has to match the path that was entered during application registration.app.MapGet("/", ...
registers a default response that the server will return when someone accesses the 'Remind me' bot from their browser. It is good practice to add a default response like this to your Space bot.
Done! We have created a
RemindMeBotHandler
class where we can start adding a first command, and registered it using the Space SDK.
Let's start with something simple – the help
command that shows hints on how to use our chatbot.
Update the
RemindMeBotHandler
class, and overwrite its code with the following code:using System.Threading.Tasks; using JetBrains.Space.AspNetCore.Experimental.WebHooks; using JetBrains.Space.Client; namespace RemindMeBot; public class RemindMeBotHandler : SpaceWebHookHandler { private readonly ChatClient _chatClient; public RemindMeBotHandler(ChatClient chatClient) { _chatClient = chatClient; } public override async Task HandleMessageAsync(MessagePayload payload) { var messageText = payload.Message.Body as ChatMessageText; if (string.IsNullOrEmpty(messageText?.Text)) return; await HandleHelpAsync(payload); } private async Task HandleHelpAsync(MessagePayload payload) { await _chatClient.Messages.SendMessageAsync( recipient: MessageRecipient.Member(ProfileIdentifier.Id(payload.UserId)), content: ChatMessage.Text("Soon the help will be shown here!")); } }
Notes:
When the ASP.NET runtime creates the
RemindMeBotHandler
class, it injects aChatClient
in its constructor. TheChatClient
is stored in a private field, so we can use it later to send chat messages to our users.The
HandleMessageAsync(MessagePayload payload)
method is inherited from the Space SDK, and is called when our bot receives a message from Space. Later on, we will use this method to handle different commands. For now, theHandleHelpAsync
method is invoked whenever a message is received.note
The
SpaceWebHookHandler
base class has other methods that you can override, intended to handle the various message types that Space can send to your bot. It also responds to Space with the required200 OK
HTTP status code.We're now handling the
MessagePayload
, which is sent when a user types something in the chat and presses Enter. Later in this tutorial, we'll also handle theListCommandsPayload
payload, which a bot receives when a user presses the/
(slash) key.In the
HandleHelpAsync(MessagePayload payload)
method, we make use of the previously injectedChatClient
through the_chatClient
field.Using the
SendMessageAsync
method, you can send a message to any recipient in Space. Recipients can be chat channels, issue comments, direct messages, and more.We're using the
MessageRecipient.Member()
method to address a user using direct message, and then build theProfileIdentifier
using the ID of the user who invoked thehelp
command.The text of our chat message is created using the
ChatMessage.Text()
method. Messages can be more than just text. They can include complex formatting and even UI elements, like buttons. For now, text is sufficient.
Nice! Now, we have a command that a user can actually try in action.
Start the application by pressing Ctrl+F5 or using the Run | Run 'RemindMeBot' menu.
Open your Space instance and find the bot: press Ctrl+K and type its name.
We don't anyhow analyze the commands sent by the user. On any type of request, we respond with the message generated in
HandleHelpAsync
. So, to test our bot, type anything in the chat. You should receive the help message:It's working! Now, let's add the rest of the bot features.
Let's make our bot fully functional:
In order to be considered a chatbot, an application must be able to respond with a list of available commands when a user types
/
(slash) in the chat. In this case, the bot receives theListCommandsPayload
type of payload, which we can handle by overriding theHandleListCommandsAsync
method in our bot class.As our bot is called the 'Remind me' bot, it needs a
remind
command that will start the timer and send a notification to the user once the timer completes.
Let's start with listing the available commands. In the
RemindMeBotHandler
class, add the following code:public override async Task<Commands> HandleListCommandsAsync(ListCommandsPayload payload) { return new Commands(new List<CommandDetail> { new CommandDetail("help", "Show this help"), new CommandDetail("remind", "Remind me in N seconds, e.g., to remind in 10 seconds, send 'remind 10'") }); }
When a bot receives
ListCommandsPayload
, it can respond withCommands
that returns a list ofCommandDetail
. EachCommandDetail
has a commandname
, and adescription
that will be displayed in the help menu.Space sends
ListCommandsPayload
every time a user presses a key. If it's the/
(slash), Space will show the full list of commands. If it's some other key, Space will find and show only commands containing the key.GifNow that we can list commands, let's update our
HandleHelpAsync
method and respond with proper help information. Replace the code of theHandleHelpAsync
method:private async Task HandleHelpAsync(MessagePayload payload) { var commands = await HandleListCommandsAsync( new ListCommandsPayload { UserId = payload.UserId }); await _chatClient.Messages.SendMessageAsync( recipient: MessageRecipient.Member(ProfileIdentifier.Id(payload.UserId)), content: ChatMessage.Block( outline: new MessageOutline("Remind me bot help", new ApiIcon("smile")), sections: new List<MessageSectionElement> { MessageSectionElement.MessageSection( header: "List of available commands", elements: new List<MessageElement> { MessageElement.MessageFields( commands.CommandsItems .Select(it => MessageFieldElement.MessageField(it.Name, it.Description)) .ToList<MessageFieldElement>()) }) })); }
Here:
We're calling
HandleListCommandsAsync
to get the list of commands. For good measure, we're passing along the user ID as well, so that if we need it in that method, we can use it.The
ChatMessage.Text()
has been replaced withChatMessage.Block()
, so we can return a formatted help message.tip
In the API Playground, you can interactively construct a chat message, and preview what it will look like.
We can now check the incoming
MessagePayload
, and determine the intended command. If the text starts with"remind"
, we'll invoke theHandleRemindAsync
method.Update the
HandleMessageAsync
method:public override async Task HandleMessageAsync(MessagePayload payload) { var messageText = payload.Message.Body as ChatMessageText; if (string.IsNullOrEmpty(messageText?.Text)) return; if (messageText.Text.Trim().StartsWith("remind")) { await HandleRemindAsync(payload, messageText); return; } await HandleHelpAsync(payload); }
Next, add a new
HandleRemindAsync
method:private async Task HandleRemindAsync(MessagePayload payload, ChatMessageText messageText) { var arguments = messageText.Text.Split(' ', StringSplitOptions.TrimEntries); if (arguments.Length != 2 || !int.TryParse(arguments[1], out var delayInSeconds)) { // We're expecting 2 elements: "remind", "X" // If that's not the case, return help. await HandleHelpAsync(payload); return; } await _chatClient.Messages.SendMessageAsync( recipient: MessageRecipient.Member(ProfileIdentifier.Id(payload.UserId)), content: ChatMessage.Block( outline: new MessageOutline($"I will remind you in {delayInSeconds} seconds", new ApiIcon("smile")), sections: new List<MessageSectionElement>())); Task.Run(async () => { try { await Task.Delay(TimeSpan.FromSeconds(delayInSeconds)); await _chatClient.Messages.SendMessageAsync( recipient: MessageRecipient.Member(ProfileIdentifier.Id(payload.UserId)), content: ChatMessage.Block( outline: new MessageOutline($"Hey! {delayInSeconds} seconds are over!", new ApiIcon("smile")), sections: new List<MessageSectionElement>())); } catch (Exception) { // Since we're using Task.Run to run code outside of the // request context, we want to catch any Exception here // to prevent the server from crashing. } }); }
Here:
HandleRemindAsync
validates the command, and invokesHandleHelpAsync
when no valid command arguments are provided. The code checks if there are two elements in the string, and that the second element is a valid integer. When a user typesremind 10
, the second argument has to be an integer10
.A confirmation message is sent to the user.
Task.Run(...)
schedules a delay for the amount of time that was requested, and sends a message to the user when the delay is over.
Let's run the bot one more time and try all bot features:
Typing slash:
The
help
command:The
remind
command:
Great job! We have finished creating our simple bot!
Thanks for your feedback!