The i in IDL
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:
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.
Interface inheritance
An interface can inherit from one or more interfaces, provided the operation names of all these interfaces are unique.
For example:
module Draw
interface Shape { rotate(degrees: int16)}
interface Fillable { idempotent setFillColor(newColor: Color)}
interface Rectangle : Shape, Fillable { idempotent resize(x: int32, y: int32)}
C# mapping This section is specific to the IceRPC + Slice integration.
Learn more
This section is specific to the IceRPC + Slice integration.
Learn more
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.
IName
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:
module Example
interface Widget { spin(speed: int32)}
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:
module Draw
interface Rectangle : Shape, Fillable { idempotent resize(x: int32, y: int32)}
maps to:
namespace Draw;
public partial interface IRectangle : IShape, IFillable{ Task ResizeAsync( int x, int y, IFeatureCollection? features = null, CancellationToken cancellationToken = default);}
NameProxy
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:
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:
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:
// Calls WidgetProxy's parameterless constructorvar 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:
module Draw
interface Rectangle : Shape, Fillable { idempotent resize(x: int32, y: int32)}
maps to:
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.
INameService
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:
module Example
interface Widget { spin(speed: int32)}
namespace Example;
// Generated codepublic 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:
module Example
interface Widget { spin(speed: int32)}
interface Counter { getCount() -> int32}
// Implements two Slice interfaces[SliceService]internal partial class MyWidget : IWidgetService, ICounterService{ // implements SpinAsync and GetCountAsync.}