Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ dotnet add package JsonApiDotNetCore.MongoDb
#nullable enable

[Resource]
public class Book : MongoIdentifiable
public class Book : HexStringMongoIdentifiable
{
[Attr]
public string Name { get; set; } = null!;
Expand Down Expand Up @@ -70,9 +70,19 @@ builder.Services.AddJsonApiMongoDb();
builder.Services.AddScoped(typeof(IResourceReadRepository<,>), typeof(MongoRepository<,>));
builder.Services.AddScoped(typeof(IResourceWriteRepository<,>), typeof(MongoRepository<,>));
builder.Services.AddScoped(typeof(IResourceRepository<,>), typeof(MongoRepository<,>));

```

## Using client-generated IDs
Resources that inherit from `HexStringMongoIdentifiable` use auto-generated (performant) 12-byte hexadecimal
[Object IDs](https://docs.mongodb.com/manual/reference/bson-types/#objectid).
You can assign an ID manually, but it must match the 12-byte hexadecimal pattern.

To assign free-format string IDs manually, make your resources inherit from `FreeStringMongoIdentifiable` instead.
When creating a resource without assigning an ID, a 12-byte hexadecimal ID will be auto-generated.

Set `options.AllowClientGeneratedIds` to `true` in Program.cs to allow API clients to assign IDs. This can be combined
with both base classes, but `FreeStringMongoIdentifiable` probably makes the most sense.

## Limitations

- JSON:API relationships are currently not supported. You can use complex object graphs though, which are stored in a single document.
Expand Down
2 changes: 1 addition & 1 deletion src/Examples/GettingStarted/Models/Book.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace GettingStarted.Models;

[UsedImplicitly(ImplicitUseTargetFlags.Members)]
[Resource]
public sealed class Book : MongoIdentifiable
public sealed class Book : HexStringMongoIdentifiable
{
[Attr]
public string Title { get; set; } = null!;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace JsonApiDotNetCoreMongoDbExample.Models;

[UsedImplicitly(ImplicitUseTargetFlags.Members)]
[Resource]
public sealed class TodoItem : MongoIdentifiable
public sealed class TodoItem : HexStringMongoIdentifiable
{
[Attr]
public string Description { get; set; } = null!;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Collections.Immutable;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.MongoDb.Resources;
using JsonApiDotNetCore.Queries;
using JsonApiDotNetCore.Queries.Internal;
using JsonApiDotNetCore.Resources;
Expand Down Expand Up @@ -38,6 +39,11 @@ public IImmutableSet<ResourceFieldAttribute> GetSparseFieldSetForSerializer(Reso
{
IImmutableSet<ResourceFieldAttribute> fieldSet = _innerCache.GetSparseFieldSetForSerializer(resourceType);

return resourceType.ClrType.IsAssignableTo(typeof(IMongoIdentifiable)) ? RemoveRelationships(fieldSet) : fieldSet;
}

private static IImmutableSet<ResourceFieldAttribute> RemoveRelationships(IImmutableSet<ResourceFieldAttribute> fieldSet)
{
ResourceFieldAttribute[] relationships = fieldSet.Where(field => field is RelationshipAttribute).ToArray();
return fieldSet.Except(relationships);
}
Expand Down
5 changes: 3 additions & 2 deletions src/JsonApiDotNetCore.MongoDb/Repositories/MongoRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using JsonApiDotNetCore.Middleware;
using JsonApiDotNetCore.MongoDb.Errors;
using JsonApiDotNetCore.MongoDb.Queries.Internal.QueryableBuilding;
using JsonApiDotNetCore.MongoDb.Resources;
using JsonApiDotNetCore.Queries;
using JsonApiDotNetCore.Queries.Expressions;
using JsonApiDotNetCore.Queries.Internal.QueryableBuilding;
Expand Down Expand Up @@ -52,9 +53,9 @@ public MongoRepository(IMongoDataAccess mongoDataAccess, ITargetedFields targete
_constraintProviders = constraintProviders;
_resourceDefinitionAccessor = resourceDefinitionAccessor;

if (typeof(TId) != typeof(string))
if (!typeof(TResource).IsAssignableTo(typeof(IMongoIdentifiable)))
{
throw new InvalidConfigurationException("MongoDB can only be used for resources with an 'Id' property of type 'string'.");
throw new InvalidConfigurationException("MongoDB can only be used with resources that implement 'IMongoIdentifiable'.");
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Bson.Serialization.IdGenerators;

namespace JsonApiDotNetCore.MongoDb.Resources;

/// <summary>
/// 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
/// client-generated IDs.
/// </summary>
public abstract class FreeStringMongoIdentifiable : IMongoIdentifiable
{
/// <inheritdoc />
[BsonId(IdGenerator = typeof(StringObjectIdGenerator))]
public virtual string? Id { get; set; }

/// <inheritdoc />
[BsonIgnore]
public string? StringId
{
get => Id;
set => Id = value;
}

/// <inheritdoc />
[BsonIgnore]
public string? LocalId { get; set; }
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
using JsonApiDotNetCore.Resources;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;

namespace JsonApiDotNetCore.MongoDb.Resources;

/// <summary>
/// A convenient basic implementation of <see cref="IIdentifiable" /> for use with MongoDB models.
/// Basic implementation of a JSON:API resource whose Id is stored as a 12-byte hexadecimal ObjectId in MongoDB.
/// </summary>
public abstract class MongoIdentifiable : IIdentifiable<string?>
public abstract class HexStringMongoIdentifiable : IMongoIdentifiable
{
/// <inheritdoc />
[BsonId]
Expand Down
10 changes: 10 additions & 0 deletions src/JsonApiDotNetCore.MongoDb/Resources/IMongoIdentifiable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using JsonApiDotNetCore.Resources;

namespace JsonApiDotNetCore.MongoDb.Resources;

/// <summary>
/// Marker interface to indicate a resource that is stored in MongoDB.
/// </summary>
public interface IMongoIdentifiable : IIdentifiable<string?>
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Serialization.Objects;
using Microsoft.Extensions.DependencyInjection;
using MongoDB.Bson;
using TestBuildingBlocks;
using Xunit;

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

var requestBody = new
{
Expand Down Expand Up @@ -82,8 +81,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
public async Task Can_create_resource_with_client_generated_string_ID_having_no_side_effects()
{
// Arrange
MusicTrack newTrack = _fakers.MusicTrack.Generate();
newTrack.Id = ObjectId.GenerateNewId().ToString();
Playlist newPlaylist = _fakers.Playlist.Generate();
newPlaylist.Id = "free-format-client-generated-id";

var requestBody = new
{
Expand All @@ -94,13 +93,11 @@ public async Task Can_create_resource_with_client_generated_string_ID_having_no_
op = "add",
data = new
{
type = "musicTracks",
id = newTrack.StringId,
type = "playlists",
id = newPlaylist.StringId,
attributes = new
{
title = newTrack.Title,
lengthInSeconds = newTrack.LengthInSeconds,
releasedAt = newTrack.ReleasedAt
name = newPlaylist.Name
}
}
}
Expand All @@ -119,10 +116,9 @@ public async Task Can_create_resource_with_client_generated_string_ID_having_no_

await _testContext.RunOnDatabaseAsync(async dbContext =>
{
MusicTrack trackInDatabase = await dbContext.MusicTracks.FirstWithIdAsync(newTrack.Id);
Playlist playlistInDatabase = await dbContext.Playlists.FirstWithIdAsync(newPlaylist.Id);

trackInDatabase.Title.Should().Be(newTrack.Title);
trackInDatabase.LengthInSeconds.Should().BeApproximately(newTrack.LengthInSeconds);
playlistInDatabase.Name.Should().Be(newPlaylist.Name);
});
}

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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations;

[UsedImplicitly(ImplicitUseTargetFlags.Members)]
[Resource(ControllerNamespace = "JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations")]
public sealed class Lyric : MongoIdentifiable
public sealed class Lyric : HexStringMongoIdentifiable
{
[Attr]
public string? Format { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations;

[UsedImplicitly(ImplicitUseTargetFlags.Members)]
[Resource(ControllerNamespace = "JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations")]
public sealed class MusicTrack : MongoIdentifiable
public sealed class MusicTrack : HexStringMongoIdentifiable
{
[RegularExpression(@"^[a-fA-F\d]{24}$")]
public override string? Id { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations;

[UsedImplicitly(ImplicitUseTargetFlags.Members)]
[Resource(ControllerNamespace = "JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations")]
public sealed class Performer : MongoIdentifiable
public sealed class Performer : HexStringMongoIdentifiable
{
[Attr]
public string? ArtistName { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations;

[UsedImplicitly(ImplicitUseTargetFlags.Members)]
[Resource(ControllerNamespace = "JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations")]
public sealed class Playlist : MongoIdentifiable
public sealed class Playlist : FreeStringMongoIdentifiable
{
[Attr]
public string Name { get; set; } = null!;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations;

[UsedImplicitly(ImplicitUseTargetFlags.Members)]
[Resource(ControllerNamespace = "JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations")]
public sealed class RecordCompany : MongoIdentifiable
public sealed class RecordCompany : HexStringMongoIdentifiable
{
[Attr]
public string Name { get; set; } = null!;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations;

[UsedImplicitly(ImplicitUseTargetFlags.Members)]
[Resource(ControllerNamespace = "JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations")]
public sealed class TextLanguage : MongoIdentifiable
public sealed class TextLanguage : FreeStringMongoIdentifiable
{
[Attr]
public string? IsoCode { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.Meta;

[UsedImplicitly(ImplicitUseTargetFlags.Members)]
[Resource(ControllerNamespace = "JsonApiDotNetCoreMongoDbTests.IntegrationTests.Meta")]
public sealed class SupportTicket : MongoIdentifiable
public sealed class SupportTicket : HexStringMongoIdentifiable
{
[Attr]
public string Description { get; set; } = null!;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings;

[UsedImplicitly(ImplicitUseTargetFlags.Members)]
public sealed class AccountPreferences : MongoIdentifiable
public sealed class AccountPreferences : HexStringMongoIdentifiable
{
[Attr]
public bool UseDarkTheme { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings;

[UsedImplicitly(ImplicitUseTargetFlags.Members)]
[Resource(ControllerNamespace = "JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings")]
public sealed class Blog : MongoIdentifiable
public sealed class Blog : HexStringMongoIdentifiable
{
[Attr]
public string Title { get; set; } = null!;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings;

[UsedImplicitly(ImplicitUseTargetFlags.Members)]
[Resource(ControllerNamespace = "JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings")]
public sealed class BlogPost : MongoIdentifiable
public sealed class BlogPost : HexStringMongoIdentifiable
{
[Attr]
public string Caption { get; set; } = null!;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings;

[UsedImplicitly(ImplicitUseTargetFlags.Members)]
[Resource(ControllerNamespace = "JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings")]
public sealed class Comment : MongoIdentifiable
public sealed class Comment : HexStringMongoIdentifiable
{
[Attr]
public string Text { get; set; } = null!;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings.Filtering;

[UsedImplicitly(ImplicitUseTargetFlags.Members)]
[Resource(ControllerNamespace = "JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings.Filtering")]
public sealed class FilterableResource : MongoIdentifiable
public sealed class FilterableResource : HexStringMongoIdentifiable
{
[Attr]
public string SomeString { get; set; } = string.Empty;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings;

[UsedImplicitly(ImplicitUseTargetFlags.Members)]
public sealed class Label : MongoIdentifiable
public sealed class Label : HexStringMongoIdentifiable
{
[Attr]
public string Name { get; set; } = null!;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings;

[UsedImplicitly(ImplicitUseTargetFlags.Members)]
public sealed class LoginAttempt : MongoIdentifiable
public sealed class LoginAttempt : HexStringMongoIdentifiable
{
[Attr]
public DateTimeOffset TriedAt { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings;

[UsedImplicitly(ImplicitUseTargetFlags.Members)]
[Resource(ControllerNamespace = "JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings")]
public sealed class WebAccount : MongoIdentifiable
public sealed class WebAccount : HexStringMongoIdentifiable
{
[Attr]
public string UserName { get; set; } = null!;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ public async Task Cannot_create_resource_with_int_ID()
ErrorObject error = responseDocument.Errors[0];
error.StatusCode.Should().Be(HttpStatusCode.InternalServerError);
error.Title.Should().Be("An unhandled error occurred while processing this request.");
error.Detail.Should().Be("MongoDB can only be used for resources with an 'Id' property of type 'string'.");
error.Detail.Should().Be("MongoDB can only be used with resources that implement 'IMongoIdentifiable'.");
}

[Fact]
Expand Down
Loading