<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jeremy</title>
    <description>The latest articles on DEV Community by Jeremy (@jeremy_tly).</description>
    <link>https://dev.to/jeremy_tly</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2017768%2Fb75b9084-48e9-492c-beb2-0fbccaa181b1.png</url>
      <title>DEV Community: Jeremy</title>
      <link>https://dev.to/jeremy_tly</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jeremy_tly"/>
    <language>en</language>
    <item>
      <title>Part 5: After the Dispatch - Handling Delivery Events</title>
      <dc:creator>Jeremy</dc:creator>
      <pubDate>Thu, 22 Oct 2026 23:00:00 +0000</pubDate>
      <link>https://dev.to/jeremy_tly/part-5-after-the-dispatch-handling-delivery-events-2o9a</link>
      <guid>https://dev.to/jeremy_tly/part-5-after-the-dispatch-handling-delivery-events-2o9a</guid>
      <description>&lt;p&gt;Part 4 ended on one word in the Communications logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Email:Simulation.Email:Dispatched] ...
[Sms:Simulation.Sms:Dispatched] ...
[Push:Simulation.Push:Dispatched] ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Dispatched&lt;/code&gt; means a provider accepted the message. It doesn't mean anyone received it.&lt;/p&gt;

&lt;p&gt;With the simulator the two happen together. With a real provider they don't. Twilio accepts an SMS and returns a message id straight away, then reports back as the message moves along: queued, sent, delivered, or undelivered because the handset was unreachable. Email providers report bounces and opens. Push services report invalid tokens.&lt;/p&gt;

&lt;p&gt;Those updates are worth keeping. Support wants to know whether the customer really got the order confirmation. Engineering wants to know which provider is failing. And customers like being able to see what a business has sent them.&lt;/p&gt;

&lt;p&gt;This article captures those delivery events in Communications, stores them, and puts them in front of the buyer on a new Messages page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ordering still doesn't change
&lt;/h2&gt;

&lt;p&gt;As in Part 4, nothing in this article touches Ordering. It dispatches &lt;code&gt;OrderCreated&lt;/code&gt; exactly as it did in Part 3. Delivery events describe what happened to a communication after Ordering stated its intent, and that's Communications' business.&lt;/p&gt;

&lt;h2&gt;
  
  
  One message per buyer
&lt;/h2&gt;

