AsterNET: A Beginner’s Guide to Building .NET Telephony Apps

How to Integrate AsterNET with ASP.NET Core — Step‑by‑StepAsterNET is a .NET library that provides access to Asterisk PBX services (AMI, AGI, ARI). Integrating AsterNET with an ASP.NET Core application lets you control telephony functions (originate calls, monitor channels, manage conferences, react to events) from web APIs, dashboards, or background services. This guide walks through a complete, practical integration: project setup, connecting to AMI, handling events, sending commands, using background services, securing credentials, and basic production considerations.


Prerequisites

  • Asterisk PBX installed and configured with AMI enabled (Asterisk 13+ recommended).
  • Development machine with .NET SDK (6, 7, or later) and ASP.NET Core.
  • Basic knowledge of C#, dependency injection (DI), and ASP.NET Core background services.
  • AsterNET library (AsterNET.ARI or AsterNET.Manager via NuGet) — this guide focuses on AsterNET.Manager (AMI) for control and events.

1) Project setup

  1. Create an ASP.NET Core project (Web API template recommended):
dotnet new webapi -n AsterNetDemo cd AsterNetDemo 
  1. Add AsterNET.Manager NuGet package:
dotnet add package AsterNET.Manager 

(If you plan to use ARI features, add AsterNET.ARI instead or in addition. ARI is more modern for application-level call control; AMI is good for monitoring and issuing manager commands.)

  1. Add configuration for AMI connection in appsettings.json:
{   "Asterisk": {     "Host": "192.0.2.10",     "Port": 5038,     "Username": "amiuser",     "Password": "amipassword"   } } 

Replace Host/Port/Username/Password with your Asterisk AMI values. Use secrets or environment variables in production.


2) Create a typed configuration model

Create a POCO model to bind settings.

File: Models/AsteriskOptions.cs

namespace AsterNetDemo.Models {     public class AsteriskOptions     {         public string Host { get; set; } = "";         public int Port { get; set; } = 5038;         public string Username { get; set; } = "";         public string Password { get; set; } = "";     } } 

Register it in Program.cs (or Startup.cs):

builder.Services.Configure<AsteriskOptions>(builder.Configuration.GetSection("Asterisk")); 

3) Build a reusable AMI service

Encapsulate AMI connection and logic in a singleton service that manages connection lifecycle, events, and actions.

File: Services/AmiManagerService.cs

