Writing your first IceRPC + Slice server in C#

This tutorial is the first part of a two part series that shows how to create a complete application with IceRPC for C#. We start from scratch—you just need to have the .NET 8 SDK installed on your computer.

The networked application we are building together consists of:

  • a server that hosts a single service (a simple greeter service)
  • a client that establishes a connection to the server and calls greet on this greeter service

The client and server are console applications that use plain .NET (no ASP.NET, no Dependency Injection).

The first part of this tutorial shows how to create the server. The second part shows how to create the client.

Let's jump right in:

shell
dotnet new install IceRpc.Templates
shell
dotnet new icerpc-slice-server -o MySliceServer

This command creates a new IceRPC server application in directory MySliceServer.

MySliceServer in Visual Studio Code

Let's examine each file:

This file holds the contract between our client and server applications, specified with the Slice language.

It's a simple greeter:

slice
[cs::namespace("MySliceServer")]
module VisitorCenter
/// Represents a simple greeter.
interface Greeter {
/// Creates a personalized greeting.
/// @param name: The name of the person to greet.
/// @returns: The greeting.
greet(name: string) -> string
}

The cs::namespace attribute instructs the Slice compiler to map module VisitorCenter to C# namespace MySliceServer (our project name) instead of the default (VisitorCenter).

If you use this code as the starting point for a new application, you should update this interface to represent something meaningful for your application. For this tutorial, we just keep Greeter as-is.

Class Chatbot is a service that implements Slice interface Greeter:

C#
[SliceService]
internal partial class Chatbot : IGreeterService
{
public ValueTask<string> GreetAsync(
string name,
IFeatureCollection features,
CancellationToken cancellationToken)
{
Console.WriteLine($"Dispatching greet request {{ name = '{name}' }}");
return new($"Hello, {name}!");
}
}

The Slice compiler generates C# interface IGreeterService from Slice interface Greeter. This C# interface is a template: we implement Greeter by implementing this interface. As you can see above, the greet operation becomes a GreetAsync method with two additional parameters.

Since we always fulfill greet synchronously, the GreetAsync implementation is not marked async. We could write the return statement as:

C#
return new ValueTask<string>($"Hello, {name}!");

However, it's more convenient to omit the type name, especially when this type is complicated.

We mark class Chatbot as partial because the SliceService attribute instructs the Slice Service source generator to implement interface IDispatcher—in other words, make Chatbot an IceRPC service implementation.

The main program starts by creating and configuring a Router:

C#
// Create a simple console logger factory and configure the log level for category IceRpc.
using ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
builder
.AddSimpleConsole()
.AddFilter("IceRpc", LogLevel.Debug));
Router router = new Router()
.UseLogger(loggerFactory)
.UseDeadline()
.Map<IGreeterService>(new Chatbot());

This router corresponds to our dispatch pipeline: when we receive a request, we first give it to the Logger middleware, then to the Deadline middleware and finally we route this request based on its path.

The Map call means if the request's path is the default service path for Slice interface Greeter, route it to the Chatbot instance. Otherwise, the router returns a response with status code NotFound.

The default service path for Greeter is /VisitorCenter.Greeter (it uses the Slice module name and interface name). The Map call above is a shortcut for:

C#
.Map("/VisitorCenter.Greeter", new Chatbot());

The main program then creates a Server that directs all incoming requests to router:

C#
await using var server = new Server(
dispatcher: router,
serverAuthenticationOptions: null,
logger: loggerFactory.CreateLogger<Server>());

We don't specify a server address so this server uses the default server address (icerpc://[::0]). This means the new server uses the icerpc protocol and will listen for connections on all network interfaces with the default port for icerpc (4062).

We don't specify a transport either so we use the default multiplexed transport (tcp). The null serverAuthenticationOptions means this server will accept plain TCP connections—it's a simple, non-secure server.

At this point, the server is created but is not doing anything yet. A client attempting to connect would get a "connection refused" error.

The server starts listening on the next line:

C#
server.Listen();

Listen returns as soon as the server is listening (so almost immediately).

The main program then awaits until it receives a Ctrl+C. After it receives Ctrl+C, it shuts down the server gracefully:

C#
await CancelKeyPressed;
await server.ShutdownAsync();

This file contains a few lines of code that Programs.cs uses to wait for Ctrl+C. It's not related to RPCs.

The project file is straightforward. It contains references to the required IceRpc NuGet packages:

shell
cd MySliceServer
dotnet run

The server is now listening for new connections from clients:

dbug: IceRpc.Server[11]
Listener 'icerpc://[::0]?transport=tcp' has started accepting connections

Press Ctrl+C on the server console to shut it down.

dbug: IceRpc.Server[12]
Listener 'icerpc://[::0]?transport=tcp' has stopped accepting connections

Was this page helpful?