# C# runtime **It's ASP.NET MVC with controllers.** You extend a base controller, override the methods you care about, and dependency injection wires up services and SDK clients. If you already know ASP.NET, skim this page and you are basically done. Package: `Sinch.Functions.Runtime` (NuGet) Target framework: `.NET 9+` Namespace: `SinchFunctions.Utils` ## Project structure ``` MyFunction/ ├── MyFunction.csproj ← references Sinch.Functions.Runtime ├── FunctionController.cs ← voice callbacks (extends SinchVoiceController) ├── Init.cs ← optional: ISinchFunctionInit for DI and extra routes ├── appsettings.json ← config (variables, not secrets) ├── sinch.json ← project manifest (name, runtime, variables) ├── assets/ ← private files └── public/ ← static files, served at / ``` There is no `Program.cs`. The runtime discovers your `ISinchFunctionInit` and controllers automatically and boots the ASP.NET pipeline for you. ## Hello world ```csharp // FunctionController.cs using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using SinchFunctions.Models; using SinchFunctions.Utils; public class FunctionController : SinchVoiceController { public FunctionController( FunctionContext context, IConfiguration configuration, ILogger logger) : base(context, configuration, logger) { } public override async Task Ice(IceCallbackModel data) { return Ok(new IceSvamletBuilder() .Instructions.Say("Hello from Sinch Functions!") .Action.Hangup() .Build()); } public override Task Pie(PieCallbackModel data) => Task.FromResult(Ok()); public override Task Ace(AceCallbackModel data) => Task.FromResult(Ok()); public override Task Dice(DiceCallbackModel data) => Task.FromResult(Ok()); } ``` That's a complete, deployable voice function. All four abstract methods must be implemented; stub the ones you do not need with `Task.FromResult(Ok())`. ## The four base controllers Extend whichever matches your use case: | Base class | Use for | | --- | --- | | `SinchVoiceController` | Voice callbacks — override `Ice`, `Ace`, `Pie`, `Dice`. | | `SinchConversationController` | Conversation API webhooks (SMS, WhatsApp, Messenger, etc.). | | `ElevenLabsController` | ElevenLabs AI voice agent webhooks. | | `SinchController` | Plain ASP.NET controllers — your own REST endpoints. | You can have multiple controllers in one project — a `FunctionController : SinchVoiceController` and a `StatusController : SinchController` side-by-side. ## Custom HTTP endpoints For REST endpoints alongside your voice logic, extend `SinchController` and write a regular MVC controller: ```csharp [Route("api")] public class MyController : SinchController { public MyController(FunctionContext context, IConfiguration config, ILogger logger) : base(context, config, logger) { } [HttpGet("status")] public IActionResult GetStatus() => Ok(new { status = "healthy" }); [HttpPost("users")] public async Task CreateUser([FromBody] CreateUserRequest req) { await Context.Cache.Set($"user:{req.Id}", req, 3600); return Ok(req); } } ``` `Context`, `Configuration`, and `Logger` are all available on the base class. See [add a custom endpoint](#) for a worked example. ## Conversation webhooks ```csharp public class MyConversationController : SinchConversationController { public override async Task MessageInbound(MessageInboundEvent callback) { var text = callback.GetText(); var channel = callback.GetChannel(); // "SMS", "WHATSAPP", etc. if (text == "hello") { var reply = Reply(callback, "Hi there!"); await Context.Conversation!.Messages.Send(reply); } return Ok(); } } ``` Helper extensions on `MessageInboundEvent`: ```csharp using SinchFunctions.Utils; callback.GetText(); // string? callback.GetMedia(); // media message callback.GetChannel(); // "SMS", "WHATSAPP", etc. callback.GetContactId(); callback.GetConversationId(); callback.GetIdentity(); // sender's phone/PSID callback.GetTo(); // your Sinch number callback.IsTextMessage(); callback.IsMediaMessage(); callback.IsPostback(); ``` `Reply(inbound, text)` is a base-class helper that fills in the recipient and app_id from the inbound event so you don't have to. ## Dependency injection via `ISinchFunctionInit` Implement `ISinchFunctionInit` in `Init.cs` to register your own services, add HTTP clients, and map extra routes. The runtime discovers it automatically — no `Program.cs` plumbing. ```csharp public class FunctionInit : ISinchFunctionInit { public void ConfigureServices(IServiceCollection services, IConfiguration configuration) { services.AddScoped(); services.AddHttpClient(client => { client.BaseAddress = new Uri(configuration["MY_API_URL"] ?? ""); }); } public void ConfigureApp(SinchWebApplication app) { app.LandingPageEnabled = true; app.MapGet("/custom", () => Results.Ok(new { status = "ok" })); } } ``` Your middleware runs **after** the platform middleware, so billing and logging cannot be bypassed. ## The context object `FunctionContext` is injected into every controller constructor. It has cache, storage, database, logger, and pre-wired SDK clients: ```csharp public override async Task Ice(IceCallbackModel data) { Logger.LogInformation("ICE from {Cli}", data.Cli); await Context.Cache.Set($"call:{data.CallId}:cli", data.Cli ?? "unknown", 3600); // Pre-wired SDK clients if (Context.Sms is not null) { await Context.Sms.Batches.Send(...); } return Ok(new IceSvamletBuilder() .Instructions.Say("Welcome.") .Action.Hangup() .Build()); } ``` See [context object](#) for the full tour, or [function context reference](/docs/functions/reference/function-context) for the Node + C# side-by-side cheat sheet. ## SVAML builders The pattern is always: `Instructions.*` → `Action.*` → `Build()`. ```csharp // ICE return Ok(new IceSvamletBuilder() .Instructions.Say("Welcome!") .Action.ConnectPstn("+15551234567", cli: data.Cli) .Build()); // ACE — continue or hangup only return Ok(new AceSvamletBuilder().Action.Continue().Build()); // PIE — respond to menu result return Ok(new PieSvamletBuilder() .Instructions.Say("Connecting you to sales.") .Action.ConnectPstn("+15551111111") .Build()); ``` See [SVAML cheat sheet](/docs/functions/reference/svaml-cheatsheet) for the most-used actions and [voice callbacks](#) for the lifecycle. ## Configuration and secrets `IConfiguration` is injected into your controllers by ASP.NET — no special API needed: ```csharp var companyName = Configuration["COMPANY_NAME"] ?? "Acme Corp"; var apiKey = Configuration["STRIPE_SECRET_KEY"] ?? throw new InvalidOperationException("STRIPE_SECRET_KEY is required"); ``` `IConfiguration` reads from environment variables (populated by the runtime from `sinch.json` variables + platform secrets) and from `appsettings.json` / `appsettings.Development.json` if you have them. See [configuration & secrets](#) for how the layers fit together. ## Database `Context.Database.ConnectionString` gives you a SQLite connection string. Bring your own SQLite library: ```csharp using Microsoft.Data.Sqlite; using var conn = new SqliteConnection(Context.Database.ConnectionString); conn.Open(); using var cmd = conn.CreateCommand(); cmd.CommandText = "CREATE TABLE IF NOT EXISTS call_log (id INTEGER PRIMARY KEY, caller TEXT, ts INTEGER)"; cmd.ExecuteNonQuery(); ``` Or with Dapper: ```csharp using var conn = new SqliteConnection(Context.Database.ConnectionString); var calls = await conn.QueryAsync("SELECT * FROM call_log ORDER BY ts DESC LIMIT 10"); ``` In production, the platform automatically replicates and backs up the database. Your code doesn't change. ## Do and don't - **Do** implement all four abstract methods on `SinchVoiceController`, even if some just return `Ok()`. - **Do** use builder `.Build()` for SVAML responses — never return raw anonymous objects. - **Do** register your own services via `ISinchFunctionInit.ConfigureServices`, not by hand-rolling a `Program.cs`. - **Don't** override `HandleWebhook` on `SinchVoiceController` — the base class handles routing. - **Don't** use `AceSvamletBuilder` to connect calls — ACE only supports `Continue` and `Hangup`. - **Don't** return `null` from any callback method — always return `Ok(...)`. ## Related - [Handlers concept](#) — URL-to-controller mapping in detail - [Context object concept](#) — cache/storage/database/SDK clients - [Function context reference](/docs/functions/reference/function-context) — side-by-side cheat sheet - [SVAML cheat sheet](/docs/functions/reference/svaml-cheatsheet) — most-used actions - [Node.js runtime](#) — the same thing for Node devs