Table of Contents

Class SupportCaseEntity

Namespace
Tharga.Team.MongoDB
Assembly
Tharga.Team.MongoDB.dll

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
PersistableEntityBase
EntityBase<ObjectId>
EntityBase
SupportCaseEntity
Implements
IEquatable<PersistableEntityBase>
IEntity<ObjectId>
IEquatable<EntityBase<ObjectId>>
IEquatable<EntityBase>
Inherited Members
EntityBase.ToString()
EntityBase.GetHashCode()
EntityBase.Equals(EntityBase<ObjectId>)
EntityBase.Equals(EntityBase)
EntityBase.<Clone>$()
EntityBase.EqualityContract
EntityBase<ObjectId>.Equals(PersistableEntityBase)
EntityBase<ObjectId>.Id
PersistableEntityBase.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

SupportAssistantState

AuthorIdentity

public required string AuthorIdentity { get; init; }

Property Value

string

AuthorName

public required string AuthorName { get; init; }

Property Value

string

Bindings

public SupportChannelBindingEntity[] Bindings { get; init; }

Property Value

SupportChannelBindingEntity[]

CaseId

The case id. Distinct from EntityBase.Id, which is the document's ObjectId.

public required string CaseId { get; init; }

Property Value

string

ClosedAt

[BsonIgnoreIfNull]
public DateTime? ClosedAt { get; init; }

Property Value

DateTime?

ClosedBy

[BsonIgnoreIfNull]
public string ClosedBy { get; init; }

Property Value

string

CreatedAt

public required DateTime CreatedAt { get; init; }

Property Value

DateTime

LastMessageAt

When the newest transcript entry was written. Absent on cases last written before this existed.

[BsonIgnoreIfNull]
public DateTime? LastMessageAt { get; init; }

Property Value

DateTime?

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

bool

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

SupportMessageEntity[]

Reads

How far each participant has read. Absent until somebody opens the case.

[BsonIgnoreIfNull]
public SupportCaseReadEntity[] Reads { get; init; }

Property Value

SupportCaseReadEntity[]

Status

[BsonRepresentation(BsonType.String)]
public required SupportCaseStatus Status { get; init; }

Property Value

SupportCaseStatus

Subject

public required string Subject { get; init; }

Property Value

string

TeamKey

The owning team, or absent when the case is unassigned.

[BsonIgnoreIfNull]
public string TeamKey { get; init; }

Property Value

string

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.