&lt;p&gt;Part 4 used the any-match strategy so we could watch all three channels go out at once. For an order confirmation, that's three messages where one would do. Part 5 goes back to Transmitly's default, first match: each buyer gets one message, on the first channel in the pipeline that can reach them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderCreated
    |
    +-- Email  verified email address?  deliver and stop
    |
    +-- SMS    verified phone number?   deliver and stop
    |
    +-- Push   registered device?       deliver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which channel that is depends on the buyer. Communications now only uses addresses the buyer has verified. &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%205/src/Communications.API/IdentityServerCustomerIdentityResolver.cs" rel="noopener noreferrer"&gt;The resolver&lt;/a&gt; drops the rest before any channel sees them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;addresses&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Addresses&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsVerified&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;eShop's two &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%205/src/Identity.API/UsersSeed.cs" rel="noopener noreferrer"&gt;seeded buyers&lt;/a&gt; are set up to show the difference. Alice has verified her email address but not her phone number. Bob has verified his phone number but not his email address. (The seed only creates users that don't exist yet. If your &lt;code&gt;identitydb&lt;/code&gt; was seeded by an earlier part, the &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%205/README.md" rel="noopener noreferrer"&gt;Part 5 README&lt;/a&gt; has the SQL to update bob.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                         Ordering
                            |
           OrderCreated + buyer reference
                            |
               +------------+------------+
               |                         |
             alice                      bob
               |                         |
    resolves to a verified     resolves to a verified
        email address               phone number
               |                         |
               v                         v
             Email                      SMS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ordering sent exactly the same intent for both. It never knew an email address or a phone number existed. Communications resolved each buyer and picked the channel.&lt;/p&gt;

&lt;p&gt;In Part 2, Ordering handed over an email address, so every order confirmation was an email. That coupling went away in Part 3, and now the buyer's verified addresses decide.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a delivery report is
&lt;/h2&gt;

&lt;p&gt;Transmitly represents every delivery event the same way, whatever provider it came from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;DeliveryReport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;EventName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;ChannelId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;ChannelProviderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;PipelineIntent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;PipelineId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;ResourceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CommunicationsStatus&lt;/span&gt; &lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;ChannelCommunication&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IContentModel&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;ContentModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few of those fields do most of the work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;EventName           what happened: OnDispatched, OnStatusChanged, OnDelivered, OnError
ChannelId           Email, Sms, Push
ChannelProviderId   who reported it: Simulation.Sms, Twilio, ...
ResourceId          the provider's id for the message
Status              a provider-agnostic outcome, with a success or failure range
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's half of what this article is about. Code that records, displays, or alerts on delivery events can be written once, against &lt;code&gt;DeliveryReport&lt;/code&gt;, and work for every provider.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reports come from two places
&lt;/h2&gt;

&lt;p&gt;Some reports are raised at dispatch. When a provider accepts a message, its Transmitly integration raises an &lt;code&gt;OnDispatched&lt;/code&gt; report with the provider's message id. The simulator does the same, which is what produced those log lines.&lt;/p&gt;

&lt;p&gt;Others arrive later. A provider like Twilio calls a webhook when a message's status changes, and Transmitly turns that callback into an &lt;code&gt;OnStatusChanged&lt;/code&gt; report.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Dispatch
   |
   v
Provider accepts the message ------&amp;gt; OnDispatched report
   |
   | seconds or minutes later
   v
Provider calls our webhook --------&amp;gt; OnStatusChanged report
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both kinds reach the same place. Next to the logging handler from Part 2, Communications &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%205/src/Communications.API/Program.cs" rel="noopener noreferrer"&gt;registers a delivery report handler&lt;/a&gt; for recording:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddDeliveryReportHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;deliveryReports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CompletedTask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It doesn't check which provider sent the report, or whether the report came from a dispatch or a webhook. It just queues it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recording reports in the background
&lt;/h2&gt;

&lt;p&gt;The handler only queues because of where reports are raised: in the middle of a dispatch, or in the middle of a webhook request from a provider. Neither is a good place to wait on a database.&lt;/p&gt;

&lt;p&gt;A &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%205/src/Communications.API/DeliveryReports/DeliveryReportProcessing.cs" rel="noopener noreferrer"&gt;background service&lt;/a&gt; reads the queue and records one report at a time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadAllAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stoppingToken&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scopeFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateAsyncScope&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;recorder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServiceProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;DeliveryReportRecorder&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;recorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RecordAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stoppingToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Processing reports in order also means a quick status update can't race the dispatch report it follows. A report that fails to record is logged, and the processor moves on to the next one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Communications gets a database
&lt;/h2&gt;

&lt;p&gt;Delivery history is communications data, so it goes in a database Communications owns. The &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%205/src/eShop.AppHost/Program.cs" rel="noopener noreferrer"&gt;AppHost&lt;/a&gt; adds &lt;code&gt;communicationsdb&lt;/code&gt; to eShop's existing Postgres server, next to the Catalog and Ordering databases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;communicationsDb&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddDatabase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"communicationsdb"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model has &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%205/src/Communications.API/DeliveryReports/CommunicationRecord.cs" rel="noopener noreferrer"&gt;two tables&lt;/a&gt;. A communication is one message to one recipient on one channel, with its latest status. A delivery event is one report about it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Communications
    Id
    PipelineIntent      OrderCreated
    ChannelId           Sms
    ChannelProviderId   Twilio
    ResourceId          SM8f2c...     the provider's message id
    RecipientId         the buyer's identity
    Summary             "We've received your eShop order #123. ..."
    Status              Delivered
    IsFailure           false

DeliveryEvents
    CommunicationRecordId
    EventName           OnStatusChanged
    Status              Delivered
    ProviderDetails     {"MessageSid": "SM8f2c...", "ErrorCode": null, ...}
    ReceivedAt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%205/src/Communications.API/DeliveryReports/DeliveryReportRecorder.cs" rel="noopener noreferrer"&gt;recorder&lt;/a&gt; ties reports together through the provider's message id. The first report for a message creates its communication. Later reports with the same channel and message id add events to it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;FindAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;new&lt;/span&gt; &lt;span class="n"&gt;CommunicationRecord&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* intent, channel, provider, message id ... */&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Communications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;record&lt;/span&gt;&lt;span class="err"&gt;);&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// A provider's later update carries less context than the dispatch, so only fill gaps.&lt;/span&gt;
&lt;span class="nc"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RecipientId&lt;/span&gt; &lt;span class="p"&gt;??=&lt;/span&gt; &lt;span class="n"&gt;DeliveryReportDetails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RecipientId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;record&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Summary&lt;/span&gt; &lt;span class="p"&gt;??=&lt;/span&gt; &lt;span class="n"&gt;DeliveryReportDetails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Summary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChannelCommunication&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;record&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;record&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IsFailure&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsFailure&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The report raised at dispatch knows a lot: the rendered message, the content model, and the recipient profile inside it. A webhook from Twilio knows only its message id and the new status. Filling gaps keeps the richer details from the dispatch.&lt;/p&gt;

&lt;p&gt;The latest report wins. Providers don't always report in order, and a production system might want to make sure a late &lt;code&gt;sent&lt;/code&gt; never overwrites &lt;code&gt;delivered&lt;/code&gt;. For the sample, the simple rule is enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Receiving provider webhooks
&lt;/h2&gt;

&lt;p&gt;Transmitly's ASP.NET Core package, &lt;code&gt;Transmitly.Microsoft.AspnetCore.Mvc&lt;/code&gt;, supplies a controller for delivery report webhooks. Communications &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%205/src/Communications.API/DeliveryReports/DeliveryReportsController.cs" rel="noopener noreferrer"&gt;exposes it&lt;/a&gt; at one address for every provider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AllowAnonymous&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"api/communications/delivery-reports"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DeliveryReportsController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ICommunicationsClient&lt;/span&gt; &lt;span class="n"&gt;communicationsClient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;ChannelProviderDeliveryReportController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;communicationsClient&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%205/src/Communications.API/Program.cs" rel="noopener noreferrer"&gt;registers the model binders&lt;/a&gt; that do the actual work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddControllers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTransmitlyDeliveryReportModelBinders&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a request arrives, the binder offers it to each registered provider's request adaptor. Each adaptor recognizes its own provider's requests and turns them into &lt;code&gt;DeliveryReport&lt;/code&gt;s, which the controller hands back to Transmitly. From there they go to the same handlers as every other report.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /api/communications/delivery-reports?...
        |
        v
Delivery report model binder
        |
        +-- Twilio SMS adaptor     "this is mine" -&amp;gt; DeliveryReport
        +-- Twilio voice adaptor   not mine
        +-- ...
        |
        v
Transmitly delivery report handlers
        |
        v
DeliveryReportQueue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The webhook is anonymous because providers can't sign in. A production deployment should validate each provider's request signature before trusting a callback. Twilio's adaptor captures the signature header, but the sample doesn't check it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using Twilio as the example
&lt;/h2&gt;

&lt;p&gt;The sample uses Twilio for this part because it's a provider that really does report delivery status after dispatch. It's &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%205/src/Communications.API/EshopChannelProviders.cs" rel="noopener noreferrer"&gt;wired up&lt;/a&gt;, but only switched on when credentials are configured:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_twilio&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddSimulationSupport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;providerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Sms"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTwilioSupport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;twilio&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;twilio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AccountSid&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_twilio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AccountSid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;twilio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AuthToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_twilio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AuthToken&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the SMS provider now depends on configuration, &lt;code&gt;EshopChannelProviders&lt;/code&gt; is no longer Part 4's static class. &lt;code&gt;Program.cs&lt;/code&gt; builds it at startup with &lt;code&gt;EshopChannelProviders.FromConfiguration(builder.Configuration)&lt;/code&gt;, and the pipeline reads each channel's provider id from that instance, as &lt;code&gt;providers.Sms&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Without Twilio settings, SMS stays on the simulator and everything in this article still works. The simulator raises its &lt;code&gt;OnDispatched&lt;/code&gt; reports, the recorder stores them, and the Messages page shows them.&lt;/p&gt;

&lt;p&gt;With Twilio configured, Twilio also needs to know where to send status updates. &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%205/src/Communications.API/Program.cs" rel="noopener noreferrer"&gt;The SMS channel&lt;/a&gt; tells it, using Twilio's own channel settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;sms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Twilio&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;StatusCallbackUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeliveryReportUrl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When it sends each message, Transmitly gives Twilio the webhook address plus a few query parameters that identify the intent, the channel, and the provider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://&amp;lt;public host&amp;gt;/api/communications/delivery-reports
    ?tlyp=OrderCreated
    &amp;amp;tlyc=Sms
    &amp;amp;tlycp=Twilio
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those parameters are how Twilio's adaptor recognizes the callback as its own when it comes back. The address has to be reachable from Twilio's servers, so local development needs a tunnel.&lt;/p&gt;

&lt;p&gt;The configuration goes in the Communications API's user secrets or environment variables, and looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Twilio"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"AccountSid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AC..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"AuthToken"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"FromNumber"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"+1..."&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Communications"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"DeliveryReportUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://&amp;lt;public host&amp;gt;/api/communications/delivery-reports"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bob's seeded phone number, &lt;code&gt;+11234567890&lt;/code&gt;, is in E.164 format, but it isn't a real number. To send him a real text, change it to a number you can receive texts on, also in E.164 format. On a Twilio trial account, it also has to be a number you've verified with Twilio. Otherwise Twilio rejects the message, and first match falls through to push.&lt;/p&gt;

&lt;h2&gt;
  
  
  Provider details when you need them
&lt;/h2&gt;

&lt;p&gt;The other half of this article is the provider's own detail. A provider-agnostic status is right for almost everything, but sometimes you need to know exactly what the provider said.&lt;/p&gt;

&lt;p&gt;When Twilio reports an undelivered SMS, the shared status says the message failed. Twilio's error code says why: &lt;code&gt;30003&lt;/code&gt; means the handset was unreachable, &lt;code&gt;30005&lt;/code&gt; means the number doesn't exist, and &lt;code&gt;30007&lt;/code&gt; means a carrier filtered the message. Each of those calls for a different response.&lt;/p&gt;

&lt;p&gt;Transmitly keeps the provider's details on the report, and each provider package exposes them through its own extension. The recorder &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%205/src/Communications.API/DeliveryReports/DeliveryReportDetails.cs" rel="noopener noreferrer"&gt;stores Twilio's&lt;/a&gt; alongside the shared status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChannelProviderId&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;StartsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChannelProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Twilio&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;StringComparison&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrdinalIgnoreCase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChannelId&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sms&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;sms&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Twilio&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;Sms&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;JsonSerializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;sms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MessageSid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;MessageStatus&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MessageStatus&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;sms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrorCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;sms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;To&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;sms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;From&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the only provider-specific code in the recording path, and it's optional. Remove it and every Twilio report is still recorded with its status. Keep it and the details are there for the support engineer who needs them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The buyer's inbox
&lt;/h2&gt;

&lt;p&gt;With communications stored, the buyer can see them. Communications exposes an &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%205/src/Communications.API/Apis/InboxApi.cs" rel="noopener noreferrer"&gt;inbox&lt;/a&gt; for the signed-in user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;endpoints&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/communications/inbox"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GetInboxAsync&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RequireAuthorization&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It returns the user's 50 most recent communications, newest first, from the records the delivery reports built. The inbox matches records against the &lt;code&gt;sub&lt;/code&gt; claim on the user's token, which is the same identity id Ordering dispatched in Part 3, so no new mapping is needed.&lt;/p&gt;

&lt;p&gt;Authentication follows eShop's existing pattern. Identity gets a &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%205/src/Identity.API/Configuration/Config.cs" rel="noopener noreferrer"&gt;&lt;code&gt;communications&lt;/code&gt; scope&lt;/a&gt;, the web app requests it when the user signs in, and Communications validates tokens with eShop's shared &lt;code&gt;AddDefaultAuthentication()&lt;/code&gt;. The &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%205/src/WebApp/Extensions/Extensions.cs" rel="noopener noreferrer"&gt;web app's HTTP client&lt;/a&gt; for Communications attaches the user's token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddHttpClient&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;InboxService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BaseAddress&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https+http://communications-api"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddAuthToken&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The web app adds a &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%205/src/WebApp/Components/Pages/User/Messages.razor" rel="noopener noreferrer"&gt;Messages page&lt;/a&gt; to the &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%205/src/WebApp/Components/Layout/UserMenu.razor" rel="noopener noreferrer"&gt;account menu&lt;/a&gt;, next to My orders. Here's what alice and bob each see after placing an order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;alice
Sent                Channel   Message                                    Status
10/2/2026 8:17 PM   Email     Thanks for your order #11                  Dispatched
                              Order created

bob
Sent                Channel   Message                                    Status
10/2/2026 8:18 PM   SMS       We've received your eShop order #12. ...   Dispatched
                              Order created
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each of them placed one order and got one message, on different channels, because their verified addresses differ. With Twilio configured, bob's status starts as whatever Twilio returned when it accepted the message (usually queued) and changes as Twilio's callbacks arrive.&lt;/p&gt;

&lt;p&gt;The same records could feed other things too. A support tool could show a customer's full delivery history, or an alert could fire when one provider's failure rate climbs. Neither needs anything new from the providers, or from Ordering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing without a provider
&lt;/h2&gt;

&lt;p&gt;None of this needs a real Twilio account to test. &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%205/tests/Application.UnitTests/DeliveryReportWebhookTests.cs" rel="noopener noreferrer"&gt;The sample's tests&lt;/a&gt; post a Twilio-shaped status callback to the real &lt;code&gt;DeliveryReportsController&lt;/code&gt;, running in an in-memory test server, at the URL Twilio would be given:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;callbackUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http://localhost/api/communications/delivery-reports"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddPipelineContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CommunicationIntents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderCreated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sms&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChannelProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Twilio&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PostAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;callbackUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;FormUrlEncodedContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"SmsSid"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"SM123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"MessageSid"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"SM123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"SmsStatus"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"delivered"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"MessageStatus"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"delivered"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"AccountSid"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ACtest"&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The callback comes out the other side as an &lt;code&gt;OnStatusChanged&lt;/code&gt; report for &lt;code&gt;SM123&lt;/code&gt;, with a &lt;code&gt;Delivered&lt;/code&gt; status. &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%205/tests/Application.UnitTests/DeliveryReportRecordingTests.cs" rel="noopener noreferrer"&gt;Another test&lt;/a&gt; records a dispatch followed by an undelivered callback, and checks that both land on the same communication with Twilio's error code kept.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where we ended up
&lt;/h2&gt;

&lt;p&gt;Communications now keeps a record of everything it sends, and of everything providers tell it afterwards:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ordering
    |
    | OrderCreated
    v
Communications
    |
    +-- compose and dispatch          (Parts 2-4)
    |
    +-- delivery reports
    |       at dispatch               simulator, Twilio, ...
    |       from webhooks             Twilio status callbacks
    |
    +-- one handler -&amp;gt; queue -&amp;gt; recorder -&amp;gt; communicationsdb
    |
    v
Inbox API -&amp;gt; WebApp Messages page
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because &lt;code&gt;DeliveryReport&lt;/code&gt; is provider-agnostic, the recording, the inbox, and anything else built on delivery history work the same for every provider. When one provider's specifics matter, its details are still there.&lt;/p&gt;

&lt;p&gt;Adding a provider doesn't change any of it. A new provider's package brings its own request adaptor, and its callbacks start landing in the same tables.&lt;/p&gt;

&lt;h2&gt;
  
  
  Looking back
&lt;/h2&gt;

&lt;p&gt;This series started with a line most applications have somewhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;emailClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The usual next step is to inject that client into the service that knows something happened. That holds up for a while. Then the service learns how to find the customer's email address, then what to put in the message, then which template to use. When SMS arrives, it learns about phone numbers and a second provider SDK. When someone asks whether the message actually arrived, it learns about webhooks. Each step is reasonable. Together they turn a business service into a communications service that happens to manage orders.&lt;/p&gt;

&lt;p&gt;eShop never went down that road. Ordering states one intent when an order is created, names the buyer by identity, and hands over the order facts it already has. Its code last changed in Part 3, when it started sending an identity reference and the order's items. Everything after that (two more channels, a different channel for each buyer, delivery tracking from any provider, and an inbox) happened without touching Ordering.&lt;/p&gt;

&lt;p&gt;The first thing Transmitly gave us was a stable seam. Application code talks to &lt;code&gt;ICommunicationsClient&lt;/code&gt; and an intent name, and that's all. Whether the pipeline runs in the same process or behind a Communications service is a middleware decision, and so is waiting for a database commit before anything goes out. Ordering didn't have to know about either.&lt;/p&gt;

&lt;p&gt;Behind that seam, each concern could live with the data it needs. Recipients are resolved through Identity, which owns them, instead of being copied into every service that sends something. Product details come from Catalog at composition time rather than being dragged through Ordering. And because a content model built for communicating sits between the business facts and the templates, email, SMS, and push each use it differently without anyone reassembling the data.&lt;/p&gt;

&lt;p&gt;Delivery decisions moved out of business code. A buyer's verified addresses decide which channels can reach them, the pipeline's delivery strategy decides whether they get one message or several, and a provider registration decides which company carries each channel. Swapping SMTP for SendGrid, or the simulator for Twilio, leaves the pipeline and every dispatching service alone.&lt;/p&gt;

&lt;p&gt;We also got to see what happened after the send. Reports from the simulator and from Twilio arrive in one provider-agnostic shape, so the recording and the inbox were written once, and provider details like Twilio's error code are still available when they matter.&lt;/p&gt;

&lt;p&gt;None of it needed a provider account to build. The simulator ran the whole pipeline, and the tests composed real messages through the real templates and enrichers without touching the network.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shaped around eShop
&lt;/h2&gt;

&lt;p&gt;None of that required eShop to reorganize itself around a library. Look at the pieces we wrote:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;IdentityServerCustomerIdentityResolver   IPlatformIdentityResolver
PushRegistrationProfileEnricher          IPlatformIdentityProfileEnricher
CatalogContentModelEnricher              IContentModelEnricher
EshopCommunicationsMiddleware            ICommunicationClientMiddleware
TransactionalCommunicationsMiddleware    ICommunicationClientMiddleware
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each one adapts Transmitly to something eShop already had. &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%205/src/Communications.API/IdentityServerCustomerIdentityResolver.cs" rel="noopener noreferrer"&gt;Buyers stayed in Identity&lt;/a&gt;, behind an endpoint Identity owns and a service token Identity issues. &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%205/src/Communications.API/OrderCreated/CatalogContentModelEnricher.cs" rel="noopener noreferrer"&gt;Product data&lt;/a&gt; stayed behind Catalog's existing batch endpoint. &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%205/src/Ordering.API/Infrastructure/Communications/TransactionalCommunicationsMiddleware.cs" rel="noopener noreferrer"&gt;The commit deferral&lt;/a&gt; hooks into Ordering's own &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%205/src/Ordering.API/Application/Behaviors/TransactionBehavior.cs" rel="noopener noreferrer"&gt;&lt;code&gt;TransactionBehavior&lt;/code&gt;&lt;/a&gt;. &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%205/src/eShop.ServiceDefaults/Communications/EshopCommunicationsMiddleware.cs" rel="noopener noreferrer"&gt;Forwarding&lt;/a&gt; uses Aspire's service discovery and eShop's shared service defaults. Transmitly supplied the extension points, and eShop decided what went in them.&lt;/p&gt;

&lt;p&gt;That's the right way round. A communications library that dictates where identities live, how transactions work, or how services talk to each other forces its way of working onto every system it touches. Transmitly asks for an implementation of a small interface at the point it needs one.&lt;/p&gt;

&lt;p&gt;The built-in pieces work the same way, so when something isn't in the box, it can be added without changing Transmitly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A channel provider is a dispatcher registered against the channels it supports. A provider Transmitly doesn't ship, an in-house SMS gateway for example, is an &lt;code&gt;IChannelProviderDispatcher&amp;lt;T&amp;gt;&lt;/code&gt; and a registration.&lt;/li&gt;
&lt;li&gt;Provider webhooks come in through &lt;code&gt;IChannelProviderDeliveryReportRequestAdaptor&lt;/code&gt;, the same interface Twilio's adaptor implements in this article.&lt;/li&gt;
&lt;li&gt;Template engines implement &lt;code&gt;ITemplateEngine&lt;/code&gt;, which is how the Fluid and Scriban integrations work.&lt;/li&gt;
&lt;li&gt;Delivery strategies derive from &lt;code&gt;BasePipelineDeliveryStrategyProvider&lt;/code&gt;, for when first match and any match don't describe your policy.&lt;/li&gt;
&lt;li&gt;Channels implement &lt;code&gt;IChannel&lt;/code&gt;, for a medium beyond email, SMS, push, and voice.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The packaged providers and integrations are small, separate repositories, which makes them good references for writing your own.&lt;/p&gt;

&lt;h2&gt;
  
  
  There's a lot more in Transmitly
&lt;/h2&gt;

&lt;p&gt;The series used a fraction of what Transmitly does. A few of the pieces we didn't get to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Our templates were C# methods that built strings. Transmitly has pluggable template engines, with Fluid and Scriban integrations, so content can live in templates instead of code.&lt;/li&gt;
&lt;li&gt;Beyond SMTP, Twilio, and Firebase there are SendGrid and Mailgun for email, and Infobip for email, SMS, and voice. Voice is a channel in its own right, supported by Twilio and Infobip.&lt;/li&gt;
&lt;li&gt;Personas define audience segments as conditions over the recipient profile with &lt;code&gt;AddPersona&lt;/code&gt;, and a pipeline can apply only to the segments it names with &lt;code&gt;AddPersonaFilter&lt;/code&gt;. They're a natural next step from this article's verified addresses.&lt;/li&gt;
&lt;li&gt;A dispatch can name the channels a recipient has chosen, and Transmitly will only deliver on those. Part 2's contract already carries them as &lt;code&gt;AllowedChannels&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;We used client middleware to forward dispatches and to wait for a commit. Dispatch middleware runs around each channel dispatch inside the pipeline, which makes it a good home for concerns like auditing.&lt;/li&gt;
&lt;li&gt;Delivery report handlers can filter on specific events, channels, providers, or intents, instead of receiving every report like ours did.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The best place to go from here is the repository: &lt;a href="https://github.com/transmitly/transmitly" rel="noopener noreferrer"&gt;github.com/transmitly/transmitly&lt;/a&gt;. It has the list of channel providers and integrations, the &lt;a href="https://github.com/transmitly/transmitly/tree/main/samples" rel="noopener noreferrer"&gt;samples&lt;/a&gt;, including this series and smaller starting points like Hello Transmitly and the Microservices sample, and the &lt;a href="https://github.com/transmitly/transmitly/wiki" rel="noopener noreferrer"&gt;wiki&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you try it and something is confusing, missing, or more ceremony than it's worth, I want to hear about it in &lt;a href="https://github.com/transmitly/transmitly/discussions" rel="noopener noreferrer"&gt;GitHub Discussions&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>programming</category>
      <category>opensource</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>Part 4: One Intent, Many Channels and Providers</title>
      <dc:creator>Jeremy</dc:creator>
      <pubDate>Wed, 21 Oct 2026 23:00:00 +0000</pubDate>
      <link>https://dev.to/jeremy_tly/part-4-one-intent-many-channels-and-providers-31g6</link>
      <guid>https://dev.to/jeremy_tly/part-4-one-intent-many-channels-and-providers-31g6</guid>
      <description>&lt;p&gt;Part 3 built the content. Ordering dispatches &lt;code&gt;OrderCreated&lt;/code&gt; with the order facts it owns, Communications resolves the buyer through Identity, a Catalog enricher fills in product details, and one enriched context comes out the other side. Then a single channel used it: email.&lt;/p&gt;

&lt;p&gt;Part 3 also sketched what SMS and push could do with the same context. This article builds them. Along the way we'll separate two ideas that are easy to blur together, channels and providers, and see what it would take to connect real ones like SMTP, Twilio, and Firebase.&lt;/p&gt;

&lt;p&gt;The code stays on Transmitly's simulator the whole time. Nothing in this article needs an account, an API key, or a phone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ordering doesn't change
&lt;/h2&gt;

&lt;p&gt;Ordering still &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%204/src/Ordering.API/Application/DomainEventHandlers/SendOrderCreatedCommunicationWhenOrderStartedDomainEventHandler.cs" rel="noopener noreferrer"&gt;dispatches exactly what it dispatched in Part 3&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_communicationsClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DispatchAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;CommunicationIntents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderCreated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;buyer&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;TransactionModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's no &lt;code&gt;channels&lt;/code&gt; argument, no SMS flag, no push token. Comparing &lt;code&gt;Ordering.API&lt;/code&gt; between the Part 3 and Part 4 code turns up no differences at all.&lt;/p&gt;

&lt;p&gt;Ordering said an order was created. Whether that becomes one message or three is a communication decision, so every change in this article happens inside Communications.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every channel needs an address
&lt;/h2&gt;

&lt;p&gt;A channel can only deliver if the recipient has an address it understands. Email needs an email address, SMS needs a phone number, and push needs a device token.&lt;/p&gt;

&lt;p&gt;Transmitly handles the matching. When it plans a dispatch, it asks each channel in the pipeline which of the recipient's addresses it can use. The email channel accepts email addresses. The SMS channel accepts phone numbers, and an address type can say which kind of phone it is, so a mobile number gets the text and a home phone doesn't. The push channel accepts device tokens and topics. A channel with no usable address is skipped for that recipient.&lt;/p&gt;

&lt;p&gt;So adding channels starts with the recipient profile. Here's what eShop's buyer looks like after identity resolution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Buyer: alice

Email
    AliceSmith@email.com

Phone
    +11234567890
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Identity already owns the email address and the phone number, and &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%204/src/Communications.API/IdentityServerCustomerIdentityResolver.cs" rel="noopener noreferrer"&gt;the resolver&lt;/a&gt; from Part 3 returns both. SMS has what it needs.&lt;/p&gt;

&lt;p&gt;Push doesn't. There's no device token anywhere in eShop, and no service in the application that would own one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Push tokens are communications data
&lt;/h2&gt;

&lt;p&gt;A device token doesn't say who the buyer is. It says where a notification can be delivered, it changes whenever the app is reinstalled, and nothing outside communications has any use for it. That makes it communications data, and Communications should own it.&lt;/p&gt;

&lt;p&gt;In Part 3 we noted that the pipeline has a stage for exactly this kind of thing, and that eShop didn't need it yet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Identity resolution
    |
    v
Identity-profile enrichment     &amp;lt;-- push tokens go here
    |
    v
Content enrichment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Profile enrichers run after resolution and before content is built. They can add to the recipient's profile, which is where channels look for addresses. So Communications gets a small &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%204/src/Communications.API/PushRegistrations/IPushRegistrationStore.cs" rel="noopener noreferrer"&gt;store of push registrations&lt;/a&gt;, and a &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%204/src/Communications.API/PushRegistrations/PushRegistrationProfileEnricher.cs" rel="noopener noreferrer"&gt;profile enricher&lt;/a&gt; that reads from it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PushRegistrationProfileEnricher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IPushRegistrationStore&lt;/span&gt; &lt;span class="n"&gt;registrations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IPlatformIdentityProfileEnricher&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;EnrichIdentityProfileAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IPlatformIdentityProfile&lt;/span&gt; &lt;span class="n"&gt;identityProfile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;identityProfile&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="n"&gt;CustomerIdentityProfile&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;registrations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetDeviceTokensAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;PlatformIdentityAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PlatformIdentityAddress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DeviceToken&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Part 3, &lt;code&gt;CustomerIdentityProfile&lt;/code&gt; was an immutable record. It's now a class with an &lt;code&gt;AddAddress&lt;/code&gt; method, so enrichers like this one can add addresses that Identity doesn't own.&lt;/p&gt;

&lt;p&gt;The enricher is &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%204/src/Communications.API/Program.cs" rel="noopener noreferrer"&gt;registered&lt;/a&gt; for the same identity type as the resolver:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddPlatformIdentityResolver&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IdentityServerCustomerIdentityResolver&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;CommunicationIdentityTypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Buyer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddPlatformIdentityProfileEnricher&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PushRegistrationProfileEnricher&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;CommunicationIdentityTypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Buyer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One gap remains. eShop's apps don't register for push notifications, so there's nothing to put in the store. The sample registers a &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%204/src/Communications.API/PushRegistrations/SimulatedPushRegistrationStore.cs" rel="noopener noreferrer"&gt;&lt;code&gt;SimulatedPushRegistrationStore&lt;/code&gt;&lt;/a&gt; that gives every buyer one simulated device:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IReadOnlyCollection&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetDeviceTokensAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;identityId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FromResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IReadOnlyCollection&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;([&lt;/span&gt;&lt;span class="s"&gt;$"simulated-device-&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;identityId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a real system, the mobile app would send its token to Communications when the user signs in, and the store would hold whatever devices are registered. The enricher wouldn't change.&lt;/p&gt;

&lt;p&gt;After enrichment the profile has an address for every channel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Buyer: alice

Email
    AliceSmith@email.com

Phone
    +11234567890

Device token
    simulated-device-…
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  One context, three renderings
&lt;/h2&gt;

&lt;p&gt;With addresses in place, the pipeline in the Communications service's &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%204/src/Communications.API/Program.cs" rel="noopener noreferrer"&gt;&lt;code&gt;Program.cs&lt;/code&gt;&lt;/a&gt; adds two channels next to the email:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddPipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CommunicationIntents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderCreated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseAnyMatchPipelineDeliveryStrategy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"orders@eshop.local"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsIdentityAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"eShop"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddChannelProviderFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EshopChannelProviders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Subject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTemplateResolver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;OrderCreatedEmail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
            &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TextBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTemplateResolver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;OrderCreatedEmail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TextBody&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;webAppUrl&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddSms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"+15555550100"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsIdentityAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"eShop"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;sms&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;sms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddChannelProviderFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EshopChannelProviders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sms&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;sms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTemplateResolver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;OrderCreatedSms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;webAppUrl&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddPushNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;push&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;push&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddChannelProviderFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EshopChannelProviders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Push&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;push&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Title&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTemplateResolver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;OrderCreatedPush&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="n"&gt;push&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTemplateResolver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;OrderCreatedPush&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="n"&gt;push&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OrderCreatedPush&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ActionKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OrderCreatedPush&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OpenOrderAction&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;push&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddDataIfNotNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OrderCreatedPush&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderIdKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;OrderCreatedPush&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We'll come back to the first line and the provider filters. For now, look at what each channel renders.&lt;/p&gt;

&lt;p&gt;All three read the same &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%204/src/Communications.API/OrderCreated/OrderCreatedContentModel.cs" rel="noopener noreferrer"&gt;&lt;code&gt;OrderCreatedContentModel&lt;/code&gt;&lt;/a&gt; that Part 3's &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%204/src/Communications.API/OrderCreated/CatalogContentModelEnricher.cs" rel="noopener noreferrer"&gt;Catalog enricher&lt;/a&gt; built. That enricher is registered per recipient, not per channel, so it runs once and every channel shares its result. Adding SMS and push didn't add any calls to Catalog.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%204/src/Communications.API/OrderCreated/OrderCreatedEmail.cs" rel="noopener noreferrer"&gt;email&lt;/a&gt; renders exactly as it did in Part 3. Its "view your order" link now comes from a small &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%204/src/Communications.API/OrderCreated/OrderLinks.cs" rel="noopener noreferrer"&gt;&lt;code&gt;OrderLinks&lt;/code&gt;&lt;/a&gt; helper that the SMS shares. The &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%204/src/Communications.API/OrderCreated/OrderCreatedSms.cs" rel="noopener noreferrer"&gt;SMS&lt;/a&gt; keeps only what fits in a text message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IDispatchCommunicationContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;webAppUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OrderCreatedContentModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ContentModel&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s"&gt;"We've received your eShop order."&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$"We've received your eShop order #&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FromResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;OrderLinks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;webAppUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;ordersLink&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;\nView your order: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ordersLink&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which renders as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;We've received your eShop order #123.
View your order: https://eshop.example/user/orders
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every template method returns &lt;code&gt;Task&amp;lt;string?&amp;gt;&lt;/code&gt;, which is what &lt;code&gt;AddTemplateResolver&lt;/code&gt; takes, so each registration is a single line. A template that later needs to await something can do it without changing how it's registered.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%204/src/Communications.API/OrderCreated/OrderCreatedPush.cs" rel="noopener noreferrer"&gt;Push&lt;/a&gt; is shorter still. The visible part is a title and a sentence. The useful part is the data: an action and an order id the app can use to open the right screen.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Title
    Thanks for your order

Body
    Order #123 has been received.

Data
    action:  open-order
    orderId: 123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's three quite different messages from one content model, and none of the templates had to fetch anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deciding which channels to use
&lt;/h2&gt;

&lt;p&gt;Now the first line of the pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseAnyMatchPipelineDeliveryStrategy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A pipeline with several channels needs a rule for using them, and it helps to know what happens without that line first.&lt;/p&gt;

&lt;p&gt;By default, Transmitly walks the channels in the order they were added and delivers on the first one that can reach the recipient. If delivery on that channel fails, it moves on to the next. For our pipeline, that means the buyer gets the email, and SMS and push only come into play if the email can't be delivered:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Default (first match)

OrderCreated
    |
    +-- Email  reachable? deliver and stop
    |
    +-- SMS    otherwise, try this
    |
    +-- Push   last resort
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives one message per order with a fallback built in, which is usually what you want for an order confirmation. Reordering the channels changes the ladder: put push first and app users get a notification, while everyone else falls through to email.&lt;/p&gt;

&lt;p&gt;Transmitly calls this rule a delivery strategy, and a pipeline can pick a different one. This sample opts into any match, which sends on every channel the recipient can be reached on, so we can watch all three channels go out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Any match

OrderCreated
    |
    +-- Email  -&amp;gt; delivered
    +-- SMS    -&amp;gt; delivered
    +-- Push   -&amp;gt; delivered
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Either way, the choice is one line in Communications, and Ordering doesn't know which one is in use. A future policy could go further and respect each recipient's channel preferences. Part 2's dispatch contract already carries &lt;code&gt;AllowedChannels&lt;/code&gt; for that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Channels and providers are different things
&lt;/h2&gt;

&lt;p&gt;So far "SMS" has meant two things at once: the kind of message, and whoever delivers it. Transmitly keeps those apart.&lt;/p&gt;

&lt;p&gt;A channel is the medium: email, SMS, push. Channels own content. They know a subject line from a message body, and what an address has to look like.&lt;/p&gt;

&lt;p&gt;A channel provider is whoever actually delivers it: an SMTP server, SendGrid, Twilio, Firebase. Providers own transport. They know about API keys, endpoints, and rate limits.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;          Channels                    Providers
     (what we send)              (who delivers it)

   Email  ----------------------&amp;gt;  SMTP / SendGrid
   SMS    ----------------------&amp;gt;  Twilio
   Push   ----------------------&amp;gt;  Firebase
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pipeline is written entirely in terms of channels. Providers are registered separately, once, and the two meet only at dispatch time.&lt;/p&gt;

&lt;p&gt;In this sample, &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%204/src/Communications.API/EshopChannelProviders.cs" rel="noopener noreferrer"&gt;all three providers&lt;/a&gt; are simulators:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EshopChannelProviders&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Email&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChannelProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Simulation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Email"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Sms&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChannelProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Simulation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Sms"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Push&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChannelProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Simulation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Push"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;CommunicationsClientBuilder&lt;/span&gt; &lt;span class="nf"&gt;AddEshopChannelProviders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="n"&gt;CommunicationsClientBuilder&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;builder&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddSimulationSupport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;providerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Email"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddSimulationSupport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;providerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Sms"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddSimulationSupport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;providerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Push"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each simulator stands in for the real provider that would sit behind its channel. That's what the &lt;code&gt;AddChannelProviderFilter&lt;/code&gt; calls in the pipeline are for: each channel names the one provider it should use.&lt;/p&gt;

&lt;p&gt;Real providers declare the channels they support, so a Twilio registration would never be offered an email. A simulator accepts any channel. Without the filters, any match would send each message through all three simulators, and the buyer would get three emails, three texts, and three pushes. Naming each channel's provider keeps the routing explicit and keeps the simulated setup shaped like the real one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting real providers
&lt;/h2&gt;

&lt;p&gt;To send real messages, you change the provider registrations. The pipeline stays as it is, and so do the templates, the enrichers, and everything outside Communications.&lt;/p&gt;

&lt;p&gt;Each provider is a package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Transmitly.ChannelProvider.Smtp
Transmitly.ChannelProvider.Twilio
Transmitly.ChannelProvider.Firebase
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and each replaces one simulator registration, with its credentials coming from configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;CommunicationsClientBuilder&lt;/span&gt; &lt;span class="nf"&gt;AddEshopChannelProviders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="n"&gt;CommunicationsClientBuilder&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IConfiguration&lt;/span&gt; &lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;builder&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddSmtpSupport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;smtp&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;smtp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Host&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Smtp:Host"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
            &lt;span class="n"&gt;smtp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;587&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;smtp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Smtp:UserName"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
            &lt;span class="n"&gt;smtp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Password&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Smtp:Password"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTwilioSupport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;twilio&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;twilio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AccountSid&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Twilio:AccountSid"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
            &lt;span class="n"&gt;twilio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AuthToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Twilio:AuthToken"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddFirebaseSupport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;firebase&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;firebase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProjectId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Firebase:ProjectId"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
            &lt;span class="n"&gt;firebase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Credential&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FirebaseCredential&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetApplicationDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The provider ids the channels pin to change with them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Email&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChannelProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Smtp&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Sms&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChannelProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Twilio&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Push&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChannelProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Firebase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole code change. The rest is operational: an SMTP server or a SendGrid account, a Twilio number to replace the placeholder &lt;code&gt;+15555550100&lt;/code&gt; the SMS is sent from, a Firebase project, and a push registration store that real devices write to.&lt;/p&gt;

&lt;p&gt;Swapping one provider for another works the same way. Moving email from SMTP to SendGrid replaces &lt;code&gt;AddSmtpSupport&lt;/code&gt; with &lt;code&gt;AddSendGridSupport&lt;/code&gt; and changes one id.&lt;/p&gt;

&lt;p&gt;Providers can also be combined. With first-match delivery, a channel that has two providers gets a fallback for free: if the first provider fails, Transmitly tries the next before moving on to the next channel. A team could run SendGrid for email and keep SMTP behind it for the day SendGrid has an outage.&lt;/p&gt;

&lt;p&gt;None of these choices reach the services that dispatch intents. Ordering didn't change when we added channels, and it wouldn't change if every provider changed tomorrow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why stay on the simulator?
&lt;/h2&gt;

&lt;p&gt;It's tempting to wire up a real provider as soon as possible. There's a good case for not doing it in the sample, and for keeping the simulator around even after real providers exist.&lt;/p&gt;

&lt;p&gt;The simulator runs the whole pipeline. Identity resolution, both enrichers, every template, and the channel and provider selection all happen exactly as they would in production. Only the last step, handing the message to an external service, is simulated.&lt;/p&gt;

&lt;p&gt;It also reports what it would have sent. Every simulated delivery raises a delivery report with the channel, the provider, and the full rendered communication. The Communications service &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%204/src/Communications.API/Program.cs" rel="noopener noreferrer"&gt;logs those reports&lt;/a&gt;, so placing an order in the web app produces three entries in the &lt;code&gt;communications-api&lt;/code&gt; logs in the Aspire dashboard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Email:Simulation.Email:Dispatched] ...
[Sms:Simulation.Sms:Dispatched] ...
[Push:Simulation.Push:Dispatched] ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;each followed by the rendered message.&lt;/p&gt;

&lt;p&gt;And it makes the composition testable. &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%204/tests/Application.UnitTests/OrderCreatedCompositionTests.cs" rel="noopener noreferrer"&gt;The sample's tests&lt;/a&gt; register the same &lt;code&gt;OrderCreated&lt;/code&gt; pipeline as &lt;code&gt;Program.cs&lt;/code&gt;, with the real resolver, enrichers, and templates and fake Identity and Catalog services behind them. They dispatch an order and check all three rendered messages, including which provider each one went through, without touching the network.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where we ended up
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;OrderCreated&lt;/code&gt; intent now reaches the buyer three ways:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ordering
    |
    | OrderCreated
    | order model + buyer reference
    v
Communications
    |
    +-- resolve buyer             Identity
    +-- add push devices          push registration store
    +-- enrich products           Catalog
    |
    v
Enriched content context
    |
    +-- Email  -&amp;gt; Simulation.Email   (SMTP, SendGrid)
    |
    +-- SMS    -&amp;gt; Simulation.Sms     (Twilio)
    |
    +-- Push   -&amp;gt; Simulation.Push    (Firebase)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ordering's code is identical to Part 3's. Everything new lives in Communications:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Recipient addresses
    resolver and profile enrichers

Channel content
    templates per channel

Channel policy
    delivery strategy

Delivery
    providers, one per channel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each of those can change on its own schedule: a new channel is a pipeline change, a new policy a strategy change, a new provider a registration change. The intent stays &lt;code&gt;OrderCreated&lt;/code&gt; through all of it.&lt;/p&gt;

&lt;p&gt;One word in those log lines deserves a second look: &lt;code&gt;Dispatched&lt;/code&gt;. With the simulator, handing a message to the provider and the message arriving are the same moment. Real providers rarely work that way. Twilio or SendGrid accept the message, queue it, and report back later as it's sent, delivered, bounced, or opened, and those later reports are worth keeping.&lt;/p&gt;




&lt;p&gt;In the next article, we'll explore how delivery updates from any provider land in one place, with Twilio as the example, and buyers get an inbox of everything we've sent them.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>opensource</category>
      <category>programming</category>
      <category>csharp</category>
    </item>
    <item>
      <title>Part 3: Composing the Message Where the Data Lives</title>
      <dc:creator>Jeremy</dc:creator>
      <pubDate>Tue, 20 Oct 2026 23:00:00 +0000</pubDate>
      <link>https://dev.to/jeremy_tly/part-3-composing-the-message-where-the-data-lives-3n0d</link>
      <guid>https://dev.to/jeremy_tly/part-3-composing-the-message-where-the-data-lives-3n0d</guid>
      <description>&lt;p&gt;Part 2 built the path. Ordering dispatches an &lt;code&gt;OrderCreated&lt;/code&gt; intent to the buyer's email address through Transmitly, holds it until the order commits, and middleware forwards the communication context to the Communications service, where the configured pipeline decides how to fulfill it.&lt;/p&gt;

&lt;p&gt;That first pipeline produced a deliberately bare email:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Subject:
Thanks for your order!

Body:
We've received your order and we'll let you know when it ships.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It was enough to prove the boundary, but a customer wouldn't get much out of it.&lt;/p&gt;

&lt;p&gt;A good order confirmation might include the customer's name, the items in the order, the amount paid, and product details. Some of that is already in hand when Ordering dispatches. The rest belongs to other services.&lt;/p&gt;

&lt;p&gt;Part 1 gave that assembly job to Communications: Ordering supplies the identifiers and facts it owns, and Communications assembles the communication model. This article builds that step, starting with the recipient. More channels, and the policy for choosing among them, come in Part 4.&lt;/p&gt;

&lt;p&gt;Transmitly treats the model supplied at dispatch as input to composition. Before content reaches a template, the pipeline can resolve platform identities, enrich those identity profiles, and enrich the content model itself.&lt;/p&gt;

&lt;p&gt;At a high level, the lifecycle looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DispatchAsync(...)
  |
  +-- (optional) Resolve identity references
  |       |
  |       v
  |   IPlatformIdentityProfile[]
  |
  +-- Run PlatformIdentityProfile enrichers
  |
  +-- Build dispatch/content context
  |
  +-- Run ContentModel enrichers
  |       by scope and order
  |
  +-- Render channel content
  |
  +-- Dispatch channels/providers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Starting with the transactional model
&lt;/h2&gt;

&lt;p&gt;When an order is created, Ordering has just built the &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%203/src/Ordering.Domain/AggregatesModel/OrderAggregate/Order.cs" rel="noopener noreferrer"&gt;&lt;code&gt;Order&lt;/code&gt;&lt;/a&gt; aggregate. eShop's aggregate holds its order items, and each &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%203/src/Ordering.Domain/AggregatesModel/OrderAggregate/OrderItem.cs" rel="noopener noreferrer"&gt;&lt;code&gt;OrderItem&lt;/code&gt;&lt;/a&gt; keeps the catalog product identifier, product name, unit price, discount, picture URL, and quantity. The order can calculate its total from those stored values.&lt;/p&gt;

&lt;p&gt;So the order itself is a natural source for the initial transactional model.&lt;/p&gt;

&lt;p&gt;We project the communication-relevant data into &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%203/src/eShop.ServiceDefaults/Communications/OrderCreatedModel.cs" rel="noopener noreferrer"&gt;&lt;code&gt;OrderCreatedModel&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;OrderCreatedModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;OrderDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IReadOnlyCollection&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderCreatedItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;OrderCreatedItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;ProductId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;ProductName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;UnitPrice&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%203/src/Ordering.API/Application/DomainEventHandlers/SendOrderCreatedCommunicationWhenOrderStartedDomainEventHandler.cs" rel="noopener noreferrer"&gt;Ordering dispatches&lt;/a&gt; with the model it already has:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;communicationsClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DispatchAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;CommunicationIntents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderCreated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;buyer&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;TransactionModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OrderCreatedModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderItems&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OrderCreatedItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProductId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProductName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Units&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UnitPrice&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToArray&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetTotal&lt;/span&gt;&lt;span class="p"&gt;())));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives Communications a solid starting point without making the originating service build the final communication.&lt;/p&gt;

