Anatomy of an operation
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:
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:
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 aboveop( x: int32 y: int32) -> int32
opNoReturn() // returns nothing (no ->)
opReturnString() -> string // returns a string
opReturnPair() -> (x: int32, y: int32) // returns two return parameters
Tagged parameters
The operation parameters and return parameters can include tagged parameters. For example:
// 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:
// returns a tagged stringop() -> tag(1) string?
Idempotent operation
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:
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.
Operation-specific attributes
compress attribute
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:
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.
oneway attribute
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:
interface Logger { [oneway] logMessage(message: string)}
C# mapping This section is specific to the IceRPC + Slice integration.
Learn more
This section is specific to the IceRPC + Slice integration.
Learn more
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:
module VisitorCenter
// An interface with a single operation.interface Greeter { greet(name: string) -> string}
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
orTask<T>
while the service method returns aValueTask
orValueTask<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
Request and Response helper classes
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:
module VisitorCenter
interface Greeter { greet(name: string) -> string}
produces 4 helper methods:
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.
cs::encodedReturn attribute
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:
- 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. - 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.