Operation

Learn how to define operations in Slice.

An operation consists of:

  • operation attributes (optional)
  • a name (the name of the operation)
  • a list of parameters (the operation parameters)
  • an arrow followed by one or more return parameters (optional)

For example:

slice
greet(name: string) -> string

Operation greet has a name (greet), an operation parameter (name), and a nameless return parameter.

Here are a few additional examples:

slice
op() // no operation parameter
[oneway] op(x: int32) // an attribute and a single operation parameter
op(x: int32, y: int32) -> int32 // two operation parameters and a return parameter
// Another way to write the operation above
op(
x: int32
y: int32
) -> int32
opNoReturn() // returns nothing (no ->)
opReturnString() -> string // returns a string
opReturnPair() -> (x: int32, y: int32) // returns two return parameters

The operation parameters and return parameters can include tagged parameters. For example:

slice
// An operation with many tagged parameters.
op(
tag(5) x: int64?
is: string
) -> (
tag(5) x: int32?
y: int32?
tag(1) s: string?
)

A single nameless return parameter can also get tagged. For example:

slice
// returns a tagged string
op() -> tag(1) string?

An operation can be marked as idempotent, which means calling this operation several times with the same arguments is semantically equivalent to calling this operation once.

For example:

slice
idempotent setTemperature(newValue: float64)

Setting the temperature over-and-over to the same value is like setting it once.

The idempotent keyword ensures that both the caller and the implementation of the service have compatible understandings of the idempotent-ness of this operation. If the caller believes an operation is idempotent, the service implementation must see and treat this operation as idempotent too.

The compress attribute instructs the generated code to request compression when sending the arguments or return value of the operation. Its argument can be Args, Return, or both.

For example:

slice
interface Greeter {
// Request compression in both directions.
[compress(Args, Return)] greet(name: string) -> string
}

In C# with IceRPC, the generated code sets the ICompressFeature in the outgoing request features.

This compression request is typically fulfilled by the compressor interceptor or middleware, which needs to be installed in your invocation resp. dispatch pipeline. If you neglect to install this interceptor or middleware, the corresponding payloads are not compressed.

Keep in mind the compressor interceptor and middleware require the icerpc protocol. They do nothing for ice invocations and dispatches.

The oneway attribute instructs the generated code to create one-way outgoing requests for this operation. It has no effect on the server-side generated code (the INameService interface).

A one-way request is a "fire and forget" request: the request is considered successful as soon as it's sent successfully.

For example:

slice
interface Logger {
[oneway] logMessage(message: string)
}

A Slice operation named opName in interface Greeter is mapped to abstract method OpNameAsync in the interface IGreeter and to abstract method OpNameAsync in interface IGreeterService.

The mapped method name is always in Pascal case, per the usual C# naming conventions, even when opName is not in Pascal case.

For example:

slice
module VisitorCenter
// An interface with a single operation.
interface Greeter {
greet(name: string) -> string
}
C#
namespace VisitorCenter;
public partial interface IGreeter
{
Task<string> GreetAsync(
string name,
IFeatureCollection? features = null,
CancellationToken cancellationToken = default);
}
public partial interface IGreeterService
{
ValueTask<string> GreetAsync(
string name,
IFeatureCollection features,
CancellationToken cancellationToken);
}

While the two methods are similar, please note they are not the same:

  • the client-side method returns a Task or Task<T> while the service method returns a ValueTask or ValueTask<T>
  • the features parameter is nullable and defaults to null only in the client-side method
  • the cancellation token parameter has a default value only in the client-side method

The Slice compiler generates helper nested static classes named Request and Response in NameProxy and INameService. These nested classes provide helper methods to encode and decode the payloads of requests and responses associated with the interface operations, with up to 4 helper methods per operation.

For example:

slice
module VisitorCenter
interface Greeter {
greet(name: string) -> string
}

produces 4 helper methods:

C#
public readonly partial record struct GreeterProxy : IGreeter, IProxy
{
public static class Request
{
// Encodes the name argument into a request payload (a PipeReader).
public static PipeReader EncodeGreet(string name, SliceEncodeOptions? encodeOptions = null)
{
...
}
}
public static class Response
{
// Decodes the response payload into a string (the greeting).
public static ValueTask<string> DecodeGreetAsync(
IncomingResponse response,
OutgoingRequest request,
IProxy sender,
CancellationToken cancellationToken)
{
...
}
}
}
public partial interface IGreeterService
{
public static class Request
{
// Decodes the name argument from the request payload.
public static ValueTask<string> DecodeGreetAsync(
IncomingRequest request,
CancellationToken cancellationToken)
{
...
}
}
public static class Response
{
// Encodes the greeting return value into a response payload.
public static PipeReader EncodeGreet(string returnValue, SliceEncodeOptions? encodeOptions = null)
{
...
}
}
}

These helper methods allow you to create/consume plain IceRPC requests and responses while still using the generated code for their payloads.

The cs::encodeReturn attribute allows you to change the return type of the mapped method on the generated Service interface: this attribute makes this method returns a ValueTask<PipeReader> instead of the usual ValueTask<T>.

The returned PipeReader represents the encoded return value. You would typically produce this value using the EncodeOpName method provided by the helper Response class.

There are two somewhat common use-cases for this attribute:

  1. You want to encode a mutable collection field of your class (such as List<T>) while holding a mutex lock; this lock prevents other operations from modifying this field while it's being encoded.
  2. You want to return over and over the same return value that is costly to encode; this attribute allows you to encode the return value once, cache the encoded bytes and then return over and over these bytes.

Was this page helpful?