&lt;p&gt;There are two models in play from here on. The transactional model is the context supplied at dispatch. The content model used for rendering keeps developing as that context moves through Communications.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ordering
    |
    | OrderCreated
    | + transactional model
    | + recipient identity
    v
Communications
    |
    | composition
    v
Enriched content context
    |
    v
Channel-specific rendering
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How much belongs in that initial model depends on the communication.&lt;/p&gt;

&lt;h2&gt;
  
  
  When should data enter the communication?
&lt;/h2&gt;

&lt;p&gt;Data can enter composition at a few different points.&lt;/p&gt;

&lt;p&gt;An originating service can supply most of the model at dispatch. That works well when the service already loaded the relevant state for the business operation. &lt;code&gt;OrderCreated&lt;/code&gt; fits: Ordering already knows which products were bought, how many, and at what price.&lt;/p&gt;

&lt;p&gt;Another communication might arrive mostly as references and lean on resolution during composition. If Communications needs data owned by another bounded context, passing a stable identifier is usually better than copying that domain's data through the originating service.&lt;/p&gt;

&lt;p&gt;And sometimes the right model is assembled later. A communication may be delayed, accumulated with related ones, or enriched with information that changes between the business operation and rendering. Part 1's five-orders summary is that case.&lt;/p&gt;

&lt;p&gt;These choices sit on a spectrum:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;More data supplied at dispatch
&amp;lt;----------------------------------------------&amp;gt;

More data resolved during composition
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's no prize for pushing every communication to one end.&lt;/p&gt;

&lt;p&gt;For eShop we'll stay near the simple end. Ordering supplies the order information it already has, and Communications does two pieces of cross-service composition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Buyer identity
      |
      v
Identity resolution

Product identifiers
      |
      v
Catalog enrichment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both use domains eShop already has, which makes them a good way to exercise Transmitly's enrichment workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resolving the recipient
&lt;/h2&gt;

&lt;p&gt;Ordering identifies the buyer because the order belongs to someone. How to reach that person is a different concern.&lt;/p&gt;

&lt;p&gt;In Part 2, Ordering handed Communications an email address taken from the buyer's access token. That worked, but it meant Ordering was supplying an address. Once SMS and push arrive, the same approach would have Ordering supplying phone numbers and device tokens too, and address management would leak into the ordering domain.&lt;/p&gt;

&lt;p&gt;Communications can resolve the identity itself, so Ordering never has to know how a buyer is reached. The value passed to &lt;code&gt;DispatchAsync()&lt;/code&gt; becomes an identity reference built from the identifier Ordering already owns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;buyer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;IdentityReference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CommunicationIdentityTypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Buyer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;domainEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ordering no longer reads the email claim at all, and &lt;code&gt;GetUserEmail()&lt;/code&gt; is gone from its identity service.&lt;/p&gt;

&lt;p&gt;When Communications processes the dispatch, Transmitly resolves that reference into one or more &lt;code&gt;IPlatformIdentityProfile&lt;/code&gt; instances. Resolvers are &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%203/src/Communications.API/Program.cs" rel="noopener noreferrer"&gt;registered by identity type&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTransmitly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tly&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;tly&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddPlatformIdentityResolver&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IdentityServerCustomerIdentityResolver&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;CommunicationIdentityTypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Buyer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Transmitly only runs a resolver whose type matches the reference, which is why &lt;code&gt;Buyer&lt;/code&gt; is a &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%203/src/eShop.ServiceDefaults/Communications/CommunicationIdentityTypes.cs" rel="noopener noreferrer"&gt;shared constant&lt;/a&gt; rather than a string typed in two services. &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%203/src/Communications.API/IdentityServerCustomerIdentityResolver.cs" rel="noopener noreferrer"&gt;eShop's resolver&lt;/a&gt; asks the service that owns the data. It gets a client-credentials token from Identity and calls a new &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%203/src/Identity.API/Apis/IdentityApi.cs" rel="noopener noreferrer"&gt;&lt;code&gt;POST /api/identities/resolve&lt;/code&gt;&lt;/a&gt; endpoint, which returns the buyer's name along with the email address and phone number on the account, each marked as verified or not. On the Identity side, &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%203/src/Identity.API/Configuration/Config.cs" rel="noopener noreferrer"&gt;&lt;code&gt;Config.cs&lt;/code&gt;&lt;/a&gt; adds a &lt;code&gt;communications&lt;/code&gt; client that may request the &lt;code&gt;IdentityServerApi&lt;/code&gt; scope, and the endpoint only accepts tokens carrying that scope.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;buyer-123
    |
    v
Identity resolution
    |
    v
IPlatformIdentityProfile
    |
    +-- Name
    |
    +-- Email address
    |
    +-- SMS address
    |
    +-- Other identities
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Today's email only needs the name and the email address. The rest matters once other channels show up, and Ordering keeps sending the same identity reference while that happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enriching the identity profile
&lt;/h2&gt;

&lt;p&gt;Resolution gives us the initial &lt;code&gt;IPlatformIdentityProfile&lt;/code&gt;. Profile enrichers run next, before the content itself is built.&lt;/p&gt;

&lt;p&gt;A profile might start with very little:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Platform identity
    buyer-123

Email
    ava@example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An enricher could then add recipient-oriented information:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Name
    Ava Rodriguez

Locale
    en-US

Time zone
    America/Denver

Communication preferences
    Email: enabled
    SMS: enabled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sources will differ between applications. Some systems get almost all of this during identity resolution. Others keep customer profiles, preference stores, tenant configuration, or communication-specific data somewhere else.&lt;/p&gt;

&lt;p&gt;These enrichers work on the recipient. They establish who the recipient is in communications terms, how to reach them, and which recipient-specific details should feed later decisions. This is the locale and preferences row from Part 1's composition model.&lt;/p&gt;

&lt;p&gt;eShop is the first kind of system. Identity resolution already returns the buyer's name and the addresses Identity holds for the account, which is everything today's email needs, and eShop has no locale or preference store to draw from. So our pipeline doesn't register a profile enricher yet. Part 4 uses this stage for push device tokens, which Identity doesn't have.&lt;/p&gt;

&lt;p&gt;Once profiles are resolved and enriched, Transmitly builds the content context for the pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enriching the transactional content
&lt;/h2&gt;

&lt;p&gt;Content-model enrichment deals with the subject of the communication.&lt;/p&gt;

&lt;p&gt;Our transactional model already has the order facts Ordering supplied:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Order
    OrderId
    OrderDate
    Total

Items
    ProductId
    ProductName
    Quantity
    UnitPrice
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;ProductId&lt;/code&gt; values give us a natural link to eShop's Catalog service.&lt;/p&gt;

&lt;p&gt;Catalog exposes a &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%203/src/Catalog.API/Apis/CatalogApi.cs" rel="noopener noreferrer"&gt;batch endpoint&lt;/a&gt; that returns several items by identifier, which is handy, since an order can contain many products.&lt;/p&gt;

&lt;p&gt;A &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%203/src/Communications.API/OrderCreated/CatalogContentModelEnricher.cs" rel="noopener noreferrer"&gt;content-model enricher&lt;/a&gt; can collect those identifiers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;productIds&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Items&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProductId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Distinct&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToArray&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and fetch their Catalog representations in one call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /api/catalog/items/by
    ?ids=1
    &amp;amp;ids=2
    &amp;amp;ids=3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Catalog contributes each product's description, a route to its picture, and its brand and type. The batch endpoint returns brand and type as identifiers, so the enricher also reads Catalog's brand and type lists to get their names.&lt;/p&gt;

&lt;p&gt;If Catalog can't be reached, the enricher logs a warning and the email goes out without product details. The order facts still come from Ordering.&lt;/p&gt;

&lt;p&gt;The enricher extends each item from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ProductId
ProductName
Quantity
PurchasedUnitPrice
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ProductId
ProductName
Quantity
PurchasedUnitPrice
Brand
ProductType
Description
ProductImage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model gets richer without Ordering taking responsibility for Catalog's representation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transactional facts and enrichment data
&lt;/h2&gt;

&lt;p&gt;Bringing in Catalog data raises a semantic question. The price stored on the order and the current Catalog price can both exist, and they mean different things.&lt;/p&gt;

