diff --git a/Datasync.Toolkit.sln b/Datasync.Toolkit.sln
index c823206..071ffe9 100644
--- a/Datasync.Toolkit.sln
+++ b/Datasync.Toolkit.sln
@@ -64,10 +64,16 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{75F7
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.Datasync.Server", "samples\datasync-server\src\Sample.Datasync.Server\Sample.Datasync.Server.csproj", "{A9967817-2A2C-4C6D-A133-967A6062E9B3}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Datasync.Server.CosmosDb", "src\CommunityToolkit.Datasync.Server.CosmosDb\CommunityToolkit.Datasync.Server.CosmosDb.csproj", "{D9356867-0A30-4B17-BD4C-0F7EF70984C6}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.Datasync.Server.SingleContainer", "samples\datasync-server\datasync-server-cosmosdb\Sample.Datasync.Server.SingleContainer\Sample.Datasync.Server.SingleContainer.csproj", "{60C73E92-9A45-4EE6-8DCF-48206CD0E5FE}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Datasync.Server.MongoDB", "src\CommunityToolkit.Datasync.Server.MongoDB\CommunityToolkit.Datasync.Server.MongoDB.csproj", "{DC20ACF9-12E9-41D9-B672-CB5FD85548E9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Datasync.Server.MongoDB.Test", "tests\CommunityToolkit.Datasync.Server.MongoDB.Test\CommunityToolkit.Datasync.Server.MongoDB.Test.csproj", "{4FC45D20-0BA9-484B-9040-641687659AF6}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Datasync.Server.CosmosDb.Test", "tests\CommunityToolkit.Datasync.Server.CosmosDb.Test\CommunityToolkit.Datasync.Server.CosmosDb.Test.csproj", "{FC15CF34-2C1A-4B15-85CC-A99EA25C47D2}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -158,6 +164,18 @@ Global
{A9967817-2A2C-4C6D-A133-967A6062E9B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A9967817-2A2C-4C6D-A133-967A6062E9B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A9967817-2A2C-4C6D-A133-967A6062E9B3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D9356867-0A30-4B17-BD4C-0F7EF70984C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D9356867-0A30-4B17-BD4C-0F7EF70984C6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D9356867-0A30-4B17-BD4C-0F7EF70984C6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D9356867-0A30-4B17-BD4C-0F7EF70984C6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {60C73E92-9A45-4EE6-8DCF-48206CD0E5FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {60C73E92-9A45-4EE6-8DCF-48206CD0E5FE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {60C73E92-9A45-4EE6-8DCF-48206CD0E5FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {60C73E92-9A45-4EE6-8DCF-48206CD0E5FE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FC15CF34-2C1A-4B15-85CC-A99EA25C47D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FC15CF34-2C1A-4B15-85CC-A99EA25C47D2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FC15CF34-2C1A-4B15-85CC-A99EA25C47D2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FC15CF34-2C1A-4B15-85CC-A99EA25C47D2}.Release|Any CPU.Build.0 = Release|Any CPU
{DC20ACF9-12E9-41D9-B672-CB5FD85548E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DC20ACF9-12E9-41D9-B672-CB5FD85548E9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DC20ACF9-12E9-41D9-B672-CB5FD85548E9}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -192,6 +210,9 @@ Global
{D3B72031-D4BD-44D3-973C-2752AB1570F6} = {84AD662A-4B9E-4E64-834D-72529FB7FCE5}
{2889E6B2-9CD1-437C-A43C-98CFAFF68B99} = {D59F1489-5D74-4F52-B78B-88037EAB2838}
{A9967817-2A2C-4C6D-A133-967A6062E9B3} = {75F709FD-8CC2-4558-A802-FE57086167C2}
+ {D9356867-0A30-4B17-BD4C-0F7EF70984C6} = {84AD662A-4B9E-4E64-834D-72529FB7FCE5}
+ {60C73E92-9A45-4EE6-8DCF-48206CD0E5FE} = {75F709FD-8CC2-4558-A802-FE57086167C2}
+ {FC15CF34-2C1A-4B15-85CC-A99EA25C47D2} = {D59F1489-5D74-4F52-B78B-88037EAB2838}
{DC20ACF9-12E9-41D9-B672-CB5FD85548E9} = {84AD662A-4B9E-4E64-834D-72529FB7FCE5}
{4FC45D20-0BA9-484B-9040-641687659AF6} = {D59F1489-5D74-4F52-B78B-88037EAB2838}
EndGlobalSection
diff --git a/Directory.Packages.props b/Directory.Packages.props
index f759986..638bd72 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -11,6 +11,7 @@
+
@@ -18,12 +19,14 @@
+
+
diff --git a/samples/datasync-server/datasync-server-cosmosdb/Sample.Datasync.Server.SingleContainer/Controllers/TodoItemController.cs b/samples/datasync-server/datasync-server-cosmosdb/Sample.Datasync.Server.SingleContainer/Controllers/TodoItemController.cs
new file mode 100644
index 0000000..86c7aa4
--- /dev/null
+++ b/samples/datasync-server/datasync-server-cosmosdb/Sample.Datasync.Server.SingleContainer/Controllers/TodoItemController.cs
@@ -0,0 +1,18 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using CommunityToolkit.Datasync.Server;
+using Microsoft.AspNetCore.Mvc;
+using Sample.Datasync.Server.SingleContainer.Models;
+
+namespace Sample.Datasync.Server.Controllers;
+
+[Route("tables/[controller]")]
+public class TodoItemController : TableController
+{
+ public TodoItemController(IRepository repository)
+ : base(repository)
+ {
+ }
+}
\ No newline at end of file
diff --git a/samples/datasync-server/datasync-server-cosmosdb/Sample.Datasync.Server.SingleContainer/Controllers/TodoListController.cs b/samples/datasync-server/datasync-server-cosmosdb/Sample.Datasync.Server.SingleContainer/Controllers/TodoListController.cs
new file mode 100644
index 0000000..1795e77
--- /dev/null
+++ b/samples/datasync-server/datasync-server-cosmosdb/Sample.Datasync.Server.SingleContainer/Controllers/TodoListController.cs
@@ -0,0 +1,18 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using CommunityToolkit.Datasync.Server;
+using Microsoft.AspNetCore.Mvc;
+using Sample.Datasync.Server.SingleContainer.Models;
+
+namespace Sample.Datasync.Server.Controllers;
+
+[Route("tables/[controller]")]
+public class TodoListController : TableController
+{
+ public TodoListController(IRepository repository) : base(repository)
+ {
+ Options = new TableControllerOptions { EnableSoftDelete = true };
+ }
+}
\ No newline at end of file
diff --git a/samples/datasync-server/datasync-server-cosmosdb/Sample.Datasync.Server.SingleContainer/Models/TodoItem.cs b/samples/datasync-server/datasync-server-cosmosdb/Sample.Datasync.Server.SingleContainer/Models/TodoItem.cs
new file mode 100644
index 0000000..d26663f
--- /dev/null
+++ b/samples/datasync-server/datasync-server-cosmosdb/Sample.Datasync.Server.SingleContainer/Models/TodoItem.cs
@@ -0,0 +1,16 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using CommunityToolkit.Datasync.Server.CosmosDb;
+using System.ComponentModel.DataAnnotations;
+
+namespace Sample.Datasync.Server.SingleContainer.Models;
+
+public class TodoItem : CosmosTableData
+{
+ [Required, MinLength(1)]
+ public string Title { get; set; } = string.Empty;
+
+ public bool IsComplete { get; set; }
+}
diff --git a/samples/datasync-server/datasync-server-cosmosdb/Sample.Datasync.Server.SingleContainer/Models/TodoList.cs b/samples/datasync-server/datasync-server-cosmosdb/Sample.Datasync.Server.SingleContainer/Models/TodoList.cs
new file mode 100644
index 0000000..0c9c296
--- /dev/null
+++ b/samples/datasync-server/datasync-server-cosmosdb/Sample.Datasync.Server.SingleContainer/Models/TodoList.cs
@@ -0,0 +1,16 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using CommunityToolkit.Datasync.Server.CosmosDb;
+using System.ComponentModel.DataAnnotations;
+
+namespace Sample.Datasync.Server.SingleContainer.Models;
+
+public class TodoList : CosmosTableData
+{
+ [Required, MinLength(1)]
+ public string Title { get; set; } = string.Empty;
+
+ public string? ListId { get; set; }
+}
diff --git a/samples/datasync-server/datasync-server-cosmosdb/Sample.Datasync.Server.SingleContainer/Program.cs b/samples/datasync-server/datasync-server-cosmosdb/Sample.Datasync.Server.SingleContainer/Program.cs
new file mode 100644
index 0000000..02059d5
--- /dev/null
+++ b/samples/datasync-server/datasync-server-cosmosdb/Sample.Datasync.Server.SingleContainer/Program.cs
@@ -0,0 +1,86 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using CommunityToolkit.Datasync.Server;
+using CommunityToolkit.Datasync.Server.Abstractions.Json;
+using CommunityToolkit.Datasync.Server.CosmosDb;
+using CommunityToolkit.Datasync.Server.Swashbuckle;
+using Microsoft.Azure.Cosmos;
+using Sample.Datasync.Server.SingleContainer.Models;
+using System.Collections.ObjectModel;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+var builder = WebApplication.CreateBuilder(args);
+
+string connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
+ ?? throw new ApplicationException("DefaultConnection is not set");
+
+CosmosClient cosmosClient = new CosmosClient(connectionString,
+ new CosmosClientOptions()
+ {
+ UseSystemTextJsonSerializerWithOptions = new()
+ {
+ Converters =
+ {
+ new JsonStringEnumConverter(),
+ new DateTimeOffsetConverter(),
+ new DateTimeConverter(),
+ new TimeOnlyConverter(),
+ new SpatialGeoJsonConverter()
+ },
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull | JsonIgnoreCondition.WhenWritingDefault,
+ NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals,
+ ReferenceHandler = ReferenceHandler.Preserve
+ }
+ });
+
+builder.Services.AddSingleton(cosmosClient);
+builder.Services.AddSingleton>(new CosmosSharedTableOptions("TodoDb", "TodoContainer"));
+builder.Services.AddSingleton>(new CosmosSharedTableOptions("TodoDb", "TodoContainer"));
+builder.Services.AddSingleton(typeof(IRepository<>), typeof(CosmosTableRepository<>));
+// Add services to the container.
+
+builder.Services.AddDatasyncServices();
+
+builder.Services.AddControllers();
+
+_ = builder.Services
+ .AddEndpointsApiExplorer()
+ .AddSwaggerGen(options => options.AddDatasyncControllers());
+
+var app = builder.Build();
+
+// Configure the HTTP request pipeline.
+if (app.Environment.IsDevelopment())
+{
+ _ = app.UseSwagger().UseSwaggerUI();
+ _ = app.UseDeveloperExceptionPage();
+
+ Database database = await cosmosClient.CreateDatabaseIfNotExistsAsync("TodoDb");
+
+ _ = await database.CreateContainerIfNotExistsAsync(new ContainerProperties("TodoContainer", "/entity")
+ {
+ IndexingPolicy = new()
+ {
+ CompositeIndexes =
+ {
+ new Collection()
+ {
+ new CompositePath() { Path = "/updatedAt", Order = CompositePathSortOrder.Ascending },
+ new CompositePath() { Path = "/id", Order = CompositePathSortOrder.Ascending }
+ }
+ }
+ }
+ });
+}
+
+app.UseHttpsRedirection();
+
+app.UseAuthorization();
+
+app.MapControllers();
+
+app.Run();
diff --git a/samples/datasync-server/datasync-server-cosmosdb/Sample.Datasync.Server.SingleContainer/Properties/launchSettings.json b/samples/datasync-server/datasync-server-cosmosdb/Sample.Datasync.Server.SingleContainer/Properties/launchSettings.json
new file mode 100644
index 0000000..5db3ef9
--- /dev/null
+++ b/samples/datasync-server/datasync-server-cosmosdb/Sample.Datasync.Server.SingleContainer/Properties/launchSettings.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "http://localhost:5024",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "https://localhost:7284;http://localhost:5024",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/samples/datasync-server/datasync-server-cosmosdb/Sample.Datasync.Server.SingleContainer/Sample.Datasync.Server.SingleContainer.csproj b/samples/datasync-server/datasync-server-cosmosdb/Sample.Datasync.Server.SingleContainer/Sample.Datasync.Server.SingleContainer.csproj
new file mode 100644
index 0000000..f7ed305
--- /dev/null
+++ b/samples/datasync-server/datasync-server-cosmosdb/Sample.Datasync.Server.SingleContainer/Sample.Datasync.Server.SingleContainer.csproj
@@ -0,0 +1,16 @@
+
+
+
+ net9.0
+ enable
+ enable
+ 4fb4ad03-5b6a-43b8-8e71-90b9d8252f94
+
+
+
+
+
+
+
+
+
diff --git a/samples/datasync-server/datasync-server-cosmosdb/Sample.Datasync.Server.SingleContainer/Sample.Datasync.Server.SingleContainer.http b/samples/datasync-server/datasync-server-cosmosdb/Sample.Datasync.Server.SingleContainer/Sample.Datasync.Server.SingleContainer.http
new file mode 100644
index 0000000..f8c3d5e
--- /dev/null
+++ b/samples/datasync-server/datasync-server-cosmosdb/Sample.Datasync.Server.SingleContainer/Sample.Datasync.Server.SingleContainer.http
@@ -0,0 +1,33 @@
+@Sample.Datasync.Server.SingleContainer_HostAddress = https://localhost:7284
+
+POST {{Sample.Datasync.Server.SingleContainer_HostAddress}}/tables/TodoItem
+Content-Type: application/json
+
+{
+ "id": "2",
+ "title": "Second item",
+ "isComplete": false
+}
+
+###
+
+GET {{Sample.Datasync.Server.SingleContainer_HostAddress}}/tables/TodoItem
+Accept: application/json
+
+###
+
+POST {{Sample.Datasync.Server.SingleContainer_HostAddress}}/tables/TodoList
+Content-Type: application/json
+
+{
+ "id": "1",
+ "title": "First List",
+ "listId": "My List"
+}
+
+###
+
+GET {{Sample.Datasync.Server.SingleContainer_HostAddress}}/tables/TodoList
+Accept: application/json
+
+###
\ No newline at end of file
diff --git a/samples/datasync-server/datasync-server-cosmosdb/Sample.Datasync.Server.SingleContainer/appsettings.Development.json b/samples/datasync-server/datasync-server-cosmosdb/Sample.Datasync.Server.SingleContainer/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/samples/datasync-server/datasync-server-cosmosdb/Sample.Datasync.Server.SingleContainer/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/samples/datasync-server/datasync-server-cosmosdb/Sample.Datasync.Server.SingleContainer/appsettings.json b/samples/datasync-server/datasync-server-cosmosdb/Sample.Datasync.Server.SingleContainer/appsettings.json
new file mode 100644
index 0000000..10f68b8
--- /dev/null
+++ b/samples/datasync-server/datasync-server-cosmosdb/Sample.Datasync.Server.SingleContainer/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/src/CommunityToolkit.Datasync.Server.CosmosDb/CommunityToolkit.Datasync.Server.CosmosDb.csproj b/src/CommunityToolkit.Datasync.Server.CosmosDb/CommunityToolkit.Datasync.Server.CosmosDb.csproj
new file mode 100644
index 0000000..442f541
--- /dev/null
+++ b/src/CommunityToolkit.Datasync.Server.CosmosDb/CommunityToolkit.Datasync.Server.CosmosDb.csproj
@@ -0,0 +1,19 @@
+
+
+
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/CommunityToolkit.Datasync.Server.CosmosDb/CosmosSharedTableOptions.cs b/src/CommunityToolkit.Datasync.Server.CosmosDb/CosmosSharedTableOptions.cs
new file mode 100644
index 0000000..93ce806
--- /dev/null
+++ b/src/CommunityToolkit.Datasync.Server.CosmosDb/CosmosSharedTableOptions.cs
@@ -0,0 +1,58 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.Azure.Cosmos;
+using System.Linq.Expressions;
+
+namespace CommunityToolkit.Datasync.Server.CosmosDb;
+///
+/// Implementation of that supports storing multiple entity types in the same container.
+/// Defaults the partition key to the entity type name.
+///
+/// The that these options applys to.
+public class CosmosSharedTableOptions : CosmosTableOptions where TEntity : CosmosTableData
+{
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// The ID of the database that the container is in.
+ /// The ID of the container that the entities are stored in.
+ /// Should the timestamp be updated when an entity is updated by the repository (default is true).
+ public CosmosSharedTableOptions(string databaseId, string containerId, bool shouldUpdateTimestamp = true) : base(databaseId, containerId, shouldUpdateTimestamp)
+ {
+ Entity = typeof(TEntity).Name;
+ }
+ ///
+ /// The entity type for the data. Used as the default partition key for shared containers. defaults to the entity type name.
+ ///
+ public virtual string Entity { get; }
+ ///
+ /// Gets the partition key for the entity from the entity. Defaults to the entity type name.
+ ///
+ /// The to retrieve the partition key from.
+ /// A containing the type name.
+ /// The original id of the entity.
+
+ public override string GetPartitionKey(TEntity entity, out PartitionKey partitionKey)
+ {
+ partitionKey = new PartitionKey(Entity);
+ return entity.Id;
+ }
+ ///
+ /// Parses the partition key from the id. Defaults to the entity type name.
+ ///
+ /// The id of the entity
+ /// A containing the entity type name.
+ /// The id passed in the id parameter.
+ public override string ParsePartitionKey(string id, out PartitionKey partitionKey)
+ {
+ partitionKey = new PartitionKey(Entity);
+ return id;
+ }
+ ///
+ /// Returns a predicate that will filters the data by the entity name.
+ ///
+ ///
+ public override Expression> QueryablePredicate() => (e) => e.Entity == Entity;
+}
diff --git a/src/CommunityToolkit.Datasync.Server.CosmosDb/CosmosSingleTableOptions.cs b/src/CommunityToolkit.Datasync.Server.CosmosDb/CosmosSingleTableOptions.cs
new file mode 100644
index 0000000..6d503ec
--- /dev/null
+++ b/src/CommunityToolkit.Datasync.Server.CosmosDb/CosmosSingleTableOptions.cs
@@ -0,0 +1,54 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.Azure.Cosmos;
+using System.Linq.Expressions;
+
+namespace CommunityToolkit.Datasync.Server.CosmosDb;
+///
+/// Implementation of for a single table. Defaults the partition key to the entity's ID.
+///
+/// The that these options apply to.
+public class CosmosSingleTableOptions : CosmosTableOptions where TEntity : CosmosTableData
+{
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// The ID of the database that the container is in.
+ /// The ID of the container that the entities are stored in.
+ /// Should the timestamp be updated when an entity is updated by the repository (default is true).
+ public CosmosSingleTableOptions(string databaseId, string containerId, bool shouldUpdateTimestamp = true) : base(databaseId, containerId, shouldUpdateTimestamp)
+ {
+ }
+
+ ///
+ /// Gets a containing the entities Id.
+ ///
+ /// The to retrieve the partition key from.
+ /// A containing the entities Id.
+ /// The original id of the entity.
+
+ public override string GetPartitionKey(TEntity entity, out PartitionKey partitionKey)
+ {
+ partitionKey = new PartitionKey(entity.Id);
+
+ return entity.Id;
+ }
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public override string ParsePartitionKey(string id, out PartitionKey partitionKey)
+ {
+ partitionKey = new PartitionKey(id);
+ return id;
+ }
+ ///
+ ///
+ ///
+ ///
+ public override Expression> QueryablePredicate() => (_) => true;
+}
diff --git a/src/CommunityToolkit.Datasync.Server.CosmosDb/CosmosTableData.cs b/src/CommunityToolkit.Datasync.Server.CosmosDb/CosmosTableData.cs
new file mode 100644
index 0000000..781956b
--- /dev/null
+++ b/src/CommunityToolkit.Datasync.Server.CosmosDb/CosmosTableData.cs
@@ -0,0 +1,42 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using CommunityToolkit.Datasync.Server;
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Text;
+using System.Text.Json.Serialization;
+
+namespace CommunityToolkit.Datasync.Server.CosmosDb;
+///
+/// Base implementation of for entities that are stored in a CosmosDB collection.
+///
+public abstract class CosmosTableData : ITableData
+{
+ ///
+ public virtual string Id { get; set; } = string.Empty;
+ ///
+ public virtual bool Deleted { get; set; } = false;
+ ///
+ public virtual DateTimeOffset? UpdatedAt { get; set; } = DateTimeOffset.UnixEpoch;
+ ///
+ public virtual byte[] Version
+ {
+ get => Encoding.UTF8.GetBytes(ETag);
+ set => ETag = Encoding.UTF8.GetString(value);
+ }
+ ///
+ /// The ETag value for the entity.
+ ///
+ [JsonPropertyName("_etag")]
+ public string ETag { get; set; } = string.Empty;
+
+ ///
+ /// Equality comparison for instances.
+ ///
+
+ public bool Equals(ITableData? other)
+ {
+ return other != null && Id == other.Id && Version.SequenceEqual(other.Version);
+ }
+}
diff --git a/src/CommunityToolkit.Datasync.Server.CosmosDb/CosmosTableDataOfT.cs b/src/CommunityToolkit.Datasync.Server.CosmosDb/CosmosTableDataOfT.cs
new file mode 100644
index 0000000..3ed18f7
--- /dev/null
+++ b/src/CommunityToolkit.Datasync.Server.CosmosDb/CosmosTableDataOfT.cs
@@ -0,0 +1,16 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace CommunityToolkit.Datasync.Server.CosmosDb;
+///
+/// Base implementation of for entities that are stored in a shared CosmosDB collection.
+///
+/// The type of the Table Data
+public abstract class CosmosTableData : CosmosTableData where T : CosmosTableData
+{
+ ///
+ /// The entity type for the data. Used as the default partition key for shared containers.
+ ///
+ public virtual string Entity => typeof(T).Name;
+}
diff --git a/src/CommunityToolkit.Datasync.Server.CosmosDb/CosmosTableOptions.cs b/src/CommunityToolkit.Datasync.Server.CosmosDb/CosmosTableOptions.cs
new file mode 100644
index 0000000..a032417
--- /dev/null
+++ b/src/CommunityToolkit.Datasync.Server.CosmosDb/CosmosTableOptions.cs
@@ -0,0 +1,73 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.Azure.Cosmos;
+using System.Linq.Expressions;
+
+namespace CommunityToolkit.Datasync.Server.CosmosDb;
+
+///
+/// Base implementation of
+///
+/// The that these options apply to.
+public abstract class CosmosTableOptions : ICosmosTableOptions where TEntity : CosmosTableData
+{
+ ///
+ public virtual string DatabaseId { get; }
+ ///
+ public virtual string ContainerId { get; }
+ ///
+ public virtual bool ShouldUpdateTimestamp { get; }
+
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// The ID of the database that the container is in.
+ /// The ID of the container that the entities are stored in.
+ /// Should the timestamp be updated when an entity is updated by the repository (default is true).
+ /// Thrown when or are null or whitespace."
+ public CosmosTableOptions(
+ string databaseId,
+ string containerId,
+ bool shouldUpdateTimestamp = true)
+ {
+ if (string.IsNullOrWhiteSpace(databaseId))
+ {
+ throw new ArgumentException($"'{nameof(databaseId)}' cannot be null or whitespace.", nameof(databaseId));
+ }
+
+ if (string.IsNullOrWhiteSpace(containerId))
+ {
+ throw new ArgumentException($"'{nameof(containerId)}' cannot be null or whitespace.", nameof(containerId));
+ }
+
+ DatabaseId = databaseId;
+ ContainerId = containerId;
+ ShouldUpdateTimestamp = shouldUpdateTimestamp;
+ }
+ ///
+ public virtual Func IdGenerator => (_) => Guid.NewGuid().ToString();
+ ///
+ public virtual bool TryParsePartitionKey(string entityId, out string id, out PartitionKey partitionKey)
+ {
+ try
+ {
+ id = ParsePartitionKey(entityId, out PartitionKey pk);
+ partitionKey = pk;
+ return true;
+ }
+ catch
+ {
+ id = string.Empty;
+ partitionKey = default;
+ return false;
+ }
+ }
+ ///
+ public abstract string ParsePartitionKey(string id, out PartitionKey partitionKey);
+ ///
+ public abstract string GetPartitionKey(TEntity entity, out PartitionKey partitionKey);
+ ///
+ public abstract Expression> QueryablePredicate();
+}
\ No newline at end of file
diff --git a/src/CommunityToolkit.Datasync.Server.CosmosDb/CosmosTableRepository.cs b/src/CommunityToolkit.Datasync.Server.CosmosDb/CosmosTableRepository.cs
new file mode 100644
index 0000000..41b39fb
--- /dev/null
+++ b/src/CommunityToolkit.Datasync.Server.CosmosDb/CosmosTableRepository.cs
@@ -0,0 +1,214 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using CommunityToolkit.Datasync.Server.CosmosDb.Extensions;
+using Microsoft.Azure.Cosmos;
+using Microsoft.Azure.Cosmos.Linq;
+using System.Net;
+using System.Text;
+
+namespace CommunityToolkit.Datasync.Server.CosmosDb;
+
+///
+/// An implementation of the interface that
+/// stores data via an Cosmos DB Container.
+///
+/// The type of entity to store in the database.
+public class CosmosTableRepository : IRepository where TEntity : CosmosTableData
+{
+ ///
+ /// The for the entity set.
+ ///
+ protected ICosmosTableOptions Options { get; }
+ ///
+ /// The used for saving changes to the entity set.
+ ///
+ protected Container Container { get; }
+
+ ///
+ public async ValueTask CountAsync(IQueryable queryable, CancellationToken cancellationToken = default)
+ => await queryable.CountAsync(cancellationToken);
+ ///
+ public async ValueTask> ToListAsync(IQueryable queryable, CancellationToken cancellationToken = default)
+ => await queryable.ToListAsync(cancellationToken);
+
+ ///
+ /// Creates a new instance of the class, using the provided
+ /// to store the entities."
+ ///
+ /// The to access the container for this repository.
+ /// The for this repository.
+ /// Thrown if the or is null.
+ public CosmosTableRepository(CosmosClient client, ICosmosTableOptions options)
+ {
+ if (client is null)
+ {
+ throw new ArgumentNullException(nameof(client));
+ }
+
+ Options = options ?? throw new ArgumentNullException(nameof(options));
+
+ Container = client.GetContainer(options.DatabaseId, options.ContainerId);
+
+ }
+
+ ///
+ /// Retrieves an untracked version of an entity from the database.
+ ///
+ /// The ID of the entity to retrieve.
+ /// The partition key for the entity.
+ /// A to observe.
+ /// A task that returns an untracked version of the entity when complete.
+ /// Thrown if an error in the backend occurs.
+ protected async Task GetEntityAsync(string entityId, PartitionKey partitionKey, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ return await Container.ReadItemAsync(entityId, partitionKey, cancellationToken: cancellationToken);
+ }
+ catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
+ {
+ return null;
+ }
+ }
+
+ ///
+ /// Updates the managed properties for this entity if required.
+ ///
+ /// The entity to be updated.
+ internal void UpdateManagedProperties(TEntity entity)
+ {
+ if (Options.ShouldUpdateTimestamp)
+ {
+ entity.UpdatedAt = DateTimeOffset.UtcNow;
+ }
+ }
+
+ ///
+ /// Runs the inner part of an operation on the database, catching all the normal exceptions and reformatting them
+ /// as appropriate.
+ ///
+ /// The ID of the entity being operated on.
+ /// The partition key for the entity.
+ /// The operation to execute.
+ /// A to observe.
+ /// A task that completes when the operation is finished.
+ /// Thrown if a concurrency exception occurs.
+ /// Thrown if an error in the backend occurs.
+ internal async Task WrapExceptionAsync(string id, PartitionKey partitionKey, Func action, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ await action.Invoke().ConfigureAwait(false);
+ }
+ catch (CosmosException ex)
+ {
+ throw new HttpException((int)ex.StatusCode, ex.Message, ex) { Payload = await GetEntityAsync(id, partitionKey, cancellationToken).ConfigureAwait(false) };
+ }
+ }
+
+ #region IRepository implementation
+ ///
+ public virtual ValueTask> AsQueryableAsync(CancellationToken cancellationToken = default)
+ => ValueTask.FromResult(Container.GetItemLinqQueryable().Where(Options.QueryablePredicate()).AsQueryable());
+
+ ///
+ public virtual async ValueTask CreateAsync(TEntity entity, CancellationToken cancellationToken = default)
+ {
+ if (string.IsNullOrEmpty(entity.Id))
+ {
+ entity.Id = Options.IdGenerator.Invoke(entity);
+ }
+
+ string id = Options.GetPartitionKey(entity, out PartitionKey partitionKey);
+
+ await WrapExceptionAsync(id, partitionKey, async () =>
+ {
+ UpdateManagedProperties(entity);
+
+ ItemResponse response = await Container.CreateItemAsync(entity, partitionKey, cancellationToken: cancellationToken);
+
+ entity.ETag = response.Resource.ETag;
+ entity.UpdatedAt = response.Resource.UpdatedAt;
+
+ }, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ public virtual async ValueTask DeleteAsync(string id, byte[]? version = null, CancellationToken cancellationToken = default)
+ {
+ if (string.IsNullOrEmpty(id))
+ {
+ throw new HttpException((int)HttpStatusCode.BadRequest, "ID is required");
+ }
+
+ if (Options.TryParsePartitionKey(id, out string entityId, out PartitionKey partitionKey) == false)
+ {
+ throw new HttpException((int)HttpStatusCode.BadRequest, "ID is not in the correct format");
+ }
+
+ await WrapExceptionAsync(id, partitionKey, async () =>
+ {
+ ItemRequestOptions? requestOptions = null;
+
+ if (version?.Length > 0)
+ {
+ requestOptions = new ItemRequestOptions()
+ {
+ IfMatchEtag = Encoding.UTF8.GetString(version)
+ };
+ }
+
+ _ = await Container.DeleteItemAsync(entityId, partitionKey, requestOptions, cancellationToken);
+
+ }, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ public virtual async ValueTask ReadAsync(string id, CancellationToken cancellationToken = default)
+ {
+ if (string.IsNullOrEmpty(id))
+ {
+ throw new HttpException((int)HttpStatusCode.BadRequest, "ID is required");
+ }
+
+ if(Options.TryParsePartitionKey(id, out string entityId, out PartitionKey partitionKey) == false)
+ {
+ throw new HttpException((int)HttpStatusCode.BadRequest, "ID is not in the correct format");
+ }
+
+ return await GetEntityAsync(entityId, partitionKey, cancellationToken).ConfigureAwait(false) ??
+ throw new HttpException((int)HttpStatusCode.NotFound, $"Entity with id {id} not found");
+ }
+
+ ///
+ public virtual async ValueTask ReplaceAsync(TEntity entity, byte[]? version = null, CancellationToken cancellationToken = default)
+ {
+ if (string.IsNullOrEmpty(entity.Id))
+ {
+ throw new HttpException((int)HttpStatusCode.BadRequest, "ID is required");
+ }
+
+ string id = Options.GetPartitionKey(entity, out PartitionKey partitionKey);
+
+ await WrapExceptionAsync(id, partitionKey, async () =>
+ {
+ UpdateManagedProperties(entity);
+
+ ItemRequestOptions? requestOptions = null;
+
+ if (version?.Length > 0)
+ {
+ requestOptions = new ItemRequestOptions()
+ {
+ IfMatchEtag = Encoding.UTF8.GetString(version)
+ };
+ }
+
+ _ = await Container.ReplaceItemAsync(entity, id, partitionKey, requestOptions, cancellationToken);
+
+ }, cancellationToken).ConfigureAwait(false);
+ }
+ #endregion
+}
diff --git a/src/CommunityToolkit.Datasync.Server.CosmosDb/Extensions/IQueryableExtensions.cs b/src/CommunityToolkit.Datasync.Server.CosmosDb/Extensions/IQueryableExtensions.cs
new file mode 100644
index 0000000..2393a11
--- /dev/null
+++ b/src/CommunityToolkit.Datasync.Server.CosmosDb/Extensions/IQueryableExtensions.cs
@@ -0,0 +1,38 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.Azure.Cosmos;
+using Microsoft.Azure.Cosmos.Linq;
+
+namespace CommunityToolkit.Datasync.Server.CosmosDb.Extensions;
+
+///
+/// CosmosDB specific extensions for IQueryable
+///
+public static class IQueryableExtensions
+{
+ ///
+ /// Converts an IQueryable to a using a
+ ///
+ /// The queryable to convert
+ /// The cancellation token to use
+ /// The type of entity in the queryable
+ public static async Task> ToListAsync(this IQueryable queryable, CancellationToken cancellationToken = default)
+ where TEntity : class
+ {
+ List list = [];
+ using (FeedIterator iterator = queryable.ToFeedIterator())
+ {
+
+ while (iterator.HasMoreResults)
+ {
+ FeedResponse response = await iterator.ReadNextAsync(cancellationToken).ConfigureAwait(false);
+
+ list.AddRange(response);
+ }
+ }
+
+ return list;
+ }
+}
diff --git a/src/CommunityToolkit.Datasync.Server.CosmosDb/ICosmosTableOptions.cs b/src/CommunityToolkit.Datasync.Server.CosmosDb/ICosmosTableOptions.cs
new file mode 100644
index 0000000..e4de5a3
--- /dev/null
+++ b/src/CommunityToolkit.Datasync.Server.CosmosDb/ICosmosTableOptions.cs
@@ -0,0 +1,58 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.Azure.Cosmos;
+using System.Linq.Expressions;
+
+namespace CommunityToolkit.Datasync.Server.CosmosDb;
+///
+///
+///
+///
+public interface ICosmosTableOptions where TEntity : CosmosTableData
+{
+ ///
+ /// The name of the cosmsos database the entities are stored in
+ ///
+ string DatabaseId { get; }
+ ///
+ /// The name of the cosmos container the entities are stored in
+ ///
+ string ContainerId { get; }
+ ///
+ /// Should the timestamp be updated when an entity is updated by the repository (default is true).
+ /// Should be false if there are triggers to update the timestamp on the container.
+ ///
+ bool ShouldUpdateTimestamp { get; }
+ ///
+ /// Function to attempt to parse the partition key from the entity
+ ///
+ /// The entity passed from the controller to the repository ID
+ /// The entity ID after the entity has been parsed
+ /// The partition key after the entity has been parsed
+ bool TryParsePartitionKey(string entityId, out string id, out PartitionKey partitionKey);
+ ///
+ /// Function to parse the partition key from the entity
+ ///
+ /// The entity id passed from the controller to the repository
+ /// The partition key after the entity has been parsed
+ /// The entity ID after the entity has been parsed
+ string ParsePartitionKey(string id, out PartitionKey partitionKey);
+ ///
+ /// Get the id and partition key for the entity from the entity
+ ///
+ /// The entity passed from the controller to the repository
+ /// The partition key after the entity has been parsed
+ /// The entity ID after the entity has been parsed
+ string GetPartitionKey(TEntity entity, out PartitionKey partitionKey);
+ ///
+ /// Function to generate a new ID for the entity
+ ///
+ /// The function to generate a new ID for the entity
+ Func IdGenerator { get; }
+ ///
+ /// Function to filter the dataset for the entity required for shared containers
+ ///
+ Expression> QueryablePredicate();
+}
diff --git a/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/CommunityToolkit.Datasync.Server.CosmosDb.Test.csproj b/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/CommunityToolkit.Datasync.Server.CosmosDb.Test.csproj
new file mode 100644
index 0000000..03b77ad
--- /dev/null
+++ b/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/CommunityToolkit.Datasync.Server.CosmosDb.Test.csproj
@@ -0,0 +1,23 @@
+
+
+
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/CosmosDbRepository_Tests.cs b/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/CosmosDbRepository_Tests.cs
new file mode 100644
index 0000000..74d0c6f
--- /dev/null
+++ b/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/CosmosDbRepository_Tests.cs
@@ -0,0 +1,156 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using CommunityToolkit.Datasync.Server.Abstractions.Json;
+using CommunityToolkit.Datasync.Server.CosmosDb.Test.Models;
+using CommunityToolkit.Datasync.TestCommon;
+using CommunityToolkit.Datasync.TestCommon.Databases;
+using Microsoft.Azure.Cosmos;
+using Microsoft.Azure.Cosmos.Linq;
+using System.Collections.ObjectModel;
+using System.Net;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace CommunityToolkit.Datasync.Server.CosmosDb.Test;
+
+[ExcludeFromCodeCoverage]
+public class CosmosDbRepository_Tests : RepositoryTests, IDisposable, IAsyncLifetime
+{
+ #region Setup
+ private readonly Random random = new();
+ private readonly string connectionString = Environment.GetEnvironmentVariable("DATASYNC_COSMOS_CONNECTIONSTRING");
+ private readonly List movies = [];
+
+ private CosmosClient _client;
+ private Container _container;
+ private CosmosTableRepository _repository;
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ this._client?.Dispose();
+ }
+ }
+
+ override protected bool CanRunLiveTests() => !string.IsNullOrEmpty(this.connectionString);
+ protected override async Task GetEntityAsync(string id)
+ {
+ try
+ {
+ return await this._container.ReadItemAsync(id, new PartitionKey(id));
+ }
+ catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
+ {
+ return null;
+ }
+ }
+
+ protected override async Task GetEntityCountAsync()
+ {
+ return await this._container.GetItemLinqQueryable().CountAsync();
+ }
+
+ protected override Task> GetPopulatedRepositoryAsync()
+ {
+ return Task.FromResult>(this._repository);
+ }
+
+ protected override Task GetRandomEntityIdAsync(bool exists)
+ {
+ return Task.FromResult(exists ? this.movies[this.random.Next(this.movies.Count)].Id : Guid.NewGuid().ToString());
+ }
+
+ public async Task InitializeAsync()
+ {
+ if (!string.IsNullOrEmpty(this.connectionString))
+ {
+ this._client = new CosmosClient(
+ this.connectionString,
+ new CosmosClientOptions()
+ {
+ UseSystemTextJsonSerializerWithOptions = new(JsonSerializerDefaults.Web)
+ {
+ AllowTrailingCommas = true,
+ Converters =
+ {
+ new JsonStringEnumConverter(),
+ new DateTimeOffsetConverter(),
+ new DateTimeConverter(),
+ new TimeOnlyConverter(),
+ new SpatialGeoJsonConverter()
+ },
+ DefaultIgnoreCondition = JsonIgnoreCondition.Never,
+ DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
+ IgnoreReadOnlyFields = true,
+ IgnoreReadOnlyProperties = true,
+ IncludeFields = true,
+ NumberHandling = JsonNumberHandling.AllowReadingFromString,
+ PropertyNameCaseInsensitive = true,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ ReadCommentHandling = JsonCommentHandling.Skip
+ }
+ });
+
+ Database database = await this._client.CreateDatabaseIfNotExistsAsync("Movies");
+
+ this._container = await database.CreateContainerIfNotExistsAsync(new ContainerProperties("Movies", "/id")
+ {
+ IndexingPolicy = new IndexingPolicy()
+ {
+ CompositeIndexes =
+ {
+ new Collection()
+ {
+ new() { Path = "/updatedAt", Order = CompositePathSortOrder.Ascending },
+ new() { Path = "/id", Order = CompositePathSortOrder.Ascending }
+ },
+ new Collection()
+ {
+ new() { Path = "/releaseDate", Order = CompositePathSortOrder.Ascending },
+ new() { Path = "/id", Order = CompositePathSortOrder.Ascending }
+ }
+ }
+ }
+ });
+
+ foreach (CosmosDbMovie movie in TestCommon.TestData.Movies.OfType())
+ {
+ movie.UpdatedAt = DateTimeOffset.UtcNow;
+ movie.Version = Guid.NewGuid().ToByteArray();
+ _ = await this._container.CreateItemAsync(movie, new PartitionKey(movie.Id));
+ this.movies.Add(movie);
+ }
+
+ this._repository = new CosmosTableRepository(
+ this._client,
+ new CosmosSingleTableOptions("Movies", "Movies")
+ );
+
+ }
+ }
+
+ public async Task DisposeAsync()
+ {
+ if (this._client != null)
+ {
+ try
+ {
+ await this._client.GetDatabase("Movies").DeleteAsync();
+ }
+ catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
+ {
+ // Ignore
+ }
+ }
+ }
+ #endregion
+}
diff --git a/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/Models/CosmosDbMovie.cs b/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/Models/CosmosDbMovie.cs
new file mode 100644
index 0000000..76d7d06
--- /dev/null
+++ b/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/Models/CosmosDbMovie.cs
@@ -0,0 +1,26 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using CommunityToolkit.Datasync.TestCommon.Models;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace CommunityToolkit.Datasync.Server.CosmosDb.Test.Models;
+public class CosmosDbMovie : CosmosTableData, IMovie
+{
+ public bool BestPictureWinner { get; set; }
+
+ public int Duration { get; set; }
+
+ public MovieRating Rating { get; set; }
+
+ public DateOnly ReleaseDate { get; set; }
+
+ public string Title { get; set; }
+
+ public int Year { get; set; }
+}