diff --git a/.editorconfig b/.editorconfig
index 3817635cbdf81..4f057cf46bbdc 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -225,6 +225,10 @@ dotnet_style_qualification_for_property = false:warning
 dotnet_style_readonly_field = true:warning
 dotnet_style_require_accessibility_modifiers = always:warning
 
+[ArchiSteamFarm/**.cs]
+# ASF project includes plugin system, therefore CA1515 typically doesn't make sense there
+dotnet_diagnostic.CA1515.severity = silent
+
 ###############################
 # JetBrains, IntelliJ/Rider   #
 ###############################
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 46c8e6af40e7d..e056e3b6b08c2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -5,7 +5,7 @@ on: [push, pull_request]
 env:
   DOTNET_CLI_TELEMETRY_OPTOUT: true
   DOTNET_NOLOGO: true
-  DOTNET_SDK_VERSION: 8.0
+  DOTNET_SDK_VERSION: 9.0
 
 permissions: {}
 
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 59b43baca86b0..e9a5a37ad0ad8 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -6,7 +6,7 @@ env:
   CONFIGURATION: Release
   DOTNET_CLI_TELEMETRY_OPTOUT: true
   DOTNET_NOLOGO: true
-  DOTNET_SDK_VERSION: 8.0
+  DOTNET_SDK_VERSION: 9.0
   NODE_JS_VERSION: 'lts/*'
   PLUGINS_BUNDLED: ArchiSteamFarm.OfficialPlugins.ItemsMatcher ArchiSteamFarm.OfficialPlugins.MobileAuthenticator ArchiSteamFarm.OfficialPlugins.SteamTokenDumper
   PLUGINS_INCLUDED: ArchiSteamFarm.OfficialPlugins.Monitoring # Apart from declaring them here, there is certain amount of hardcoding needed below for uploading
diff --git a/ArchiSteamFarm/Program.cs b/ArchiSteamFarm/Program.cs
index bef84fd064d7e..1828d933a8a73 100644
--- a/ArchiSteamFarm/Program.cs
+++ b/ArchiSteamFarm/Program.cs
@@ -432,8 +432,6 @@ private static async Task<bool> InitGlobalDatabaseAndServices() {
 			}
 		}
 
-		WebBrowser.Init();
-
 		return true;
 	}
 
diff --git a/ArchiSteamFarm/Steam/Bot.cs b/ArchiSteamFarm/Steam/Bot.cs
index 6d5f2bd6eb895..b2d3e25cd34c3 100644
--- a/ArchiSteamFarm/Steam/Bot.cs
+++ b/ArchiSteamFarm/Steam/Bot.cs
@@ -3430,9 +3430,7 @@ private async void OnPersonaState(SteamFriends.PersonaStateCallback callback) {
 		string? avatarHash = null;
 
 		if ((callback.AvatarHash?.Length > 0) && callback.AvatarHash.Any(static singleByte => singleByte > 0)) {
-#pragma warning disable CA1308 // False positive, we're intentionally converting this part to lowercase and it's not used for any security decisions based on the result of the normalization
-			avatarHash = Convert.ToHexString(callback.AvatarHash).ToLowerInvariant();
-#pragma warning restore CA1308 // False positive, we're intentionally converting this part to lowercase and it's not used for any security decisions based on the result of the normalization
+			avatarHash = Convert.ToHexStringLower(callback.AvatarHash);
 
 			if (string.IsNullOrEmpty(avatarHash) || avatarHash.All(static singleChar => singleChar == '0')) {
 				avatarHash = null;
diff --git a/ArchiSteamFarm/Steam/Integration/ArchiWebHandler.cs b/ArchiSteamFarm/Steam/Integration/ArchiWebHandler.cs
index 75ee3aeb81e3b..48ecc8bcdc644 100644
--- a/ArchiSteamFarm/Steam/Integration/ArchiWebHandler.cs
+++ b/ArchiSteamFarm/Steam/Integration/ArchiWebHandler.cs
@@ -1995,9 +1995,7 @@ internal async Task<bool> Init(ulong steamID, EUniverse universe, string accessT
 
 		Initialized = false;
 
-#pragma warning disable CA1308 // False positive, we're intentionally converting this part to lowercase and it's not used for any security decisions based on the result of the normalization
-		string sessionID = Convert.ToHexString(RandomNumberGenerator.GetBytes(SessionIDLength / 2)).ToLowerInvariant();
-#pragma warning restore CA1308 // False positive, we're intentionally converting this part to lowercase and it's not used for any security decisions based on the result of the normalization
+		string sessionID = Convert.ToHexStringLower(RandomNumberGenerator.GetBytes(SessionIDLength / 2));
 
 		WebBrowser.CookieContainer.Add(new Cookie("sessionid", sessionID, "/", $".{SteamCheckoutURL.Host}"));
 		WebBrowser.CookieContainer.Add(new Cookie("sessionid", sessionID, "/", $".{SteamCommunityURL.Host}"));
diff --git a/ArchiSteamFarm/Web/WebBrowser.cs b/ArchiSteamFarm/Web/WebBrowser.cs
index 1bdbac87078e5..01680e1486253 100644
--- a/ArchiSteamFarm/Web/WebBrowser.cs
+++ b/ArchiSteamFarm/Web/WebBrowser.cs
@@ -682,20 +682,6 @@ public HttpClient GenerateDisposableHttpClient(bool extendedTimeout = false) {
 		return null;
 	}
 
-	internal static void Init() {
-		// Set max connection limit from default of 2 to desired value
-		ServicePointManager.DefaultConnectionLimit = MaxConnections;
-
-		// Set max idle time from default of 100 seconds (100 * 1000) to desired value
-		ServicePointManager.MaxServicePointIdleTime = MaxIdleTime * 1000;
-
-		// Don't use Expect100Continue, we're sure about our POSTs, save some TCP packets
-		ServicePointManager.Expect100Continue = false;
-
-		// Reuse ports if possible
-		ServicePointManager.ReusePort = true;
-	}
-
 	private async Task<HttpResponseMessage?> InternalGet(Uri request, IReadOnlyCollection<KeyValuePair<string, string>>? headers = null, Uri? referer = null, ERequestOptions requestOptions = ERequestOptions.None, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead, CancellationToken cancellationToken = default) {
 		ArgumentNullException.ThrowIfNull(request);
 
diff --git a/Directory.Build.props b/Directory.Build.props
index 00a5bdedccdf5..b3ed00f3fa0e3 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -29,7 +29,7 @@
 		<RepositoryUrl>$(PackageProjectUrl).git</RepositoryUrl>
 		<RollForward>LatestMajor</RollForward>
 		<RuntimeIdentifiers>linux-arm;linux-arm64;linux-x64;osx-arm64;osx-x64;win-arm64;win-x64</RuntimeIdentifiers>
-		<TargetFramework>net8.0</TargetFramework>
+		<TargetFramework>net9.0</TargetFramework>
 		<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
 	</PropertyGroup>
 
@@ -84,5 +84,8 @@
 		<StackTraceSupport>false</StackTraceSupport>
 		<UseNativeHttpHandler>true</UseNativeHttpHandler>
 		<TrimMode>partial</TrimMode>
+
+		<!-- TODO: Perhaps can be removed with stable release of .NET 9 -->
+		<_DefaultValueAttributeSupport>true</_DefaultValueAttributeSupport>
 	</PropertyGroup>
 </Project>
diff --git a/Dockerfile b/Dockerfile
index c0f085d5005d5..305834f8489b4 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -15,7 +15,7 @@ RUN <<EOF
     npm run deploy --no-progress
 EOF
 
-FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0${IMAGESUFFIX} AS build-dotnet
+FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:9.0${IMAGESUFFIX} AS build-dotnet
 ARG CONFIGURATION=Release
 ARG TARGETARCH
 ARG TARGETOS
@@ -76,7 +76,7 @@ RUN --mount=type=secret,id=ASF_PRIVATE_SNK --mount=type=secret,id=STEAM_TOKEN_DU
     done
 EOF
 
-FROM mcr.microsoft.com/dotnet/aspnet:8.0${IMAGESUFFIX} AS runtime
+FROM mcr.microsoft.com/dotnet/aspnet:9.0${IMAGESUFFIX} AS runtime
 ENV ASF_PATH=/app
 ENV ASF_USER=asf
 ENV ASPNETCORE_URLS=
diff --git a/Dockerfile.Service b/Dockerfile.Service
index 883380beb6b69..595b7b81eef72 100644
--- a/Dockerfile.Service
+++ b/Dockerfile.Service
@@ -15,7 +15,7 @@ RUN <<EOF
     npm run deploy --no-progress
 EOF
 
-FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0${IMAGESUFFIX} AS build-dotnet
+FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:9.0${IMAGESUFFIX} AS build-dotnet
 ARG CONFIGURATION=Release
 ARG TARGETARCH
 ARG TARGETOS
@@ -76,7 +76,7 @@ RUN --mount=type=secret,id=ASF_PRIVATE_SNK --mount=type=secret,id=STEAM_TOKEN_DU
     done
 EOF
 
-FROM mcr.microsoft.com/dotnet/runtime-deps:8.0${IMAGESUFFIX} AS runtime
+FROM mcr.microsoft.com/dotnet/runtime-deps:9.0${IMAGESUFFIX} AS runtime
 ENV ASF_PATH=/app
 ENV ASF_USER=asf
 ENV ASPNETCORE_URLS=