|
1 | 1 | # TypedItem |
2 | 2 |
|
3 | | -This library is a set of method extensions other CosmosDb SQL Api to handle typed elements and soft delete in a container |
| 3 | +**TypedItem** is a .NET extension library for **Azure Cosmos DB (SQL API)** that adds typed item management, soft delete, and hierarchical type queries to any Cosmos DB container. |
4 | 4 |
|
5 | | -## Why giving a type for giving a type in a CosmosDb Container. |
| 5 | +Each document stores a `_type` field computed from C# inheritance and `[ItemType]` attributes. This lets you safely store multiple item types in a single container and query them with full type safety. |
6 | 6 |
|
7 | | -This is wellknown best practice for Cosmos DB (see: https://youtu.be/bQBeTeYUrR8?t=1074). This library helps you to hide the complexity of the type management |
| 7 | +## Installation |
8 | 8 |
|
9 | | -## What are the constraints on my container? |
10 | | - |
11 | | -TypedItem add specifics fields and methods in your documents: |
12 | | -* ```_pk``` the partition key ( for 1.X version of the SDK) |
13 | | -* ```_type``` the type of the item |
14 | | -* ```_deleted``` the deletion status (SoftDelete) |
15 | | - |
16 | | -Type and deletion status are handled by the extention methods |
17 | | - |
18 | | -Starting with version 2.0, the library is compatible with the 3.X version of the SDK. The partition key is now managed by the fonction ```abstract PartitionKey GetPartitionKey()``` and the library doesn't need the ```_pk``` field anymore. This model let you manage the partition key as you want. Even for hierarchical partition key |
19 | | - |
20 | | -### Before 2.0: How to add a type to a class ? |
| 9 | +```bash |
| 10 | +dotnet add package TypedItem |
| 11 | +``` |
21 | 12 |
|
22 | | -You have to: |
23 | | -* inherits from ```TypedItemBase``` |
24 | | -* add the attribute ```ItemType``` |
25 | | -* seal the class |
| 13 | +Requires **Microsoft.Azure.Cosmos 3.x** and **.NET 10**. |
26 | 14 |
|
27 | | -For example: |
| 15 | +## Quick start |
28 | 16 |
|
| 17 | +### 1 — Define a partition-key base class |
29 | 18 |
|
30 | 19 | ```csharp |
31 | | - |
32 | | - [ItemType("person")] |
33 | | - public sealed class PersonItem: TypedItemBase |
34 | | - { |
35 | | - |
36 | | - [JsonProperty("firstName")] |
37 | | - public string FirstName { get; set; } |
38 | | - |
39 | | - [JsonProperty("lastName")] |
40 | | - public string LastName { get; set; } |
41 | | - |
42 | | - [JsonProperty("birthdate")] |
43 | | - public DateTime BirthDate { get; set; } |
44 | | - } |
| 20 | +public class MyContainerItem : TypedItemBase |
| 21 | +{ |
| 22 | + [JsonProperty("part")] |
| 23 | + public string Part { get; set; } |
| 24 | + |
| 25 | + public override PartitionKey GetPartitionKey() |
| 26 | + => CreatePartitionKey(Part); |
| 27 | +} |
45 | 28 | ``` |
46 | | -In the container, the type will be ```person``` |
47 | 29 |
|
| 30 | +### 2 — Define typed item classes |
48 | 31 |
|
49 | | -If you what to create type hierarchy you can do this: |
| 32 | +Annotate with `[ItemType("name")]`. Write operations require a `sealed` class. |
50 | 33 |
|
51 | 34 | ```csharp |
52 | | - |
53 | | - [ItemType("event")] |
54 | | - public class EventItem: TypedItemBase |
55 | | - { |
| 35 | +[ItemType("person")] |
| 36 | +public sealed class PersonItem : MyContainerItem |
| 37 | +{ |
| 38 | + [JsonProperty("firstName")] public string FirstName { get; set; } |
| 39 | + [JsonProperty("lastName")] public string LastName { get; set; } |
| 40 | +} |
| 41 | +``` |
56 | 42 |
|
57 | | - [JsonProperty("date")] |
58 | | - public DateTime Date { get; set; } |
59 | | - } |
| 43 | +### 3 — Use extension methods |
60 | 44 |
|
61 | | - [ItemType("phonecall")] |
62 | | - public sealed class PhonecallItem: EventItem |
63 | | - { |
| 45 | +All methods extend `Microsoft.Azure.Cosmos.Container` and `TransactionalBatch`: |
64 | 46 |
|
65 | | - [JsonProperty("date")] |
66 | | - public int Duration { get; set; } |
67 | | - } |
| 47 | +```csharp |
| 48 | +// Write |
| 49 | +await container.CreateTypedItemAsync(person); |
| 50 | +await container.UpsertTypedItemAsync(person); |
| 51 | +await container.ReplaceTypedItemAsync(person, person.Id); |
68 | 52 |
|
69 | | - [ItemType("teamsmeeting")] |
70 | | - public sealed class TeamsMeeting: EventItem |
71 | | - { |
| 53 | +// Read — throws CosmosException (404) if soft-deleted or wrong type |
| 54 | +var response = await container.ReadTypedItemAsync<PersonItem>(id, partitionKey); |
72 | 55 |
|
73 | | - [JsonProperty("peoples")] |
74 | | - public string[] Peoples { get; set; } |
75 | | - } |
| 56 | +// Soft-delete (sets _deleted = true via conditional PATCH) |
| 57 | +await container.SoftDeleteTypedItemAsync(person); |
76 | 58 |
|
| 59 | +// Query |
| 60 | +var result = await container.QueryTypedItemAsync<PersonItem, PersonItem>(q => q); |
77 | 61 | ``` |
78 | 62 |
|
79 | | -You can create 2 differents items in the container: |
80 | | -* one with the type ```event.phonecall``` |
81 | | -* one with the type ```event.teamsmeeting``` |
82 | | - |
83 | | -You can query all the events at once too with the method ```QueryTypedItemAsync``` |
84 | | - |
85 | | - |
86 | | -### After 2.0: How to add a type to a class ? |
| 63 | +## Type hierarchy |
87 | 64 |
|
88 | | -You have to: |
89 | | -* create a root class from ```TypedItemBase``` to manage the partition key |
90 | | -* add the attribute ```ItemType``` |
91 | | -* seal the class |
92 | | - |
93 | | -For example: |
| 65 | +Use C# inheritance with multiple `[ItemType]` attributes to build dot-separated type hierarchies: |
94 | 66 |
|
95 | 67 | ```csharp |
96 | | - |
97 | | - public class ContainerItem : TypedItemBase |
98 | | - { |
99 | | - [JsonProperty("part")] |
100 | | - public string Part { get; set; } |
101 | | - |
102 | | - public override PartitionKey GetPartitionKey() |
103 | | - { |
104 | | - return CreatePartitionKey(Part); |
105 | | - } |
106 | | - } |
| 68 | +[ItemType("event")] // non-sealed = queryable parent |
| 69 | +public class EventItem : MyContainerItem |
| 70 | +{ |
| 71 | + [JsonProperty("date")] public DateTime Date { get; set; } |
| 72 | +} |
| 73 | + |
| 74 | +[ItemType("phonecall")] // _type stored as "event.phonecall" |
| 75 | +public sealed class PhonecallItem : EventItem |
| 76 | +{ |
| 77 | + [JsonProperty("duration")] public int Duration { get; set; } |
| 78 | +} |
| 79 | + |
| 80 | +[ItemType("meeting")] // _type stored as "event.meeting" |
| 81 | +public sealed class MeetingItem : EventItem |
| 82 | +{ |
| 83 | + [JsonProperty("attendees")] public string[] Attendees { get; set; } |
| 84 | +} |
107 | 85 | ``` |
108 | 86 |
|
109 | | -```csharp |
110 | | - |
111 | | - [ItemType("person")] |
112 | | - public sealed class PersonItem: ContainerItem |
113 | | - { |
114 | | - |
115 | | - [JsonProperty("firstName")] |
116 | | - public string FirstName { get; set; } |
117 | | - |
118 | | - [JsonProperty("lastName")] |
119 | | - public string LastName { get; set; } |
120 | | - |
121 | | - [JsonProperty("birthdate")] |
122 | | - public DateTime BirthDate { get; set; } |
123 | | - } |
124 | | -``` |
125 | | -In the container, the type will be ```person``` |
126 | | - |
127 | | - |
128 | | -If you what to create type hierarchy you can do this: |
| 87 | +`QueryTypedItemAsync` automatically filters by type: |
129 | 88 |
|
130 | 89 | ```csharp |
131 | | - |
132 | | - [ItemType("event")] |
133 | | - public class EventItem: ContainerItem |
134 | | - { |
| 90 | +// Returns only phone calls |
| 91 | +var calls = await container.QueryTypedItemAsync<PhonecallItem, PhonecallItem>(q => q); |
135 | 92 |
|
136 | | - [JsonProperty("date")] |
137 | | - public DateTime Date { get; set; } |
138 | | - } |
139 | | - |
140 | | - [ItemType("phonecall")] |
141 | | - public sealed class PhonecallItem: EventItem |
142 | | - { |
143 | | - |
144 | | - [JsonProperty("date")] |
145 | | - public int Duration { get; set; } |
146 | | - } |
147 | | - |
148 | | - [ItemType("teamsmeeting")] |
149 | | - public sealed class TeamsMeeting: EventItem |
150 | | - { |
| 93 | +// Returns ALL events (phone calls + meetings) |
| 94 | +var events = await container.QueryTypedItemAsync<EventItem, EventItem>(q => q); |
| 95 | +``` |
151 | 96 |
|
152 | | - [JsonProperty("peoples")] |
153 | | - public string[] Peoples { get; set; } |
154 | | - } |
| 97 | +## Fields stored in documents |
155 | 98 |
|
156 | | -``` |
| 99 | +| JSON field | Description | |
| 100 | +|---|---| |
| 101 | +| `_type` | Dot-separated type identifier (e.g., `"event.phonecall"`) | |
| 102 | +| `_deleted` | Soft-delete flag (`true` = logically deleted) | |
| 103 | +| `id` | Document id (auto-generated if not set) | |
157 | 104 |
|
158 | | -You can create 2 differents items in the container: |
159 | | -* one with the type ```event.phonecall``` |
160 | | -* one with the type ```event.teamsmeeting``` |
| 105 | +## Links |
161 | 106 |
|
162 | | -You can query all the events at once too with the method ```QueryTypedItemAsync``` |
| 107 | +- [Getting Started](docs/user/getting-started.md) — full usage guide with all options |
| 108 | +- [API Reference](docs/technical/api-reference.md) — all extension methods and types |
| 109 | +- [Running Tests](docs/technical/running-tests.md) — test infrastructure and known limitations |
0 commit comments