mirror of
https://github.com/FalloutCollaborationProject/FCP-Mod-Updater.git
synced 2026-07-27 17:04:05 -07:00
Add Dependency Injection for Future Updates
This commit is contained in:
+5
-12
@@ -6,33 +6,26 @@ using Spectre.Console.Cli;
|
||||
|
||||
namespace FCPModUpdater.Commands;
|
||||
|
||||
public class ScanCommand : AsyncCommand<ModPathSettings>
|
||||
public class ScanCommand(
|
||||
IGitService gitService,
|
||||
IGitHubApiService gitHubApiService,
|
||||
IModDiscoveryService modDiscoveryService,
|
||||
UpdateCheckService updateCheckService) : AsyncCommand<ModPathSettings>
|
||||
{
|
||||
public override async Task<int> ExecuteAsync(CommandContext context, ModPathSettings settings,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Resolve mods directory
|
||||
var modsDirectory = ModsDirectoryResolver.Resolve(settings.ModDirectory?.FullName, interactive: true);
|
||||
if (modsDirectory == null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
AnsiConsole.MarkupLine($"[grey]Using mods directory: {modsDirectory}[/]");
|
||||
AnsiConsole.WriteLine();
|
||||
|
||||
// Initialize services
|
||||
var gitService = new GitService();
|
||||
var gitHubApiService = new GitHubApiService();
|
||||
var modDiscoveryService = new ModDiscoveryService(gitService, gitHubApiService);
|
||||
|
||||
// Start update check in background (non-blocking)
|
||||
var updateCheckService = new UpdateCheckService(gitHubApiService.HttpClient);
|
||||
var updateCheckTask = updateCheckService.CheckForUpdateAsync(cancellationToken);
|
||||
|
||||
// Run interactive menu
|
||||
var menu = new InteractiveMenu(
|
||||
gitService,
|
||||
gitHubApiService,
|
||||
|
||||
@@ -9,7 +9,10 @@ using Spectre.Console.Cli;
|
||||
namespace FCPModUpdater.Commands;
|
||||
|
||||
[UsedImplicitly]
|
||||
public class UpdateCommand : AsyncCommand<ModPathSettings>
|
||||
public class UpdateCommand(
|
||||
IGitService gitService,
|
||||
IModDiscoveryService modDiscoveryService,
|
||||
UpdateCheckService updateCheckService) : AsyncCommand<ModPathSettings>
|
||||
{
|
||||
public static IReadOnlyList<InstalledMod> GetUpdateableMods(IReadOnlyList<InstalledMod> mods)
|
||||
=> mods.Where(m => m.Source == ModSource.Git && m.Status == ModStatus.Behind).ToList();
|
||||
@@ -19,31 +22,19 @@ public class UpdateCommand : AsyncCommand<ModPathSettings>
|
||||
{
|
||||
try
|
||||
{
|
||||
// Resolve mods directory
|
||||
var modsDirectory = ModsDirectoryResolver.Resolve(settings.ModDirectory?.FullName, interactive: false);
|
||||
if (modsDirectory == null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
AnsiConsole.MarkupLine($"[grey]Using mods directory: {modsDirectory}[/]");
|
||||
AnsiConsole.WriteLine();
|
||||
|
||||
// Initialize services
|
||||
var gitService = new GitService();
|
||||
var gitHubApiService = new GitHubApiService();
|
||||
var modDiscoveryService = new ModDiscoveryService(gitService, gitHubApiService);
|
||||
|
||||
// Start update check in background (non-blocking)
|
||||
var updateCheckService = new UpdateCheckService(gitHubApiService.HttpClient);
|
||||
var updateCheckTask = updateCheckService.CheckForUpdateAsync(cancellationToken);
|
||||
|
||||
// Discover mods
|
||||
var mods = await ProgressReporter.WithStatusAsync(
|
||||
"Scanning mods directory...",
|
||||
async () => await modDiscoveryService.DiscoverModsAsync(modsDirectory, ct: cancellationToken));
|
||||
|
||||
// Find updateable mods
|
||||
var updateableMods = GetUpdateableMods(mods);
|
||||
|
||||
if (updateableMods.Count == 0)
|
||||
@@ -60,7 +51,6 @@ public class UpdateCommand : AsyncCommand<ModPathSettings>
|
||||
|
||||
AnsiConsole.WriteLine();
|
||||
|
||||
// Update all
|
||||
var results = await ProgressReporter.WithBatchProgressAsync(
|
||||
"Updating mods",
|
||||
updateableMods,
|
||||
@@ -81,7 +71,6 @@ public class UpdateCommand : AsyncCommand<ModPathSettings>
|
||||
|
||||
ModTableRenderer.RenderUpdateSummary(results);
|
||||
|
||||
// Show update notification if available
|
||||
var updateResult = await updateCheckTask;
|
||||
if (updateResult != null)
|
||||
{
|
||||
|
||||
@@ -9,14 +9,14 @@ public class GitHubApiServiceTests : IDisposable
|
||||
public GitHubApiServiceTests()
|
||||
{
|
||||
_handler = new MockHttpMessageHandler();
|
||||
_httpClient = new HttpClient(_handler);
|
||||
_httpClient = new HttpClient(_handler) { BaseAddress = new Uri("https://api.github.com") };
|
||||
_service = new GitHubApiService(_httpClient);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
_service.Dispose();
|
||||
_httpClient.Dispose();
|
||||
}
|
||||
|
||||
private static RemoteRepo MakeRepo(string name, List<string>? topics = null) =>
|
||||
@@ -176,12 +176,11 @@ public class GitHubApiServiceTests : IDisposable
|
||||
// Since cache is still fresh, this will return cached anyway
|
||||
// Let's just verify network error on fresh service
|
||||
var handler2 = new MockHttpMessageHandler { ThrowOnSend = true };
|
||||
var client2 = new HttpClient(handler2);
|
||||
var client2 = new HttpClient(handler2) { BaseAddress = new Uri("https://api.github.com") };
|
||||
var service2 = new GitHubApiService(client2);
|
||||
|
||||
IReadOnlyList<RemoteRepo> result = await service2.GetOrganizationReposAsync(token);
|
||||
Assert.Empty(result);
|
||||
service2.Dispose();
|
||||
client2.Dispose();
|
||||
}
|
||||
|
||||
@@ -191,12 +190,11 @@ public class GitHubApiServiceTests : IDisposable
|
||||
CancellationToken token = TestContext.Current.CancellationToken;
|
||||
|
||||
var handler = new MockHttpMessageHandler { ThrowOnSend = true };
|
||||
var client = new HttpClient(handler);
|
||||
var client = new HttpClient(handler) { BaseAddress = new Uri("https://api.github.com") };
|
||||
var service = new GitHubApiService(client);
|
||||
|
||||
IReadOnlyList<RemoteRepo> result = await service.GetOrganizationReposAsync(token);
|
||||
Assert.Empty(result);
|
||||
service.Dispose();
|
||||
client.Dispose();
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CliWrap" Version="3.10.1" />
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2025.2.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.5" />
|
||||
<PackageReference Include="Spectre.Console" Version="0.54.0" />
|
||||
<PackageReference Include="Spectre.Console.Cli" Version="0.53.1" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Spectre.Console.Cli;
|
||||
|
||||
namespace FCPModUpdater.Infrastructure;
|
||||
|
||||
// Used to hook up Spectre.Cli to Dependency Injection
|
||||
|
||||
public sealed class TypeRegistrar(IServiceCollection services) : ITypeRegistrar
|
||||
{
|
||||
public ITypeResolver Build() => new TypeResolver(services.BuildServiceProvider());
|
||||
public void Register(Type service, Type implementation) => services.AddSingleton(service, implementation);
|
||||
public void RegisterInstance(Type service, object implementation) => services.AddSingleton(service, implementation);
|
||||
public void RegisterLazy(Type service, Func<object> factory) => services.AddSingleton(service, _ => factory());
|
||||
}
|
||||
|
||||
internal sealed class TypeResolver(IServiceProvider provider) : ITypeResolver, IDisposable
|
||||
{
|
||||
public object? Resolve(Type? type) => type is null ? null : provider.GetService(type);
|
||||
public void Dispose() => (provider as IDisposable)?.Dispose();
|
||||
}
|
||||
+24
-2
@@ -1,10 +1,22 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Runtime.InteropServices;
|
||||
using FCPModUpdater;
|
||||
using FCPModUpdater.Commands;
|
||||
using FCPModUpdater.Infrastructure;
|
||||
using FCPModUpdater.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Spectre.Console;
|
||||
using Spectre.Console.Cli;
|
||||
|
||||
var app = new CommandApp();
|
||||
var services = new ServiceCollection();
|
||||
|
||||
services.AddHttpClient<IGitHubApiService, GitHubApiService>(ConfigureGitHubClient);
|
||||
services.AddHttpClient<UpdateCheckService>(ConfigureGitHubClient);
|
||||
|
||||
services.AddSingleton<IGitService, GitService>();
|
||||
services.AddSingleton<IModDiscoveryService, ModDiscoveryService>();
|
||||
|
||||
var app = new CommandApp(new TypeRegistrar(services));
|
||||
|
||||
app.Configure(config =>
|
||||
{
|
||||
@@ -21,7 +33,6 @@ app.Configure(config =>
|
||||
.WithDescription("Update all FCP mods (non-interactive)")
|
||||
.WithExample("update")
|
||||
.WithExample("update", "--directory", "/path/to/RimWorld/Mods");
|
||||
|
||||
});
|
||||
|
||||
app.SetDefaultCommand<ScanCommand>();
|
||||
@@ -37,3 +48,14 @@ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
void ConfigureGitHubClient(HttpClient client)
|
||||
{
|
||||
client.BaseAddress = new Uri("https://api.github.com");
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd($"FCPModUpdater/{AppVersion.SemanticVersion}");
|
||||
client.DefaultRequestHeaders.Accept.ParseAdd("application/vnd.github+json");
|
||||
|
||||
var token = Environment.GetEnvironmentVariable("GITHUB_TOKEN");
|
||||
if (!string.IsNullOrEmpty(token))
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
}
|
||||
|
||||
@@ -3,36 +3,20 @@ using FCPModUpdater.Models;
|
||||
|
||||
namespace FCPModUpdater.Services;
|
||||
|
||||
public class GitHubApiService : IGitHubApiService, IDisposable
|
||||
public class GitHubApiService(HttpClient httpClient) : IGitHubApiService
|
||||
{
|
||||
private const string OrgName = "FalloutCollaborationProject";
|
||||
private const string BaseUrl = "https://api.github.com";
|
||||
private const string RequiredTopic = "rimworld-mod";
|
||||
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly HttpClient _httpClient = httpClient;
|
||||
private readonly TimeSpan _cacheExpiry = TimeSpan.FromHours(1);
|
||||
|
||||
private IReadOnlyList<RemoteRepo>? _cachedRepos;
|
||||
private DateTimeOffset _cacheTime = DateTimeOffset.MinValue;
|
||||
|
||||
public HttpClient HttpClient => _httpClient;
|
||||
public int? RemainingRateLimit { get; private set; }
|
||||
public DateTimeOffset? RateLimitReset { get; private set; }
|
||||
|
||||
public GitHubApiService(HttpClient? httpClient = null)
|
||||
{
|
||||
_httpClient = httpClient ?? new HttpClient();
|
||||
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd($"FCPModUpdater/{AppVersion.SemanticVersion}");
|
||||
_httpClient.DefaultRequestHeaders.Accept.ParseAdd("application/vnd.github+json");
|
||||
|
||||
var token = Environment.GetEnvironmentVariable("GITHUB_TOKEN");
|
||||
if (!string.IsNullOrEmpty(token))
|
||||
{
|
||||
_httpClient.DefaultRequestHeaders.Authorization =
|
||||
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<RemoteRepo>> GetOrganizationReposAsync(CancellationToken ct = default)
|
||||
{
|
||||
if (_cachedRepos != null && DateTimeOffset.UtcNow - _cacheTime < _cacheExpiry)
|
||||
@@ -46,7 +30,7 @@ public class GitHubApiService : IGitHubApiService, IDisposable
|
||||
|
||||
while (true)
|
||||
{
|
||||
var url = $"{BaseUrl}/orgs/{OrgName}/repos?per_page={perPage}&page={page}";
|
||||
var url = $"orgs/{OrgName}/repos?per_page={perPage}&page={page}";
|
||||
|
||||
try
|
||||
{
|
||||
@@ -120,9 +104,4 @@ public class GitHubApiService : IGitHubApiService, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_httpClient.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,18 +3,10 @@ using FCPModUpdater.Models;
|
||||
|
||||
namespace FCPModUpdater.Services;
|
||||
|
||||
public class UpdateCheckService
|
||||
public class UpdateCheckService(HttpClient httpClient)
|
||||
{
|
||||
private const string RepoOwner = "FalloutCollaborationProject";
|
||||
private const string RepoName = "FCP-Mod-Updater";
|
||||
private const string BaseUrl = "https://api.github.com";
|
||||
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public UpdateCheckService(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a newer version is available. Returns null if check fails or no update needed.
|
||||
@@ -24,8 +16,8 @@ public class UpdateCheckService
|
||||
{
|
||||
try
|
||||
{
|
||||
var url = $"{BaseUrl}/repos/{RepoOwner}/{RepoName}/releases?per_page=15";
|
||||
var releases = await _httpClient.GetFromJsonAsync<List<ReleaseInfo>>(url, ct);
|
||||
var url = $"repos/{RepoOwner}/{RepoName}/releases?per_page=15";
|
||||
var releases = await httpClient.GetFromJsonAsync<List<ReleaseInfo>>(url, ct);
|
||||
|
||||
if (releases is null || releases.Count == 0)
|
||||
return null;
|
||||
@@ -34,13 +26,13 @@ public class UpdateCheckService
|
||||
ReleaseInfo? newest = null;
|
||||
Version? newestVersion = null;
|
||||
|
||||
foreach (var release in releases)
|
||||
foreach (ReleaseInfo release in releases)
|
||||
{
|
||||
if (release.Draft)
|
||||
continue;
|
||||
|
||||
var tagBase = GetBaseVersion(release.TagName.TrimStart('v'));
|
||||
if (!Version.TryParse(tagBase, out var ver))
|
||||
if (!Version.TryParse(tagBase, out Version? ver))
|
||||
continue;
|
||||
|
||||
if (newestVersion is null || ver > newestVersion)
|
||||
@@ -54,7 +46,7 @@ public class UpdateCheckService
|
||||
return null;
|
||||
|
||||
var currentBase = GetBaseVersion(AppVersion.SemanticVersion);
|
||||
if (!Version.TryParse(currentBase, out var currentVer))
|
||||
if (!Version.TryParse(currentBase, out Version? currentVer))
|
||||
return null;
|
||||
|
||||
if (newestVersion > currentVer)
|
||||
|
||||
@@ -14,6 +14,29 @@
|
||||
"resolved": "2025.2.4",
|
||||
"contentHash": "TwbgxAkXxY+vNEhNVx/QXjJ4vqxmepOjsgRvvImQPbHkHMMb4W+ahL3laMsxXKtNT7iMy+E1B3xkqao2hf1n3A=="
|
||||
},
|
||||
"Microsoft.Extensions.DependencyInjection": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.5, )",
|
||||
"resolved": "10.0.5",
|
||||
"contentHash": "v1SVsowG6YE1YnHVGmLWz57YTRCQRx9pH5ebIESXfm5isI9gA3QaMyg/oMTzPpXYZwSAVDzYItGJKfmV+pqXkQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Http": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.5, )",
|
||||
"resolved": "10.0.5",
|
||||
"contentHash": "AiFvHYM8nP0wPC7bGPI3NHQlSYSLqjjT7DMJUuuxhd+7pz3O89iu2gdQfgACy5DxsXENiok5i1bMacJL7KR8jA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
|
||||
"Microsoft.Extensions.Diagnostics": "10.0.5",
|
||||
"Microsoft.Extensions.Logging": "10.0.5",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "10.0.5",
|
||||
"Microsoft.Extensions.Options": "10.0.5"
|
||||
}
|
||||
},
|
||||
"Spectre.Console": {
|
||||
"type": "Direct",
|
||||
"requested": "[0.54.0, )",
|
||||
@@ -28,6 +51,100 @@
|
||||
"dependencies": {
|
||||
"Spectre.Console": "0.53.1"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.5",
|
||||
"contentHash": "8Rx5sqg04FttxrumyG6bmoRuFRgYzK6IVwF1i0/o0cXfKBdDeVpJejKHtJCMjyg9E/DNMVqpqOGe/tCT5gYvVA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
|
||||
"Microsoft.Extensions.Primitives": "10.0.5"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.5",
|
||||
"contentHash": "P09QpTHjqHmCLQOTC+WyLkoRNxek4NIvfWt+TnU0etoDUSRxcltyd6+j/ouRbMdLR0j44GqGO+lhI2M4fAHG4g==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Primitives": "10.0.5"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.Binder": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.5",
|
||||
"contentHash": "99Z4rjyXopb1MIazDSPcvwYCUdYNO01Cf1GUs2WUjIFAbkGmwzj2vPa2k+3pheJRV+YgNd2QqRKHAri0oBAU4Q==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration": "10.0.5",
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "10.0.5"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.5",
|
||||
"contentHash": "iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA=="
|
||||
},
|
||||
"Microsoft.Extensions.Diagnostics": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.5",
|
||||
"contentHash": "vAJHd4yOpmKoK+jBuYV7a3y+Ab9U4ARCc29b6qvMy276RgJFw9LFs0DdsPqOL3ahwzyrX7tM+i4cCxU/RX0qAg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration": "10.0.5",
|
||||
"Microsoft.Extensions.Diagnostics.Abstractions": "10.0.5",
|
||||
"Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.5"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Diagnostics.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.5",
|
||||
"contentHash": "/nYGrpa9/0BZofrVpBbbj+Ns8ZesiPE0V/KxsuHgDgHQopIzN54nRaQGSuvPw16/kI9sW1Zox5yyAPqvf0Jz6A==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
|
||||
"Microsoft.Extensions.Options": "10.0.5"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Logging": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.5",
|
||||
"contentHash": "+XTMKQyDWg4ODoNHU/BN3BaI1jhGO7VCS+BnzT/4IauiG6y2iPAte7MyD7rHKS+hNP0TkFkjrae8DFjDUxtcxg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "10.0.5",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "10.0.5",
|
||||
"Microsoft.Extensions.Options": "10.0.5"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Logging.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.5",
|
||||
"contentHash": "9HOdqlDtPptVcmKAjsQ/Nr5Rxfq6FMYLdhvZh1lVmeKR738qeYecQD7+ldooXf+u2KzzR1kafSphWngIM3C6ug==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Options": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.5",
|
||||
"contentHash": "MDaQMdUplw0AIRhWWmbLA7yQEXaLIHb+9CTroTiNS8OlI0LMXS4LCxtopqauiqGCWlRgJ+xyraVD8t6veRAFbw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
|
||||
"Microsoft.Extensions.Primitives": "10.0.5"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Options.ConfigurationExtensions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.5",
|
||||
"contentHash": "BB9uUW3+6Rxu1R97OB1H/13lUF8P2+H1+eDhpZlK30kDh/6E4EKHBUqTp+ilXQmZLzsRErxON8aBSR6WpUKJdg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
|
||||
"Microsoft.Extensions.Configuration.Binder": "10.0.5",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
|
||||
"Microsoft.Extensions.Options": "10.0.5",
|
||||
"Microsoft.Extensions.Primitives": "10.0.5"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Primitives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.5",
|
||||
"contentHash": "/HUHJ0tw/LQvD0DZrz50eQy/3z7PfX7WWEaXnjKTV9/TNdcgFlNTZGo49QhS7PTmhDqMyHRMqAXSBxLh0vso4g=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user