ContactWise.Messaging.Contracts 0.1.1

dotnet add package ContactWise.Messaging.Contracts --version 0.1.1
                    
NuGet\Install-Package ContactWise.Messaging.Contracts -Version 0.1.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="ContactWise.Messaging.Contracts" Version="0.1.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ContactWise.Messaging.Contracts" Version="0.1.1" />
                    
Directory.Packages.props
<PackageReference Include="ContactWise.Messaging.Contracts" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add ContactWise.Messaging.Contracts --version 0.1.1
                    
#r "nuget: ContactWise.Messaging.Contracts, 0.1.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package ContactWise.Messaging.Contracts@0.1.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=ContactWise.Messaging.Contracts&version=0.1.1
                    
Install as a Cake Addin
#tool nuget:?package=ContactWise.Messaging.Contracts&version=0.1.1
                    
Install as a Cake Tool

ContactWise.Messaging.Contracts

Event contracts shared by the services of the ContactWise messaging platform. The types are plain data classes with no dependencies; their JSON shape is the wire contract between producers and consumers.

Installation

dotnet add package ContactWise.Messaging.Contracts

The package targets .NET 10 only, from version 0.1.0 onwards. Services still on .NET 8 stay on 0.0.5, the last version that targets net8.0, until they move to .NET 10.

What is in the package

Type Namespace Purpose
CloudEvent ...V1.Envelope Envelope that carries every event
CloudEvent<TData> ...V1.Envelope The same envelope with a typed payload, for consumers
CloudEventDefaults ...V1.Envelope Default specversion and content type of the envelope
SmsRequestReceivedEvent ...V1.Events A request to send an SMS that the platform has accepted
DeliveryStatusReceivedEvent ...V1.Events A delivery report received from the current SMS vendor
SmppPduAuditEvent ...V1.Events One SMPP PDU as it crossed the socket of an SMPP service
EventTypes, EventSources, Topics, KafkaHeaders ...V1.Constants The type strings consumers route on, producer sources, topic names and header names
SmppPduFields ...V1.Constants Keys of the decoded PDU body, spelled as in SMPP 3.4
CountryCodes ...V1.Constants The country code values of SmsRequestReceivedEvent
MessageType, ServiceType, SmppPduDirection ...V1.Enums Enumerations used by the events
SmppPduDirectionJsonConverter, SmppPduFieldsJsonConverter ...V1.Converters Property-level converters that pin the wire format; you do not register them

... stands for ContactWise.Messaging.Contracts. Folders in the repository match the namespaces.

Usage

Producing an event:

using System.Text.Json;
using ContactWise.Messaging.Contracts.V1.Constants;
using ContactWise.Messaging.Contracts.V1.Enums;
using ContactWise.Messaging.Contracts.V1.Envelope;
using ContactWise.Messaging.Contracts.V1.Events;

var envelope = CloudEvent.Create(
    EventTypes.SmsRequestReceived,
    new Uri("urn:contactwise:my-service"),
    new SmsRequestReceivedEvent
    {
        Id = "request-id",
        TenantId = "tenant-id",
        From = "SENDER",
        To = "919999999999",
        Body = "Hello",
        MessageType = MessageType.Text
    });

string json = JsonSerializer.Serialize(envelope);

Create generates the envelope Id and sets Timestamp to the current UTC time. If you build the envelope with an object initializer instead, set Id, Source and Timestamp yourself: Id and Source default to empty and consumers rely on the pair to de-duplicate events, and Timestamp defaults to null.

Consuming an event:

// When the payload type is known, read the typed envelope. Its JSON shape is identical to CloudEvent.
var typed = JsonSerializer.Deserialize<CloudEvent<SmsRequestReceivedEvent>>(json)!;
SmsRequestReceivedEvent request = typed.Data!;

// When the payload type depends on Type, read the untyped envelope first. Data is declared as object,
// so System.Text.Json materialises it as a JsonElement.
var envelope = JsonSerializer.Deserialize<CloudEvent>(json)!;
var payload = ((JsonElement)envelope.Data!).Deserialize<SmsRequestReceivedEvent>()!;

Topics and event types

Topics is where a message is produced to and consumed from; EventTypes is the type attribute of the envelope inside the message. They are different strings, and one topic can carry more than one type, so consumers route on the type after reading.

