diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index 3e0f0cb7f6..d9d23dea6b 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -27,8 +27,8 @@
-
-
+
+
diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
index 728af5124e..42f70151ac 100644
--- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
+++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
@@ -5,7 +5,7 @@
-
+
WinExe
diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
index af16f39563..e51b20c9fe 100644
--- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
+++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
@@ -5,7 +5,7 @@
-
+
WinExe
diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
index 3d2d1f3fec..f1f75148ef 100644
--- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
+++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
@@ -5,7 +5,7 @@
-
+
WinExe
diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
index fa00922706..c9a320bdd5 100644
--- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
+++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
@@ -5,7 +5,7 @@
-
+
WinExe
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
index faa5d9e6fc..8cfe5d8af2 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
@@ -56,6 +56,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
beatmaps.Add(new BeatmapInfo
{
Ruleset = rulesets.GetRuleset(i % 4),
+ RulesetID = i % 4, // workaround for efcore 5 compatibility.
OnlineBeatmapID = beatmapId,
Length = length,
BPM = bpm,
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs
index 53a956c77c..9b8b74e6f6 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs
@@ -186,6 +186,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Metadata = metadata,
BaseDifficulty = new BeatmapDifficulty(),
Ruleset = ruleset,
+ RulesetID = ruleset.ID.GetValueOrDefault(), // workaround for efcore 5 compatibility.
StarDifficulty = difficultyIndex + 1,
Version = $"SR{difficultyIndex + 1}"
}).ToList()
diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
index 35c6d62cb7..2d192ae207 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
@@ -911,9 +911,11 @@ namespace osu.Game.Tests.Visual.SongSelect
int length = RNG.Next(30000, 200000);
double bpm = RNG.NextSingle(80, 200);
+ var ruleset = getRuleset();
beatmaps.Add(new BeatmapInfo
{
- Ruleset = getRuleset(),
+ Ruleset = ruleset,
+ RulesetID = ruleset.ID.GetValueOrDefault(), // workaround for efcore 5 compatibility.
OnlineBeatmapID = beatmapId,
Version = $"{beatmapId} (length {TimeSpan.FromMilliseconds(length):m\\:ss}, bpm {bpm:0.#})",
Length = length,
diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj
index e36b3cdc74..6f8e0fac6f 100644
--- a/osu.Game.Tests/osu.Game.Tests.csproj
+++ b/osu.Game.Tests/osu.Game.Tests.csproj
@@ -6,7 +6,7 @@
-
+
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index 29b3f5d3a3..f42fba79cb 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -174,6 +174,8 @@ namespace osu.Game.Beatmaps
if (beatmapSet.Beatmaps.Any(b => b.BaseDifficulty == null))
throw new InvalidOperationException($"Cannot import {nameof(BeatmapInfo)} with null {nameof(BeatmapInfo.BaseDifficulty)}.");
+ beatmapSet.Requery(ContextFactory);
+
// check if a set already exists with the same online id, delete if it does.
if (beatmapSet.OnlineBeatmapSetID != null)
{
diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs
index d809dbcb01..64428882ac 100644
--- a/osu.Game/Database/ArchiveModelManager.cs
+++ b/osu.Game/Database/ArchiveModelManager.cs
@@ -462,6 +462,8 @@ namespace osu.Game.Database
// Dereference the existing file info, since the file model will be removed.
if (file.FileInfo != null)
{
+ file.Requery(usage.Context);
+
Files.Dereference(file.FileInfo);
// This shouldn't be required, but here for safety in case the provided TModel is not being change tracked
@@ -635,10 +637,12 @@ namespace osu.Game.Database
{
using (Stream s = reader.GetStream(file))
{
+ var fileInfo = files.Add(s);
fileInfos.Add(new TFileModel
{
Filename = file.Substring(prefix.Length).ToStandardisedPath(),
- FileInfo = files.Add(s)
+ FileInfo = fileInfo,
+ FileInfoID = fileInfo.ID // workaround for efcore 5 compatibility.
});
}
}
diff --git a/osu.Game/Database/DatabaseWorkaroundExtensions.cs b/osu.Game/Database/DatabaseWorkaroundExtensions.cs
new file mode 100644
index 0000000000..a3a982f232
--- /dev/null
+++ b/osu.Game/Database/DatabaseWorkaroundExtensions.cs
@@ -0,0 +1,71 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using osu.Game.Beatmaps;
+using osu.Game.Scoring;
+using osu.Game.Skinning;
+
+namespace osu.Game.Database
+{
+ ///
+ /// Extension methods which contain workarounds to make EFcore 5.x work with our existing (incorrect) thread safety.
+ /// The intention is to avoid blocking package updates while we consider the future of the database backend, with a potential backend switch imminent.
+ ///
+ public static class DatabaseWorkaroundExtensions
+ {
+ ///
+ /// Re-query the provided model to ensure it is in a sane state. This method requires explicit implementation per model type.
+ ///
+ ///
+ ///
+ public static void Requery(this IHasPrimaryKey model, IDatabaseContextFactory contextFactory)
+ {
+ switch (model)
+ {
+ case SkinInfo skinInfo:
+ requeryFiles(skinInfo.Files, contextFactory);
+ break;
+
+ case ScoreInfo scoreInfo:
+ requeryFiles(scoreInfo.Beatmap.BeatmapSet.Files, contextFactory);
+ requeryFiles(scoreInfo.Files, contextFactory);
+ break;
+
+ case BeatmapSetInfo beatmapSetInfo:
+ var context = contextFactory.Get();
+
+ foreach (var beatmap in beatmapSetInfo.Beatmaps)
+ {
+ // Workaround System.InvalidOperationException
+ // The instance of entity type 'RulesetInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked.
+ beatmap.Ruleset = context.RulesetInfo.Find(beatmap.RulesetID);
+ }
+
+ requeryFiles(beatmapSetInfo.Files, contextFactory);
+ break;
+
+ default:
+ throw new ArgumentException($"{nameof(Requery)} does not have support for the provided model type", nameof(model));
+ }
+
+ void requeryFiles(List files, IDatabaseContextFactory databaseContextFactory) where T : class, INamedFileInfo
+ {
+ var dbContext = databaseContextFactory.Get();
+
+ foreach (var file in files)
+ {
+ Requery(file, dbContext);
+ }
+ }
+ }
+
+ public static void Requery(this INamedFileInfo file, OsuDbContext dbContext)
+ {
+ // Workaround System.InvalidOperationException
+ // The instance of entity type 'FileInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked.
+ file.FileInfo = dbContext.FileInfo.Find(file.FileInfoID);
+ }
+ }
+}
diff --git a/osu.Game/Database/OsuDbContext.cs b/osu.Game/Database/OsuDbContext.cs
index 2aae62edea..2342ab07d4 100644
--- a/osu.Game/Database/OsuDbContext.cs
+++ b/osu.Game/Database/OsuDbContext.cs
@@ -3,7 +3,6 @@
using System;
using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Logging;
using osu.Framework.Logging;
using osu.Framework.Statistics;
@@ -111,10 +110,10 @@ namespace osu.Game.Database
{
base.OnConfiguring(optionsBuilder);
optionsBuilder
- // this is required for the time being due to the way we are querying in places like BeatmapStore.
- // if we ever move to having consumers file their own .Includes, or get eager loading support, this could be re-enabled.
- .ConfigureWarnings(warnings => warnings.Ignore(CoreEventId.IncludeIgnoredWarning))
- .UseSqlite(connectionString, sqliteOptions => sqliteOptions.CommandTimeout(10))
+ .UseSqlite(connectionString,
+ sqliteOptions => sqliteOptions
+ .CommandTimeout(10)
+ .UseQuerySplittingBehavior(QuerySplittingBehavior.SingleQuery))
.UseLoggerFactory(logger.Value);
}
diff --git a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs
index 2925107766..662f55317b 100644
--- a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs
+++ b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs
@@ -138,7 +138,6 @@ namespace osu.Game.Overlays.Profile.Header
if (!string.IsNullOrEmpty(user.Twitter))
anyInfoAdded |= tryAddInfo(FontAwesome.Brands.Twitter, "@" + user.Twitter, $@"https://twitter.com/{user.Twitter}");
anyInfoAdded |= tryAddInfo(FontAwesome.Brands.Discord, user.Discord);
- anyInfoAdded |= tryAddInfo(FontAwesome.Brands.Skype, user.Skype, @"skype:" + user.Skype + @"?chat");
anyInfoAdded |= tryAddInfo(FontAwesome.Solid.Link, websiteWithoutProtocol, user.Website);
// If no information was added to the bottomLinkContainer, hide it to avoid unwanted padding
diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs
index f5192f3a40..78101991f6 100644
--- a/osu.Game/Scoring/ScoreInfo.cs
+++ b/osu.Game/Scoring/ScoreInfo.cs
@@ -73,7 +73,7 @@ namespace osu.Game.Scoring
}
set
{
- modsJson = null;
+ modsJson = JsonConvert.SerializeObject(value.Select(m => new DeserializedMod { Acronym = m.Acronym }));
mods = value;
}
}
@@ -86,16 +86,7 @@ namespace osu.Game.Scoring
[Column("Mods")]
public string ModsJson
{
- get
- {
- if (modsJson != null)
- return modsJson;
-
- if (mods == null)
- return null;
-
- return modsJson = JsonConvert.SerializeObject(mods.Select(m => new DeserializedMod { Acronym = m.Acronym }));
- }
+ get => modsJson;
set
{
modsJson = value;
diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs
index 96ec9644b5..1e90ee1ac7 100644
--- a/osu.Game/Scoring/ScoreManager.cs
+++ b/osu.Game/Scoring/ScoreManager.cs
@@ -52,6 +52,11 @@ namespace osu.Game.Scoring
this.configManager = configManager;
}
+ protected override void PreImport(ScoreInfo model)
+ {
+ model.Requery(ContextFactory);
+ }
+
protected override ScoreInfo CreateModel(ArchiveReader archive)
{
if (archive == null)
diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs
index fcde9f041b..601b77e782 100644
--- a/osu.Game/Skinning/SkinManager.cs
+++ b/osu.Game/Skinning/SkinManager.cs
@@ -142,6 +142,11 @@ namespace osu.Game.Skinning
}
}
+ protected override void PreImport(SkinInfo model)
+ {
+ model.Requery(ContextFactory);
+ }
+
///
/// Retrieve a instance for the provided
///
diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs
index 4d537b91bd..6c45417db0 100644
--- a/osu.Game/Users/User.cs
+++ b/osu.Game/Users/User.cs
@@ -111,9 +111,6 @@ namespace osu.Game.Users
[JsonProperty(@"twitter")]
public string Twitter;
- [JsonProperty(@"skype")]
- public string Skype;
-
[JsonProperty(@"discord")]
public string Discord;
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 90c8b98f42..fa1b0a95c3 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -24,10 +24,10 @@
-
-
+
+
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index ccd33bf88c..71fcdd45f3 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -90,8 +90,6 @@
-
-