&lt;p&gt;Say the customer bought an item for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$29.99
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and Catalog now lists it for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$34.99
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The order confirmation should show what the customer actually paid. eShop's &lt;code&gt;OrderItem&lt;/code&gt; keeps the unit price from when the item was added to the order, alongside its product identifier and other transactional values. The enricher copies that price into &lt;code&gt;PurchasedUnitPrice&lt;/code&gt; and never reads Catalog's.&lt;/p&gt;

&lt;p&gt;Catalog adds context without redefining the transaction. That suggests a rule for enrichment:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;An enricher should understand the semantics of the model it is enriching. Current data can supplement transactional data, refresh it when the communication requires current state, or participate in a new derived representation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Which behavior fits depends on the communication. For &lt;code&gt;OrderCreated&lt;/code&gt;, the purchased quantity and price describe the transaction, and current Catalog data layers richer product presentation on top. Another pipeline might need something quite different.&lt;/p&gt;

&lt;h2&gt;
  
  
  From dispatch to composed content
&lt;/h2&gt;

&lt;p&gt;With identity resolution and Catalog enrichment in place, the full composition path looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ordering
    |
    | DispatchAsync("OrderCreated", ...)
    |
    | Transactional model
    |   OrderId
    |   OrderDate
    |   Items
    |       ProductId
    |       ProductName
    |       Quantity
    |       UnitPrice
    |   Total
    |
    | Recipient
    |   BuyerIdentityId
    v
Communications
    |
    v
Resolve BuyerIdentityId
    |
    v
IPlatformIdentityProfile[]
    |
    v
PlatformIdentityProfile enrichers
    (none needed for eShop yet)
    |
    v
Build dispatch/content context
    |
    v
ContentModel enrichers
    |
    +-- Batch resolve ProductIds from Catalog
    |
    +-- Add product presentation data
    |
    v
Enriched OrderCreated content context
    |
    v
Render channel content
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application supplied an intent, a recipient reference, and some transactional context. The pipeline turned those into a model built for communication.&lt;/p&gt;

&lt;h2&gt;
  
  
  A purpose-built content model
&lt;/h2&gt;

&lt;p&gt;By the time rendering starts, the &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%203/src/Communications.API/OrderCreated/OrderCreatedContentModel.cs" rel="noopener noreferrer"&gt;content context&lt;/a&gt; holds information from several sources:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;OrderCreatedContentModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;RecipientModel&lt;/span&gt; &lt;span class="n"&gt;Recipient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;OrderDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IReadOnlyCollection&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderCreatedContentItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Items&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;RecipientModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;FirstName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;OrderCreatedContentItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;ProductId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;ProductName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;PurchasedUnitPrice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Brand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;ProductType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;ProductImage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Broken down by where each piece came from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ordering
    OrderId
    OrderDate
    ProductName
    Quantity
    PurchasedUnitPrice
    Total

Identity
    FirstName
    communication addresses

Catalog
    Brand
    ProductType
    Description
    product presentation data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Neither the template nor the channel provider has to reconstruct that composition. Data spread across the application has already been turned into one communication context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why make enrichment part of the pipeline?
&lt;/h2&gt;

&lt;p&gt;We could do all of this before calling Transmitly. Ordering could resolve the customer, call Catalog, build a large content model, pick an address, and submit a finished message.&lt;/p&gt;

&lt;p&gt;That's the leak Part 1 described. Ordering slowly turns into an orchestrator for data that exists mostly to produce communications, just because it happened to create the intent.&lt;/p&gt;

&lt;p&gt;Transmitly gives that composition an explicit lifecycle instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Dispatch
   |
   v
Identity resolution
   |
   v
Identity enrichment
   |
   v
Content enrichment
   |
   v
Channel rendering
   |
   v
Delivery
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;OrderCreated&lt;/code&gt; pipeline can evolve around this lifecycle while Ordering keeps making the same call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;communicationsClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DispatchAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;CommunicationIntents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderCreated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;buyer&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;TransactionModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A new requirement for product metadata goes in a content enricher. More recipient information comes from an identity-profile enricher. Every channel in the pipeline then reuses the resulting context, and none of that work spreads into the services that create communications.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enrichers also give us control over timing
&lt;/h2&gt;

&lt;p&gt;So far, the example uses Ordering's transactional model and adds Catalog data. That's the simplest useful implementation, but supplying a model to &lt;code&gt;DispatchAsync()&lt;/code&gt; doesn't commit us to rendering those exact values. Composition might happen later.&lt;/p&gt;

&lt;p&gt;Imagine an ordering system that lets customers modify an order for a while after placing it. Over a few minutes a customer might:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Place order
    |
Add item
    |
Change quantity
    |
Remove item
    |
Update payment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Turning each of those into a notification produces a stream of messages that's technically accurate and practically annoying.&lt;/p&gt;

&lt;p&gt;A communication policy could instead accumulate the related activity for a window, much like Part 1's five-orders summary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderPlaced -------\
ItemAdded ----------\
QuantityChanged -----&amp;gt; accumulated communication
ItemRemoved --------/
PaymentUpdated ----/
                         |
                         | N-minute window
                         v
                  ready to compose
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the accumulated communication is ready, Communications might decide the most useful thing to show is the current order rather than the sequence of models originally supplied.&lt;/p&gt;

&lt;p&gt;An order content enricher can take the &lt;code&gt;OrderId&lt;/code&gt;, fetch the latest authoritative state, and update the content model before rendering:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Accumulated context
       |
       | OrderId
       v
Order enricher
       |
       v
Current order state
       |
       v
One composed communication
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The original dispatch models still mattered. They described the operations that triggered the communication and carried the data available at each point. The communication policy decides what's finally presented.&lt;/p&gt;

&lt;p&gt;The general rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Dispatch captures the context that caused a communication. Composition determines the information ultimately presented to the recipient.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sometimes those two models are nearly identical. Sometimes composition adds a few properties. A delayed or accumulated workflow may refresh much more before rendering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resolving at composition time is another valid starting point
&lt;/h2&gt;

&lt;p&gt;The same reasoning explains why another application might dispatch a much smaller model from the start.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderCreated
    OrderId
    OrderDate
    Items
    Total
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;it could send:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderCreated
    OrderId
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and let an order enricher resolve the full state later.&lt;/p&gt;

&lt;p&gt;That can make sense when the source domain exposes a stable historical representation, when communications are routinely delayed, or when current state matters more than saving a lookup.&lt;/p&gt;

&lt;p&gt;Our eShop implementation doesn't need that. The order is already in hand when it's created, so passing a useful transactional model keeps the example direct. The architecture leaves the choice open for intents that need different semantics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Direct calls versus local projections
&lt;/h2&gt;

&lt;p&gt;The Catalog enricher can start with the simplest data access: call Catalog during composition.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ContentModel enricher
       |
       v
Catalog API
       |
       v
Catalog items
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For this example's volume and purpose, that's enough.&lt;/p&gt;

&lt;p&gt;A larger communications system might get enricher data differently. Communications could keep a local projection fed by Catalog integration events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Catalog
    |
    | integration events
    v
Communications product projection
    |
    v
Catalog enricher
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From the pipeline's point of view, the enrichment stage doesn't change. Only the enricher's data source does.&lt;/p&gt;

&lt;p&gt;Recipient profiles can evolve the same way. A platform identity resolver might call an identity service directly at first, then later resolve from communications-owned data synchronized from that service.&lt;/p&gt;

&lt;p&gt;The architecture can grow when latency, availability, or throughput actually call for it. The first implementation doesn't need any of those mechanisms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rendering comes last
&lt;/h2&gt;

&lt;p&gt;Once identity resolution and enrichment finish, the Communications service has one shared context that each channel can use to express the intent. This is where keeping the communication intent separate from the channel content pays off.&lt;/p&gt;

&lt;p&gt;Our intent is still:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderCreated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The enriched content model contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderCreatedContentModel

Recipient
    FirstName

Order
    OrderId
    OrderDate
    Total

Items
    ProductName
    Quantity
    PurchasedUnitPrice
    Brand
    ProductType
    Description
    ProductImage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The recipient's addresses aren't in it. They stay on the resolved identity profile, which is where each channel looks for an address it can use.&lt;/p&gt;

&lt;p&gt;Email has room to show most of the content model directly. &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%203/src/Communications.API/OrderCreated/OrderCreatedEmail.cs" rel="noopener noreferrer"&gt;The code-defined email template&lt;/a&gt; renders a summary of the order. If alice, one of eShop's seeded buyers, orders two pairs of hiking boots and a climbing harness, she gets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Subject:
Thanks for your order #123

Hi Alice,

We've received your order. We'll let you know when it ships.

2 × Wanderer Black Hiking Boots
    Daybird
    $109.99 each

1 × Summit Pro Harness
    Gravitator
    $89.99

Order total: $309.97

View your order:
https://eshop.example/user/orders
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;eShop has no page for a single order, so the link goes to the buyer's order list. The other channels come in Part 4, but here's how differently they'll use the same context.&lt;/p&gt;

&lt;p&gt;SMS has different constraints. Repeating the full email would waste the channel, so the same &lt;code&gt;OrderCreated&lt;/code&gt; pipeline can define a much shorter SMS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;We've received your eShop order #123.
View your order: https://eshop.example/user/orders
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Push can be shorter still, because the notification itself can open the app. The visible part is a title and a sentence, and the data tells the app which order to show:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Title:
Thanks for your order

Body:
Order #123 has been received.

Data:
action:  open-order
orderId: 123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three express the same communication:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                         OrderCreated
                              |
                    enriched content context
                              |
              +---------------+---------------+
              |               |               |
              v               v               v
            Email            SMS             Push
              |               |               |
       detailed summary    short link      brief alert
       and order link      to order        + app action
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ordering didn't have to make any of those choices.&lt;/p&gt;

&lt;p&gt;Communications knows things Ordering doesn't need to reason about: which addresses the recipient profile has, which channels the recipient has enabled, how much content suits each medium, whether a channel can link into the app, and which providers are configured to deliver it. Those factors shape the communication strategy independently of the intent.&lt;/p&gt;

&lt;p&gt;For one customer, &lt;code&gt;OrderCreated&lt;/code&gt; might mean email only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderCreated
    |
    +-- Email
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Another might prefer SMS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderCreated
    |
    +-- SMS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A mobile-app user might get push immediately and a richer email for reference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderCreated
    |
    +-- Push
    |
    +-- Email
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The content differs by channel, and the enriched context underneath is shared.&lt;/p&gt;

&lt;p&gt;So Ordering doesn't dispatch &lt;code&gt;OrderCreatedEmail&lt;/code&gt;, &lt;code&gt;OrderCreatedSms&lt;/code&gt;, or &lt;code&gt;OrderCreatedPush&lt;/code&gt;, the kind of channel-bound name Part 1 warned against. (Those names do show up as template classes inside Communications, starting with this part's &lt;code&gt;OrderCreatedEmail&lt;/code&gt;, which is where channel-specific names belong.)&lt;/p&gt;

&lt;p&gt;It dispatches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;communicationsClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DispatchAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;CommunicationIntents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderCreated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;buyer&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;TransactionModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Communications decides how &lt;code&gt;OrderCreated&lt;/code&gt; is expressed for that recipient under the current policy. For an imperative like the hypothetical &lt;code&gt;PasswordReset&lt;/code&gt; from Part 2, the pipeline fixes those choices instead of leaving them to policy. Resolution, enrichment, and rendering work the same way in both cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Communications domain is taking shape
&lt;/h2&gt;

&lt;p&gt;Our &lt;code&gt;OrderCreated&lt;/code&gt; implementation now has several stages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ordering
    |
    | business operation
    v
OrderCreated intent
    |
    | transactional context
    | recipient reference
    v
Communications
    |
    +-- identity resolution
    |
    +-- identity-profile enrichment (available, unused for now)
    |
    +-- content-model enrichment
    |
    +-- channel-specific rendering
    |
    +-- provider dispatch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each stage adds information or applies policy at the point where it's useful.&lt;/p&gt;

&lt;p&gt;Ordering contributes the context it already has when the order is created. Identity resolution turns the buyer reference into a recipient Communications can address, and for now it also supplies everything the email needs to know about the buyer. Profile enrichers can add recipient information later. The Catalog content enricher uses the product identifiers already on the order to pull presentation data through eShop's existing batch Catalog API.&lt;/p&gt;

&lt;p&gt;Every configured channel then gets that context, so email, SMS, push, or anything else can present the same intent in the form that suits it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where we ended up
&lt;/h2&gt;

&lt;p&gt;The transactional model passed to &lt;code&gt;DispatchAsync()&lt;/code&gt; gave &lt;code&gt;OrderCreated&lt;/code&gt; a head start. Ordering already had the order loaded, so we projected the relevant state instead of loading it again somewhere else.&lt;/p&gt;

&lt;p&gt;The rest of the model was built inside Communications:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DispatchAsync(...)
       |
       | Order transactional model
       | Buyer identity reference
       v
Resolve identities
       |
       v
IPlatformIdentityProfile[]
       |
       v
Build content context
       |
       v
Catalog ContentModel enricher
       |
       | ProductId -&amp;gt; richer product information
       v
Enriched content context
       |
       +-------- Email template
       |
       +-------- SMS template   (Part 4)
       |
       +-------- Push content   (Part 4)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Other communications can start differently. One might arrive with little more than identifiers and resolve most of its state during composition. A delayed one might refresh before rendering, and a high-volume system might feed its enrichers from local projections. An accumulated one might combine several operations and compose a single message from the latest state.&lt;/p&gt;

&lt;p&gt;They all fit the same pipeline, because the transactional model is only an input to composition.&lt;/p&gt;

&lt;p&gt;For eShop, Identity and Catalog are enough to show the mechanics without inventing services the application doesn't have. Between them they cover the two main kinds of enrichment: who we're communicating with, and what we're communicating about.&lt;/p&gt;

&lt;p&gt;Once that context exists, each channel can use it differently. Today, email gives the detailed order summary. When SMS and push join the pipeline, SMS will link to the buyer's orders and push will carry the data an app needs to open the order.&lt;/p&gt;




&lt;p&gt;Next, we'll demonstrate how the same intent goes out as an email, an SMS, and a push notification, with the simulator standing in for SMTP, Twilio, and Firebase.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>dotnet</category>
      <category>programming</category>
      <category>csharp</category>
    </item>
    <item>
      <title>Part 2: Sending Email Without Choosing Email</title>
      <dc:creator>Jeremy</dc:creator>
      <pubDate>Mon, 19 Oct 2026 23:00:00 +0000</pubDate>
      <link>https://dev.to/jeremy_tly/part-2-sending-email-without-choosing-email-1eim</link>
      <guid>https://dev.to/jeremy_tly/part-2-sending-email-without-choosing-email-1eim</guid>
      <description>&lt;p&gt;Part 1 ended with a plan: add Transmitly to Microsoft eShop, create a new boundary around an order communication, and start with simulated delivery.&lt;/p&gt;

&lt;p&gt;This article adds an &lt;code&gt;OrderCreated&lt;/code&gt; communication. The first version produces an email, but as we work through each step, we'll see how dealing with intents helps us expand functionality.&lt;/p&gt;

&lt;p&gt;The flow is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ordering [Transmitly]
    |
    | OrderCreated
    v
Communications [Transmitly]
    |
    | communication policy
    v
Email
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Transmitly provides the abstraction on both sides of this boundary. eShop is distributed today, but the same application code could just as easily run a Transmitly pipeline locally. Moving communication processing into another service becomes a configuration and middleware concern, not a new programming model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Expressing the communication from Ordering
&lt;/h2&gt;

&lt;p&gt;eShop already has a well-defined order-creation operation. When a buyer checks out, Ordering creates the order and owns that state, so it has the business context to say an &lt;code&gt;OrderCreated&lt;/code&gt; communication intent now exists.&lt;/p&gt;

&lt;p&gt;At the application level, we want that call to stay small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;communicationsClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DispatchAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;CommunicationIntents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderCreated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;buyerEmail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;domainEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What matters here is the vocabulary.&lt;/p&gt;

&lt;p&gt;Ordering expresses an intent to communicate with &lt;code&gt;OrderCreated&lt;/code&gt;, not a delivery mechanism. The configured pipeline decides what that means operationally.&lt;/p&gt;

&lt;p&gt;Ordering makes this call itself, inside &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%202/src/Ordering.API/Application/DomainEventHandlers/SendOrderCreatedCommunicationWhenOrderStartedDomainEventHandler.cs" rel="noopener noreferrer"&gt;&lt;code&gt;SendOrderCreatedCommunicationWhenOrderStartedDomainEventHandler&lt;/code&gt;&lt;/a&gt;. It handles &lt;code&gt;OrderStartedDomainEvent&lt;/code&gt;, the domain event eShop raises when a new order is created. Another handler for the same event, &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%202/src/Ordering.API/Application/DomainEventHandlers/ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler.cs" rel="noopener noreferrer"&gt;&lt;code&gt;ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler&lt;/code&gt;&lt;/a&gt;, publishes &lt;code&gt;OrderStatusChangedToSubmittedIntegrationEvent&lt;/code&gt;. Communications could subscribe to that integration event and guess that a message is warranted, but as Part 1 argued, that's the wrong way around. The integration event tells other services a fact. The dispatch tells Communications that Ordering has decided there's an intent to communicate, and it's up to Communications to decide how and when to communicate, if at all.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;buyerEmail&lt;/code&gt; comes from &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%202/src/Ordering.API/Infrastructure/Services/IdentityService.cs" rel="noopener noreferrer"&gt;the &lt;code&gt;email&lt;/code&gt; claim&lt;/a&gt; on the access token that arrives with the create-order request. Ordering reads it for this one call and doesn't store it. That keeps the first slice small, but it's a shortcut, and we'll come back to it.&lt;/p&gt;

&lt;p&gt;In a smaller application, the whole pipeline could be registered in the same host &lt;em&gt;(Program.cs)&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;communicationsClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CommunicationsClientBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddSimulationSupport&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddPipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CommunicationIntents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderCreated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"orders@eshop.local"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsIdentityAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"eShop"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Subject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddStringTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="s"&gt;"Thanks for your order!"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

                &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TextBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddStringTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="s"&gt;"We've received your order and we'll let you know when it ships."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BuildClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Calling &lt;code&gt;DispatchAsync()&lt;/code&gt; triggers our &lt;code&gt;OrderCreated&lt;/code&gt; pipeline, composes the configured email, and hands it to the simulation provider.&lt;/p&gt;

&lt;p&gt;When communication requirements are local to one application, that's a perfectly reasonable deployment model.&lt;/p&gt;

&lt;p&gt;eShop gives us a reason to go further.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving pipeline execution into Communications
&lt;/h2&gt;

&lt;p&gt;Once several services need transactional communications, centralizing composition and delivery starts to pay off.&lt;/p&gt;

&lt;p&gt;Ordering may need &lt;code&gt;OrderCreated&lt;/code&gt;, &lt;code&gt;OrderShipped&lt;/code&gt;, and &lt;code&gt;OrderCancelled&lt;/code&gt;. Identity may need &lt;code&gt;PasswordReset&lt;/code&gt;. Payment may eventually need &lt;code&gt;PaymentFailed&lt;/code&gt;. Over time each of those can pick up templates, localization, recipient preferences, multiple channels, and provider-specific behavior.&lt;/p&gt;

&lt;p&gt;Instead of spreading that configuration across every service, we move pipeline configuration and management into a dedicated Communications service.&lt;/p&gt;

&lt;p&gt;Even with a whole new service in the picture, our application code &lt;em&gt;still&lt;/em&gt; doesn't change. Transmitly's middleware gives us a seam between dispatching an intent and processing it.&lt;/p&gt;