Topics Carries (EventTypes) Key Produced by Consumed by
SmsRequestReceived = sms-request-received SmsRequestReceived = sms.request.new none Submission API RequestProcessor
DeliveryStatusReceived = sms-delivery-report-received DeliveryStatusReceived = sms.dlr.status none Submission API
SmsDatastoreReceived = sms-datastore-received both sms.request.new (keyed by tenant) and sms.dlr.status (keyed by the vendor's transaction id) see left Submission API DataProcessor
SmsRequestLogged = sms-request-logged SmsRequestLogged = sms.request.logged; the payload is defined by the Submission API, not by this package tenant Submission API
SmppServerAudit = sms-smpp-server-audit SmppPduAudit = sms.smpp.pdu SmppPduAuditEvent.PartitionKey SMPP server

An empty "Consumed by" means no consumer was found in the repositories checked when this table was written.

SMPP PDU audit event

sms_smpp_server publishes every SMPP PDU it receives or sends, one envelope per PDU. sms_smpp_connector will emit the same record on its own topic.

Topic Topics.SmppServerAudit = sms-smpp-server-audit (one topic per service)
Key SmppPduAuditEvent.PartitionKey: tenantId once the session is bound, else the bind's system_id, else the remote endpoint
Headers KafkaHeaders.TraceParent = traceparent, only when the envelope carries one
type EventTypes.SmppPduAudit = sms.smpp.pdu
source EventSources.SmppServer for the server; the connector uses its own
subject the SMPP command name, the same as data.command
timestamp when the PDU crossed the socket, the same as data.timestamp
traceparent W3C trace context, an extension attribute. Omitted, never null, when the PDU did not cross the socket inside a trace

Producing:

var record = new SmppPduAuditEvent
{
    Timestamp = DateTimeOffset.UtcNow,
    Direction = SmppPduDirection.In,
    RemoteEndpoint = "203.0.113.9:51234",
    Command = "enquire_link",
    CommandId = "0x00000015",
    CommandStatus = "ESME_ROK",
    CommandStatusCode = 0,
    SequenceNumber = 9,
    Pdu = new Dictionary<string, object?>()
};

CloudEvent<SmppPduAuditEvent> envelope = record.ToCloudEvent(new Uri(EventSources.SmppServer), Guid.NewGuid().ToString(), traceParent);

The record (data):

JSON name Type Null? Meaning
timestamp string, ISO 8601 with offset no when the PDU crossed the socket
direction "in" / "out" no in: peer to the emitting service; out: emitting service to the peer
sessionId string yes null before a bind is accepted
tenantId string yes null before a bind is accepted
systemId string yes the bound account; before the bind, the system_id the bind carried
remoteEndpoint string ip:port no the peer
bindType TX / RX / TRX yes from the session, else from the bind command
command string no SMPP 3.4 command name; an id without a name is its hex form
commandId string no 0x + 8 upper-case hex digits
commandStatus string no the ESME_* name
commandStatusCode number no the same status as a number
sequenceNumber number no 0 for a frame that could not be decoded
messageId string yes the platform message id, from an accepted submit_sm_resp or a receipt's deliver_sm. The join key to SmsRequestReceivedEvent and the delivery status events
pdu object no, may be {} the decoded body, keyed by the SmppPduFields constants
  • Nulls are written ("messageId": null), not omitted. Members that are not nullable are required: reading JSON that lacks one fails.
  • direction is always "in" or "out", whatever enum converter your options register; reading ignores case.
  • pdu keys are the SMPP 3.4 field names in snake_case and are written verbatim at every level, whatever DictionaryKeyPolicy your options set. Binary values are upper-case hex, cut at 1024 octets (short_message_truncated is true when the body was cut; sm_length keeps the real length). optional_parameters maps the TLV tag (0x + 4 hex digits) to the value in hex. When you read a record, every pdu value is a JsonElement.
  • The bind password is never in a record. Producers write SmppPduFields.Redacted ([redacted]) in its place, or null when the bind carried none.
  • A record with command generic_nack, direction in, sequenceNumber 0 and status ESME_RINVCMDID means "the peer sent a PDU that could not be decoded", not "the peer sent a generic_nack".

Serialization

The contracts are written for System.Text.Json.

  • Every property carries an explicit [JsonPropertyName], so a naming policy on your JsonSerializerOptions does not change property names.
  • Enum properties are always numbers on the wire. Each one carries a property-level JsonNumberEnumConverter, so a JsonStringEnumConverter registered on your options does not change them. A string such as "Unicode" is rejected when reading.
  • Prefer not to produce with JsonIgnoreCondition.WhenWritingDefault or WhenWritingNull. A consumer falls back to the property's initial value for anything you omit, so an omitted null can come back as an empty string or dictionary.
  • The envelope uses timestamp and content-type as attribute names rather than the CloudEvents time and datacontenttype. Existing consumers depend on these names.
  • DeliveryStatusReceivedEvent mirrors the current vendor's delivery report field names verbatim (txid, deliverydt, corelationid, dltcontentid). Its formats are the vendor's and are not specified here; the shape will be replaced when delivery reports move to SMPP.
  • to is the receiver and from is the sender. The contracts do not validate them; validation rules are applied by the Submission API.

Compatibility

  • Contracts live in versioned namespaces (V1). Changes within a version are additive: new optional properties and new enum values appended at the end.
  • JSON property names and enum numeric values are never renamed, reused or removed within a version. A breaking change ships as a new namespace (V2) alongside the old one.
  • One exception was made before the first .NET 10 release: 0.1.0 renumbered MessageType (then called MessageTypeEnum) to Auto = 0, Text = 1, Unicode = 2 (up to 0.0.5 it was Text = 0, Unicode = 1, Auto = 2). Producers and consumers of messageType must move to 0.1.0 together, and stored values written with the old numbers need migrating or reinterpreting.
  • Consumers should ignore properties they do not know and tolerate enum values they do not know.
  • Every contract change is released as a new package version.

Upgrading from 0.0.5

0.1.0 renamed several C# types and properties and moved some types to new namespaces. JSON property names did not change, so none of this affects the wire.

0.0.5 0.1.0
V1.Events.CloudEvents V1.Envelope.CloudEvent
V1.Events.Constants V1.Envelope.CloudEventDefaults
MessageTypeEnum MessageType
ServiceTypeEnum ServiceType
V1.Enums.CountryCodeEnum V1.Constants.CountryCodes
SmsRequestReceivedEvent.Country SmsRequestReceivedEvent.CountryCode
SmsRequestReceivedEvent.Flash (bool?) SmsRequestReceivedEvent.IsFlash (bool)
SmsRequestReceivedEvent.Source SmsRequestReceivedEvent.RequestSource
DeliveryStatusReceivedEvent.CorelationId DeliveryStatusReceivedEvent.CorrelationId (JSON stays corelationid)

The changes that do affect the wire or behaviour are listed in the package release notes; the MessageType renumbering is the one that needs producers and consumers to upgrade together.

Versioning

Package versions follow major.minor.patch:

Change Bump Example
Documentation, packaging or metadata only; no change to the API or to what is sent on the wire patch 0.1.0 → 0.1.1
Additive contract change: a new event, a new optional property, a new enum value minor 0.1.0 → 0.2.0
Anything a consumer or producer can notice without opting in: a changed default, a removed or renamed public member, a dropped target framework minor while the version is 0.x, major from 1.0.0 0.1.0 → 0.2.0, 1.2.0 → 2.0.0

While the version is 0.x, read the release notes before taking a new minor version. A breaking change to the JSON shape of an existing event never ships under the same namespace at any version; it becomes a new V2 type.

License

This project is licensed under the MIT License.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.1 93 9/19/2026
0.1.0 85 9/19/2026
0.0.5 384 4/3/2025
0.0.4 229 4/2/2025
0.0.3 229 4/2/2025
0.0.2 226 4/2/2025
0.0.1 226 4/2/2025

0.1.1
- Packaging only: the package is now validated against 0.1.0 when it is built. No change to the API or to the JSON.

0.1.0
Breaking:
- Targets .NET 10 only. 0.0.5 is the last version for .NET 8.
- The message type enum is renumbered to Auto = 0, Text = 1, Unicode = 2 (was Text = 0, Unicode = 1, Auto = 2). Producers and consumers of messageType must upgrade together; stored numbers need migrating.
- The flash property of SmsRequestReceivedEvent is bool instead of bool?. A JSON null for flash fails deserialization.
- SmsRequestReceivedEvent.TemplateId, EntityId, Metadata and Properties are no longer nullable; null is read as empty.
- The country code constants class is static.
Renamed (C# only; JSON property names are unchanged):
- Namespaces now follow folders: the envelope types are in V1.Envelope and the constants classes (including CountryCodes) in V1.Constants; events stay in V1.Events and enums in V1.Enums.
- CloudEvents to CloudEvent; Constants to CloudEventDefaults.
- MessageTypeEnum to MessageType; ServiceTypeEnum to ServiceType; CountryCodeEnum to CountryCodes.
- SmsRequestReceivedEvent: Country to CountryCode, Flash to IsFlash, Source to RequestSource.
- DeliveryStatusReceivedEvent: CorelationId to CorrelationId.
Behaviour:
- DeliveryStatusReceivedEvent.DeliveryDate defaults to an empty string instead of the current local time.
- CloudEvent.Timestamp defaults to null instead of the current time. Use CloudEvent.Create to stamp it.
- Enum properties are always serialized as numbers, whatever converters the serializer options register.
Added:
- CloudEvent<TData>, a typed envelope with the same JSON shape.
- CloudEvent.Create, which generates the id and timestamp.
- EventTypes constants for the envelope type attribute, and Topics constants for the Kafka topic names.
- SmppPduAuditEvent (type sms.smpp.pdu), SmppPduDirection, SmppPduFields, and the EventSources, Topics and KafkaHeaders constants. The JSON is identical to what sms_smpp_server emits today.
- CloudEvent.TraceParent, the traceparent extension attribute, omitted when null. A subclass that declares its own traceparent property must drop it.
- ServiceType.ServiceImplicit = 2 and ServiceType.Government = 3.
- XML documentation and a symbols package.