Interface

Learn how to define interfaces in Slice.

The ultimate goal of the Slice language is to define operations and their enclosing scopes, interfaces. All other Slice constructs and statements merely support the definition of interfaces.

An interface specifies an abstraction—a group of abstract operations. A client application calls these operations while a server application hosts a service that implements this interface.

For example:

slice
module VisitorCenter
// An interface with a single operation.
interface Greeter {
greet(name: string) -> string
}

All operations are abstract at the Slice level: you can't implement an operation in Slice.

When you create an application with Slice, these interfaces are your entry point into Slice: the Slice code generators generate code from your Slice interfaces, and you interact with this generated code in both your client and server applications. On the client side, you call generated proxies (concrete classes) that send requests to remote services. On the server side, you implement services using templates (abstract interfaces) generated from the Slice interfaces.

An interface is a Slice construct but not a Slice type. This means you cannot use an interface as the type of a Slice field or operation parameter.

An interface can inherit from one or more interfaces, provided the operation names of all these interfaces are unique.

For example:

slice
module Draw
interface Shape {
rotate(degrees: int16)
}
interface Fillable {
idempotent setFillColor(newColor: Color)
}
interface Rectangle : Shape, Fillable {
idempotent resize(x: int32, y: int32)
}

The code generator for C# creates two C# interfaces (IName and INameService) and one C# record struct (NameProxy) from Slice interface Name. The identifiers of the generated interfaces and proxy structs are always in Pascal case, per the usual C# naming conventions, even when Name is not in Pascal case.

The attribute cs::identifier allows you to remap Name to an identifier of your choice.

Interface IName provides the client-side API that allows you to call the remote service that implements the associated Slice interface. It's a minimal interface with an abstract method for each operation defined in this interface.

For example:

slice
module Example
interface Widget {
spin(speed: int32)
}
C#
namespace Example;
internal partial interface IWidget
{
// One method per operation
Task SpinAsync(
int speed,
IFeatureCollection? features = null,
CancellationToken cancellationToken =
default);
}

Slice interface inheritance naturally maps to interface inheritance in C#. For example:

slice
module Draw
interface Rectangle : Shape, Fillable {
idempotent resize(x: int32, y: int32)
}

maps to:

C#
namespace Draw;
internal partial interface IRectangle : IShape, IFillable
{
Task ResizeAsync(
int x,
int y,
IFeatureCollection? features = null,
CancellationToken cancellationToken = default);
}

The generated record struct NameProxy implements IName by sending requests to a remote service with IceRPC.

An instance of this struct is a local surrogate for the remote service that implements Name—in other words, a proxy for this service.

In order to call a remote service, you need to construct a proxy struct using one of its "invoker" constructors:

C#
internal readonly partial record struct WidgetProxy : IWidget, IProxy
{
internal WidgetProxy(
IInvoker invoker,
ServiceAddress? serviceAddress = null,
SliceEncodeOptions? encodeOptions = null)
{
...
}
internal WidgetProxy(
IInvoker invoker,
System.Uri serviceAddressUri,
SliceEncodeOptions? encodeOptions = null)
: this(invoker, new ServiceAddress(serviceAddressUri), encodeOptions)
{
}
}

The invoker parameter represents your invocation pipeline, the serviceAddress or serviceAddressUri parameter corresponds to the address of the remote service, and the encodeOptions parameter allows you to customize the Slice encoding of operation parameters. See SliceEncodeOptions for details.

A null service address is equivalent to an icerpc service address with the default service path of the associated Slice interface.

The default service path of a Slice interface is / followed by its fully qualified name with :: replaced by .. For example, the default service path of Slice interface VisitorCenter::Greeter is /VisitorCenter.Greeter. It's also available as the constant DefaultServicePath in the generated proxy struct:

C#
internal readonly partial record struct WidgetProxy : IWidget, IProxy
{
internal const string DefaultServicePath = "/Example/Widget";
}

If you want to create a relative proxy, call the FromPath static method:

C#
internal readonly partial record struct WidgetProxy : IWidget, IProxy
{
internal static WidgetProxy FromPath(string path) { ... }
}

For example, if you want to create a relative proxy with the default service path, call:

C#
// Creates a relative proxy with the default service path and an invalid invoker.
var relativeProxy = WidgetProxy.FromPath(WidgetProxy.DefaultServicePath);

The generated proxy struct also provides a parameterless constructor that initializes the proxy's service address to an icerpc service address with the default service path. If you call this constructor directly, you also need to initialize the invoker, for example:

C#
// Calls WidgetProxy's parameterless constructor
var proxy = new WidgetProxy { Invoker = connection };
// The above is equivalent to:
var proxy = new WidgetPRoxy(connection);

When a Slice interface derives from another interface, its proxy struct provides an implicit conversion operator to be base interface. For example:

slice
module Draw
interface Rectangle : Shape, Fillable {
idempotent resize(x: int32, y: int32)
}

maps to:

C#
namespace Draw;
internal readonly partial record struct RectangleProxy : IRectangle, IProxy
{
internal static implicit operator ShapeProxy(RectangleProxy proxy)
{
...
}
internal static implicit operator FillableProxy(RectangleProxy proxy)
{
...
}
}

This way, you can pass a "derived" proxy to a method that expects a "base" proxy, even though there is naturally no inheritance relationship between these proxy structs.

Interface INameService is a server-side helper: it helps you create a service (a C# class) that implements Slice interface Name.

The principle is straightforward: your service class must be a partial class that implements INameService. It must also carry the Service attribute.

The Service attribute instructs the Service Generator (a C# source generator) to implement interface IDispatcher by directing incoming requests to INameService methods based on the operation names.

For example:

slice
module Example
interface Widget {
spin(speed: int32)
}
C#
namespace Example;
// Generated code
internal partial interface IWidgetService
{
// One method per operation
ValueTask SpinAsync(
int speed,
IFeatureCollection features,
CancellationToken cancellationToken);
}
// Application code
[Service]
internal partial class MyWidget : IWidgetService
{
// implement SpinAsync ...
}

Even though INameService is an interface, it's not used as an abstraction: you shouldn't make calls to this interface or create decorators for this interface. It's just a model that your service class must implement.

Note that the same service class can implement any number of Slice interfaces provided their operations have unique names. For example:

slice
module Example
interface Widget {
spin(speed: int32)
}
interface Counter {
getCount() -> int32
}
C#
// Implements two Slice interfaces
[Service]
internal partial class MyWidget : IWidgetService,
ICounterService
{
// implements SpinAsync and GetCountAsync.
}

Was this page helpful?

CookiesYour privacy
This website uses cookies to analyze traffic and improve your experience.
By clicking "Accept," you consent to the use of these cookies. You can learn more about our cookies policy in our Privacy Policy.