&lt;p&gt;The path becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    Ordering
                       |
                       | DispatchAsync(
                       |   "OrderCreated",
                       |   buyerEmail,
                       |   { orderId })
                       v
                    Transmitly
                       |
                 client middleware
                       |
                       v
              Communications API
                       |
                    Transmitly
                       |
                  OrderCreated
                    pipeline
                       |
                       v
                     Email
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The middleware receives the communication context created during dispatch and forwards what the Communications service needs to continue processing it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/transmitly/transmitly/tree/main/samples/Microservices" rel="noopener noreferrer"&gt;Transmitly's Microservices sample&lt;/a&gt; uses the same pattern. Application code keeps working through &lt;code&gt;ICommunicationsClient&lt;/code&gt;, and middleware decides that processing continues across a service boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sharing the client configuration
&lt;/h2&gt;

&lt;p&gt;In an app like eShop, several services will probably need the same remote communications capability. Repeating the transport configuration in each project just invites drift, so this is a natural spot for a shared extension.&lt;/p&gt;

&lt;p&gt;In an application project, configuration can be as small as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddEshopCommunications&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%202/src/eShop.ServiceDefaults/Communications/EshopCommunicationsExtensions.cs" rel="noopener noreferrer"&gt;That extension&lt;/a&gt; configures Transmitly and registers &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%202/src/eShop.ServiceDefaults/Communications/EshopCommunicationsMiddleware.cs" rel="noopener noreferrer"&gt;the middleware&lt;/a&gt; that forwards communications to the central service.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AddEshopCommunications()
        |
        +-- register Transmitly
        |
        +-- add forwarding middleware
        |
        +-- configure Communications endpoint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ordering, Identity, Payment, and any other service can use the same extension and keep their own communication vocabulary.&lt;/p&gt;

&lt;p&gt;Their application code still depends on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;ICommunicationsClient&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and dispatches intents the same way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;communicationsClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DispatchAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;CommunicationIntents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderCreated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;buyerEmail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;domainEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shared code holds the stable intent names used across the transport boundary, not their pipeline definitions. &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%202/src/eShop.ServiceDefaults/Communications/CommunicationIntents.cs" rel="noopener noreferrer"&gt;&lt;code&gt;CommunicationIntents&lt;/code&gt;&lt;/a&gt; publishes the intents available to everyone. Composition, channels, templates, and delivery policy stay with Communications.&lt;/p&gt;

&lt;p&gt;This pays off if the deployment architecture changes later. A service could process pipelines locally in one environment and forward them remotely in another.&lt;/p&gt;

&lt;h2&gt;
  
  
  Waiting for the order to commit
&lt;/h2&gt;

&lt;p&gt;There's a timing problem hiding in that handler. eShop runs domain event handlers before it saves anything: &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%202/src/Ordering.Infrastructure/OrderingContext.cs" rel="noopener noreferrer"&gt;&lt;code&gt;OrderingContext.SaveEntitiesAsync&lt;/code&gt;&lt;/a&gt; dispatches domain events, then calls &lt;code&gt;SaveChanges&lt;/code&gt;, and &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%202/src/Ordering.API/Application/Behaviors/TransactionBehavior.cs" rel="noopener noreferrer"&gt;&lt;code&gt;TransactionBehavior&lt;/code&gt;&lt;/a&gt; commits the transaction after the command handler returns. Integration events cope with this through eShop's outbox. They're saved inside the transaction and published only after the commit.&lt;/p&gt;

&lt;p&gt;A dispatch sent straight from the handler doesn't get that protection. It goes out while the transaction is still open. If the commit then fails, the buyer gets a confirmation for an order that doesn't exist.&lt;/p&gt;

&lt;p&gt;The fix uses the same seam as forwarding: client middleware. Ordering &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%202/src/Ordering.API/Program.cs" rel="noopener noreferrer"&gt;registers one more middleware&lt;/a&gt; next to the shared setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddEshopCommunications&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ICommunicationClientMiddleware&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TransactionalCommunicationsMiddleware&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;AddEshopCommunications()&lt;/code&gt; adds any registered middleware after the forwarding middleware, so it sees each dispatch first. &lt;code&gt;TransactionBehavior&lt;/code&gt; opens a &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%202/src/Ordering.API/Infrastructure/Communications/PendingCommunications.cs" rel="noopener noreferrer"&gt;scope&lt;/a&gt; around each transaction and flushes it once the commit and the integration events are done:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pendingCommunications&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PendingCommunications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Begin&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// ... begin, handle the command, commit ...&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_orderingIntegrationEventService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PublishEventsThroughEventBusAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transactionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;pendingCommunications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FlushAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;None&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While the scope is open, &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%202/src/Ordering.API/Infrastructure/Communications/TransactionalCommunicationsMiddleware.cs" rel="noopener noreferrer"&gt;the middleware&lt;/a&gt; holds each dispatch instead of forwarding it. If the transaction fails, the scope is disposed without a flush and the dispatch is dropped. The handler still calls &lt;code&gt;communicationsClient.DispatchAsync()&lt;/code&gt; exactly as before. It doesn't know the dispatch was deferred.&lt;/p&gt;

&lt;p&gt;Held dispatches live in memory. If the process stops between the commit and the flush, the communication is lost. A durable outbox for communication intents is possible, but it's outside the scope of this series.&lt;/p&gt;

&lt;h2&gt;
  
  
  A general dispatch boundary
&lt;/h2&gt;

&lt;p&gt;The Communications service's &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%202/src/Communications.API/Apis/CommunicationsApi.cs" rel="noopener noreferrer"&gt;HTTP surface&lt;/a&gt; can follow the same principle.&lt;/p&gt;

&lt;p&gt;Every application performs the same operation: dispatch a communication intent along with the context needed to process it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /api/communications/dispatch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dispatch context carries the intent, the recipients, the transactional model, any dispatch options, and metadata about the dispatch itself. In the &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%202/src/eShop.ServiceDefaults/Communications/CommunicationDispatchContract.cs" rel="noopener noreferrer"&gt;shared contract&lt;/a&gt; that's &lt;code&gt;CommunicationDispatchRequest&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CommunicationDispatchRequest

    Intent
        OrderCreated

    Recipients
        alice@example.com

    Model
        orderId: 123

    AllowedChannels / PipelineId / Culture
        (empty: the pipeline decides)

    Metadata
        CorrelationId   trace id of the create-order request
        Source          Ordering.API
        DispatchedAt    2026-10-01T16:04:12Z
        Properties      baggage from the current activity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The forwarding middleware &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%202/src/eShop.ServiceDefaults/Communications/EshopCommunicationsMiddleware.cs" rel="noopener noreferrer"&gt;fills in the metadata&lt;/a&gt;, so application code doesn't pass any of it. The correlation id reuses the current trace id, which lets one dispatch be followed through both services' logs. Properties come from the activity's baggage, so a service can attach ambient context, such as a tenant, without changing the &lt;code&gt;DispatchAsync()&lt;/code&gt; call.&lt;/p&gt;

&lt;p&gt;When it arrives, the Communications service hands that context back to Transmitly, which resolves the intent against the configured pipeline catalog.&lt;/p&gt;

&lt;p&gt;So the transport API stays stable as the catalog grows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderCreated
OrderShipped
OrderCancelled
PasswordReset
PaymentFailed
WelcomeCustomer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each intent is a different communication policy. They all enter Communications through the same dispatch operation.&lt;/p&gt;

&lt;p&gt;That gives the service a clean shape. The HTTP layer transports communication context. Transmitly handles data resolution, composition, and dispatch strategy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defining the pipeline catalog
&lt;/h2&gt;

&lt;p&gt;For the first implementation, we define the pipeline directly in code, in the &lt;a href="https://github.com/transmitly/transmitly/blob/main/samples/eShop%20Series/part%202/src/Communications.API/Program.cs" rel="noopener noreferrer"&gt;Communications service's &lt;code&gt;Program.cs&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTransmitly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tly&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;tly&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddSimulationSupport&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddPipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CommunicationIntents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderCreated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"orders@eshop.local"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsIdentityAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"eShop"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Subject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddStringTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="s"&gt;"Thanks for your order!"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

                &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TextBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddStringTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="s"&gt;"We've received your order and we'll let you know when it ships."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks familiar, doesn't it? It's the same pipeline from the single-host example, now living in Communications. That gives &lt;code&gt;OrderCreated&lt;/code&gt; a concrete meaning inside the service.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderCreated
     |
     v
   Email
     |
     v
Simulation Provider
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Static configuration works well for a sample because the whole policy fits in a few lines of code. It's also enough for plenty of production applications.&lt;/p&gt;

&lt;p&gt;More complex systems may eventually want pipeline configuration to come from somewhere else.&lt;/p&gt;

&lt;p&gt;A database might hold tenant-specific communication rules. Templates might live in a CMS, and localization in a content service. Or administrators might need to change communication policy without redeploying the Communications service.&lt;/p&gt;

&lt;p&gt;Those are all different implementations of the same responsibility:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    Communications
                          |
                          v
                  Pipeline Resolution
                          |
             +------------+------------+
             |            |            |
             v            v            v
           Code        Database       CMS
             \            |            /
              \           |           /
               +----------+----------+
                          |
                          v
                 Communication Pipeline
                          |
                   +------+------+
                   |      |      |
                 Email   SMS    Push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Transmitly's extensibility leaves the Communications domain room to grow in that direction without touching the services that dispatch intents.&lt;/p&gt;

&lt;p&gt;So the real decision isn't where pipeline configuration is stored. It's where responsibility for the pipeline lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why use Transmitly on the client side?
&lt;/h2&gt;

&lt;p&gt;At first glance, the originating service needs very little. We could write an &lt;code&gt;HttpClient&lt;/code&gt;, serialize an intent, and post it to &lt;code&gt;/api/communications/dispatch&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For a single application with a fixed distributed architecture, that might be enough.&lt;/p&gt;

&lt;p&gt;Transmitly gives us a broader abstraction.&lt;/p&gt;

&lt;p&gt;The same application call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;communicationsClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DispatchAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;CommunicationIntents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderCreated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;buyerEmail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;domainEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;works in either of these deployments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Local processing

Application
    |
    v
ICommunicationsClient
    |
    v
Pipeline
    |
    v
Provider
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Remote processing

Application
    |
    v
ICommunicationsClient
    |
    v
Forwarding Middleware
    |
    v
Communications Service
    |
    v
Pipeline
    |
    v
Provider
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application is coupled to the communication concept, not to where that communication gets processed.&lt;/p&gt;

&lt;p&gt;That matters as a system changes.&lt;/p&gt;

&lt;p&gt;An application can start with local SMTP and a handful of pipelines. As communications grow, a shared extension can configure the same client to forward dispatches to a central service. Pipeline definitions then move into Communications, along with templates, recipient resolution, delivery reporting, and channel policy.&lt;/p&gt;

&lt;p&gt;The business code that created those intents doesn't have to follow that migration.&lt;/p&gt;

&lt;p&gt;That's the practical reason to use Transmitly on both sides. One communications programming model, while middleware, pipeline configuration, and deployment topology evolve around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping the communication context meaningful
&lt;/h2&gt;

&lt;p&gt;The generic dispatch endpoint works because the payload is a communication intent, not a prebuilt message.&lt;/p&gt;

&lt;p&gt;In this first slice, Ordering supplies the recipient as an email address, along with a model containing information it owns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderCreated

Recipient
    alice@example.com

Model
    orderId: 123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Communications uses that context to build whatever the configured pipeline needs. Today that's one email.&lt;/p&gt;

&lt;p&gt;This is the shortcut from earlier. By passing an address, Ordering has taken on a small piece of address management. One email claim from a token is easy to live with. But when SMS arrives, the same buyer also needs a phone number:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+1 555 ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and push delivery needs one or more device tokens. None of that belongs in Ordering. Ordering knows the buyer as an identity, and knowing how to reach that identity on each channel is Communications' job.&lt;/p&gt;

&lt;p&gt;In Part 3, Ordering stops passing an address. It dispatches the buyer's identity, and Communications resolves that identity to the addresses each channel needs. The intent and the model stay the same. The recipient becomes an identity reference instead of an address.&lt;/p&gt;

&lt;p&gt;Once recipients are resolved inside Communications, an &lt;code&gt;OrderCreated&lt;/code&gt; pipeline can grow from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderCreated
     |
     v
   Email
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderCreated
     |
     +----&amp;gt; Email
     |
     +----&amp;gt; SMS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;without changing anything Ordering sends.&lt;/p&gt;

&lt;p&gt;Recipient resolution, channel availability, and communication preferences all become part of the communications model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the first vertical slice
&lt;/h2&gt;

&lt;p&gt;For this article, we keep the first pipeline deliberately small.&lt;/p&gt;

&lt;p&gt;Ordering dispatches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;communicationsClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DispatchAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;CommunicationIntents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderCreated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;buyerEmail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;domainEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the order commits, Transmitly middleware forwards that communication context to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /api/communications/dispatch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Communications service resolves:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderCreated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and finds the pipeline we configured earlier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderCreated
     |
     v
Email
     |
     v
Simulation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The simulation provider gives us a complete dispatch path without bringing an external provider into the example.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌────────────────────────────┐
│          Ordering          │
│                            │
│ Order created              │
│      |                     │
│      v                     │
│ Dispatch("OrderCreated")   │
└─────────────┬──────────────┘
              |
              v
    Transmitly Middleware
              |
              v
  /api/communications/dispatch
              |
              v
┌────────────────────────────┐
│       Communications       │
│                            │
│ Pipeline Catalog           │
│      |                     │
│      +-- OrderCreated      │
│              |             │
│              v             │
│            Email           │
│              |             │
│              v             │
│          Simulation        │
└────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once that path works, swapping simulation for SMTP or SendGrid is a provider configuration change inside Communications.&lt;/p&gt;

&lt;p&gt;The more interesting change is adding a channel. If Product decides new orders should also send an SMS, the pipeline grows and Ordering keeps dispatching the same &lt;code&gt;OrderCreated&lt;/code&gt; intent.&lt;/p&gt;

&lt;p&gt;That's where the intent abstraction starts earning its keep.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding another service
&lt;/h2&gt;

&lt;p&gt;The design gets clearer once another part of eShop needs communications.&lt;/p&gt;

&lt;p&gt;Say Identity needs to send password-reset communications.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;PasswordReset&lt;/code&gt; isn't part of this series' code. It's here to show how a second service would plug into the same boundary.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Identity would use the same shared setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddEshopCommunications&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the same application abstraction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;communicationsClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DispatchAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;CommunicationIntents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PasswordReset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;recipient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The context would travel through the same dispatch endpoint to the same Communications service.&lt;/p&gt;

&lt;p&gt;The catalog would then contain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderCreated
PasswordReset
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each pipeline would have its own composition and channel policy. Both originating services would use the same communication model.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;PasswordReset&lt;/code&gt; isn't quite like &lt;code&gt;OrderCreated&lt;/code&gt;, though. In Part 1's terms it's closer to an imperative. Identity can't let channel policy or customer preferences decide where a reset link goes; it has to reach the verified address on the account. So the &lt;code&gt;PasswordReset&lt;/code&gt; pipeline would be locked to that channel and recipient, while &lt;code&gt;OrderCreated&lt;/code&gt; stays free to grow. The dispatch call would look the same either way. The difference lives in how Communications configures each pipeline.&lt;/p&gt;

&lt;p&gt;As the application grows, the architecture ends up looking like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ordering --------\
                  \
Identity ----------&amp;gt; Communications
                  /        |
Payment ----------/         v
                     Pipeline Catalog
                      /     |      \
                     /      |       \
            OrderCreated PasswordReset ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The shared code stays small because it defines how to reach Communications, not what Communications knows how to send.&lt;/p&gt;

&lt;p&gt;That keeps the catalog centralized without making every application depend on a catalog library.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The communications client and the applications don't have to evolve in lockstep. Developers can dispatch intents before Communications has configured them, and the communications team can wire up templates, channels, and delivery strategy on its own schedule. Until an intent has a pipeline, Transmitly reports the dispatch as Pipeline Not Found and nothing is sent, but nothing breaks either.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Where we ended up
&lt;/h2&gt;

&lt;p&gt;The requirement most applications start with is easy to state:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Send an email when an order is placed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The implementation gives the application a more durable concept:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderCreated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ordering dispatches that intent through &lt;code&gt;ICommunicationsClient&lt;/code&gt;, and Ordering's own middleware holds it until the order commits. Shared Transmitly configuration adds middleware that forwards the communication context to a generic dispatch endpoint. The Communications service resolves &lt;code&gt;OrderCreated&lt;/code&gt; against its pipeline catalog and, for now, produces one simulated email.&lt;/p&gt;

&lt;p&gt;The design separates three concerns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
    expresses communication intent

Transmitly infrastructure
    determines where it is processed

Communications
    determines how it is composed and delivered
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In eShop, processing happens in a dedicated service. A smaller application could use the same Transmitly API with locally configured pipelines. The Communications service defines its catalog in code today. A more advanced implementation could pull pipelines, templates, or other policy from a database, a CMS, or some other configuration system.&lt;/p&gt;

&lt;p&gt;Those choices can change independently because the business-facing abstraction stays the same:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;communicationsClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DispatchAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;CommunicationIntents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderCreated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;buyerEmail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;domainEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We have an email in the system now. The bigger result is the communications boundary around it.&lt;/p&gt;

&lt;p&gt;The one piece of that call that won't last is &lt;code&gt;buyerEmail&lt;/code&gt;.&lt;/p&gt;




&lt;p&gt;Next up, we'll stop Ordering from passing an email address, and have Communications resolve the buyer through Identity and enrich the order with product details from Catalog.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>programming</category>
    </item>
    <item>
      <title>Part 1: Where Should Communications Live?</title>
      <dc:creator>Jeremy</dc:creator>
      <pubDate>Sun, 18 Oct 2026 23:00:00 +0000</pubDate>
      <link>https://dev.to/jeremy_tly/part-1-where-should-communications-live-505l</link>
      <guid>https://dev.to/jeremy_tly/part-1-where-should-communications-live-505l</guid>
      <description>&lt;p&gt;Almost every application sends communications. Most of us call them notifications: order confirmations, password resets, welcome emails, shipping updates, security alerts. They're so ordinary that, while an application is new, nobody treats them as a separate architectural concern.&lt;/p&gt;

&lt;p&gt;They usually start like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;emailClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Maybe the application uses an SMTP server. Maybe it starts with a provider such as SendGrid. Either way the requirement is simple. Something happened, and the business rules say somebody needs to hear about it.&lt;/p&gt;

&lt;p&gt;For a small application this is often the right call. If the system sends a handful of emails and everything needed to build them is already in the same process, a separate communications architecture is a lot of infrastructure that won't pay for itself any time soon.&lt;/p&gt;

&lt;p&gt;Microsoft's &lt;a href="https://github.com/dotnet/eShop" rel="noopener noreferrer"&gt;.NET eShop reference application&lt;/a&gt; isn't small, and that makes it a good place to study the problem. It's already split along business contexts. Ordering owns orders. Identity owns users and authentication. Payment processing runs as its own service and reports outcomes back through integration events. The services talk through deliberate contracts instead of sharing one convenient object graph.&lt;/p&gt;

&lt;p&gt;eShop doesn't send customer communications today. If we added an order confirmation the way most applications start, we'd inject an &lt;code&gt;IEmailClient&lt;/code&gt; into Ordering and call it a day, and that small change would start working against the boundaries eShop was built around.&lt;/p&gt;

