// Copyright Epic Games, Inc. All Rights Reserved.
using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;
using System;
using System.Reflection;
using System.Threading.Tasks;
namespace EpicGames.MongoDB
{
///
/// Attribute specifying the unique id for a singleton document
///
[AttributeUsage(AttributeTargets.Class)]
public sealed class SingletonDocumentAttribute : Attribute
{
///
/// Unique id for the singleton document
///
public string Id { get; }
///
/// Constructor
///
/// Unique id for the singleton document
public SingletonDocumentAttribute(string id)
{
Id = id;
}
}
///
/// Base class for singletons
///
public class SingletonBase
{
///
/// Unique id of the
///
public ObjectId Id { get; set; }
///
/// Current revision number
///
public int Revision { get; set; }
///
/// Private constructor for serialization
///
[BsonConstructor]
protected SingletonBase()
{
}
///
/// Constructor
///
/// Unique id for the singleton
public SingletonBase(ObjectId id)
{
Id = id;
}
///
/// Base class for singleton documents
///
class CachedId
{
public static ObjectId Value { get; } = GetSingletonId();
static ObjectId GetSingletonId()
{
SingletonDocumentAttribute? attribute = typeof(T).GetCustomAttribute() ?? throw new Exception($"Type {typeof(T).Name} is missing a {nameof(SingletonDocumentAttribute)} annotation");
return ObjectId.Parse(attribute.Id);
}
}
///
/// Gets the id for a singleton type
///
///
///
public static ObjectId GetId() where T : SingletonBase
{
return CachedId.Value;
}
}
///
/// Interface for the getting and setting the singleton
///
/// Type of document
public interface ISingletonDocument
{
///
/// Gets the current document
///
/// The current document
Task GetAsync();
///
/// Attempts to update the document
///
/// New state of the document
/// True if the document was updated, false otherwise
Task TryUpdateAsync(T value);
}
///
/// Concrete implementation of
///
/// The document type
public class SingletonDocument : ISingletonDocument where T : SingletonBase, new()
{
///
/// The database service instance
///
readonly IMongoCollection _collection;
///
/// Unique id for the singleton document
///
readonly ObjectId _objectId;
///
/// Static constructor. Registers the document using the automapper.
///
static SingletonDocument()
{
BsonClassMap.RegisterClassMap();
}
///
/// Constructor
///
/// The database service instance
public SingletonDocument(IMongoCollection collection)
{
_collection = collection.OfType();
SingletonDocumentAttribute? attribute = typeof(T).GetCustomAttribute() ?? throw new Exception($"Type {typeof(T).Name} is missing a {nameof(SingletonDocumentAttribute)} annotation");
_objectId = new ObjectId(attribute.Id);
}
///
public async Task GetAsync()
{
for (; ; )
{
T? @object = await _collection.Find(x => x.Id == _objectId).FirstOrDefaultAsync();
if (@object != null)
{
return @object;
}
T newItem = new T();
newItem.Id = _objectId;
await _collection.InsertOneAsync(newItem);
}
}
///
public async Task TryUpdateAsync(T value)
{
int prevRevision = value.Revision++;
try
{
ReplaceOneResult result = await _collection.ReplaceOneAsync(x => x.Id == _objectId && x.Revision == prevRevision, value, new ReplaceOptions { IsUpsert = true });
return result.MatchedCount > 0;
}
catch (MongoWriteException ex)
{
// Duplicate key error occurs if filter fails to match because revision is not the same.
if (ex.WriteError != null && ex.WriteError.Category == ServerErrorCategory.DuplicateKey)
{
return false;
}
else
{
throw;
}
}
}
}
}