using AsterNET.Manager; using AsterNET.Manager.Event; using AsterNetDemo.Models; using Microsoft.Extensions.Options; namespace AsterNetDemo.Services {     public class AmiManagerService : IDisposable     {         private readonly ManagerConnection _manager;         private readonly AsteriskOptions _opts;         private readonly ILogger<AmiManagerService> _logger;         private bool _connected = false;         public AmiManagerService(IOptions<AsteriskOptions> options, ILogger<AmiManagerService> logger)         {             _opts = options.Value;             _logger = logger;             _manager = new ManagerConnection(_opts.Host, _opts.Port, _opts.Username, _opts.Password);             // Subscribe to events             _manager.UnhandledEvent += OnUnhandledEvent;             _manager.Registered += OnRegistered;             _manager.ConnectionStateChanged += OnConnectionStateChanged;         }         public void Connect()         {             if (_connected) return;             try             {                 _manager.LogLevel = ManagerLogLevel.All;                 _manager.Login(); // synchronous; alternatively use BeginLogin/EndLogin                 _connected = true;                 _logger.LogInformation("Connected to Asterisk AMI at {Host}:{Port}", _opts.Host, _opts.Port);             }             catch (Exception ex)             {                 _logger.LogError(ex, "AMI connection failed");                 throw;             }         }         public void Disconnect()         {             if (!_connected) return;             try             {                 _manager.Logoff();             }             catch (Exception ex)             {                 _logger.LogWarning(ex, "Error logging off");             }             finally             {                 _connected = false;             }         }         public void Dispose()         {             Disconnect();             _manager.UnhandledEvent -= OnUnhandledEvent;             _manager.Registered -= OnRegistered;             _manager.ConnectionStateChanged -= OnConnectionStateChanged;             _manager?.Dispose();         }         private void OnRegistered(object? sender, ManagerEvent e)         {             _logger.LogInformation("AMI registered: {Event}", e.GetType().Name);         }         private void OnConnectionStateChanged(object? sender, ManagerConnectionStateEventArgs e)         {             _logger.LogInformation("AMI state: {State}", e.State);         }         private void OnUnhandledEvent(object? sender, ManagerEvent e)         {             // Example: log call-related events; forward to other components as needed             _logger.LogDebug("AMI event: {Event}", e.GetType().Name);             // handle specific events by type             switch (e)             {                 case NewStateEvent nse:                     _logger.LogInformation("Channel {Channel} changed state to {State}", nse.Channel, nse.State);                     break;                 case DialEvent de:                     _logger.LogInformation("Dial from {Src} to {Dest} - SubEvent: {SubEvent}", de.Source, de.Destination, de.SubEvent);                     break;                 // add handling for other events you care about             }         }         // Example action: originate a call         public ManagerResponse OriginateCall(string channel, string exten, string context, int priority = 1, int timeout = 30000)         {             if (!_connected) throw new InvalidOperationException("AMI not connected");             var originate = new AsterNET.Manager.Action.OriginateAction             {                 Channel = channel,                 Exten = exten,                 Context = context,                 Priority = priority,                 Timeout = timeout,                 Async = true             };             return _manager.SendAction(originate);         }     } } 

Register the service as a singleton and ensure it starts on app start:

In Program.cs:

builder.Services.AddSingleton<AmiManagerService>(); builder.Services.AddHostedService<AmiStartupHostedService>(); 

Create a hosted service to connect on startup:

File: Services/AmiStartupHostedService.cs

using Microsoft.Extensions.Hosting; namespace AsterNetDemo.Services {     public class AmiStartupHostedService : IHostedService     {         private readonly AmiManagerService _ami;         public AmiStartupHostedService(AmiManagerService ami)         {             _ami = ami;         }         public Task StartAsync(CancellationToken cancellationToken)         {             _ami.Connect();             return Task.CompletedTask;         }         public Task StopAsync(CancellationToken cancellationToken)         {             _ami.Disconnect();             return Task.CompletedTask;         }     } } 

4) Expose actions via Web API

Create a controller to expose operations (e.g., originate a call, get status).

File: Controllers/AmiController.cs

using Microsoft.AspNetCore.Mvc; using AsterNetDemo.Services; [ApiController] [Route("api/ami")] public class AmiController : ControllerBase {     private readonly AmiManagerService _ami;     private readonly ILogger<AmiController> _logger;     public AmiController(AmiManagerService ami, ILogger<AmiController> logger)     {         _ami = ami;         _logger = logger;     }     [HttpPost("originate")]     public IActionResult Originate([FromBody] OriginateRequest req)     {         try         {             var res = _ami.OriginateCall(req.Channel, req.Exten, req.Context, req.Priority, req.Timeout);             return Ok(new { Success = true, Response = res.Response, Message = res.Message });         }         catch (Exception ex)         {             _logger.LogError(ex, "Originate failed");             return StatusCode(500, new { Success = false, Error = ex.Message });         }     }     public class OriginateRequest     {         public string Channel { get; set; } = "";         public string Exten { get; set; } = "";         public string Context { get; set; } = "from-internal";         public int Priority { get; set; } = 1;         public int Timeout { get; set; } = 30000;     } } 

Test with curl/Postman: curl -X POST https://localhost:5001/api/ami/originate -d ‘{“channel”:“SIP/100”,“exten”:“200”,“context”:“from-internal”}’ -H “Content-Type: application/json”


5) Handling AMI events robustly

  • Subscribe to specific events you need instead of only UnhandledEvent when possible. Use ManagerConnection’s specific event delegates (e.g., NewState, Dial, Hangup).
  • Offload heavy processing to background queues (Channel events can be high frequency). Use IHostedService or BackgroundService + Channels/Queues for processing.
  • Correlate events with call identifiers (Uniqueid, Linkedid) to track call lifecycle.

Example of registering a specific event handler:

_manager.NewState += (s, e) => {     // handle new state }; 

6) Using BackgroundService for continuous tasks

If you need to monitor events and perform periodic tasks, implement BackgroundService:

File: Services/CallEventProcessor.cs

using Microsoft.Extensions.Hosting; public class CallEventProcessor : BackgroundService {     private readonly AmiManagerService _ami;     private readonly ILogger<CallEventProcessor> _logger;     public CallEventProcessor(AmiManagerService ami, ILogger<CallEventProcessor> logger)     {         _ami = ami;         _logger = logger;     }     protected override Task ExecuteAsync(CancellationToken stoppingToken)     {         // Example: subscribe to events and push to an internal channel         _ami.ManagerConnection.NewState += OnNewState;         return Task.CompletedTask;     }     private void OnNewState(object? sender, AsterNET.Manager.Event.NewStateEvent e)     {         _logger.LogInformation("NewState: {Channel} {State}", e.Channel, e.State);         // enqueue for processing     }     public override Task StopAsync(CancellationToken cancellationToken)     {         // detach handlers if needed         return base.StopAsync(cancellationToken);     } } 

Note: In the above sample you’d need to expose ManagerConnection or event registration APIs from AmiManagerService; prefer small APIs on AmiManagerService to register callbacks rather than exposing internal connection object.


7) Security and secrets

  • Never store AMI credentials in source code. Use user secrets, environment variables, or a secrets manager.
  • Use least privilege: create AMI user with only necessary privileges in manager.conf or via Asterisk’s allowed actions.
  • If exposing APIs that trigger telephony actions, require proper authentication/authorization (JWT, OAuth, API keys). Rate-limit sensitive operations.
  • Secure network access: restrict AMI to trusted hosts or private networks and use firewall rules.

8) Deployment and production considerations

  • Run AMI connection in a resilient way: automatic reconnects, backoff on failure, and health checks. ManagerConnection has Login/Logoff and reconnection utilities; implement retry/backoff around Connect.
  • Monitor metrics: event rates, connection state, latency for originate actions, and failures.
  • Consider using ARI (AsterNET.ARI) if you need application-level control (bridge, channels, media handling). ARI uses HTTP/WebSocket and is better suited for complex call-control apps. Use AMI for monitoring and issuing manager commands when appropriate.
  • Use containerization carefully: ensure network and port mapping allow connectivity to Asterisk and RNAT/ports for RTP (if media flows through app).

9) Example: Originate and track call lifecycle

  1. Call originate with Async=true and capture Response/ActionID to correlate.
  2. Listen for NewchannelEvent, DialEvent, BridgeEvent, HangupEvent and match by UniqueID or ActionID.
  3. Update application state (database) accordingly.

Pseudocode flow:

  • Send OriginateAction with ActionID = GUID.
  • On NewchannelEvent: match ActionID/UniqueID → create call record.
  • On DialEvent/BridgeEvent: update status to ringing/answered.
  • On HangupEvent: mark call finished and store duration/reason.

10) Troubleshooting tips

  • If Login fails: check host/port, credentials, manager.conf permissions, and firewall. Use telnet host 5038 to test connectivity.
  • If no events arrive: ensure “event” permissions in AMI user and proper event filters. Check Asterisk logger for AMI messages.
  • For high event volume: profile your handlers and offload heavy work to background queues.

Sample repo layout

  • AsterNetDemo/
    • Controllers/
      • AmiController.cs
    • Models/
      • AsteriskOptions.cs
    • Services/
      • AmiManagerService.cs
      • AmiStartupHostedService.cs
      • CallEventProcessor.cs
    • Program.cs
    • appsettings.json

Conclusion

Integrating AsterNET with ASP.NET Core gives you powerful programmatic control over Asterisk from web apps and services. Key steps: configure AMI securely, encapsulate connection logic in a singleton service, handle events efficiently, expose safe APIs, and build resilience for production. For advanced call-control and media manipulation, consider ARI (AsterNET.ARI) instead of AMI.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *