Skip to content

Commit 9f8ae9d

Browse files
author
Bart Koelman
authored
Built-in support for free-format client-generated IDs (#14)
* Rename MongoIdentifiable to HexStringMongoIdentifiable, extract interface IMongoIdentifiable * Added FreeStringMongoIdentifiable, updated tests for client-generated IDs * Updated documentation
1 parent 674889e commit 9f8ae9d

36 files changed

+107
-61
lines changed

README.md

+12-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ dotnet add package JsonApiDotNetCore.MongoDb
1818
#nullable enable
1919

2020
[Resource]
21-
public class Book : MongoIdentifiable
21+
public class Book : HexStringMongoIdentifiable
2222
{
2323
[Attr]
2424
public string Name { get; set; } = null!;
@@ -70,9 +70,19 @@ builder.Services.AddJsonApiMongoDb();
7070
builder.Services.AddScoped(typeof(IResourceReadRepository<,>), typeof(MongoRepository<,>));
7171
builder.Services.AddScoped(typeof(IResourceWriteRepository<,>), typeof(MongoRepository<,>));
7272
builder.Services.AddScoped(typeof(IResourceRepository<,>), typeof(MongoRepository<,>));
73-
7473
```
7574

75+
## Using client-generated IDs
76+
Resources that inherit from `HexStringMongoIdentifiable` use auto-generated (performant) 12-byte hexadecimal
77+
[Object IDs](https://docs.mongodb.com/manual/reference/bson-types/#objectid).
78+
You can assign an ID manually, but it must match the 12-byte hexadecimal pattern.
79+
80+
To assign free-format string IDs manually, make your resources inherit from `FreeStringMongoIdentifiable` instead.
81+
When creating a resource without assigning an ID, a 12-byte hexadecimal ID will be auto-generated.
82+
83+
Set `options.AllowClientGeneratedIds` to `true` in Program.cs to allow API clients to assign IDs. This can be combined
84+
with both base classes, but `FreeStringMongoIdentifiable` probably makes the most sense.
85+
7686
## Limitations
7787

7888
- JSON:API relationships are currently not supported. You can use complex object graphs though, which are stored in a single document.

src/Examples/GettingStarted/Models/Book.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ namespace GettingStarted.Models;
66

77
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
88
[Resource]
9-
public sealed class Book : MongoIdentifiable
9+
public sealed class Book : HexStringMongoIdentifiable
1010
{
1111
[Attr]
1212
public string Title { get; set; } = null!;

src/Examples/JsonApiDotNetCoreMongoDbExample/Models/TodoItem.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace JsonApiDotNetCoreMongoDbExample.Models;
77

88
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
99
[Resource]
10-
public sealed class TodoItem : MongoIdentifiable
10+
public sealed class TodoItem : HexStringMongoIdentifiable
1111
{
1212
[Attr]
1313
public string Description { get; set; } = null!;

src/JsonApiDotNetCore.MongoDb/Queries/Internal/HideRelationshipsSparseFieldSetCache.cs

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Collections.Immutable;
22
using JsonApiDotNetCore.Configuration;
3+
using JsonApiDotNetCore.MongoDb.Resources;
34
using JsonApiDotNetCore.Queries;
45
using JsonApiDotNetCore.Queries.Internal;
56
using JsonApiDotNetCore.Resources;
@@ -38,6 +39,11 @@ public IImmutableSet<ResourceFieldAttribute> GetSparseFieldSetForSerializer(Reso
3839
{
3940
IImmutableSet<ResourceFieldAttribute> fieldSet = _innerCache.GetSparseFieldSetForSerializer(resourceType);
4041

42+
return resourceType.ClrType.IsAssignableTo(typeof(IMongoIdentifiable)) ? RemoveRelationships(fieldSet) : fieldSet;
43+
}
44+
45+
private static IImmutableSet<ResourceFieldAttribute> RemoveRelationships(IImmutableSet<ResourceFieldAttribute> fieldSet)
46+
{
4147
ResourceFieldAttribute[] relationships = fieldSet.Where(field => field is RelationshipAttribute).ToArray();
4248
return fieldSet.Except(relationships);
4349
}

src/JsonApiDotNetCore.MongoDb/Repositories/MongoRepository.cs

+3-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using JsonApiDotNetCore.Middleware;
66
using JsonApiDotNetCore.MongoDb.Errors;
77
using JsonApiDotNetCore.MongoDb.Queries.Internal.QueryableBuilding;
8+
using JsonApiDotNetCore.MongoDb.Resources;
89
using JsonApiDotNetCore.Queries;
910
using JsonApiDotNetCore.Queries.Expressions;
1011
using JsonApiDotNetCore.Queries.Internal.QueryableBuilding;
@@ -52,9 +53,9 @@ public MongoRepository(IMongoDataAccess mongoDataAccess, ITargetedFields targete
5253
_constraintProviders = constraintProviders;
5354
_resourceDefinitionAccessor = resourceDefinitionAccessor;
5455

55-
if (typeof(TId) != typeof(string))
56+
if (!typeof(TResource).IsAssignableTo(typeof(IMongoIdentifiable)))
5657
{
57-
throw new InvalidConfigurationException("MongoDB can only be used for resources with an 'Id' property of type 'string'.");
58+
throw new InvalidConfigurationException("MongoDB can only be used with resources that implement 'IMongoIdentifiable'.");
5859
}
5960
}
6061

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using MongoDB.Bson.Serialization.Attributes;
2+
using MongoDB.Bson.Serialization.IdGenerators;
3+
4+
namespace JsonApiDotNetCore.MongoDb.Resources;
5+
6+
/// <summary>
7+
/// Basic implementation of a JSON:API resource whose Id is stored as a free-format string in MongoDB. Useful for resources that are created using
8+
/// client-generated IDs.
9+
/// </summary>
10+
public abstract class FreeStringMongoIdentifiable : IMongoIdentifiable
11+
{
12+
/// <inheritdoc />
13+
[BsonId(IdGenerator = typeof(StringObjectIdGenerator))]
14+
public virtual string? Id { get; set; }
15+
16+
/// <inheritdoc />
17+
[BsonIgnore]
18+
public string? StringId
19+
{
20+
get => Id;
21+
set => Id = value;
22+
}
23+
24+
/// <inheritdoc />
25+
[BsonIgnore]
26+
public string? LocalId { get; set; }
27+
}

src/JsonApiDotNetCore.MongoDb/Resources/MongoIdentifiable.cs renamed to src/JsonApiDotNetCore.MongoDb/Resources/HexStringMongoIdentifiable.cs

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
1-
using JsonApiDotNetCore.Resources;
21
using MongoDB.Bson;
32
using MongoDB.Bson.Serialization.Attributes;
43

54
namespace JsonApiDotNetCore.MongoDb.Resources;
65

76
/// <summary>
8-
/// A convenient basic implementation of <see cref="IIdentifiable" /> for use with MongoDB models.
7+
/// Basic implementation of a JSON:API resource whose Id is stored as a 12-byte hexadecimal ObjectId in MongoDB.
98
/// </summary>
10-
public abstract class MongoIdentifiable : IIdentifiable<string?>
9+
public abstract class HexStringMongoIdentifiable : IMongoIdentifiable
1110
{
1211
/// <inheritdoc />
1312
[BsonId]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using JsonApiDotNetCore.Resources;
2+
3+
namespace JsonApiDotNetCore.MongoDb.Resources;
4+
5+
/// <summary>
6+
/// Marker interface to indicate a resource that is stored in MongoDB.
7+
/// </summary>
8+
public interface IMongoIdentifiable : IIdentifiable<string?>
9+
{
10+
}

test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs

+9-13
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
using JsonApiDotNetCore.Configuration;
44
using JsonApiDotNetCore.Serialization.Objects;
55
using Microsoft.Extensions.DependencyInjection;
6-
using MongoDB.Bson;
76
using TestBuildingBlocks;
87
using Xunit;
98

@@ -28,7 +27,7 @@ public async Task Can_create_resource_with_client_generated_string_ID_having_sid
2827
{
2928
// Arrange
3029
TextLanguage newLanguage = _fakers.TextLanguage.Generate();
31-
newLanguage.Id = ObjectId.GenerateNewId().ToString();
30+
newLanguage.Id = "free-format-client-generated-id";
3231

3332
var requestBody = new
3433
{
@@ -82,8 +81,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
8281
public async Task Can_create_resource_with_client_generated_string_ID_having_no_side_effects()
8382
{
8483
// Arrange
85-
MusicTrack newTrack = _fakers.MusicTrack.Generate();
86-
newTrack.Id = ObjectId.GenerateNewId().ToString();
84+
Playlist newPlaylist = _fakers.Playlist.Generate();
85+
newPlaylist.Id = "free-format-client-generated-id";
8786

8887
var requestBody = new
8988
{
@@ -94,13 +93,11 @@ public async Task Can_create_resource_with_client_generated_string_ID_having_no_
9493
op = "add",
9594
data = new
9695
{
97-
type = "musicTracks",
98-
id = newTrack.StringId,
96+
type = "playlists",
97+
id = newPlaylist.StringId,
9998
attributes = new
10099
{
101-
title = newTrack.Title,
102-
lengthInSeconds = newTrack.LengthInSeconds,
103-
releasedAt = newTrack.ReleasedAt
100+
name = newPlaylist.Name
104101
}
105102
}
106103
}
@@ -119,10 +116,9 @@ public async Task Can_create_resource_with_client_generated_string_ID_having_no_
119116

120117
await _testContext.RunOnDatabaseAsync(async dbContext =>
121118
{
122-
MusicTrack trackInDatabase = await dbContext.MusicTracks.FirstWithIdAsync(newTrack.Id);
119+
Playlist playlistInDatabase = await dbContext.Playlists.FirstWithIdAsync(newPlaylist.Id);
123120

124-
trackInDatabase.Title.Should().Be(newTrack.Title);
125-
trackInDatabase.LengthInSeconds.Should().BeApproximately(newTrack.LengthInSeconds);
121+
playlistInDatabase.Name.Should().Be(newPlaylist.Name);
126122
});
127123
}
128124

@@ -131,7 +127,7 @@ public async Task Cannot_create_resource_for_existing_client_generated_ID()
131127
{
132128
// Arrange
133129
TextLanguage existingLanguage = _fakers.TextLanguage.Generate();
134-
existingLanguage.Id = ObjectId.GenerateNewId().ToString();
130+
existingLanguage.Id = "existing-free-format-client-generated-id";
135131

136132
string newIsoCode = _fakers.TextLanguage.Generate().IsoCode!;
137133

test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Lyric.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations;
77

88
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
99
[Resource(ControllerNamespace = "JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations")]
10-
public sealed class Lyric : MongoIdentifiable
10+
public sealed class Lyric : HexStringMongoIdentifiable
1111
{
1212
[Attr]
1313
public string? Format { get; set; }

test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/MusicTrack.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations;
88

99
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
1010
[Resource(ControllerNamespace = "JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations")]
11-
public sealed class MusicTrack : MongoIdentifiable
11+
public sealed class MusicTrack : HexStringMongoIdentifiable
1212
{
1313
[RegularExpression(@"^[a-fA-F\d]{24}$")]
1414
public override string? Id { get; set; }

test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Performer.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations;
66

77
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
88
[Resource(ControllerNamespace = "JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations")]
9-
public sealed class Performer : MongoIdentifiable
9+
public sealed class Performer : HexStringMongoIdentifiable
1010
{
1111
[Attr]
1212
public string? ArtistName { get; set; }

test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Playlist.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations;
77

88
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
99
[Resource(ControllerNamespace = "JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations")]
10-
public sealed class Playlist : MongoIdentifiable
10+
public sealed class Playlist : FreeStringMongoIdentifiable
1111
{
1212
[Attr]
1313
public string Name { get; set; } = null!;

test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/RecordCompany.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations;
77

88
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
99
[Resource(ControllerNamespace = "JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations")]
10-
public sealed class RecordCompany : MongoIdentifiable
10+
public sealed class RecordCompany : HexStringMongoIdentifiable
1111
{
1212
[Attr]
1313
public string Name { get; set; } = null!;

test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/TextLanguage.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations;
77

88
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
99
[Resource(ControllerNamespace = "JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations")]
10-
public sealed class TextLanguage : MongoIdentifiable
10+
public sealed class TextLanguage : FreeStringMongoIdentifiable
1111
{
1212
[Attr]
1313
public string? IsoCode { get; set; }

test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/Meta/SupportTicket.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.Meta;
66

77
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
88
[Resource(ControllerNamespace = "JsonApiDotNetCoreMongoDbTests.IntegrationTests.Meta")]
9-
public sealed class SupportTicket : MongoIdentifiable
9+
public sealed class SupportTicket : HexStringMongoIdentifiable
1010
{
1111
[Attr]
1212
public string Description { get; set; } = null!;

test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/AccountPreferences.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings;
66

77
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
8-
public sealed class AccountPreferences : MongoIdentifiable
8+
public sealed class AccountPreferences : HexStringMongoIdentifiable
99
{
1010
[Attr]
1111
public bool UseDarkTheme { get; set; }

test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Blog.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings;
77

88
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
99
[Resource(ControllerNamespace = "JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings")]
10-
public sealed class Blog : MongoIdentifiable
10+
public sealed class Blog : HexStringMongoIdentifiable
1111
{
1212
[Attr]
1313
public string Title { get; set; } = null!;

test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/BlogPost.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings;
77

88
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
99
[Resource(ControllerNamespace = "JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings")]
10-
public sealed class BlogPost : MongoIdentifiable
10+
public sealed class BlogPost : HexStringMongoIdentifiable
1111
{
1212
[Attr]
1313
public string Caption { get; set; } = null!;

test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Comment.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings;
77

88
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
99
[Resource(ControllerNamespace = "JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings")]
10-
public sealed class Comment : MongoIdentifiable
10+
public sealed class Comment : HexStringMongoIdentifiable
1111
{
1212
[Attr]
1313
public string Text { get; set; } = null!;

test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Filtering/FilterableResource.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings.Filtering;
77

88
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
99
[Resource(ControllerNamespace = "JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings.Filtering")]
10-
public sealed class FilterableResource : MongoIdentifiable
10+
public sealed class FilterableResource : HexStringMongoIdentifiable
1111
{
1212
[Attr]
1313
public string SomeString { get; set; } = string.Empty;

test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Label.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings;
77

88
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
9-
public sealed class Label : MongoIdentifiable
9+
public sealed class Label : HexStringMongoIdentifiable
1010
{
1111
[Attr]
1212
public string Name { get; set; } = null!;

test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/LoginAttempt.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings;
66

77
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
8-
public sealed class LoginAttempt : MongoIdentifiable
8+
public sealed class LoginAttempt : HexStringMongoIdentifiable
99
{
1010
[Attr]
1111
public DateTimeOffset TriedAt { get; set; }

test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/WebAccount.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings;
77

88
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
99
[Resource(ControllerNamespace = "JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings")]
10-
public sealed class WebAccount : MongoIdentifiable
10+
public sealed class WebAccount : HexStringMongoIdentifiable
1111
{
1212
[Attr]
1313
public string UserName { get; set; } = null!;

test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ public async Task Cannot_create_resource_with_int_ID()
9898
ErrorObject error = responseDocument.Errors[0];
9999
error.StatusCode.Should().Be(HttpStatusCode.InternalServerError);
100100
error.Title.Should().Be("An unhandled error occurred while processing this request.");
101-
error.Detail.Should().Be("MongoDB can only be used for resources with an 'Id' property of type 'string'.");
101+
error.Detail.Should().Be("MongoDB can only be used with resources that implement 'IMongoIdentifiable'.");
102102
}
103103

104104
[Fact]

0 commit comments

Comments
 (0)