using LocationsApi.Models; using MongoDB.Bson; using MongoDB.Driver; namespace LocationsApi.Services; public class LocationStore { private readonly IMongoCollection _col; private readonly IMongoCollection _rawCol; private const string CoordIndexName = "coord_x_1_coord_y_1"; public LocationStore(IConfiguration cfg) { var cs = cfg["MongoDB:ConnectionString"] ?? "mongodb://127.0.0.1:27017"; var dbName = cfg["MongoDB:DatabaseName"] ?? "GameDb"; var client = new MongoClient(cs); var db = client.GetDatabase(dbName); var collectionName = "Locations"; EnsureLocationSchema(db, collectionName); _col = db.GetCollection(collectionName); _rawCol = db.GetCollection(collectionName); EnsureCoordIndexes(); EnsureOriginLocation(); } private static void EnsureLocationSchema(IMongoDatabase db, string collectionName) { var validator = new BsonDocument { { "$jsonSchema", new BsonDocument { { "bsonType", "object" }, { "required", new BsonArray { "name", "coord", "createdUtc" } }, { "properties", new BsonDocument { { "name", new BsonDocument { { "bsonType", "string" } } }, { "coord", new BsonDocument { { "bsonType", "object" }, { "required", new BsonArray { "x", "y" } }, { "properties", new BsonDocument { { "x", new BsonDocument { { "bsonType", "int" } } }, { "y", new BsonDocument { { "bsonType", "int" } } } } } } }, { "createdUtc", new BsonDocument { { "bsonType", "date" } } } } } } } }; var collections = db.ListCollectionNames().ToList(); if (!collections.Contains(collectionName)) { var createCommand = new BsonDocument { { "create", collectionName }, { "validator", validator }, { "validationAction", "error" } }; db.RunCommand(createCommand); return; } var command = new BsonDocument { { "collMod", collectionName }, { "validator", validator }, { "validationAction", "error" } }; db.RunCommand(command); } public Task CreateAsync(Location location) => _col.InsertOneAsync(location); public Task> GetAllAsync() => _col.Find(Builders.Filter.Empty).ToListAsync(); public async Task DeleteAsync(string id) { var filter = Builders.Filter.Eq(l => l.Id, id); var result = await _col.DeleteOneAsync(filter); return result.DeletedCount > 0; } public async Task UpdateNameAsync(string id, string name) { var filter = Builders.Filter.Eq(l => l.Id, id); var update = Builders.Update.Set(l => l.Name, name); var result = await _col.UpdateOneAsync(filter, update); return result.ModifiedCount > 0; } private void EnsureCoordIndexes() { var indexes = _rawCol.Indexes.List().ToList(); foreach (var index in indexes) { var name = index.GetValue("name", "").AsString; if (name == "_id_") continue; var keyDoc = index.GetValue("key", new BsonDocument()).AsBsonDocument; if (IsLegacyCoordIndex(keyDoc) || IsUnexpectedCoordIndex(keyDoc)) _rawCol.Indexes.DropOne(name); } var coordIndex = new BsonDocument { { "coord.x", 1 }, { "coord.y", 1 } }; var coordIndexOptions = new CreateIndexOptions { Unique = true, Name = CoordIndexName }; _rawCol.Indexes.CreateOne(new CreateIndexModel(coordIndex, coordIndexOptions)); } private static bool IsLegacyCoordIndex(BsonDocument keyDoc) => keyDoc.ElementCount == 2 && keyDoc.TryGetValue("Coord.X", out var xValue) && xValue.IsInt32 && xValue.AsInt32 == 1 && keyDoc.TryGetValue("Coord.Y", out var yValue) && yValue.IsInt32 && yValue.AsInt32 == 1; private static bool IsUnexpectedCoordIndex(BsonDocument keyDoc) { var hasLower = keyDoc.Contains("coord.x") || keyDoc.Contains("coord.y"); var hasUpper = keyDoc.Contains("Coord.X") || keyDoc.Contains("Coord.Y"); return hasUpper || (hasLower && !(keyDoc.Contains("coord.x") && keyDoc.Contains("coord.y"))); } private void EnsureOriginLocation() { var filter = Builders.Filter.And( Builders.Filter.Eq(l => l.Coord.X, 0), Builders.Filter.Eq(l => l.Coord.Y, 0) ); var existing = _col.Find(filter).FirstOrDefault(); if (existing is not null) return; var origin = new Location { Name = "Origin", Coord = new Coord { X = 0, Y = 0 }, CreatedUtc = DateTime.UtcNow }; try { _col.InsertOne(origin); } catch (MongoWriteException ex) when (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) { // Another instance seeded it first. } } }