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
68 changes: 68 additions & 0 deletions Tharga.Cache.MongoDB.Tests/MongoDBKeyFilterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using MongoDB.Driver;
using Moq;
using Tharga.Cache.Core;
using Tharga.MongoDB;
using Xunit;
using Sut = Tharga.Cache.MongoDB.MongoDB;

namespace Tharga.Cache.MongoDB.Tests;

/// <summary>
/// The cache key has to reach Mongo as an id, never as a filter document.
/// </summary>
/// <remarks>
/// DeleteOneAsync and UpdateOneAsync have no TKey overload, so a bare string is picked up by the
/// FilterDefinition overload through the driver's implicit string -> JsonFilterDefinition conversion.
/// That compiles cleanly and then fails at runtime, once per drop, with
/// "JSON reader was expecting a value but found '&lt;TypeName&gt;'" - because CacheBase prefixes every
/// key with the type name. Rendering the filter here is what tells the two apart.
/// </remarks>
public class MongoDBKeyFilterTests
{
private const string CacheKey = "FeedStatusDto.my-feed-key";

private static BsonDocument Render(FilterDefinition<CacheEntity> filter)
{
return filter.Render(new RenderArgs<CacheEntity>(BsonSerializer.SerializerRegistry.GetSerializer<CacheEntity>(), BsonSerializer.SerializerRegistry));
}

private static (Sut Sut, Mock<ICacheRepositoryCollection> Collection) BuildSut()
{
var collection = new Mock<ICacheRepositoryCollection>();

var collectionProvider = new Mock<ICollectionProvider>();
collectionProvider
.Setup(x => x.GetCollection<ICacheRepositoryCollection, CacheEntity, string>(It.IsAny<DatabaseContext>()))
.Returns(collection.Object);

var sut = new Sut(collectionProvider.Object, Mock.Of<IManagedCacheMonitor>(), Options.Create(new MongoDBCacheOptions()), Mock.Of<ILogger<Sut>>());
return (sut, collection);
}

[Fact]
public async Task DropAsync_FiltersOnTheIdRatherThanParsingTheKeyAsJson()
{
var (sut, collection) = BuildSut();
FilterDefinition<CacheEntity> captured = null;
collection
.Setup(x => x.DeleteOneAsync(It.IsAny<FilterDefinition<CacheEntity>>(), It.IsAny<OneOption<CacheEntity>>(), It.IsAny<IClientSessionHandle>()))
.Callback<FilterDefinition<CacheEntity>, OneOption<CacheEntity>, IClientSessionHandle>((f, _, _) => captured = f)
.ReturnsAsync((CacheEntity)null);

await sut.DropAsync<object>(CacheKey);

captured.Should().NotBeNull();
var rendered = Render(captured);
rendered.ElementCount.Should().Be(1);
rendered.GetElement(0).Name.Should().Be("_id");
rendered.GetElement(0).Value.AsString.Should().Be(CacheKey);
}

}


24 changes: 20 additions & 4 deletions Tharga.Cache.MongoDB/MongoDB.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public async Task<CacheItem<T>> GetAsync<T>(Key key)
{
if (!item.StaleWhileRevalidate && item.FreshSpan.HasValue && item.FreshSpan.Value != TimeSpan.MaxValue && item.CreateTime.Add(item.FreshSpan.Value) < DateTime.UtcNow)
{
await collection.DeleteOneAsync(item.Id);
await collection.DeleteOneAsync(ById(item.Id));
return null;
}

Expand Down Expand Up @@ -104,15 +104,30 @@ public Task<bool> Invalidate<T>(Key key)
public async Task<bool> DropAsync<T>(Key key)
{
var collection = GetCollection();
var item = await collection.DeleteOneAsync(key.Value);
var item = await collection.DeleteOneAsync(ById(key.Value));
return item != null;
}

/// <summary>
/// The id of a cache entity as an explicit filter.
/// </summary>
/// <remarks>
/// There is no DeleteOneAsync(TKey) overload, so a bare string is picked up by
/// DeleteOneAsync(FilterDefinition&lt;CacheEntity&gt;) through the driver's implicit
/// string -&gt; JsonFilterDefinition conversion. That compiles, and then Mongo parses the
/// cache key as a JSON filter document: "JSON reader was expecting a value but found 'MyType'".
/// Building the filter here means the key can only ever be read as an id.
/// </remarks>
private static FilterDefinition<CacheEntity> ById(string id)
{
return new FilterDefinitionBuilder<CacheEntity>().Eq(x => x.Id, id);
}

private async Task<bool> SetUpdateTime(Key key, DateTime updateTime)
{
var collection = GetCollection();
var update = new UpdateDefinitionBuilder<CacheEntity>().Set(x => x.UpdateTime, updateTime);
var result = await collection.UpdateOneAsync(key.Value, update, OneOption<CacheEntity>.SingleOrDefault);
var result = await collection.UpdateOneAsync(ById(key.Value), update, OneOption<CacheEntity>.SingleOrDefault);
return result.Before != null;
}

Expand Down Expand Up @@ -150,4 +165,5 @@ public ValueTask DisposeAsync()
{
return ValueTask.CompletedTask;
}
}
}

1 change: 1 addition & 0 deletions Tharga.Cache/Tharga.Cache.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
<InternalsVisibleTo Include="Tharga.Cache.Redis" />
<InternalsVisibleTo Include="Tharga.Cache.File" />
<InternalsVisibleTo Include="Tharga.Cache.Tests" />
<InternalsVisibleTo Include="Tharga.Cache.MongoDB.Tests" />
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
</ItemGroup>

Expand Down
Loading