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 correspond to your entry point into Slice: you call and implement the C# (or Rust, Python...) abstractions and concrete implementations that the Slice compiler generates from your 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 Slice compiler for C# compiles Slice interface Name into two C# interfaces (IName and INameService) and one C# struct (NameProxy). The identifiers of the generated interfaces and struct 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;
public 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;
public 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#
public readonly partial record struct WidgetProxy : IWidget, IProxy
{
public WidgetProxy(
IInvoker invoker,
ServiceAddress? serviceAddress = null,
SliceEncodeOptions? encodeOptions = null)
{
...
}
public 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#
public readonly partial record struct WidgetProxy : IWidget, IProxy
{
public const string DefaultServicePath = "/Example/Widget";
}

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;
public readonly partial record struct RectangleProxy : IRectangle, IProxy
{
public static implicit operator ShapeProxy(RectangleProxy proxy)
{
...
}
public 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 SliceService attribute.

The SliceService attribute instructs the Slice Service 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
public partial interface IWidgetService
{
// One method per operation
ValueTask SpinAsync(
int speed,
IFeatureCollection features,
CancellationToken cancellationToken);
}
// Application code
[SliceService]
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
[SliceService]
internal partial class MyWidget : IWidgetService,
ICounterService
{
// implements SpinAsync and GetCountAsync.
}

Was this page helpful?