Class SupportCaseEntity
A support case and its whole transcript, in one document.
public record SupportCaseEntity : EntityBase, ISupportInitialize, IEquatable<PersistableEntityBase>, IEntity<ObjectId>, IEquatable<EntityBase<ObjectId>>, IEquatable<EntityBase>, IEquatable<SupportCaseEntity>
- Inheritance
-
PersistableEntityBaseEntityBase<ObjectId>EntityBaseSupportCaseEntity
- Implements
-
IEquatable<PersistableEntityBase>IEntity<ObjectId>IEquatable<EntityBase>
- Inherited Members
-
EntityBase.ToString()EntityBase.GetHashCode()EntityBase.Equals(EntityBase<ObjectId>)EntityBase.Equals(EntityBase)EntityBase.<Clone>$()EntityBase.EqualityContractEntityBase<ObjectId>.Equals(PersistableEntityBase)EntityBase<ObjectId>.IdPersistableEntityBase.NeedsCleaning()PersistableEntityBase.BeginInit()PersistableEntityBase.EndInit()PersistableEntityBase.CatchAll
Remarks
Messages are embedded rather than stored in their own collection, and that is what buys atomicity.
Raising a case creates a case and its first message; closing one sets a status and records why. Embedded,
each of those is a single document write and cannot half-happen. In a second collection each would be two
writes needing a transaction to keep the model's promise that a case always has a transcript. Team members
are embedded in TeamEntityBase.Members for the same reason.
What bounds the document, since an embedded array grows. MongoDB's hard limit is 16 MB. Two caps keep this well clear of it, and both are enforced in the store rather than left to hope: MaxMessagesPerCase and MaxMessageLength. At their product the transcript is roughly 5 MB of text, so even a case of maximum-length messages has substantial headroom.
The message-length cap is not only about the document size. Support text is exactly where somebody pastes a log file, so an unbounded body is how one message becomes a megabyte.
Properties
AssistantState
[BsonIgnoreIfNull]
[BsonRepresentation(BsonType.String)]
public SupportAssistantState AssistantState { get; init; }
Property Value
AuthorIdentity
public required string AuthorIdentity { get; init; }
Property Value
AuthorName
public required string AuthorName { get; init; }
Property Value
Bindings
public SupportChannelBindingEntity[] Bindings { get; init; }
Property Value
CaseId
The case id. Distinct from EntityBase.Id, which is the document's ObjectId.
public required string CaseId { get; init; }
Property Value
ClosedAt
[BsonIgnoreIfNull]
public DateTime? ClosedAt { get; init; }
Property Value
ClosedBy
[BsonIgnoreIfNull]
public string ClosedBy { get; init; }
Property Value
CreatedAt
public required DateTime CreatedAt { get; init; }
Property Value
LastMessageAt
When the newest transcript entry was written. Absent on cases last written before this existed.
[BsonIgnoreIfNull]
public DateTime? LastMessageAt { get; init; }
Property Value
Remarks
Denormalized for the same reason as LastMessageFromAuthor: "how old is the last element of an embedded array" is not an indexable filter, and the inactivity sweep has to narrow the whole collection cheaply before it looks at any transcript.
Absent means never swept, and that is the safe direction. A case last touched before this field existed does not auto-close until somebody writes to it — auto-closing is new behaviour, and applying it retroactively to a backlog nobody has looked at would close cases in bulk on the first sweep after an upgrade.
LastMessageFromAuthor
Whether the newest transcript entry came from the person who raised the case — that is, whether the case is waiting on support.
public bool LastMessageFromAuthor { get; init; }
Property Value
Remarks
Denormalized deliberately. The question is "is the last element of an embedded array authored by the same person as this document's author", which cannot be expressed as a cheap indexed filter - it needs an aggregation over every case. Maintained at the two places a transcript grows, so the awaiting-support count stays a plain filter however many cases a team accumulates.
Messages
public required SupportMessageEntity[] Messages { get; init; }
Property Value
Reads
How far each participant has read. Absent until somebody opens the case.
[BsonIgnoreIfNull]
public SupportCaseReadEntity[] Reads { get; init; }
Property Value
Status
[BsonRepresentation(BsonType.String)]
public required SupportCaseStatus Status { get; init; }
Property Value
Subject
public required string Subject { get; init; }
Property Value
TeamKey
The owning team, or absent when the case is unassigned.
[BsonIgnoreIfNull]
public string TeamKey { get; init; }
Property Value
Remarks
[BsonIgnoreIfNull] so an unassigned case stores no team at all rather than an empty string —
a filter for "no team" then means what it says, and cannot accidentally match a case whose key was
written as blank.