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
<PackageReference Include="ContactWise.Messaging.Contracts" Version="0.1.1" />
<PackageVersion Include="ContactWise.Messaging.Contracts" Version="0.1.1" />
<PackageReference Include="ContactWise.Messaging.Contracts" />
paket add ContactWise.Messaging.Contracts --version 0.1.1
#r "nuget: ContactWise.Messaging.Contracts, 0.1.1"
#:package ContactWise.Messaging.Contracts@0.1.1
#addin nuget:?package=ContactWise.Messaging.Contracts&version=0.1.1
#tool nuget:?package=ContactWise.Messaging.Contracts&version=0.1.1
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 arerequired: reading JSON that lacks one fails. directionis always"in"or"out", whatever enum converter your options register; reading ignores case.pdukeys are the SMPP 3.4 field names in snake_case and are written verbatim at every level, whateverDictionaryKeyPolicyyour options set. Binary values are upper-case hex, cut at 1024 octets (short_message_truncatedistruewhen the body was cut;sm_lengthkeeps the real length).optional_parametersmaps the TLV tag (0x+ 4 hex digits) to the value in hex. When you read a record, everypduvalue is aJsonElement.- The bind password is never in a record. Producers write
SmppPduFields.Redacted([redacted]) in its place, ornullwhen the bind carried none. - A record with
commandgeneric_nack,directionin,sequenceNumber0 and statusESME_RINVCMDIDmeans "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 yourJsonSerializerOptionsdoes not change property names. - Enum properties are always numbers on the wire. Each one carries a property-level
JsonNumberEnumConverter, so aJsonStringEnumConverterregistered on your options does not change them. A string such as"Unicode"is rejected when reading. - Prefer not to produce with
JsonIgnoreCondition.WhenWritingDefaultorWhenWritingNull. A consumer falls back to the property's initial value for anything you omit, so an omittednullcan come back as an empty string or dictionary. - The envelope uses
timestampandcontent-typeas attribute names rather than the CloudEventstimeanddatacontenttype. Existing consumers depend on these names. DeliveryStatusReceivedEventmirrors 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.tois the receiver andfromis 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 calledMessageTypeEnum) toAuto = 0,Text = 1,Unicode = 2(up to 0.0.5 it wasText = 0,Unicode = 1,Auto = 2). Producers and consumers ofmessageTypemust 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 | Versions 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. |
-
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.
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.