From 22105418941e984803d9d950e5c04ea398188f1c Mon Sep 17 00:00:00 2001 From: Josh Date: Sun, 2 Apr 2023 18:45:19 -0500 Subject: [PATCH] Fix memory leak in GameServer (#731) * Convert entities to serializable after aggregating rather before * Cache instances of CustomXmlSerializer and create readonly constants for reused settings * Change CustomXmlSerializer and serializer cache to work with deserializer --- .../Controllers/SlotEndpoints.cs | 6 +- .../Controllers/UserEndpoints.cs | 6 +- .../Controllers/CommentController.cs | 5 +- .../Controllers/Resources/PhotosController.cs | 15 ++--- .../Controllers/Slots/CollectionController.cs | 20 +++---- .../Controllers/Slots/ListController.cs | 22 ++++--- .../Controllers/Slots/ReviewController.cs | 10 ++-- .../Controllers/Slots/ScoreController.cs | 2 +- .../Controllers/Slots/SearchController.cs | 6 +- .../Controllers/Slots/SlotsController.cs | 55 +++++++----------- .../Extensions/ControllerExtensions.cs | 4 +- .../Extensions/QueryExtensions.cs | 11 ++++ .../Serialization/CustomXmlSerializer.cs | 58 +++++-------------- .../Serialization/LighthouseSerializer.cs | 46 ++++++++++----- ProjectLighthouse/Types/Levels/Category.cs | 2 +- .../Types/Serialization/GamePhoto.cs | 4 +- 16 files changed, 125 insertions(+), 147 deletions(-) create mode 100644 ProjectLighthouse/Extensions/QueryExtensions.cs diff --git a/ProjectLighthouse.Servers.API/Controllers/SlotEndpoints.cs b/ProjectLighthouse.Servers.API/Controllers/SlotEndpoints.cs index caa1f3c0..c4960bfc 100644 --- a/ProjectLighthouse.Servers.API/Controllers/SlotEndpoints.cs +++ b/ProjectLighthouse.Servers.API/Controllers/SlotEndpoints.cs @@ -1,6 +1,7 @@ #nullable enable using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Servers.API.Responses; using LBPUnion.ProjectLighthouse.Types.Entities.Level; using Microsoft.AspNetCore.Mvc; @@ -35,11 +36,10 @@ public class SlotEndpoints : ApiEndpointController if (limit < 0) limit = 0; limit = Math.Min(ServerStatics.PageSize, limit); - IEnumerable minimalSlots = await this.database.Slots.OrderByDescending(s => s.FirstUploaded) + List minimalSlots = (await this.database.Slots.OrderByDescending(s => s.FirstUploaded) .Skip(skip) .Take(limit) - .Select(s => ApiSlot.CreateFromEntity(s)) - .ToListAsync(); + .ToListAsync()).ToSerializableList(ApiSlot.CreateFromEntity); return this.Ok(minimalSlots); } diff --git a/ProjectLighthouse.Servers.API/Controllers/UserEndpoints.cs b/ProjectLighthouse.Servers.API/Controllers/UserEndpoints.cs index a0e96239..cb095a60 100644 --- a/ProjectLighthouse.Servers.API/Controllers/UserEndpoints.cs +++ b/ProjectLighthouse.Servers.API/Controllers/UserEndpoints.cs @@ -1,5 +1,6 @@ #nullable enable using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Servers.API.Responses; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; @@ -63,13 +64,12 @@ public class UserEndpoints : ApiEndpointController [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task SearchUsers(string query) { - List users = await this.database.Users + List users = (await this.database.Users .Where(u => u.PermissionLevel != PermissionLevel.Banned && u.Username.Contains(query)) .Where(u => u.ProfileVisibility == PrivacyType.All) // TODO: change check for when user is logged in .OrderByDescending(b => b.UserId) .Take(20) - .Select(u => ApiUser.CreateFromEntity(u)) - .ToListAsync(); + .ToListAsync()).ToSerializableList(ApiUser.CreateFromEntity); if (!users.Any()) return this.NotFound(); return this.Ok(users); diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/CommentController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/CommentController.cs index af17842f..1a5b98ce 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/CommentController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/CommentController.cs @@ -77,15 +77,14 @@ public class CommentController : ControllerBase where blockedProfile.UserId == token.UserId select blockedProfile.BlockedUserId).ToListAsync(); - List comments = await this.database.Comments.Where(p => p.TargetId == targetId && p.Type == type) + List comments = (await this.database.Comments.Where(p => p.TargetId == targetId && p.Type == type) .OrderByDescending(p => p.Timestamp) .Where(p => !blockedUsers.Contains(p.PosterUserId)) .Include(c => c.Poster) .Where(p => p.Poster.PermissionLevel != PermissionLevel.Banned) .Skip(Math.Max(0, pageStart - 1)) .Take(Math.Min(pageSize, 30)) - .Select(c => GameComment.CreateFromEntity(c, token.UserId)) - .ToListAsync(); + .ToListAsync()).ToSerializableList(c => GameComment.CreateFromEntity(c, token.UserId)); return this.Ok(new CommentListResponse(comments)); } diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Resources/PhotosController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Resources/PhotosController.cs index 401a3fe6..aa994f19 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Resources/PhotosController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Resources/PhotosController.cs @@ -167,13 +167,12 @@ public class PhotosController : ControllerBase if (slotType == "developer") id = await SlotHelper.GetPlaceholderSlotId(this.database, id, SlotType.Developer); - List photos = await this.database.Photos.Include(p => p.PhotoSubjects) + List photos = (await this.database.Photos.Include(p => p.PhotoSubjects) .Where(p => p.SlotId == id) .OrderByDescending(s => s.Timestamp) .Skip(Math.Max(0, pageStart - 1)) .Take(Math.Min(pageSize, 30)) - .Select(p => GamePhoto.CreateFromEntity(p)) - .ToListAsync(); + .ToListAsync()).ToSerializableList(GamePhoto.CreateFromEntity); return this.Ok(new PhotoListResponse(photos)); } @@ -186,13 +185,12 @@ public class PhotosController : ControllerBase int targetUserId = await this.database.UserIdFromUsername(user); if (targetUserId == 0) return this.NotFound(); - List photos = await this.database.Photos.Include(p => p.PhotoSubjects) + List photos = (await this.database.Photos.Include(p => p.PhotoSubjects) .Where(p => p.CreatorId == targetUserId) .OrderByDescending(s => s.Timestamp) .Skip(Math.Max(0, pageStart - 1)) .Take(Math.Min(pageSize, 30)) - .Select(p => GamePhoto.CreateFromEntity(p)) - .ToListAsync(); + .ToListAsync()).ToSerializableList(GamePhoto.CreateFromEntity); return this.Ok(new PhotoListResponse(photos)); } @@ -204,13 +202,12 @@ public class PhotosController : ControllerBase int targetUserId = await this.database.UserIdFromUsername(user); if (targetUserId == 0) return this.NotFound(); - List photos = await this.database.Photos.Include(p => p.PhotoSubjects) + List photos = (await this.database.Photos.Include(p => p.PhotoSubjects) .Where(p => p.PhotoSubjects.Any(ps => ps.UserId == targetUserId)) .OrderByDescending(s => s.Timestamp) .Skip(Math.Max(0, pageStart - 1)) .Take(Math.Min(pageSize, 30)) - .Select(p => GamePhoto.CreateFromEntity(p)) - .ToListAsync(); + .ToListAsync()).ToSerializableList(GamePhoto.CreateFromEntity); return this.Ok(new PhotoListResponse(photos)); } diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/CollectionController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/CollectionController.cs index 5542c619..4a64e18b 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/CollectionController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/CollectionController.cs @@ -37,9 +37,8 @@ public class CollectionController : ControllerBase GameTokenEntity token = this.GetToken(); - List slots = await this.database.Slots.Where(s => targetPlaylist.SlotIds.Contains(s.SlotId)) - .Select(s => SlotBase.CreateFromEntity(s, token)) - .ToListAsync(); + List slots = (await this.database.Slots.Where(s => targetPlaylist.SlotIds.Contains(s.SlotId)).ToListAsync()) + .ToSerializableList(s => SlotBase.CreateFromEntity(s, token)); int total = targetPlaylist.SlotIds.Length; @@ -101,9 +100,8 @@ public class CollectionController : ControllerBase private async Task GetUserPlaylists(int userId) { - List playlists = await this.database.Playlists.Where(p => p.CreatorId == userId) - .Select(p => GamePlaylist.CreateFromEntity(p)) - .ToListAsync(); + List playlists = (await this.database.Playlists.Where(p => p.CreatorId == userId) + .ToListAsync()).ToSerializableList(GamePlaylist.CreateFromEntity); int total = this.database.Playlists.Count(p => p.CreatorId == userId); return new PlaylistResponse @@ -189,16 +187,16 @@ public class CollectionController : ControllerBase if (category is CategoryWithUser categoryWithUser) { - slots = categoryWithUser.GetSlots(this.database, user, pageStart, pageSize) - .Select(s => SlotBase.CreateFromEntity(s, token)) - .ToList(); + slots = (await categoryWithUser.GetSlots(this.database, user, pageStart, pageSize) + .ToListAsync()) + .ToSerializableList(s => SlotBase.CreateFromEntity(s, token)); totalSlots = categoryWithUser.GetTotalSlots(this.database, user); } else { slots = category.GetSlots(this.database, pageStart, pageSize) - .Select(s => SlotBase.CreateFromEntity(s, token)) - .ToList(); + .ToList() + .ToSerializableList(s => SlotBase.CreateFromEntity(s, token)); totalSlots = category.GetTotalSlots(this.database); } diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ListController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ListController.cs index e390149a..2b902ee6 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ListController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ListController.cs @@ -47,10 +47,11 @@ public class ListController : ControllerBase if (pageSize <= 0) return this.BadRequest(); - List queuedLevels = await this.filterListByRequest(gameFilterType, dateFilterType, token.GameVersion, username, ListFilterType.Queue) + List queuedLevels = (await this.filterListByRequest(gameFilterType, dateFilterType, token.GameVersion, username, ListFilterType.Queue) .Skip(Math.Max(0, pageStart - 1)) .Take(Math.Min(pageSize, 30)) - .Select(s => SlotBase.CreateFromEntity(s, token)).ToListAsync(); + .ToListAsync()) + .ToSerializableList(s => SlotBase.CreateFromEntity(s, token)); int total = await this.database.QueuedLevels.CountAsync(q => q.UserId == token.UserId); int start = pageStart + Math.Min(pageSize, 30); @@ -119,11 +120,11 @@ public class ListController : ControllerBase UserEntity? targetUser = await this.database.Users.FirstOrDefaultAsync(u => u.Username == username); if (targetUser == null) return this.Forbid(); - List heartedLevels = await this.filterListByRequest(gameFilterType, dateFilterType, token.GameVersion, username, ListFilterType.FavouriteSlots) + List heartedLevels = (await this.filterListByRequest(gameFilterType, dateFilterType, token.GameVersion, username, ListFilterType.FavouriteSlots) .Skip(Math.Max(0, pageStart - 1)) .Take(Math.Min(pageSize, 30)) - .Select(s => SlotBase.CreateFromEntity(s, token)) - .ToListAsync(); + .ToListAsync()).ToSerializableList(s => SlotBase.CreateFromEntity(s, token)); + int total = await this.database.HeartedLevels.CountAsync(q => q.UserId == targetUser.UserId); int start = pageStart + Math.Min(pageSize, 30); @@ -191,13 +192,12 @@ public class ListController : ControllerBase int targetUserId = await this.database.UserIdFromUsername(username); if (targetUserId == 0) return this.Forbid(); - List heartedPlaylists = await this.database.HeartedPlaylists.Where(p => p.UserId == targetUserId) + List heartedPlaylists = (await this.database.HeartedPlaylists.Where(p => p.UserId == targetUserId) .Include(p => p.Playlist) .Include(p => p.Playlist.Creator) .OrderByDescending(p => p.HeartedPlaylistId) .Select(p => p.Playlist) - .Select(p => GamePlaylist.CreateFromEntity(p)) - .ToListAsync(); + .ToListAsync()).ToSerializableList(GamePlaylist.CreateFromEntity); int total = await this.database.HeartedPlaylists.CountAsync(p => p.UserId == targetUserId); @@ -250,15 +250,13 @@ public class ListController : ControllerBase if (pageSize <= 0) return this.BadRequest(); - List heartedProfiles = await this.database.HeartedProfiles.Include - (h => h.HeartedUser) + List heartedProfiles = (await this.database.HeartedProfiles.Include(h => h.HeartedUser) .OrderBy(h => h.HeartedProfileId) .Where(h => h.UserId == targetUser.UserId) .Select(h => h.HeartedUser) .Skip(Math.Max(0, pageStart - 1)) .Take(Math.Min(pageSize, 30)) - .Select(h => GameUser.CreateFromEntity(h, token.GameVersion)) - .ToListAsync(); + .ToListAsync()).ToSerializableList(u => GameUser.CreateFromEntity(u, token.GameVersion)); int total = await this.database.HeartedProfiles.CountAsync(h => h.UserId == targetUser.UserId); diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ReviewController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ReviewController.cs index 8bdee6b6..1491cc0a 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ReviewController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ReviewController.cs @@ -153,14 +153,13 @@ public class ReviewController : ControllerBase SlotEntity? slot = await this.database.Slots.FirstOrDefaultAsync(s => s.SlotId == slotId); if (slot == null) return this.BadRequest(); - List reviews = await this.database.Reviews.ByGameVersion(gameVersion, true) + List reviews = (await this.database.Reviews.ByGameVersion(gameVersion, true) .Where(r => r.SlotId == slotId) .OrderByDescending(r => r.ThumbsUp - r.ThumbsDown) .ThenByDescending(r => r.Timestamp) .Skip(Math.Max(0, pageStart - 1)) .Take(Math.Min(pageSize, 30)) - .Select(r => GameReview.CreateFromEntity(r, token)) - .ToListAsync(); + .ToListAsync()).ToSerializableList(r => GameReview.CreateFromEntity(r, token)); return this.Ok(new ReviewResponse(reviews, reviews.LastOrDefault()?.Timestamp ?? TimeHelper.TimestampMillis, pageStart + Math.Min(pageSize, 30))); @@ -179,13 +178,12 @@ public class ReviewController : ControllerBase if (targetUserId == 0) return this.BadRequest(); - List reviews = await this.database.Reviews.ByGameVersion(gameVersion, true) + List reviews = (await this.database.Reviews.ByGameVersion(gameVersion, true) .Where(r => r.ReviewerId == targetUserId) .OrderByDescending(r => r.Timestamp) .Skip(Math.Max(0, pageStart - 1)) .Take(Math.Min(pageSize, 30)) - .Select(r => GameReview.CreateFromEntity(r, token)) - .ToListAsync(); + .ToListAsync()).ToSerializableList(r => GameReview.CreateFromEntity(r, token)); return this.Ok(new ReviewResponse(reviews, reviews.LastOrDefault()?.Timestamp ?? TimeHelper.TimestampMillis, pageStart)); } diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ScoreController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ScoreController.cs index 4b7ed751..3b5f2df2 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ScoreController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ScoreController.cs @@ -322,7 +322,7 @@ public class ScoreController : ControllerBase // Paginated viewing: if not requesting pageStart, get results around user var pagedScores = rankedScores.Skip(options.PageStart != -1 || myScore == null ? options.PageStart - 1 : myScore.Rank - 3).Take(Math.Min(options.PageSize, 30)); - List gameScores = pagedScores.Select(ps => GameScore.CreateFromEntity(ps.Score, ps.Rank)).ToList(); + List gameScores = pagedScores.ToSerializableList(ps => GameScore.CreateFromEntity(ps.Score, ps.Rank)); return new ScoreboardResponse(options.RootName, gameScores, rankedScores.Count, myScore?.Score.Points ?? 0, myScore?.Rank ?? 0); } diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SearchController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SearchController.cs index dd0c5aec..07c2296e 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SearchController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SearchController.cs @@ -61,10 +61,10 @@ public class SearchController : ControllerBase s.SlotId.ToString().Equals(keyword) ); - List slots = await dbQuery.Skip(Math.Max(0, pageStart - 1)) + List slots = (await dbQuery.Skip(Math.Max(0, pageStart - 1)) .Take(Math.Min(pageSize, 30)) - .Select(s => SlotBase.CreateFromEntity(s, token)) - .ToListAsync(); + .ToListAsync()) + .ToSerializableList(s => SlotBase.CreateFromEntity(s, token)); return this.Ok(new GenericSlotResponse(keyName, slots, await dbQuery.CountAsync(), 0)); } diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SlotsController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SlotsController.cs index d9e5731b..fcf58166 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SlotsController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SlotsController.cs @@ -40,12 +40,11 @@ public class SlotsController : ControllerBase int usedSlots = this.database.Slots.Count(s => s.CreatorId == targetUserId); - List slots = await this.database.Slots.Where(s => s.CreatorId == targetUserId) + List slots = (await this.database.Slots.Where(s => s.CreatorId == targetUserId) .ByGameVersion(token.GameVersion, token.UserId == targetUserId) .Skip(Math.Max(0, pageStart - 1)) .Take(Math.Min(pageSize, usedSlots)) - .Select(s => SlotBase.CreateFromEntity(s, token)) - .ToListAsync(); + .ToListAsync()).ToSerializableList(s => SlotBase.CreateFromEntity(s, token)); int start = pageStart + Math.Min(pageSize, usedSlots); int total = await this.database.Slots.CountAsync(s => s.CreatorId == targetUserId); @@ -158,12 +157,11 @@ public class SlotsController : ControllerBase GameVersion gameVersion = token.GameVersion; - List slots = await this.database.Slots.ByGameVersion(gameVersion, false, true) + List slots = (await this.database.Slots.ByGameVersion(gameVersion, false, true) .OrderByDescending(s => s.FirstUploaded) .Skip(Math.Max(0, pageStart - 1)) .Take(Math.Min(pageSize, 30)) - .Select(s => SlotBase.CreateFromEntity(s, token)) - .ToListAsync(); + .ToListAsync()).ToSerializableList(s => SlotBase.CreateFromEntity(s, token)); int start = pageStart + Math.Min(pageSize, ServerConfiguration.Instance.UserGeneratedContentLimits.EntitledSlots); int total = await StatisticsHelper.SlotCountForGame(this.database, token.GameVersion); @@ -192,13 +190,12 @@ public class SlotsController : ControllerBase .Select(r => r.SlotId) .ToList(); - List slots = await this.database.Slots.ByGameVersion(gameVersion, false, true) + List slots = (await this.database.Slots.ByGameVersion(gameVersion, false, true) .Where(s => slotIdsWithTag.Contains(s.SlotId)) .OrderByDescending(s => s.PlaysLBP1) .Skip(Math.Max(0, pageStart - 1)) .Take(Math.Min(pageSize, 30)) - .Select(s => SlotBase.CreateFromEntity(s, token)) - .ToListAsync(); + .ToListAsync()).ToSerializableList(s => SlotBase.CreateFromEntity(s, token)); int start = pageStart + Math.Min(pageSize, ServerConfiguration.Instance.UserGeneratedContentLimits.EntitledSlots); int total = slotIdsWithTag.Count; @@ -215,13 +212,12 @@ public class SlotsController : ControllerBase GameVersion gameVersion = token.GameVersion; - List slots = await this.database.Slots.ByGameVersion(gameVersion, false, true) + List slots = (await this.database.Slots.ByGameVersion(gameVersion, false, true) .ToAsyncEnumerable() .OrderByDescending(s => s.RatingLBP1) .Skip(Math.Max(0, pageStart - 1)) .Take(Math.Min(pageSize, 30)) - .Select(s => SlotBase.CreateFromEntity(s, token)) - .ToListAsync(); + .ToListAsync()).ToSerializableList(s => SlotBase.CreateFromEntity(s, token)); int start = pageStart + Math.Min(pageSize, ServerConfiguration.Instance.UserGeneratedContentLimits.EntitledSlots); int total = await StatisticsHelper.SlotCount(this.database); @@ -241,13 +237,12 @@ public class SlotsController : ControllerBase .Select(s => s.SlotId) .ToListAsync(); - List slots = await this.database.Slots.Where(s => slotIdsWithTag.Contains(s.SlotId)) + List slots = (await this.database.Slots.Where(s => slotIdsWithTag.Contains(s.SlotId)) .ByGameVersion(token.GameVersion, false, true) .OrderByDescending(s => s.PlaysLBP1) .Skip(Math.Max(0, pageStart - 1)) .Take(Math.Min(pageSize, 30)) - .Select(s => SlotBase.CreateFromEntity(s, token)) - .ToListAsync(); + .ToListAsync()).ToSerializableList(s => SlotBase.CreateFromEntity(s, token)); int start = pageStart + Math.Min(pageSize, ServerConfiguration.Instance.UserGeneratedContentLimits.EntitledSlots); int total = slotIdsWithTag.Count; @@ -262,13 +257,12 @@ public class SlotsController : ControllerBase if (pageSize <= 0) return this.BadRequest(); - List slots = await this.database.Slots.Where(s => s.TeamPick) + List slots = (await this.database.Slots.Where(s => s.TeamPick) .ByGameVersion(token.GameVersion, false, true) .OrderByDescending(s => s.LastUpdated) .Skip(Math.Max(0, pageStart - 1)) .Take(Math.Min(pageSize, 30)) - .Select(s => SlotBase.CreateFromEntity(s, token)) - .ToListAsync(); + .ToListAsync()).ToSerializableList(s => SlotBase.CreateFromEntity(s, token)); int start = pageStart + Math.Min(pageSize, ServerConfiguration.Instance.UserGeneratedContentLimits.EntitledSlots); int total = await StatisticsHelper.TeamPickCountForGame(this.database, token.GameVersion); @@ -284,11 +278,10 @@ public class SlotsController : ControllerBase GameVersion gameVersion = token.GameVersion; - List slots = await this.database.Slots.ByGameVersion(gameVersion, false, true) + List slots = (await this.database.Slots.ByGameVersion(gameVersion, false, true) .OrderBy(_ => EF.Functions.Random()) .Take(Math.Min(pageSize, 30)) - .Select(s => SlotBase.CreateFromEntity(s, token)) - .ToListAsync(); + .ToListAsync()).ToSerializableList(s => SlotBase.CreateFromEntity(s, token)); int start = pageStart + Math.Min(pageSize, ServerConfiguration.Instance.UserGeneratedContentLimits.EntitledSlots); int total = await StatisticsHelper.SlotCountForGame(this.database, token.GameVersion); @@ -313,14 +306,13 @@ public class SlotsController : ControllerBase Random rand = new(); - List slots = await this.filterByRequest(gameFilterType, dateFilterType, token.GameVersion) + List slots = (await this.filterByRequest(gameFilterType, dateFilterType, token.GameVersion) .AsAsyncEnumerable() .OrderByDescending(s => s.Thumbsup) .ThenBy(_ => rand.Next()) .Skip(Math.Max(0, pageStart - 1)) .Take(Math.Min(pageSize, 30)) - .Select(s => SlotBase.CreateFromEntity(s, token)) - .ToListAsync(); + .ToListAsync()).ToSerializableList(s => SlotBase.CreateFromEntity(s, token)); int start = pageStart + Math.Min(pageSize, ServerConfiguration.Instance.UserGeneratedContentLimits.EntitledSlots); int total = await StatisticsHelper.SlotCountForGame(this.database, token.GameVersion); @@ -345,7 +337,7 @@ public class SlotsController : ControllerBase Random rand = new(); - List slots = await this.filterByRequest(gameFilterType, dateFilterType, token.GameVersion) + List slots = (await this.filterByRequest(gameFilterType, dateFilterType, token.GameVersion) .AsAsyncEnumerable() .OrderByDescending( // probably not the best way to do this? @@ -363,8 +355,7 @@ public class SlotsController : ControllerBase .ThenBy(_ => rand.Next()) .Skip(Math.Max(0, pageStart - 1)) .Take(Math.Min(pageSize, 30)) - .Select(s => SlotBase.CreateFromEntity(s, token)) - .ToListAsync(); + .ToListAsync()).ToSerializableList(s => SlotBase.CreateFromEntity(s, token)); int start = pageStart + Math.Min(pageSize, ServerConfiguration.Instance.UserGeneratedContentLimits.EntitledSlots); int total = await StatisticsHelper.SlotCountForGame(this.database, token.GameVersion); @@ -387,14 +378,13 @@ public class SlotsController : ControllerBase if (pageSize <= 0) return this.BadRequest(); - List slots = await this.filterByRequest(gameFilterType, dateFilterType, token.GameVersion) + List slots = (await this.filterByRequest(gameFilterType, dateFilterType, token.GameVersion) .AsAsyncEnumerable() .OrderByDescending(s => s.Hearts) .ThenBy(_ => RandomNumberGenerator.GetInt32(int.MaxValue)) .Skip(Math.Max(0, pageStart - 1)) .Take(Math.Min(pageSize, 30)) - .Select(s => SlotBase.CreateFromEntity(s, token)) - .ToListAsync(); + .ToListAsync()).ToSerializableList(s => SlotBase.CreateFromEntity(s, token)); int start = pageStart + Math.Min(pageSize, ServerConfiguration.Instance.UserGeneratedContentLimits.EntitledSlots); int total = await StatisticsHelper.SlotCountForGame(this.database, token.GameVersion); @@ -443,13 +433,12 @@ public class SlotsController : ControllerBase foreach (int slotId in orderedPlayersBySlotId) { - SlotBase? slot = await this.database.Slots.ByGameVersion(token.GameVersion, false, true) + SlotEntity? slot = await this.database.Slots.ByGameVersion(token.GameVersion, false, true) .Where(s => s.SlotId == slotId) - .Select(s => SlotBase.CreateFromEntity(s, token)) .FirstOrDefaultAsync(); if (slot == null) continue; // shouldn't happen ever unless the room is borked - slots.Add(slot); + slots.Add(SlotBase.CreateFromEntity(slot, token)); } int start = pageStart + Math.Min(pageSize, ServerConfiguration.Instance.UserGeneratedContentLimits.EntitledSlots); diff --git a/ProjectLighthouse/Extensions/ControllerExtensions.cs b/ProjectLighthouse/Extensions/ControllerExtensions.cs index 5f511527..189d9883 100644 --- a/ProjectLighthouse/Extensions/ControllerExtensions.cs +++ b/ProjectLighthouse/Extensions/ControllerExtensions.cs @@ -9,8 +9,8 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Xml; using System.Xml.Serialization; -using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Logging; +using LBPUnion.ProjectLighthouse.Serialization; using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Logging; using Microsoft.AspNetCore.Mvc; @@ -118,7 +118,7 @@ public static partial class ControllerExtensions } root = new XmlRootAttribute(rootElement); } - XmlSerializer serializer = new(typeof(T), root); + XmlSerializer serializer = LighthouseSerializer.GetSerializer(typeof(T), root); T? obj = (T?)serializer.Deserialize(new StringReader(bodyString)); return obj; } diff --git a/ProjectLighthouse/Extensions/QueryExtensions.cs b/ProjectLighthouse/Extensions/QueryExtensions.cs new file mode 100644 index 00000000..02d34700 --- /dev/null +++ b/ProjectLighthouse/Extensions/QueryExtensions.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace LBPUnion.ProjectLighthouse.Extensions; + +public static class QueryExtensions +{ + public static List ToSerializableList(this IEnumerable enumerable, Func selector) + => enumerable.Select(selector).ToList(); +} \ No newline at end of file diff --git a/ProjectLighthouse/Serialization/CustomXmlSerializer.cs b/ProjectLighthouse/Serialization/CustomXmlSerializer.cs index 1768d6e2..d43ba15e 100644 --- a/ProjectLighthouse/Serialization/CustomXmlSerializer.cs +++ b/ProjectLighthouse/Serialization/CustomXmlSerializer.cs @@ -2,7 +2,6 @@ using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -18,40 +17,12 @@ namespace LBPUnion.ProjectLighthouse.Serialization; /// public class CustomXmlSerializer : XmlSerializer { - private readonly IServiceProvider provider; + public CustomXmlSerializer(Type type, XmlRootAttribute rootAttribute) : base(type, rootAttribute) + { } - public CustomXmlSerializer(Type type, IServiceProvider provider, XmlRootAttribute rootAttribute) : base(type, rootAttribute) + public void Serialize(IServiceProvider provider, XmlWriter xmlWriter, object o, XmlSerializerNamespaces namespaces) { - this.provider = provider; - } - - public new void Serialize(object o, XmlSerializationWriter xmlSerializationWriter) - { - this.TriggerCallback(o); - base.Serialize(o, xmlSerializationWriter); - } - - public new void Serialize(Stream stream, object o) - { - this.TriggerCallback(o); - base.Serialize(stream, o); - } - - public new void Serialize(TextWriter textWriter, object o) - { - this.TriggerCallback(o); - base.Serialize(textWriter, o); - } - - public new void Serialize(XmlWriter xmlWriter, object o) - { - this.TriggerCallback(o); - base.Serialize(xmlWriter, o); - } - - public new void Serialize(XmlWriter xmlWriter, object o, XmlSerializerNamespaces namespaces) - { - this.TriggerCallback(o); + this.TriggerCallback(provider, o); base.Serialize(xmlWriter, o, namespaces); } @@ -86,11 +57,12 @@ public class CustomXmlSerializer : XmlSerializer /// /// Recursively finds all properties of an object /// + /// The service provider from the ASP.NET request that is used to resolve dependencies /// The object to recursively find all properties of /// A list of type references that have already been prepared to prevent duplicate preparing /// A number tracking how deep into the recursion call stack we are to prevent recursive loops /// A list of object references of all properties of the object - public void RecursivelyPrepare(object obj, List alreadyPrepared, int recursionDepth = 0) + private void RecursivelyPrepare(IServiceProvider provider, object obj, ICollection alreadyPrepared, int recursionDepth = 0) { if (recursionDepth > 5) return; switch (obj) @@ -98,7 +70,7 @@ public class CustomXmlSerializer : XmlSerializer case INeedsPreparationForSerialization needsPreparation: if (alreadyPrepared.Contains(needsPreparation)) break; - this.PrepareForSerialization(needsPreparation); + PrepareForSerialization(provider, needsPreparation); alreadyPrepared.Add(needsPreparation); break; case null: return; @@ -131,32 +103,32 @@ public class CustomXmlSerializer : XmlSerializer case IList list: foreach (object o in list) { - this.RecursivelyPrepare(o, alreadyPrepared, recursionDepth+1); + this.RecursivelyPrepare(provider, o, alreadyPrepared, recursionDepth+1); } break; case INeedsPreparationForSerialization nP: if (alreadyPrepared.Contains(nP)) break; // Prepare object - this.PrepareForSerialization(nP); + PrepareForSerialization(provider, nP); alreadyPrepared.Add(nP); // Recursively find objects in this INeedsPreparationForSerialization object - this.RecursivelyPrepare(nP, alreadyPrepared, recursionDepth+1); + this.RecursivelyPrepare(provider, nP, alreadyPrepared, recursionDepth+1); break; case ILbpSerializable serializable: // Recursively find objects in this ILbpSerializable object - this.RecursivelyPrepare(serializable, alreadyPrepared, recursionDepth+1); + this.RecursivelyPrepare(provider, serializable, alreadyPrepared, recursionDepth+1); break; } } } - public void PrepareForSerialization(INeedsPreparationForSerialization obj) - => LighthouseSerializer.PrepareForSerialization(this.provider, obj); + private static void PrepareForSerialization(IServiceProvider provider, INeedsPreparationForSerialization obj) + => LighthouseSerializer.PrepareForSerialization(provider, obj); - public void TriggerCallback(object o) + private void TriggerCallback(IServiceProvider provider, object o) { - this.RecursivelyPrepare(o, new List()); + this.RecursivelyPrepare(provider, o, new List()); } } \ No newline at end of file diff --git a/ProjectLighthouse/Serialization/LighthouseSerializer.cs b/ProjectLighthouse/Serialization/LighthouseSerializer.cs index a865a888..ea38160c 100644 --- a/ProjectLighthouse/Serialization/LighthouseSerializer.cs +++ b/ProjectLighthouse/Serialization/LighthouseSerializer.cs @@ -10,40 +10,56 @@ using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Logging; using LBPUnion.ProjectLighthouse.Types.Logging; using LBPUnion.ProjectLighthouse.Types.Serialization; -using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; namespace LBPUnion.ProjectLighthouse.Serialization; public static class LighthouseSerializer { + + private static readonly Dictionary serializerCache = new(); + + private static readonly XmlSerializerNamespaces emptyNamespace = new(new[] + { + XmlQualifiedName.Empty, + }); + + private static readonly XmlWriterSettings defaultWriterSettings = new() + { + OmitXmlDeclaration = true, + CheckCharacters = false, + }; + + public static CustomXmlSerializer GetSerializer(Type type, XmlRootAttribute? rootAttribute = null) + { + if (serializerCache.TryGetValue(type, out CustomXmlSerializer? value)) return value; + + CustomXmlSerializer serializer = new(type, rootAttribute); + + serializerCache.Add(type, serializer); + return serializer; + } + public static string Serialize(IServiceProvider serviceProvider, ILbpSerializable? serializableObject) { if (serializableObject == null) return ""; XmlRootAttribute? rootAttribute = null; + if (serializableObject is IHasCustomRoot customRoot) rootAttribute = new XmlRootAttribute(customRoot.GetRoot()); - // Required to omit the xml namespace - XmlSerializerNamespaces namespaces = new(); - namespaces.Add(string.Empty, string.Empty); + CustomXmlSerializer serializer = GetSerializer(serializableObject.GetType(), rootAttribute); + using StringWriter stringWriter = new(); - CustomXmlSerializer serializer = new(serializableObject.GetType(), serviceProvider, rootAttribute); - WriteFullClosingTagXmlWriter xmlWriter = new(stringWriter, - new XmlWriterSettings - { - OmitXmlDeclaration = true, - CheckCharacters = false, - }); - serializer.Serialize(xmlWriter, serializableObject, namespaces); + + WriteFullClosingTagXmlWriter xmlWriter = new(stringWriter, defaultWriterSettings); + + serializer.Serialize(serviceProvider, xmlWriter, serializableObject, emptyNamespace); string finalResult = stringWriter.ToString(); stringWriter.Dispose(); return finalResult; } - public static string Serialize(this ControllerBase controllerBase, ILbpSerializable serializableObject) - => Serialize(controllerBase.Request.HttpContext.RequestServices, serializableObject); - public static void PrepareForSerialization(IServiceProvider serviceProvider, INeedsPreparationForSerialization serializableObject) { MethodInfo? methodInfo = serializableObject.GetType().GetMethod("PrepareSerialization"); diff --git a/ProjectLighthouse/Types/Levels/Category.cs b/ProjectLighthouse/Types/Levels/Category.cs index db354c9f..33579472 100644 --- a/ProjectLighthouse/Types/Levels/Category.cs +++ b/ProjectLighthouse/Types/Levels/Category.cs @@ -41,7 +41,7 @@ public abstract class Category List slots = new(); SlotEntity? previewSlot = this.GetPreviewSlot(database); if (previewSlot != null) - slots.Add(SlotBase.CreateFromEntity(this.GetPreviewSlot(database), GameVersion.LittleBigPlanet3, -1)); + slots.Add(SlotBase.CreateFromEntity(previewSlot, GameVersion.LittleBigPlanet3, -1)); int totalSlots = this.GetTotalSlots(database); return GameCategory.CreateFromEntity(this, new GenericSlotResponse(slots, totalSlots, 2)); diff --git a/ProjectLighthouse/Types/Serialization/GamePhoto.cs b/ProjectLighthouse/Types/Serialization/GamePhoto.cs index a53187fd..771fe2a9 100644 --- a/ProjectLighthouse/Types/Serialization/GamePhoto.cs +++ b/ProjectLighthouse/Types/Serialization/GamePhoto.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Threading.Tasks; using System.Xml.Serialization; using LBPUnion.ProjectLighthouse.Database; -using LBPUnion.ProjectLighthouse.Serialization; +using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; using LBPUnion.ProjectLighthouse.Types.Levels; using Microsoft.EntityFrameworkCore; @@ -102,7 +102,7 @@ public class GamePhoto : ILbpSerializable, INeedsPreparationForSerialization MediumHash = entity.MediumHash, LargeHash = entity.MediumHash, PlanHash = entity.PlanHash, - Subjects = entity.PhotoSubjects.Select(GamePhotoSubject.CreateFromEntity).ToList(), + Subjects = entity.PhotoSubjects.ToSerializableList(GamePhotoSubject.CreateFromEntity), }; } \ No newline at end of file