From 45f557235f425293f2d51d929fbc93bf3f93b597 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Thu, 13 Dec 2018 10:18:04 -0800 Subject: [PATCH 01/10] Replace package reference to Process.Sources with a local copy of the code --- build/dependencies.props | 1 - build/external-dependencies.props | 1 - eng/Dependencies.props | 1 - src/AzureIntegration/Directory.Build.props | 1 + src/AzureIntegration/build/dependencies.props | 3 +- ...re.AzureAppServices.FunctionalTests.csproj | 2 +- ...spNetCore.Server.IntegrationTesting.csproj | 2 +- src/Shared/Process/ProcessExtensions.cs | 113 ++++++++++++++++++ src/Templating/Directory.Build.props | 1 + src/Templating/build/dependencies.props | 3 +- .../test/Templates.Test/Templates.Test.csproj | 2 +- .../dotnet-watch/src/dotnet-watch.csproj | 2 +- 12 files changed, 121 insertions(+), 11 deletions(-) create mode 100644 src/Shared/Process/ProcessExtensions.cs diff --git a/build/dependencies.props b/build/dependencies.props index 1c6eb173a4be..005fa627ce33 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -79,7 +79,6 @@ 2.1.1 2.1.1 2.1.6 - 2.1.1 2.1.1 2.1.1 2.1.1 diff --git a/build/external-dependencies.props b/build/external-dependencies.props index 7cb96166b253..273e998914ab 100644 --- a/build/external-dependencies.props +++ b/build/external-dependencies.props @@ -68,7 +68,6 @@ - diff --git a/eng/Dependencies.props b/eng/Dependencies.props index b7b342f0e715..b205f0f89c0b 100644 --- a/eng/Dependencies.props +++ b/eng/Dependencies.props @@ -41,7 +41,6 @@ - diff --git a/src/AzureIntegration/Directory.Build.props b/src/AzureIntegration/Directory.Build.props index 24c8148124d3..5785b0e457fa 100644 --- a/src/AzureIntegration/Directory.Build.props +++ b/src/AzureIntegration/Directory.Build.props @@ -16,6 +16,7 @@ true true true + $(MSBuildThisFileDirectory)..\Shared\ diff --git a/src/AzureIntegration/build/dependencies.props b/src/AzureIntegration/build/dependencies.props index f338fce0d418..640d5a8d6e0c 100644 --- a/src/AzureIntegration/build/dependencies.props +++ b/src/AzureIntegration/build/dependencies.props @@ -51,6 +51,5 @@ 2.1.1 2.1.1 2.1.1 - 2.1.1 - \ No newline at end of file + diff --git a/src/AzureIntegration/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/Microsoft.AspNetCore.AzureAppServices.FunctionalTests.csproj b/src/AzureIntegration/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/Microsoft.AspNetCore.AzureAppServices.FunctionalTests.csproj index 6bf10bc884ce..dd2aa41bb23e 100644 --- a/src/AzureIntegration/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/Microsoft.AspNetCore.AzureAppServices.FunctionalTests.csproj +++ b/src/AzureIntegration/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/Microsoft.AspNetCore.AzureAppServices.FunctionalTests.csproj @@ -9,13 +9,13 @@ + - diff --git a/src/Hosting/Server.IntegrationTesting/src/Microsoft.AspNetCore.Server.IntegrationTesting.csproj b/src/Hosting/Server.IntegrationTesting/src/Microsoft.AspNetCore.Server.IntegrationTesting.csproj index e693a222784e..d04c320b5915 100644 --- a/src/Hosting/Server.IntegrationTesting/src/Microsoft.AspNetCore.Server.IntegrationTesting.csproj +++ b/src/Hosting/Server.IntegrationTesting/src/Microsoft.AspNetCore.Server.IntegrationTesting.csproj @@ -16,6 +16,7 @@ + @@ -25,7 +26,6 @@ - diff --git a/src/Shared/Process/ProcessExtensions.cs b/src/Shared/Process/ProcessExtensions.cs new file mode 100644 index 000000000000..cf42a7e3a77e --- /dev/null +++ b/src/Shared/Process/ProcessExtensions.cs @@ -0,0 +1,113 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; + +namespace Microsoft.Extensions.Internal +{ + internal static class ProcessExtensions + { + private static readonly bool _isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + private static readonly TimeSpan _defaultTimeout = TimeSpan.FromSeconds(30); + + public static void KillTree(this Process process) + { + process.KillTree(_defaultTimeout); + } + + public static void KillTree(this Process process, TimeSpan timeout) + { + string stdout; + if (_isWindows) + { + RunProcessAndWaitForExit( + "taskkill", + $"/T /F /PID {process.Id}", + timeout, + out stdout); + } + else + { + var children = new HashSet(); + GetAllChildIdsUnix(process.Id, children, timeout); + foreach (var childId in children) + { + KillProcessUnix(childId, timeout); + } + KillProcessUnix(process.Id, timeout); + } + } + + private static void GetAllChildIdsUnix(int parentId, ISet children, TimeSpan timeout) + { + string stdout; + var exitCode = RunProcessAndWaitForExit( + "pgrep", + $"-P {parentId}", + timeout, + out stdout); + + if (exitCode == 0 && !string.IsNullOrEmpty(stdout)) + { + using (var reader = new StringReader(stdout)) + { + while (true) + { + var text = reader.ReadLine(); + if (text == null) + { + return; + } + + int id; + if (int.TryParse(text, out id)) + { + children.Add(id); + // Recursively get the children + GetAllChildIdsUnix(id, children, timeout); + } + } + } + } + } + + private static void KillProcessUnix(int processId, TimeSpan timeout) + { + string stdout; + RunProcessAndWaitForExit( + "kill", + $"-TERM {processId}", + timeout, + out stdout); + } + + private static int RunProcessAndWaitForExit(string fileName, string arguments, TimeSpan timeout, out string stdout) + { + var startInfo = new ProcessStartInfo + { + FileName = fileName, + Arguments = arguments, + RedirectStandardOutput = true, + UseShellExecute = false + }; + + var process = Process.Start(startInfo); + + stdout = null; + if (process.WaitForExit((int)timeout.TotalMilliseconds)) + { + stdout = process.StandardOutput.ReadToEnd(); + } + else + { + process.Kill(); + } + + return process.ExitCode; + } + } +} diff --git a/src/Templating/Directory.Build.props b/src/Templating/Directory.Build.props index d7eb96413892..356908672af2 100644 --- a/src/Templating/Directory.Build.props +++ b/src/Templating/Directory.Build.props @@ -13,5 +13,6 @@ https://github.com/aspnet/AspNetCore git true + $(MSBuildThisFileDirectory)..\Shared\ diff --git a/src/Templating/build/dependencies.props b/src/Templating/build/dependencies.props index 3110c18d6430..9ecb05f43321 100644 --- a/src/Templating/build/dependencies.props +++ b/src/Templating/build/dependencies.props @@ -1,4 +1,4 @@ - + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) @@ -35,7 +35,6 @@ 2.1.2-rtm-30913 2.1.2-rtm-30913 2.1.1 - 2.1.1 2.1.5 2.1.2-rtm-30913 15.6.1 diff --git a/src/Templating/test/Templates.Test/Templates.Test.csproj b/src/Templating/test/Templates.Test/Templates.Test.csproj index 19230e2958d0..c16008913cc5 100644 --- a/src/Templating/test/Templates.Test/Templates.Test.csproj +++ b/src/Templating/test/Templates.Test/Templates.Test.csproj @@ -6,6 +6,7 @@ + @@ -14,7 +15,6 @@ - diff --git a/src/Tools/dotnet-watch/src/dotnet-watch.csproj b/src/Tools/dotnet-watch/src/dotnet-watch.csproj index 4aff8041864e..59945888fb98 100644 --- a/src/Tools/dotnet-watch/src/dotnet-watch.csproj +++ b/src/Tools/dotnet-watch/src/dotnet-watch.csproj @@ -13,12 +13,12 @@ + - From b0c2b8709855bf132965251301bac973735a4532 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Thu, 13 Dec 2018 10:34:34 -0800 Subject: [PATCH 02/10] Replace a package ref to Microsoft.AspNetCore.Certificates.Generation.Sources with local code This code was copied here from aspnet/Extensions --- build/dependencies.props | 1 - build/external-dependencies.props | 1 - eng/Dependencies.props | 1 - ...soft.AspNetCore.Server.Kestrel.Core.csproj | 2 +- .../CertificateManager.cs | 720 ++++++++++++++++++ .../CertificatePurpose.cs | 12 + .../EnsureCertificateResult.cs | 20 + src/Templating/build/dependencies.props | 3 +- .../test/Templates.Test/Templates.Test.csproj | 2 +- src/Tools/Directory.Build.props | 1 + ...NetCore.DeveloperCertificates.XPlat.csproj | 2 +- .../src/Properties/AssemblyInfo.cs | 6 + .../test/CertificateManagerTests.cs | 300 ++++++++ ...e.DeveloperCertificates.XPlat.Tests.csproj | 11 + .../src => Shared/CommandLine}/CliContext.cs | 0 .../CommandLineApplicationExtensions.cs | 0 .../CommandLine}/ConsoleReporter.cs | 0 .../src => Shared/CommandLine}/DebugHelper.cs | 0 .../src => Shared/CommandLine}/Ensure.cs | 0 .../src => Shared/CommandLine}/IConsole.cs | 0 .../src => Shared/CommandLine}/IReporter.cs | 0 .../CommandLine}/NullReporter.cs | 0 .../CommandLine}/PhysicalConsole.cs | 0 .../TestHelpers}/TestConsole.cs | 0 .../TestHelpers}/TestReporter.cs | 0 .../src/dotnet-dev-certs.csproj | 12 +- .../src/dotnet-sql-cache.csproj | 2 +- .../src/dotnet-user-secrets.csproj | 2 +- .../test/dotnet-user-secrets.Tests.csproj | 2 +- .../dotnet-watch/src/dotnet-watch.csproj | 2 +- .../test/dotnet-watch.Tests.csproj | 2 +- 31 files changed, 1081 insertions(+), 23 deletions(-) create mode 100644 src/Shared/CertificateGeneration/CertificateManager.cs create mode 100644 src/Shared/CertificateGeneration/CertificatePurpose.cs create mode 100644 src/Shared/CertificateGeneration/EnsureCertificateResult.cs create mode 100644 src/Tools/FirstRunCertGenerator/src/Properties/AssemblyInfo.cs create mode 100644 src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs create mode 100644 src/Tools/FirstRunCertGenerator/test/Microsoft.AspNetCore.DeveloperCertificates.XPlat.Tests.csproj rename src/Tools/{shared/src => Shared/CommandLine}/CliContext.cs (100%) rename src/Tools/{shared/src => Shared/CommandLine}/CommandLineApplicationExtensions.cs (100%) rename src/Tools/{shared/src => Shared/CommandLine}/ConsoleReporter.cs (100%) rename src/Tools/{shared/src => Shared/CommandLine}/DebugHelper.cs (100%) rename src/Tools/{shared/src => Shared/CommandLine}/Ensure.cs (100%) rename src/Tools/{shared/src => Shared/CommandLine}/IConsole.cs (100%) rename src/Tools/{shared/src => Shared/CommandLine}/IReporter.cs (100%) rename src/Tools/{shared/src => Shared/CommandLine}/NullReporter.cs (100%) rename src/Tools/{shared/src => Shared/CommandLine}/PhysicalConsole.cs (100%) rename src/Tools/{shared/test => Shared/TestHelpers}/TestConsole.cs (100%) rename src/Tools/{shared/test => Shared/TestHelpers}/TestReporter.cs (100%) diff --git a/build/dependencies.props b/build/dependencies.props index 005fa627ce33..53eaa6fa6a94 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -26,7 +26,6 @@ 2.1.1 - 2.1.1 2.1.0 2.1.1 2.1.2 diff --git a/build/external-dependencies.props b/build/external-dependencies.props index 273e998914ab..51299edcd598 100644 --- a/build/external-dependencies.props +++ b/build/external-dependencies.props @@ -15,7 +15,6 @@ - diff --git a/eng/Dependencies.props b/eng/Dependencies.props index b205f0f89c0b..5ae008dde2c3 100644 --- a/eng/Dependencies.props +++ b/eng/Dependencies.props @@ -10,7 +10,6 @@ - diff --git a/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj b/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj index dae8056002d2..55d5278ff9d4 100644 --- a/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj +++ b/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj @@ -11,10 +11,10 @@ + - diff --git a/src/Shared/CertificateGeneration/CertificateManager.cs b/src/Shared/CertificateGeneration/CertificateManager.cs new file mode 100644 index 000000000000..4e2a0a99643f --- /dev/null +++ b/src/Shared/CertificateGeneration/CertificateManager.cs @@ -0,0 +1,720 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Text.RegularExpressions; + +namespace Microsoft.AspNetCore.Certificates.Generation +{ + internal class CertificateManager + { + public const string AspNetHttpsOid = "1.3.6.1.4.1.311.84.1.1"; + public const string AspNetHttpsOidFriendlyName = "ASP.NET Core HTTPS development certificate"; + + public const string AspNetIdentityOid = "1.3.6.1.4.1.311.84.1.2"; + public const string AspNetIdentityOidFriendlyName = "ASP.NET Core Identity Json Web Token signing development certificate"; + + private const string ServerAuthenticationEnhancedKeyUsageOid = "1.3.6.1.5.5.7.3.1"; + private const string ServerAuthenticationEnhancedKeyUsageOidFriendlyName = "Server Authentication"; + + private const string LocalhostHttpsDnsName = "localhost"; + private const string LocalhostHttpsDistinguishedName = "CN=" + LocalhostHttpsDnsName; + + private const string IdentityDistinguishedName = "CN=Microsoft.AspNetCore.Identity.Signing"; + + public const int RSAMinimumKeySizeInBits = 2048; + + private static readonly TimeSpan MaxRegexTimeout = TimeSpan.FromMinutes(1); + private const string CertificateSubjectRegex = "CN=(.*[^,]+).*"; + private const string MacOSSystemKeyChain = "/Library/Keychains/System.keychain"; + private static readonly string MacOSUserKeyChain = Environment.GetEnvironmentVariable("HOME") + "/Library/Keychains/login.keychain-db"; + private const string MacOSFindCertificateCommandLine = "security"; +#if NETCOREAPP2_0 || NETCOREAPP2_1 + private static readonly string MacOSFindCertificateCommandLineArgumentsFormat = "find-certificate -c {0} -a -Z -p " + MacOSSystemKeyChain; +#endif + private const string MacOSFindCertificateOutputRegex = "SHA-1 hash: ([0-9A-Z]+)"; + private const string MacOSRemoveCertificateTrustCommandLine = "sudo"; + private const string MacOSRemoveCertificateTrustCommandLineArgumentsFormat = "security remove-trusted-cert -d {0}"; + private const string MacOSDeleteCertificateCommandLine = "sudo"; + private const string MacOSDeleteCertificateCommandLineArgumentsFormat = "security delete-certificate -Z {0} {1}"; + private const string MacOSTrustCertificateCommandLine = "sudo"; +#if NETCOREAPP2_0 || NETCOREAPP2_1 + private static readonly string MacOSTrustCertificateCommandLineArguments = "security add-trusted-cert -d -r trustRoot -k " + MacOSSystemKeyChain + " "; +#endif + private const int UserCancelledErrorCode = 1223; + + public IList ListCertificates( + CertificatePurpose purpose, + StoreName storeName, + StoreLocation location, + bool isValid, + bool requireExportable = true) + { + var certificates = new List(); + try + { + using (var store = new X509Store(storeName, location)) + { + store.Open(OpenFlags.ReadOnly); + certificates.AddRange(store.Certificates.OfType()); + IEnumerable matchingCertificates = certificates; + switch (purpose) + { + case CertificatePurpose.All: + matchingCertificates = matchingCertificates + .Where(c => HasOid(c, AspNetHttpsOid) || HasOid(c, AspNetIdentityOid)); + break; + case CertificatePurpose.HTTPS: + matchingCertificates = matchingCertificates + .Where(c => HasOid(c, AspNetHttpsOid)); + break; + case CertificatePurpose.Signing: + matchingCertificates = matchingCertificates + .Where(c => HasOid(c, AspNetIdentityOid)); + break; + default: + break; + } + if (isValid) + { + // Ensure the certificate hasn't expired, has a private key and its exportable + // (for container/unix scenarios). + var now = DateTimeOffset.Now; + matchingCertificates = matchingCertificates + .Where(c => c.NotBefore <= now && + now <= c.NotAfter && + (!requireExportable || !RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || IsExportable(c))); + } + + // We need to enumerate the certificates early to prevent dispoisng issues. + matchingCertificates = matchingCertificates.ToList(); + + var certificatesToDispose = certificates.Except(matchingCertificates); + DisposeCertificates(certificatesToDispose); + + store.Close(); + + return (IList)matchingCertificates; + } + } + catch + { + DisposeCertificates(certificates); + certificates.Clear(); + return certificates; + } + + bool HasOid(X509Certificate2 certificate, string oid) => + certificate.Extensions.OfType() + .Any(e => string.Equals(oid, e.Oid.Value, StringComparison.Ordinal)); +#if !XPLAT + bool IsExportable(X509Certificate2 c) => + ((c.GetRSAPrivateKey() is RSACryptoServiceProvider rsaPrivateKey && + rsaPrivateKey.CspKeyContainerInfo.Exportable) || + (c.GetRSAPrivateKey() is RSACng cngPrivateKey && + cngPrivateKey.Key.ExportPolicy == CngExportPolicies.AllowExport)); +#else + // Only check for RSA CryptoServiceProvider and do not fail in XPlat tooling as + // System.Security.Cryptography.Cng is not pat of the shared framework and we don't + // want to bring the dependency in on CLI scenarios. This functionality will be used + // on CLI scenarios as part of the first run experience, so checking the exportability + // of the certificate is not important. + bool IsExportable(X509Certificate2 c) => + ((c.GetRSAPrivateKey() is RSACryptoServiceProvider rsaPrivateKey && + rsaPrivateKey.CspKeyContainerInfo.Exportable) || !(c.GetRSAPrivateKey() is RSACryptoServiceProvider)); +#endif + } + + private void DisposeCertificates(IEnumerable disposables) + { + foreach (var disposable in disposables) + { + try + { + disposable.Dispose(); + } + catch + { + } + } + } + +#if NETCOREAPP2_0 || NETCOREAPP2_1 + + public X509Certificate2 CreateAspNetCoreHttpsDevelopmentCertificate(DateTimeOffset notBefore, DateTimeOffset notAfter, string subjectOverride) + { + var subject = new X500DistinguishedName(subjectOverride ?? LocalhostHttpsDistinguishedName); + var extensions = new List(); + var sanBuilder = new SubjectAlternativeNameBuilder(); + sanBuilder.AddDnsName(LocalhostHttpsDnsName); + + var keyUsage = new X509KeyUsageExtension(X509KeyUsageFlags.KeyEncipherment, critical: true); + var enhancedKeyUsage = new X509EnhancedKeyUsageExtension( + new OidCollection() { + new Oid( + ServerAuthenticationEnhancedKeyUsageOid, + ServerAuthenticationEnhancedKeyUsageOidFriendlyName) + }, + critical: true); + + var basicConstraints = new X509BasicConstraintsExtension( + certificateAuthority: false, + hasPathLengthConstraint: false, + pathLengthConstraint: 0, + critical: true); + + var aspNetHttpsExtension = new X509Extension( + new AsnEncodedData( + new Oid(AspNetHttpsOid, AspNetHttpsOidFriendlyName), + Encoding.ASCII.GetBytes(AspNetHttpsOidFriendlyName)), + critical: false); + + extensions.Add(basicConstraints); + extensions.Add(keyUsage); + extensions.Add(enhancedKeyUsage); + extensions.Add(sanBuilder.Build(critical: true)); + extensions.Add(aspNetHttpsExtension); + + var certificate = CreateSelfSignedCertificate(subject, extensions, notBefore, notAfter); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + certificate.FriendlyName = AspNetHttpsOidFriendlyName; + } + + return certificate; + } + + public X509Certificate2 CreateApplicationTokenSigningDevelopmentCertificate(DateTimeOffset notBefore, DateTimeOffset notAfter, string subjectOverride) + { + var subject = new X500DistinguishedName(subjectOverride ?? IdentityDistinguishedName); + var extensions = new List(); + + var keyUsage = new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, critical: true); + var enhancedKeyUsage = new X509EnhancedKeyUsageExtension( + new OidCollection() { + new Oid( + ServerAuthenticationEnhancedKeyUsageOid, + ServerAuthenticationEnhancedKeyUsageOidFriendlyName) + }, + critical: true); + + var basicConstraints = new X509BasicConstraintsExtension( + certificateAuthority: false, + hasPathLengthConstraint: false, + pathLengthConstraint: 0, + critical: true); + + var aspNetIdentityExtension = new X509Extension( + new AsnEncodedData( + new Oid(AspNetIdentityOid, AspNetIdentityOidFriendlyName), + Encoding.ASCII.GetBytes(AspNetIdentityOidFriendlyName)), + critical: false); + + extensions.Add(basicConstraints); + extensions.Add(keyUsage); + extensions.Add(enhancedKeyUsage); + extensions.Add(aspNetIdentityExtension); + + var certificate = CreateSelfSignedCertificate(subject, extensions, notBefore, notAfter); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + certificate.FriendlyName = AspNetIdentityOidFriendlyName; + } + + return certificate; + } + + public X509Certificate2 CreateSelfSignedCertificate( + X500DistinguishedName subject, + IEnumerable extensions, + DateTimeOffset notBefore, + DateTimeOffset notAfter) + { + var key = CreateKeyMaterial(RSAMinimumKeySizeInBits); + + var request = new CertificateRequest(subject, key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + foreach (var extension in extensions) + { + request.CertificateExtensions.Add(extension); + } + + return request.CreateSelfSigned(notBefore, notAfter); + + RSA CreateKeyMaterial(int minimumKeySize) + { + var rsa = RSA.Create(minimumKeySize); + if (rsa.KeySize < minimumKeySize) + { + throw new InvalidOperationException($"Failed to create a key with a size of {minimumKeySize} bits"); + } + + return rsa; + } + } + + public X509Certificate2 SaveCertificateInStore(X509Certificate2 certificate, StoreName name, StoreLocation location) + { + var imported = certificate; + if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + // On non OSX systems we need to export the certificate and import it so that the transient + // key that we generated gets persisted. + var export = certificate.Export(X509ContentType.Pkcs12, ""); + imported = new X509Certificate2(export, "", X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable); + Array.Clear(export, 0, export.Length); + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + imported.FriendlyName = certificate.FriendlyName; + } + + using (var store = new X509Store(name, location)) + { + store.Open(OpenFlags.ReadWrite); + store.Add(imported); + store.Close(); + }; + + return imported; + } + + public void ExportCertificate(X509Certificate2 certificate, string path, bool includePrivateKey, string password) + { + if (Path.GetDirectoryName(path) != "") + { + Directory.CreateDirectory(Path.GetDirectoryName(path)); + } + + if (includePrivateKey) + { + var bytes = certificate.Export(X509ContentType.Pkcs12, password); + try + { + File.WriteAllBytes(path, bytes); + } + finally + { + Array.Clear(bytes, 0, bytes.Length); + } + } + else + { + var bytes = certificate.Export(X509ContentType.Cert); + File.WriteAllBytes(path, bytes); + } + } + + public void TrustCertificate(X509Certificate2 certificate) + { + // Strip certificate of the private key if any. + var publicCertificate = new X509Certificate2(certificate.Export(X509ContentType.Cert)); + + if (!IsTrusted(publicCertificate)) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + TrustCertificateOnWindows(certificate, publicCertificate); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + TrustCertificateOnMac(publicCertificate); + } + } + } + + private void TrustCertificateOnMac(X509Certificate2 publicCertificate) + { + var tmpFile = Path.GetTempFileName(); + try + { + ExportCertificate(publicCertificate, tmpFile, includePrivateKey: false, password: null); + using (var process = Process.Start(MacOSTrustCertificateCommandLine, MacOSTrustCertificateCommandLineArguments + tmpFile)) + { + process.WaitForExit(); + if (process.ExitCode != 0) + { + throw new InvalidOperationException("There was an error trusting the certificate."); + } + } + } + finally + { + try + { + if (File.Exists(tmpFile)) + { + File.Delete(tmpFile); + } + } + catch + { + // We don't care if we can't delete the temp file. + } + } + } + + private static void TrustCertificateOnWindows(X509Certificate2 certificate, X509Certificate2 publicCertificate) + { + publicCertificate.FriendlyName = certificate.FriendlyName; + + using (var store = new X509Store(StoreName.Root, StoreLocation.CurrentUser)) + { + store.Open(OpenFlags.ReadWrite); + try + { + store.Add(publicCertificate); + } + catch (CryptographicException exception) when (exception.HResult == UserCancelledErrorCode) + { + throw new UserCancelledTrustException(); + } + store.Close(); + }; + } + + public bool IsTrusted(X509Certificate2 certificate) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return ListCertificates(CertificatePurpose.HTTPS, StoreName.Root, StoreLocation.CurrentUser, isValid: true, requireExportable: false) + .Any(c => c.Thumbprint == certificate.Thumbprint); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + var subjectMatch = Regex.Match(certificate.Subject, CertificateSubjectRegex, RegexOptions.Singleline, MaxRegexTimeout); + if (!subjectMatch.Success) + { + throw new InvalidOperationException($"Can't determine the subject for the certificate with subject '{certificate.Subject}'."); + } + var subject = subjectMatch.Groups[1].Value; + using (var checkTrustProcess = Process.Start(new ProcessStartInfo( + MacOSFindCertificateCommandLine, + string.Format(MacOSFindCertificateCommandLineArgumentsFormat, subject)) + { + RedirectStandardOutput = true + })) + { + var output = checkTrustProcess.StandardOutput.ReadToEnd(); + checkTrustProcess.WaitForExit(); + var matches = Regex.Matches(output, MacOSFindCertificateOutputRegex, RegexOptions.Multiline, MaxRegexTimeout); + var hashes = matches.OfType().Select(m => m.Groups[1].Value).ToList(); + return hashes.Any(h => string.Equals(h, certificate.Thumbprint, StringComparison.Ordinal)); + } + } + else + { + return false; + } + } + + public void CleanupHttpsCertificates(string subject = LocalhostHttpsDistinguishedName) + { + CleanupCertificates(CertificatePurpose.HTTPS, subject); + } + + public void CleanupCertificates(CertificatePurpose purpose, string subject) + { + // On OS X we don't have a good way to manage trusted certificates in the system keychain + // so we do everything by invoking the native toolchain. + // This has some limitations, like for example not being able to identify our custom OID extension. For that + // matter, when we are cleaning up certificates on the machine, we start by removing the trusted certificates. + // To do this, we list the certificates that we can identify on the current user personal store and we invoke + // the native toolchain to remove them from the sytem keychain. Once we have removed the trusted certificates, + // we remove the certificates from the local user store to finish up the cleanup. + var certificates = ListCertificates(purpose, StoreName.My, StoreLocation.CurrentUser, isValid: false); + foreach (var certificate in certificates) + { + RemoveCertificate(certificate, RemoveLocations.All); + } + } + + public void RemoveAllCertificates(CertificatePurpose purpose, StoreName storeName, StoreLocation storeLocation, string subject = null) + { + var certificates = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? + ListCertificates(purpose, StoreName.My, StoreLocation.CurrentUser, isValid: false) : + ListCertificates(purpose, storeName, storeLocation, isValid: false); + var certificatesWithName = subject == null ? certificates : certificates.Where(c => c.Subject == subject); + + var removeLocation = storeName == StoreName.My ? RemoveLocations.Local : RemoveLocations.Trusted; + + foreach (var certificate in certificates) + { + RemoveCertificate(certificate, removeLocation); + } + + DisposeCertificates(certificates); + } + + private void RemoveCertificate(X509Certificate2 certificate, RemoveLocations locations) + { + switch (locations) + { + case RemoveLocations.Undefined: + throw new InvalidOperationException($"'{nameof(RemoveLocations.Undefined)}' is not a valid location."); + case RemoveLocations.Local: + RemoveCertificateFromUserStore(certificate); + break; + case RemoveLocations.Trusted when !RuntimeInformation.IsOSPlatform(OSPlatform.Linux): + RemoveCertificateFromTrustedRoots(certificate); + break; + case RemoveLocations.All: + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + RemoveCertificateFromTrustedRoots(certificate); + } + RemoveCertificateFromUserStore(certificate); + break; + default: + throw new InvalidOperationException("Invalid location."); + } + } + + private static void RemoveCertificateFromUserStore(X509Certificate2 certificate) + { + using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser)) + { + store.Open(OpenFlags.ReadWrite); + var matching = store.Certificates + .OfType() + .Single(c => c.SerialNumber == certificate.SerialNumber); + + store.Remove(matching); + store.Close(); + } + } + + private void RemoveCertificateFromTrustedRoots(X509Certificate2 certificate) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + using (var store = new X509Store(StoreName.Root, StoreLocation.CurrentUser)) + { + store.Open(OpenFlags.ReadWrite); + var matching = store.Certificates + .OfType() + .Single(c => c.SerialNumber == certificate.SerialNumber); + + store.Remove(matching); + store.Close(); + } + } + else + { + if (IsTrusted(certificate)) // On OSX this check just ensures its on the system keychain + { + try + { + RemoveCertificateTrustRule(certificate); + } + catch + { + // We don't care if we fail to remove the trust rule if + // for some reason the certificate became untrusted. + // The delete command will fail if the certificate is + // trusted. + } + RemoveCertificateFromKeyChain(MacOSSystemKeyChain, certificate); + } + } + } + + private static void RemoveCertificateTrustRule(X509Certificate2 certificate) + { + var certificatePath = Path.GetTempFileName(); + try + { + var certBytes = certificate.Export(X509ContentType.Cert); + File.WriteAllBytes(certificatePath, certBytes); + var processInfo = new ProcessStartInfo( + MacOSRemoveCertificateTrustCommandLine, + string.Format( + MacOSRemoveCertificateTrustCommandLineArgumentsFormat, + certificatePath + )); + using (var process = Process.Start(processInfo)) + { + process.WaitForExit(); + } + } + finally + { + try + { + if (File.Exists(certificatePath)) + { + File.Delete(certificatePath); + } + } + catch + { + // We don't care about failing to do clean-up on a temp file. + } + } + } + + private static void RemoveCertificateFromKeyChain(string keyChain, X509Certificate2 certificate) + { + var processInfo = new ProcessStartInfo( + MacOSDeleteCertificateCommandLine, + string.Format( + MacOSDeleteCertificateCommandLineArgumentsFormat, + certificate.Thumbprint.ToUpperInvariant(), + keyChain + )) + { + RedirectStandardOutput = true, + RedirectStandardError = true + }; + + using (var process = Process.Start(processInfo)) + { + var output = process.StandardOutput.ReadToEnd() + process.StandardError.ReadToEnd(); + process.WaitForExit(); + + if (process.ExitCode != 0) + { + throw new InvalidOperationException($@"There was an error removing the certificate with thumbprint '{certificate.Thumbprint}'. + +{output}"); + } + } + } + + public EnsureCertificateResult EnsureAspNetCoreHttpsDevelopmentCertificate( + DateTimeOffset notBefore, + DateTimeOffset notAfter, + string path = null, + bool trust = false, + bool includePrivateKey = false, + string password = null, + string subject = LocalhostHttpsDistinguishedName) + { + return EnsureValidCertificateExists(notBefore, notAfter, CertificatePurpose.HTTPS, path, trust, includePrivateKey, password, subject); + } + + public EnsureCertificateResult EnsureAspNetCoreApplicationTokensDevelopmentCertificate( + DateTimeOffset notBefore, + DateTimeOffset notAfter, + string path = null, + bool trust = false, + bool includePrivateKey = false, + string password = null, + string subject = IdentityDistinguishedName) + { + return EnsureValidCertificateExists(notBefore, notAfter, CertificatePurpose.Signing, path, trust, includePrivateKey, password, subject); + } + + public EnsureCertificateResult EnsureValidCertificateExists( + DateTimeOffset notBefore, + DateTimeOffset notAfter, + CertificatePurpose purpose, + string path = null, + bool trust = false, + bool includePrivateKey = false, + string password = null, + string subjectOverride = null) + { + if (purpose == CertificatePurpose.All) + { + throw new ArgumentException("The certificate must have a specific purpose."); + } + + var certificates = ListCertificates(purpose, StoreName.My, StoreLocation.CurrentUser, isValid: true).Concat( + ListCertificates(purpose, StoreName.My, StoreLocation.LocalMachine, isValid: true)); + + certificates = subjectOverride == null ? certificates : certificates.Where(c => c.Subject == subjectOverride); + + var result = EnsureCertificateResult.Succeeded; + + X509Certificate2 certificate = null; + if (certificates.Count() > 0) + { + certificate = certificates.FirstOrDefault(); + result = EnsureCertificateResult.ValidCertificatePresent; + } + else + { + try + { + switch (purpose) + { + case CertificatePurpose.All: + throw new InvalidOperationException("The certificate must have a specific purpose."); + case CertificatePurpose.HTTPS: + certificate = CreateAspNetCoreHttpsDevelopmentCertificate(notBefore, notAfter, subjectOverride); + break; + case CertificatePurpose.Signing: + certificate = CreateApplicationTokenSigningDevelopmentCertificate(notBefore, notAfter, subjectOverride); + break; + default: + throw new InvalidOperationException("The certificate must have a purpose."); + } + } + catch + { + return EnsureCertificateResult.ErrorCreatingTheCertificate; + } + + try + { + certificate = SaveCertificateInStore(certificate, StoreName.My, StoreLocation.CurrentUser); + } + catch + { + return EnsureCertificateResult.ErrorSavingTheCertificateIntoTheCurrentUserPersonalStore; + } + } + if (path != null) + { + try + { + ExportCertificate(certificate, path, includePrivateKey, password); + } + catch + { + return EnsureCertificateResult.ErrorExportingTheCertificate; + } + } + + if ((RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) && trust) + { + try + { + TrustCertificate(certificate); + } + catch (UserCancelledTrustException) + { + return EnsureCertificateResult.UserCancelledTrustStep; + } + catch + { + return EnsureCertificateResult.FailedToTrustTheCertificate; + } + } + + return result; + } + + private class UserCancelledTrustException : Exception + { + } + + private enum RemoveLocations + { + Undefined, + Local, + Trusted, + All + } +#endif + } +} \ No newline at end of file diff --git a/src/Shared/CertificateGeneration/CertificatePurpose.cs b/src/Shared/CertificateGeneration/CertificatePurpose.cs new file mode 100644 index 000000000000..1ad1a6d79b65 --- /dev/null +++ b/src/Shared/CertificateGeneration/CertificatePurpose.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Certificates.Generation +{ + internal enum CertificatePurpose + { + All, + HTTPS, + Signing + } +} \ No newline at end of file diff --git a/src/Shared/CertificateGeneration/EnsureCertificateResult.cs b/src/Shared/CertificateGeneration/EnsureCertificateResult.cs new file mode 100644 index 000000000000..d3c86ce05d08 --- /dev/null +++ b/src/Shared/CertificateGeneration/EnsureCertificateResult.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#if NETCOREAPP2_0 || NETCOREAPP2_1 + +namespace Microsoft.AspNetCore.Certificates.Generation +{ + internal enum EnsureCertificateResult + { + Succeeded = 1, + ValidCertificatePresent, + ErrorCreatingTheCertificate, + ErrorSavingTheCertificateIntoTheCurrentUserPersonalStore, + ErrorExportingTheCertificate, + FailedToTrustTheCertificate, + UserCancelledTrustStep + } +} + +#endif \ No newline at end of file diff --git a/src/Templating/build/dependencies.props b/src/Templating/build/dependencies.props index 9ecb05f43321..835fdd08437e 100644 --- a/src/Templating/build/dependencies.props +++ b/src/Templating/build/dependencies.props @@ -1,4 +1,4 @@ - + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) @@ -15,7 +15,6 @@ 2.1.2-rtm-30913 2.1.2-rtm-30913 2.1.2-rtm-30913 - 2.1.1 2.1.2-rtm-30913 2.1.1 2.1.1 diff --git a/src/Templating/test/Templates.Test/Templates.Test.csproj b/src/Templating/test/Templates.Test/Templates.Test.csproj index c16008913cc5..3aa44382f86f 100644 --- a/src/Templating/test/Templates.Test/Templates.Test.csproj +++ b/src/Templating/test/Templates.Test/Templates.Test.csproj @@ -7,12 +7,12 @@ + - diff --git a/src/Tools/Directory.Build.props b/src/Tools/Directory.Build.props index 6b358026891f..0cbe48018a31 100644 --- a/src/Tools/Directory.Build.props +++ b/src/Tools/Directory.Build.props @@ -4,6 +4,7 @@ $(RepositoryRoot)obj\$(MSBuildProjectName)\ $(RepositoryRoot)bin\$(MSBuildProjectName)\ + $(MSBuildThisFileDirectory)Shared\ diff --git a/src/Tools/FirstRunCertGenerator/src/Microsoft.AspNetCore.DeveloperCertificates.XPlat.csproj b/src/Tools/FirstRunCertGenerator/src/Microsoft.AspNetCore.DeveloperCertificates.XPlat.csproj index ce61b3ab6240..8a5011b7ef68 100644 --- a/src/Tools/FirstRunCertGenerator/src/Microsoft.AspNetCore.DeveloperCertificates.XPlat.csproj +++ b/src/Tools/FirstRunCertGenerator/src/Microsoft.AspNetCore.DeveloperCertificates.XPlat.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/Tools/FirstRunCertGenerator/src/Properties/AssemblyInfo.cs b/src/Tools/FirstRunCertGenerator/src/Properties/AssemblyInfo.cs new file mode 100644 index 000000000000..0f02f95e4a28 --- /dev/null +++ b/src/Tools/FirstRunCertGenerator/src/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.DeveloperCertificates.XPlat.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs b/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs new file mode 100644 index 000000000000..f6503673e527 --- /dev/null +++ b/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs @@ -0,0 +1,300 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Certificates.Generation.Tests +{ + public class CertificateManagerTests + { + public CertificateManagerTests(ITestOutputHelper output) + { + Output = output; + } + + public const string TestCertificateSubject = "CN=aspnet.test"; + + public ITestOutputHelper Output { get; } + + [Fact] + public void EnsureCreateHttpsCertificate_CreatesACertificate_WhenThereAreNoHttpsCertificates() + { + try + { + // Arrange + const string CertificateName = nameof(EnsureCreateHttpsCertificate_CreatesACertificate_WhenThereAreNoHttpsCertificates) + ".cer"; + var manager = new CertificateManager(); + + manager.RemoveAllCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, TestCertificateSubject); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + manager.RemoveAllCertificates(CertificatePurpose.HTTPS, StoreName.Root, StoreLocation.CurrentUser, TestCertificateSubject); + } + + // Act + DateTimeOffset now = DateTimeOffset.UtcNow; + now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset); + var result = manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), CertificateName, trust: false, subject: TestCertificateSubject); + + // Assert + Assert.Equal(EnsureCertificateResult.Succeeded, result); + Assert.True(File.Exists(CertificateName)); + + var exportedCertificate = new X509Certificate2(File.ReadAllBytes(CertificateName)); + Assert.NotNull(exportedCertificate); + Assert.False(exportedCertificate.HasPrivateKey); + + var httpsCertificates = manager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: false); + var httpsCertificate = Assert.Single(httpsCertificates, c => c.Subject == TestCertificateSubject); + Assert.True(httpsCertificate.HasPrivateKey); + Assert.Equal(TestCertificateSubject, httpsCertificate.Subject); + Assert.Equal(TestCertificateSubject, httpsCertificate.Issuer); + Assert.Equal("sha256RSA", httpsCertificate.SignatureAlgorithm.FriendlyName); + Assert.Equal("1.2.840.113549.1.1.11", httpsCertificate.SignatureAlgorithm.Value); + + Assert.Equal(now.LocalDateTime, httpsCertificate.NotBefore); + Assert.Equal(now.AddYears(1).LocalDateTime, httpsCertificate.NotAfter); + Assert.Contains( + httpsCertificate.Extensions.OfType(), + e => e is X509BasicConstraintsExtension basicConstraints && + basicConstraints.Critical == true && + basicConstraints.CertificateAuthority == false && + basicConstraints.HasPathLengthConstraint == false && + basicConstraints.PathLengthConstraint == 0); + + Assert.Contains( + httpsCertificate.Extensions.OfType(), + e => e is X509KeyUsageExtension keyUsage && + keyUsage.Critical == true && + keyUsage.KeyUsages == X509KeyUsageFlags.KeyEncipherment); + + Assert.Contains( + httpsCertificate.Extensions.OfType(), + e => e is X509EnhancedKeyUsageExtension enhancedKeyUsage && + enhancedKeyUsage.Critical == true && + enhancedKeyUsage.EnhancedKeyUsages.OfType().Single() is Oid keyUsage && + keyUsage.Value == "1.3.6.1.5.5.7.3.1"); + + // Subject alternative name + Assert.Contains( + httpsCertificate.Extensions.OfType(), + e => e.Critical == true && + e.Oid.Value == "2.5.29.17"); + + // ASP.NET HTTPS Development certificate extension + Assert.Contains( + httpsCertificate.Extensions.OfType(), + e => e.Critical == false && + e.Oid.Value == "1.3.6.1.4.1.311.84.1.1" && + Encoding.ASCII.GetString(e.RawData) == "ASP.NET Core HTTPS development certificate"); + + Assert.Equal(httpsCertificate.GetCertHashString(), exportedCertificate.GetCertHashString()); + + } + catch (Exception e) + { + Output.WriteLine(e.Message); + ListCertificates(Output); + throw; + } + } + + private void ListCertificates(ITestOutputHelper output) + { + using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser)) + { + store.Open(OpenFlags.ReadOnly); + var certificates = store.Certificates; + foreach (var certificate in certificates) + { + Output.WriteLine($"Certificate: '{Convert.ToBase64String(certificate.Export(X509ContentType.Cert))}'."); + certificate.Dispose(); + } + + store.Close(); + } + } + + [Fact] + public void EnsureCreateHttpsCertificate_DoesNotCreateACertificate_WhenThereIsAnExistingHttpsCertificates() + { + // Arrange + const string CertificateName = nameof(EnsureCreateHttpsCertificate_DoesNotCreateACertificate_WhenThereIsAnExistingHttpsCertificates) + ".pfx"; + var certificatePassword = Guid.NewGuid().ToString(); + + var manager = new CertificateManager(); + + manager.RemoveAllCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, TestCertificateSubject); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + manager.RemoveAllCertificates(CertificatePurpose.HTTPS, StoreName.Root, StoreLocation.CurrentUser, TestCertificateSubject); + } + + DateTimeOffset now = DateTimeOffset.UtcNow; + now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset); + manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, subject: TestCertificateSubject); + + var httpsCertificate = manager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: false).Single(c => c.Subject == TestCertificateSubject); + + // Act + var result = manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), CertificateName, trust: false, includePrivateKey: true, password: certificatePassword, subject: TestCertificateSubject); + + // Assert + Assert.Equal(EnsureCertificateResult.ValidCertificatePresent, result); + Assert.True(File.Exists(CertificateName)); + + var exportedCertificate = new X509Certificate2(File.ReadAllBytes(CertificateName), certificatePassword); + Assert.NotNull(exportedCertificate); + Assert.True(exportedCertificate.HasPrivateKey); + + + Assert.Equal(httpsCertificate.GetCertHashString(), exportedCertificate.GetCertHashString()); + } + + [Fact(Skip = "Requires user interaction")] + public void EnsureAspNetCoreHttpsDevelopmentCertificate_ReturnsCorrectResult_WhenUserCancelsTrustStepOnWindows() + { + var manager = new CertificateManager(); + + manager.RemoveAllCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, TestCertificateSubject); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + manager.RemoveAllCertificates(CertificatePurpose.HTTPS, StoreName.Root, StoreLocation.CurrentUser, TestCertificateSubject); + } + + DateTimeOffset now = DateTimeOffset.UtcNow; + now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset); + var trustFailed = manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: true, subject: TestCertificateSubject); + + Assert.Equal(EnsureCertificateResult.UserCancelledTrustStep, trustFailed); + } + + [Fact(Skip = "Requires user interaction")] + public void EnsureAspNetCoreHttpsDevelopmentCertificate_CanRemoveCertificates() + { + var manager = new CertificateManager(); + + DateTimeOffset now = DateTimeOffset.UtcNow; + now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset); + manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: true, subject: TestCertificateSubject); + + manager.CleanupHttpsCertificates(TestCertificateSubject); + + Assert.Empty(manager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: false).Where(c => c.Subject == TestCertificateSubject)); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Assert.Empty(manager.ListCertificates(CertificatePurpose.HTTPS, StoreName.Root, StoreLocation.CurrentUser, isValid: false).Where(c => c.Subject == TestCertificateSubject)); + } + } + + [Fact] + public void EnsureCreateIdentityTokenSigningCertificate_CreatesACertificate_WhenThereAreNoHttpsCertificates() + { + // Arrange + const string CertificateName = nameof(EnsureCreateIdentityTokenSigningCertificate_CreatesACertificate_WhenThereAreNoHttpsCertificates) + ".cer"; + var manager = new CertificateManager(); + + manager.RemoveAllCertificates(CertificatePurpose.Signing, StoreName.My, StoreLocation.CurrentUser, TestCertificateSubject); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + manager.RemoveAllCertificates(CertificatePurpose.Signing, StoreName.Root, StoreLocation.CurrentUser, TestCertificateSubject); + } + + // Act + DateTimeOffset now = DateTimeOffset.UtcNow; + now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset); + var result = manager.EnsureAspNetCoreApplicationTokensDevelopmentCertificate(now, now.AddYears(1), CertificateName, trust: false, subject: TestCertificateSubject); + + // Assert + Assert.Equal(EnsureCertificateResult.Succeeded, result); + Assert.True(File.Exists(CertificateName)); + + var exportedCertificate = new X509Certificate2(File.ReadAllBytes(CertificateName)); + Assert.NotNull(exportedCertificate); + Assert.False(exportedCertificate.HasPrivateKey); + + var identityCertificates = manager.ListCertificates(CertificatePurpose.Signing, StoreName.My, StoreLocation.CurrentUser, isValid: false); + var identityCertificate = Assert.Single(identityCertificates, i => i.Subject == TestCertificateSubject); + Assert.True(identityCertificate.HasPrivateKey); + Assert.Equal(TestCertificateSubject, identityCertificate.Subject); + Assert.Equal(TestCertificateSubject, identityCertificate.Issuer); + Assert.Equal("sha256RSA", identityCertificate.SignatureAlgorithm.FriendlyName); + Assert.Equal("1.2.840.113549.1.1.11", identityCertificate.SignatureAlgorithm.Value); + + Assert.Equal(now.LocalDateTime, identityCertificate.NotBefore); + Assert.Equal(now.AddYears(1).LocalDateTime, identityCertificate.NotAfter); + Assert.Contains( + identityCertificate.Extensions.OfType(), + e => e is X509BasicConstraintsExtension basicConstraints && + basicConstraints.Critical == true && + basicConstraints.CertificateAuthority == false && + basicConstraints.HasPathLengthConstraint == false && + basicConstraints.PathLengthConstraint == 0); + + Assert.Contains( + identityCertificate.Extensions.OfType(), + e => e is X509KeyUsageExtension keyUsage && + keyUsage.Critical == true && + keyUsage.KeyUsages == X509KeyUsageFlags.DigitalSignature); + + Assert.Contains( + identityCertificate.Extensions.OfType(), + e => e is X509EnhancedKeyUsageExtension enhancedKeyUsage && + enhancedKeyUsage.Critical == true && + enhancedKeyUsage.EnhancedKeyUsages.OfType().Single() is Oid keyUsage && + keyUsage.Value == "1.3.6.1.5.5.7.3.1"); + + // ASP.NET Core Identity Json Web Token signing development certificate + Assert.Contains( + identityCertificate.Extensions.OfType(), + e => e.Critical == false && + e.Oid.Value == "1.3.6.1.4.1.311.84.1.2" && + Encoding.ASCII.GetString(e.RawData) == "ASP.NET Core Identity Json Web Token signing development certificate"); + + Assert.Equal(identityCertificate.GetCertHashString(), exportedCertificate.GetCertHashString()); + } + + [Fact] + public void EnsureCreateIdentityTokenSigningCertificate_DoesNotCreateACertificate_WhenThereIsAnExistingHttpsCertificates() + { + // Arrange + const string CertificateName = nameof(EnsureCreateIdentityTokenSigningCertificate_DoesNotCreateACertificate_WhenThereIsAnExistingHttpsCertificates) + ".pfx"; + var certificatePassword = Guid.NewGuid().ToString(); + + var manager = new CertificateManager(); + + manager.RemoveAllCertificates(CertificatePurpose.Signing, StoreName.My, StoreLocation.CurrentUser, TestCertificateSubject); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + manager.RemoveAllCertificates(CertificatePurpose.Signing, StoreName.Root, StoreLocation.CurrentUser, TestCertificateSubject); + } + + DateTimeOffset now = DateTimeOffset.UtcNow; + now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset); + manager.EnsureAspNetCoreApplicationTokensDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, subject: TestCertificateSubject); + + var identityTokenSigningCertificates = manager.ListCertificates(CertificatePurpose.Signing, StoreName.My, StoreLocation.CurrentUser, isValid: false).Single(c => c.Subject == TestCertificateSubject); + + // Act + var result = manager.EnsureAspNetCoreApplicationTokensDevelopmentCertificate(now, now.AddYears(1), CertificateName, trust: false, includePrivateKey: true, password: certificatePassword, subject: TestCertificateSubject); + + // Assert + Assert.Equal(EnsureCertificateResult.ValidCertificatePresent, result); + Assert.True(File.Exists(CertificateName)); + + var exportedCertificate = new X509Certificate2(File.ReadAllBytes(CertificateName), certificatePassword); + Assert.NotNull(exportedCertificate); + Assert.True(exportedCertificate.HasPrivateKey); + + Assert.Equal(identityTokenSigningCertificates.GetCertHashString(), exportedCertificate.GetCertHashString()); + } + } +} diff --git a/src/Tools/FirstRunCertGenerator/test/Microsoft.AspNetCore.DeveloperCertificates.XPlat.Tests.csproj b/src/Tools/FirstRunCertGenerator/test/Microsoft.AspNetCore.DeveloperCertificates.XPlat.Tests.csproj new file mode 100644 index 000000000000..4bf1174f62ac --- /dev/null +++ b/src/Tools/FirstRunCertGenerator/test/Microsoft.AspNetCore.DeveloperCertificates.XPlat.Tests.csproj @@ -0,0 +1,11 @@ + + + + netcoreapp2.1 + + + + + + + diff --git a/src/Tools/shared/src/CliContext.cs b/src/Tools/Shared/CommandLine/CliContext.cs similarity index 100% rename from src/Tools/shared/src/CliContext.cs rename to src/Tools/Shared/CommandLine/CliContext.cs diff --git a/src/Tools/shared/src/CommandLineApplicationExtensions.cs b/src/Tools/Shared/CommandLine/CommandLineApplicationExtensions.cs similarity index 100% rename from src/Tools/shared/src/CommandLineApplicationExtensions.cs rename to src/Tools/Shared/CommandLine/CommandLineApplicationExtensions.cs diff --git a/src/Tools/shared/src/ConsoleReporter.cs b/src/Tools/Shared/CommandLine/ConsoleReporter.cs similarity index 100% rename from src/Tools/shared/src/ConsoleReporter.cs rename to src/Tools/Shared/CommandLine/ConsoleReporter.cs diff --git a/src/Tools/shared/src/DebugHelper.cs b/src/Tools/Shared/CommandLine/DebugHelper.cs similarity index 100% rename from src/Tools/shared/src/DebugHelper.cs rename to src/Tools/Shared/CommandLine/DebugHelper.cs diff --git a/src/Tools/shared/src/Ensure.cs b/src/Tools/Shared/CommandLine/Ensure.cs similarity index 100% rename from src/Tools/shared/src/Ensure.cs rename to src/Tools/Shared/CommandLine/Ensure.cs diff --git a/src/Tools/shared/src/IConsole.cs b/src/Tools/Shared/CommandLine/IConsole.cs similarity index 100% rename from src/Tools/shared/src/IConsole.cs rename to src/Tools/Shared/CommandLine/IConsole.cs diff --git a/src/Tools/shared/src/IReporter.cs b/src/Tools/Shared/CommandLine/IReporter.cs similarity index 100% rename from src/Tools/shared/src/IReporter.cs rename to src/Tools/Shared/CommandLine/IReporter.cs diff --git a/src/Tools/shared/src/NullReporter.cs b/src/Tools/Shared/CommandLine/NullReporter.cs similarity index 100% rename from src/Tools/shared/src/NullReporter.cs rename to src/Tools/Shared/CommandLine/NullReporter.cs diff --git a/src/Tools/shared/src/PhysicalConsole.cs b/src/Tools/Shared/CommandLine/PhysicalConsole.cs similarity index 100% rename from src/Tools/shared/src/PhysicalConsole.cs rename to src/Tools/Shared/CommandLine/PhysicalConsole.cs diff --git a/src/Tools/shared/test/TestConsole.cs b/src/Tools/Shared/TestHelpers/TestConsole.cs similarity index 100% rename from src/Tools/shared/test/TestConsole.cs rename to src/Tools/Shared/TestHelpers/TestConsole.cs diff --git a/src/Tools/shared/test/TestReporter.cs b/src/Tools/Shared/TestHelpers/TestReporter.cs similarity index 100% rename from src/Tools/shared/test/TestReporter.cs rename to src/Tools/Shared/TestHelpers/TestReporter.cs diff --git a/src/Tools/dotnet-dev-certs/src/dotnet-dev-certs.csproj b/src/Tools/dotnet-dev-certs/src/dotnet-dev-certs.csproj index 1f0f906e5015..77193cb47717 100644 --- a/src/Tools/dotnet-dev-certs/src/dotnet-dev-certs.csproj +++ b/src/Tools/dotnet-dev-certs/src/dotnet-dev-certs.csproj @@ -12,19 +12,11 @@ - - - - - - - - - + + - diff --git a/src/Tools/dotnet-sql-cache/src/dotnet-sql-cache.csproj b/src/Tools/dotnet-sql-cache/src/dotnet-sql-cache.csproj index 3856c4877cdf..63b64800b511 100644 --- a/src/Tools/dotnet-sql-cache/src/dotnet-sql-cache.csproj +++ b/src/Tools/dotnet-sql-cache/src/dotnet-sql-cache.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/Tools/dotnet-user-secrets/src/dotnet-user-secrets.csproj b/src/Tools/dotnet-user-secrets/src/dotnet-user-secrets.csproj index a66a31dd7efd..5e7e80d02e82 100644 --- a/src/Tools/dotnet-user-secrets/src/dotnet-user-secrets.csproj +++ b/src/Tools/dotnet-user-secrets/src/dotnet-user-secrets.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/Tools/dotnet-user-secrets/test/dotnet-user-secrets.Tests.csproj b/src/Tools/dotnet-user-secrets/test/dotnet-user-secrets.Tests.csproj index 0254a866e8f5..ea4686125b8d 100644 --- a/src/Tools/dotnet-user-secrets/test/dotnet-user-secrets.Tests.csproj +++ b/src/Tools/dotnet-user-secrets/test/dotnet-user-secrets.Tests.csproj @@ -6,7 +6,7 @@ - + diff --git a/src/Tools/dotnet-watch/src/dotnet-watch.csproj b/src/Tools/dotnet-watch/src/dotnet-watch.csproj index 59945888fb98..14f3253cdbd7 100644 --- a/src/Tools/dotnet-watch/src/dotnet-watch.csproj +++ b/src/Tools/dotnet-watch/src/dotnet-watch.csproj @@ -12,8 +12,8 @@ - + diff --git a/src/Tools/dotnet-watch/test/dotnet-watch.Tests.csproj b/src/Tools/dotnet-watch/test/dotnet-watch.Tests.csproj index 2bad8c40c4bb..ada6cec74c60 100644 --- a/src/Tools/dotnet-watch/test/dotnet-watch.Tests.csproj +++ b/src/Tools/dotnet-watch/test/dotnet-watch.Tests.csproj @@ -7,7 +7,7 @@ - + From d9be2e72edd203b761c522985e797555e47337bf Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Thu, 13 Dec 2018 10:42:22 -0800 Subject: [PATCH 03/10] Replace Microsoft.Extensions.CopyOnWriteDictionary.Sources with a local copy of code This moves shared source from aspnet/Extensions into this repo --- build/dependencies.props | 1 - build/external-dependencies.props | 1 - eng/Dependencies.props | 1 - .../Http/src/Microsoft.AspNetCore.Http.csproj | 5 +- src/Mvc/build/dependencies.props | 1 - ...crosoft.AspNetCore.Mvc.Abstractions.csproj | 5 +- ...soft.AspNetCore.Mvc.DataAnnotations.csproj | 5 +- ...Microsoft.AspNetCore.Mvc.RazorPages.csproj | 5 +- ...crosoft.AspNetCore.Mvc.ViewFeatures.csproj | 5 +- src/Razor/build/dependencies.props | 1 - .../Microsoft.AspNetCore.Razor.Runtime.csproj | 1 - .../CopyOnWriteDictionary.cs | 155 ++++++++++++++++ .../CopyOnWriteDictionaryHolder.cs | 166 ++++++++++++++++++ .../test/CopyOnWriteDictionaryHolderTest.cs | 91 ++++++++++ src/Shared/test/CopyOnWriteDictionaryTest.cs | 109 ++++++++++++ .../Microsoft.AspNetCore.Shared.Tests.csproj | 11 ++ 16 files changed, 552 insertions(+), 11 deletions(-) create mode 100644 src/Shared/CopyOnWriteDictionary/CopyOnWriteDictionary.cs create mode 100644 src/Shared/CopyOnWriteDictionary/CopyOnWriteDictionaryHolder.cs create mode 100644 src/Shared/test/CopyOnWriteDictionaryHolderTest.cs create mode 100644 src/Shared/test/CopyOnWriteDictionaryTest.cs create mode 100644 src/Shared/test/Microsoft.AspNetCore.Shared.Tests.csproj diff --git a/build/dependencies.props b/build/dependencies.props index 53eaa6fa6a94..74d1b2a14eab 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -46,7 +46,6 @@ 2.1.1 2.1.1 2.1.1 - 2.1.1 2.1.1 2.1.1 2.1.1 diff --git a/build/external-dependencies.props b/build/external-dependencies.props index 51299edcd598..8019787eee6b 100644 --- a/build/external-dependencies.props +++ b/build/external-dependencies.props @@ -35,7 +35,6 @@ - diff --git a/eng/Dependencies.props b/eng/Dependencies.props index 5ae008dde2c3..f8b6b7374626 100644 --- a/eng/Dependencies.props +++ b/eng/Dependencies.props @@ -25,7 +25,6 @@ - diff --git a/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj b/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj index 4344d0ae8eab..e9d04e1ebcb9 100644 --- a/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj +++ b/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj @@ -9,10 +9,13 @@ aspnetcore + + + + - diff --git a/src/Mvc/build/dependencies.props b/src/Mvc/build/dependencies.props index 3e21512352a2..10b657425ca7 100644 --- a/src/Mvc/build/dependencies.props +++ b/src/Mvc/build/dependencies.props @@ -74,7 +74,6 @@ 2.1.1 2.1.1 2.1.1 - 2.1.1 2.1.1 2.1.0 2.1.1 diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Abstractions/Microsoft.AspNetCore.Mvc.Abstractions.csproj b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Abstractions/Microsoft.AspNetCore.Mvc.Abstractions.csproj index 4ebbaeec61b1..49c1c013769b 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Abstractions/Microsoft.AspNetCore.Mvc.Abstractions.csproj +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Abstractions/Microsoft.AspNetCore.Mvc.Abstractions.csproj @@ -10,10 +10,13 @@ Microsoft.AspNetCore.Mvc.IActionResult aspnetcore;aspnetcoremvc + + + + - diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Microsoft.AspNetCore.Mvc.DataAnnotations.csproj b/src/Mvc/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Microsoft.AspNetCore.Mvc.DataAnnotations.csproj index 336474aae07a..1b46947fc963 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Microsoft.AspNetCore.Mvc.DataAnnotations.csproj +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Microsoft.AspNetCore.Mvc.DataAnnotations.csproj @@ -8,11 +8,14 @@ aspnetcore;aspnetcoremvc + + + + - diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/Microsoft.AspNetCore.Mvc.RazorPages.csproj b/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/Microsoft.AspNetCore.Mvc.RazorPages.csproj index f0be433ea554..88bf2d885ef3 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/Microsoft.AspNetCore.Mvc.RazorPages.csproj +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/Microsoft.AspNetCore.Mvc.RazorPages.csproj @@ -8,11 +8,14 @@ aspnetcore;aspnetcoremvc;cshtml;razor + + + + - diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Microsoft.AspNetCore.Mvc.ViewFeatures.csproj b/src/Mvc/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Microsoft.AspNetCore.Mvc.ViewFeatures.csproj index 27d839320d99..ac83154373c5 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Microsoft.AspNetCore.Mvc.ViewFeatures.csproj +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Microsoft.AspNetCore.Mvc.ViewFeatures.csproj @@ -13,6 +13,10 @@ Microsoft.AspNetCore.Mvc.ViewComponent aspnetcore;aspnetcoremvc + + + + @@ -23,7 +27,6 @@ Microsoft.AspNetCore.Mvc.ViewComponent - diff --git a/src/Razor/build/dependencies.props b/src/Razor/build/dependencies.props index e128924edba6..294cf2f14e4a 100644 --- a/src/Razor/build/dependencies.props +++ b/src/Razor/build/dependencies.props @@ -65,7 +65,6 @@ 2.1.1 2.1.0 2.1.1 - 2.1.1 2.1.1 2.1.1 diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Runtime/Microsoft.AspNetCore.Razor.Runtime.csproj b/src/Razor/src/Microsoft.AspNetCore.Razor.Runtime/Microsoft.AspNetCore.Razor.Runtime.csproj index b58a7f9c3f1f..e3102cbf8ffc 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Runtime/Microsoft.AspNetCore.Razor.Runtime.csproj +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Runtime/Microsoft.AspNetCore.Razor.Runtime.csproj @@ -12,7 +12,6 @@ - diff --git a/src/Shared/CopyOnWriteDictionary/CopyOnWriteDictionary.cs b/src/Shared/CopyOnWriteDictionary/CopyOnWriteDictionary.cs new file mode 100644 index 000000000000..1408059ad923 --- /dev/null +++ b/src/Shared/CopyOnWriteDictionary/CopyOnWriteDictionary.cs @@ -0,0 +1,155 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Microsoft.Extensions.Internal +{ + internal class CopyOnWriteDictionary : IDictionary + { + private readonly IDictionary _sourceDictionary; + private readonly IEqualityComparer _comparer; + private IDictionary _innerDictionary; + + public CopyOnWriteDictionary( + IDictionary sourceDictionary, + IEqualityComparer comparer) + { + if (sourceDictionary == null) + { + throw new ArgumentNullException(nameof(sourceDictionary)); + } + + if (comparer == null) + { + throw new ArgumentNullException(nameof(comparer)); + } + + _sourceDictionary = sourceDictionary; + _comparer = comparer; + } + + private IDictionary ReadDictionary + { + get + { + return _innerDictionary ?? _sourceDictionary; + } + } + + private IDictionary WriteDictionary + { + get + { + if (_innerDictionary == null) + { + _innerDictionary = new Dictionary(_sourceDictionary, + _comparer); + } + + return _innerDictionary; + } + } + + public virtual ICollection Keys + { + get + { + return ReadDictionary.Keys; + } + } + + public virtual ICollection Values + { + get + { + return ReadDictionary.Values; + } + } + + public virtual int Count + { + get + { + return ReadDictionary.Count; + } + } + + public virtual bool IsReadOnly + { + get + { + return false; + } + } + + public virtual TValue this[TKey key] + { + get + { + return ReadDictionary[key]; + } + set + { + WriteDictionary[key] = value; + } + } + + public virtual bool ContainsKey(TKey key) + { + return ReadDictionary.ContainsKey(key); + } + + public virtual void Add(TKey key, TValue value) + { + WriteDictionary.Add(key, value); + } + + public virtual bool Remove(TKey key) + { + return WriteDictionary.Remove(key); + } + + public virtual bool TryGetValue(TKey key, out TValue value) + { + return ReadDictionary.TryGetValue(key, out value); + } + + public virtual void Add(KeyValuePair item) + { + WriteDictionary.Add(item); + } + + public virtual void Clear() + { + WriteDictionary.Clear(); + } + + public virtual bool Contains(KeyValuePair item) + { + return ReadDictionary.Contains(item); + } + + public virtual void CopyTo(KeyValuePair[] array, int arrayIndex) + { + ReadDictionary.CopyTo(array, arrayIndex); + } + + public bool Remove(KeyValuePair item) + { + return WriteDictionary.Remove(item); + } + + public virtual IEnumerator> GetEnumerator() + { + return ReadDictionary.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/src/Shared/CopyOnWriteDictionary/CopyOnWriteDictionaryHolder.cs b/src/Shared/CopyOnWriteDictionary/CopyOnWriteDictionaryHolder.cs new file mode 100644 index 000000000000..7cd935e94018 --- /dev/null +++ b/src/Shared/CopyOnWriteDictionary/CopyOnWriteDictionaryHolder.cs @@ -0,0 +1,166 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.Extensions.Internal +{ + internal struct CopyOnWriteDictionaryHolder + { + private readonly Dictionary _source; + private Dictionary _copy; + + public CopyOnWriteDictionaryHolder(Dictionary source) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + _source = source; + _copy = null; + } + + public CopyOnWriteDictionaryHolder(CopyOnWriteDictionaryHolder source) + { + _source = source._copy ?? source._source; + _copy = null; + } + + public bool HasBeenCopied => _copy != null; + + public Dictionary ReadDictionary + { + get + { + if (_copy != null) + { + return _copy; + } + else if (_source != null) + { + return _source; + } + else + { + // Default-Constructor case + _copy = new Dictionary(); + return _copy; + } + } + } + + public Dictionary WriteDictionary + { + get + { + if (_copy == null && _source == null) + { + // Default-Constructor case + _copy = new Dictionary(); + } + else if (_copy == null) + { + _copy = new Dictionary(_source, _source.Comparer); + } + + return _copy; + } + } + + public Dictionary.KeyCollection Keys + { + get + { + return ReadDictionary.Keys; + } + } + + public Dictionary.ValueCollection Values + { + get + { + return ReadDictionary.Values; + } + } + + public int Count + { + get + { + return ReadDictionary.Count; + } + } + + public bool IsReadOnly + { + get + { + return false; + } + } + + public TValue this[TKey key] + { + get + { + return ReadDictionary[key]; + } + set + { + WriteDictionary[key] = value; + } + } + + public bool ContainsKey(TKey key) + { + return ReadDictionary.ContainsKey(key); + } + + public void Add(TKey key, TValue value) + { + WriteDictionary.Add(key, value); + } + + public bool Remove(TKey key) + { + return WriteDictionary.Remove(key); + } + + public bool TryGetValue(TKey key, out TValue value) + { + return ReadDictionary.TryGetValue(key, out value); + } + + public void Add(KeyValuePair item) + { + ((ICollection>)WriteDictionary).Add(item); + } + + public void Clear() + { + WriteDictionary.Clear(); + } + + public bool Contains(KeyValuePair item) + { + return ((ICollection>)ReadDictionary).Contains(item); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + ((ICollection>)ReadDictionary).CopyTo(array, arrayIndex); + } + + public bool Remove(KeyValuePair item) + { + return ((ICollection>)WriteDictionary).Remove(item); + } + + public Dictionary.Enumerator GetEnumerator() + { + return ReadDictionary.GetEnumerator(); + } + } +} diff --git a/src/Shared/test/CopyOnWriteDictionaryHolderTest.cs b/src/Shared/test/CopyOnWriteDictionaryHolderTest.cs new file mode 100644 index 000000000000..9a0951eb271a --- /dev/null +++ b/src/Shared/test/CopyOnWriteDictionaryHolderTest.cs @@ -0,0 +1,91 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Microsoft.Extensions.Internal +{ + public class CopyOnWriteDictionaryHolderTest + { + [Fact] + public void ReadOperation_DelegatesToSourceDictionary_IfNoMutationsArePerformed() + { + // Arrange + var source = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "test-key", "test-value" }, + { "key2", "key2-value" } + }; + + var holder = new CopyOnWriteDictionaryHolder(source); + + // Act and Assert + Assert.Equal("key2-value", holder["key2"]); + Assert.Equal(2, holder.Count); + Assert.Equal(new string[] { "test-key", "key2" }, holder.Keys.ToArray()); + Assert.Equal(new object[] { "test-value", "key2-value" }, holder.Values.ToArray()); + Assert.True(holder.ContainsKey("test-key")); + + object value; + Assert.False(holder.TryGetValue("different-key", out value)); + + Assert.False(holder.HasBeenCopied); + Assert.Same(source, holder.ReadDictionary); + } + + [Fact] + public void ReadOperation_DoesNotDelegateToSourceDictionary_OnceAValueIsChanged() + { + // Arrange + var source = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "key1", "value1" }, + { "key2", "value2" } + }; + + var holder = new CopyOnWriteDictionaryHolder(source); + + // Act + holder["key2"] = "value3"; + + // Assert + Assert.Equal("value2", source["key2"]); + Assert.Equal(2, holder.Count); + Assert.Equal("value1", holder["key1"]); + Assert.Equal("value3", holder["key2"]); + + Assert.True(holder.HasBeenCopied); + Assert.NotSame(source, holder.ReadDictionary); + } + + [Fact] + public void ReadOperation_DoesNotDelegateToSourceDictionary_OnceValueIsAdded() + { + // Arrange + var source = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "key1", "value1" }, + { "key2", "value2" } + }; + + var holder = new CopyOnWriteDictionaryHolder(source); + + // Act + holder.Add("key3", "value3"); + holder.Remove("key1"); + + // Assert + Assert.Equal(2, source.Count); + Assert.Equal("value1", source["key1"]); + Assert.Equal(2, holder.Count); + Assert.Equal("value2", holder["KeY2"]); + Assert.Equal("value3", holder["key3"]); + + Assert.True(holder.HasBeenCopied); + Assert.NotSame(source, holder.ReadDictionary); + } + } +} diff --git a/src/Shared/test/CopyOnWriteDictionaryTest.cs b/src/Shared/test/CopyOnWriteDictionaryTest.cs new file mode 100644 index 000000000000..c1b54036d4f9 --- /dev/null +++ b/src/Shared/test/CopyOnWriteDictionaryTest.cs @@ -0,0 +1,109 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Internal +{ + public class CopyOnWriteDictionaryTest + { + [Fact] + public void ReadOperation_DelegatesToSourceDictionary_IfNoMutationsArePerformed() + { + // Arrange + var values = new List(); + var enumerator = Mock.Of>>(); + var sourceDictionary = new Mock>(MockBehavior.Strict); + sourceDictionary + .SetupGet(d => d.Count) + .Returns(100) + .Verifiable(); + sourceDictionary + .SetupGet(d => d.Values) + .Returns(values) + .Verifiable(); + sourceDictionary + .Setup(d => d.ContainsKey("test-key")) + .Returns(value: true) + .Verifiable(); + sourceDictionary + .Setup(d => d.GetEnumerator()) + .Returns(enumerator) + .Verifiable(); + sourceDictionary + .Setup(d => d["key2"]) + .Returns("key2-value") + .Verifiable(); + object value; + sourceDictionary.Setup(d => d.TryGetValue("different-key", out value)) + .Returns(false) + .Verifiable(); + + var copyOnWriteDictionary = new CopyOnWriteDictionary(sourceDictionary.Object, + StringComparer.OrdinalIgnoreCase); + + // Act and Assert + Assert.Equal("key2-value", copyOnWriteDictionary["key2"]); + Assert.Equal(100, copyOnWriteDictionary.Count); + Assert.Same(values, copyOnWriteDictionary.Values); + Assert.True(copyOnWriteDictionary.ContainsKey("test-key")); + Assert.Same(enumerator, copyOnWriteDictionary.GetEnumerator()); + Assert.False(copyOnWriteDictionary.TryGetValue("different-key", out value)); + sourceDictionary.Verify(); + } + + [Fact] + public void ReadOperation_DoesNotDelegateToSourceDictionary_OnceAValueIsChanged() + { + // Arrange + var values = new List(); + var sourceDictionary = new Dictionary + { + { "key1", "value1" }, + { "key2", "value2" } + }; + var copyOnWriteDictionary = new CopyOnWriteDictionary( + sourceDictionary, + StringComparer.OrdinalIgnoreCase); + + // Act + copyOnWriteDictionary["key2"] = "value3"; + + // Assert + Assert.Equal("value2", sourceDictionary["key2"]); + Assert.Equal(2, copyOnWriteDictionary.Count); + Assert.Equal("value1", copyOnWriteDictionary["key1"]); + Assert.Equal("value3", copyOnWriteDictionary["key2"]); + } + + [Fact] + public void ReadOperation_DoesNotDelegateToSourceDictionary_OnceDictionaryIsModified() + { + // Arrange + var values = new List(); + var sourceDictionary = new Dictionary + { + { "key1", "value1" }, + { "key2", "value2" } + }; + var copyOnWriteDictionary = new CopyOnWriteDictionary( + sourceDictionary, + StringComparer.OrdinalIgnoreCase); + + // Act + copyOnWriteDictionary.Add("key3", "value3"); + copyOnWriteDictionary.Remove("key1"); + + + // Assert + Assert.Equal(2, sourceDictionary.Count); + Assert.Equal("value1", sourceDictionary["key1"]); + Assert.Equal(2, copyOnWriteDictionary.Count); + Assert.Equal("value2", copyOnWriteDictionary["KeY2"]); + Assert.Equal("value3", copyOnWriteDictionary["key3"]); + } + } +} diff --git a/src/Shared/test/Microsoft.AspNetCore.Shared.Tests.csproj b/src/Shared/test/Microsoft.AspNetCore.Shared.Tests.csproj new file mode 100644 index 000000000000..c7472c372527 --- /dev/null +++ b/src/Shared/test/Microsoft.AspNetCore.Shared.Tests.csproj @@ -0,0 +1,11 @@ + + + + netcoreapp2.1;net461 + + + + + + + From b469be7a44af43f31f58c4e34581bcf44dac7a9d Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Thu, 13 Dec 2018 10:58:59 -0800 Subject: [PATCH 04/10] Replace Microsoft.Extensions.ClosedGenericMatcher.Sources with a local copy of code --- build/dependencies.props | 1 - build/external-dependencies.props | 1 - eng/Dependencies.props | 1 - .../src/Microsoft.AspNetCore.JsonPatch.csproj | 5 +- src/Mvc/Directory.Build.props | 1 + src/Mvc/build/dependencies.props | 1 - ...crosoft.AspNetCore.Mvc.Abstractions.csproj | 3 +- ...icrosoft.AspNetCore.Mvc.ApiExplorer.csproj | 5 +- .../Microsoft.AspNetCore.Mvc.Core.csproj | 4 +- ...soft.AspNetCore.Mvc.DataAnnotations.csproj | 2 +- ...soft.AspNetCore.Mvc.Formatters.Json.csproj | 5 +- ...osoft.AspNetCore.Mvc.Formatters.Xml.csproj | 5 +- .../Microsoft.AspNetCore.Mvc.Razor.csproj | 5 +- ...Microsoft.AspNetCore.Mvc.RazorPages.csproj | 2 +- ...crosoft.AspNetCore.Mvc.ViewFeatures.csproj | 2 +- .../ClosedGenericMatcher.cs | 106 ++++++ src/Shared/test/ClosedGenericMatcherTest.cs | 360 ++++++++++++++++++ src/SignalR/Directory.Build.props | 1 + src/SignalR/build/dependencies.props | 3 +- .../Microsoft.AspNetCore.SignalR.Core.csproj | 5 +- 20 files changed, 500 insertions(+), 18 deletions(-) create mode 100644 src/Shared/ClosedGenericMatcher/ClosedGenericMatcher.cs create mode 100644 src/Shared/test/ClosedGenericMatcherTest.cs diff --git a/build/dependencies.props b/build/dependencies.props index 74d1b2a14eab..7d43787c159c 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -32,7 +32,6 @@ 2.1.2 2.1.2 2.1.2 - 2.1.1 2.1.1 2.1.1 2.1.1 diff --git a/build/external-dependencies.props b/build/external-dependencies.props index 8019787eee6b..290e68d55f88 100644 --- a/build/external-dependencies.props +++ b/build/external-dependencies.props @@ -21,7 +21,6 @@ - diff --git a/eng/Dependencies.props b/eng/Dependencies.props index f8b6b7374626..da9a801a07ca 100644 --- a/eng/Dependencies.props +++ b/eng/Dependencies.props @@ -18,7 +18,6 @@ - diff --git a/src/Features/JsonPatch/src/Microsoft.AspNetCore.JsonPatch.csproj b/src/Features/JsonPatch/src/Microsoft.AspNetCore.JsonPatch.csproj index 3708e92927ef..f6f11fd3b3fc 100644 --- a/src/Features/JsonPatch/src/Microsoft.AspNetCore.JsonPatch.csproj +++ b/src/Features/JsonPatch/src/Microsoft.AspNetCore.JsonPatch.csproj @@ -8,10 +8,13 @@ aspnetcore;json;jsonpatch + + + + - diff --git a/src/Mvc/Directory.Build.props b/src/Mvc/Directory.Build.props index fad2bb06104e..db07d3358bd2 100644 --- a/src/Mvc/Directory.Build.props +++ b/src/Mvc/Directory.Build.props @@ -16,6 +16,7 @@ true true true + $(MSBuildThisFileDirectory)..\Shared\ diff --git a/src/Mvc/build/dependencies.props b/src/Mvc/build/dependencies.props index 10b657425ca7..b3c6ff450660 100644 --- a/src/Mvc/build/dependencies.props +++ b/src/Mvc/build/dependencies.props @@ -71,7 +71,6 @@ 2.1.1 2.1.1 2.1.1 - 2.1.1 2.1.1 2.1.1 2.1.1 diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Abstractions/Microsoft.AspNetCore.Mvc.Abstractions.csproj b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Abstractions/Microsoft.AspNetCore.Mvc.Abstractions.csproj index 49c1c013769b..f981b4b8a158 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Abstractions/Microsoft.AspNetCore.Mvc.Abstractions.csproj +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Abstractions/Microsoft.AspNetCore.Mvc.Abstractions.csproj @@ -11,12 +11,13 @@ Microsoft.AspNetCore.Mvc.IActionResult + + - diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.ApiExplorer/Microsoft.AspNetCore.Mvc.ApiExplorer.csproj b/src/Mvc/src/Microsoft.AspNetCore.Mvc.ApiExplorer/Microsoft.AspNetCore.Mvc.ApiExplorer.csproj index 9ea5be90f49c..efd7d873c4e1 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.ApiExplorer/Microsoft.AspNetCore.Mvc.ApiExplorer.csproj +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.ApiExplorer/Microsoft.AspNetCore.Mvc.ApiExplorer.csproj @@ -8,10 +8,13 @@ aspnetcore;aspnetcoremvc + + + + - diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Microsoft.AspNetCore.Mvc.Core.csproj b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Microsoft.AspNetCore.Mvc.Core.csproj index b63ba839ef09..6a044b4cf4f8 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Microsoft.AspNetCore.Mvc.Core.csproj +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Microsoft.AspNetCore.Mvc.Core.csproj @@ -17,7 +17,8 @@ Microsoft.AspNetCore.Mvc.RouteAttribute - + + @@ -30,7 +31,6 @@ Microsoft.AspNetCore.Mvc.RouteAttribute - diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Microsoft.AspNetCore.Mvc.DataAnnotations.csproj b/src/Mvc/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Microsoft.AspNetCore.Mvc.DataAnnotations.csproj index 1b46947fc963..542474780eae 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Microsoft.AspNetCore.Mvc.DataAnnotations.csproj +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Microsoft.AspNetCore.Mvc.DataAnnotations.csproj @@ -10,12 +10,12 @@ + - diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Formatters.Json/Microsoft.AspNetCore.Mvc.Formatters.Json.csproj b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Formatters.Json/Microsoft.AspNetCore.Mvc.Formatters.Json.csproj index 608e37025741..c57c948d9ea0 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Formatters.Json/Microsoft.AspNetCore.Mvc.Formatters.Json.csproj +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Formatters.Json/Microsoft.AspNetCore.Mvc.Formatters.Json.csproj @@ -8,10 +8,13 @@ aspnetcore;aspnetcoremvc;json + + + + - diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/Microsoft.AspNetCore.Mvc.Formatters.Xml.csproj b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/Microsoft.AspNetCore.Mvc.Formatters.Xml.csproj index b716f5cbbb81..5dcb593dee39 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/Microsoft.AspNetCore.Mvc.Formatters.Xml.csproj +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/Microsoft.AspNetCore.Mvc.Formatters.Xml.csproj @@ -8,10 +8,13 @@ aspnetcore;aspnetcoremvc;xml + + + + - diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor/Microsoft.AspNetCore.Mvc.Razor.csproj b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor/Microsoft.AspNetCore.Mvc.Razor.csproj index 74e943a8d9d3..069a8f9292dd 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor/Microsoft.AspNetCore.Mvc.Razor.csproj +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor/Microsoft.AspNetCore.Mvc.Razor.csproj @@ -10,6 +10,10 @@ true + + + + @@ -19,7 +23,6 @@ - diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/Microsoft.AspNetCore.Mvc.RazorPages.csproj b/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/Microsoft.AspNetCore.Mvc.RazorPages.csproj index 88bf2d885ef3..6b859f0d5ba1 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/Microsoft.AspNetCore.Mvc.RazorPages.csproj +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/Microsoft.AspNetCore.Mvc.RazorPages.csproj @@ -9,13 +9,13 @@ + - diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Microsoft.AspNetCore.Mvc.ViewFeatures.csproj b/src/Mvc/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Microsoft.AspNetCore.Mvc.ViewFeatures.csproj index ac83154373c5..fce88c27cc79 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Microsoft.AspNetCore.Mvc.ViewFeatures.csproj +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Microsoft.AspNetCore.Mvc.ViewFeatures.csproj @@ -14,6 +14,7 @@ Microsoft.AspNetCore.Mvc.ViewComponent + @@ -26,7 +27,6 @@ Microsoft.AspNetCore.Mvc.ViewComponent - diff --git a/src/Shared/ClosedGenericMatcher/ClosedGenericMatcher.cs b/src/Shared/ClosedGenericMatcher/ClosedGenericMatcher.cs new file mode 100644 index 000000000000..f234c2edbc40 --- /dev/null +++ b/src/Shared/ClosedGenericMatcher/ClosedGenericMatcher.cs @@ -0,0 +1,106 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Reflection; + +namespace Microsoft.Extensions.Internal +{ + /// + /// Helper related to generic interface definitions and implementing classes. + /// + internal static class ClosedGenericMatcher + { + /// + /// Determine whether is or implements a closed generic + /// created from . + /// + /// The of interest. + /// The open generic to match. Usually an interface. + /// + /// The closed generic created from that + /// is or implements. null if the two s have no such + /// relationship. + /// + /// + /// This method will return if is + /// typeof(KeyValuePair{,}), and is + /// typeof(KeyValuePair{string, object}). + /// + public static Type ExtractGenericInterface(Type queryType, Type interfaceType) + { + if (queryType == null) + { + throw new ArgumentNullException(nameof(queryType)); + } + + if (interfaceType == null) + { + throw new ArgumentNullException(nameof(interfaceType)); + } + + if (IsGenericInstantiation(queryType, interfaceType)) + { + // queryType matches (i.e. is a closed generic type created from) the open generic type. + return queryType; + } + + // Otherwise check all interfaces the type implements for a match. + // - If multiple different generic instantiations exists, we want the most derived one. + // - If that doesn't break the tie, then we sort alphabetically so that it's deterministic. + // + // We do this by looking at interfaces on the type, and recursing to the base type + // if we don't find any matches. + return GetGenericInstantiation(queryType, interfaceType); + } + + private static bool IsGenericInstantiation(Type candidate, Type interfaceType) + { + return + candidate.GetTypeInfo().IsGenericType && + candidate.GetGenericTypeDefinition() == interfaceType; + } + + private static Type GetGenericInstantiation(Type queryType, Type interfaceType) + { + Type bestMatch = null; + var interfaces = queryType.GetInterfaces(); + foreach (var @interface in interfaces) + { + if (IsGenericInstantiation(@interface, interfaceType)) + { + if (bestMatch == null) + { + bestMatch = @interface; + } + else if (StringComparer.Ordinal.Compare(@interface.FullName, bestMatch.FullName) < 0) + { + bestMatch = @interface; + } + else + { + // There are two matches at this level of the class hierarchy, but @interface is after + // bestMatch in the sort order. + } + } + } + + if (bestMatch != null) + { + return bestMatch; + } + + // BaseType will be null for object and interfaces, which means we've reached 'bottom'. + var baseType = queryType?.GetTypeInfo().BaseType; + if (baseType == null) + { + return null; + } + else + { + return GetGenericInstantiation(baseType, interfaceType); + } + } + } +} \ No newline at end of file diff --git a/src/Shared/test/ClosedGenericMatcherTest.cs b/src/Shared/test/ClosedGenericMatcherTest.cs new file mode 100644 index 000000000000..e71a7926921a --- /dev/null +++ b/src/Shared/test/ClosedGenericMatcherTest.cs @@ -0,0 +1,360 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Xunit; + +namespace Microsoft.Extensions.Internal +{ + public class ClosedGenericMatcherTest + { + // queryType, interfaceType, expectedResult + public static TheoryData ExtractGenericInterfaceDataSet + { + get + { + return new TheoryData + { + // Closed generic types that match given open generic type. + { + typeof(IEnumerable), + typeof(IEnumerable<>), + typeof(IEnumerable) + }, + { + typeof(IReadOnlyList), + typeof(IReadOnlyList<>), + typeof(IReadOnlyList) + }, + { + typeof(KeyValuePair), + typeof(KeyValuePair<,>), + typeof(KeyValuePair) + }, + // Closed generic interfaces that implement sub-interface of given open generic type. + { + typeof(ICollection), + typeof(IEnumerable<>), + typeof(IEnumerable) + }, + { + typeof(IReadOnlyList), + typeof(IEnumerable<>), + typeof(IEnumerable) + }, + { + typeof(IDictionary), + typeof(IEnumerable<>), + typeof(IEnumerable>) + }, + // Class that implements closed generic based on given open generic interface. + { + typeof(BaseClass), + typeof(IDictionary<,>), + typeof(IDictionary) + }, + { + typeof(BaseClass), + typeof(IEquatable<>), + typeof(IEquatable) + }, + { + typeof(BaseClass), + typeof(ICollection<>), + typeof(ICollection>) + }, + // Derived class that implements closed generic based on given open generic interface. + { + typeof(DerivedClass), + typeof(IDictionary<,>), + typeof(IDictionary) + }, + { + typeof(DerivedClass), + typeof(IEquatable<>), + typeof(IEquatable) + }, + { + typeof(DerivedClass), + typeof(ICollection<>), + typeof(ICollection>) + }, + // Derived class that also implements another interface. + { + typeof(DerivedClassWithComparable), + typeof(IDictionary<,>), + typeof(IDictionary) + }, + { + typeof(DerivedClassWithComparable), + typeof(IEquatable<>), + typeof(IEquatable) + }, + { + typeof(DerivedClassWithComparable), + typeof(ICollection<>), + typeof(ICollection>) + }, + { + typeof(DerivedClassWithComparable), + typeof(IComparable<>), + typeof(IComparable) + }, + // Derived class using system implementation. + { + typeof(DerivedClassFromSystemImplementation), + typeof(ICollection<>), + typeof(ICollection) + }, + { + typeof(DerivedClassFromSystemImplementation), + typeof(IReadOnlyList<>), + typeof(IReadOnlyList) + }, + { + typeof(DerivedClassFromSystemImplementation), + typeof(IEnumerable<>), + typeof(IEnumerable) + }, + // Not given an open generic type. + { + typeof(IEnumerable), + typeof(IEnumerable), + null + }, + { + typeof(IEnumerable), + typeof(IEnumerable), + null + }, + { + typeof(IReadOnlyList), + typeof(BaseClass), + null + }, + { + typeof(KeyValuePair<,>), + typeof(KeyValuePair), + null + }, + // Not a match. + { + typeof(IEnumerable), + typeof(IReadOnlyList<>), + null + }, + { + typeof(IList), + typeof(IReadOnlyList<>), + null + }, + { + typeof(IDictionary), + typeof(KeyValuePair<,>), + null + }, + }; + } + } + + [Theory] + [MemberData(nameof(ExtractGenericInterfaceDataSet))] + public void ExtractGenericInterface_ReturnsExpectedType( + Type queryType, + Type interfaceType, + Type expectedResult) + { + // Arrange & Act + var result = ClosedGenericMatcher.ExtractGenericInterface(queryType, interfaceType); + + // Assert + Assert.Equal(expectedResult, result); + } + + // IEnumerable is preferred because it is defined on the more-derived type. + [Fact] + public void ExtractGenericInterface_MultipleDefinitionsInherited() + { + // Arrange + var type = typeof(TwoIEnumerableImplementationsInherited); + + // Act + var result = ClosedGenericMatcher.ExtractGenericInterface(type, typeof(IEnumerable<>)); + + // Sort + Assert.Equal(typeof(IEnumerable), result); + } + + // IEnumerable is preferred because we sort by Ordinal on the full name. + [Fact] + public void ExtractGenericInterface_MultipleDefinitionsOnSameType() + { + // Arrange + var type = typeof(TwoIEnumerableImplementationsOnSameClass); + + // Act + var result = ClosedGenericMatcher.ExtractGenericInterface(type, typeof(IEnumerable<>)); + + // Sort + Assert.Equal(typeof(IEnumerable), result); + } + + private class TwoIEnumerableImplementationsOnSameClass : IEnumerable, IEnumerable + { + IEnumerator IEnumerable.GetEnumerator() + { + throw new NotImplementedException(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + throw new NotImplementedException(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + throw new NotImplementedException(); + } + } + + private class TwoIEnumerableImplementationsInherited : List, IEnumerable + { + IEnumerator IEnumerable.GetEnumerator() + { + throw new NotImplementedException(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + throw new NotImplementedException(); + } + } + + private class BaseClass : IDictionary, IEquatable + { + object IDictionary.this[string key] + { + get + { + throw new NotImplementedException(); + } + + set + { + throw new NotImplementedException(); + } + } + + int ICollection>.Count + { + get + { + throw new NotImplementedException(); + } + } + + bool ICollection>.IsReadOnly + { + get + { + throw new NotImplementedException(); + } + } + + ICollection IDictionary.Keys + { + get + { + throw new NotImplementedException(); + } + } + + ICollection IDictionary.Values + { + get + { + throw new NotImplementedException(); + } + } + + public bool Equals(BaseClass other) + { + throw new NotImplementedException(); + } + + void ICollection>.Add(KeyValuePair item) + { + throw new NotImplementedException(); + } + + void IDictionary.Add(string key, object value) + { + throw new NotImplementedException(); + } + + void ICollection>.Clear() + { + throw new NotImplementedException(); + } + + bool ICollection>.Contains(KeyValuePair item) + { + throw new NotImplementedException(); + } + + bool IDictionary.ContainsKey(string key) + { + throw new NotImplementedException(); + } + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + throw new NotImplementedException(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + throw new NotImplementedException(); + } + + IEnumerator> IEnumerable>.GetEnumerator() + { + throw new NotImplementedException(); + } + + bool ICollection>.Remove(KeyValuePair item) + { + throw new NotImplementedException(); + } + + bool IDictionary.Remove(string key) + { + throw new NotImplementedException(); + } + + bool IDictionary.TryGetValue(string key, out object value) + { + throw new NotImplementedException(); + } + } + + private class DerivedClass : BaseClass + { + } + + private class DerivedClassWithComparable : DerivedClass, IComparable + { + public int CompareTo(DerivedClassWithComparable other) + { + throw new NotImplementedException(); + } + } + + private class DerivedClassFromSystemImplementation : Collection + { + } + } +} \ No newline at end of file diff --git a/src/SignalR/Directory.Build.props b/src/SignalR/Directory.Build.props index 3a47101b913c..19cc48835fc9 100644 --- a/src/SignalR/Directory.Build.props +++ b/src/SignalR/Directory.Build.props @@ -17,5 +17,6 @@ true true latest + $(MSBuildThisFileDirectory)..\Shared\ diff --git a/src/SignalR/build/dependencies.props b/src/SignalR/build/dependencies.props index a68a5f5ec185..6cefc3fe3d93 100644 --- a/src/SignalR/build/dependencies.props +++ b/src/SignalR/build/dependencies.props @@ -65,7 +65,6 @@ 2.1.1 2.1.1 2.1.1 - 2.1.1 2.1.1 2.1.1 2.1.1 @@ -84,4 +83,4 @@ 2.1.1 2.1.1 - \ No newline at end of file + diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/Microsoft.AspNetCore.SignalR.Core.csproj b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/Microsoft.AspNetCore.SignalR.Core.csproj index d63b5aed428a..d9d9af7b69f6 100644 --- a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/Microsoft.AspNetCore.SignalR.Core.csproj +++ b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/Microsoft.AspNetCore.SignalR.Core.csproj @@ -6,6 +6,10 @@ Microsoft.AspNetCore.SignalR + + + + @@ -15,7 +19,6 @@ - From 9e1aee8057aaa25409d2c1b38c80bbb4b74bfa7e Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Thu, 13 Dec 2018 11:07:14 -0800 Subject: [PATCH 05/10] Replace Microsoft.Extensions.ObjectMethodExecutor.Sources with a local copy of code --- build/dependencies.props | 1 - build/external-dependencies.props | 1 - eng/Dependencies.props | 1 + src/Mvc/build/dependencies.props | 1 - .../Microsoft.AspNetCore.Mvc.Core.csproj | 4 +- ...crosoft.AspNetCore.Mvc.ViewFeatures.csproj | 2 +- .../ObjectMethodExecutor/AwaitableInfo.cs | 127 ++++ .../CoercedAwaitableInfo.cs | 55 ++ .../ObjectMethodExecutor.cs | 340 ++++++++++ .../ObjectMethodExecutorAwaitable.cs | 114 ++++ .../ObjectMethodExecutorFSharpSupport.cs | 151 +++++ .../Microsoft.AspNetCore.Shared.Tests.csproj | 6 + src/Shared/test/ObjectMethodExecutorTest.cs | 634 ++++++++++++++++++ src/SignalR/build/dependencies.props | 1 - .../Microsoft.AspNetCore.SignalR.Core.csproj | 2 +- 15 files changed, 1432 insertions(+), 8 deletions(-) create mode 100644 src/Shared/ObjectMethodExecutor/AwaitableInfo.cs create mode 100644 src/Shared/ObjectMethodExecutor/CoercedAwaitableInfo.cs create mode 100644 src/Shared/ObjectMethodExecutor/ObjectMethodExecutor.cs create mode 100644 src/Shared/ObjectMethodExecutor/ObjectMethodExecutorAwaitable.cs create mode 100644 src/Shared/ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs create mode 100644 src/Shared/test/ObjectMethodExecutorTest.cs diff --git a/build/dependencies.props b/build/dependencies.props index 7d43787c159c..0222f0c6bb58 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -70,7 +70,6 @@ 2.1.1 2.1.1 2.1.1 - 2.1.1 2.1.6 2.1.1 2.1.1 diff --git a/build/external-dependencies.props b/build/external-dependencies.props index 290e68d55f88..d73865747c2b 100644 --- a/build/external-dependencies.props +++ b/build/external-dependencies.props @@ -59,7 +59,6 @@ - diff --git a/eng/Dependencies.props b/eng/Dependencies.props index da9a801a07ca..8cc1807b8b6b 100644 --- a/eng/Dependencies.props +++ b/eng/Dependencies.props @@ -60,6 +60,7 @@ + diff --git a/src/Mvc/build/dependencies.props b/src/Mvc/build/dependencies.props index b3c6ff450660..79feafc75023 100644 --- a/src/Mvc/build/dependencies.props +++ b/src/Mvc/build/dependencies.props @@ -86,7 +86,6 @@ 2.1.1 2.1.1 2.1.1 - 2.1.1 2.1.1 2.1.1 2.1.1 diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Microsoft.AspNetCore.Mvc.Core.csproj b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Microsoft.AspNetCore.Mvc.Core.csproj index 6a044b4cf4f8..7c6c5cc2a384 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Microsoft.AspNetCore.Mvc.Core.csproj +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Microsoft.AspNetCore.Mvc.Core.csproj @@ -17,8 +17,9 @@ Microsoft.AspNetCore.Mvc.RouteAttribute - + + @@ -36,7 +37,6 @@ Microsoft.AspNetCore.Mvc.RouteAttribute - diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Microsoft.AspNetCore.Mvc.ViewFeatures.csproj b/src/Mvc/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Microsoft.AspNetCore.Mvc.ViewFeatures.csproj index fce88c27cc79..fb7dd6c50f9c 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Microsoft.AspNetCore.Mvc.ViewFeatures.csproj +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Microsoft.AspNetCore.Mvc.ViewFeatures.csproj @@ -16,6 +16,7 @@ Microsoft.AspNetCore.Mvc.ViewComponent + @@ -27,7 +28,6 @@ Microsoft.AspNetCore.Mvc.ViewComponent - diff --git a/src/Shared/ObjectMethodExecutor/AwaitableInfo.cs b/src/Shared/ObjectMethodExecutor/AwaitableInfo.cs new file mode 100644 index 000000000000..431b83a6e561 --- /dev/null +++ b/src/Shared/ObjectMethodExecutor/AwaitableInfo.cs @@ -0,0 +1,127 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace Microsoft.Extensions.Internal +{ + internal struct AwaitableInfo + { + public Type AwaiterType { get; } + public PropertyInfo AwaiterIsCompletedProperty { get; } + public MethodInfo AwaiterGetResultMethod { get; } + public MethodInfo AwaiterOnCompletedMethod { get; } + public MethodInfo AwaiterUnsafeOnCompletedMethod { get; } + public Type ResultType { get; } + public MethodInfo GetAwaiterMethod { get; } + + public AwaitableInfo( + Type awaiterType, + PropertyInfo awaiterIsCompletedProperty, + MethodInfo awaiterGetResultMethod, + MethodInfo awaiterOnCompletedMethod, + MethodInfo awaiterUnsafeOnCompletedMethod, + Type resultType, + MethodInfo getAwaiterMethod) + { + AwaiterType = awaiterType; + AwaiterIsCompletedProperty = awaiterIsCompletedProperty; + AwaiterGetResultMethod = awaiterGetResultMethod; + AwaiterOnCompletedMethod = awaiterOnCompletedMethod; + AwaiterUnsafeOnCompletedMethod = awaiterUnsafeOnCompletedMethod; + ResultType = resultType; + GetAwaiterMethod = getAwaiterMethod; + } + + public static bool IsTypeAwaitable(Type type, out AwaitableInfo awaitableInfo) + { + // Based on Roslyn code: http://source.roslyn.io/#Microsoft.CodeAnalysis.Workspaces/Shared/Extensions/ISymbolExtensions.cs,db4d48ba694b9347 + + // Awaitable must have method matching "object GetAwaiter()" + var getAwaiterMethod = type.GetRuntimeMethods().FirstOrDefault(m => + m.Name.Equals("GetAwaiter", StringComparison.OrdinalIgnoreCase) + && m.GetParameters().Length == 0 + && m.ReturnType != null); + if (getAwaiterMethod == null) + { + awaitableInfo = default(AwaitableInfo); + return false; + } + + var awaiterType = getAwaiterMethod.ReturnType; + + // Awaiter must have property matching "bool IsCompleted { get; }" + var isCompletedProperty = awaiterType.GetRuntimeProperties().FirstOrDefault(p => + p.Name.Equals("IsCompleted", StringComparison.OrdinalIgnoreCase) + && p.PropertyType == typeof(bool) + && p.GetMethod != null); + if (isCompletedProperty == null) + { + awaitableInfo = default(AwaitableInfo); + return false; + } + + // Awaiter must implement INotifyCompletion + var awaiterInterfaces = awaiterType.GetInterfaces(); + var implementsINotifyCompletion = awaiterInterfaces.Any(t => t == typeof(INotifyCompletion)); + if (!implementsINotifyCompletion) + { + awaitableInfo = default(AwaitableInfo); + return false; + } + + // INotifyCompletion supplies a method matching "void OnCompleted(Action action)" + var iNotifyCompletionMap = awaiterType + .GetTypeInfo() + .GetRuntimeInterfaceMap(typeof(INotifyCompletion)); + var onCompletedMethod = iNotifyCompletionMap.InterfaceMethods.Single(m => + m.Name.Equals("OnCompleted", StringComparison.OrdinalIgnoreCase) + && m.ReturnType == typeof(void) + && m.GetParameters().Length == 1 + && m.GetParameters()[0].ParameterType == typeof(Action)); + + // Awaiter optionally implements ICriticalNotifyCompletion + var implementsICriticalNotifyCompletion = awaiterInterfaces.Any(t => t == typeof(ICriticalNotifyCompletion)); + MethodInfo unsafeOnCompletedMethod; + if (implementsICriticalNotifyCompletion) + { + // ICriticalNotifyCompletion supplies a method matching "void UnsafeOnCompleted(Action action)" + var iCriticalNotifyCompletionMap = awaiterType + .GetTypeInfo() + .GetRuntimeInterfaceMap(typeof(ICriticalNotifyCompletion)); + unsafeOnCompletedMethod = iCriticalNotifyCompletionMap.InterfaceMethods.Single(m => + m.Name.Equals("UnsafeOnCompleted", StringComparison.OrdinalIgnoreCase) + && m.ReturnType == typeof(void) + && m.GetParameters().Length == 1 + && m.GetParameters()[0].ParameterType == typeof(Action)); + } + else + { + unsafeOnCompletedMethod = null; + } + + // Awaiter must have method matching "void GetResult" or "T GetResult()" + var getResultMethod = awaiterType.GetRuntimeMethods().FirstOrDefault(m => + m.Name.Equals("GetResult") + && m.GetParameters().Length == 0); + if (getResultMethod == null) + { + awaitableInfo = default(AwaitableInfo); + return false; + } + + awaitableInfo = new AwaitableInfo( + awaiterType, + isCompletedProperty, + getResultMethod, + onCompletedMethod, + unsafeOnCompletedMethod, + getResultMethod.ReturnType, + getAwaiterMethod); + return true; + } + } +} diff --git a/src/Shared/ObjectMethodExecutor/CoercedAwaitableInfo.cs b/src/Shared/ObjectMethodExecutor/CoercedAwaitableInfo.cs new file mode 100644 index 000000000000..4e48ef09a15e --- /dev/null +++ b/src/Shared/ObjectMethodExecutor/CoercedAwaitableInfo.cs @@ -0,0 +1,55 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq.Expressions; + +namespace Microsoft.Extensions.Internal +{ + internal struct CoercedAwaitableInfo + { + public AwaitableInfo AwaitableInfo { get; } + public Expression CoercerExpression { get; } + public Type CoercerResultType { get; } + public bool RequiresCoercion => CoercerExpression != null; + + public CoercedAwaitableInfo(AwaitableInfo awaitableInfo) + { + AwaitableInfo = awaitableInfo; + CoercerExpression = null; + CoercerResultType = null; + } + + public CoercedAwaitableInfo(Expression coercerExpression, Type coercerResultType, AwaitableInfo coercedAwaitableInfo) + { + CoercerExpression = coercerExpression; + CoercerResultType = coercerResultType; + AwaitableInfo = coercedAwaitableInfo; + } + + public static bool IsTypeAwaitable(Type type, out CoercedAwaitableInfo info) + { + if (AwaitableInfo.IsTypeAwaitable(type, out var directlyAwaitableInfo)) + { + info = new CoercedAwaitableInfo(directlyAwaitableInfo); + return true; + } + + // It's not directly awaitable, but maybe we can coerce it. + // Currently we support coercing FSharpAsync. + if (ObjectMethodExecutorFSharpSupport.TryBuildCoercerFromFSharpAsyncToAwaitable(type, + out var coercerExpression, + out var coercerResultType)) + { + if (AwaitableInfo.IsTypeAwaitable(coercerResultType, out var coercedAwaitableInfo)) + { + info = new CoercedAwaitableInfo(coercerExpression, coercerResultType, coercedAwaitableInfo); + return true; + } + } + + info = default(CoercedAwaitableInfo); + return false; + } + } +} diff --git a/src/Shared/ObjectMethodExecutor/ObjectMethodExecutor.cs b/src/Shared/ObjectMethodExecutor/ObjectMethodExecutor.cs new file mode 100644 index 000000000000..f8e5b70f0ddd --- /dev/null +++ b/src/Shared/ObjectMethodExecutor/ObjectMethodExecutor.cs @@ -0,0 +1,340 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; + +namespace Microsoft.Extensions.Internal +{ + internal class ObjectMethodExecutor + { + private readonly object[] _parameterDefaultValues; + private readonly MethodExecutorAsync _executorAsync; + private readonly MethodExecutor _executor; + + private static readonly ConstructorInfo _objectMethodExecutorAwaitableConstructor = + typeof(ObjectMethodExecutorAwaitable).GetConstructor(new[] { + typeof(object), // customAwaitable + typeof(Func), // getAwaiterMethod + typeof(Func), // isCompletedMethod + typeof(Func), // getResultMethod + typeof(Action), // onCompletedMethod + typeof(Action) // unsafeOnCompletedMethod + }); + + private ObjectMethodExecutor(MethodInfo methodInfo, TypeInfo targetTypeInfo, object[] parameterDefaultValues) + { + if (methodInfo == null) + { + throw new ArgumentNullException(nameof(methodInfo)); + } + + MethodInfo = methodInfo; + MethodParameters = methodInfo.GetParameters(); + TargetTypeInfo = targetTypeInfo; + MethodReturnType = methodInfo.ReturnType; + + var isAwaitable = CoercedAwaitableInfo.IsTypeAwaitable(MethodReturnType, out var coercedAwaitableInfo); + + IsMethodAsync = isAwaitable; + AsyncResultType = isAwaitable ? coercedAwaitableInfo.AwaitableInfo.ResultType : null; + + // Upstream code may prefer to use the sync-executor even for async methods, because if it knows + // that the result is a specific Task where T is known, then it can directly cast to that type + // and await it without the extra heap allocations involved in the _executorAsync code path. + _executor = GetExecutor(methodInfo, targetTypeInfo); + + if (IsMethodAsync) + { + _executorAsync = GetExecutorAsync(methodInfo, targetTypeInfo, coercedAwaitableInfo); + } + + _parameterDefaultValues = parameterDefaultValues; + } + + private delegate ObjectMethodExecutorAwaitable MethodExecutorAsync(object target, object[] parameters); + + private delegate object MethodExecutor(object target, object[] parameters); + + private delegate void VoidMethodExecutor(object target, object[] parameters); + + public MethodInfo MethodInfo { get; } + + public ParameterInfo[] MethodParameters { get; } + + public TypeInfo TargetTypeInfo { get; } + + public Type AsyncResultType { get; } + + // This field is made internal set because it is set in unit tests. + public Type MethodReturnType { get; internal set; } + + public bool IsMethodAsync { get; } + + public static ObjectMethodExecutor Create(MethodInfo methodInfo, TypeInfo targetTypeInfo) + { + return new ObjectMethodExecutor(methodInfo, targetTypeInfo, null); + } + + public static ObjectMethodExecutor Create(MethodInfo methodInfo, TypeInfo targetTypeInfo, object[] parameterDefaultValues) + { + if (parameterDefaultValues == null) + { + throw new ArgumentNullException(nameof(parameterDefaultValues)); + } + + return new ObjectMethodExecutor(methodInfo, targetTypeInfo, parameterDefaultValues); + } + + /// + /// Executes the configured method on . This can be used whether or not + /// the configured method is asynchronous. + /// + /// + /// Even if the target method is asynchronous, it's desirable to invoke it using Execute rather than + /// ExecuteAsync if you know at compile time what the return type is, because then you can directly + /// "await" that value (via a cast), and then the generated code will be able to reference the + /// resulting awaitable as a value-typed variable. If you use ExecuteAsync instead, the generated + /// code will have to treat the resulting awaitable as a boxed object, because it doesn't know at + /// compile time what type it would be. + /// + /// The object whose method is to be executed. + /// Parameters to pass to the method. + /// The method return value. + public object Execute(object target, object[] parameters) + { + return _executor(target, parameters); + } + + /// + /// Executes the configured method on . This can only be used if the configured + /// method is asynchronous. + /// + /// + /// If you don't know at compile time the type of the method's returned awaitable, you can use ExecuteAsync, + /// which supplies an awaitable-of-object. This always works, but can incur several extra heap allocations + /// as compared with using Execute and then using "await" on the result value typecasted to the known + /// awaitable type. The possible extra heap allocations are for: + /// + /// 1. The custom awaitable (though usually there's a heap allocation for this anyway, since normally + /// it's a reference type, and you normally create a new instance per call). + /// 2. The custom awaiter (whether or not it's a value type, since if it's not, you need a new instance + /// of it, and if it is, it will have to be boxed so the calling code can reference it as an object). + /// 3. The async result value, if it's a value type (it has to be boxed as an object, since the calling + /// code doesn't know what type it's going to be). + /// + /// The object whose method is to be executed. + /// Parameters to pass to the method. + /// An object that you can "await" to get the method return value. + public ObjectMethodExecutorAwaitable ExecuteAsync(object target, object[] parameters) + { + return _executorAsync(target, parameters); + } + + public object GetDefaultValueForParameter(int index) + { + if (_parameterDefaultValues == null) + { + throw new InvalidOperationException($"Cannot call {nameof(GetDefaultValueForParameter)}, because no parameter default values were supplied."); + } + + if (index < 0 || index > MethodParameters.Length - 1) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + return _parameterDefaultValues[index]; + } + + private static MethodExecutor GetExecutor(MethodInfo methodInfo, TypeInfo targetTypeInfo) + { + // Parameters to executor + var targetParameter = Expression.Parameter(typeof(object), "target"); + var parametersParameter = Expression.Parameter(typeof(object[]), "parameters"); + + // Build parameter list + var parameters = new List(); + var paramInfos = methodInfo.GetParameters(); + for (int i = 0; i < paramInfos.Length; i++) + { + var paramInfo = paramInfos[i]; + var valueObj = Expression.ArrayIndex(parametersParameter, Expression.Constant(i)); + var valueCast = Expression.Convert(valueObj, paramInfo.ParameterType); + + // valueCast is "(Ti) parameters[i]" + parameters.Add(valueCast); + } + + // Call method + var instanceCast = Expression.Convert(targetParameter, targetTypeInfo.AsType()); + var methodCall = Expression.Call(instanceCast, methodInfo, parameters); + + // methodCall is "((Ttarget) target) method((T0) parameters[0], (T1) parameters[1], ...)" + // Create function + if (methodCall.Type == typeof(void)) + { + var lambda = Expression.Lambda(methodCall, targetParameter, parametersParameter); + var voidExecutor = lambda.Compile(); + return WrapVoidMethod(voidExecutor); + } + else + { + // must coerce methodCall to match ActionExecutor signature + var castMethodCall = Expression.Convert(methodCall, typeof(object)); + var lambda = Expression.Lambda(castMethodCall, targetParameter, parametersParameter); + return lambda.Compile(); + } + } + + private static MethodExecutor WrapVoidMethod(VoidMethodExecutor executor) + { + return delegate (object target, object[] parameters) + { + executor(target, parameters); + return null; + }; + } + + private static MethodExecutorAsync GetExecutorAsync( + MethodInfo methodInfo, + TypeInfo targetTypeInfo, + CoercedAwaitableInfo coercedAwaitableInfo) + { + // Parameters to executor + var targetParameter = Expression.Parameter(typeof(object), "target"); + var parametersParameter = Expression.Parameter(typeof(object[]), "parameters"); + + // Build parameter list + var parameters = new List(); + var paramInfos = methodInfo.GetParameters(); + for (int i = 0; i < paramInfos.Length; i++) + { + var paramInfo = paramInfos[i]; + var valueObj = Expression.ArrayIndex(parametersParameter, Expression.Constant(i)); + var valueCast = Expression.Convert(valueObj, paramInfo.ParameterType); + + // valueCast is "(Ti) parameters[i]" + parameters.Add(valueCast); + } + + // Call method + var instanceCast = Expression.Convert(targetParameter, targetTypeInfo.AsType()); + var methodCall = Expression.Call(instanceCast, methodInfo, parameters); + + // Using the method return value, construct an ObjectMethodExecutorAwaitable based on + // the info we have about its implementation of the awaitable pattern. Note that all + // the funcs/actions we construct here are precompiled, so that only one instance of + // each is preserved throughout the lifetime of the ObjectMethodExecutor. + + // var getAwaiterFunc = (object awaitable) => + // (object)((CustomAwaitableType)awaitable).GetAwaiter(); + var customAwaitableParam = Expression.Parameter(typeof(object), "awaitable"); + var awaitableInfo = coercedAwaitableInfo.AwaitableInfo; + var postCoercionMethodReturnType = coercedAwaitableInfo.CoercerResultType ?? methodInfo.ReturnType; + var getAwaiterFunc = Expression.Lambda>( + Expression.Convert( + Expression.Call( + Expression.Convert(customAwaitableParam, postCoercionMethodReturnType), + awaitableInfo.GetAwaiterMethod), + typeof(object)), + customAwaitableParam).Compile(); + + // var isCompletedFunc = (object awaiter) => + // ((CustomAwaiterType)awaiter).IsCompleted; + var isCompletedParam = Expression.Parameter(typeof(object), "awaiter"); + var isCompletedFunc = Expression.Lambda>( + Expression.MakeMemberAccess( + Expression.Convert(isCompletedParam, awaitableInfo.AwaiterType), + awaitableInfo.AwaiterIsCompletedProperty), + isCompletedParam).Compile(); + + var getResultParam = Expression.Parameter(typeof(object), "awaiter"); + Func getResultFunc; + if (awaitableInfo.ResultType == typeof(void)) + { + // var getResultFunc = (object awaiter) => + // { + // ((CustomAwaiterType)awaiter).GetResult(); // We need to invoke this to surface any exceptions + // return (object)null; + // }; + getResultFunc = Expression.Lambda>( + Expression.Block( + Expression.Call( + Expression.Convert(getResultParam, awaitableInfo.AwaiterType), + awaitableInfo.AwaiterGetResultMethod), + Expression.Constant(null) + ), + getResultParam).Compile(); + } + else + { + // var getResultFunc = (object awaiter) => + // (object)((CustomAwaiterType)awaiter).GetResult(); + getResultFunc = Expression.Lambda>( + Expression.Convert( + Expression.Call( + Expression.Convert(getResultParam, awaitableInfo.AwaiterType), + awaitableInfo.AwaiterGetResultMethod), + typeof(object)), + getResultParam).Compile(); + } + + // var onCompletedFunc = (object awaiter, Action continuation) => { + // ((CustomAwaiterType)awaiter).OnCompleted(continuation); + // }; + var onCompletedParam1 = Expression.Parameter(typeof(object), "awaiter"); + var onCompletedParam2 = Expression.Parameter(typeof(Action), "continuation"); + var onCompletedFunc = Expression.Lambda>( + Expression.Call( + Expression.Convert(onCompletedParam1, awaitableInfo.AwaiterType), + awaitableInfo.AwaiterOnCompletedMethod, + onCompletedParam2), + onCompletedParam1, + onCompletedParam2).Compile(); + + Action unsafeOnCompletedFunc = null; + if (awaitableInfo.AwaiterUnsafeOnCompletedMethod != null) + { + // var unsafeOnCompletedFunc = (object awaiter, Action continuation) => { + // ((CustomAwaiterType)awaiter).UnsafeOnCompleted(continuation); + // }; + var unsafeOnCompletedParam1 = Expression.Parameter(typeof(object), "awaiter"); + var unsafeOnCompletedParam2 = Expression.Parameter(typeof(Action), "continuation"); + unsafeOnCompletedFunc = Expression.Lambda>( + Expression.Call( + Expression.Convert(unsafeOnCompletedParam1, awaitableInfo.AwaiterType), + awaitableInfo.AwaiterUnsafeOnCompletedMethod, + unsafeOnCompletedParam2), + unsafeOnCompletedParam1, + unsafeOnCompletedParam2).Compile(); + } + + // If we need to pass the method call result through a coercer function to get an + // awaitable, then do so. + var coercedMethodCall = coercedAwaitableInfo.RequiresCoercion + ? Expression.Invoke(coercedAwaitableInfo.CoercerExpression, methodCall) + : (Expression)methodCall; + + // return new ObjectMethodExecutorAwaitable( + // (object)coercedMethodCall, + // getAwaiterFunc, + // isCompletedFunc, + // getResultFunc, + // onCompletedFunc, + // unsafeOnCompletedFunc); + var returnValueExpression = Expression.New( + _objectMethodExecutorAwaitableConstructor, + Expression.Convert(coercedMethodCall, typeof(object)), + Expression.Constant(getAwaiterFunc), + Expression.Constant(isCompletedFunc), + Expression.Constant(getResultFunc), + Expression.Constant(onCompletedFunc), + Expression.Constant(unsafeOnCompletedFunc, typeof(Action))); + + var lambda = Expression.Lambda(returnValueExpression, targetParameter, parametersParameter); + return lambda.Compile(); + } + } +} diff --git a/src/Shared/ObjectMethodExecutor/ObjectMethodExecutorAwaitable.cs b/src/Shared/ObjectMethodExecutor/ObjectMethodExecutorAwaitable.cs new file mode 100644 index 000000000000..7509b86b2ba5 --- /dev/null +++ b/src/Shared/ObjectMethodExecutor/ObjectMethodExecutorAwaitable.cs @@ -0,0 +1,114 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Runtime.CompilerServices; + +namespace Microsoft.Extensions.Internal +{ + /// + /// Provides a common awaitable structure that can + /// return, regardless of whether the underlying value is a System.Task, an FSharpAsync, or an + /// application-defined custom awaitable. + /// + internal struct ObjectMethodExecutorAwaitable + { + private readonly object _customAwaitable; + private readonly Func _getAwaiterMethod; + private readonly Func _isCompletedMethod; + private readonly Func _getResultMethod; + private readonly Action _onCompletedMethod; + private readonly Action _unsafeOnCompletedMethod; + + // Perf note: since we're requiring the customAwaitable to be supplied here as an object, + // this will trigger a further allocation if it was a value type (i.e., to box it). We can't + // fix this by making the customAwaitable type generic, because the calling code typically + // does not know the type of the awaitable/awaiter at compile-time anyway. + // + // However, we could fix it by not passing the customAwaitable here at all, and instead + // passing a func that maps directly from the target object (e.g., controller instance), + // target method (e.g., action method info), and params array to the custom awaiter in the + // GetAwaiter() method below. In effect, by delaying the actual method call until the + // upstream code calls GetAwaiter on this ObjectMethodExecutorAwaitable instance. + // This optimization is not currently implemented because: + // [1] It would make no difference when the awaitable was an object type, which is + // by far the most common scenario (e.g., System.Task). + // [2] It would be complex - we'd need some kind of object pool to track all the parameter + // arrays until we needed to use them in GetAwaiter(). + // We can reconsider this in the future if there's a need to optimize for ValueTask + // or other value-typed awaitables. + + public ObjectMethodExecutorAwaitable( + object customAwaitable, + Func getAwaiterMethod, + Func isCompletedMethod, + Func getResultMethod, + Action onCompletedMethod, + Action unsafeOnCompletedMethod) + { + _customAwaitable = customAwaitable; + _getAwaiterMethod = getAwaiterMethod; + _isCompletedMethod = isCompletedMethod; + _getResultMethod = getResultMethod; + _onCompletedMethod = onCompletedMethod; + _unsafeOnCompletedMethod = unsafeOnCompletedMethod; + } + + public Awaiter GetAwaiter() + { + var customAwaiter = _getAwaiterMethod(_customAwaitable); + return new Awaiter(customAwaiter, _isCompletedMethod, _getResultMethod, _onCompletedMethod, _unsafeOnCompletedMethod); + } + + public struct Awaiter : ICriticalNotifyCompletion + { + private readonly object _customAwaiter; + private readonly Func _isCompletedMethod; + private readonly Func _getResultMethod; + private readonly Action _onCompletedMethod; + private readonly Action _unsafeOnCompletedMethod; + + public Awaiter( + object customAwaiter, + Func isCompletedMethod, + Func getResultMethod, + Action onCompletedMethod, + Action unsafeOnCompletedMethod) + { + _customAwaiter = customAwaiter; + _isCompletedMethod = isCompletedMethod; + _getResultMethod = getResultMethod; + _onCompletedMethod = onCompletedMethod; + _unsafeOnCompletedMethod = unsafeOnCompletedMethod; + } + + public bool IsCompleted => _isCompletedMethod(_customAwaiter); + + public object GetResult() => _getResultMethod(_customAwaiter); + + public void OnCompleted(Action continuation) + { + _onCompletedMethod(_customAwaiter, continuation); + } + + public void UnsafeOnCompleted(Action continuation) + { + // If the underlying awaitable implements ICriticalNotifyCompletion, use its UnsafeOnCompleted. + // If not, fall back on using its OnCompleted. + // + // Why this is safe: + // - Implementing ICriticalNotifyCompletion is a way of saying the caller can choose whether it + // needs the execution context to be preserved (which it signals by calling OnCompleted), or + // that it doesn't (which it signals by calling UnsafeOnCompleted). Obviously it's faster *not* + // to preserve and restore the context, so we prefer that where possible. + // - If a caller doesn't need the execution context to be preserved and hence calls UnsafeOnCompleted, + // there's no harm in preserving it anyway - it's just a bit of wasted cost. That's what will happen + // if a caller sees that the proxy implements ICriticalNotifyCompletion but the proxy chooses to + // pass the call on to the underlying awaitable's OnCompleted method. + + var underlyingMethodToUse = _unsafeOnCompletedMethod ?? _onCompletedMethod; + underlyingMethodToUse(_customAwaiter, continuation); + } + } + } +} \ No newline at end of file diff --git a/src/Shared/ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs b/src/Shared/ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs new file mode 100644 index 000000000000..2198c0ce4506 --- /dev/null +++ b/src/Shared/ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs @@ -0,0 +1,151 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Internal +{ + /// + /// Helper for detecting whether a given type is FSharpAsync`1, and if so, supplying + /// an for mapping instances of that type to a C# awaitable. + /// + /// + /// The main design goal here is to avoid taking a compile-time dependency on + /// FSharp.Core.dll, because non-F# applications wouldn't use it. So all the references + /// to FSharp types have to be constructed dynamically at runtime. + /// + internal static class ObjectMethodExecutorFSharpSupport + { + private static object _fsharpValuesCacheLock = new object(); + private static Assembly _fsharpCoreAssembly; + private static MethodInfo _fsharpAsyncStartAsTaskGenericMethod; + private static PropertyInfo _fsharpOptionOfTaskCreationOptionsNoneProperty; + private static PropertyInfo _fsharpOptionOfCancellationTokenNoneProperty; + + public static bool TryBuildCoercerFromFSharpAsyncToAwaitable( + Type possibleFSharpAsyncType, + out Expression coerceToAwaitableExpression, + out Type awaitableType) + { + var methodReturnGenericType = possibleFSharpAsyncType.IsGenericType + ? possibleFSharpAsyncType.GetGenericTypeDefinition() + : null; + + if (!IsFSharpAsyncOpenGenericType(methodReturnGenericType)) + { + coerceToAwaitableExpression = null; + awaitableType = null; + return false; + } + + var awaiterResultType = possibleFSharpAsyncType.GetGenericArguments().Single(); + awaitableType = typeof(Task<>).MakeGenericType(awaiterResultType); + + // coerceToAwaitableExpression = (object fsharpAsync) => + // { + // return (object)FSharpAsync.StartAsTask( + // (Microsoft.FSharp.Control.FSharpAsync)fsharpAsync, + // FSharpOption.None, + // FSharpOption.None); + // }; + var startAsTaskClosedMethod = _fsharpAsyncStartAsTaskGenericMethod + .MakeGenericMethod(awaiterResultType); + var coerceToAwaitableParam = Expression.Parameter(typeof(object)); + coerceToAwaitableExpression = Expression.Lambda( + Expression.Convert( + Expression.Call( + startAsTaskClosedMethod, + Expression.Convert(coerceToAwaitableParam, possibleFSharpAsyncType), + Expression.MakeMemberAccess(null, _fsharpOptionOfTaskCreationOptionsNoneProperty), + Expression.MakeMemberAccess(null, _fsharpOptionOfCancellationTokenNoneProperty)), + typeof(object)), + coerceToAwaitableParam); + + return true; + } + + private static bool IsFSharpAsyncOpenGenericType(Type possibleFSharpAsyncGenericType) + { + var typeFullName = possibleFSharpAsyncGenericType?.FullName; + if (!string.Equals(typeFullName, "Microsoft.FSharp.Control.FSharpAsync`1", StringComparison.Ordinal)) + { + return false; + } + + lock (_fsharpValuesCacheLock) + { + if (_fsharpCoreAssembly != null) + { + // Since we've already found the real FSharpAsync.Core assembly, we just have + // to check that the supplied FSharpAsync`1 type is the one from that assembly. + return possibleFSharpAsyncGenericType.Assembly == _fsharpCoreAssembly; + } + else + { + // We'll keep trying to find the FSharp types/values each time any type called + // FSharpAsync`1 is supplied. + return TryPopulateFSharpValueCaches(possibleFSharpAsyncGenericType); + } + } + } + + private static bool TryPopulateFSharpValueCaches(Type possibleFSharpAsyncGenericType) + { + var assembly = possibleFSharpAsyncGenericType.Assembly; + var fsharpOptionType = assembly.GetType("Microsoft.FSharp.Core.FSharpOption`1"); + var fsharpAsyncType = assembly.GetType("Microsoft.FSharp.Control.FSharpAsync"); + + if (fsharpOptionType == null || fsharpAsyncType == null) + { + return false; + } + + // Get a reference to FSharpOption.None + var fsharpOptionOfTaskCreationOptionsType = fsharpOptionType + .MakeGenericType(typeof(TaskCreationOptions)); + _fsharpOptionOfTaskCreationOptionsNoneProperty = fsharpOptionOfTaskCreationOptionsType + .GetTypeInfo() + .GetRuntimeProperty("None"); + + // Get a reference to FSharpOption.None + var fsharpOptionOfCancellationTokenType = fsharpOptionType + .MakeGenericType(typeof(CancellationToken)); + _fsharpOptionOfCancellationTokenNoneProperty = fsharpOptionOfCancellationTokenType + .GetTypeInfo() + .GetRuntimeProperty("None"); + + // Get a reference to FSharpAsync.StartAsTask<> + var fsharpAsyncMethods = fsharpAsyncType + .GetRuntimeMethods() + .Where(m => m.Name.Equals("StartAsTask", StringComparison.Ordinal)); + foreach (var candidateMethodInfo in fsharpAsyncMethods) + { + var parameters = candidateMethodInfo.GetParameters(); + if (parameters.Length == 3 + && TypesHaveSameIdentity(parameters[0].ParameterType, possibleFSharpAsyncGenericType) + && parameters[1].ParameterType == fsharpOptionOfTaskCreationOptionsType + && parameters[2].ParameterType == fsharpOptionOfCancellationTokenType) + { + // This really does look like the correct method (and hence assembly). + _fsharpAsyncStartAsTaskGenericMethod = candidateMethodInfo; + _fsharpCoreAssembly = assembly; + break; + } + } + + return _fsharpCoreAssembly != null; + } + + private static bool TypesHaveSameIdentity(Type type1, Type type2) + { + return type1.Assembly == type2.Assembly + && string.Equals(type1.Namespace, type2.Namespace, StringComparison.Ordinal) + && string.Equals(type1.Name, type2.Name, StringComparison.Ordinal); + } + } +} diff --git a/src/Shared/test/Microsoft.AspNetCore.Shared.Tests.csproj b/src/Shared/test/Microsoft.AspNetCore.Shared.Tests.csproj index c7472c372527..ca95ec8b525a 100644 --- a/src/Shared/test/Microsoft.AspNetCore.Shared.Tests.csproj +++ b/src/Shared/test/Microsoft.AspNetCore.Shared.Tests.csproj @@ -5,7 +5,13 @@ + + + + + + diff --git a/src/Shared/test/ObjectMethodExecutorTest.cs b/src/Shared/test/ObjectMethodExecutorTest.cs new file mode 100644 index 000000000000..1c26ef1de157 --- /dev/null +++ b/src/Shared/test/ObjectMethodExecutorTest.cs @@ -0,0 +1,634 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.FSharp.Control; +using Microsoft.FSharp.Core; +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Extensions.Internal +{ + public class ObjectMethodExecutorTest + { + private TestObject _targetObject = new TestObject(); + private TypeInfo targetTypeInfo = typeof(TestObject).GetTypeInfo(); + + [Fact] + public void ExecuteValueMethod() + { + var executor = GetExecutorForMethod("ValueMethod"); + var result = executor.Execute( + _targetObject, + new object[] { 10, 20 }); + Assert.False(executor.IsMethodAsync); + Assert.Equal(30, (int)result); + } + + [Fact] + public void ExecuteVoidValueMethod() + { + var executor = GetExecutorForMethod("VoidValueMethod"); + var result = executor.Execute( + _targetObject, + new object[] { 10 }); + Assert.False(executor.IsMethodAsync); + Assert.Null(result); + } + + [Fact] + public void ExecuteValueMethodWithReturnType() + { + var executor = GetExecutorForMethod("ValueMethodWithReturnType"); + var result = executor.Execute( + _targetObject, + new object[] { 10 }); + var resultObject = Assert.IsType(result); + Assert.False(executor.IsMethodAsync); + Assert.Equal("Hello", resultObject.value); + } + + [Fact] + public void ExecuteValueMethodUpdateValue() + { + var executor = GetExecutorForMethod("ValueMethodUpdateValue"); + var parameter = new TestObject(); + var result = executor.Execute( + _targetObject, + new object[] { parameter }); + var resultObject = Assert.IsType(result); + Assert.False(executor.IsMethodAsync); + Assert.Equal("HelloWorld", resultObject.value); + } + + [Fact] + public void ExecuteValueMethodWithReturnTypeThrowsException() + { + var executor = GetExecutorForMethod("ValueMethodWithReturnTypeThrowsException"); + var parameter = new TestObject(); + Assert.False(executor.IsMethodAsync); + Assert.Throws( + () => executor.Execute( + _targetObject, + new object[] { parameter })); + } + + [Fact] + public async Task ExecuteValueMethodAsync() + { + var executor = GetExecutorForMethod("ValueMethodAsync"); + var result = await executor.ExecuteAsync( + _targetObject, + new object[] { 10, 20 }); + Assert.True(executor.IsMethodAsync); + Assert.Equal(30, (int)result); + } + + [Fact] + public async Task ExecuteValueMethodWithReturnTypeAsync() + { + var executor = GetExecutorForMethod("ValueMethodWithReturnTypeAsync"); + var result = await executor.ExecuteAsync( + _targetObject, + new object[] { 10 }); + var resultObject = Assert.IsType(result); + Assert.True(executor.IsMethodAsync); + Assert.Equal("Hello", resultObject.value); + } + + [Fact] + public async Task ExecuteValueMethodUpdateValueAsync() + { + var executor = GetExecutorForMethod("ValueMethodUpdateValueAsync"); + var parameter = new TestObject(); + var result = await executor.ExecuteAsync( + _targetObject, + new object[] { parameter }); + var resultObject = Assert.IsType(result); + Assert.True(executor.IsMethodAsync); + Assert.Equal("HelloWorld", resultObject.value); + } + + [Fact] + public async Task ExecuteValueMethodWithReturnTypeThrowsExceptionAsync() + { + var executor = GetExecutorForMethod("ValueMethodWithReturnTypeThrowsExceptionAsync"); + var parameter = new TestObject(); + Assert.True(executor.IsMethodAsync); + await Assert.ThrowsAsync( + async () => await executor.ExecuteAsync( + _targetObject, + new object[] { parameter })); + } + + [Fact] + public async Task ExecuteValueMethodWithReturnVoidThrowsExceptionAsync() + { + var executor = GetExecutorForMethod("ValueMethodWithReturnVoidThrowsExceptionAsync"); + var parameter = new TestObject(); + Assert.True(executor.IsMethodAsync); + await Assert.ThrowsAsync( + async () => await executor.ExecuteAsync( + _targetObject, + new object[] { parameter })); + } + + [Fact] + public void GetDefaultValueForParameters_ReturnsSuppliedValues() + { + var suppliedDefaultValues = new object[] { 123, "test value" }; + var executor = GetExecutorForMethod("MethodWithMultipleParameters", suppliedDefaultValues); + Assert.Equal(suppliedDefaultValues[0], executor.GetDefaultValueForParameter(0)); + Assert.Equal(suppliedDefaultValues[1], executor.GetDefaultValueForParameter(1)); + Assert.Throws(() => executor.GetDefaultValueForParameter(2)); + } + + [Fact] + public void GetDefaultValueForParameters_ThrowsIfNoneWereSupplied() + { + var executor = GetExecutorForMethod("MethodWithMultipleParameters"); + Assert.Throws(() => executor.GetDefaultValueForParameter(0)); + } + + [Fact] + public async void TargetMethodReturningCustomAwaitableOfReferenceType_CanInvokeViaExecute() + { + // Arrange + var executor = GetExecutorForMethod("CustomAwaitableOfReferenceTypeAsync"); + + // Act + var result = await (TestAwaitable)executor.Execute(_targetObject, new object[] { "Hello", 123 }); + + // Assert + Assert.True(executor.IsMethodAsync); + Assert.Same(typeof(TestObject), executor.AsyncResultType); + Assert.NotNull(result); + Assert.Equal("Hello 123", result.value); + } + + [Fact] + public async void TargetMethodReturningCustomAwaitableOfValueType_CanInvokeViaExecute() + { + // Arrange + var executor = GetExecutorForMethod("CustomAwaitableOfValueTypeAsync"); + + // Act + var result = await (TestAwaitable)executor.Execute(_targetObject, new object[] { 123, 456 }); + + // Assert + Assert.True(executor.IsMethodAsync); + Assert.Same(typeof(int), executor.AsyncResultType); + Assert.Equal(579, result); + } + + [Fact] + public async void TargetMethodReturningCustomAwaitableOfReferenceType_CanInvokeViaExecuteAsync() + { + // Arrange + var executor = GetExecutorForMethod("CustomAwaitableOfReferenceTypeAsync"); + + // Act + var result = await executor.ExecuteAsync(_targetObject, new object[] { "Hello", 123 }); + + // Assert + Assert.True(executor.IsMethodAsync); + Assert.Same(typeof(TestObject), executor.AsyncResultType); + Assert.NotNull(result); + Assert.IsType(result); + Assert.Equal("Hello 123", ((TestObject)result).value); + } + + [Fact] + public async void TargetMethodReturningCustomAwaitableOfValueType_CanInvokeViaExecuteAsync() + { + // Arrange + var executor = GetExecutorForMethod("CustomAwaitableOfValueTypeAsync"); + + // Act + var result = await executor.ExecuteAsync(_targetObject, new object[] { 123, 456 }); + + // Assert + Assert.True(executor.IsMethodAsync); + Assert.Same(typeof(int), executor.AsyncResultType); + Assert.NotNull(result); + Assert.IsType(result); + Assert.Equal(579, (int)result); + } + + [Fact] + public async void TargetMethodReturningAwaitableOfVoidType_CanInvokeViaExecuteAsync() + { + // Arrange + var executor = GetExecutorForMethod("VoidValueMethodAsync"); + + // Act + var result = await executor.ExecuteAsync(_targetObject, new object[] { 123 }); + + // Assert + Assert.True(executor.IsMethodAsync); + Assert.Same(typeof(void), executor.AsyncResultType); + Assert.Null(result); + } + + [Fact] + public async void TargetMethodReturningAwaitableWithICriticalNotifyCompletion_UsesUnsafeOnCompleted() + { + // Arrange + var executor = GetExecutorForMethod("CustomAwaitableWithICriticalNotifyCompletion"); + + // Act + var result = await executor.ExecuteAsync(_targetObject, new object[0]); + + // Assert + Assert.True(executor.IsMethodAsync); + Assert.Same(typeof(string), executor.AsyncResultType); + Assert.Equal("Used UnsafeOnCompleted", (string)result); + } + + [Fact] + public async void TargetMethodReturningAwaitableWithoutICriticalNotifyCompletion_UsesOnCompleted() + { + // Arrange + var executor = GetExecutorForMethod("CustomAwaitableWithoutICriticalNotifyCompletion"); + + // Act + var result = await executor.ExecuteAsync(_targetObject, new object[0]); + + // Assert + Assert.True(executor.IsMethodAsync); + Assert.Same(typeof(string), executor.AsyncResultType); + Assert.Equal("Used OnCompleted", (string)result); + } + + [Fact] + public async void TargetMethodReturningValueTaskOfValueType_CanBeInvokedViaExecute() + { + // Arrange + var executor = GetExecutorForMethod("ValueTaskOfValueType"); + + // Act + var result = await (ValueTask)executor.Execute(_targetObject, new object[] { 123 }); + + // Assert + Assert.True(executor.IsMethodAsync); + Assert.Same(typeof(int), executor.AsyncResultType); + Assert.Equal(123, result); + } + + [Fact] + public async void TargetMethodReturningValueTaskOfReferenceType_CanBeInvokedViaExecute() + { + // Arrange + var executor = GetExecutorForMethod("ValueTaskOfReferenceType"); + + // Act + var result = await (ValueTask)executor.Execute(_targetObject, new object[] { "test result" }); + + // Assert + Assert.True(executor.IsMethodAsync); + Assert.Same(typeof(string), executor.AsyncResultType); + Assert.Equal("test result", result); + } + + [Fact] + public async void TargetMethodReturningValueTaskOfValueType_CanBeInvokedViaExecuteAsync() + { + // Arrange + var executor = GetExecutorForMethod("ValueTaskOfValueType"); + + // Act + var result = await executor.ExecuteAsync(_targetObject, new object[] { 123 }); + + // Assert + Assert.True(executor.IsMethodAsync); + Assert.Same(typeof(int), executor.AsyncResultType); + Assert.NotNull(result); + Assert.Equal(123, (int)result); + } + + [Fact] + public async void TargetMethodReturningValueTaskOfReferenceType_CanBeInvokedViaExecuteAsync() + { + // Arrange + var executor = GetExecutorForMethod("ValueTaskOfReferenceType"); + + // Act + var result = await executor.ExecuteAsync(_targetObject, new object[] { "test result" }); + + // Assert + Assert.True(executor.IsMethodAsync); + Assert.Same(typeof(string), executor.AsyncResultType); + Assert.Equal("test result", result); + } + + [Fact] + public async void TargetMethodReturningFSharpAsync_CanBeInvokedViaExecute() + { + // Arrange + var executor = GetExecutorForMethod("FSharpAsyncMethod"); + + // Act + var fsharpAsync = (FSharpAsync)executor.Execute(_targetObject, new object[] { "test result" }); + var result = await FSharpAsync.StartAsTask(fsharpAsync, + FSharpOption.None, + FSharpOption.None); + + // Assert + Assert.True(executor.IsMethodAsync); + Assert.Same(typeof(string), executor.AsyncResultType); + Assert.Equal("test result", result); + } + + [Fact] + public async void TargetMethodReturningFailingFSharpAsync_CanBeInvokedViaExecute() + { + // Arrange + var executor = GetExecutorForMethod("FSharpAsyncFailureMethod"); + + // Act + var fsharpAsync = (FSharpAsync)executor.Execute(_targetObject, new object[] { "test result" }); + var resultTask = FSharpAsync.StartAsTask(fsharpAsync, + FSharpOption.None, + FSharpOption.None); + + // Assert + Assert.True(executor.IsMethodAsync); + Assert.Same(typeof(string), executor.AsyncResultType); + + var exception = await Assert.ThrowsAsync(async () => await resultTask); + Assert.IsType(exception.InnerException); + Assert.Equal("Test exception", exception.InnerException.Message); + } + + [Fact] + public async void TargetMethodReturningFSharpAsync_CanBeInvokedViaExecuteAsync() + { + // Arrange + var executor = GetExecutorForMethod("FSharpAsyncMethod"); + + // Act + var result = await executor.ExecuteAsync(_targetObject, new object[] { "test result" }); + + // Assert + Assert.True(executor.IsMethodAsync); + Assert.Same(typeof(string), executor.AsyncResultType); + Assert.Equal("test result", result); + } + + [Fact] + public async void TargetMethodReturningFailingFSharpAsync_CanBeInvokedViaExecuteAsync() + { + // Arrange + var executor = GetExecutorForMethod("FSharpAsyncFailureMethod"); + + // Act + var resultTask = executor.ExecuteAsync(_targetObject, new object[] { "test result" }); + + // Assert + Assert.True(executor.IsMethodAsync); + Assert.Same(typeof(string), executor.AsyncResultType); + + var exception = await Assert.ThrowsAsync(async () => await resultTask); + Assert.IsType(exception.InnerException); + Assert.Equal("Test exception", exception.InnerException.Message); + } + + private ObjectMethodExecutor GetExecutorForMethod(string methodName) + { + var method = typeof(TestObject).GetMethod(methodName); + return ObjectMethodExecutor.Create(method, targetTypeInfo); + } + + private ObjectMethodExecutor GetExecutorForMethod(string methodName, object[] parameterDefaultValues) + { + var method = typeof(TestObject).GetMethod(methodName); + return ObjectMethodExecutor.Create(method, targetTypeInfo, parameterDefaultValues); + } + + public class TestObject + { + public string value; + public int ValueMethod(int i, int j) + { + return i + j; + } + + public void VoidValueMethod(int i) + { + + } + + public TestObject ValueMethodWithReturnType(int i) + { + return new TestObject() { value = "Hello" }; ; + } + + public TestObject ValueMethodWithReturnTypeThrowsException(TestObject i) + { + throw new NotImplementedException("Not Implemented Exception"); + } + + public TestObject ValueMethodUpdateValue(TestObject parameter) + { + parameter.value = "HelloWorld"; + return parameter; + } + + public Task ValueMethodAsync(int i, int j) + { + return Task.FromResult(i + j); + } + + public async Task VoidValueMethodAsync(int i) + { + await ValueMethodAsync(3, 4); + } + public Task ValueMethodWithReturnTypeAsync(int i) + { + return Task.FromResult(new TestObject() { value = "Hello" }); + } + + public async Task ValueMethodWithReturnVoidThrowsExceptionAsync(TestObject i) + { + await Task.CompletedTask; + throw new NotImplementedException("Not Implemented Exception"); + } + + public async Task ValueMethodWithReturnTypeThrowsExceptionAsync(TestObject i) + { + await Task.CompletedTask; + throw new NotImplementedException("Not Implemented Exception"); + } + + public Task ValueMethodUpdateValueAsync(TestObject parameter) + { + parameter.value = "HelloWorld"; + return Task.FromResult(parameter); + } + + public TestAwaitable CustomAwaitableOfReferenceTypeAsync( + string input1, + int input2) + { + return new TestAwaitable(new TestObject + { + value = $"{input1} {input2}" + }); + } + + public TestAwaitable CustomAwaitableOfValueTypeAsync( + int input1, + int input2) + { + return new TestAwaitable(input1 + input2); + } + + public TestAwaitableWithICriticalNotifyCompletion CustomAwaitableWithICriticalNotifyCompletion() + { + return new TestAwaitableWithICriticalNotifyCompletion(); + } + + public TestAwaitableWithoutICriticalNotifyCompletion CustomAwaitableWithoutICriticalNotifyCompletion() + { + return new TestAwaitableWithoutICriticalNotifyCompletion(); + } + + public ValueTask ValueTaskOfValueType(int result) + { + return new ValueTask(result); + } + + public ValueTask ValueTaskOfReferenceType(string result) + { + return new ValueTask(result); + } + + public void MethodWithMultipleParameters(int valueTypeParam, string referenceTypeParam) + { + } + + public FSharpAsync FSharpAsyncMethod(string parameter) + { + return FSharpAsync.AwaitTask(Task.FromResult(parameter)); + } + + public FSharpAsync FSharpAsyncFailureMethod(string parameter) + { + return FSharpAsync.AwaitTask( + Task.FromException(new InvalidOperationException("Test exception"))); + } + } + + public class TestAwaitable + { + private T _result; + private bool _isCompleted; + private List _onCompletedCallbacks = new List(); + + public TestAwaitable(T result) + { + _result = result; + + // Simulate a brief delay before completion + ThreadPool.QueueUserWorkItem(_ => + { + Thread.Sleep(100); + SetCompleted(); + }); + } + + private void SetCompleted() + { + _isCompleted = true; + + foreach (var callback in _onCompletedCallbacks) + { + callback(); + } + } + + public TestAwaiter GetAwaiter() + { + return new TestAwaiter(this); + } + + public struct TestAwaiter : INotifyCompletion + { + private TestAwaitable _owner; + + public TestAwaiter(TestAwaitable owner) : this() + { + _owner = owner; + } + + public bool IsCompleted => _owner._isCompleted; + + public void OnCompleted(Action continuation) + { + if (_owner._isCompleted) + { + continuation(); + } + else + { + _owner._onCompletedCallbacks.Add(continuation); + } + } + + public T GetResult() + { + return _owner._result; + } + } + } + + public class TestAwaitableWithICriticalNotifyCompletion + { + public TestAwaiterWithICriticalNotifyCompletion GetAwaiter() + => new TestAwaiterWithICriticalNotifyCompletion(); + } + + public class TestAwaitableWithoutICriticalNotifyCompletion + { + public TestAwaiterWithoutICriticalNotifyCompletion GetAwaiter() + => new TestAwaiterWithoutICriticalNotifyCompletion(); + } + + public class TestAwaiterWithICriticalNotifyCompletion + : CompletionTrackingAwaiterBase, ICriticalNotifyCompletion + { + } + + public class TestAwaiterWithoutICriticalNotifyCompletion + : CompletionTrackingAwaiterBase, INotifyCompletion + { + } + + public class CompletionTrackingAwaiterBase + { + private string _result; + + public bool IsCompleted { get; private set; } + + public string GetResult() => _result; + + public void OnCompleted(Action continuation) + { + _result = "Used OnCompleted"; + IsCompleted = true; + continuation(); + } + + public void UnsafeOnCompleted(Action continuation) + { + _result = "Used UnsafeOnCompleted"; + IsCompleted = true; + continuation(); + } + } + } +} diff --git a/src/SignalR/build/dependencies.props b/src/SignalR/build/dependencies.props index 6cefc3fe3d93..de016788110e 100644 --- a/src/SignalR/build/dependencies.props +++ b/src/SignalR/build/dependencies.props @@ -77,7 +77,6 @@ 2.1.1 2.1.1 2.1.1 - 2.1.1 2.1.1 2.1.1 2.1.1 diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/Microsoft.AspNetCore.SignalR.Core.csproj b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/Microsoft.AspNetCore.SignalR.Core.csproj index d9d9af7b69f6..728a93a055c3 100644 --- a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/Microsoft.AspNetCore.SignalR.Core.csproj +++ b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/Microsoft.AspNetCore.SignalR.Core.csproj @@ -8,6 +8,7 @@ + @@ -19,7 +20,6 @@ - From e7ab6388e3de3cd5cd78a15ccb367c2e36ff0871 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Thu, 13 Dec 2018 11:15:39 -0800 Subject: [PATCH 06/10] Replace Microsoft.Extensions.{PropertyActivator, PropertyHelper}.Sources with a local copy of code --- build/dependencies.props | 2 - build/external-dependencies.props | 2 - eng/Dependencies.props | 1 - ...oft.AspNetCore.Routing.Abstractions.csproj | 5 +- .../src/Microsoft.AspNetCore.Routing.csproj | 5 +- src/Mvc/build/dependencies.props | 2 - ...crosoft.AspNetCore.Mvc.Abstractions.csproj | 3 +- ...icrosoft.AspNetCore.Mvc.ApiExplorer.csproj | 2 +- .../Microsoft.AspNetCore.Mvc.Core.csproj | 4 +- ...osoft.AspNetCore.Mvc.Formatters.Xml.csproj | 3 +- ...crosoft.AspNetCore.Mvc.Localization.csproj | 5 +- .../Microsoft.AspNetCore.Mvc.Razor.csproj | 4 +- ...Microsoft.AspNetCore.Mvc.RazorPages.csproj | 4 +- ...Microsoft.AspNetCore.Mvc.TagHelpers.csproj | 2 +- ...crosoft.AspNetCore.Mvc.ViewFeatures.csproj | 4 +- ...oft.AspNetCore.Mvc.WebApiCompatShim.csproj | 5 +- .../PropertyActivator/PropertyActivator.cs | 110 +++ src/Shared/PropertyHelper/PropertyHelper.cs | 526 +++++++++++ .../Microsoft.AspNetCore.Shared.Tests.csproj | 2 + src/Shared/test/PropertyActivatorTest.cs | 187 ++++ src/Shared/test/PropertyHelperTest.cs | 831 ++++++++++++++++++ 21 files changed, 1684 insertions(+), 25 deletions(-) create mode 100644 src/Shared/PropertyActivator/PropertyActivator.cs create mode 100644 src/Shared/PropertyHelper/PropertyHelper.cs create mode 100644 src/Shared/test/PropertyActivatorTest.cs create mode 100644 src/Shared/test/PropertyHelperTest.cs diff --git a/build/dependencies.props b/build/dependencies.props index 0222f0c6bb58..18845e77e8ed 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -75,8 +75,6 @@ 2.1.1 2.1.1 2.1.6 - 2.1.1 - 2.1.1 2.1.1 2.1.1 2.1.1 diff --git a/build/external-dependencies.props b/build/external-dependencies.props index d73865747c2b..ceb82576f807 100644 --- a/build/external-dependencies.props +++ b/build/external-dependencies.props @@ -64,8 +64,6 @@ - - diff --git a/eng/Dependencies.props b/eng/Dependencies.props index 8cc1807b8b6b..161726cfff04 100644 --- a/eng/Dependencies.props +++ b/eng/Dependencies.props @@ -38,7 +38,6 @@ - diff --git a/src/Http/Routing.Abstractions/src/Microsoft.AspNetCore.Routing.Abstractions.csproj b/src/Http/Routing.Abstractions/src/Microsoft.AspNetCore.Routing.Abstractions.csproj index edb292bb22cc..ff154fe535de 100644 --- a/src/Http/Routing.Abstractions/src/Microsoft.AspNetCore.Routing.Abstractions.csproj +++ b/src/Http/Routing.Abstractions/src/Microsoft.AspNetCore.Routing.Abstractions.csproj @@ -11,8 +11,11 @@ Microsoft.AspNetCore.Routing.RouteData aspnetcore;routing + + + + - diff --git a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj index cc8684caf401..af4bc3a24f2f 100644 --- a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj +++ b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj @@ -11,6 +11,10 @@ Microsoft.AspNetCore.Routing.RouteCollection aspnetcore;routing + + + + @@ -18,6 +22,5 @@ Microsoft.AspNetCore.Routing.RouteCollection - diff --git a/src/Mvc/build/dependencies.props b/src/Mvc/build/dependencies.props index 79feafc75023..641493262579 100644 --- a/src/Mvc/build/dependencies.props +++ b/src/Mvc/build/dependencies.props @@ -89,8 +89,6 @@ 2.1.1 2.1.1 2.1.1 - 2.1.1 - 2.1.1 2.1.1 2.1.1 2.1.1 diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Abstractions/Microsoft.AspNetCore.Mvc.Abstractions.csproj b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Abstractions/Microsoft.AspNetCore.Mvc.Abstractions.csproj index f981b4b8a158..41cfcba32b6b 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Abstractions/Microsoft.AspNetCore.Mvc.Abstractions.csproj +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Abstractions/Microsoft.AspNetCore.Mvc.Abstractions.csproj @@ -13,13 +13,12 @@ Microsoft.AspNetCore.Mvc.IActionResult + - - diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.ApiExplorer/Microsoft.AspNetCore.Mvc.ApiExplorer.csproj b/src/Mvc/src/Microsoft.AspNetCore.Mvc.ApiExplorer/Microsoft.AspNetCore.Mvc.ApiExplorer.csproj index efd7d873c4e1..0e6afbe93965 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.ApiExplorer/Microsoft.AspNetCore.Mvc.ApiExplorer.csproj +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.ApiExplorer/Microsoft.AspNetCore.Mvc.ApiExplorer.csproj @@ -10,13 +10,13 @@ + - diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Microsoft.AspNetCore.Mvc.Core.csproj b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Microsoft.AspNetCore.Mvc.Core.csproj index 7c6c5cc2a384..73da4b10f57b 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Microsoft.AspNetCore.Mvc.Core.csproj +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Microsoft.AspNetCore.Mvc.Core.csproj @@ -19,6 +19,8 @@ Microsoft.AspNetCore.Mvc.RouteAttribute + + @@ -38,8 +40,6 @@ Microsoft.AspNetCore.Mvc.RouteAttribute - - diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/Microsoft.AspNetCore.Mvc.Formatters.Xml.csproj b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/Microsoft.AspNetCore.Mvc.Formatters.Xml.csproj index 5dcb593dee39..ebcf792899a4 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/Microsoft.AspNetCore.Mvc.Formatters.Xml.csproj +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/Microsoft.AspNetCore.Mvc.Formatters.Xml.csproj @@ -10,11 +10,10 @@ + - - diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Localization/Microsoft.AspNetCore.Mvc.Localization.csproj b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Localization/Microsoft.AspNetCore.Mvc.Localization.csproj index ea50d2531291..c8d53a4da179 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Localization/Microsoft.AspNetCore.Mvc.Localization.csproj +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Localization/Microsoft.AspNetCore.Mvc.Localization.csproj @@ -11,13 +11,16 @@ Microsoft.AspNetCore.Mvc.Localization.IViewLocalizer aspnetcore;aspnetcoremvc;localization + + + + - diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor/Microsoft.AspNetCore.Mvc.Razor.csproj b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor/Microsoft.AspNetCore.Mvc.Razor.csproj index 069a8f9292dd..2679f72275f2 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor/Microsoft.AspNetCore.Mvc.Razor.csproj +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor/Microsoft.AspNetCore.Mvc.Razor.csproj @@ -12,6 +12,8 @@ + + @@ -24,8 +26,6 @@ - - diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/Microsoft.AspNetCore.Mvc.RazorPages.csproj b/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/Microsoft.AspNetCore.Mvc.RazorPages.csproj index 6b859f0d5ba1..fd59e58282af 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/Microsoft.AspNetCore.Mvc.RazorPages.csproj +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/Microsoft.AspNetCore.Mvc.RazorPages.csproj @@ -11,14 +11,14 @@ + + - - diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.TagHelpers/Microsoft.AspNetCore.Mvc.TagHelpers.csproj b/src/Mvc/src/Microsoft.AspNetCore.Mvc.TagHelpers/Microsoft.AspNetCore.Mvc.TagHelpers.csproj index 75d40d73c269..c77fa2c2b472 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.TagHelpers/Microsoft.AspNetCore.Mvc.TagHelpers.csproj +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.TagHelpers/Microsoft.AspNetCore.Mvc.TagHelpers.csproj @@ -10,6 +10,7 @@ + @@ -21,6 +22,5 @@ - diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Microsoft.AspNetCore.Mvc.ViewFeatures.csproj b/src/Mvc/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Microsoft.AspNetCore.Mvc.ViewFeatures.csproj index fb7dd6c50f9c..54a2f8e04201 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Microsoft.AspNetCore.Mvc.ViewFeatures.csproj +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Microsoft.AspNetCore.Mvc.ViewFeatures.csproj @@ -17,6 +17,8 @@ Microsoft.AspNetCore.Mvc.ViewComponent + + @@ -29,8 +31,6 @@ Microsoft.AspNetCore.Mvc.ViewComponent - - diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.WebApiCompatShim/Microsoft.AspNetCore.Mvc.WebApiCompatShim.csproj b/src/Mvc/src/Microsoft.AspNetCore.Mvc.WebApiCompatShim/Microsoft.AspNetCore.Mvc.WebApiCompatShim.csproj index ab1d11a599f7..a40f7a0d1566 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.WebApiCompatShim/Microsoft.AspNetCore.Mvc.WebApiCompatShim.csproj +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.WebApiCompatShim/Microsoft.AspNetCore.Mvc.WebApiCompatShim.csproj @@ -10,13 +10,16 @@ System.Web.Http.ApiController aspnetcore;aspnetcoremvc;aspnetwebapi + + + + - diff --git a/src/Shared/PropertyActivator/PropertyActivator.cs b/src/Shared/PropertyActivator/PropertyActivator.cs new file mode 100644 index 000000000000..925f6a76ae0b --- /dev/null +++ b/src/Shared/PropertyActivator/PropertyActivator.cs @@ -0,0 +1,110 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Reflection; + +namespace Microsoft.Extensions.Internal +{ + internal class PropertyActivator + { + private readonly Func _valueAccessor; + private readonly Action _fastPropertySetter; + + public PropertyActivator( + PropertyInfo propertyInfo, + Func valueAccessor) + { + if (propertyInfo == null) + { + throw new ArgumentNullException(nameof(propertyInfo)); + } + + if (valueAccessor == null) + { + throw new ArgumentNullException(nameof(valueAccessor)); + } + + PropertyInfo = propertyInfo; + _valueAccessor = valueAccessor; + _fastPropertySetter = PropertyHelper.MakeFastPropertySetter(propertyInfo); + } + + public PropertyInfo PropertyInfo { get; private set; } + + public object Activate(object instance, TContext context) + { + if (instance == null) + { + throw new ArgumentNullException(nameof(instance)); + } + + var value = _valueAccessor(context); + _fastPropertySetter(instance, value); + return value; + } + + public static PropertyActivator[] GetPropertiesToActivate( + Type type, + Type activateAttributeType, + Func> createActivateInfo) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + + if (activateAttributeType == null) + { + throw new ArgumentNullException(nameof(activateAttributeType)); + } + + if (createActivateInfo == null) + { + throw new ArgumentNullException(nameof(createActivateInfo)); + } + + return GetPropertiesToActivate(type, activateAttributeType, createActivateInfo, includeNonPublic: false); + } + + public static PropertyActivator[] GetPropertiesToActivate( + Type type, + Type activateAttributeType, + Func> createActivateInfo, + bool includeNonPublic) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + + if (activateAttributeType == null) + { + throw new ArgumentNullException(nameof(activateAttributeType)); + } + + if (createActivateInfo == null) + { + throw new ArgumentNullException(nameof(createActivateInfo)); + } + + var properties = type.GetRuntimeProperties() + .Where((property) => + { + return + property.IsDefined(activateAttributeType) && + property.GetIndexParameters().Length == 0 && + property.SetMethod != null && + !property.SetMethod.IsStatic; + }); + + if (!includeNonPublic) + { + properties = properties.Where(property => property.SetMethod.IsPublic); + } + + return properties.Select(createActivateInfo).ToArray(); + } + } +} \ No newline at end of file diff --git a/src/Shared/PropertyHelper/PropertyHelper.cs b/src/Shared/PropertyHelper/PropertyHelper.cs new file mode 100644 index 000000000000..27ba5661a423 --- /dev/null +++ b/src/Shared/PropertyHelper/PropertyHelper.cs @@ -0,0 +1,526 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; + +namespace Microsoft.Extensions.Internal +{ + internal class PropertyHelper + { + // Delegate type for a by-ref property getter + private delegate TValue ByRefFunc(ref TDeclaringType arg); + + private static readonly MethodInfo CallPropertyGetterOpenGenericMethod = + typeof(PropertyHelper).GetTypeInfo().GetDeclaredMethod(nameof(CallPropertyGetter)); + + private static readonly MethodInfo CallPropertyGetterByReferenceOpenGenericMethod = + typeof(PropertyHelper).GetTypeInfo().GetDeclaredMethod(nameof(CallPropertyGetterByReference)); + + private static readonly MethodInfo CallNullSafePropertyGetterOpenGenericMethod = + typeof(PropertyHelper).GetTypeInfo().GetDeclaredMethod(nameof(CallNullSafePropertyGetter)); + + private static readonly MethodInfo CallNullSafePropertyGetterByReferenceOpenGenericMethod = + typeof(PropertyHelper).GetTypeInfo().GetDeclaredMethod(nameof(CallNullSafePropertyGetterByReference)); + + private static readonly MethodInfo CallPropertySetterOpenGenericMethod = + typeof(PropertyHelper).GetTypeInfo().GetDeclaredMethod(nameof(CallPropertySetter)); + + // Using an array rather than IEnumerable, as target will be called on the hot path numerous times. + private static readonly ConcurrentDictionary PropertiesCache = + new ConcurrentDictionary(); + + private static readonly ConcurrentDictionary VisiblePropertiesCache = + new ConcurrentDictionary(); + + private Action _valueSetter; + private Func _valueGetter; + + /// + /// Initializes a fast . + /// This constructor does not cache the helper. For caching, use . + /// + public PropertyHelper(PropertyInfo property) + { + if (property == null) + { + throw new ArgumentNullException(nameof(property)); + } + + Property = property; + Name = property.Name; + } + + /// + /// Gets the backing . + /// + public PropertyInfo Property { get; } + + /// + /// Gets (or sets in derived types) the property name. + /// + public virtual string Name { get; protected set; } + + /// + /// Gets the property value getter. + /// + public Func ValueGetter + { + get + { + if (_valueGetter == null) + { + _valueGetter = MakeFastPropertyGetter(Property); + } + + return _valueGetter; + } + } + + /// + /// Gets the property value setter. + /// + public Action ValueSetter + { + get + { + if (_valueSetter == null) + { + _valueSetter = MakeFastPropertySetter(Property); + } + + return _valueSetter; + } + } + + /// + /// Returns the property value for the specified . + /// + /// The object whose property value will be returned. + /// The property value. + public object GetValue(object instance) + { + return ValueGetter(instance); + } + + /// + /// Sets the property value for the specified . + /// + /// The object whose property value will be set. + /// The property value. + public void SetValue(object instance, object value) + { + ValueSetter(instance, value); + } + + /// + /// Creates and caches fast property helpers that expose getters for every public get property on the + /// underlying type. + /// + /// The type info to extract property accessors for. + /// A cached array of all public properties of the specified type. + /// + public static PropertyHelper[] GetProperties(TypeInfo typeInfo) + { + return GetProperties(typeInfo.AsType()); + } + + /// + /// Creates and caches fast property helpers that expose getters for every public get property on the + /// specified type. + /// + /// The type to extract property accessors for. + /// A cached array of all public properties of the specified type. + /// + public static PropertyHelper[] GetProperties(Type type) + { + return GetProperties(type, CreateInstance, PropertiesCache); + } + + /// + /// + /// Creates and caches fast property helpers that expose getters for every non-hidden get property + /// on the specified type. + /// + /// + /// excludes properties defined on base types that have been + /// hidden by definitions using the new keyword. + /// + /// + /// The type info to extract property accessors for. + /// + /// A cached array of all public properties of the specified type. + /// + public static PropertyHelper[] GetVisibleProperties(TypeInfo typeInfo) + { + return GetVisibleProperties(typeInfo.AsType(), CreateInstance, PropertiesCache, VisiblePropertiesCache); + } + + /// + /// + /// Creates and caches fast property helpers that expose getters for every non-hidden get property + /// on the specified type. + /// + /// + /// excludes properties defined on base types that have been + /// hidden by definitions using the new keyword. + /// + /// + /// The type to extract property accessors for. + /// + /// A cached array of all public properties of the specified type. + /// + public static PropertyHelper[] GetVisibleProperties(Type type) + { + return GetVisibleProperties(type, CreateInstance, PropertiesCache, VisiblePropertiesCache); + } + + /// + /// Creates a single fast property getter. The result is not cached. + /// + /// propertyInfo to extract the getter for. + /// a fast getter. + /// + /// This method is more memory efficient than a dynamically compiled lambda, and about the + /// same speed. + /// + public static Func MakeFastPropertyGetter(PropertyInfo propertyInfo) + { + Debug.Assert(propertyInfo != null); + + return MakeFastPropertyGetter( + propertyInfo, + CallPropertyGetterOpenGenericMethod, + CallPropertyGetterByReferenceOpenGenericMethod); + } + + /// + /// Creates a single fast property getter which is safe for a null input object. The result is not cached. + /// + /// propertyInfo to extract the getter for. + /// a fast getter. + /// + /// This method is more memory efficient than a dynamically compiled lambda, and about the + /// same speed. + /// + public static Func MakeNullSafeFastPropertyGetter(PropertyInfo propertyInfo) + { + Debug.Assert(propertyInfo != null); + + return MakeFastPropertyGetter( + propertyInfo, + CallNullSafePropertyGetterOpenGenericMethod, + CallNullSafePropertyGetterByReferenceOpenGenericMethod); + } + + private static Func MakeFastPropertyGetter( + PropertyInfo propertyInfo, + MethodInfo propertyGetterWrapperMethod, + MethodInfo propertyGetterByRefWrapperMethod) + { + Debug.Assert(propertyInfo != null); + + // Must be a generic method with a Func<,> parameter + Debug.Assert(propertyGetterWrapperMethod != null); + Debug.Assert(propertyGetterWrapperMethod.IsGenericMethodDefinition); + Debug.Assert(propertyGetterWrapperMethod.GetParameters().Length == 2); + + // Must be a generic method with a ByRefFunc<,> parameter + Debug.Assert(propertyGetterByRefWrapperMethod != null); + Debug.Assert(propertyGetterByRefWrapperMethod.IsGenericMethodDefinition); + Debug.Assert(propertyGetterByRefWrapperMethod.GetParameters().Length == 2); + + var getMethod = propertyInfo.GetMethod; + Debug.Assert(getMethod != null); + Debug.Assert(!getMethod.IsStatic); + Debug.Assert(getMethod.GetParameters().Length == 0); + + // Instance methods in the CLR can be turned into static methods where the first parameter + // is open over "target". This parameter is always passed by reference, so we have a code + // path for value types and a code path for reference types. + if (getMethod.DeclaringType.GetTypeInfo().IsValueType) + { + // Create a delegate (ref TDeclaringType) -> TValue + return MakeFastPropertyGetter( + typeof(ByRefFunc<,>), + getMethod, + propertyGetterByRefWrapperMethod); + } + else + { + // Create a delegate TDeclaringType -> TValue + return MakeFastPropertyGetter( + typeof(Func<,>), + getMethod, + propertyGetterWrapperMethod); + } + } + + private static Func MakeFastPropertyGetter( + Type openGenericDelegateType, + MethodInfo propertyGetMethod, + MethodInfo openGenericWrapperMethod) + { + var typeInput = propertyGetMethod.DeclaringType; + var typeOutput = propertyGetMethod.ReturnType; + + var delegateType = openGenericDelegateType.MakeGenericType(typeInput, typeOutput); + var propertyGetterDelegate = propertyGetMethod.CreateDelegate(delegateType); + + var wrapperDelegateMethod = openGenericWrapperMethod.MakeGenericMethod(typeInput, typeOutput); + var accessorDelegate = wrapperDelegateMethod.CreateDelegate( + typeof(Func), + propertyGetterDelegate); + + return (Func)accessorDelegate; + } + + /// + /// Creates a single fast property setter for reference types. The result is not cached. + /// + /// propertyInfo to extract the setter for. + /// a fast getter. + /// + /// This method is more memory efficient than a dynamically compiled lambda, and about the + /// same speed. This only works for reference types. + /// + public static Action MakeFastPropertySetter(PropertyInfo propertyInfo) + { + Debug.Assert(propertyInfo != null); + Debug.Assert(!propertyInfo.DeclaringType.GetTypeInfo().IsValueType); + + var setMethod = propertyInfo.SetMethod; + Debug.Assert(setMethod != null); + Debug.Assert(!setMethod.IsStatic); + Debug.Assert(setMethod.ReturnType == typeof(void)); + var parameters = setMethod.GetParameters(); + Debug.Assert(parameters.Length == 1); + + // Instance methods in the CLR can be turned into static methods where the first parameter + // is open over "target". This parameter is always passed by reference, so we have a code + // path for value types and a code path for reference types. + var typeInput = setMethod.DeclaringType; + var parameterType = parameters[0].ParameterType; + + // Create a delegate TDeclaringType -> { TDeclaringType.Property = TValue; } + var propertySetterAsAction = + setMethod.CreateDelegate(typeof(Action<,>).MakeGenericType(typeInput, parameterType)); + var callPropertySetterClosedGenericMethod = + CallPropertySetterOpenGenericMethod.MakeGenericMethod(typeInput, parameterType); + var callPropertySetterDelegate = + callPropertySetterClosedGenericMethod.CreateDelegate( + typeof(Action), propertySetterAsAction); + + return (Action)callPropertySetterDelegate; + } + + /// + /// Given an object, adds each instance property with a public get method as a key and its + /// associated value to a dictionary. + /// + /// If the object is already an instance, then a copy + /// is returned. + /// + /// + /// The implementation of PropertyHelper will cache the property accessors per-type. This is + /// faster when the same type is used multiple times with ObjectToDictionary. + /// + public static IDictionary ObjectToDictionary(object value) + { + var dictionary = value as IDictionary; + if (dictionary != null) + { + return new Dictionary(dictionary, StringComparer.OrdinalIgnoreCase); + } + + dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + + if (value != null) + { + foreach (var helper in GetProperties(value.GetType())) + { + dictionary[helper.Name] = helper.GetValue(value); + } + } + + return dictionary; + } + + private static PropertyHelper CreateInstance(PropertyInfo property) + { + return new PropertyHelper(property); + } + + // Called via reflection + private static object CallPropertyGetter( + Func getter, + object target) + { + return getter((TDeclaringType)target); + } + + // Called via reflection + private static object CallPropertyGetterByReference( + ByRefFunc getter, + object target) + { + var unboxed = (TDeclaringType)target; + return getter(ref unboxed); + } + + // Called via reflection + private static object CallNullSafePropertyGetter( + Func getter, + object target) + { + if (target == null) + { + return null; + } + + return getter((TDeclaringType)target); + } + + // Called via reflection + private static object CallNullSafePropertyGetterByReference( + ByRefFunc getter, + object target) + { + if (target == null) + { + return null; + } + + var unboxed = (TDeclaringType)target; + return getter(ref unboxed); + } + + private static void CallPropertySetter( + Action setter, + object target, + object value) + { + setter((TDeclaringType)target, (TValue)value); + } + + protected static PropertyHelper[] GetVisibleProperties( + Type type, + Func createPropertyHelper, + ConcurrentDictionary allPropertiesCache, + ConcurrentDictionary visiblePropertiesCache) + { + PropertyHelper[] result; + if (visiblePropertiesCache.TryGetValue(type, out result)) + { + return result; + } + + // The simple and common case, this is normal POCO object - no need to allocate. + var allPropertiesDefinedOnType = true; + var allProperties = GetProperties(type, createPropertyHelper, allPropertiesCache); + foreach (var propertyHelper in allProperties) + { + if (propertyHelper.Property.DeclaringType != type) + { + allPropertiesDefinedOnType = false; + break; + } + } + + if (allPropertiesDefinedOnType) + { + result = allProperties; + visiblePropertiesCache.TryAdd(type, result); + return result; + } + + // There's some inherited properties here, so we need to check for hiding via 'new'. + var filteredProperties = new List(allProperties.Length); + foreach (var propertyHelper in allProperties) + { + var declaringType = propertyHelper.Property.DeclaringType; + if (declaringType == type) + { + filteredProperties.Add(propertyHelper); + continue; + } + + // If this property was declared on a base type then look for the definition closest to the + // the type to see if we should include it. + var ignoreProperty = false; + + // Walk up the hierarchy until we find the type that actually declares this + // PropertyInfo. + var currentTypeInfo = type.GetTypeInfo(); + var declaringTypeInfo = declaringType.GetTypeInfo(); + while (currentTypeInfo != null && currentTypeInfo != declaringTypeInfo) + { + // We've found a 'more proximal' public definition + var declaredProperty = currentTypeInfo.GetDeclaredProperty(propertyHelper.Name); + if (declaredProperty != null) + { + ignoreProperty = true; + break; + } + + currentTypeInfo = currentTypeInfo.BaseType?.GetTypeInfo(); + } + + if (!ignoreProperty) + { + filteredProperties.Add(propertyHelper); + } + } + + result = filteredProperties.ToArray(); + visiblePropertiesCache.TryAdd(type, result); + return result; + } + + protected static PropertyHelper[] GetProperties( + Type type, + Func createPropertyHelper, + ConcurrentDictionary cache) + { + // Unwrap nullable types. This means Nullable.Value and Nullable.HasValue will not be + // part of the sequence of properties returned by this method. + type = Nullable.GetUnderlyingType(type) ?? type; + + PropertyHelper[] helpers; + if (!cache.TryGetValue(type, out helpers)) + { + // We avoid loading indexed properties using the Where statement. + var properties = type.GetRuntimeProperties().Where(IsInterestingProperty); + + var typeInfo = type.GetTypeInfo(); + if (typeInfo.IsInterface) + { + // Reflection does not return information about inherited properties on the interface itself. + properties = properties.Concat(typeInfo.ImplementedInterfaces.SelectMany( + interfaceType => interfaceType.GetRuntimeProperties().Where(IsInterestingProperty))); + } + + helpers = properties.Select(p => createPropertyHelper(p)).ToArray(); + cache.TryAdd(type, helpers); + } + + return helpers; + } + + // Indexed properties are not useful (or valid) for grabbing properties off an object. + private static bool IsInterestingProperty(PropertyInfo property) + { + // For improving application startup time, do not use GetIndexParameters() api early in this check as it + // creates a copy of parameter array and also we would like to check for the presence of a get method + // and short circuit asap. + return property.GetMethod != null && + property.GetMethod.IsPublic && + !property.GetMethod.IsStatic && + property.GetMethod.GetParameters().Length == 0; + } + } +} diff --git a/src/Shared/test/Microsoft.AspNetCore.Shared.Tests.csproj b/src/Shared/test/Microsoft.AspNetCore.Shared.Tests.csproj index ca95ec8b525a..72799639c133 100644 --- a/src/Shared/test/Microsoft.AspNetCore.Shared.Tests.csproj +++ b/src/Shared/test/Microsoft.AspNetCore.Shared.Tests.csproj @@ -8,6 +8,8 @@ + + diff --git a/src/Shared/test/PropertyActivatorTest.cs b/src/Shared/test/PropertyActivatorTest.cs new file mode 100644 index 000000000000..a5cb1605b38c --- /dev/null +++ b/src/Shared/test/PropertyActivatorTest.cs @@ -0,0 +1,187 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Reflection; +using Xunit; + +namespace Microsoft.Extensions.Internal +{ + public class PropertyActivatorTest + { + [Fact] + public void Activate_InvokesValueAccessorWithExpectedValue() + { + // Arrange + var instance = new TestClass(); + var typeInfo = instance.GetType().GetTypeInfo(); + var property = typeInfo.GetDeclaredProperty("IntProperty"); + var invokedWith = -1; + var activator = new PropertyActivator( + property, + valueAccessor: (val) => + { + invokedWith = val; + return val; + }); + + // Act + activator.Activate(instance, 123); + + // Assert + Assert.Equal(123, invokedWith); + } + + [Fact] + public void Activate_SetsPropertyValue() + { + // Arrange + var instance = new TestClass(); + var typeInfo = instance.GetType().GetTypeInfo(); + var property = typeInfo.GetDeclaredProperty("IntProperty"); + var activator = new PropertyActivator(property, valueAccessor: (val) => val + 1); + + // Act + activator.Activate(instance, 123); + + // Assert + Assert.Equal(124, instance.IntProperty); + } + + [Fact] + public void GetPropertiesToActivate_RestrictsActivatableProperties() + { + // Arrange + var instance = new TestClass(); + var typeInfo = instance.GetType().GetTypeInfo(); + var expectedPropertyInfo = typeInfo.GetDeclaredProperty("ActivatableProperty"); + + // Act + var propertiesToActivate = PropertyActivator.GetPropertiesToActivate( + type: typeof(TestClass), + activateAttributeType: typeof(TestActivateAttribute), + createActivateInfo: + (propertyInfo) => new PropertyActivator(propertyInfo, valueAccessor: (val) => val + 1)); + + // Assert + Assert.Collection( + propertiesToActivate, + (activator) => + { + Assert.Equal(expectedPropertyInfo, activator.PropertyInfo); + }); + } + + [Fact] + public void GetPropertiesToActivate_CanCreateCustomPropertyActivators() + { + // Arrange + var instance = new TestClass(); + var typeInfo = instance.GetType().GetTypeInfo(); + var expectedPropertyInfo = typeInfo.GetDeclaredProperty("IntProperty"); + + // Act + var propertiesToActivate = PropertyActivator.GetPropertiesToActivate( + type: typeof(TestClass), + activateAttributeType: typeof(TestActivateAttribute), + createActivateInfo: + (propertyInfo) => new PropertyActivator(expectedPropertyInfo, valueAccessor: (val) => val + 1)); + + // Assert + Assert.Collection( + propertiesToActivate, + (activator) => + { + Assert.Equal(expectedPropertyInfo, activator.PropertyInfo); + }); + } + + [Fact] + public void GetPropertiesToActivate_ExcludesNonPublic() + { + // Arrange + var instance = new TestClassWithPropertyVisiblity(); + var typeInfo = instance.GetType().GetTypeInfo(); + var expectedPropertyInfo = typeInfo.GetDeclaredProperty("Public"); + + // Act + var propertiesToActivate = PropertyActivator.GetPropertiesToActivate( + typeof(TestClassWithPropertyVisiblity), + typeof(TestActivateAttribute), + (propertyInfo) => new PropertyActivator(propertyInfo, valueAccessor: (val) => val)); + + // Assert + Assert.Single(propertiesToActivate); + Assert.Single(propertiesToActivate, p => p.PropertyInfo == expectedPropertyInfo); + } + + [Fact] + public void GetPropertiesToActivate_IncludesNonPublic() + { + // Arrange + var instance = new TestClassWithPropertyVisiblity(); + var typeInfo = instance.GetType().GetTypeInfo(); + + // Act + var propertiesToActivate = PropertyActivator.GetPropertiesToActivate( + typeof(TestClassWithPropertyVisiblity), + typeof(TestActivateAttribute), + (propertyInfo) => new PropertyActivator(propertyInfo, valueAccessor: (val) => val), + includeNonPublic: true); + + // Assert + Assert.Equal(5, propertiesToActivate.Length); + } + + private class TestClass + { + public int IntProperty { get; set; } + + [TestActivate] + public int ActivatableProperty { get; set; } + + [TestActivate] + public int NoSetterActivatableProperty { get; } + + [TestActivate] + public int this[int something] // Not activatable + { + get + { + return 0; + } + } + + [TestActivate] + public static int StaticActivatablProperty { get; set; } + } + + private class TestClassWithPropertyVisiblity + { + [TestActivate] + public int Public { get; set; } + + [TestActivate] + protected int Protected { get; set; } + + [TestActivate] + internal int Internal { get; set; } + + [TestActivate] + protected internal int ProtectedInternal {get; set; } + + [TestActivate] + private int Private { get; set; } + } + + [AttributeUsage(AttributeTargets.Property)] + private class TestActivateAttribute : Attribute + { + } + + private class ActivationInfo + { + public string Name { get; set; } + } + } +} diff --git a/src/Shared/test/PropertyHelperTest.cs b/src/Shared/test/PropertyHelperTest.cs new file mode 100644 index 000000000000..19cf08b3705a --- /dev/null +++ b/src/Shared/test/PropertyHelperTest.cs @@ -0,0 +1,831 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Xunit; + +namespace Microsoft.Extensions.Internal +{ + public class PropertyHelperTest + { + [Fact] + public void PropertyHelper_ReturnsNameCorrectly() + { + // Arrange + var anonymous = new { foo = "bar" }; + var property = PropertyHelper.GetProperties(anonymous.GetType()).First().Property; + + // Act + var helper = new PropertyHelper(property); + + // Assert + Assert.Equal("foo", property.Name); + Assert.Equal("foo", helper.Name); + } + + [Fact] + public void PropertyHelper_ReturnsValueCorrectly() + { + // Arrange + var anonymous = new { bar = "baz" }; + var property = PropertyHelper.GetProperties(anonymous.GetType()).First().Property; + + // Act + var helper = new PropertyHelper(property); + + // Assert + Assert.Equal("bar", helper.Name); + Assert.Equal("baz", helper.GetValue(anonymous)); + } + + [Fact] + public void PropertyHelper_ReturnsGetterDelegate() + { + // Arrange + var anonymous = new { bar = "baz" }; + var property = PropertyHelper.GetProperties(anonymous.GetType()).First().Property; + + // Act + var helper = new PropertyHelper(property); + + // Assert + Assert.NotNull(helper.ValueGetter); + Assert.Equal("baz", helper.ValueGetter(anonymous)); + } + + [Fact] + public void SetValue_SetsPropertyValue() + { + // Arrange + var expected = "new value"; + var instance = new BaseClass { PropA = "old value" }; + var helper = PropertyHelper.GetProperties( + instance.GetType()).First(prop => prop.Name == "PropA"); + + // Act + helper.SetValue(instance, expected); + + // Assert + Assert.Equal(expected, instance.PropA); + } + + [Fact] + public void PropertyHelper_ReturnsSetterDelegate() + { + // Arrange + var expected = "new value"; + var instance = new BaseClass { PropA = "old value" }; + var helper = PropertyHelper.GetProperties( + instance.GetType()).First(prop => prop.Name == "PropA"); + + // Act and Assert + Assert.NotNull(helper.ValueSetter); + helper.ValueSetter(instance, expected); + + // Assert + Assert.Equal(expected, instance.PropA); + } + + [Fact] + public void PropertyHelper_ReturnsValueCorrectly_ForValueTypes() + { + // Arrange + var anonymous = new { foo = 32 }; + var property = PropertyHelper.GetProperties(anonymous.GetType()).First().Property; + + // Act + var helper = new PropertyHelper(property); + + // Assert + Assert.Equal("foo", helper.Name); + Assert.Equal(32, helper.GetValue(anonymous)); + } + + [Fact] + public void PropertyHelper_ReturnsCachedPropertyHelper() + { + // Arrange + var anonymous = new { foo = "bar" }; + + // Act + var helpers1 = PropertyHelper.GetProperties(anonymous.GetType().GetTypeInfo()); + var helpers2 = PropertyHelper.GetProperties(anonymous.GetType().GetTypeInfo()); + + // Assert + Assert.Single(helpers1); + Assert.Same(helpers1, helpers2); + Assert.Same(helpers1[0], helpers2[0]); + } + + [Fact] + public void PropertyHelper_DoesNotChangeUnderscores() + { + // Arrange + var anonymous = new { bar_baz2 = "foo" }; + + // Act + Assert + var helper = Assert.Single(PropertyHelper.GetProperties(anonymous.GetType().GetTypeInfo())); + Assert.Equal("bar_baz2", helper.Name); + } + + [Fact] + public void PropertyHelper_DoesNotFindPrivateProperties() + { + // Arrange + var anonymous = new PrivateProperties(); + + // Act + Assert + var helper = Assert.Single(PropertyHelper.GetProperties(anonymous.GetType().GetTypeInfo())); + Assert.Equal("Prop1", helper.Name); + } + + [Fact] + public void PropertyHelper_DoesNotFindStaticProperties() + { + // Arrange + var anonymous = new Static(); + + // Act + Assert + var helper = Assert.Single(PropertyHelper.GetProperties(anonymous.GetType().GetTypeInfo())); + Assert.Equal("Prop5", helper.Name); + } + + [Fact] + public void PropertyHelper_DoesNotFindSetOnlyProperties() + { + // Arrange + var anonymous = new SetOnly(); + + // Act + Assert + var helper = Assert.Single(PropertyHelper.GetProperties(anonymous.GetType().GetTypeInfo())); + Assert.Equal("Prop6", helper.Name); + } + + [Theory] + [InlineData(typeof(int?))] + [InlineData(typeof(DayOfWeek?))] + public void PropertyHelper_WorksForNullablePrimitiveAndEnumTypes(Type nullableType) + { + // Act + var properties = PropertyHelper.GetProperties(nullableType); + + // Assert + Assert.Empty(properties); + } + + [Fact] + public void PropertyHelper_UnwrapsNullableTypes() + { + // Arrange + var myType = typeof(MyStruct?); + + // Act + var properties = PropertyHelper.GetProperties(myType); + + // Assert + var property = Assert.Single(properties); + Assert.Equal("Foo", property.Name); + } + + [Fact] + public void PropertyHelper_WorksForStruct() + { + // Arrange + var anonymous = new MyProperties(); + + anonymous.IntProp = 3; + anonymous.StringProp = "Five"; + + // Act + Assert + var helper1 = Assert.Single(PropertyHelper.GetProperties(anonymous.GetType().GetTypeInfo()).Where(prop => prop.Name == "IntProp")); + var helper2 = Assert.Single(PropertyHelper.GetProperties(anonymous.GetType().GetTypeInfo()).Where(prop => prop.Name == "StringProp")); + Assert.Equal(3, helper1.GetValue(anonymous)); + Assert.Equal("Five", helper2.GetValue(anonymous)); + } + + [Fact] + public void PropertyHelper_ForDerivedClass() + { + // Arrange + var derived = new DerivedClass { PropA = "propAValue", PropB = "propBValue" }; + + // Act + var helpers = PropertyHelper.GetProperties(derived.GetType().GetTypeInfo()).ToArray(); + + // Assert + Assert.NotNull(helpers); + Assert.Equal(2, helpers.Length); + + var propAHelper = Assert.Single(helpers.Where(h => h.Name == "PropA")); + var propBHelper = Assert.Single(helpers.Where(h => h.Name == "PropB")); + + Assert.Equal("propAValue", propAHelper.GetValue(derived)); + Assert.Equal("propBValue", propBHelper.GetValue(derived)); + } + + [Fact] + public void PropertyHelper_ForDerivedClass_WithNew() + { + // Arrange + var derived = new DerivedClassWithNew { PropA = "propAValue" }; + + // Act + var helpers = PropertyHelper.GetProperties(derived.GetType().GetTypeInfo()).ToArray(); + + // Assert + Assert.NotNull(helpers); + Assert.Equal(2, helpers.Length); + + var propAHelper = Assert.Single(helpers.Where(h => h.Name == "PropA")); + var propBHelper = Assert.Single(helpers.Where(h => h.Name == "PropB")); + + Assert.Equal("propAValue", propAHelper.GetValue(derived)); + Assert.Equal("Newed", propBHelper.GetValue(derived)); + } + + [Fact] + public void PropertyHelper_ForDerived_WithVirtual() + { + // Arrange + var derived = new DerivedClassWithOverride { PropA = "propAValue", PropB = "propBValue" }; + + // Act + var helpers = PropertyHelper.GetProperties(derived.GetType().GetTypeInfo()).ToArray(); + + // Assert + Assert.NotNull(helpers); + Assert.Equal(2, helpers.Length); + + var propAHelper = Assert.Single(helpers.Where(h => h.Name == "PropA")); + var propBHelper = Assert.Single(helpers.Where(h => h.Name == "PropB")); + + Assert.Equal("OverridenpropAValue", propAHelper.GetValue(derived)); + Assert.Equal("propBValue", propBHelper.GetValue(derived)); + } + + [Fact] + public void PropertyHelper_ForInterface_ReturnsExpectedProperties() + { + // Arrange + var expectedNames = new[] { "Count", "IsReadOnly" }; + + // Act + var helpers = PropertyHelper.GetProperties(typeof(ICollection)); + + // Assert + Assert.Collection( + helpers.OrderBy(helper => helper.Name, StringComparer.Ordinal), + helper => { Assert.Equal(expectedNames[0], helper.Name, StringComparer.Ordinal); }, + helper => { Assert.Equal(expectedNames[1], helper.Name, StringComparer.Ordinal); }); + } + + [Fact] + public void PropertyHelper_ForDerivedInterface_ReturnsAllProperties() + { + // Arrange + var expectedNames = new[] { "Count", "IsReadOnly", "Keys", "Values" }; + + // Act + var helpers = PropertyHelper.GetProperties(typeof(IDictionary)); + + // Assert + Assert.Collection( + helpers.OrderBy(helper => helper.Name, StringComparer.Ordinal), + helper => { Assert.Equal(expectedNames[0], helper.Name, StringComparer.Ordinal); }, + helper => { Assert.Equal(expectedNames[1], helper.Name, StringComparer.Ordinal); }, + helper => { Assert.Equal(expectedNames[2], helper.Name, StringComparer.Ordinal); }, + helper => { Assert.Equal(expectedNames[3], helper.Name, StringComparer.Ordinal); }); + } + + [Fact] + public void GetProperties_ExcludesIndexersAndPropertiesWithoutPublicGetters() + { + // Arrange + var type = typeof(DerivedClassWithNonReadableProperties); + + // Act + var result = PropertyHelper.GetProperties(type).ToArray(); + + // Assert + Assert.Equal(3, result.Length); + Assert.Equal("Visible", result[0].Name); + Assert.Equal("PropA", result[1].Name); + Assert.Equal("PropB", result[2].Name); + } + + [Fact] + public void GetVisibleProperties_NoHiddenProperty() + { + // Arrange + var type = typeof(string); + + // Act + var result = PropertyHelper.GetVisibleProperties(type).ToArray(); + + // Assert + var property = Assert.Single(result); + Assert.Equal("Length", property.Name); + Assert.Equal(typeof(int), property.Property.PropertyType); + } + + [Fact] + public void GetVisibleProperties_HiddenProperty() + { + // Arrange + var type = typeof(DerivedHiddenProperty); + + // Act + var result = PropertyHelper.GetVisibleProperties(type).ToArray(); + + // Assert + Assert.Equal(2, result.Length); + Assert.Equal("Id", result[0].Name); + Assert.Equal(typeof(string), result[0].Property.PropertyType); + Assert.Equal("Name", result[1].Name); + Assert.Equal(typeof(string), result[1].Property.PropertyType); + } + + [Fact] + public void GetVisibleProperties_HiddenProperty_TwoLevels() + { + // Arrange + var type = typeof(DerivedHiddenProperty2); + + // Act + var result = PropertyHelper.GetVisibleProperties(type).ToArray(); + + // Assert + Assert.Equal(2, result.Length); + Assert.Equal("Id", result[0].Name); + Assert.Equal(typeof(Guid), result[0].Property.PropertyType); + Assert.Equal("Name", result[1].Name); + Assert.Equal(typeof(string), result[1].Property.PropertyType); + } + + [Fact] + public void GetVisibleProperties_NoHiddenPropertyWithTypeInfoInput() + { + // Arrange + var type = typeof(string); + + // Act + var result = PropertyHelper.GetVisibleProperties(type.GetTypeInfo()).ToArray(); + + // Assert + var property = Assert.Single(result); + Assert.Equal("Length", property.Name); + Assert.Equal(typeof(int), property.Property.PropertyType); + } + + [Fact] + public void GetVisibleProperties_HiddenPropertyWithTypeInfoInput() + { + // Arrange + var type = typeof(DerivedHiddenProperty); + + // Act + var result = PropertyHelper.GetVisibleProperties(type.GetTypeInfo()).ToArray(); + + // Assert + Assert.Equal(2, result.Length); + Assert.Equal("Id", result[0].Name); + Assert.Equal(typeof(string), result[0].Property.PropertyType); + Assert.Equal("Name", result[1].Name); + Assert.Equal(typeof(string), result[1].Property.PropertyType); + } + + [Fact] + public void GetVisibleProperties_HiddenProperty_TwoLevelsWithTypeInfoInput() + { + // Arrange + var type = typeof(DerivedHiddenProperty2); + + // Act + var result = PropertyHelper.GetVisibleProperties(type.GetTypeInfo()).ToArray(); + + // Assert + Assert.Equal(2, result.Length); + Assert.Equal("Id", result[0].Name); + Assert.Equal(typeof(Guid), result[0].Property.PropertyType); + Assert.Equal("Name", result[1].Name); + Assert.Equal(typeof(string), result[1].Property.PropertyType); + } + + [Fact] + public void MakeFastPropertySetter_SetsPropertyValues_ForPublicAndNobPublicProperties() + { + // Arrange + var instance = new BaseClass(); + var typeInfo = instance.GetType().GetTypeInfo(); + var publicProperty = typeInfo.GetDeclaredProperty("PropA"); + var protectedProperty = typeInfo.GetDeclaredProperty("PropProtected"); + var publicPropertySetter = PropertyHelper.MakeFastPropertySetter(publicProperty); + var protectedPropertySetter = PropertyHelper.MakeFastPropertySetter(protectedProperty); + + // Act + publicPropertySetter(instance, "TestPublic"); + protectedPropertySetter(instance, "TestProtected"); + + // Assert + Assert.Equal("TestPublic", instance.PropA); + Assert.Equal("TestProtected", instance.GetPropProtected()); + } + + [Fact] + public void MakeFastPropertySetter_SetsPropertyValues_ForOverridenProperties() + { + // Arrange + var instance = new DerivedClassWithOverride(); + var typeInfo = instance.GetType().GetTypeInfo(); + var property = typeInfo.GetDeclaredProperty("PropA"); + var propertySetter = PropertyHelper.MakeFastPropertySetter(property); + + // Act + propertySetter(instance, "Test value"); + + // Assert + Assert.Equal("OverridenTest value", instance.PropA); + } + + [Fact] + public void MakeFastPropertySetter_SetsPropertyValues_ForNewedProperties() + { + // Arrange + var instance = new DerivedClassWithNew(); + var typeInfo = instance.GetType().GetTypeInfo(); + var property = typeInfo.GetDeclaredProperty("PropB"); + var propertySetter = PropertyHelper.MakeFastPropertySetter(property); + + // Act + propertySetter(instance, "Test value"); + + // Assert + Assert.Equal("NewedTest value", instance.PropB); + } + + [Fact] + public void MakeFastPropertyGetter_ReferenceType_ForNullObject_Throws() + { + // Arrange + var property = PropertyHelper + .GetProperties(typeof(BaseClass)) + .Single(p => p.Name == nameof(BaseClass.PropA)); + + var accessor = PropertyHelper.MakeFastPropertyGetter(property.Property); + + // Act & Assert + Assert.Throws(() => accessor(null)); + } + + [Fact] + public void MakeFastPropertyGetter_ValueType_ForNullObject_Throws() + { + // Arrange + var property = PropertyHelper + .GetProperties(typeof(MyProperties)) + .Single(p => p.Name == nameof(MyProperties.StringProp)); + + var accessor = PropertyHelper.MakeFastPropertyGetter(property.Property); + + // Act & Assert + Assert.Throws(() => accessor(null)); + } + + [Fact] + public void MakeNullSafeFastPropertyGetter_ReferenceType_Success() + { + // Arrange + var property = PropertyHelper + .GetProperties(typeof(BaseClass)) + .Single(p => p.Name == nameof(BaseClass.PropA)); + + var accessor = PropertyHelper.MakeNullSafeFastPropertyGetter(property.Property); + + // Act + var value = accessor(new BaseClass() { PropA = "Hi" }); + + // Assert + Assert.Equal("Hi", value); + } + + [Fact] + public void MakeNullSafeFastPropertyGetter_ValueType_Success() + { + // Arrange + var property = PropertyHelper + .GetProperties(typeof(MyProperties)) + .Single(p => p.Name == nameof(MyProperties.StringProp)); + + var accessor = PropertyHelper.MakeNullSafeFastPropertyGetter(property.Property); + + // Act + var value = accessor(new MyProperties() { StringProp = "Hi" }); + + // Assert + Assert.Equal("Hi", value); + } + + [Fact] + public void MakeNullSafeFastPropertyGetter_ReferenceType_ForNullObject_ReturnsNull() + { + // Arrange + var property = PropertyHelper + .GetProperties(typeof(BaseClass)) + .Single(p => p.Name == nameof(BaseClass.PropA)); + + var accessor = PropertyHelper.MakeNullSafeFastPropertyGetter(property.Property); + + // Act + var value = accessor(null); + + // Assert + Assert.Null(value); + } + + [Fact] + public void MakeNullSafeFastPropertyGetter_ValueType_ForNullObject_ReturnsNull() + { + // Arrange + var property = PropertyHelper + .GetProperties(typeof(MyProperties)) + .Single(p => p.Name == nameof(MyProperties.StringProp)); + + var accessor = PropertyHelper.MakeNullSafeFastPropertyGetter(property.Property); + + // Act + var value = accessor(null); + + // Assert + Assert.Null(value); + } + + public static TheoryData> IgnoreCaseTestData + { + get + { + return new TheoryData> + { + { + new + { + selected = true, + SeLeCtEd = false + }, + new KeyValuePair("selected", false) + }, + { + new + { + SeLeCtEd = false, + selected = true + }, + new KeyValuePair("SeLeCtEd", true) + }, + { + new + { + SelECTeD = false, + SeLECTED = true + }, + new KeyValuePair("SelECTeD", true) + } + }; + } + } + + [Theory] + [MemberData(nameof(IgnoreCaseTestData))] + public void ObjectToDictionary_IgnoresPropertyCase(object testObject, + KeyValuePair expectedEntry) + { + // Act + var result = PropertyHelper.ObjectToDictionary(testObject); + + // Assert + var entry = Assert.Single(result); + Assert.Equal(expectedEntry, entry); + } + + [Fact] + public void ObjectToDictionary_WithNullObject_ReturnsEmptyDictionary() + { + // Arrange + object value = null; + + // Act + var dictValues = PropertyHelper.ObjectToDictionary(value); + + // Assert + Assert.NotNull(dictValues); + Assert.Equal(0, dictValues.Count); + } + + [Fact] + public void ObjectToDictionary_WithPlainObjectType_ReturnsEmptyDictionary() + { + // Arrange + var value = new object(); + + // Act + var dictValues = PropertyHelper.ObjectToDictionary(value); + + // Assert + Assert.NotNull(dictValues); + Assert.Equal(0, dictValues.Count); + } + + [Fact] + public void ObjectToDictionary_WithPrimitiveType_LooksUpPublicProperties() + { + // Arrange + var value = "test"; + + // Act + var dictValues = PropertyHelper.ObjectToDictionary(value); + + // Assert + Assert.NotNull(dictValues); + Assert.Equal(1, dictValues.Count); + Assert.Equal(4, dictValues["Length"]); + } + + [Fact] + public void ObjectToDictionary_WithAnonymousType_LooksUpProperties() + { + // Arrange + var value = new { test = "value", other = 1 }; + + // Act + var dictValues = PropertyHelper.ObjectToDictionary(value); + + // Assert + Assert.NotNull(dictValues); + Assert.Equal(2, dictValues.Count); + Assert.Equal("value", dictValues["test"]); + Assert.Equal(1, dictValues["other"]); + } + + [Fact] + public void ObjectToDictionary_ReturnsCaseInsensitiveDictionary() + { + // Arrange + var value = new { TEST = "value", oThEr = 1 }; + + // Act + var dictValues = PropertyHelper.ObjectToDictionary(value); + + // Assert + Assert.NotNull(dictValues); + Assert.Equal(2, dictValues.Count); + Assert.Equal("value", dictValues["test"]); + Assert.Equal(1, dictValues["other"]); + } + + [Fact] + public void ObjectToDictionary_ReturnsInheritedProperties() + { + // Arrange + var value = new ThreeDPoint() { X = 5, Y = 10, Z = 17 }; + + // Act + var dictValues = PropertyHelper.ObjectToDictionary(value); + + // Assert + Assert.NotNull(dictValues); + Assert.Equal(3, dictValues.Count); + Assert.Equal(5, dictValues["X"]); + Assert.Equal(10, dictValues["Y"]); + Assert.Equal(17, dictValues["Z"]); + } + + private class Point + { + public int X { get; set; } + public int Y { get; set; } + } + + private class ThreeDPoint : Point + { + public int Z { get; set; } + } + + private class Static + { + public static int Prop2 { get; set; } + public int Prop5 { get; set; } + } + + private struct MyProperties + { + public int IntProp { get; set; } + public string StringProp { get; set; } + } + + private class SetOnly + { + public int Prop2 { set { } } + public int Prop6 { get; set; } + } + + private class PrivateProperties + { + public int Prop1 { get; set; } + protected int Prop2 { get; set; } + private int Prop3 { get; set; } + } + + public class BaseClass + { + public string PropA { get; set; } + + protected string PropProtected { get; set; } + + public string GetPropProtected() + { + return PropProtected; + } + } + + public class DerivedClass : BaseClass + { + public string PropB { get; set; } + } + + public class BaseClassWithVirtual + { + public virtual string PropA { get; set; } + public string PropB { get; set; } + } + + public class DerivedClassWithNew : BaseClassWithVirtual + { + private string _value = "Newed"; + + public new string PropB + { + get { return _value; } + set { _value = "Newed" + value; } + } + } + + public class DerivedClassWithOverride : BaseClassWithVirtual + { + private string _value = "Overriden"; + + public override string PropA + { + get { return _value; } + set { _value = "Overriden" + value; } + } + } + + private class DerivedClassWithNonReadableProperties : BaseClassWithVirtual + { + public string this[int index] + { + get { return string.Empty; } + set { } + } + + public int Visible { get; set; } + + private string NotVisible { get; set; } + + public string NotVisible2 { private get; set; } + + public string NotVisible3 + { + set { } + } + + public static string NotVisible4 { get; set; } + } + + private struct MyStruct + { + public string Foo { get; set; } + } + + private class BaseHiddenProperty + { + public int Id { get; set; } + } + + private class DerivedHiddenProperty : BaseHiddenProperty + { + public new string Id { get; set; } + + public string Name { get; set; } + } + + private class DerivedHiddenProperty2 : DerivedHiddenProperty + { + public new Guid Id { get; set; } + + public new string Name { get; private set; } + } + } +} From f6acead733891030fd30087807f9a3b8acbf454c Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Thu, 13 Dec 2018 11:18:50 -0800 Subject: [PATCH 07/10] Replace Microsoft.Extensions.RazorViews.Sources with a local copy of code --- build/dependencies.props | 1 - build/external-dependencies.props | 1 - eng/Dependencies.props | 1 - src/AuthSamples/build/dependencies.props | 1 - .../src/Microsoft.AspNetCore.Hosting.csproj | 5 +- src/Identity/build/dependencies.props | 1 - ...ore.Diagnostics.EntityFrameworkCore.csproj | 2 +- .../Microsoft.AspNetCore.Diagnostics.csproj | 2 +- src/Shared/RazorViews/AttributeValue.cs | 38 +++ src/Shared/RazorViews/BaseView.cs | 279 ++++++++++++++++++ src/Shared/RazorViews/HelperResult.cs | 34 +++ 11 files changed, 357 insertions(+), 8 deletions(-) create mode 100644 src/Shared/RazorViews/AttributeValue.cs create mode 100644 src/Shared/RazorViews/BaseView.cs create mode 100644 src/Shared/RazorViews/HelperResult.cs diff --git a/build/dependencies.props b/build/dependencies.props index 18845e77e8ed..4d18b81fbc89 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -75,7 +75,6 @@ 2.1.1 2.1.1 2.1.6 - 2.1.1 2.1.1 2.1.1 2.1.1 diff --git a/build/external-dependencies.props b/build/external-dependencies.props index ceb82576f807..1b4d187b13bd 100644 --- a/build/external-dependencies.props +++ b/build/external-dependencies.props @@ -64,7 +64,6 @@ - diff --git a/eng/Dependencies.props b/eng/Dependencies.props index 161726cfff04..6c18f94b4eb0 100644 --- a/eng/Dependencies.props +++ b/eng/Dependencies.props @@ -38,7 +38,6 @@ - diff --git a/src/AuthSamples/build/dependencies.props b/src/AuthSamples/build/dependencies.props index 01ccc8687a4c..6eb99d606941 100644 --- a/src/AuthSamples/build/dependencies.props +++ b/src/AuthSamples/build/dependencies.props @@ -57,7 +57,6 @@ 2.1.1 2.1.1 2.1.1 - 2.1.1 2.1.1 3.14.2 5.2.0 diff --git a/src/Hosting/Hosting/src/Microsoft.AspNetCore.Hosting.csproj b/src/Hosting/Hosting/src/Microsoft.AspNetCore.Hosting.csproj index 627b36bbc2f1..43bcd9834cfa 100644 --- a/src/Hosting/Hosting/src/Microsoft.AspNetCore.Hosting.csproj +++ b/src/Hosting/Hosting/src/Microsoft.AspNetCore.Hosting.csproj @@ -8,6 +8,10 @@ aspnetcore;hosting + + + + @@ -20,7 +24,6 @@ - diff --git a/src/Identity/build/dependencies.props b/src/Identity/build/dependencies.props index 21e3cb41e564..54ff881547e3 100644 --- a/src/Identity/build/dependencies.props +++ b/src/Identity/build/dependencies.props @@ -73,7 +73,6 @@ 2.1.1 2.1.1 2.1.1 - 2.1.1 2.1.1 2.1.1 2.1.1 diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/src/Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.csproj b/src/Middleware/Diagnostics.EntityFrameworkCore/src/Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.csproj index f418a501481c..23e9b782f18f 100644 --- a/src/Middleware/Diagnostics.EntityFrameworkCore/src/Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.csproj +++ b/src/Middleware/Diagnostics.EntityFrameworkCore/src/Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.csproj @@ -10,12 +10,12 @@ + - diff --git a/src/Middleware/Diagnostics/src/Microsoft.AspNetCore.Diagnostics.csproj b/src/Middleware/Diagnostics/src/Microsoft.AspNetCore.Diagnostics.csproj index a5ae405dcd9e..bb289144933d 100644 --- a/src/Middleware/Diagnostics/src/Microsoft.AspNetCore.Diagnostics.csproj +++ b/src/Middleware/Diagnostics/src/Microsoft.AspNetCore.Diagnostics.csproj @@ -10,6 +10,7 @@ + @@ -20,7 +21,6 @@ - diff --git a/src/Shared/RazorViews/AttributeValue.cs b/src/Shared/RazorViews/AttributeValue.cs new file mode 100644 index 000000000000..7a066a7040b3 --- /dev/null +++ b/src/Shared/RazorViews/AttributeValue.cs @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.Extensions.RazorViews +{ + internal class AttributeValue + { + public AttributeValue(string prefix, object value, bool literal) + { + Prefix = prefix; + Value = value; + Literal = literal; + } + + public string Prefix { get; } + + public object Value { get; } + + public bool Literal { get; } + + public static AttributeValue FromTuple(Tuple value) + { + return new AttributeValue(value.Item1, value.Item2, value.Item3); + } + + public static AttributeValue FromTuple(Tuple value) + { + return new AttributeValue(value.Item1, value.Item2, value.Item3); + } + + public static implicit operator AttributeValue(Tuple value) + { + return FromTuple(value); + } + } +} \ No newline at end of file diff --git a/src/Shared/RazorViews/BaseView.cs b/src/Shared/RazorViews/BaseView.cs new file mode 100644 index 000000000000..a171d8d1f215 --- /dev/null +++ b/src/Shared/RazorViews/BaseView.cs @@ -0,0 +1,279 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.Extensions.RazorViews +{ + /// + /// Infrastructure + /// + internal abstract class BaseView + { + private static readonly Encoding UTF8NoBOM = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); + private readonly Stack _textWriterStack = new Stack(); + + /// + /// The request context + /// + protected HttpContext Context { get; private set; } + + /// + /// The request + /// + protected HttpRequest Request { get; private set; } + + /// + /// The response + /// + protected HttpResponse Response { get; private set; } + + /// + /// The output stream + /// + protected TextWriter Output { get; private set; } + + /// + /// Html encoder used to encode content. + /// + protected HtmlEncoder HtmlEncoder { get; set; } = HtmlEncoder.Default; + + /// + /// Url encoder used to encode content. + /// + protected UrlEncoder UrlEncoder { get; set; } = UrlEncoder.Default; + + /// + /// JavaScript encoder used to encode content. + /// + protected JavaScriptEncoder JavaScriptEncoder { get; set; } = JavaScriptEncoder.Default; + + /// + /// Execute an individual request + /// + /// + public async Task ExecuteAsync(HttpContext context) + { + Context = context; + Request = Context.Request; + Response = Context.Response; + Output = new StreamWriter(Response.Body, UTF8NoBOM, 4096, leaveOpen: true); + await ExecuteAsync(); + Output.Dispose(); + } + + /// + /// Execute an individual request + /// + public abstract Task ExecuteAsync(); + + protected virtual void PushWriter(TextWriter writer) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + _textWriterStack.Push(Output); + Output = writer; + } + + protected virtual TextWriter PopWriter() + { + Output = _textWriterStack.Pop(); + return Output; + } + + /// + /// Write the given value without HTML encoding directly to . + /// + /// The to write. + protected void WriteLiteral(object value) + { + WriteLiteral(Convert.ToString(value, CultureInfo.InvariantCulture)); + } + + /// + /// Write the given value without HTML encoding directly to . + /// + /// The to write. + protected void WriteLiteral(string value) + { + if (!string.IsNullOrEmpty(value)) + { + Output.Write(value); + } + } + + private List AttributeValues { get; set; } + + protected void WriteAttributeValue(string thingy, int startPostion, object value, int endValue, int dealyo, bool yesno) + { + if (AttributeValues == null) + { + AttributeValues = new List(); + } + + AttributeValues.Add(value.ToString()); + } + + private string AttributeEnding { get; set; } + + protected void BeginWriteAttribute(string name, string begining, int startPosition, string ending, int endPosition, int thingy) + { + Debug.Assert(string.IsNullOrEmpty(AttributeEnding)); + + Output.Write(begining); + AttributeEnding = ending; + } + + protected void EndWriteAttribute() + { + Debug.Assert(!string.IsNullOrEmpty(AttributeEnding)); + + var attributes = string.Join(" ", AttributeValues); + Output.Write(attributes); + AttributeValues = null; + + Output.Write(AttributeEnding); + AttributeEnding = null; + } + + /// + /// Writes the given attribute to the given writer + /// + /// The name of the attribute to write + /// The value of the prefix + /// The value of the suffix + /// The s to write. + protected void WriteAttribute( + string name, + string leader, + string trailer, + params AttributeValue[] values) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (leader == null) + { + throw new ArgumentNullException(nameof(leader)); + } + + if (trailer == null) + { + throw new ArgumentNullException(nameof(trailer)); + } + + WriteLiteral(leader); + foreach (var value in values) + { + WriteLiteral(value.Prefix); + + // The special cases here are that the value we're writing might already be a string, or that the + // value might be a bool. If the value is the bool 'true' we want to write the attribute name + // instead of the string 'true'. If the value is the bool 'false' we don't want to write anything. + // Otherwise the value is another object (perhaps an HtmlString) and we'll ask it to format itself. + string stringValue; + if (value.Value is bool) + { + if ((bool)value.Value) + { + stringValue = name; + } + else + { + continue; + } + } + else + { + stringValue = value.Value as string; + } + + // Call the WriteTo(string) overload when possible + if (value.Literal && stringValue != null) + { + WriteLiteral(stringValue); + } + else if (value.Literal) + { + WriteLiteral(value.Value); + } + else if (stringValue != null) + { + Write(stringValue); + } + else + { + Write(value.Value); + } + } + WriteLiteral(trailer); + } + + /// + /// is invoked + /// + /// The to invoke + protected void Write(HelperResult result) + { + Write(result); + } + + /// + /// Writes the specified to . + /// + /// The to write. + /// + /// is invoked for types. + /// For all other types, the encoded result of is written to + /// . + /// + protected void Write(object value) + { + if (value is HelperResult helperResult) + { + helperResult.WriteTo(Output); + } + else + { + Write(Convert.ToString(value, CultureInfo.InvariantCulture)); + } + } + + /// + /// Writes the specified with HTML encoding to . + /// + /// The to write. + protected void Write(string value) + { + WriteLiteral(HtmlEncoder.Encode(value)); + } + + protected string HtmlEncodeAndReplaceLineBreaks(string input) + { + if (string.IsNullOrEmpty(input)) + { + return string.Empty; + } + + // Split on line breaks before passing it through the encoder. + return string.Join("
" + Environment.NewLine, + input.Split(new[] { "\r\n" }, StringSplitOptions.None) + .SelectMany(s => s.Split(new[] { '\r', '\n' }, StringSplitOptions.None)) + .Select(HtmlEncoder.Encode)); + } + } +} \ No newline at end of file diff --git a/src/Shared/RazorViews/HelperResult.cs b/src/Shared/RazorViews/HelperResult.cs new file mode 100644 index 000000000000..c79944aae646 --- /dev/null +++ b/src/Shared/RazorViews/HelperResult.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; + +namespace Microsoft.Extensions.RazorViews +{ + /// + /// Represents a deferred write operation in a . + /// + internal class HelperResult + { + /// + /// Creates a new instance of . + /// + /// The delegate to invoke when is called. + public HelperResult(Action action) + { + WriteAction = action; + } + + public Action WriteAction { get; } + + /// + /// Method invoked to produce content from the . + /// + /// The instance to write to. + public void WriteTo(TextWriter writer) + { + WriteAction(writer); + } + } +} \ No newline at end of file From 3d52a9d9bf2c0ca9bef5579fdc6f705656f5ca66 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Thu, 13 Dec 2018 11:24:28 -0800 Subject: [PATCH 08/10] Replace Microsoft.Extensions.SecurityHelper.Sources with a local copy of code --- build/dependencies.props | 1 - build/external-dependencies.props | 1 - src/AuthSamples/build/dependencies.props | 1 - src/Identity/Directory.Build.props | 1 + src/Identity/build/dependencies.props | 1 - src/Mvc/build/dependencies.props | 1 - .../Microsoft.AspNetCore.Mvc.Core.csproj | 2 +- src/Security/build/dependencies.props | 3 +- ...Microsoft.AspNetCore.Authentication.csproj | 5 +- ...oft.AspNetCore.Authorization.Policy.csproj | 5 +- src/Shared/SecurityHelper/SecurityHelper.cs | 40 ++++++++ .../Microsoft.AspNetCore.Shared.Tests.csproj | 1 + src/Shared/test/SecurityHelperTests.cs | 93 +++++++++++++++++++ src/SignalR/build/dependencies.props | 1 - ...crosoft.AspNetCore.Http.Connections.csproj | 5 +- 15 files changed, 149 insertions(+), 12 deletions(-) create mode 100644 src/Shared/SecurityHelper/SecurityHelper.cs create mode 100644 src/Shared/test/SecurityHelperTests.cs diff --git a/build/dependencies.props b/build/dependencies.props index 4d18b81fbc89..635195b25304 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -75,7 +75,6 @@ 2.1.1 2.1.1 2.1.6 - 2.1.1 2.1.1 2.1.1 2.1.1 diff --git a/build/external-dependencies.props b/build/external-dependencies.props index 1b4d187b13bd..eac1f74bccde 100644 --- a/build/external-dependencies.props +++ b/build/external-dependencies.props @@ -64,7 +64,6 @@ - diff --git a/src/AuthSamples/build/dependencies.props b/src/AuthSamples/build/dependencies.props index 6eb99d606941..c8622f6dd3d3 100644 --- a/src/AuthSamples/build/dependencies.props +++ b/src/AuthSamples/build/dependencies.props @@ -57,7 +57,6 @@ 2.1.1 2.1.1 2.1.1 - 2.1.1 3.14.2 5.2.0 2.0.0 diff --git a/src/Identity/Directory.Build.props b/src/Identity/Directory.Build.props index 230ecfc69ebc..f7f18cf4165e 100644 --- a/src/Identity/Directory.Build.props +++ b/src/Identity/Directory.Build.props @@ -15,6 +15,7 @@ $(MSBuildThisFileDirectory)build\Key.snk true true + $(MSBuildThisFileDirectory)..\Shared\ diff --git a/src/Identity/build/dependencies.props b/src/Identity/build/dependencies.props index 54ff881547e3..3ae698e9d810 100644 --- a/src/Identity/build/dependencies.props +++ b/src/Identity/build/dependencies.props @@ -73,7 +73,6 @@ 2.1.1 2.1.1 2.1.1 - 2.1.1 2.1.1 2.1.1 diff --git a/src/Mvc/build/dependencies.props b/src/Mvc/build/dependencies.props index 641493262579..3b46192a7852 100644 --- a/src/Mvc/build/dependencies.props +++ b/src/Mvc/build/dependencies.props @@ -89,7 +89,6 @@ 2.1.1 2.1.1 2.1.1 - 2.1.1 2.1.1 2.1.1 2.1.1 diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Microsoft.AspNetCore.Mvc.Core.csproj b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Microsoft.AspNetCore.Mvc.Core.csproj index 73da4b10f57b..a54324689b1c 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Microsoft.AspNetCore.Mvc.Core.csproj +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Microsoft.AspNetCore.Mvc.Core.csproj @@ -22,6 +22,7 @@ Microsoft.AspNetCore.Mvc.RouteAttribute +
@@ -40,7 +41,6 @@ Microsoft.AspNetCore.Mvc.RouteAttribute - diff --git a/src/Security/build/dependencies.props b/src/Security/build/dependencies.props index 828f9c7ab283..fb898d743ee3 100644 --- a/src/Security/build/dependencies.props +++ b/src/Security/build/dependencies.props @@ -52,7 +52,6 @@ 2.1.1 2.1.1 2.1.1 - 2.1.1 2.1.1 - \ No newline at end of file + diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Microsoft.AspNetCore.Authentication.csproj b/src/Security/src/Microsoft.AspNetCore.Authentication/Microsoft.AspNetCore.Authentication.csproj index 7e3ce4eb39eb..ce5b4c6f9384 100644 --- a/src/Security/src/Microsoft.AspNetCore.Authentication/Microsoft.AspNetCore.Authentication.csproj +++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Microsoft.AspNetCore.Authentication.csproj @@ -8,6 +8,10 @@ aspnetcore;authentication;security + + + + @@ -15,7 +19,6 @@ - diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization.Policy/Microsoft.AspNetCore.Authorization.Policy.csproj b/src/Security/src/Microsoft.AspNetCore.Authorization.Policy/Microsoft.AspNetCore.Authorization.Policy.csproj index 16e4aa26228b..9516b0838866 100644 --- a/src/Security/src/Microsoft.AspNetCore.Authorization.Policy/Microsoft.AspNetCore.Authorization.Policy.csproj +++ b/src/Security/src/Microsoft.AspNetCore.Authorization.Policy/Microsoft.AspNetCore.Authorization.Policy.csproj @@ -8,13 +8,16 @@ aspnetcore;authorization + + + + - diff --git a/src/Shared/SecurityHelper/SecurityHelper.cs b/src/Shared/SecurityHelper/SecurityHelper.cs new file mode 100644 index 000000000000..408ef6b22439 --- /dev/null +++ b/src/Shared/SecurityHelper/SecurityHelper.cs @@ -0,0 +1,40 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Security.Claims; + +namespace Microsoft.Extensions.Internal +{ + /// + /// Helper code used when implementing authentication middleware + /// + internal static class SecurityHelper + { + /// + /// Add all ClaimsIdentities from an additional ClaimPrincipal to the ClaimsPrincipal + /// Merges a new claims principal, placing all new identities first, and eliminating + /// any empty unauthenticated identities from context.User + /// + /// The containing existing . + /// The containing to be added. + public static ClaimsPrincipal MergeUserPrincipal(ClaimsPrincipal existingPrincipal, ClaimsPrincipal additionalPrincipal) + { + var newPrincipal = new ClaimsPrincipal(); + + // New principal identities go first + if (additionalPrincipal != null) + { + newPrincipal.AddIdentities(additionalPrincipal.Identities); + } + + // Then add any existing non empty or authenticated identities + if (existingPrincipal != null) + { + newPrincipal.AddIdentities(existingPrincipal.Identities.Where(i => i.IsAuthenticated || i.Claims.Any())); + } + return newPrincipal; + } + } +} diff --git a/src/Shared/test/Microsoft.AspNetCore.Shared.Tests.csproj b/src/Shared/test/Microsoft.AspNetCore.Shared.Tests.csproj index 72799639c133..3baf83868e2d 100644 --- a/src/Shared/test/Microsoft.AspNetCore.Shared.Tests.csproj +++ b/src/Shared/test/Microsoft.AspNetCore.Shared.Tests.csproj @@ -10,6 +10,7 @@ + diff --git a/src/Shared/test/SecurityHelperTests.cs b/src/Shared/test/SecurityHelperTests.cs new file mode 100644 index 000000000000..8e7515ad36e7 --- /dev/null +++ b/src/Shared/test/SecurityHelperTests.cs @@ -0,0 +1,93 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using System.Security.Claims; +using System.Security.Principal; +using Xunit; + +namespace Microsoft.Extensions.Internal +{ + public class SecurityHelperTests + { + [Fact] + public void AddingToAnonymousIdentityDoesNotKeepAnonymousIdentity() + { + var user = SecurityHelper.MergeUserPrincipal(new ClaimsPrincipal(), new GenericPrincipal(new GenericIdentity("Test1", "Alpha"), new string[0])); + + Assert.NotNull(user); + Assert.Equal("Alpha", user.Identity.AuthenticationType); + Assert.Equal("Test1", user.Identity.Name); + Assert.IsAssignableFrom(user); + Assert.IsAssignableFrom(user.Identity); + Assert.Single(user.Identities); + } + + [Fact] + public void AddingExistingIdentityChangesDefaultButPreservesPrior() + { + ClaimsPrincipal user = new GenericPrincipal(new GenericIdentity("Test1", "Alpha"), null); + + Assert.Equal("Alpha", user.Identity.AuthenticationType); + Assert.Equal("Test1", user.Identity.Name); + + user = SecurityHelper.MergeUserPrincipal(user, new GenericPrincipal(new GenericIdentity("Test2", "Beta"), new string[0])); + + Assert.Equal("Beta", user.Identity.AuthenticationType); + Assert.Equal("Test2", user.Identity.Name); + + user = SecurityHelper.MergeUserPrincipal(user, new GenericPrincipal(new GenericIdentity("Test3", "Gamma"), new string[0])); + + Assert.Equal("Gamma", user.Identity.AuthenticationType); + Assert.Equal("Test3", user.Identity.Name); + + Assert.Equal(3, user.Identities.Count()); + Assert.Equal("Test3", user.Identities.Skip(0).First().Name); + Assert.Equal("Test2", user.Identities.Skip(1).First().Name); + Assert.Equal("Test1", user.Identities.Skip(2).First().Name); + } + + [Fact] + public void AddingPreservesNewIdentitiesAndDropsEmpty() + { + var existingPrincipal = new ClaimsPrincipal(new ClaimsIdentity()); + var identityNoAuthTypeWithClaim = new ClaimsIdentity(); + identityNoAuthTypeWithClaim.AddClaim(new Claim("identityNoAuthTypeWithClaim", "yes")); + existingPrincipal.AddIdentity(identityNoAuthTypeWithClaim); + var identityEmptyWithAuthType = new ClaimsIdentity("empty"); + existingPrincipal.AddIdentity(identityEmptyWithAuthType); + + Assert.False(existingPrincipal.Identity.IsAuthenticated); + + var newPrincipal = new ClaimsPrincipal(); + var newEmptyIdentity = new ClaimsIdentity(); + var identityTwo = new ClaimsIdentity("yep"); + newPrincipal.AddIdentity(newEmptyIdentity); + newPrincipal.AddIdentity(identityTwo); + + var user = SecurityHelper.MergeUserPrincipal(existingPrincipal, newPrincipal); + + // Preserve newPrincipal order + Assert.False(user.Identity.IsAuthenticated); + Assert.Null(user.Identity.Name); + + Assert.Equal(4, user.Identities.Count()); + Assert.Equal(newEmptyIdentity, user.Identities.Skip(0).First()); + Assert.Equal(identityTwo, user.Identities.Skip(1).First()); + Assert.Equal(identityNoAuthTypeWithClaim, user.Identities.Skip(2).First()); + Assert.Equal(identityEmptyWithAuthType, user.Identities.Skip(3).First()); + + // This merge should drop newEmptyIdentity since its empty + user = SecurityHelper.MergeUserPrincipal(user, new GenericPrincipal(new GenericIdentity("Test3", "Gamma"), new string[0])); + + Assert.Equal("Gamma", user.Identity.AuthenticationType); + Assert.Equal("Test3", user.Identity.Name); + + Assert.Equal(4, user.Identities.Count()); + Assert.Equal("Test3", user.Identities.Skip(0).First().Name); + Assert.Equal(identityTwo, user.Identities.Skip(1).First()); + Assert.Equal(identityNoAuthTypeWithClaim, user.Identities.Skip(2).First()); + Assert.Equal(identityEmptyWithAuthType, user.Identities.Skip(3).First()); + } + } +} diff --git a/src/SignalR/build/dependencies.props b/src/SignalR/build/dependencies.props index de016788110e..42d0a0524cc2 100644 --- a/src/SignalR/build/dependencies.props +++ b/src/SignalR/build/dependencies.props @@ -78,7 +78,6 @@ 2.1.1 2.1.1 2.1.1 - 2.1.1 2.1.1 2.1.1 diff --git a/src/SignalR/src/Microsoft.AspNetCore.Http.Connections/Microsoft.AspNetCore.Http.Connections.csproj b/src/SignalR/src/Microsoft.AspNetCore.Http.Connections/Microsoft.AspNetCore.Http.Connections.csproj index 0b0a149c3a88..66c8cd59b610 100644 --- a/src/SignalR/src/Microsoft.AspNetCore.Http.Connections/Microsoft.AspNetCore.Http.Connections.csproj +++ b/src/SignalR/src/Microsoft.AspNetCore.Http.Connections/Microsoft.AspNetCore.Http.Connections.csproj @@ -14,6 +14,10 @@ + + + + @@ -24,7 +28,6 @@ - From a868e418893ff649caae3d25c7ba38be252a3260 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Thu, 13 Dec 2018 11:38:27 -0800 Subject: [PATCH 09/10] Replace Microsoft.Extensions.StackTrace.Sources with a local copy of code --- build/dependencies.props | 1 - build/external-dependencies.props | 1 - build/repo.props | 1 + eng/Dependencies.props | 2 +- .../src/Microsoft.AspNetCore.Hosting.csproj | 2 +- .../Microsoft.AspNetCore.Diagnostics.csproj | 2 +- .../ExceptionDetails/ExceptionDetails.cs | 29 ++ .../ExceptionDetailsProvider.cs | 170 +++++++++ .../StackFrame/MethodDisplayInfo.cs | 49 +++ .../StackFrame/ParameterDisplayInfo.cs | 33 ++ .../StackFrame/PortablePdbReader.cs | 135 +++++++ .../StackTrace/StackFrame/StackFrameInfo.cs | 18 + .../StackFrame/StackFrameSourceCodeInfo.cs | 54 +++ .../StackTrace/StackFrame/StackTraceHelper.cs | 261 +++++++++++++ .../ClosedGenericMatcherTest.cs | 0 .../CopyOnWriteDictionaryHolderTest.cs | 0 .../CopyOnWriteDictionaryTest.cs | 0 .../Microsoft.AspNetCore.Shared.Tests.csproj | 8 + .../ObjectMethodExecutorTest.cs | 0 .../PropertyActivatorTest.cs | 0 .../{ => Shared.Tests}/PropertyHelperTest.cs | 0 .../{ => Shared.Tests}/SecurityHelperTests.cs | 0 .../test/Shared.Tests/StackTraceHelperTest.cs | 345 ++++++++++++++++++ .../testassets/ThrowingLibrary/Thrower.cs | 20 + .../ThrowingLibrary/ThrowingLibrary.csproj | 8 + 25 files changed, 1134 insertions(+), 5 deletions(-) create mode 100644 src/Shared/StackTrace/ExceptionDetails/ExceptionDetails.cs create mode 100644 src/Shared/StackTrace/ExceptionDetails/ExceptionDetailsProvider.cs create mode 100644 src/Shared/StackTrace/StackFrame/MethodDisplayInfo.cs create mode 100644 src/Shared/StackTrace/StackFrame/ParameterDisplayInfo.cs create mode 100644 src/Shared/StackTrace/StackFrame/PortablePdbReader.cs create mode 100644 src/Shared/StackTrace/StackFrame/StackFrameInfo.cs create mode 100644 src/Shared/StackTrace/StackFrame/StackFrameSourceCodeInfo.cs create mode 100644 src/Shared/StackTrace/StackFrame/StackTraceHelper.cs rename src/Shared/test/{ => Shared.Tests}/ClosedGenericMatcherTest.cs (100%) rename src/Shared/test/{ => Shared.Tests}/CopyOnWriteDictionaryHolderTest.cs (100%) rename src/Shared/test/{ => Shared.Tests}/CopyOnWriteDictionaryTest.cs (100%) rename src/Shared/test/{ => Shared.Tests}/Microsoft.AspNetCore.Shared.Tests.csproj (63%) rename src/Shared/test/{ => Shared.Tests}/ObjectMethodExecutorTest.cs (100%) rename src/Shared/test/{ => Shared.Tests}/PropertyActivatorTest.cs (100%) rename src/Shared/test/{ => Shared.Tests}/PropertyHelperTest.cs (100%) rename src/Shared/test/{ => Shared.Tests}/SecurityHelperTests.cs (100%) create mode 100644 src/Shared/test/Shared.Tests/StackTraceHelperTest.cs create mode 100644 src/Shared/test/testassets/ThrowingLibrary/Thrower.cs create mode 100644 src/Shared/test/testassets/ThrowingLibrary/ThrowingLibrary.csproj diff --git a/build/dependencies.props b/build/dependencies.props index 635195b25304..dc93d7886843 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -75,7 +75,6 @@ 2.1.1 2.1.1 2.1.6 - 2.1.1 2.1.1 2.1.1 2.1.1 diff --git a/build/external-dependencies.props b/build/external-dependencies.props index eac1f74bccde..3377389180f3 100644 --- a/build/external-dependencies.props +++ b/build/external-dependencies.props @@ -64,7 +64,6 @@ - diff --git a/build/repo.props b/build/repo.props index 4ee59e96d790..f9200f270f9a 100644 --- a/build/repo.props +++ b/build/repo.props @@ -74,6 +74,7 @@ $(RepositoryRoot)src\Html\**\*.*proj; $(RepositoryRoot)src\Servers\**\*.csproj; $(RepositoryRoot)src\Servers\**\*.pkgproj; + $(RepositoryRoot)src\Shared\**\*.*proj; $(RepositoryRoot)src\Tools\**\*.*proj; $(RepositoryRoot)src\Middleware\**\*.*proj; " diff --git a/eng/Dependencies.props b/eng/Dependencies.props index 6c18f94b4eb0..9a9d42775f0b 100644 --- a/eng/Dependencies.props +++ b/eng/Dependencies.props @@ -38,7 +38,6 @@ - @@ -50,6 +49,7 @@ + diff --git a/src/Hosting/Hosting/src/Microsoft.AspNetCore.Hosting.csproj b/src/Hosting/Hosting/src/Microsoft.AspNetCore.Hosting.csproj index 43bcd9834cfa..fbefc75eef63 100644 --- a/src/Hosting/Hosting/src/Microsoft.AspNetCore.Hosting.csproj +++ b/src/Hosting/Hosting/src/Microsoft.AspNetCore.Hosting.csproj @@ -10,6 +10,7 @@ + @@ -24,7 +25,6 @@ - diff --git a/src/Middleware/Diagnostics/src/Microsoft.AspNetCore.Diagnostics.csproj b/src/Middleware/Diagnostics/src/Microsoft.AspNetCore.Diagnostics.csproj index bb289144933d..9bdc1cea7779 100644 --- a/src/Middleware/Diagnostics/src/Microsoft.AspNetCore.Diagnostics.csproj +++ b/src/Middleware/Diagnostics/src/Microsoft.AspNetCore.Diagnostics.csproj @@ -11,6 +11,7 @@ + @@ -21,7 +22,6 @@ - diff --git a/src/Shared/StackTrace/ExceptionDetails/ExceptionDetails.cs b/src/Shared/StackTrace/ExceptionDetails/ExceptionDetails.cs new file mode 100644 index 000000000000..8862611136e7 --- /dev/null +++ b/src/Shared/StackTrace/ExceptionDetails/ExceptionDetails.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.Extensions.StackTrace.Sources +{ + /// + /// Contains details for individual exception messages. + /// + internal class ExceptionDetails + { + /// + /// An individual exception + /// + public Exception Error { get; set; } + + /// + /// The generated stack frames + /// + public IEnumerable StackFrames { get; set; } + + /// + /// Gets or sets the summary message. + /// + public string ErrorMessage { get; set; } + } +} diff --git a/src/Shared/StackTrace/ExceptionDetails/ExceptionDetailsProvider.cs b/src/Shared/StackTrace/ExceptionDetails/ExceptionDetailsProvider.cs new file mode 100644 index 000000000000..2d1dd20710ba --- /dev/null +++ b/src/Shared/StackTrace/ExceptionDetails/ExceptionDetailsProvider.cs @@ -0,0 +1,170 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using Microsoft.Extensions.FileProviders; + +namespace Microsoft.Extensions.StackTrace.Sources +{ + internal class ExceptionDetailsProvider + { + private readonly IFileProvider _fileProvider; + private readonly int _sourceCodeLineCount; + + public ExceptionDetailsProvider(IFileProvider fileProvider, int sourceCodeLineCount) + { + _fileProvider = fileProvider; + _sourceCodeLineCount = sourceCodeLineCount; + } + + public IEnumerable GetDetails(Exception exception) + { + var exceptions = FlattenAndReverseExceptionTree(exception); + + foreach (var ex in exceptions) + { + yield return new ExceptionDetails + { + Error = ex, + StackFrames = StackTraceHelper.GetFrames(ex) + .Select(frame => GetStackFrameSourceCodeInfo( + frame.MethodDisplayInfo.ToString(), + frame.FilePath, + frame.LineNumber)) + }; + } + } + + private static IEnumerable FlattenAndReverseExceptionTree(Exception ex) + { + // ReflectionTypeLoadException is special because the details are in + // the LoaderExceptions property + var typeLoadException = ex as ReflectionTypeLoadException; + if (typeLoadException != null) + { + var typeLoadExceptions = new List(); + foreach (var loadException in typeLoadException.LoaderExceptions) + { + typeLoadExceptions.AddRange(FlattenAndReverseExceptionTree(loadException)); + } + + typeLoadExceptions.Add(ex); + return typeLoadExceptions; + } + + var list = new List(); + if (ex is AggregateException aggregateException) + { + list.Add(ex); + foreach (var innerException in aggregateException.Flatten().InnerExceptions) + { + list.Add(innerException); + } + } + + else + { + while (ex != null) + { + list.Add(ex); + ex = ex.InnerException; + } + list.Reverse(); + } + + return list; + } + + // make it internal to enable unit testing + internal StackFrameSourceCodeInfo GetStackFrameSourceCodeInfo(string method, string filePath, int lineNumber) + { + var stackFrame = new StackFrameSourceCodeInfo + { + Function = method, + File = filePath, + Line = lineNumber + }; + + if (string.IsNullOrEmpty(stackFrame.File)) + { + return stackFrame; + } + + IEnumerable lines = null; + if (File.Exists(stackFrame.File)) + { + lines = File.ReadLines(stackFrame.File); + } + else + { + // Handle relative paths and embedded files + var fileInfo = _fileProvider.GetFileInfo(stackFrame.File); + if (fileInfo.Exists) + { + // ReadLines doesn't accept a stream. Use ReadLines as its more efficient + // relative to reading lines via stream reader + if (!string.IsNullOrEmpty(fileInfo.PhysicalPath)) + { + lines = File.ReadLines(fileInfo.PhysicalPath); + } + else + { + lines = ReadLines(fileInfo); + } + } + } + + if (lines != null) + { + ReadFrameContent(stackFrame, lines, stackFrame.Line, stackFrame.Line); + } + + return stackFrame; + } + + // make it internal to enable unit testing + internal void ReadFrameContent( + StackFrameSourceCodeInfo frame, + IEnumerable allLines, + int errorStartLineNumberInFile, + int errorEndLineNumberInFile) + { + // Get the line boundaries in the file to be read and read all these lines at once into an array. + var preErrorLineNumberInFile = Math.Max(errorStartLineNumberInFile - _sourceCodeLineCount, 1); + var postErrorLineNumberInFile = errorEndLineNumberInFile + _sourceCodeLineCount; + var codeBlock = allLines + .Skip(preErrorLineNumberInFile - 1) + .Take(postErrorLineNumberInFile - preErrorLineNumberInFile + 1) + .ToArray(); + + var numOfErrorLines = (errorEndLineNumberInFile - errorStartLineNumberInFile) + 1; + var errorStartLineNumberInArray = errorStartLineNumberInFile - preErrorLineNumberInFile; + + frame.PreContextLine = preErrorLineNumberInFile; + frame.PreContextCode = codeBlock.Take(errorStartLineNumberInArray).ToArray(); + frame.ContextCode = codeBlock + .Skip(errorStartLineNumberInArray) + .Take(numOfErrorLines) + .ToArray(); + frame.PostContextCode = codeBlock + .Skip(errorStartLineNumberInArray + numOfErrorLines) + .ToArray(); + } + + private static IEnumerable ReadLines(IFileInfo fileInfo) + { + using (var reader = new StreamReader(fileInfo.CreateReadStream())) + { + string line; + while ((line = reader.ReadLine()) != null) + { + yield return line; + } + } + } + } +} diff --git a/src/Shared/StackTrace/StackFrame/MethodDisplayInfo.cs b/src/Shared/StackTrace/StackFrame/MethodDisplayInfo.cs new file mode 100644 index 000000000000..b1c0ccc18871 --- /dev/null +++ b/src/Shared/StackTrace/StackFrame/MethodDisplayInfo.cs @@ -0,0 +1,49 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.Extensions.StackTrace.Sources +{ + internal class MethodDisplayInfo + { + public string DeclaringTypeName { get; set; } + + public string Name { get; set; } + + public string GenericArguments { get; set; } + + public string SubMethod { get; set; } + + public IEnumerable Parameters { get; set; } + + public override string ToString() + { + var builder = new StringBuilder(); + if (!string.IsNullOrEmpty(DeclaringTypeName)) + { + builder + .Append(DeclaringTypeName) + .Append("."); + } + + builder.Append(Name); + builder.Append(GenericArguments); + + builder.Append("("); + builder.Append(string.Join(", ", Parameters.Select(p => p.ToString()))); + builder.Append(")"); + + if (!string.IsNullOrEmpty(SubMethod)) + { + builder.Append("+"); + builder.Append(SubMethod); + builder.Append("()"); + } + + return builder.ToString(); + } + } +} diff --git a/src/Shared/StackTrace/StackFrame/ParameterDisplayInfo.cs b/src/Shared/StackTrace/StackFrame/ParameterDisplayInfo.cs new file mode 100644 index 000000000000..1199a8386d2f --- /dev/null +++ b/src/Shared/StackTrace/StackFrame/ParameterDisplayInfo.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Text; + +namespace Microsoft.Extensions.StackTrace.Sources +{ + internal class ParameterDisplayInfo + { + public string Name { get; set; } + + public string Type { get; set; } + + public string Prefix { get; set; } + + public override string ToString() + { + var builder = new StringBuilder(); + if (!string.IsNullOrEmpty(Prefix)) + { + builder + .Append(Prefix) + .Append(" "); + } + + builder.Append(Type); + builder.Append(" "); + builder.Append(Name); + + return builder.ToString(); + } + } +} diff --git a/src/Shared/StackTrace/StackFrame/PortablePdbReader.cs b/src/Shared/StackTrace/StackFrame/PortablePdbReader.cs new file mode 100644 index 000000000000..ff6a4947f80e --- /dev/null +++ b/src/Shared/StackTrace/StackFrame/PortablePdbReader.cs @@ -0,0 +1,135 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Reflection.PortableExecutable; + +namespace Microsoft.Extensions.StackTrace.Sources +{ + internal class PortablePdbReader : IDisposable + { + private readonly Dictionary _cache = + new Dictionary(StringComparer.Ordinal); + + public void PopulateStackFrame(StackFrameInfo frameInfo, MethodBase method, int IlOffset) + { + if (method.Module.Assembly.IsDynamic) + { + return; + } + + var metadataReader = GetMetadataReader(method.Module.Assembly.Location); + + if (metadataReader == null) + { + return; + } + + var methodToken = MetadataTokens.Handle(method.MetadataToken); + + Debug.Assert(methodToken.Kind == HandleKind.MethodDefinition); + + var handle = ((MethodDefinitionHandle)methodToken).ToDebugInformationHandle(); + + if (!handle.IsNil) + { + var methodDebugInfo = metadataReader.GetMethodDebugInformation(handle); + var sequencePoints = methodDebugInfo.GetSequencePoints(); + SequencePoint? bestPointSoFar = null; + + foreach (var point in sequencePoints) + { + if (point.Offset > IlOffset) + { + break; + } + + if (point.StartLine != SequencePoint.HiddenLine) + { + bestPointSoFar = point; + } + } + + if (bestPointSoFar.HasValue) + { + frameInfo.LineNumber = bestPointSoFar.Value.StartLine; + frameInfo.FilePath = metadataReader.GetString(metadataReader.GetDocument(bestPointSoFar.Value.Document).Name); + } + } + } + + private MetadataReader GetMetadataReader(string assemblyPath) + { + MetadataReaderProvider provider = null; + if (!_cache.TryGetValue(assemblyPath, out provider)) + { + var pdbPath = GetPdbPath(assemblyPath); + + if (!string.IsNullOrEmpty(pdbPath) && File.Exists(pdbPath) && IsPortable(pdbPath)) + { + var pdbStream = File.OpenRead(pdbPath); + provider = MetadataReaderProvider.FromPortablePdbStream(pdbStream); + } + + _cache[assemblyPath] = provider; + } + + return provider?.GetMetadataReader(); + } + + private static string GetPdbPath(string assemblyPath) + { + if (string.IsNullOrEmpty(assemblyPath)) + { + return null; + } + + if (File.Exists(assemblyPath)) + { + var peStream = File.OpenRead(assemblyPath); + + using (var peReader = new PEReader(peStream)) + { + foreach (var entry in peReader.ReadDebugDirectory()) + { + if (entry.Type == DebugDirectoryEntryType.CodeView) + { + var codeViewData = peReader.ReadCodeViewDebugDirectoryData(entry); + var peDirectory = Path.GetDirectoryName(assemblyPath); + return Path.Combine(peDirectory, Path.GetFileName(codeViewData.Path)); + } + } + } + } + + return null; + } + + private static bool IsPortable(string pdbPath) + { + using (var pdbStream = File.OpenRead(pdbPath)) + { + return pdbStream.ReadByte() == 'B' && + pdbStream.ReadByte() == 'S' && + pdbStream.ReadByte() == 'J' && + pdbStream.ReadByte() == 'B'; + } + } + + public void Dispose() + { + foreach (var entry in _cache) + { + entry.Value?.Dispose(); + } + + _cache.Clear(); + } + } +} diff --git a/src/Shared/StackTrace/StackFrame/StackFrameInfo.cs b/src/Shared/StackTrace/StackFrame/StackFrameInfo.cs new file mode 100644 index 000000000000..ffd91f213c08 --- /dev/null +++ b/src/Shared/StackTrace/StackFrame/StackFrameInfo.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Diagnostics; + +namespace Microsoft.Extensions.StackTrace.Sources +{ + internal class StackFrameInfo + { + public int LineNumber { get; set; } + + public string FilePath { get; set; } + + public StackFrame StackFrame { get; set; } + + public MethodDisplayInfo MethodDisplayInfo { get; set; } + } +} diff --git a/src/Shared/StackTrace/StackFrame/StackFrameSourceCodeInfo.cs b/src/Shared/StackTrace/StackFrame/StackFrameSourceCodeInfo.cs new file mode 100644 index 000000000000..2932e083b132 --- /dev/null +++ b/src/Shared/StackTrace/StackFrame/StackFrameSourceCodeInfo.cs @@ -0,0 +1,54 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.Extensions.StackTrace.Sources +{ + /// + /// Contains the source code where the exception occurred. + /// + internal class StackFrameSourceCodeInfo + { + /// + /// Function containing instruction + /// + public string Function { get; set; } + + /// + /// File containing the instruction + /// + public string File { get; set; } + + /// + /// The line number of the instruction + /// + public int Line { get; set; } + + /// + /// The line preceding the frame line + /// + public int PreContextLine { get; set; } + + /// + /// Lines of code before the actual error line(s). + /// + public IEnumerable PreContextCode { get; set; } = Enumerable.Empty(); + + /// + /// Line(s) of code responsible for the error. + /// + public IEnumerable ContextCode { get; set; } = Enumerable.Empty(); + + /// + /// Lines of code after the actual error line(s). + /// + public IEnumerable PostContextCode { get; set; } = Enumerable.Empty(); + + /// + /// Specific error details for this stack frame. + /// + public string ErrorDetails { get; set; } + } +} diff --git a/src/Shared/StackTrace/StackFrame/StackTraceHelper.cs b/src/Shared/StackTrace/StackFrame/StackTraceHelper.cs new file mode 100644 index 000000000000..5ce9a4090387 --- /dev/null +++ b/src/Shared/StackTrace/StackFrame/StackTraceHelper.cs @@ -0,0 +1,261 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.ExceptionServices; +using Microsoft.Extensions.Internal; + +namespace Microsoft.Extensions.StackTrace.Sources +{ + internal class StackTraceHelper + { + public static IList GetFrames(Exception exception) + { + var frames = new List(); + + if (exception == null) + { + return frames; + } + + using (var portablePdbReader = new PortablePdbReader()) + { + var needFileInfo = true; + var stackTrace = new System.Diagnostics.StackTrace(exception, needFileInfo); + var stackFrames = stackTrace.GetFrames(); + + if (stackFrames == null) + { + return frames; + } + + for (var i = 0; i < stackFrames.Length; i++) + { + var frame = stackFrames[i]; + var method = frame.GetMethod(); + + // Always show last stackFrame + if (!ShowInStackTrace(method) && i < stackFrames.Length - 1) + { + continue; + } + + var stackFrame = new StackFrameInfo + { + StackFrame = frame, + FilePath = frame.GetFileName(), + LineNumber = frame.GetFileLineNumber(), + MethodDisplayInfo = GetMethodDisplayString(frame.GetMethod()), + }; + + if (string.IsNullOrEmpty(stackFrame.FilePath)) + { + // .NET Framework and older versions of mono don't support portable PDBs + // so we read it manually to get file name and line information + portablePdbReader.PopulateStackFrame(stackFrame, method, frame.GetILOffset()); + } + + frames.Add(stackFrame); + } + + return frames; + } + } + + internal static MethodDisplayInfo GetMethodDisplayString(MethodBase method) + { + // Special case: no method available + if (method == null) + { + return null; + } + + var methodDisplayInfo = new MethodDisplayInfo(); + + // Type name + var type = method.DeclaringType; + + var methodName = method.Name; + + if (type != null && type.IsDefined(typeof(CompilerGeneratedAttribute)) && + (typeof(IAsyncStateMachine).IsAssignableFrom(type) || typeof(IEnumerator).IsAssignableFrom(type))) + { + // Convert StateMachine methods to correct overload +MoveNext() + if (TryResolveStateMachineMethod(ref method, out type)) + { + methodDisplayInfo.SubMethod = methodName; + } + } + // ResolveStateMachineMethod may have set declaringType to null + if (type != null) + { + methodDisplayInfo.DeclaringTypeName = TypeNameHelper.GetTypeDisplayName(type, includeGenericParameterNames: true); + } + + // Method name + methodDisplayInfo.Name = method.Name; + if (method.IsGenericMethod) + { + var genericArguments = string.Join(", ", method.GetGenericArguments() + .Select(arg => TypeNameHelper.GetTypeDisplayName(arg, fullName: false, includeGenericParameterNames: true))); + methodDisplayInfo.GenericArguments += "<" + genericArguments + ">"; + } + + // Method parameters + methodDisplayInfo.Parameters = method.GetParameters().Select(parameter => + { + var parameterType = parameter.ParameterType; + + var prefix = string.Empty; + if (parameter.IsOut) + { + prefix = "out"; + } + else if (parameterType != null && parameterType.IsByRef) + { + prefix = "ref"; + } + + var parameterTypeString = "?"; + if (parameterType != null) + { + if (parameterType.IsByRef) + { + parameterType = parameterType.GetElementType(); + } + + parameterTypeString = TypeNameHelper.GetTypeDisplayName(parameterType, fullName: false, includeGenericParameterNames: true); + } + + return new ParameterDisplayInfo + { + Prefix = prefix, + Name = parameter.Name, + Type = parameterTypeString, + }; + }); + + return methodDisplayInfo; + } + + private static bool ShowInStackTrace(MethodBase method) + { + Debug.Assert(method != null); + + // Don't show any methods marked with the StackTraceHiddenAttribute + // https://github.com/dotnet/coreclr/pull/14652 + if (HasStackTraceHiddenAttribute(method)) + { + return false; + } + + + var type = method.DeclaringType; + if (type == null) + { + return true; + } + + if (HasStackTraceHiddenAttribute(type)) + { + return false; + } + + // Fallbacks for runtime pre-StackTraceHiddenAttribute + if (type == typeof(ExceptionDispatchInfo) && method.Name == "Throw") + { + return false; + } + else if (type == typeof(TaskAwaiter) || + type == typeof(TaskAwaiter<>) || + type == typeof(ConfiguredTaskAwaitable.ConfiguredTaskAwaiter) || + type == typeof(ConfiguredTaskAwaitable<>.ConfiguredTaskAwaiter)) + { + switch (method.Name) + { + case "HandleNonSuccessAndDebuggerNotification": + case "ThrowForNonSuccess": + case "ValidateEnd": + case "GetResult": + return false; + } + } + + return true; + } + + private static bool TryResolveStateMachineMethod(ref MethodBase method, out Type declaringType) + { + Debug.Assert(method != null); + Debug.Assert(method.DeclaringType != null); + + declaringType = method.DeclaringType; + + var parentType = declaringType.DeclaringType; + if (parentType == null) + { + return false; + } + + var methods = parentType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly); + if (methods == null) + { + return false; + } + + foreach (var candidateMethod in methods) + { + var attributes = candidateMethod.GetCustomAttributes(); + if (attributes == null) + { + continue; + } + + foreach (var asma in attributes) + { + if (asma.StateMachineType == declaringType) + { + method = candidateMethod; + declaringType = candidateMethod.DeclaringType; + // Mark the iterator as changed; so it gets the + annotation of the original method + // async statemachines resolve directly to their builder methods so aren't marked as changed + return asma is IteratorStateMachineAttribute; + } + } + } + + return false; + } + + private static bool HasStackTraceHiddenAttribute(MemberInfo memberInfo) + { + IList attributes; + try + { + // Accessing MembmerInfo.GetCustomAttributesData throws for some types (such as types in dynamically generated assemblies). + // We'll skip looking up StackTraceHiddenAttributes on such types. + attributes = memberInfo.GetCustomAttributesData(); + } + catch + { + return false; + } + + for (var i = 0; i < attributes.Count; i++) + { + if (attributes[i].AttributeType.Name == "StackTraceHiddenAttribute") + { + return true; + } + } + + return false; + } + } +} diff --git a/src/Shared/test/ClosedGenericMatcherTest.cs b/src/Shared/test/Shared.Tests/ClosedGenericMatcherTest.cs similarity index 100% rename from src/Shared/test/ClosedGenericMatcherTest.cs rename to src/Shared/test/Shared.Tests/ClosedGenericMatcherTest.cs diff --git a/src/Shared/test/CopyOnWriteDictionaryHolderTest.cs b/src/Shared/test/Shared.Tests/CopyOnWriteDictionaryHolderTest.cs similarity index 100% rename from src/Shared/test/CopyOnWriteDictionaryHolderTest.cs rename to src/Shared/test/Shared.Tests/CopyOnWriteDictionaryHolderTest.cs diff --git a/src/Shared/test/CopyOnWriteDictionaryTest.cs b/src/Shared/test/Shared.Tests/CopyOnWriteDictionaryTest.cs similarity index 100% rename from src/Shared/test/CopyOnWriteDictionaryTest.cs rename to src/Shared/test/Shared.Tests/CopyOnWriteDictionaryTest.cs diff --git a/src/Shared/test/Microsoft.AspNetCore.Shared.Tests.csproj b/src/Shared/test/Shared.Tests/Microsoft.AspNetCore.Shared.Tests.csproj similarity index 63% rename from src/Shared/test/Microsoft.AspNetCore.Shared.Tests.csproj rename to src/Shared/test/Shared.Tests/Microsoft.AspNetCore.Shared.Tests.csproj index 3baf83868e2d..42629957b8e8 100644 --- a/src/Shared/test/Microsoft.AspNetCore.Shared.Tests.csproj +++ b/src/Shared/test/Shared.Tests/Microsoft.AspNetCore.Shared.Tests.csproj @@ -2,6 +2,7 @@ netcoreapp2.1;net461 + portable @@ -11,10 +12,17 @@ + + + + + + + diff --git a/src/Shared/test/ObjectMethodExecutorTest.cs b/src/Shared/test/Shared.Tests/ObjectMethodExecutorTest.cs similarity index 100% rename from src/Shared/test/ObjectMethodExecutorTest.cs rename to src/Shared/test/Shared.Tests/ObjectMethodExecutorTest.cs diff --git a/src/Shared/test/PropertyActivatorTest.cs b/src/Shared/test/Shared.Tests/PropertyActivatorTest.cs similarity index 100% rename from src/Shared/test/PropertyActivatorTest.cs rename to src/Shared/test/Shared.Tests/PropertyActivatorTest.cs diff --git a/src/Shared/test/PropertyHelperTest.cs b/src/Shared/test/Shared.Tests/PropertyHelperTest.cs similarity index 100% rename from src/Shared/test/PropertyHelperTest.cs rename to src/Shared/test/Shared.Tests/PropertyHelperTest.cs diff --git a/src/Shared/test/SecurityHelperTests.cs b/src/Shared/test/Shared.Tests/SecurityHelperTests.cs similarity index 100% rename from src/Shared/test/SecurityHelperTests.cs rename to src/Shared/test/Shared.Tests/SecurityHelperTests.cs diff --git a/src/Shared/test/Shared.Tests/StackTraceHelperTest.cs b/src/Shared/test/Shared.Tests/StackTraceHelperTest.cs new file mode 100644 index 000000000000..657a310b6e24 --- /dev/null +++ b/src/Shared/test/Shared.Tests/StackTraceHelperTest.cs @@ -0,0 +1,345 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Microsoft.Extensions.StackTrace.Sources; +using ThrowingLibrary; +using Xunit; + +namespace Microsoft.Extensions.Internal +{ + public class StackTraceHelperTest + { + [Fact] + public void StackTraceHelper_IncludesLineNumbersForFiles() + { + // Arrange + Exception exception = null; + try + { + // Throwing an exception in the current assembly always seems to populate the full stack + // trace regardless of symbol type. Crossing assembly boundaries ensures PortablePdbReader gets used + // on desktop. + Thrower.Throw(); + } + catch (Exception ex) + { + exception = ex; + } + + // Act + var stackFrames = StackTraceHelper.GetFrames(exception); + + // Assert + Assert.Collection(stackFrames, + frame => + { + Assert.Contains("Thrower.cs", frame.FilePath); + Assert.Equal(17, frame.LineNumber); + }, + frame => + { + Assert.Contains("StackTraceHelperTest.cs", frame.FilePath); + }); + } + + [Fact] + public void StackTraceHelper_PrettyPrintsStackTraceForGenericMethods() + { + // Arrange + var exception = Record.Exception(() => GenericMethod(null)); + + // Act + var stackFrames = StackTraceHelper.GetFrames(exception); + + // Assert + var methods = stackFrames.Select(frame => frame.MethodDisplayInfo.ToString()).ToArray(); + Assert.Equal("Microsoft.Extensions.Internal.StackTraceHelperTest.GenericMethod(T val)", methods[0]); + } + + [Fact] + public void StackTraceHelper_PrettyPrintsStackTraceForMethodsWithOutParameters() + { + // Arrange + var exception = Record.Exception(() => MethodWithOutParameter(out var value)); + + // Act + var stackFrames = StackTraceHelper.GetFrames(exception); + + // Assert + var methods = stackFrames.Select(frame => frame.MethodDisplayInfo.ToString()).ToArray(); + Assert.Equal("Microsoft.Extensions.Internal.StackTraceHelperTest.MethodWithOutParameter(out int value)", methods[0]); + } + + [Fact] + public void StackTraceHelper_PrettyPrintsStackTraceForMethodsWithGenericOutParameters() + { + // Arrange + var exception = Record.Exception(() => MethodWithGenericOutParameter("Test", out int value)); + + // Act + var stackFrames = StackTraceHelper.GetFrames(exception); + + // Assert + var methods = stackFrames.Select(frame => frame.MethodDisplayInfo.ToString()).ToArray(); + Assert.Equal("Microsoft.Extensions.Internal.StackTraceHelperTest.MethodWithGenericOutParameter(string a, out TVal value)", methods[0]); + } + + [Fact] + public void StackTraceHelper_PrettyPrintsStackTraceForMethodsWithRefParameters() + { + // Arrange + var value = 0; + var exception = Record.Exception(() => MethodWithRefParameter(ref value)); + + // Act + var stackFrames = StackTraceHelper.GetFrames(exception); + + // Assert + var methods = stackFrames.Select(frame => frame.MethodDisplayInfo.ToString()).ToArray(); + Assert.Equal("Microsoft.Extensions.Internal.StackTraceHelperTest.MethodWithRefParameter(ref int value)", methods[0]); + } + + [Fact] + public void StackTraceHelper_PrettyPrintsStackTraceForMethodsWithGenericRefParameters() + { + // Arrange + var value = 0; + var exception = Record.Exception(() => MethodWithGenericRefParameter(ref value)); + + // Act + var stackFrames = StackTraceHelper.GetFrames(exception); + + // Assert + var methods = stackFrames.Select(frame => frame.MethodDisplayInfo.ToString()).ToArray(); + Assert.Equal("Microsoft.Extensions.Internal.StackTraceHelperTest.MethodWithGenericRefParameter(ref TVal value)", methods[0]); + } + + [Fact] + public void StackTraceHelper_PrettyPrintsStackTraceForMethodsWithNullableParameters() + { + // Arrange + var value = 0; + var exception = Record.Exception(() => MethodWithNullableParameter(value)); + + // Act + var stackFrames = StackTraceHelper.GetFrames(exception); + + // Assert + var methods = stackFrames.Select(frame => frame.MethodDisplayInfo.ToString()).ToArray(); + Assert.Equal("Microsoft.Extensions.Internal.StackTraceHelperTest.MethodWithNullableParameter(Nullable value)", methods[0]); + } + + [Fact] + public void StackTraceHelper_PrettyPrintsStackTraceForMethodsOnGenericTypes() + { + // Arrange + var exception = Record.Exception(() => new GenericClass().Throw(0)); + + // Act + var stackFrames = StackTraceHelper.GetFrames(exception); + + // Assert + var methods = stackFrames.Select(frame => frame.MethodDisplayInfo.ToString()).ToArray(); + Assert.Equal("Microsoft.Extensions.Internal.StackTraceHelperTest+GenericClass.Throw(T parameter)", methods[0]); + } + + [Fact] + public void StackTraceHelper_ProducesReadableOutput() + { + // Arrange + var expectedCallStack = new List() + { + "Microsoft.Extensions.Internal.StackTraceHelperTest.Iterator()+MoveNext()", + "string.Join(string separator, IEnumerable values)", + "Microsoft.Extensions.Internal.StackTraceHelperTest+GenericClass.GenericMethod(ref V value)", + "Microsoft.Extensions.Internal.StackTraceHelperTest.MethodAsync(int value)", + "Microsoft.Extensions.Internal.StackTraceHelperTest.MethodAsync(TValue value)", + "Microsoft.Extensions.Internal.StackTraceHelperTest.Method(string value)", + "Microsoft.Extensions.Internal.StackTraceHelperTest.StackTraceHelper_ProducesReadableOutput()", + }; + + Exception exception = null; + try + { + Method("test"); + } + catch (Exception ex) + { + exception = ex; + } + + // Act + var stackFrames = StackTraceHelper.GetFrames(exception); + var methodNames = stackFrames.Select(stackFrame => stackFrame.MethodDisplayInfo.ToString()).ToArray(); + + // Assert + Assert.Equal(expectedCallStack, methodNames); + } + + [Fact] + public void StackTraceHelper_DoesNotIncludeInstanceMethodsOnTypesWithStackTraceHiddenAttribute() + { + // Arrange + var exception = Record.Exception(() => InvokeMethodOnTypeWithStackTraceHiddenAttribute()); + + // Act + var stackFrames = StackTraceHelper.GetFrames(exception); + + // Assert + var methods = stackFrames.Select(frame => frame.MethodDisplayInfo.ToString()).ToArray(); + Assert.Equal("Microsoft.Extensions.Internal.StackTraceHelperTest.ThrowCore()", methods[0]); + Assert.Equal("Microsoft.Extensions.Internal.StackTraceHelperTest.InvokeMethodOnTypeWithStackTraceHiddenAttribute()", methods[1]); + } + + [Fact] + public void StackTraceHelper_DoesNotIncludeStaticMethodsOnTypesWithStackTraceHiddenAttribute() + { + // Arrange + var exception = Record.Exception(() => InvokeStaticMethodOnTypeWithStackTraceHiddenAttribute()); + + // Act + var stackFrames = StackTraceHelper.GetFrames(exception); + + // Assert + var methods = stackFrames.Select(frame => frame.MethodDisplayInfo.ToString()).ToArray(); + Assert.Equal("Microsoft.Extensions.Internal.StackTraceHelperTest.ThrowCore()", methods[0]); + Assert.Equal("Microsoft.Extensions.Internal.StackTraceHelperTest.InvokeStaticMethodOnTypeWithStackTraceHiddenAttribute()", methods[1]); + } + + [Fact] + public void StackTraceHelper_DoesNotIncludeMethodsWithStackTraceHiddenAttribute() + { + // Arrange + var exception = Record.Exception(() => new TypeWithMethodWithStackTraceHiddenAttribute().Throw()); + + // Act + var stackFrames = StackTraceHelper.GetFrames(exception); + + // Assert + var methods = stackFrames.Select(frame => frame.MethodDisplayInfo.ToString()).ToArray(); + Assert.Equal("Microsoft.Extensions.Internal.StackTraceHelperTest.ThrowCore()", methods[0]); + Assert.Equal("Microsoft.Extensions.Internal.StackTraceHelperTest+TypeWithMethodWithStackTraceHiddenAttribute.Throw()", methods[1]); + } + + [Fact] + public void GetFrames_DoesNotFailForDynamicallyGeneratedAssemblies() + { + // Arrange + var action = (Action)Expression.Lambda( + Expression.Throw( + Expression.New(typeof(Exception)))).Compile(); + var exception = Record.Exception(action); + + // Act + var frames = StackTraceHelper.GetFrames(exception).ToArray(); + + // Assert + var frame = frames[0]; + Assert.Null(frame.FilePath); + Assert.Equal($"lambda_method(Closure )", frame.MethodDisplayInfo.ToString()); + } + + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + async Task MethodAsync(int value) + { + await Task.Delay(0); + return GenericClass.GenericMethod(ref value); + } + + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + async Task MethodAsync(TValue value) + { + return await MethodAsync(1); + } + + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + string Method(string value) + { + return MethodAsync(value).GetAwaiter().GetResult(); + } + + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + static IEnumerable Iterator() + { + yield return "Success"; + throw new Exception(); + } + + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + void MethodWithOutParameter(out int value) => throw new Exception(); + + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + void MethodWithGenericOutParameter(string a, out TVal value) => throw new Exception(); + + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + void MethodWithRefParameter(ref int value) => throw new Exception(); + + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + void MethodWithGenericRefParameter(ref TVal value) => throw new Exception(); + + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + void MethodWithNullableParameter(int? value) => throw new Exception(); + + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + void InvokeMethodOnTypeWithStackTraceHiddenAttribute() => new TypeWithStackTraceHiddenAttribute().Throw(); + + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + void InvokeStaticMethodOnTypeWithStackTraceHiddenAttribute() => TypeWithStackTraceHiddenAttribute.ThrowStatic(); + + class GenericClass + { + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + public static string GenericMethod(ref V value) + { + var returnVal = ""; + for (var i = 0; i < 10; i++) + { + returnVal += string.Join(", ", Iterator()); + } + return returnVal; + } + + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + public void Throw(T parameter) => throw new Exception(); + } + + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + private void GenericMethod(T val) where T : class => throw new Exception(); + + private class StackTraceHiddenAttribute : Attribute + { + } + + [StackTraceHidden] + private class TypeWithStackTraceHiddenAttribute + { + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + public void Throw() => ThrowCore(); + + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + public static void ThrowStatic() => ThrowCore(); + } + + private class TypeWithMethodWithStackTraceHiddenAttribute + { + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + [StackTraceHidden] + public void MethodWithStackTraceHiddenAttribute() + { + ThrowCore(); + } + + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + public void Throw() => MethodWithStackTraceHiddenAttribute(); + } + + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] + private static void ThrowCore() => throw new Exception(); + } +} diff --git a/src/Shared/test/testassets/ThrowingLibrary/Thrower.cs b/src/Shared/test/testassets/ThrowingLibrary/Thrower.cs new file mode 100644 index 000000000000..babe2387c6ab --- /dev/null +++ b/src/Shared/test/testassets/ThrowingLibrary/Thrower.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Runtime.CompilerServices; + +namespace ThrowingLibrary +{ + // Throwing an exception in the current assembly always seems to populate the full stack + // trace regardless of symbol type. This type exists to simulate an exception thrown + // across assemblies which is the typical use case for StackTraceHelper. + public static class Thrower + { + [MethodImpl(MethodImplOptions.NoInlining)] + public static void Throw() + { + throw new DivideByZeroException(); + } + } +} diff --git a/src/Shared/test/testassets/ThrowingLibrary/ThrowingLibrary.csproj b/src/Shared/test/testassets/ThrowingLibrary/ThrowingLibrary.csproj new file mode 100644 index 000000000000..d77d392873d7 --- /dev/null +++ b/src/Shared/test/testassets/ThrowingLibrary/ThrowingLibrary.csproj @@ -0,0 +1,8 @@ + + + + netstandard2.0 + portable + + + From 400af929a287de4d9ab5fd0dc3440dd9174882df Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Thu, 13 Dec 2018 11:42:23 -0800 Subject: [PATCH 10/10] Replace Microsoft.Extensions.WebEncoders.Sources with a local copy of code --- build/dependencies.props | 1 - build/external-dependencies.props | 1 - eng/Dependencies.props | 1 - ...NetCore.DataProtection.Abstractions.csproj | 5 +- .../Microsoft.AspNetCore.WebUtilities.csproj | 5 +- .../Properties/EncoderResources.cs | 38 ++ src/Shared/WebEncoders/WebEncoders.cs | 388 ++++++++++++++++++ .../Microsoft.AspNetCore.Shared.Tests.csproj | 1 + .../test/Shared.Tests/WebEncodersTests.cs | 113 +++++ src/SignalR/build/dependencies.props | 1 - ...crosoft.AspNetCore.Http.Connections.csproj | 2 +- 11 files changed, 546 insertions(+), 10 deletions(-) create mode 100644 src/Shared/WebEncoders/Properties/EncoderResources.cs create mode 100644 src/Shared/WebEncoders/WebEncoders.cs create mode 100644 src/Shared/test/Shared.Tests/WebEncodersTests.cs diff --git a/build/dependencies.props b/build/dependencies.props index dc93d7886843..1ab5074b562b 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -78,7 +78,6 @@ 2.1.1 2.1.1 2.1.1 - 2.1.1 2.1.1 diff --git a/build/external-dependencies.props b/build/external-dependencies.props index 3377389180f3..f3bf216028a7 100644 --- a/build/external-dependencies.props +++ b/build/external-dependencies.props @@ -66,7 +66,6 @@ - diff --git a/eng/Dependencies.props b/eng/Dependencies.props index 9a9d42775f0b..5a2feb83351d 100644 --- a/eng/Dependencies.props +++ b/eng/Dependencies.props @@ -39,7 +39,6 @@ - diff --git a/src/DataProtection/Abstractions/src/Microsoft.AspNetCore.DataProtection.Abstractions.csproj b/src/DataProtection/Abstractions/src/Microsoft.AspNetCore.DataProtection.Abstractions.csproj index 021b3fde2ce9..e1c44678f62e 100644 --- a/src/DataProtection/Abstractions/src/Microsoft.AspNetCore.DataProtection.Abstractions.csproj +++ b/src/DataProtection/Abstractions/src/Microsoft.AspNetCore.DataProtection.Abstractions.csproj @@ -12,10 +12,7 @@ Microsoft.AspNetCore.DataProtection.IDataProtector - - - - + diff --git a/src/Http/WebUtilities/src/Microsoft.AspNetCore.WebUtilities.csproj b/src/Http/WebUtilities/src/Microsoft.AspNetCore.WebUtilities.csproj index 3c7d2d8255b6..fdc9592cc871 100644 --- a/src/Http/WebUtilities/src/Microsoft.AspNetCore.WebUtilities.csproj +++ b/src/Http/WebUtilities/src/Microsoft.AspNetCore.WebUtilities.csproj @@ -10,7 +10,10 @@ - + + + + diff --git a/src/Shared/WebEncoders/Properties/EncoderResources.cs b/src/Shared/WebEncoders/Properties/EncoderResources.cs new file mode 100644 index 000000000000..3474ae82c5b7 --- /dev/null +++ b/src/Shared/WebEncoders/Properties/EncoderResources.cs @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Globalization; + +namespace Microsoft.Extensions.WebEncoders.Sources +{ + // TODO using a resx file. project.json, unfortunately, fails to embed resx files when there are also compile items + // in the contentFiles section. Revisit once we convert repos to MSBuild + internal static class EncoderResources + { + /// + /// Invalid {0}, {1} or {2} length. + /// + internal static readonly string WebEncoders_InvalidCountOffsetOrLength = "Invalid {0}, {1} or {2} length."; + + /// + /// Malformed input: {0} is an invalid input length. + /// + internal static readonly string WebEncoders_MalformedInput = "Malformed input: {0} is an invalid input length."; + + /// + /// Invalid {0}, {1} or {2} length. + /// + internal static string FormatWebEncoders_InvalidCountOffsetOrLength(object p0, object p1, object p2) + { + return string.Format(CultureInfo.CurrentCulture, WebEncoders_InvalidCountOffsetOrLength, p0, p1, p2); + } + + /// + /// Malformed input: {0} is an invalid input length. + /// + internal static string FormatWebEncoders_MalformedInput(object p0) + { + return string.Format(CultureInfo.CurrentCulture, WebEncoders_MalformedInput, p0); + } + } +} diff --git a/src/Shared/WebEncoders/WebEncoders.cs b/src/Shared/WebEncoders/WebEncoders.cs new file mode 100644 index 000000000000..17068ae67a55 --- /dev/null +++ b/src/Shared/WebEncoders/WebEncoders.cs @@ -0,0 +1,388 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.Globalization; +using Microsoft.Extensions.WebEncoders.Sources; + +#if WebEncoders_In_WebUtilities +namespace Microsoft.AspNetCore.WebUtilities +#else +namespace Microsoft.Extensions.Internal +#endif +{ + /// + /// Contains utility APIs to assist with common encoding and decoding operations. + /// +#if WebEncoders_In_WebUtilities + public +#else + internal +#endif + static class WebEncoders + { + private static readonly byte[] EmptyBytes = new byte[0]; + + /// + /// Decodes a base64url-encoded string. + /// + /// The base64url-encoded input to decode. + /// The base64url-decoded form of the input. + /// + /// The input must not contain any whitespace or padding characters. + /// Throws if the input is malformed. + /// + public static byte[] Base64UrlDecode(string input) + { + if (input == null) + { + throw new ArgumentNullException(nameof(input)); + } + + return Base64UrlDecode(input, offset: 0, count: input.Length); + } + + /// + /// Decodes a base64url-encoded substring of a given string. + /// + /// A string containing the base64url-encoded input to decode. + /// The position in at which decoding should begin. + /// The number of characters in to decode. + /// The base64url-decoded form of the input. + /// + /// The input must not contain any whitespace or padding characters. + /// Throws if the input is malformed. + /// + public static byte[] Base64UrlDecode(string input, int offset, int count) + { + if (input == null) + { + throw new ArgumentNullException(nameof(input)); + } + + ValidateParameters(input.Length, nameof(input), offset, count); + + // Special-case empty input + if (count == 0) + { + return EmptyBytes; + } + + // Create array large enough for the Base64 characters, not just shorter Base64-URL-encoded form. + var buffer = new char[GetArraySizeRequiredToDecode(count)]; + + return Base64UrlDecode(input, offset, buffer, bufferOffset: 0, count: count); + } + + /// + /// Decodes a base64url-encoded into a byte[]. + /// + /// A string containing the base64url-encoded input to decode. + /// The position in at which decoding should begin. + /// + /// Scratch buffer to hold the s to decode. Array must be large enough to hold + /// and characters as well as Base64 padding + /// characters. Content is not preserved. + /// + /// + /// The offset into at which to begin writing the s to decode. + /// + /// The number of characters in to decode. + /// The base64url-decoded form of the . + /// + /// The input must not contain any whitespace or padding characters. + /// Throws if the input is malformed. + /// + public static byte[] Base64UrlDecode(string input, int offset, char[] buffer, int bufferOffset, int count) + { + if (input == null) + { + throw new ArgumentNullException(nameof(input)); + } + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + ValidateParameters(input.Length, nameof(input), offset, count); + if (bufferOffset < 0) + { + throw new ArgumentOutOfRangeException(nameof(bufferOffset)); + } + + if (count == 0) + { + return EmptyBytes; + } + + // Assumption: input is base64url encoded without padding and contains no whitespace. + + var paddingCharsToAdd = GetNumBase64PaddingCharsToAddForDecode(count); + var arraySizeRequired = checked(count + paddingCharsToAdd); + Debug.Assert(arraySizeRequired % 4 == 0, "Invariant: Array length must be a multiple of 4."); + + if (buffer.Length - bufferOffset < arraySizeRequired) + { + throw new ArgumentException( + string.Format( + CultureInfo.CurrentCulture, + EncoderResources.WebEncoders_InvalidCountOffsetOrLength, + nameof(count), + nameof(bufferOffset), + nameof(input)), + nameof(count)); + } + + // Copy input into buffer, fixing up '-' -> '+' and '_' -> '/'. + var i = bufferOffset; + for (var j = offset; i - bufferOffset < count; i++, j++) + { + var ch = input[j]; + if (ch == '-') + { + buffer[i] = '+'; + } + else if (ch == '_') + { + buffer[i] = '/'; + } + else + { + buffer[i] = ch; + } + } + + // Add the padding characters back. + for (; paddingCharsToAdd > 0; i++, paddingCharsToAdd--) + { + buffer[i] = '='; + } + + // Decode. + // If the caller provided invalid base64 chars, they'll be caught here. + return Convert.FromBase64CharArray(buffer, bufferOffset, arraySizeRequired); + } + + /// + /// Gets the minimum char[] size required for decoding of characters + /// with the method. + /// + /// The number of characters to decode. + /// + /// The minimum char[] size required for decoding of characters. + /// + public static int GetArraySizeRequiredToDecode(int count) + { + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + if (count == 0) + { + return 0; + } + + var numPaddingCharsToAdd = GetNumBase64PaddingCharsToAddForDecode(count); + + return checked(count + numPaddingCharsToAdd); + } + + /// + /// Encodes using base64url encoding. + /// + /// The binary input to encode. + /// The base64url-encoded form of . + public static string Base64UrlEncode(byte[] input) + { + if (input == null) + { + throw new ArgumentNullException(nameof(input)); + } + + return Base64UrlEncode(input, offset: 0, count: input.Length); + } + + /// + /// Encodes using base64url encoding. + /// + /// The binary input to encode. + /// The offset into at which to begin encoding. + /// The number of bytes from to encode. + /// The base64url-encoded form of . + public static string Base64UrlEncode(byte[] input, int offset, int count) + { + if (input == null) + { + throw new ArgumentNullException(nameof(input)); + } + + ValidateParameters(input.Length, nameof(input), offset, count); + + // Special-case empty input + if (count == 0) + { + return string.Empty; + } + + var buffer = new char[GetArraySizeRequiredToEncode(count)]; + var numBase64Chars = Base64UrlEncode(input, offset, buffer, outputOffset: 0, count: count); + + return new String(buffer, startIndex: 0, length: numBase64Chars); + } + + /// + /// Encodes using base64url encoding. + /// + /// The binary input to encode. + /// The offset into at which to begin encoding. + /// + /// Buffer to receive the base64url-encoded form of . Array must be large enough to + /// hold characters and the full base64-encoded form of + /// , including padding characters. + /// + /// + /// The offset into at which to begin writing the base64url-encoded form of + /// . + /// + /// The number of bytes from to encode. + /// + /// The number of characters written to , less any padding characters. + /// + public static int Base64UrlEncode(byte[] input, int offset, char[] output, int outputOffset, int count) + { + if (input == null) + { + throw new ArgumentNullException(nameof(input)); + } + if (output == null) + { + throw new ArgumentNullException(nameof(output)); + } + + ValidateParameters(input.Length, nameof(input), offset, count); + if (outputOffset < 0) + { + throw new ArgumentOutOfRangeException(nameof(outputOffset)); + } + + var arraySizeRequired = GetArraySizeRequiredToEncode(count); + if (output.Length - outputOffset < arraySizeRequired) + { + throw new ArgumentException( + string.Format( + CultureInfo.CurrentCulture, + EncoderResources.WebEncoders_InvalidCountOffsetOrLength, + nameof(count), + nameof(outputOffset), + nameof(output)), + nameof(count)); + } + + // Special-case empty input. + if (count == 0) + { + return 0; + } + + // Use base64url encoding with no padding characters. See RFC 4648, Sec. 5. + + // Start with default Base64 encoding. + var numBase64Chars = Convert.ToBase64CharArray(input, offset, count, output, outputOffset); + + // Fix up '+' -> '-' and '/' -> '_'. Drop padding characters. + for (var i = outputOffset; i - outputOffset < numBase64Chars; i++) + { + var ch = output[i]; + if (ch == '+') + { + output[i] = '-'; + } + else if (ch == '/') + { + output[i] = '_'; + } + else if (ch == '=') + { + // We've reached a padding character; truncate the remainder. + return i - outputOffset; + } + } + + return numBase64Chars; + } + + /// + /// Get the minimum output char[] size required for encoding + /// s with the method. + /// + /// The number of characters to encode. + /// + /// The minimum output char[] size required for encoding s. + /// + public static int GetArraySizeRequiredToEncode(int count) + { + var numWholeOrPartialInputBlocks = checked(count + 2) / 3; + return checked(numWholeOrPartialInputBlocks * 4); + } + + private static int GetNumBase64PaddingCharsInString(string str) + { + // Assumption: input contains a well-formed base64 string with no whitespace. + + // base64 guaranteed have 0 - 2 padding characters. + if (str[str.Length - 1] == '=') + { + if (str[str.Length - 2] == '=') + { + return 2; + } + return 1; + } + return 0; + } + + private static int GetNumBase64PaddingCharsToAddForDecode(int inputLength) + { + switch (inputLength % 4) + { + case 0: + return 0; + case 2: + return 2; + case 3: + return 1; + default: + throw new FormatException( + string.Format( + CultureInfo.CurrentCulture, + EncoderResources.WebEncoders_MalformedInput, + inputLength)); + } + } + + private static void ValidateParameters(int bufferLength, string inputName, int offset, int count) + { + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (bufferLength - offset < count) + { + throw new ArgumentException( + string.Format( + CultureInfo.CurrentCulture, + EncoderResources.WebEncoders_InvalidCountOffsetOrLength, + nameof(count), + nameof(offset), + inputName), + nameof(count)); + } + } + } +} diff --git a/src/Shared/test/Shared.Tests/Microsoft.AspNetCore.Shared.Tests.csproj b/src/Shared/test/Shared.Tests/Microsoft.AspNetCore.Shared.Tests.csproj index 42629957b8e8..48b2099db720 100644 --- a/src/Shared/test/Shared.Tests/Microsoft.AspNetCore.Shared.Tests.csproj +++ b/src/Shared/test/Shared.Tests/Microsoft.AspNetCore.Shared.Tests.csproj @@ -13,6 +13,7 @@ + diff --git a/src/Shared/test/Shared.Tests/WebEncodersTests.cs b/src/Shared/test/Shared.Tests/WebEncodersTests.cs new file mode 100644 index 000000000000..5c71403fd653 --- /dev/null +++ b/src/Shared/test/Shared.Tests/WebEncodersTests.cs @@ -0,0 +1,113 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using Xunit; + +namespace Microsoft.Extensions.Internal +{ + public class WebEncodersTests + { + [Theory] + [InlineData("", 1, 0)] + [InlineData("", 0, 1)] + [InlineData("0123456789", 9, 2)] + [InlineData("0123456789", Int32.MaxValue, 2)] + [InlineData("0123456789", 9, -1)] + public void Base64UrlDecode_BadOffsets(string input, int offset, int count) + { + // Act & assert + Assert.ThrowsAny(() => + { + var retVal = WebEncoders.Base64UrlDecode(input, offset, count); + }); + } + + [Theory] + [InlineData("x")] + [InlineData("(x)")] + public void Base64UrlDecode_MalformedInput(string input) + { + // Act & assert + Assert.Throws(() => + { + var retVal = WebEncoders.Base64UrlDecode(input); + }); + } + + [Theory] + [InlineData("", "")] + [InlineData("123456qwerty++//X+/x", "123456qwerty--__X-_x")] + [InlineData("123456qwerty++//X+/xxw==", "123456qwerty--__X-_xxw")] + [InlineData("123456qwerty++//X+/xxw0=", "123456qwerty--__X-_xxw0")] + public void Base64UrlEncode_And_Decode(string base64Input, string expectedBase64Url) + { + // Arrange + byte[] input = new byte[3].Concat(Convert.FromBase64String(base64Input)).Concat(new byte[2]).ToArray(); + + // Act & assert - 1 + string actualBase64Url = WebEncoders.Base64UrlEncode(input, 3, input.Length - 5); // also helps test offsets + Assert.Equal(expectedBase64Url, actualBase64Url); + + // Act & assert - 2 + // Verify that values round-trip + byte[] roundTripped = WebEncoders.Base64UrlDecode("xx" + actualBase64Url + "yyy", 2, actualBase64Url.Length); // also helps test offsets + string roundTrippedAsBase64 = Convert.ToBase64String(roundTripped); + Assert.Equal(roundTrippedAsBase64, base64Input); + } + + [Theory] + [InlineData("", "")] + [InlineData("123456qwerty++//X+/x", "123456qwerty--__X-_x")] + [InlineData("123456qwerty++//X+/xxw==", "123456qwerty--__X-_xxw")] + [InlineData("123456qwerty++//X+/xxw0=", "123456qwerty--__X-_xxw0")] + public void Base64UrlEncode_And_Decode_WithBufferOffsets(string base64Input, string expectedBase64Url) + { + // Arrange + var input = new byte[3].Concat(Convert.FromBase64String(base64Input)).Concat(new byte[2]).ToArray(); + var buffer = new char[30]; + var output = new char[30]; + for (var i = 0; i < buffer.Length; i++) + { + buffer[i] = '^'; + output[i] = '^'; + } + + // Act 1 + var numEncodedChars = + WebEncoders.Base64UrlEncode(input, offset: 3, output: output, outputOffset: 4, count: input.Length - 5); + + // Assert 1 + var encodedString = new string(output, startIndex: 4, length: numEncodedChars); + Assert.Equal(expectedBase64Url, encodedString); + + // Act 2 + var roundTripInput = new string(output); + var roundTripped = + WebEncoders.Base64UrlDecode(roundTripInput, offset: 4, buffer: buffer, bufferOffset: 5, count: numEncodedChars); + + // Assert 2, verify that values round-trip + var roundTrippedAsBase64 = Convert.ToBase64String(roundTripped); + Assert.Equal(roundTrippedAsBase64, base64Input); + } + + [Theory] + [InlineData(0, 1, 0)] + [InlineData(0, 0, 1)] + [InlineData(10, 9, 2)] + [InlineData(10, Int32.MaxValue, 2)] + [InlineData(10, 9, -1)] + public void Base64UrlEncode_BadOffsets(int inputLength, int offset, int count) + { + // Arrange + byte[] input = new byte[inputLength]; + + // Act & assert + Assert.ThrowsAny(() => + { + var retVal = WebEncoders.Base64UrlEncode(input, offset, count); + }); + } + } +} diff --git a/src/SignalR/build/dependencies.props b/src/SignalR/build/dependencies.props index 42d0a0524cc2..c3d36120d3d2 100644 --- a/src/SignalR/build/dependencies.props +++ b/src/SignalR/build/dependencies.props @@ -79,6 +79,5 @@ 2.1.1 2.1.1 2.1.1 - 2.1.1 diff --git a/src/SignalR/src/Microsoft.AspNetCore.Http.Connections/Microsoft.AspNetCore.Http.Connections.csproj b/src/SignalR/src/Microsoft.AspNetCore.Http.Connections/Microsoft.AspNetCore.Http.Connections.csproj index 66c8cd59b610..4c572c8aba08 100644 --- a/src/SignalR/src/Microsoft.AspNetCore.Http.Connections/Microsoft.AspNetCore.Http.Connections.csproj +++ b/src/SignalR/src/Microsoft.AspNetCore.Http.Connections/Microsoft.AspNetCore.Http.Connections.csproj @@ -16,6 +16,7 @@ + @@ -28,7 +29,6 @@ -