分离和复用HttpClient
This commit is contained in:
parent
ec06b11d7e
commit
82a816e40d
@ -8,12 +8,12 @@ using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FastGithub.ReverseProxy
|
||||
namespace FastGithub.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// 域名解析器
|
||||
/// </summary>
|
||||
sealed class DomainResolver
|
||||
sealed class DomainResolver : IDomainResolver
|
||||
{
|
||||
private readonly IMemoryCache memoryCache;
|
||||
private readonly FastGithubConfig fastGithubConfig;
|
||||
14
FastGithub.Http/FastGithub.Http.csproj
Normal file
14
FastGithub.Http/FastGithub.Http.csproj
Normal file
@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DNS" Version="6.1.0" />
|
||||
<PackageReference Include="Portable.BouncyCastle" Version="1.8.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" />
|
||||
<ProjectReference Include="..\FastGithub.Core\FastGithub.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
56
FastGithub.Http/HttpClient.cs
Normal file
56
FastGithub.Http/HttpClient.cs
Normal file
@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FastGithub.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// 表示http客户端
|
||||
/// </summary>
|
||||
public class HttpClient : HttpMessageInvoker
|
||||
{
|
||||
private readonly DomainConfig domainConfig;
|
||||
|
||||
/// <summary>
|
||||
/// http客户端
|
||||
/// </summary>
|
||||
/// <param name="domainConfig"></param>
|
||||
/// <param name="domainResolver"></param>
|
||||
public HttpClient(DomainConfig domainConfig, IDomainResolver domainResolver)
|
||||
: this(domainConfig, new HttpClientHandler(domainResolver), disposeHandler: true)
|
||||
{
|
||||
this.domainConfig = domainConfig;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// http客户端
|
||||
/// </summary>
|
||||
/// <param name="domainConfig"></param>
|
||||
/// <param name="handler"></param>
|
||||
/// <param name="disposeHandler"></param>
|
||||
internal HttpClient(DomainConfig domainConfig, HttpClientHandler handler, bool disposeHandler)
|
||||
: base(handler, disposeHandler)
|
||||
{
|
||||
this.domainConfig = domainConfig;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送数据
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
request.SetRequestContext(new RequestContext
|
||||
{
|
||||
Host = request.RequestUri?.Host,
|
||||
IsHttps = request.RequestUri?.Scheme == Uri.UriSchemeHttps,
|
||||
TlsSniPattern = this.domainConfig.GetTlsSniPattern(),
|
||||
TlsIgnoreNameMismatch = this.domainConfig.TlsIgnoreNameMismatch
|
||||
});
|
||||
return base.SendAsync(request, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
35
FastGithub.Http/HttpClientFactory.cs
Normal file
35
FastGithub.Http/HttpClientFactory.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace FastGithub.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// HttpClient工厂
|
||||
/// </summary>
|
||||
sealed class HttpClientFactory : IHttpClientFactory
|
||||
{
|
||||
private HttpClientHandler httpClientHanlder;
|
||||
|
||||
/// <summary>
|
||||
/// HttpClient工厂
|
||||
/// </summary>
|
||||
/// <param name="domainResolver"></param>
|
||||
/// <param name="options"></param>
|
||||
public HttpClientFactory(
|
||||
IDomainResolver domainResolver,
|
||||
IOptionsMonitor<FastGithubOptions> options)
|
||||
{
|
||||
this.httpClientHanlder = new HttpClientHandler(domainResolver);
|
||||
options.OnChange(opt => this.httpClientHanlder = new HttpClientHandler(domainResolver));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建httpClient
|
||||
/// </summary>
|
||||
/// <param name="domainConfig"></param>
|
||||
/// <returns></returns>
|
||||
public HttpClient CreateHttpClient(DomainConfig domainConfig)
|
||||
{
|
||||
return new HttpClient(domainConfig, this.httpClientHanlder, disposeHandler: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -9,20 +9,20 @@ using System.Security.Cryptography.X509Certificates;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FastGithub.ReverseProxy
|
||||
namespace FastGithub.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// YARP的HttpClientHandler
|
||||
/// HttpClientHandler
|
||||
/// </summary>
|
||||
class HttpClientHanlder : DelegatingHandler
|
||||
class HttpClientHandler : DelegatingHandler
|
||||
{
|
||||
private readonly DomainResolver domainResolver;
|
||||
private readonly IDomainResolver domainResolver;
|
||||
|
||||
/// <summary>
|
||||
/// YARP的HttpClientHandler
|
||||
/// HttpClientHandler
|
||||
/// </summary>
|
||||
/// <param name="domainResolver"></param>
|
||||
public HttpClientHanlder(DomainResolver domainResolver)
|
||||
public HttpClientHandler(IDomainResolver domainResolver)
|
||||
{
|
||||
this.domainResolver = domainResolver;
|
||||
this.InnerHandler = CreateSocketsHttpHandler();
|
||||
25
FastGithub.Http/HttpClientServiceCollectionExtensions.cs
Normal file
25
FastGithub.Http/HttpClientServiceCollectionExtensions.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using FastGithub.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace FastGithub
|
||||
{
|
||||
/// <summary>
|
||||
/// httpClient的服务注册扩展
|
||||
/// </summary>
|
||||
public static class HttpClientServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 添加HttpClient相关服务
|
||||
/// </summary>
|
||||
/// <param name="services"></param>
|
||||
/// <returns></returns>
|
||||
public static IServiceCollection AddHttpClient(this IServiceCollection services)
|
||||
{
|
||||
services.AddMemoryCache();
|
||||
services.TryAddSingleton<IDomainResolver, DomainResolver>();
|
||||
services.TryAddSingleton<IHttpClientFactory, HttpClientFactory>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
20
FastGithub.Http/IDomainResolver.cs
Normal file
20
FastGithub.Http/IDomainResolver.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FastGithub.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// 域名解析器
|
||||
/// </summary>
|
||||
public interface IDomainResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// 解析域名
|
||||
/// </summary>
|
||||
/// <param name="domain"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task<IPAddress> ResolveAsync(string domain, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
15
FastGithub.Http/IHttpClientFactory.cs
Normal file
15
FastGithub.Http/IHttpClientFactory.cs
Normal file
@ -0,0 +1,15 @@
|
||||
namespace FastGithub.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// httpClient工厂
|
||||
/// </summary>
|
||||
public interface IHttpClientFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建httpClient
|
||||
/// </summary>
|
||||
/// <param name="domainConfig"></param>
|
||||
/// <returns></returns>
|
||||
HttpClient CreateHttpClient(DomainConfig domainConfig);
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
namespace FastGithub.ReverseProxy
|
||||
namespace FastGithub.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// 表示请求上下文
|
||||
@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace FastGithub.ReverseProxy
|
||||
namespace FastGithub.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// 请求上下文扩展
|
||||
@ -6,13 +6,13 @@
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="DNS" Version="6.1.0" />
|
||||
<PackageReference Include="Portable.BouncyCastle" Version="1.8.10" />
|
||||
<PackageReference Include="Yarp.ReverseProxy" Version="1.0.0-preview.12.21328.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\FastGithub.Core\FastGithub.Core.csproj" />
|
||||
<ProjectReference Include="..\FastGithub.Http\FastGithub.Http.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@ -1,73 +0,0 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FastGithub.ReverseProxy
|
||||
{
|
||||
/// <summary>
|
||||
/// HttpClient工厂
|
||||
/// </summary>
|
||||
sealed class HttpClientFactory
|
||||
{
|
||||
private HttpClientHanlder httpClientHanlder;
|
||||
|
||||
/// <summary>
|
||||
/// HttpClient工厂
|
||||
/// </summary>
|
||||
/// <param name="domainResolver"></param>
|
||||
/// <param name="options"></param>
|
||||
public HttpClientFactory(
|
||||
DomainResolver domainResolver,
|
||||
IOptionsMonitor<FastGithubOptions> options)
|
||||
{
|
||||
this.httpClientHanlder = new HttpClientHanlder(domainResolver);
|
||||
options.OnChange(opt => this.httpClientHanlder = new HttpClientHanlder(domainResolver));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建httpClient
|
||||
/// </summary>
|
||||
/// <param name="domainConfig"></param>
|
||||
/// <returns></returns>
|
||||
public HttpMessageInvoker CreateHttpClient(DomainConfig domainConfig)
|
||||
{
|
||||
return new HttpClient(this.httpClientHanlder, domainConfig, disposeHandler: false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// http客户端
|
||||
/// </summary>
|
||||
private class HttpClient : HttpMessageInvoker
|
||||
{
|
||||
private readonly DomainConfig domainConfig;
|
||||
|
||||
public HttpClient(
|
||||
HttpMessageHandler handler,
|
||||
DomainConfig domainConfig,
|
||||
bool disposeHandler = false) : base(handler, disposeHandler)
|
||||
{
|
||||
this.domainConfig = domainConfig;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送数据
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
request.SetRequestContext(new RequestContext
|
||||
{
|
||||
Host = request.RequestUri?.Host,
|
||||
IsHttps = request.RequestUri?.Scheme == Uri.UriSchemeHttps,
|
||||
TlsSniPattern = this.domainConfig.GetTlsSniPattern(),
|
||||
TlsIgnoreNameMismatch = this.domainConfig.TlsIgnoreNameMismatch
|
||||
});
|
||||
return base.SendAsync(request, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using FastGithub.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
@ -12,13 +13,13 @@ namespace FastGithub.ReverseProxy
|
||||
sealed class ReverseProxyMiddleware
|
||||
{
|
||||
private readonly IHttpForwarder httpForwarder;
|
||||
private readonly HttpClientFactory httpClientFactory;
|
||||
private readonly IHttpClientFactory httpClientFactory;
|
||||
private readonly FastGithubConfig fastGithubConfig;
|
||||
private readonly ILogger<ReverseProxyMiddleware> logger;
|
||||
|
||||
public ReverseProxyMiddleware(
|
||||
IHttpForwarder httpForwarder,
|
||||
HttpClientFactory httpClientFactory,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
FastGithubConfig fastGithubConfig,
|
||||
ILogger<ReverseProxyMiddleware> logger)
|
||||
{
|
||||
|
||||
@ -18,8 +18,7 @@ namespace FastGithub
|
||||
return services
|
||||
.AddMemoryCache()
|
||||
.AddHttpForwarder()
|
||||
.AddSingleton<DomainResolver>()
|
||||
.AddTransient<HttpClientFactory>()
|
||||
.AddHttpClient()
|
||||
.AddSingleton<RequestLoggingMilldeware>()
|
||||
.AddSingleton<ReverseProxyMiddleware>();
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\FastGithub.Core\FastGithub.Core.csproj" />
|
||||
<ProjectReference Include="..\FastGithub.Http\FastGithub.Http.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
17
FastGithub.Upgrade/GithubRequestMessage.cs
Normal file
17
FastGithub.Upgrade/GithubRequestMessage.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace FastGithub.Upgrade
|
||||
{
|
||||
/// <summary>
|
||||
/// github请求消息
|
||||
/// </summary>
|
||||
class GithubRequestMessage : HttpRequestMessage
|
||||
{
|
||||
public GithubRequestMessage()
|
||||
{
|
||||
this.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("*/*"));
|
||||
this.Headers.UserAgent.Add(new ProductInfoHeaderValue(nameof(FastGithub), "1.0"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,46 +0,0 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FastGithub.Upgrade
|
||||
{
|
||||
/// <summary>
|
||||
/// 本机反向代理的httpHandler
|
||||
/// </summary>
|
||||
sealed class ReverseProxyHttpHandler : DelegatingHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// 本机反向代理的httpHandler
|
||||
/// </summary>
|
||||
public ReverseProxyHttpHandler()
|
||||
{
|
||||
this.InnerHandler = new HttpClientHandler
|
||||
{
|
||||
ServerCertificateCustomValidationCallback = delegate { return true; }
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 替换为Loopback
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
var uri = request.RequestUri;
|
||||
if (uri != null && uri.HostNameType == UriHostNameType.Dns)
|
||||
{
|
||||
var domain = uri.Host;
|
||||
var builder = new UriBuilder(uri) { Host = IPAddress.Loopback.ToString() };
|
||||
|
||||
request.RequestUri = builder.Uri;
|
||||
request.Headers.Host = domain;
|
||||
}
|
||||
|
||||
return await base.SendAsync(request, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,7 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using FastGithub.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@ -14,15 +13,20 @@ namespace FastGithub.Upgrade
|
||||
/// </summary>
|
||||
sealed class UpgradeService
|
||||
{
|
||||
private readonly IDomainResolver domainResolver;
|
||||
private readonly ILogger<UpgradeService> logger;
|
||||
private const string ReleasesUri = "https://api.github.com/repos/xljiulang/fastgithub/releases";
|
||||
private readonly Uri releasesUri = new("https://api.github.com/repos/xljiulang/fastgithub/releases");
|
||||
|
||||
/// <summary>
|
||||
/// 升级服务
|
||||
/// </summary>
|
||||
/// <param name="domainResolver"></param>
|
||||
/// <param name="logger"></param>
|
||||
public UpgradeService(ILogger<UpgradeService> logger)
|
||||
public UpgradeService(
|
||||
IDomainResolver domainResolver,
|
||||
ILogger<UpgradeService> logger)
|
||||
{
|
||||
this.domainResolver = domainResolver;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
@ -48,9 +52,13 @@ namespace FastGithub.Upgrade
|
||||
var lastedVersion = lastRelease.GetProductionVersion();
|
||||
if (lastedVersion.CompareTo(currentVersion) > 0)
|
||||
{
|
||||
this.logger.LogInformation($"您正在使用{currentVersion}版本{Environment.NewLine}请前往{lastRelease.HtmlUrl}下载新版本");
|
||||
this.logger.LogInformation($"当前版本{currentVersion}为老版本{Environment.NewLine}请前往{lastRelease.HtmlUrl}下载新版本");
|
||||
this.logger.LogInformation(lastRelease.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
this.logger.LogInformation($"当前版本{currentVersion}为最新版本");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -59,14 +67,21 @@ namespace FastGithub.Upgrade
|
||||
/// <returns></returns>
|
||||
public async Task<GithubRelease?> GetLastedReleaseAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using var httpClient = new HttpClient(new ReverseProxyHttpHandler())
|
||||
var domainConfig = new DomainConfig
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(30d),
|
||||
TlsSni = false,
|
||||
TlsIgnoreNameMismatch = true,
|
||||
Timeout = TimeSpan.FromSeconds(30d)
|
||||
};
|
||||
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("*/*"));
|
||||
httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(nameof(FastGithub), "1.0"));
|
||||
|
||||
var releases = await httpClient.GetFromJsonAsync<GithubRelease[]>(ReleasesUri, cancellationToken);
|
||||
using var request = new GithubRequestMessage
|
||||
{
|
||||
RequestUri = this.releasesUri
|
||||
};
|
||||
|
||||
using var httpClient = new HttpClient(domainConfig, this.domainResolver);
|
||||
var response = await httpClient.SendAsync(request, cancellationToken);
|
||||
var releases = await response.Content.ReadFromJsonAsync<GithubRelease[]>(cancellationToken: cancellationToken);
|
||||
return releases?.FirstOrDefault(item => item.Prerelease == false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,6 +16,7 @@ namespace FastGithub
|
||||
public static IServiceCollection AddAppUpgrade(this IServiceCollection services)
|
||||
{
|
||||
return services
|
||||
.AddHttpClient()
|
||||
.AddSingleton<UpgradeService>()
|
||||
.AddHostedService<UpgradeHostedService>();
|
||||
}
|
||||
|
||||
@ -15,6 +15,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FastGithub.ReverseProxy", "
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FastGithub.DnscryptProxy", "FastGithub.DnscryptProxy\FastGithub.DnscryptProxy.csproj", "{26BB826F-4117-4746-9BD0-C6D8043E2808}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FastGithub.Http", "FastGithub.Http\FastGithub.Http.csproj", "{B5DCB3E4-5094-4170-B844-6F395002CA42}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@ -45,6 +47,10 @@ Global
|
||||
{26BB826F-4117-4746-9BD0-C6D8043E2808}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{26BB826F-4117-4746-9BD0-C6D8043E2808}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{26BB826F-4117-4746-9BD0-C6D8043E2808}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B5DCB3E4-5094-4170-B844-6F395002CA42}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B5DCB3E4-5094-4170-B844-6F395002CA42}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B5DCB3E4-5094-4170-B844-6F395002CA42}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B5DCB3E4-5094-4170-B844-6F395002CA42}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
Loading…
Reference in New Issue
Block a user