// 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; } } } } }