&lt;p&gt;This series adds a communications service to eShop. Before writing any code, we'll work out where the service's boundary belongs and why.&lt;/p&gt;

&lt;p&gt;The full source for every part of the series is in the &lt;a href="https://github.com/transmitly/transmitly/tree/main/samples/eShop%20Series" rel="noopener noreferrer"&gt;eShop sample in the Transmitly repository&lt;/a&gt;. Each part has its own folder, and each one builds on the code from the part before.&lt;/p&gt;

&lt;h2&gt;
  
  
  An order communication crosses domain boundaries
&lt;/h2&gt;

&lt;p&gt;Take the confirmation we send after an order is created. Ordering looks like the obvious owner. It knows the order exists, and it holds the order number, line items, quantities, and the rest of the ordering data.&lt;/p&gt;

&lt;p&gt;But a good order communication might also include the recipient's name and email address, payment information, shipping details, localization settings, or communication preferences. Each of those has a natural owner, and that owner isn't necessarily Ordering.&lt;/p&gt;

&lt;p&gt;You can see this in eShop. Ordering has its own model of a buyer, shaped for ordering. It keeps an identity identifier and whatever else Ordering needs to do its work. That model doesn't need to grow into a full representation of the user's identity just because we eventually want to send a message.&lt;/p&gt;

&lt;p&gt;Payment works the same way. It has its own lifecycle and sends relevant outcomes across a boundary. Ordering reacts to those outcomes and keeps ownership of its own state.&lt;/p&gt;

&lt;p&gt;This is one of the practical payoffs of Domain-Driven Design: a bounded context models only what it needs for its own responsibilities.&lt;/p&gt;

&lt;p&gt;Ordering understands a buyer in terms of placing and managing orders. Identity understands the same person differently, and Payment sees the transaction through yet another model. Those representations can coexist without collapsing into one universal &lt;code&gt;Customer&lt;/code&gt; object that every component in the system shares.&lt;/p&gt;

&lt;p&gt;Communications complicate this, because a message often needs a view of the business that spans several of those boundaries.&lt;/p&gt;

&lt;p&gt;An order confirmation is a representation, built for a recipient, of something that happened in the business. Serializing the &lt;code&gt;Order&lt;/code&gt; into HTML only covers part of it. Building the message may take data from Ordering, Identity, Payment, Shipping, and other contexts.&lt;/p&gt;

&lt;p&gt;That makes the ownership question easier to answer. The order may be the reason a communication exists, but the communication has its own model and its own lifecycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  When composition leaks back into the originating domain
&lt;/h2&gt;

&lt;p&gt;Say we make Ordering responsible for the whole communication. At first it needs only an order and an email address. Over time the message gets more useful, and more useful means more information.&lt;/p&gt;

&lt;p&gt;Customer service wants the payment status included. Product wants an estimated delivery date. Marketing wants localized copy. Later, customers get to choose between email, SMS, and push notifications.&lt;/p&gt;

&lt;p&gt;The flow starts to look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Order Created
     |
     v
 Ordering
     |
     +------&amp;gt; Identity
     |         recipient information and preferences
     |
     +------&amp;gt; Payment
     |         payment state
     |
     +------&amp;gt; Shipping
     |         delivery information
     |
     v
 Compose Communication
     |
     v
 Render Content
     |
     v
 Select Channel / Provider
     |
     v
 Deliver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's rarely a single moment when this feels obviously wrong. Every new dependency shows up attached to a reasonable requirement. We need the recipient's email address, so we fetch it. We need the payment status, so we fetch that too. Eventually Ordering has become an orchestrator for data and behavior that exist mostly to produce customer communications.&lt;/p&gt;

&lt;p&gt;The dependency is easy to miss because the communication really is about an order. The question is what each domain is responsible for knowing.&lt;/p&gt;

&lt;p&gt;Ordering should know that an order was created, paid, cancelled, or shipped, along with the identifiers and facts it needs to perform ordering behavior. It doesn't follow that Ordering should also know how those facts are presented to a recipient, where the recipient's addresses come from, which channels are appropriate, or how delivery works. Those concerns make up a domain of their own.&lt;/p&gt;

&lt;h2&gt;
  
  
  What eShop's existing boundaries already show
&lt;/h2&gt;

&lt;p&gt;eShop already has the kind of separation we want to keep. Information moves between parts of the application through deliberate contracts, so no domain has to understand another domain's internals.&lt;/p&gt;

&lt;p&gt;Integration events are one way eShop does this. A payment result can cross a service boundary as a fact, letting Ordering update the state it owns. An order status change can be published the same way, for any consumer with its own reason to care.&lt;/p&gt;

&lt;p&gt;That's a good pattern for boundaries. It's tempting to stretch it further and make Communications one more subscriber that watches those events for something worth sending, but a communication intent means something different from an integration event.&lt;/p&gt;

&lt;p&gt;An integration event says something happened and makes that fact available to whoever is interested. A communication intent says the service that owns a business operation has decided there is now something to communicate. The two can come from the same operation and still serve different purposes.&lt;/p&gt;

&lt;p&gt;Take an order that has just been created:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    Ordering
                       |
                 Order is created
                       |
            +----------+----------+
            |                     |
            v                     v
    Integration Event      Communication Intent
            |                     |
            v                     v
      Other Services        Communications
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ordering owns the business operation, so it's the natural place to decide that the operation creates a communication intent.&lt;/p&gt;

&lt;p&gt;It doesn't have to wait for a communications service to notice an event and guess that a message would probably help. The business behavior can be explicit: this operation produces an &lt;code&gt;OrderCreated&lt;/code&gt; communication intent. From there, the intent belongs to the communications domain, which gives us the boundary:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The service that owns the business operation creates the communication intent. The communications domain owns the lifecycle of that intent.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Communication intent describes purpose
&lt;/h2&gt;

&lt;p&gt;The word &lt;em&gt;intent&lt;/em&gt; separates the reason for communicating from the mechanism that fulfills it.&lt;/p&gt;

&lt;p&gt;Suppose Ordering dispatches an intent named:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderCreated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From Ordering's side, that intent can stay small: an order identifier, the buyer's identity identifier, a payment identifier, and anything else Ordering legitimately owns and considers relevant to the communication.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ordering
    |
    | OrderCreated
    | orderId: 123
    | paymentId: xyz
    | recipientId: abc
    | itemCount: 4
    v
Communications
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ordering has said why the communication exists, and Communications now has enough to start fulfilling it.&lt;/p&gt;

&lt;p&gt;Today's communication policy might produce a single email:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderCreated
     |
     v
   Email
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As the product evolves, the same intent might produce both email and SMS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderCreated
     |
     +----&amp;gt; Email
     |
     +----&amp;gt; SMS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Later, channel selection might depend on recipient preferences, geography, account type, urgency, or other communication-specific rules.&lt;/p&gt;

&lt;p&gt;None of those changes alter the intent, which is still to tell someone that an order was created. They change the organization's communication policy around it.&lt;/p&gt;

&lt;p&gt;So Ordering has no reason to bake today's delivery strategy into an abstraction like &lt;code&gt;SendOrderConfirmationEmail&lt;/code&gt;. That name welds together two decisions that change for different reasons: why we are communicating, and which channel we happen to use right now.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;OrderCreated&lt;/code&gt; is the more durable vocabulary. It states the business purpose and leaves the communications domain to decide what that purpose means operationally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Communications becomes a domain of composition
&lt;/h2&gt;

&lt;p&gt;Once an intent crosses into Communications, the problem shifts from managing business state to composing a communication.&lt;/p&gt;

&lt;p&gt;An &lt;code&gt;OrderCreated&lt;/code&gt; communication might need a model like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Order
    Ordering

Recipient
    Identity

Payment Status
    Payment

Delivery Information
    Shipping

Locale / Preferences
    Profile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The communications domain can build that model however suits the architecture. Some information gets resolved when the communication is composed. Some may already sit in a communications-oriented projection. Integration events can supply facts that Communications keeps for later. In more complex systems, an intent might be enriched gradually as related information arrives.&lt;/p&gt;

&lt;p&gt;Which approach fits depends on latency, consistency, resilience, and how much data is involved. Those are implementation details inside the larger ownership model.&lt;/p&gt;

&lt;p&gt;Ordering supplies the identifiers and facts it owns. Communications assembles the communication model, so the cross-domain work happens in one place.&lt;/p&gt;

&lt;p&gt;It also gives Communications a coherent set of responsibilities. Recipient resolution, message composition, localization, channel policy, template rendering, provider selection, delivery reporting, scheduling, suppression, retries, and auditing all belong to the lifecycle of a communication. None of them belong to the lifecycle of an order.&lt;/p&gt;

&lt;p&gt;Where Communications physically runs can vary. In one application it's a module; in another it's an independently deployed service. The architectural benefit comes from treating it as a separate responsibility before deciding where the code lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Intent has a lifecycle
&lt;/h2&gt;

&lt;p&gt;One consequence of this model: a communication intent doesn't have to mean immediate delivery.&lt;/p&gt;

&lt;p&gt;When communication code sits right beside the business operation, we drift toward a one-to-one relationship:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Business Event -&amp;gt; Message
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An order is created, so an email goes out. A payment succeeds, so another email goes out. Another order, another message.&lt;/p&gt;

&lt;p&gt;That works until communication policy gets more sophisticated.&lt;/p&gt;

&lt;p&gt;Say a customer places five separate orders in a short window, and each payment is processed independently. A straightforward implementation could send five order confirmations followed by five payment confirmations. Every event was communicated correctly, and the customer still got ten emails.&lt;/p&gt;

&lt;p&gt;Maybe the real requirement is to wait for those payments to settle, then send the customer one summary with the orders and their payment statuses.&lt;/p&gt;

&lt;p&gt;With a communications domain, the intent can outlive the operation that created it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderCreated #1 ----\
OrderCreated #2 -----\
OrderCreated #3 ------\
OrderCreated #4 -------&amp;gt; Communications
OrderCreated #5 ------/       |
                              | accumulate
                              | correlate
Payment facts ---------&amp;gt;      | enrich
                              |
                              v
                     Communication ready
                              |
                              v
                       One summary
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ordering doesn't need to know the recipient just placed four other orders, and Payment doesn't need to understand the customer's notification experience. Each domain keeps owning its own business behavior.&lt;/p&gt;

&lt;p&gt;Communications owns the correlation of intents and the policy that decides when a communication is ready to be composed and delivered.&lt;/p&gt;

&lt;p&gt;This is where communication intent differs from a generic event stream. Communications isn't consuming every event in the system and trying to work out which ones deserve a message. The originating services have already stated the intent. Other events and projections may supply information used to fulfill it, but the intent itself is explicit, which lets communication policy operate over time without turning the business domains into communication orchestrators.&lt;/p&gt;

&lt;h2&gt;
  
  
  Some communications are imperative
&lt;/h2&gt;

&lt;p&gt;Not every communication leaves the decisions to Communications.&lt;/p&gt;

&lt;p&gt;With an intent like &lt;code&gt;OrderCreated&lt;/code&gt;, the communications domain decides almost everything: who receives it, which channels to use, whether to send now or wait, even whether to send anything at all. The originating domain only says that something worth communicating happened.&lt;/p&gt;

&lt;p&gt;Imperative communications flip that. The originating domain has already decided what must happen, often down to the recipient, the channel, and sometimes the provider.&lt;/p&gt;

&lt;p&gt;Take an escheatment notice. Before a business turns unclaimed property over to the state, it may be required to notify the owner by certified mail at their last known address. The recipient is fixed. The channel is fixed. Email isn't an acceptable substitute, and neither is the customer's stated preference for SMS. The provider may be fixed too, if only one vendor can produce the proof of certified delivery the business needs to keep.&lt;/p&gt;

&lt;p&gt;A one-time passcode is a smaller example. Identity needs the code delivered by SMS to the phone number registered on the account, even if the customer would rather get email, and not by push either. Letting channel policy choose here would undermine the security the code exists to provide.&lt;/p&gt;

&lt;p&gt;In both cases, decisions that Communications would normally own are made upstream, because they're part of the business rule itself.&lt;/p&gt;

&lt;p&gt;That doesn't move the rest of the work back into the business domain. Identity shouldn't be retrying SMS sends or tracking delivery receipts, and whoever owns escheatment shouldn't be integrating directly with a certified mail vendor. Communications still owns fulfillment: rendering the approved template, attempting delivery, retrying, escalating when delivery fails, and keeping the audit history and evidence of delivery that a regulator or security review may ask for.&lt;/p&gt;

&lt;p&gt;So the boundary moves, but it doesn't go away. An intent hands Communications a purpose and lets it decide how to fulfill it. An imperative hands it a purpose along with constraints it has to honor. Either way, the business domain decides what must happen, and Communications is accountable for making it happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Templates belong with the communication model
&lt;/h2&gt;

&lt;p&gt;With composition living in Communications, templates get easier to reason about too.&lt;/p&gt;

&lt;p&gt;An &lt;code&gt;OrderCreated&lt;/code&gt; communication may eventually have an email subject, an HTML body, a plain-text body, an SMS variation, a push-notification title, and localized versions of each.&lt;/p&gt;

&lt;p&gt;The organization might keep those templates in source control, give Product or Marketing tooling to maintain them, or use a content-management system. That choice can change without touching who owns the underlying order.&lt;/p&gt;

&lt;p&gt;Changing:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Thanks for your order!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Thanks, Alice! We've received order #123.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;is a communication change. So is adding an SMS version, and so is localizing the message. Moving from SMTP to SendGrid is a delivery change. None of them touch the Ordering model.&lt;/p&gt;

&lt;p&gt;Keeping composition inside Communications means content can change as customer-experience requirements change, while the business domains that started the communication stay focused on their own models.&lt;/p&gt;

&lt;h2&gt;
  
  
  The transport is only one part of the problem
&lt;/h2&gt;

&lt;p&gt;When communications first get complicated, provider abstraction tends to get most of the attention. Swapping SMTP for SendGrid, or one SMS provider for another, is an obvious dependency to isolate.&lt;/p&gt;

&lt;p&gt;Provider independence is useful, but it sits near the bottom of the communications problem.&lt;/p&gt;

&lt;p&gt;As the system matures, the harder questions are about policy and composition. Why does this communication exist? Who should receive it, and what information does composing it require? Should it go out now, or wait and be combined with other intents? Which channels and preferences apply? What happens when delivery fails?&lt;/p&gt;

&lt;p&gt;Those questions belong together because they all describe the lifecycle of communicating with a recipient.&lt;/p&gt;

&lt;p&gt;The resulting split looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Business Domains
      |
      | communication intents
      | communication imperatives
      v
Communications
      |
      | resolve
      | enrich
      | accumulate
      | compose
      | apply policy
      | deliver
      v
Email / SMS / Push / ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The business domains stay responsible for the facts and rules they own. Communications is responsible for turning explicit communication requirements into messages that are composed and delivered correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Applying the model to eShop
&lt;/h2&gt;

&lt;p&gt;When Ordering completes an operation like creating an order, it stays the authority on that business action. As part of completing it, Ordering can express an &lt;code&gt;OrderCreated&lt;/code&gt; communication intent using the identifiers and facts it owns.&lt;/p&gt;

&lt;p&gt;The rest of the lifecycle moves to Communications:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 Ordering
                    |
              Order created
                    |
                    v
             OrderCreated
                 intent
                    |
                    v
              Communications
                    |
             resolve recipient
                    |
             gather/enrich data
                    |
              compose content
                    |
              apply policy
                    |
             +------+------+
             |      |      |
           Email   SMS    Push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Integration events still matter here. They carry facts between bounded contexts and can give Communications what it needs to enrich or complete an existing intent. They just don't need to be the way Communications discovers that a communication should exist, because the business service can state that decision directly.&lt;/p&gt;

&lt;p&gt;Business intent stays explicit where the business operation happens, and communication policy stays in one place, the domain responsible for communication.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Transmitly enters the picture
&lt;/h2&gt;

&lt;p&gt;Nothing so far depends on Transmitly. It's an architecture problem.&lt;/p&gt;

&lt;p&gt;The goal is a boundary that pays off once communications start spanning domains, channels, providers, and time. Choosing a library comes after that.&lt;/p&gt;

&lt;p&gt;Ordering expresses something like &lt;code&gt;OrderCreated&lt;/code&gt; using information it legitimately owns. Communications accepts that intent and owns its lifecycle from there. Recipient resolution, composition, templates, channel selection, providers, and delivery all stay on the communications side of the boundary.&lt;/p&gt;

&lt;p&gt;Transmitly is one way to implement that model. In the next article we'll add it to the .NET eShop codebase and put that boundary around an order communication. We'll start with simulated delivery so SMTP credentials and provider configuration don't distract from the architecture.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ordering
    |
    | OrderCreated
    v
Communications
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ordering stays responsible for knowing that an order was created and that the operation gives it a reason to communicate. After that, the communication belongs to a domain built to compose and deliver it.&lt;/p&gt;




