Skip to content

Commit 7fc31f3

Browse files
Add JsonApiDotNetCoreExample.Cosmos
This commit adds an example for Cosmos DB, using the JsonApiDotNetCoreExample as the starting point and making changes required for Cosmos and to showcase Cosmos features (e.g., embedded / owned entities).
1 parent 7396735 commit 7fc31f3

16 files changed

+645
-12
lines changed

JsonApiDotNetCore.sln

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MultiDbContextTests", "test
4444
EndProject
4545
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestBuildingBlocks", "test\TestBuildingBlocks\TestBuildingBlocks.csproj", "{210FD61E-FF5D-4CEE-8E0D-C739ECCCBA21}"
4646
EndProject
47+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JsonApiDotNetCoreExample.Cosmos", "src\Examples\JsonApiDotNetCoreExample.Cosmos\JsonApiDotNetCoreExample.Cosmos.csproj", "{74819978-5C4C-460F-B5DE-31D6B6B1289C}"
48+
EndProject
4749
Global
4850
GlobalSection(SolutionConfigurationPlatforms) = preSolution
4951
Debug|Any CPU = Debug|Any CPU
@@ -54,6 +56,18 @@ Global
5456
Release|x86 = Release|x86
5557
EndGlobalSection
5658
GlobalSection(ProjectConfigurationPlatforms) = postSolution
59+
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
60+
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Debug|Any CPU.Build.0 = Debug|Any CPU
61+
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Debug|x64.ActiveCfg = Debug|Any CPU
62+
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Debug|x64.Build.0 = Debug|Any CPU
63+
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Debug|x86.ActiveCfg = Debug|Any CPU
64+
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Debug|x86.Build.0 = Debug|Any CPU
65+
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Release|Any CPU.ActiveCfg = Release|Any CPU
66+
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Release|Any CPU.Build.0 = Release|Any CPU
67+
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Release|x64.ActiveCfg = Release|Any CPU
68+
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Release|x64.Build.0 = Release|Any CPU
69+
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Release|x86.ActiveCfg = Release|Any CPU
70+
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Release|x86.Build.0 = Release|Any CPU
5771
{CAF331F8-9255-4D72-A1A8-A54141E99F1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
5872
{CAF331F8-9255-4D72-A1A8-A54141E99F1E}.Debug|Any CPU.Build.0 = Debug|Any CPU
5973
{CAF331F8-9255-4D72-A1A8-A54141E99F1E}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -162,18 +176,6 @@ Global
162176
{21D27239-138D-4604-8E49-DCBE41BCE4C8}.Release|x64.Build.0 = Release|Any CPU
163177
{21D27239-138D-4604-8E49-DCBE41BCE4C8}.Release|x86.ActiveCfg = Release|Any CPU
164178
{21D27239-138D-4604-8E49-DCBE41BCE4C8}.Release|x86.Build.0 = Release|Any CPU
165-
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
166-
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Debug|Any CPU.Build.0 = Debug|Any CPU
167-
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Debug|x64.ActiveCfg = Debug|Any CPU
168-
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Debug|x64.Build.0 = Debug|Any CPU
169-
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Debug|x86.ActiveCfg = Debug|Any CPU
170-
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Debug|x86.Build.0 = Debug|Any CPU
171-
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Release|Any CPU.ActiveCfg = Release|Any CPU
172-
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Release|Any CPU.Build.0 = Release|Any CPU
173-
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Release|x64.ActiveCfg = Release|Any CPU
174-
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Release|x64.Build.0 = Release|Any CPU
175-
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Release|x86.ActiveCfg = Release|Any CPU
176-
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Release|x86.Build.0 = Release|Any CPU
177179
{6CAFDDBE-00AB-4784-801B-AB419C3C3A26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
178180
{6CAFDDBE-00AB-4784-801B-AB419C3C3A26}.Debug|Any CPU.Build.0 = Debug|Any CPU
179181
{6CAFDDBE-00AB-4784-801B-AB419C3C3A26}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -210,6 +212,18 @@ Global
210212
{210FD61E-FF5D-4CEE-8E0D-C739ECCCBA21}.Release|x64.Build.0 = Release|Any CPU
211213
{210FD61E-FF5D-4CEE-8E0D-C739ECCCBA21}.Release|x86.ActiveCfg = Release|Any CPU
212214
{210FD61E-FF5D-4CEE-8E0D-C739ECCCBA21}.Release|x86.Build.0 = Release|Any CPU
215+
{74819978-5C4C-460F-B5DE-31D6B6B1289C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
216+
{74819978-5C4C-460F-B5DE-31D6B6B1289C}.Debug|Any CPU.Build.0 = Debug|Any CPU
217+
{74819978-5C4C-460F-B5DE-31D6B6B1289C}.Debug|x64.ActiveCfg = Debug|Any CPU
218+
{74819978-5C4C-460F-B5DE-31D6B6B1289C}.Debug|x64.Build.0 = Debug|Any CPU
219+
{74819978-5C4C-460F-B5DE-31D6B6B1289C}.Debug|x86.ActiveCfg = Debug|Any CPU
220+
{74819978-5C4C-460F-B5DE-31D6B6B1289C}.Debug|x86.Build.0 = Debug|Any CPU
221+
{74819978-5C4C-460F-B5DE-31D6B6B1289C}.Release|Any CPU.ActiveCfg = Release|Any CPU
222+
{74819978-5C4C-460F-B5DE-31D6B6B1289C}.Release|Any CPU.Build.0 = Release|Any CPU
223+
{74819978-5C4C-460F-B5DE-31D6B6B1289C}.Release|x64.ActiveCfg = Release|Any CPU
224+
{74819978-5C4C-460F-B5DE-31D6B6B1289C}.Release|x64.Build.0 = Release|Any CPU
225+
{74819978-5C4C-460F-B5DE-31D6B6B1289C}.Release|x86.ActiveCfg = Release|Any CPU
226+
{74819978-5C4C-460F-B5DE-31D6B6B1289C}.Release|x86.Build.0 = Release|Any CPU
213227
EndGlobalSection
214228
GlobalSection(SolutionProperties) = preSolution
215229
HideSolutionNode = FALSE
@@ -228,6 +242,7 @@ Global
228242
{6CAFDDBE-00AB-4784-801B-AB419C3C3A26} = {026FBC6C-AF76-4568-9B87-EC73457899FD}
229243
{EC3202C6-1D4C-4B14-A599-B9D3F27FE3BA} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F}
230244
{210FD61E-FF5D-4CEE-8E0D-C739ECCCBA21} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F}
245+
{74819978-5C4C-460F-B5DE-31D6B6B1289C} = {026FBC6C-AF76-4568-9B87-EC73457899FD}
231246
EndGlobalSection
232247
GlobalSection(ExtensibilityGlobals) = postSolution
233248
SolutionGuid = {A2421882-8F0A-4905-928F-B550B192F9A4}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
using System.IO;
2+
using System.Threading.Tasks;
3+
using Microsoft.AspNetCore.Mvc;
4+
5+
namespace JsonApiDotNetCoreExample.Cosmos.Controllers
6+
{
7+
[Route("[controller]")]
8+
public sealed class NonJsonApiController : ControllerBase
9+
{
10+
[HttpGet]
11+
public IActionResult Get()
12+
{
13+
string[] result =
14+
{
15+
"Welcome!"
16+
};
17+
18+
return Ok(result);
19+
}
20+
21+
[HttpPost]
22+
public async Task<IActionResult> PostAsync()
23+
{
24+
string name = await new StreamReader(Request.Body).ReadToEndAsync();
25+
26+
if (string.IsNullOrEmpty(name))
27+
{
28+
return BadRequest("Please send your name.");
29+
}
30+
31+
string result = $"Hello, {name}";
32+
return Ok(result);
33+
}
34+
35+
[HttpPut]
36+
public IActionResult Put([FromBody] string name)
37+
{
38+
string result = $"Hi, {name}";
39+
return Ok(result);
40+
}
41+
42+
[HttpPatch]
43+
public IActionResult Patch(string name)
44+
{
45+
string result = $"Good day, {name}";
46+
return Ok(result);
47+
}
48+
49+
[HttpDelete]
50+
public IActionResult Delete()
51+
{
52+
return Ok("Bye.");
53+
}
54+
}
55+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using JsonApiDotNetCore.AtomicOperations;
2+
using JsonApiDotNetCore.Configuration;
3+
using JsonApiDotNetCore.Controllers;
4+
using JsonApiDotNetCore.Middleware;
5+
using JsonApiDotNetCore.Resources;
6+
using Microsoft.Extensions.Logging;
7+
8+
namespace JsonApiDotNetCoreExample.Cosmos.Controllers
9+
{
10+
public sealed class OperationsController : JsonApiOperationsController
11+
{
12+
public OperationsController(IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory, IOperationsProcessor processor,
13+
IJsonApiRequest request, ITargetedFields targetedFields)
14+
: base(options, resourceGraph, loggerFactory, processor, request, targetedFields)
15+
{
16+
}
17+
}
18+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using System;
2+
using JsonApiDotNetCore.Configuration;
3+
using JsonApiDotNetCore.Controllers;
4+
using JsonApiDotNetCore.Services;
5+
using JsonApiDotNetCoreExample.Cosmos.Models;
6+
using Microsoft.Extensions.Logging;
7+
8+
namespace JsonApiDotNetCoreExample.Cosmos.Controllers
9+
{
10+
public sealed class PeopleController : JsonApiController<Person, Guid>
11+
{
12+
public PeopleController(IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory,
13+
IResourceService<Person, Guid> resourceService)
14+
: base(options, resourceGraph, loggerFactory, resourceService)
15+
{
16+
}
17+
}
18+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using System;
2+
using JsonApiDotNetCore.Configuration;
3+
using JsonApiDotNetCore.Controllers;
4+
using JsonApiDotNetCore.Services;
5+
using JsonApiDotNetCoreExample.Cosmos.Models;
6+
using Microsoft.Extensions.Logging;
7+
8+
namespace JsonApiDotNetCoreExample.Cosmos.Controllers
9+
{
10+
public sealed class TodoItemsController : JsonApiController<TodoItem, Guid>
11+
{
12+
public TodoItemsController(IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory,
13+
IResourceService<TodoItem, Guid> resourceService)
14+
: base(options, resourceGraph, loggerFactory, resourceService)
15+
{
16+
}
17+
}
18+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
using System;
2+
using JetBrains.Annotations;
3+
using JsonApiDotNetCoreExample.Cosmos.Models;
4+
using Microsoft.EntityFrameworkCore;
5+
6+
#pragma warning disable IDE0058 // Expression value is never used
7+
#pragma warning disable AV1706 // Identifier contains an abbreviation or is too short
8+
9+
// @formatter:wrap_chained_method_calls chop_always
10+
11+
namespace JsonApiDotNetCoreExample.Cosmos.Data
12+
{
13+
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
14+
public sealed class AppDbContext : DbContext
15+
{
16+
public DbSet<Person> People => Set<Person>();
17+
18+
public DbSet<TodoItem> TodoItems => Set<TodoItem>();
19+
20+
public AppDbContext(DbContextOptions<AppDbContext> options)
21+
: base(options)
22+
{
23+
}
24+
25+
protected override void OnModelCreating(ModelBuilder builder)
26+
{
27+
builder.HasDefaultContainer("PeopleAndTodoItems");
28+
builder.UsePropertyAccessMode(PropertyAccessMode.Property);
29+
30+
builder.Entity<Person>(entity =>
31+
{
32+
entity.HasKey(e => e.Id);
33+
34+
entity.Property(e => e.Id)
35+
.ValueGeneratedOnAdd();
36+
37+
entity.HasPartitionKey(e => e.PartitionKey);
38+
});
39+
40+
builder.Entity<TodoItem>(entity =>
41+
{
42+
entity.HasKey(e => e.Id);
43+
44+
entity.Property(e => e.Id)
45+
.ValueGeneratedOnAdd();
46+
47+
entity.HasPartitionKey(e => e.PartitionKey);
48+
49+
// @formatter:off
50+
51+
entity.Property(e => e.Priority)
52+
.HasConversion(
53+
value => value.ToString(),
54+
value => (TodoItemPriority)Enum.Parse(typeof(TodoItemPriority), value));
55+
56+
// @formatter:on
57+
58+
entity.HasOne(todoItem => todoItem.Owner)
59+
.WithMany(person => person.OwnedTodoItems)
60+
.HasForeignKey(todoItem => todoItem.OwnerId)
61+
.IsRequired();
62+
63+
entity.HasOne(todoItem => todoItem.Assignee)
64+
.WithMany(person => person!.AssignedTodoItems)
65+
.HasForeignKey(todoItem => todoItem.AssigneeId)
66+
.IsRequired(false);
67+
68+
entity.OwnsMany(todoItem => todoItem.Tags);
69+
});
70+
}
71+
}
72+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
using System;
2+
using System.ComponentModel;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using JetBrains.Annotations;
6+
using JsonApiDotNetCore.Configuration;
7+
using JsonApiDotNetCore.Middleware;
8+
using JsonApiDotNetCore.Queries.Expressions;
9+
using JsonApiDotNetCore.Resources;
10+
using JsonApiDotNetCoreExample.Cosmos.Models;
11+
using Microsoft.AspNetCore.Authentication;
12+
13+
#pragma warning disable AV2310 // Code block should not contain inline comment
14+
15+
namespace JsonApiDotNetCoreExample.Cosmos.Definitions
16+
{
17+
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
18+
public sealed class TodoItemDefinition : JsonApiResourceDefinition<TodoItem, Guid>
19+
{
20+
private readonly ISystemClock _systemClock;
21+
22+
public TodoItemDefinition(IResourceGraph resourceGraph, ISystemClock systemClock)
23+
: base(resourceGraph)
24+
{
25+
_systemClock = systemClock;
26+
}
27+
28+
/// <inheritdoc />
29+
public override SortExpression OnApplySort(SortExpression? existingSort)
30+
{
31+
return existingSort ?? GetDefaultSortOrder();
32+
}
33+
34+
private SortExpression GetDefaultSortOrder()
35+
{
36+
// Cosmos DB, we would have to define a composite index in order to support a composite sort expression.
37+
// Therefore, we will only sort on a single property.
38+
return CreateSortExpressionFromLambda(new PropertySortOrder
39+
{
40+
(todoItem => todoItem.Priority, ListSortDirection.Descending)
41+
});
42+
}
43+
44+
/// <inheritdoc />
45+
public override Task OnWritingAsync(TodoItem resource, WriteOperationKind writeOperation, CancellationToken cancellationToken)
46+
{
47+
if (writeOperation == WriteOperationKind.CreateResource)
48+
{
49+
resource.CreatedAt = _systemClock.UtcNow;
50+
}
51+
else if (writeOperation == WriteOperationKind.UpdateResource)
52+
{
53+
resource.LastModifiedAt = _systemClock.UtcNow;
54+
}
55+
56+
return Task.CompletedTask;
57+
}
58+
}
59+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
<PropertyGroup>
3+
<TargetFramework>$(NetCoreAppVersion)</TargetFramework>
4+
</PropertyGroup>
5+
6+
<ItemGroup>
7+
<ProjectReference Include="..\..\JsonApiDotNetCore\JsonApiDotNetCore.csproj" />
8+
</ItemGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.21.0" />
12+
<PackageReference Include="Microsoft.EntityFrameworkCore.Cosmos" Version="$(EFCoreVersion)" />
13+
</ItemGroup>
14+
</Project>
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using JetBrains.Annotations;
4+
using JsonApiDotNetCore.Resources;
5+
using JsonApiDotNetCore.Resources.Annotations;
6+
7+
namespace JsonApiDotNetCoreExample.Cosmos.Models
8+
{
9+
[NoSqlResource]
10+
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
11+
public sealed class Person : Identifiable<Guid>
12+
{
13+
/// <summary>
14+
/// Gets or sets the partition key.
15+
/// </summary>
16+
/// <remarks>
17+
/// In this example, we are using a generic name for the partition key. We could have used any other sensible name such as PersonId, for example. In any
18+
/// case, the property must exist in all classes the instances of which are to be stored in the same container. In our example project, both
19+
/// <see cref="Person" /> and <see cref="TodoItem" /> instances are stored in that container. A <see cref="Person" /> instance and all
20+
/// <see cref="TodoItem" /> instances owned by that person will thus be stored in the same logical partition.
21+
/// </remarks>
22+
[Attr(Capabilities = AttrCapabilities.AllowFilter | AttrCapabilities.AllowSort)]
23+
public string PartitionKey
24+
{
25+
get => Id.ToString();
26+
set => Id = Guid.Parse(value);
27+
}
28+
29+
/// <summary>
30+
/// Gets or sets the optional first name.
31+
/// </summary>
32+
[Attr]
33+
public string? FirstName { get; set; }
34+
35+
/// <summary>
36+
/// Gets or sets the required last name.
37+
/// </summary>
38+
[Attr]
39+
public string LastName { get; set; } = null!;
40+
41+
/// <summary>
42+
/// Gets or sets the set of <see cref="TodoItem" /> instances that are owned by this <see cref="Person" />.
43+
/// </summary>
44+
/// <remarks>
45+
/// To enable the navigation of relationships, the name of the foreign key property must be specified for the navigation properties.
46+
/// </remarks>
47+
[HasMany]
48+
[NoSqlHasForeignKey(nameof(TodoItem.OwnerId))]
49+
public ISet<TodoItem> OwnedTodoItems { get; set; } = new HashSet<TodoItem>();
50+
51+
/// <summary>
52+
/// Gets or sets the set of <see cref="TodoItem" /> instances that are assigned to this <see cref="Person" />.
53+
/// </summary>
54+
/// <remarks>
55+
/// To enable the navigation of relationships, the name of the foreign key property must be specified for the navigation properties.
56+
/// </remarks>
57+
[HasMany]
58+
[NoSqlHasForeignKey(nameof(TodoItem.AssigneeId))]
59+
public ISet<TodoItem> AssignedTodoItems { get; set; } = new HashSet<TodoItem>();
60+
}
61+
}

0 commit comments

Comments
 (0)