&lt;p&gt;Next, we'll start writing code. Ordering will dispatch its &lt;code&gt;OrderCreated&lt;/code&gt; intent, and a new Communications service will turn it into an email.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>opensource</category>
      <category>architecture</category>
      <category>programming</category>
    </item>
    <item>
      <title>Sending Transactional Emails with Transmitly (C#)</title>
      <dc:creator>Jeremy</dc:creator>
      <pubDate>Tue, 29 Oct 2024 18:00:00 +0000</pubDate>
      <link>https://dev.to/jeremy_tly/sending-transactional-emails-with-transmitly-c-ede</link>
      <guid>https://dev.to/jeremy_tly/sending-transactional-emails-with-transmitly-c-ede</guid>
      <description>&lt;p&gt;Whether you’re confirming an order, resetting a password, or sending an account notification, transactional emails are key to ensuring a smooth user experience. However, managing these emails effectively can be complex. That’s where Transmitly comes in — a solution designed to streamline and enhance your transactional email process. In this article, you’ll explore how Transmitly, with its powerful features, can revolutionize the way you handle email communication.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are Transactional Emails?
&lt;/h2&gt;

&lt;p&gt;Transactional emails are messages sent in response to specific actions or events within your application or service. Unlike marketing emails, which are sent in bulk to promote products or services, transactional emails are integral to your business operations. Common examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Order confirmations&lt;/li&gt;
&lt;li&gt;Password reset requests&lt;/li&gt;
&lt;li&gt;Account notifications&lt;/li&gt;
&lt;li&gt;Shipping updates&lt;/li&gt;
&lt;li&gt;Appointment reminders&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is Transmitly?
&lt;/h2&gt;

&lt;p&gt;Transmitly is a robust platform for managing and controlling outgoing transactional communications, including emails, SMS, and push notifications. It centralizes your communication management, offers flexible configuration, and simplifies message composition. Whether you’re a developer or running a business, Transmitly helps you streamline your communication processes. It supports multiple service providers and integrates easily with a variety of tools, making your communications more efficient and reliable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Methods of Sending Email
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;SMTP: Gmail, Zoho, Custom Mail Servers, SendGrid&lt;/li&gt;
&lt;li&gt;3rd-Party Service Providers: SendGrid, Infobip, Mailgun, and more&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Without Transmitly
&lt;/h2&gt;

&lt;p&gt;Typically, your first few email communications might involve using Simple Mail Transfer Protocol (SMTP) servers because it’s the simplest and fastest method to implement.&lt;/p&gt;

&lt;p&gt;A common approach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Net&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Net.Mail&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EmailService&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IEmailService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SendPasswordReminder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;toEmail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;fromAddress&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;MailAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"your-from-email@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Your Name"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;toAddress&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;MailAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toEmail&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;fromPassword&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"your-email-password"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;smtp&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;SmtpClient&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;Host&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"smtp.example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="n"&gt;Port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;587&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="n"&gt;EnableSsl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="n"&gt;DeliveryMethod&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SmtpDeliveryMethod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Network&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="n"&gt;UseDefaultCredentials&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="n"&gt;Credentials&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;NetworkCredential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fromAddress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fromPassword&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;

      &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;MailMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fromAddress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;toAddress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;Subject&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="n"&gt;Body&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;smtp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What’s Wrong with This Approach?
&lt;/h2&gt;

&lt;p&gt;While this method works for low-volume emails, it has several limitations if you need to scale:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scalability Issues: Handling a high volume of emails can strain your SMTP server, especially if it’s not optimized for large-scale sending.&lt;/li&gt;
&lt;li&gt;Deliverability Concerns: Ensuring your emails land in the recipient’s inbox (and not their spam folder) requires constant monitoring and fine-tuning, often without much insight or tooling.&lt;/li&gt;
&lt;li&gt; Limited Features: Basic SMTP solutions lack advanced features like email templates, scheduling, and automated workflows.&lt;/li&gt;
&lt;li&gt;Security Risks: Directly managing email sending involves handling sensitive credentials and configurations, which increases the risk of security breaches.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Transmitly Way
&lt;/h2&gt;

&lt;p&gt;First, you need to install the nuget package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install the base Transmitly package&lt;/span&gt;
dotnet add package Transmitly

&lt;span class="c"&gt;# We'll need the SMTP Transmitly package&lt;/span&gt;
dotnet add package Transmitly.ChannelProvider.Smtp

&lt;span class="c"&gt;# To make our lives easier, we'll use the optional MS DI extensions &lt;/span&gt;
dotnet add package Transmitly.Microsoft.Extensions.DependencyInjection
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We need to do a little configuration first. In your Startup.cs&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;//All usual extensions and properties are located in this root namespace&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Transmitly&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;//SecureSocketOptions comes from the Smtp configuration namespace&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Transmitly.ChannelProvider.Smtp.Configuration&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="c1"&gt;//AddTransmitly comes from MS DI extensions library.&lt;/span&gt;
&lt;span class="c1"&gt;//Without the MS DI library, you can still wire up Transmitly using its&lt;/span&gt;
&lt;span class="c1"&gt;//fluent builder.&lt;/span&gt;
&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTransmitly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tly&lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;{&lt;/span&gt; 
  &lt;span class="n"&gt;tly&lt;/span&gt;
  &lt;span class="c1"&gt;//AddSmtpSupport comes from the Smtp library&lt;/span&gt;
  &lt;span class="c1"&gt;//This allows us to setup what's needed in a strongly typed and central way&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddSmtpSupport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Host&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"smtp.example.com"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;587&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SocketOptions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SecureSocketOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StartTls&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"MySMTPUsername"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Password&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"MyPassword"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="c1"&gt;//A Pipeline defines a communication, in this case a single email.&lt;/span&gt;
  &lt;span class="c1"&gt;//Something you can learn more about in the GitHub docs&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddPipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SendPasswordReminder"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"your-from-email@example.com"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsIdentityAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Your Name"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Subject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddStringTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Reset your password"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HtmlBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddStringTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;a href=\"https://url.com/reset-password\"&amp;gt;Click here to reset your password&amp;lt;/a&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first glance, it looks similar to the original SMTP code. You still have credentials and email content, but now everything is centralized. Configuration is placed in your Startup.cs, but as your system grows, you can easily move this into its own class library.&lt;/p&gt;

&lt;p&gt;With Transmitly, you’re also creating a communications pipeline, which gives you centralized control over your communications. You can manage what messages are available and which channel providers are used to send them. In short, you now have a loosely coupled, centralized way of managing your communications.&lt;/p&gt;

&lt;p&gt;Next, let’s modify our original sending code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Transmitly&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EmailService&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IEmailService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ICommunicationsClient&lt;/span&gt; &lt;span class="n"&gt;_commsClient&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;EmailService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ICommunicationsClient&lt;/span&gt; &lt;span class="n"&gt;commsClient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_commsClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;commsClient&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;SendPasswordReminder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;toEmail&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_commsClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DispatchAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SendPasswordReminder"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;toEmail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsSuccessful&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Email sent successfully!"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Failed to send email."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s all there is to it! The code may look similar, but these changes open up several new possibilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Change Channels: While today you might only need email, in the future, you might want to add push notifications or give users communication preferences. Transmitly lets you manage that easily with Pipelines, Channels, and Channel Providers.&lt;/li&gt;
&lt;li&gt;Change Providers: If SMTP gives you issues, you can quickly switch to another provider, like SendGrid, Mailgun, or Infobip, by adding a new Transmitly library and updating your configuration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You now have the power and flexibility to grow your communication strategy in a safe, controlled, and testable manner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Benefits of Using Transmitly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Centralized Communication Management: Manage emails, SMS, and push notifications from one platform, reducing complexity and boosting efficiency.&lt;/li&gt;
&lt;li&gt;Flexible Configuration: Switch between service providers like SendGrid, Twilio, or SMTP without altering your code.&lt;/li&gt;
&lt;li&gt;Simplified Message Composition: Externalize message composition, making it easier to modify and manage your communication strategy.&lt;/li&gt;
&lt;li&gt;Scalability and Reliability: Designed for high-volume communications with reliable performance.&lt;/li&gt;
&lt;li&gt;Provider Agnostic: Integrates seamlessly with almost any communication service (e.g., Twilio, Infobip, SMTP).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Transactional emails are vital to modern applications, ensuring timely and effective communication with users. While traditional email-sending methods can be limiting, Transmitly offers a powerful alternative that simplifies the process, improves deliverability, and provides valuable insights. By integrating Transmitly into your app, you can ensure your transactional emails are sent efficiently and reliably, enhancing user experience and engagement.&lt;/p&gt;

&lt;p&gt;For more information check out the &lt;a href="https://github.com/transmitly/transmitly" rel="noopener noreferrer"&gt;Transmitly GitHub Repo&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>csharp</category>
    </item>
    <item>
      <title>Sending SMS with Transmitly (C#)</title>
      <dc:creator>Jeremy</dc:creator>
      <pubDate>Wed, 23 Oct 2024 06:00:00 +0000</pubDate>
      <link>https://dev.to/jeremy_tly/sending-sms-with-transmitly-c-13m</link>
      <guid>https://dev.to/jeremy_tly/sending-sms-with-transmitly-c-13m</guid>
      <description>&lt;p&gt;Whether you’re confirming an order, resetting a password, or sending an account notification, SMS messages are key to ensuring a great user experience. However, managing these SMS messages effectively can get complex, fast. That’s where Transmitly comes in — a solution designed to streamline and enhance your SMS message process. In this article, you’ll explore how Transmitly, with its powerful features, can revolutionize the way you handle SMS communications.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a transactional communication?
&lt;/h2&gt;

&lt;p&gt;Transactional communications are sent in response to specific actions or events within your application or service. Unlike marketing communications, which are typically sent in bulk to promote products or services, transactional communications are integral parts of your business operations. Common examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Order confirmations&lt;/li&gt;
&lt;li&gt;Password reset requests&lt;/li&gt;
&lt;li&gt;Account notifications&lt;/li&gt;
&lt;li&gt;One time password (OTP)&lt;/li&gt;
&lt;li&gt;Appointment reminders&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is Transmitly?
&lt;/h2&gt;

&lt;p&gt;Transmitly is a robust platform for managing and controlling outgoing transactional communications, including emails, SMS, and push notifications. It centralizes your communication management, offers flexible configuration, and simplifies message composition. Whether you’re a developer or running a business, Transmitly helps you streamline your communication processes. It supports multiple service providers and integrates easily with a variety of tools, making your communications more efficient and reliable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Methods of Sending SMS
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://en.wikipedia.org/wiki/SMS_gateway" rel="noopener noreferrer"&gt;SMS Gateway&lt;/a&gt;: Strangely enough, some carriers support sending SMS messages via special email addresses. They are generally not very reliable and you must know which provider your recipient is using.&lt;/li&gt;
&lt;li&gt;3rd-Party Service Providers: Twilio, InfoBip, Plivo, and more&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Without Transmitly
&lt;/h2&gt;

&lt;p&gt;There’s quite a few options out there to choose from for SMS communications. There’s also a lot more restrictions to be allowed to send SMS with any service. We’ll assume you’ve made it through the hoops of getting setup and you’ve chosen Twilio.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Add the Twilio Api:&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Twilio
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Install the C# / .NET helper library from twilio.com/docs/csharp/install&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Twilio&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Twilio.Rest.Api.V2010.Account&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Threading.Tasks&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SmsService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;OrderProcessed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;recipient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

        &lt;span class="c1"&gt;// Find your Account SID and Auth Token at twilio.com/console&lt;/span&gt;
        &lt;span class="c1"&gt;// and set the environment variables. See http://twil.io/secure&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;accountSid&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetEnvironmentVariable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"TWILIO_ACCOUNT_SID"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;authToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetEnvironmentVariable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"TWILIO_AUTH_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;TwilioClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accountSid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;authToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;MessageResource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Your order is being processed, thanks!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Twilio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PhoneNumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"+15017122661"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Twilio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PhoneNumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recipient&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://www.twilio.com/docs/messaging/tutorials/how-to-send-sms-messages/how-to-send-sms-messages-csharp" rel="noopener noreferrer"&gt;(Example taken from the Twilio quick start)&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s Wrong with This Approach?
&lt;/h2&gt;

&lt;p&gt;On the surface, not a lot. Twilio and their SDK will work great for a long time.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;More work to be done: Not shown is just how to deserialize the API response and make sense of it in your application.&lt;/li&gt;
&lt;li&gt;Vendor Lock: The more communications, the more you’re going to use their API. That doesn’t pose a problem until something changes with their service. Price increases, API changes, etc are all going to cost you and your application.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Transmitly Way
&lt;/h2&gt;

&lt;p&gt;First, you need to install the nuget package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install the base Transmitly package&lt;/span&gt;
dotnet add package Transmitly
&lt;span class="c"&gt;# We'll need the Twilio Transmitly package to send SMS&lt;/span&gt;
dotnet add package Transmitly.ChannelProvider.Twilio
&lt;span class="c"&gt;# To make our lives easier, we'll use the optional MS DI extensions &lt;/span&gt;
dotnet add package Transmitly.Microsoft.Extensions.DependencyInjection
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We need to do a little configuration first. In your Startup.cs&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;//All usual extensions and properties are located in this root namespace&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Transmitly&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="c1"&gt;//AddTransmitly comes from MS DI extensions library.&lt;/span&gt;
&lt;span class="c1"&gt;//Without the MS DI library, you can still wire up Transmitly using its&lt;/span&gt;
&lt;span class="c1"&gt;//fluent builder.&lt;/span&gt;
&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTransmitly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tly&lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;{&lt;/span&gt; 
  &lt;span class="n"&gt;tly&lt;/span&gt;
  &lt;span class="c1"&gt;//AddTwilioSupport comes from the Twilio library&lt;/span&gt;
  &lt;span class="c1"&gt;//This allows us to setup what's needed in a strongly typed and central way&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTwilioSupport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;twilio&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;twilio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AuthToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetEnvironmentVariable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"TWILIO_AUTH_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="n"&gt;twilio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AccountSid&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetEnvironmentVariable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"TWILIO_ACCOUNT_SID"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddPipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"OrderProcessing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;//AddSms is a core feature, a channel, of Transmitly&lt;/span&gt;
    &lt;span class="c1"&gt;//Out of the box there are Email, SMS, Voice and Push&lt;/span&gt;
    &lt;span class="c1"&gt;//The first argument is the number we're sending from&lt;/span&gt;
    &lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddSms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"+15017122661"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsIdentityAddress&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;sms&lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;//Transmitly is a bit different. All of our communication content is configured by using templates.&lt;/span&gt;
      &lt;span class="c1"&gt;//Out of the box, we have string templates, embedded resource templates and custom template resolvers.&lt;/span&gt;
      &lt;span class="c1"&gt;//There are multiple types of templates to get you started. You can even create templates &lt;/span&gt;
      &lt;span class="c1"&gt;//specific to certain cultures!&lt;/span&gt;
      &lt;span class="n"&gt;sms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddStringTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Your order is being processed, thanks!"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="c1"&gt;//We've kept it simple for this example, but Transmitly also supports various template engines.&lt;/span&gt;
      &lt;span class="c1"&gt;//Adding a template engine can make your messages a bit more useful by showing the order# or user's name.&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first glance, it looks similar to the original code. You still have credentials and SMS content, but now everything is centralized. Configuration is placed in your &lt;code&gt;Startup.cs&lt;/code&gt;, but as your system grows, you can easily move this into its own class library.&lt;/p&gt;

&lt;p&gt;With Transmitly, you’re also creating a communications pipeline, which gives you centralized control over your communications. You can manage what messages are available and which channel providers are used to send them. In short, you now have a loosely coupled, centralized way of managing your communications.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Next, let’s modify our original sending code:&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Transmitly&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SmsService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ICommunicationsClient&lt;/span&gt; &lt;span class="n"&gt;_commsClient&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;SmsService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ICommunicationsClient&lt;/span&gt; &lt;span class="n"&gt;commsClient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_commsClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;commsClient&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;OrderProcessed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;recipient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;//Dispatch (send) the transactional sms to our recipient using our configured Twilio account and our "OrderProcessing" pipeline.&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_commsClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DispatchAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"OrderProcessing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;recipient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsSuccessful&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Sms sent successfully!"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Failed to send SMS"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s all there is to it! The code may look similar, but these changes open up several new possibilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Change Channels: While today you might only need SMS, in the future, you might want to add email, push notifications or better yet, let users set their communication preferences. Transmitly lets you manage that easily with Pipelines, Channels, and Channel Providers.&lt;/li&gt;
&lt;li&gt;Change Providers: If Twilio gives you issues, you can quickly switch to another provider, like Infobip by adding a new Transmitly library and updating your configuration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You now have the power and flexibility to grow your communication strategy in a safe, controlled, and testable manner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Benefits of Using Transmitly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;   Centralized Communication Management: Manage emails, SMS, and push notifications from one platform, reducing complexity and boosting efficiency.&lt;/li&gt;
&lt;li&gt;   Flexible Configuration: Switch between service providers like SendGrid or Infobip without altering your business logic.&lt;/li&gt;
&lt;li&gt;   Simplified Message Composition: Externalize message composition, making it easier to modify and manage your communication strategy.&lt;/li&gt;
&lt;li&gt;   Scalability and Reliability: Designed for high-volume communications with reliable performance.&lt;/li&gt;
&lt;li&gt;   Provider Agnostic: Integrates seamlessly with almost any communication service (e.g., Twilio, Infobip, SMTP). Easily extensible for adding any missing features you might need.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Transactional communications are vital to modern applications, ensuring timely and effective communication with users. Transmitly offers a powerful alternative that simplifies the process, improves deliverability, and provides valuable insights. By integrating Transmitly into your app, you can ensure your transactional communications are sent efficiently and reliably, enhancing user experience and engagement.&lt;/p&gt;

&lt;p&gt;Piqued your interest? Check out the &lt;a href="https://github.com/transmitly/transmitly" rel="noopener noreferrer"&gt;Transmitly GitHub Repo&lt;/a&gt; for more exciting ways to manage your business' communications strategies.&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>programming</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Transactional Emails: A long term view</title>
      <dc:creator>Jeremy</dc:creator>
      <pubDate>Thu, 17 Oct 2024 01:59:40 +0000</pubDate>
      <link>https://dev.to/jeremy_tly/transactional-emails-a-long-term-view-13l6</link>
      <guid>https://dev.to/jeremy_tly/transactional-emails-a-long-term-view-13l6</guid>
      <description>&lt;p&gt;Sending emails has become essential for almost all modern applications and games. The most common type of emails in your first app are transactional. Examples of transactional communications include order confirmations, password reset requests, account notifications, and appointment reminders. Luckily, Microsoft has made this task pretty simple out of the box. So much so that you may not put much thought into how you’re sending out your app’s communications. And that’s okay — we all have deadlines to meet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;Once you’ve identified the required transactional communications for your app, you need to decide how to send them. Again, you’re in luck here, too. You have a few different options when deciding how to send your emails. To keep things simple, you can start by using an established SMTP server. There are countless examples of how to use Gmail, Zoho, and other third-party SMTP services. It’s simple enough and will get you through development and even to the MVP phase.&lt;/p&gt;

&lt;p&gt;As your app grows, you might notice you’re being rate-limited by the SMTP service, transient errors are creeping in, and your simple email solution is no longer holding up to the stress of your growing app. First of all, congrats! Your app is growing! Second, it’s time to revisit our communications strategy. You’re in luck here as well. Numerous third-party APIs make it even easier to send emails. Services such as SendGrid, Mailgun, and ZeptoMail all come with their own pros and cons and a wide range of features. Now, you can do some really neat things with your emails. Again, your communications strategy has evolved, and so has the complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Smooth Sailing…
&lt;/h2&gt;

&lt;p&gt;You’ve chosen a third-party service you love, and it’s working great. You’ve created an email service using their SDK. Now you can get back to growing your app, team, and business. Unfortunately, you receive an email from your third-party service. They are increasing their prices… again. Or worse, they’ve moved a feature you’ve been using heavily behind an expensive paywall.&lt;/p&gt;

&lt;p&gt;It’s time to change your service… again. It’s generally not too big of a deal, right? …Right?! Integrating a new email service only took a day. But the app is growing quickly, we have tech debt throughout, and not all of our emails are consistent. It wasn’t a big deal, but the new service doesn’t handle our older, legacy formats.&lt;/p&gt;

&lt;p&gt;While it may only be a mild annoyance that only rears its ugly head every few years, you’ve got a successful app. You don’t have time to learn a new service, a new API, all the quirks of a new service, and regression test all of your communications… again. This is why the open-source Transmitly communications library was created.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transmit-Who?
&lt;/h2&gt;

&lt;p&gt;The Transmitly library is an open-source library designed to manage and control outgoing transactional communications, including emails, SMS, and push notifications. It offers centralized communication management, allowing developers to handle multiple communication channels from a single platform.&lt;/p&gt;

&lt;p&gt;Transmitly provides flexible configuration, enabling easy switching between SMTP and service providers like SendGrid, Twilio, and Infobip without altering domain code. It simplifies message composition and delivery and integrates seamlessly with various dependency injection containers and template engines.&lt;/p&gt;

&lt;h2&gt;
  
  
  And Now for Something Completely Different
&lt;/h2&gt;

&lt;p&gt;Transmitly is built on concepts pioneered by Serilog, Microsoft, and many other extensibility-first libraries. At its core, Transmitly enables you to choose as much or as little integration as you have the time or effort to use.&lt;/p&gt;

&lt;p&gt;Love the idea? Convert your whole codebase in a day or two. Not sure? Try it on only new transactional communications to start. It’s up to you.&lt;/p&gt;

&lt;p&gt;All this extensibility doesn’t come without some downsides. There’s going to be a ‘different’ way of doing things. And with different, there come some new terms we need to understand before we can really grasp the full scope of what’s possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Channels
&lt;/h2&gt;

&lt;p&gt;A channel is the base of all communications. Out of the box, Transmitly supports email, SMS, voice and push notifications. But you’re not limited to these channels. Transmitly is built with extensibility in mind at all times. This means that if you have an idea for a channel; such as physical mail, fax, or Slack, practically any kind of channel can be built in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Channel Providers
&lt;/h2&gt;

&lt;p&gt;Channel Providers do the heavy lifting for your Channels. A Channel Provider can support one to many channels. Examples include SMTP (Email), SendGrid (Email), Infobip (Email &amp;amp; SMS), or Firebase (iOS/Android/App push notifications).&lt;/p&gt;

&lt;h2&gt;
  
  
  Pipelines
&lt;/h2&gt;

&lt;p&gt;Pipelines are where the real power of Transmitly starts to materialize. With a pipeline, you gain control of managing communications in a single location. You can also manage which communications are available and which channel providers are allowed to send them.&lt;/p&gt;

&lt;p&gt;That’s it! For now, at least. There are a few more advanced concepts that make the Transmitly transition even sweeter. For now, we’re sticking to the basics to get you up to speed ASAP. After all, you have a fast-growing application to maintain!&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing Code
&lt;/h2&gt;

&lt;p&gt;First, you’ll need to install the Transmitly NuGet library in your app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install the base Transmitly package&lt;/span&gt;
dotnet add package Transmitly

&lt;span class="c"&gt;# To make our lives easier, we'll use the optional MS DI extensions &lt;/span&gt;
dotnet add package Transmitly.Microsoft.Extensions.DependencyInjection
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we’ll continue with our email theme by recreating a welcome email that is sent to a newly signed-up user of your app.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Program.cs&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Transmitly&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WebApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTransmitly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tly&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Pipelines allow you to define your communications&lt;/span&gt;
    &lt;span class="c1"&gt;// as a domain action. This allows your domain code to &lt;/span&gt;
    &lt;span class="c1"&gt;// stay agnostic to the details of how you&lt;/span&gt;
    &lt;span class="c1"&gt;// may send out a transactional communication.&lt;/span&gt;

    &lt;span class="c1"&gt;// Notice "WelcomeKit" doesn't specify the how or what&lt;/span&gt;
    &lt;span class="c1"&gt;// might be sent. This is our 'intent'. It's up to our configuration&lt;/span&gt;
    &lt;span class="c1"&gt;// or domain if any communications will spawn and to whom.&lt;/span&gt;
    &lt;span class="n"&gt;tly&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddPipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WelcomeKit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// AsIdentityAddress() is also a convenience method that helps us create an audience identity&lt;/span&gt;
        &lt;span class="c1"&gt;// Identity addresses can be anything: email, phone, or even a device/app ID for push notifications!&lt;/span&gt;
        &lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"welcome@my.app"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsIdentityAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Welcome Committee"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Transmitly is a bit different. Most channel content is configured by using &lt;/span&gt;
            &lt;span class="c1"&gt;// templates. The immediate benefit out of the box is being able to configure&lt;/span&gt;
            &lt;span class="c1"&gt;// culture-based templates. &lt;/span&gt;
            &lt;span class="c1"&gt;// For this example, we'll keep things simple and send a static message.&lt;/span&gt;
            &lt;span class="c1"&gt;// But as you may have guessed, Transmitly also has Template Engine plugins as well.&lt;/span&gt;
            &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Subject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddStringTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Thanks for creating an account!"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HtmlBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddStringTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Check out the &amp;lt;a href=\"https://my.app/getting-started\"&amp;gt;Getting Started&amp;lt;/a&amp;gt; section to see all the cool things you can do!"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TextBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddStringTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Check out the Getting Started (https://my.app/getting-started) section to see all the cool things you can do!"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we can trigger this welcome kit to happen in the same place we used to send our email. Note how simple your domain logic becomes when we remove communications details.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Using Transmitly:&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RegistrationService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ICommunicationsClient&lt;/span&gt; &lt;span class="n"&gt;_commsClient&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;RegistrationService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ICommunicationsClient&lt;/span&gt; &lt;span class="n"&gt;commsClient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_commsClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;commsClient&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentNullException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;commsClient&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;RegisterUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AccountDetail&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Create user&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;newUser&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;User&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// Trigger the welcome kit email&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_commsClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DispatchAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WelcomeKit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsSuccessful&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;newUser&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;RegistrationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Registration failed. Rollback!"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you run this new code… technically, it will work. However, you’ll be disappointed that you’re no longer sending emails. This is because we have not yet defined the 'how' of sending the emails out. With Transmitly, the 'how' or services are referred to as Channel Providers. Channel Providers are what the name implies: they are providers for any given channel. Examples of channel providers are SMTP, Twilio, SendGrid, Infobip etc.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# We want to continue using SMTP for now, so we'll add the SMTP extension&lt;/span&gt;
dotnet add package Transmitly.ChannelProvider.Smtp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we can configure the SMTP server settings Transmitly will use for our pipeline intent, 'WelcomeKit'.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Transmitly&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WebApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// EmailSettingsConfiguration is your own settings class, bound from the "EmailSettings" section of appsettings.json&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;emailConfig&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetRequiredSection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"EmailSettings"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;EmailSettingsConfiguration&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTransmitly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tly&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Configure the SMTP Channel Provider Settings&lt;/span&gt;
    &lt;span class="n"&gt;tly&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddSmtpSupport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;smtp&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;smtp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Host&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;emailConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;smtp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;emailConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Port&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;smtp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;emailConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Username&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;smtp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Password&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;emailConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Password&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="c1"&gt;// Pipelines allow you to define your communications&lt;/span&gt;
    &lt;span class="c1"&gt;// as a domain action. This allows your domain code to &lt;/span&gt;
    &lt;span class="c1"&gt;// stay agnostic to the details of how you&lt;/span&gt;
    &lt;span class="c1"&gt;// may send out a transactional communication.&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddPipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WelcomeKit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// AsIdentityAddress() is also a convenience method that helps us create an audience identity&lt;/span&gt;
        &lt;span class="c1"&gt;// Identity addresses can be anything: email, phone, or even a device/app ID for push notifications!&lt;/span&gt;
        &lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"welcome@my.app"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsIdentityAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Welcome Committee"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Transmitly is a bit different. Most channel content is configured by using &lt;/span&gt;
            &lt;span class="c1"&gt;// templates. The immediate benefit out of the box is being able to configure&lt;/span&gt;
            &lt;span class="c1"&gt;// culture-based templates. &lt;/span&gt;
            &lt;span class="c1"&gt;// For this example, we'll keep things simple and send a static message.&lt;/span&gt;
            &lt;span class="c1"&gt;// But as you may have guessed, Transmitly also has Template Engine plugins as well.&lt;/span&gt;
            &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Subject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddStringTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Thanks for creating an account!"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HtmlBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddStringTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Check out the &amp;lt;a href=\"https://my.app/getting-started\"&amp;gt;Getting Started&amp;lt;/a&amp;gt; section to see all the cool things you can do!"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TextBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddStringTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Check out the Getting Started (https://my.app/getting-started) section to see all the cool things you can do!"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After deploying your code, sending a simple email can remain just as easy as it was before.&lt;/p&gt;

&lt;p&gt;If SMTP doesn’t quite fit the bill for your app anymore, try a different channel provider.&lt;/p&gt;

&lt;p&gt;Adding SMS or push notifications? Communication preferences? Modify your pipelines to effortlessly add new channels. Handling your user communication preferences are just as easy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;As your app grows and evolves, having a flexible and scalable communication strategy is crucial. Transmitly not only simplifies the process of sending transactional communications but also provides a robust framework for handling various channels and channel providers. This reduces the time spent managing communication logic and enables you to focus on building and improving your core application.&lt;/p&gt;

&lt;p&gt;Check out the &lt;a href="https://github.com/transmitly/transmitly" rel="noopener noreferrer"&gt;Transmitly GitHub repo&lt;/a&gt; if you're interested in learning more!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.csadvent.christmas/" rel="noopener noreferrer"&gt;Happy holidays from the C# Advent Calendar 2024!&lt;/a&gt;&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>opensource</category>
      <category>programming</category>
    </item>
    <item>
      <title>Transactional Emails: From SMTP to Transmitly</title>
      <dc:creator>Jeremy</dc:creator>
      <pubDate>Mon, 02 Sep 2024 21:21:06 +0000</pubDate>
      <link>https://dev.to/jeremy_tly/transactional-emails-from-smtp-to-transmitly-3hng</link>
      <guid>https://dev.to/jeremy_tly/transactional-emails-from-smtp-to-transmitly-3hng</guid>
      <description>&lt;p&gt;Sending emails has become essential for almost all modern applications and games. The most common type of emails in your first app are transactional. Examples of transactional communications include order confirmations, password reset requests, account notifications, and appointment reminders. Luckily, Microsoft has made this task pretty simple out of the box. So much so that you may not put much thought into how you’re sending out your app’s communications. And that’s okay — we all have deadlines to meet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;Once you’ve identified the required transactional communications for your app, you need to decide how to send them. Again, you’re in luck here, too. You have a few different options when deciding how to send your emails. To keep things simple, you can start by using an established SMTP server. There are countless examples of how to use Gmail, Zoho, and other third-party SMTP services. It’s simple enough and will get you through development and even to the MVP phase.&lt;/p&gt;

&lt;p&gt;As your app grows, you might notice you’re being rate-limited by the SMTP service, transient errors are creeping in, and your simple email solution is no longer holding up to the stress of your growing app. First of all, congrats! Your app is growing! Second, it’s time to revisit our communications strategy. You’re in luck here as well. Numerous third-party APIs make it even easier to send emails. Services such as SendGrid, Mailgun, and ZeptoMail all come with their own pros and cons and a wide range of features. Now, you can do some really neat things with your emails. Again, your communications strategy has evolved, and so has the complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Smooth Sailing…
&lt;/h2&gt;

&lt;p&gt;You’ve chosen a third-party service you love, and it’s working great. You’ve created an email service using their SDK. Now you can get back to growing your app, team, and business. Unfortunately, you receive an email from your third-party service. They are increasing their prices… again. Or worse, they’ve moved a feature you’ve been using heavily behind an expensive paywall.&lt;/p&gt;

&lt;p&gt;It’s time to change your service… again. It’s generally not too big of a deal, right? …Right?! Integrating a new email service only took a day. But the app is growing quickly, we have tech debt throughout, and not all of our emails are consistent. It wasn’t a big deal, but the new service doesn’t handle our older, legacy formats.&lt;/p&gt;

&lt;p&gt;While it may only be a mild annoyance that only rears its ugly head every few years, you’ve got a successful app. You don’t have time to learn a new service, a new API, all the quirks of a new service, and regression test all of your communications… again. This is why the open-source Transmitly communications library was created.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transmit-Who?
&lt;/h2&gt;

&lt;p&gt;The Transmitly library is an open-source library designed to manage and control outgoing transactional communications, including emails, SMS, and push notifications. It offers centralized communication management, allowing developers to handle multiple communication channels from a single platform.&lt;/p&gt;

&lt;p&gt;Transmitly provides flexible configuration, enabling easy switching between SMTP (MailKit) and service providers like SendGrid, Twilio, and Infobip without altering domain code. It simplifies message composition and delivery and integrates seamlessly with various dependency injection containers and template engines.&lt;/p&gt;

&lt;h2&gt;
  
  
  And Now for Something Completely Different
&lt;/h2&gt;

&lt;p&gt;Transmitly is built on concepts pioneered by Serilog, Microsoft, and many other extensibility-first libraries. At its core, Transmitly enables you to choose as much or as little integration as you have the time or effort to use.&lt;/p&gt;

&lt;p&gt;Love the idea? Convert your whole codebase in a day or two. Not sure? Try it on only new transactional communications to start. It’s up to you.&lt;/p&gt;

&lt;p&gt;All this extensibility doesn’t come without some downsides. There’s going to be a ‘different’ way of doing things. And with different, there come some new terms we need to understand before we can really grasp the full scope of what’s possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Channels
&lt;/h2&gt;

&lt;p&gt;A channel is the base of all communications. Out of the box, Transmitly supports email, SMS, and push notifications. But you’re not limited to these channels. Transmitly is built with extensibility in mind at all times. This means that if you have a need for any kind of channel, such as physical mail, fax, or Slack, practically any kind of channel can be built in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Channel Providers
&lt;/h2&gt;

&lt;p&gt;Channel Providers do the heavy lifting for your Channels. A Channel Provider can support one to many channels. Examples include MailKit (SMTP Email), SendGrid (Email), Infobip (Email &amp;amp; SMS), or Firebase (iOS/Android/App push notifications).&lt;/p&gt;

&lt;h2&gt;
  
  
  Pipelines
&lt;/h2&gt;

&lt;p&gt;Pipelines are where the real power of Transmitly starts to materialize. With a pipeline, you gain control of managing communications in a single location. You can also manage which communications are available and which channel providers are allowed to send them.&lt;/p&gt;

&lt;p&gt;That’s it! For now, at least. There are a few more advanced concepts that make the Transmitly transition even sweeter. For now, we’re sticking to the basics to get you up to speed ASAP. After all, you have a fast-growing application to maintain!&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing Code
&lt;/h2&gt;

&lt;p&gt;First, you’ll need to install the Transmitly NuGet library in your app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install the base Transmitly package&lt;/span&gt;
dotnet &lt;span class="nb"&gt;install &lt;/span&gt;Transmitly

&lt;span class="c"&gt;# To make our lives easier, we'll use the optional MS DI extensions &lt;/span&gt;
dotnet &lt;span class="nb"&gt;install &lt;/span&gt;Transmitly.Microsoft.Extensions.DependencyInjectionbash

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we’ll continue with our email theme by recreating a welcome email that is sent to a newly signed-up user of your app.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Startup.cs&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Transmitly&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WebApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;emailConfig&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetRequiredSection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"EmailSettings"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;EmailSettingsConfiguration&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTransmitly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tly&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Pipelines allow you to define your communications&lt;/span&gt;
    &lt;span class="c1"&gt;// as a domain action. This allows your domain code to &lt;/span&gt;
    &lt;span class="c1"&gt;// stay agnostic to the details of how you&lt;/span&gt;
    &lt;span class="c1"&gt;// may send out a transactional communication.&lt;/span&gt;
    &lt;span class="n"&gt;tly&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddPipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WelcomeKit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// AsIdentityAddress() is also a convenience method that helps us create an audience identity&lt;/span&gt;
        &lt;span class="c1"&gt;// Identity addresses can be anything: email, phone, or even a device/app ID for push notifications!&lt;/span&gt;
        &lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"welcome@my.app"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsIdentityAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Welcome Committee"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Transmitly is a bit different. Most channel content is configured by using &lt;/span&gt;
            &lt;span class="c1"&gt;// templates. The immediate benefit out of the box is being able to configure&lt;/span&gt;
            &lt;span class="c1"&gt;// culture-based templates. &lt;/span&gt;
            &lt;span class="c1"&gt;// For this example, we'll keep things simple and send a static message.&lt;/span&gt;
            &lt;span class="c1"&gt;// But as you may have guessed, Transmitly also has Template Engine plugins as well.&lt;/span&gt;
            &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Subject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddStringTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Thanks for creating an account!"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HtmlBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddStringTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Check out the &amp;lt;a href=\"https://my.app/getting-started\"&amp;gt;Getting Started&amp;lt;/a&amp;gt; section to see all the cool things you can do!"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TextBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddStringTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Check out the Getting Started (https://my.app/getting-started) section to see all the cool things you can do!"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we can trigger this welcome kit to happen in the same place we used to send our email. Note how simple your domain logic becomes when we remove communications details.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Using Transmitly:&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RegistrationService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ICommunicationsClient&lt;/span&gt; &lt;span class="n"&gt;_commsClient&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;RegistrationService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ICommunicationsClient&lt;/span&gt; &lt;span class="n"&gt;commsClient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_commsClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;commsClient&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentNullException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;commsClient&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;RegisterUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AccountDetail&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Create user&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;newUser&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;User&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// Trigger the welcome kit email&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_commsClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DispatchAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WelcomeKit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TransactionalModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsSuccessful&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;newUser&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;RegistrationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Registration failed. Rollback!"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you run this new code… technically, it will work. However, you’ll be disappointed that you’re no longer sending emails. This is because we have not yet defined the ‘how’ of sending the emails out. With Transmitly, the “how” or services are referred to as Channel Providers. Channel Providers are what the name implies: they are providers for any given channel (email, SMS, push, etc.).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# We want to continue using SMTP for now, so we'll add the MailKit extension&lt;/span&gt;
dotnet &lt;span class="nb"&gt;install &lt;/span&gt;Transmitly.ChannelProvider.MailKit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we can configure the SMTP server settings Transmitly will use for our WelcomeKit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Transmitly&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WebApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;emailConfig&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetRequiredSection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"EmailSettings"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;EmailSettingsConfiguration&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTransmitly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tly&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Configure the MailKit Channel Provider Settings&lt;/span&gt;
    &lt;span class="n"&gt;tly&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddMailKitSupport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mailkit&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;mailkit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Host&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;emailConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;mailkit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UseSsl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;emailConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UseSsl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;mailkit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;emailConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Port&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;mailkit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;emailConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Username&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;mailkit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Password&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;emailConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Password&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="c1"&gt;// Pipelines allow you to define your communications&lt;/span&gt;
    &lt;span class="c1"&gt;// as a domain action. This allows your domain code to &lt;/span&gt;
    &lt;span class="c1"&gt;// stay agnostic to the details of how you&lt;/span&gt;
    &lt;span class="c1"&gt;// may send out a transactional communication.&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddPipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WelcomeKit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// AsIdentityAddress() is also a convenience method that helps us create an audience identity&lt;/span&gt;
        &lt;span class="c1"&gt;// Identity addresses can be anything: email, phone, or even a device/app ID for push notifications!&lt;/span&gt;
        &lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"welcome@my.app"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsIdentityAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Welcome Committee"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Transmitly is a bit different. Most channel content is configured by using &lt;/span&gt;
            &lt;span class="c1"&gt;// templates. The immediate benefit out of the box is being able to configure&lt;/span&gt;
            &lt;span class="c1"&gt;// culture-based templates. &lt;/span&gt;
            &lt;span class="c1"&gt;// For this example, we'll keep things simple and send a static message.&lt;/span&gt;
            &lt;span class="c1"&gt;// But as you may have guessed, Transmitly also has Template Engine plugins as well.&lt;/span&gt;
            &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Subject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddStringTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Thanks for creating an account!"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HtmlBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddStringTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Check out the &amp;lt;a href=\"https://my.app/getting-started\"&amp;gt;Getting Started&amp;lt;/a&amp;gt; section to see all the cool things you can do!"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TextBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddStringTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Check out the Getting Started (https://my.app/getting-started) section to see all the cool things you can do!"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After deploying your code, sending a simple email can remain just as easy as it was before.&lt;/p&gt;

&lt;p&gt;If SMTP doesn’t quite fit the bill for your app anymore, try a different channel provider.&lt;/p&gt;

&lt;p&gt;Adding SMS or push notifications? Communication preferences? Modify your pipelines to effortlessly add new channels. Handling your user communication preferences are just as easy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;As your app grows and evolves, having a flexible and scalable communication strategy is crucial. Transmitly not only simplifies the process of sending transactional communications but also provides a robust framework for handling various channels and channel providers. This reduces the time spent managing communication logic and enables you to focus on building and improving your core application.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/transmitly/transmitly" rel="noopener noreferrer"&gt;Check out the github repo for more details and examples of all the great things Transmitly can do.&lt;/a&gt;&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>opensource</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
