diff --git a/src/Configuration.KeyPerFile/Directory.Build.props b/src/Configuration.KeyPerFile/Directory.Build.props index 2082380096..63d0c8b102 100644 --- a/src/Configuration.KeyPerFile/Directory.Build.props +++ b/src/Configuration.KeyPerFile/Directory.Build.props @@ -2,7 +2,8 @@ - true + true configuration + $(NoWarn);PKG0001 diff --git a/src/Configuration.KeyPerFile/ref/Microsoft.Extensions.Configuration.KeyPerFile.csproj b/src/Configuration.KeyPerFile/ref/Microsoft.Extensions.Configuration.KeyPerFile.csproj new file mode 100644 index 0000000000..21f0053e59 --- /dev/null +++ b/src/Configuration.KeyPerFile/ref/Microsoft.Extensions.Configuration.KeyPerFile.csproj @@ -0,0 +1,11 @@ + + + + netstandard2.0 + + + + + + + diff --git a/src/Configuration.KeyPerFile/ref/Microsoft.Extensions.Configuration.KeyPerFile.netstandard2.0.cs b/src/Configuration.KeyPerFile/ref/Microsoft.Extensions.Configuration.KeyPerFile.netstandard2.0.cs new file mode 100644 index 0000000000..e26ca1909d --- /dev/null +++ b/src/Configuration.KeyPerFile/ref/Microsoft.Extensions.Configuration.KeyPerFile.netstandard2.0.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. + +namespace Microsoft.Extensions.Configuration +{ + public static partial class KeyPerFileConfigurationBuilderExtensions + { + public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddKeyPerFile(this Microsoft.Extensions.Configuration.IConfigurationBuilder builder, System.Action configureSource) { throw null; } + public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddKeyPerFile(this Microsoft.Extensions.Configuration.IConfigurationBuilder builder, string directoryPath, bool optional) { throw null; } + } +} +namespace Microsoft.Extensions.Configuration.KeyPerFile +{ + public partial class KeyPerFileConfigurationProvider : Microsoft.Extensions.Configuration.ConfigurationProvider + { + public KeyPerFileConfigurationProvider(Microsoft.Extensions.Configuration.KeyPerFile.KeyPerFileConfigurationSource source) { } + public override void Load() { } + public override string ToString() { throw null; } + } + public partial class KeyPerFileConfigurationSource : Microsoft.Extensions.Configuration.IConfigurationSource + { + public KeyPerFileConfigurationSource() { } + public Microsoft.Extensions.FileProviders.IFileProvider FileProvider { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public System.Func IgnoreCondition { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public string IgnorePrefix { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public bool Optional { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public Microsoft.Extensions.Configuration.IConfigurationProvider Build(Microsoft.Extensions.Configuration.IConfigurationBuilder builder) { throw null; } + } +} diff --git a/src/Configuration.KeyPerFile/src/KeyPerFileConfigurationProvider.cs b/src/Configuration.KeyPerFile/src/KeyPerFileConfigurationProvider.cs index 6e4234ecf3..2e33b9dfcd 100644 --- a/src/Configuration.KeyPerFile/src/KeyPerFileConfigurationProvider.cs +++ b/src/Configuration.KeyPerFile/src/KeyPerFileConfigurationProvider.cs @@ -40,10 +40,8 @@ namespace Microsoft.Extensions.Configuration.KeyPerFile Data = data; return; } - else - { - throw new DirectoryNotFoundException("A non-null file provider for the directory is required when this source is not optional."); - } + + throw new DirectoryNotFoundException("A non-null file provider for the directory is required when this source is not optional."); } var directory = Source.FileProvider.GetDirectoryContents("/"); @@ -71,5 +69,15 @@ namespace Microsoft.Extensions.Configuration.KeyPerFile Data = data; } + + private string GetDirectoryName() + => Source.FileProvider?.GetFileInfo("/")?.PhysicalPath ?? ""; + + /// + /// Generates a string representing this provider name and relevant details. + /// + /// The configuration name. + public override string ToString() + => $"{GetType().Name} for files in '{GetDirectoryName()}' ({(Source.Optional ? "Optional" : "Required")})"; } } diff --git a/src/Configuration.KeyPerFile/src/Microsoft.Extensions.Configuration.KeyPerFile.csproj b/src/Configuration.KeyPerFile/src/Microsoft.Extensions.Configuration.KeyPerFile.csproj index 4eb19f3293..5bd7b2c7ef 100644 --- a/src/Configuration.KeyPerFile/src/Microsoft.Extensions.Configuration.KeyPerFile.csproj +++ b/src/Configuration.KeyPerFile/src/Microsoft.Extensions.Configuration.KeyPerFile.csproj @@ -3,7 +3,7 @@ Configuration provider that uses files in a directory for Microsoft.Extensions.Configuration. netstandard2.0 - false + true diff --git a/src/Configuration.KeyPerFile/test/ConfigurationProviderCommandLineTest.cs b/src/Configuration.KeyPerFile/test/ConfigurationProviderCommandLineTest.cs new file mode 100644 index 0000000000..066aecf337 --- /dev/null +++ b/src/Configuration.KeyPerFile/test/ConfigurationProviderCommandLineTest.cs @@ -0,0 +1,43 @@ +// 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 Microsoft.Extensions.Configuration.Test; +using Microsoft.Extensions.FileProviders; + +namespace Microsoft.Extensions.Configuration.KeyPerFile.Test +{ + public class ConfigurationProviderCommandLineTest : ConfigurationProviderTestBase + { + protected override (IConfigurationProvider Provider, Action Initializer) LoadThroughProvider( + TestSection testConfig) + { + var testFiles = new List(); + SectionToTestFiles(testFiles, "", testConfig); + + var provider = new KeyPerFileConfigurationProvider( + new KeyPerFileConfigurationSource + { + Optional = true, + FileProvider = new TestFileProvider(testFiles.ToArray()) + }); + + return (provider, () => { }); + } + + private void SectionToTestFiles(List testFiles, string sectionName, TestSection section) + { + foreach (var tuple in section.Values.SelectMany(e => e.Value.Expand(e.Key))) + { + testFiles.Add(new TestFile(sectionName + tuple.Key, tuple.Value)); + } + + foreach (var tuple in section.Sections) + { + SectionToTestFiles(testFiles, sectionName + tuple.Key + "__", tuple.Section); + } + } + } +} diff --git a/src/Configuration.KeyPerFile/test/ConfigurationProviderTestBase.cs b/src/Configuration.KeyPerFile/test/ConfigurationProviderTestBase.cs new file mode 100644 index 0000000000..4609ee2560 --- /dev/null +++ b/src/Configuration.KeyPerFile/test/ConfigurationProviderTestBase.cs @@ -0,0 +1,761 @@ +// 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 Microsoft.Extensions.Configuration.Memory; +using Xunit; + +namespace Microsoft.Extensions.Configuration.Test +{ + public abstract class ConfigurationProviderTestBase + { + [Fact] + public virtual void Load_from_single_provider() + { + var configRoot = BuildConfigRoot(LoadThroughProvider(TestSection.TestConfig)); + + AssertConfig(configRoot); + } + + [Fact] + public virtual void Has_debug_view() + { + var configRoot = BuildConfigRoot(LoadThroughProvider(TestSection.TestConfig)); + var providerTag = configRoot.Providers.Single().ToString(); + + var expected = + $@"Key1=Value1 ({providerTag}) +Section1: + Key2=Value12 ({providerTag}) + Section2: + Key3=Value123 ({providerTag}) + Key3a: + 0=ArrayValue0 ({providerTag}) + 1=ArrayValue1 ({providerTag}) + 2=ArrayValue2 ({providerTag}) +Section3: + Section4: + Key4=Value344 ({providerTag}) +"; + + AssertDebugView(configRoot, expected); + } + + [Fact] + public virtual void Null_values_are_included_in_the_config() + { + AssertConfig(BuildConfigRoot(LoadThroughProvider(TestSection.NullsTestConfig)), expectNulls: true, nullValue: ""); + } + + [Fact] + public virtual void Combine_after_other_provider() + { + AssertConfig( + BuildConfigRoot( + LoadUsingMemoryProvider(TestSection.MissingSection2ValuesConfig), + LoadThroughProvider(TestSection.MissingSection4Config))); + + AssertConfig( + BuildConfigRoot( + LoadUsingMemoryProvider(TestSection.MissingSection4Config), + LoadThroughProvider(TestSection.MissingSection2ValuesConfig))); + } + + [Fact] + public virtual void Combine_before_other_provider() + { + AssertConfig( + BuildConfigRoot( + LoadThroughProvider(TestSection.MissingSection2ValuesConfig), + LoadUsingMemoryProvider(TestSection.MissingSection4Config))); + + AssertConfig( + BuildConfigRoot( + LoadThroughProvider(TestSection.MissingSection4Config), + LoadUsingMemoryProvider(TestSection.MissingSection2ValuesConfig))); + } + + [Fact] + public virtual void Second_provider_overrides_values_from_first() + { + AssertConfig( + BuildConfigRoot( + LoadUsingMemoryProvider(TestSection.NoValuesTestConfig), + LoadThroughProvider(TestSection.TestConfig))); + } + + [Fact] + public virtual void Combining_from_multiple_providers_is_case_insensitive() + { + AssertConfig( + BuildConfigRoot( + LoadUsingMemoryProvider(TestSection.DifferentCasedTestConfig), + LoadThroughProvider(TestSection.TestConfig))); + } + + [Fact] + public virtual void Load_from_single_provider_with_duplicates_throws() + { + AssertFormatOrArgumentException( + () => BuildConfigRoot(LoadThroughProvider(TestSection.DuplicatesTestConfig))); + } + + [Fact] + public virtual void Load_from_single_provider_with_differing_case_duplicates_throws() + { + AssertFormatOrArgumentException( + () => BuildConfigRoot(LoadThroughProvider(TestSection.DuplicatesDifferentCaseTestConfig))); + } + + private void AssertFormatOrArgumentException(Action test) + { + Exception caught = null; + try + { + test(); + } + catch (Exception e) + { + caught = e; + } + + Assert.True(caught is ArgumentException + || caught is FormatException); + } + + [Fact] + public virtual void Bind_to_object() + { + var configuration = BuildConfigRoot(LoadThroughProvider(TestSection.TestConfig)); + + var options = configuration.Get(); + + Assert.Equal("Value1", options.Key1); + Assert.Equal("Value12", options.Section1.Key2); + Assert.Equal("Value123", options.Section1.Section2.Key3); + Assert.Equal("Value344", options.Section3.Section4.Key4); + Assert.Equal(new[] { "ArrayValue0", "ArrayValue1", "ArrayValue2" }, options.Section1.Section2.Key3a); + } + + public class AsOptions + { + public string Key1 { get; set; } + + public Section1AsOptions Section1 { get; set; } + public Section3AsOptions Section3 { get; set; } + } + + public class Section1AsOptions + { + public string Key2 { get; set; } + + public Section2AsOptions Section2 { get; set; } + } + + public class Section2AsOptions + { + public string Key3 { get; set; } + public string[] Key3a { get; set; } + } + + public class Section3AsOptions + { + public Section4AsOptions Section4 { get; set; } + } + + public class Section4AsOptions + { + public string Key4 { get; set; } + } + + protected virtual void AssertDebugView( + IConfigurationRoot config, + string expected) + { + string RemoveLineEnds(string source) => source.Replace("\n", "").Replace("\r", ""); + + var actual = config.GetDebugView(); + + Assert.Equal( + RemoveLineEnds(expected), + RemoveLineEnds(actual)); + } + + protected virtual void AssertConfig( + IConfigurationRoot config, + bool expectNulls = false, + string nullValue = null) + { + var value1 = expectNulls ? nullValue : "Value1"; + var value12 = expectNulls ? nullValue : "Value12"; + var value123 = expectNulls ? nullValue : "Value123"; + var arrayvalue0 = expectNulls ? nullValue : "ArrayValue0"; + var arrayvalue1 = expectNulls ? nullValue : "ArrayValue1"; + var arrayvalue2 = expectNulls ? nullValue : "ArrayValue2"; + var value344 = expectNulls ? nullValue : "Value344"; + + Assert.Equal(value1, config["Key1"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(value12, config["Section1:Key2"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(value123, config["Section1:Section2:Key3"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(arrayvalue0, config["Section1:Section2:Key3a:0"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(arrayvalue1, config["Section1:Section2:Key3a:1"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(arrayvalue2, config["Section1:Section2:Key3a:2"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(value344, config["Section3:Section4:Key4"], StringComparer.InvariantCultureIgnoreCase); + + var section1 = config.GetSection("Section1"); + Assert.Equal(value12, section1["Key2"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(value123, section1["Section2:Key3"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(arrayvalue0, section1["Section2:Key3a:0"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(arrayvalue1, section1["Section2:Key3a:1"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(arrayvalue2, section1["Section2:Key3a:2"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal("Section1", section1.Path, StringComparer.InvariantCultureIgnoreCase); + Assert.Null(section1.Value); + + var section2 = config.GetSection("Section1:Section2"); + Assert.Equal(value123, section2["Key3"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(arrayvalue0, section2["Key3a:0"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(arrayvalue1, section2["Key3a:1"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(arrayvalue2, section2["Key3a:2"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal("Section1:Section2", section2.Path, StringComparer.InvariantCultureIgnoreCase); + Assert.Null(section2.Value); + + section2 = section1.GetSection("Section2"); + Assert.Equal(value123, section2["Key3"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(arrayvalue0, section2["Key3a:0"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(arrayvalue1, section2["Key3a:1"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(arrayvalue2, section2["Key3a:2"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal("Section1:Section2", section2.Path, StringComparer.InvariantCultureIgnoreCase); + Assert.Null(section2.Value); + + var section3a = section2.GetSection("Key3a"); + Assert.Equal(arrayvalue0, section3a["0"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(arrayvalue1, section3a["1"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(arrayvalue2, section3a["2"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal("Section1:Section2:Key3a", section3a.Path, StringComparer.InvariantCultureIgnoreCase); + Assert.Null(section3a.Value); + + var section3 = config.GetSection("Section3"); + Assert.Equal("Section3", section3.Path, StringComparer.InvariantCultureIgnoreCase); + Assert.Null(section3.Value); + + var section4 = config.GetSection("Section3:Section4"); + Assert.Equal(value344, section4["Key4"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal("Section3:Section4", section4.Path, StringComparer.InvariantCultureIgnoreCase); + Assert.Null(section4.Value); + + section4 = config.GetSection("Section3").GetSection("Section4"); + Assert.Equal(value344, section4["Key4"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal("Section3:Section4", section4.Path, StringComparer.InvariantCultureIgnoreCase); + Assert.Null(section4.Value); + + var sections = config.GetChildren().ToList(); + + Assert.Equal(3, sections.Count); + + Assert.Equal("Key1", sections[0].Key, StringComparer.InvariantCultureIgnoreCase); + Assert.Equal("Key1", sections[0].Path, StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(value1, sections[0].Value, StringComparer.InvariantCultureIgnoreCase); + + Assert.Equal("Section1", sections[1].Key, StringComparer.InvariantCultureIgnoreCase); + Assert.Equal("Section1", sections[1].Path, StringComparer.InvariantCultureIgnoreCase); + Assert.Null(sections[1].Value); + + Assert.Equal("Section3", sections[2].Key, StringComparer.InvariantCultureIgnoreCase); + Assert.Equal("Section3", sections[2].Path, StringComparer.InvariantCultureIgnoreCase); + Assert.Null(sections[2].Value); + + sections = section1.GetChildren().ToList(); + + Assert.Equal(2, sections.Count); + + Assert.Equal("Key2", sections[0].Key, StringComparer.InvariantCultureIgnoreCase); + Assert.Equal("Section1:Key2", sections[0].Path, StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(value12, sections[0].Value, StringComparer.InvariantCultureIgnoreCase); + + Assert.Equal("Section2", sections[1].Key, StringComparer.InvariantCultureIgnoreCase); + Assert.Equal("Section1:Section2", sections[1].Path, StringComparer.InvariantCultureIgnoreCase); + Assert.Null(sections[1].Value); + + sections = section2.GetChildren().ToList(); + + Assert.Equal(2, sections.Count); + + Assert.Equal("Key3", sections[0].Key, StringComparer.InvariantCultureIgnoreCase); + Assert.Equal("Section1:Section2:Key3", sections[0].Path, StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(value123, sections[0].Value, StringComparer.InvariantCultureIgnoreCase); + + Assert.Equal("Key3a", sections[1].Key, StringComparer.InvariantCultureIgnoreCase); + Assert.Equal("Section1:Section2:Key3a", sections[1].Path, StringComparer.InvariantCultureIgnoreCase); + Assert.Null(sections[1].Value); + + sections = section3a.GetChildren().ToList(); + + Assert.Equal(3, sections.Count); + + Assert.Equal("0", sections[0].Key, StringComparer.InvariantCultureIgnoreCase); + Assert.Equal("Section1:Section2:Key3a:0", sections[0].Path, StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(arrayvalue0, sections[0].Value, StringComparer.InvariantCultureIgnoreCase); + + Assert.Equal("1", sections[1].Key, StringComparer.InvariantCultureIgnoreCase); + Assert.Equal("Section1:Section2:Key3a:1", sections[1].Path, StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(arrayvalue1, sections[1].Value, StringComparer.InvariantCultureIgnoreCase); + + Assert.Equal("2", sections[2].Key, StringComparer.InvariantCultureIgnoreCase); + Assert.Equal("Section1:Section2:Key3a:2", sections[2].Path, StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(arrayvalue2, sections[2].Value, StringComparer.InvariantCultureIgnoreCase); + + sections = section3.GetChildren().ToList(); + + Assert.Single(sections); + + Assert.Equal("Section4", sections[0].Key, StringComparer.InvariantCultureIgnoreCase); + Assert.Equal("Section3:Section4", sections[0].Path, StringComparer.InvariantCultureIgnoreCase); + Assert.Null(sections[0].Value); + + sections = section4.GetChildren().ToList(); + + Assert.Single(sections); + + Assert.Equal("Key4", sections[0].Key, StringComparer.InvariantCultureIgnoreCase); + Assert.Equal("Section3:Section4:Key4", sections[0].Path, StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(value344, sections[0].Value, StringComparer.InvariantCultureIgnoreCase); + } + + protected abstract (IConfigurationProvider Provider, Action Initializer) LoadThroughProvider(TestSection testConfig); + + protected virtual IConfigurationRoot BuildConfigRoot( + params (IConfigurationProvider Provider, Action Initializer)[] providers) + { + var root = new ConfigurationRoot(providers.Select(e => e.Provider).ToList()); + + foreach (var initializer in providers.Select(e => e.Initializer)) + { + initializer(); + } + + return root; + } + + protected static (IConfigurationProvider Provider, Action Initializer) LoadUsingMemoryProvider(TestSection testConfig) + { + var values = new List>(); + SectionToValues(testConfig, "", values); + + return (new MemoryConfigurationProvider( + new MemoryConfigurationSource + { + InitialData = values + }), + () => { }); + } + + protected static void SectionToValues( + TestSection section, + string sectionName, + IList> values) + { + foreach (var tuple in section.Values.SelectMany(e => e.Value.Expand(e.Key))) + { + values.Add(new KeyValuePair(sectionName + tuple.Key, tuple.Value)); + } + + foreach (var tuple in section.Sections) + { + SectionToValues( + tuple.Section, + sectionName + tuple.Key + ":", + values); + } + } + + protected class TestKeyValue + { + public object Value { get; } + + public TestKeyValue(string value) + { + Value = value; + } + + public TestKeyValue(string[] values) + { + Value = values; + } + + public static implicit operator TestKeyValue(string value) => new TestKeyValue(value); + public static implicit operator TestKeyValue(string[] values) => new TestKeyValue(values); + + public string[] AsArray => Value as string[]; + + public string AsString => Value as string; + + public IEnumerable<(string Key, string Value)> Expand(string key) + { + if (AsArray == null) + { + yield return (key, AsString); + } + else + { + for (var i = 0; i < AsArray.Length; i++) + { + yield return ($"{key}:{i}", AsArray[i]); + } + } + } + } + + protected class TestSection + { + public IEnumerable<(string Key, TestKeyValue Value)> Values { get; set; } + = Enumerable.Empty<(string, TestKeyValue)>(); + + public IEnumerable<(string Key, TestSection Section)> Sections { get; set; } + = Enumerable.Empty<(string, TestSection)>(); + + public static TestSection TestConfig { get; } + = new TestSection + { + Values = new[] { ("Key1", (TestKeyValue)"Value1") }, + Sections = new[] + { + ("Section1", new TestSection + { + Values = new[] {("Key2", (TestKeyValue)"Value12")}, + Sections = new[] + { + ("Section2", new TestSection + { + Values = new[] + { + ("Key3", (TestKeyValue)"Value123"), + ("Key3a", (TestKeyValue)new[] {"ArrayValue0", "ArrayValue1", "ArrayValue2"}) + }, + }) + } + }), + ("Section3", new TestSection + { + Sections = new[] + { + ("Section4", new TestSection + { + Values = new[] {("Key4", (TestKeyValue)"Value344")} + }) + } + }) + } + }; + + public static TestSection NoValuesTestConfig { get; } + = new TestSection + { + Values = new[] { ("Key1", (TestKeyValue)"------") }, + Sections = new[] + { + ("Section1", new TestSection + { + Values = new[] {("Key2", (TestKeyValue)"-------")}, + Sections = new[] + { + ("Section2", new TestSection + { + Values = new[] + { + ("Key3", (TestKeyValue)"-----"), + ("Key3a", (TestKeyValue)new[] {"-----------", "-----------", "-----------"}) + }, + }) + } + }), + ("Section3", new TestSection + { + Sections = new[] + { + ("Section4", new TestSection + { + Values = new[] {("Key4", (TestKeyValue)"--------")} + }) + } + }) + } + }; + + public static TestSection MissingSection2ValuesConfig { get; } + = new TestSection + { + Values = new[] { ("Key1", (TestKeyValue)"Value1") }, + Sections = new[] + { + ("Section1", new TestSection + { + Values = new[] {("Key2", (TestKeyValue)"Value12")}, + Sections = new[] + { + ("Section2", new TestSection + { + Values = new[] + { + ("Key3a", (TestKeyValue)new[] {"ArrayValue0"}) + }, + }) + } + }), + ("Section3", new TestSection + { + Sections = new[] + { + ("Section4", new TestSection + { + Values = new[] {("Key4", (TestKeyValue)"Value344")} + }) + } + }) + } + }; + + + public static TestSection MissingSection4Config { get; } + = new TestSection + { + Values = new[] { ("Key1", (TestKeyValue)"Value1") }, + Sections = new[] + { + ("Section1", new TestSection + { + Values = new[] {("Key2", (TestKeyValue)"Value12")}, + Sections = new[] + { + ("Section2", new TestSection + { + Values = new[] + { + ("Key3", (TestKeyValue)"Value123"), + ("Key3a", (TestKeyValue)new[] {"ArrayValue0", "ArrayValue1", "ArrayValue2"}) + }, + }) + } + }), + ("Section3", new TestSection()) + } + }; + + public static TestSection DifferentCasedTestConfig { get; } + = new TestSection + { + Values = new[] { ("KeY1", (TestKeyValue)"Value1") }, + Sections = new[] + { + ("SectioN1", new TestSection + { + Values = new[] {("KeY2", (TestKeyValue)"Value12")}, + Sections = new[] + { + ("SectioN2", new TestSection + { + Values = new[] + { + ("KeY3", (TestKeyValue)"Value123"), + ("KeY3a", (TestKeyValue)new[] {"ArrayValue0", "ArrayValue1", "ArrayValue2"}) + }, + }) + } + }), + ("SectioN3", new TestSection + { + Sections = new[] + { + ("SectioN4", new TestSection + { + Values = new[] {("KeY4", (TestKeyValue)"Value344")} + }) + } + }) + } + }; + + public static TestSection DuplicatesTestConfig { get; } + = new TestSection + { + Values = new[] + { + ("Key1", (TestKeyValue)"Value1"), + ("Key1", (TestKeyValue)"Value1") + }, + Sections = new[] + { + ("Section1", new TestSection + { + Values = new[] {("Key2", (TestKeyValue)"Value12")}, + Sections = new[] + { + ("Section2", new TestSection + { + Values = new[] + { + ("Key3", (TestKeyValue)"Value123"), + ("Key3a", (TestKeyValue)new[] {"ArrayValue0", "ArrayValue1", "ArrayValue2"}) + }, + }), + ("Section2", new TestSection + { + Values = new[] + { + ("Key3", (TestKeyValue)"Value123"), + ("Key3a", (TestKeyValue)new[] {"ArrayValue0", "ArrayValue1", "ArrayValue2"}) + }, + }) + + } + }), + ("Section3", new TestSection + { + Sections = new[] + { + ("Section4", new TestSection + { + Values = new[] {("Key4", (TestKeyValue)"Value344")} + }) + } + }) + } + }; + + public static TestSection DuplicatesDifferentCaseTestConfig { get; } + = new TestSection + { + Values = new[] + { + ("Key1", (TestKeyValue)"Value1"), + ("KeY1", (TestKeyValue)"Value1") + }, + Sections = new[] + { + ("Section1", new TestSection + { + Values = new[] {("Key2", (TestKeyValue)"Value12")}, + Sections = new[] + { + ("Section2", new TestSection + { + Values = new[] + { + ("Key3", (TestKeyValue)"Value123"), + ("Key3a", (TestKeyValue)new[] {"ArrayValue0", "ArrayValue1", "ArrayValue2"}) + }, + }), + ("SectioN2", new TestSection + { + Values = new[] + { + ("KeY3", (TestKeyValue)"Value123"), + ("KeY3a", (TestKeyValue)new[] {"ArrayValue0", "ArrayValue1", "ArrayValue2"}) + }, + }) + + } + }), + ("Section3", new TestSection + { + Sections = new[] + { + ("Section4", new TestSection + { + Values = new[] {("Key4", (TestKeyValue)"Value344")} + }) + } + }) + } + }; + + public static TestSection NullsTestConfig { get; } + = new TestSection + { + Values = new[] { ("Key1", new TestKeyValue((string)null)) }, + Sections = new[] + { + ("Section1", new TestSection + { + Values = new[] {("Key2", new TestKeyValue((string)null))}, + Sections = new[] + { + ("Section2", new TestSection + { + Values = new[] + { + ("Key3", new TestKeyValue((string)null)), + ("Key3a", (TestKeyValue)new string[] {null, null, null}) + }, + }) + } + }), + ("Section3", new TestSection + { + Sections = new[] + { + ("Section4", new TestSection + { + Values = new[] {("Key4", new TestKeyValue((string)null))} + }) + } + }) + } + }; + + public static TestSection ExtraValuesTestConfig { get; } + = new TestSection + { + Values = new[] + { + ("Key1", (TestKeyValue)"Value1"), + ("Key1r", (TestKeyValue)"Value1r") + }, + Sections = new[] + { + ("Section1", new TestSection + { + Values = new[] + { + ("Key2", (TestKeyValue)"Value12"), + ("Key2r", (TestKeyValue)"Value12r") + }, + Sections = new[] + { + ("Section2", new TestSection + { + Values = new[] + { + ("Key3", (TestKeyValue)"Value123"), + ("Key3a", (TestKeyValue)new[] {"ArrayValue0", "ArrayValue1", "ArrayValue2", "ArrayValue2r"}), + ("Key3ar", (TestKeyValue)new[] {"ArrayValue0r"}) + }, + }) + } + }), + ("Section3", new TestSection + { + Sections = new[] + { + ("Section4", new TestSection + { + Values = new[] {("Key4", (TestKeyValue)"Value344")} + }) + } + }), + ("Section5r", new TestSection + { + Sections = new[] + { + ("Section6r", new TestSection + { + Values = new[] {("Key5r", (TestKeyValue)"Value565r")} + }) + } + }) + } + }; + } + } +} diff --git a/src/Configuration.KeyPerFile/test/KeyPerFileTests.cs b/src/Configuration.KeyPerFile/test/KeyPerFileTests.cs index 499c25106c..838e62222d 100644 --- a/src/Configuration.KeyPerFile/test/KeyPerFileTests.cs +++ b/src/Configuration.KeyPerFile/test/KeyPerFileTests.cs @@ -1,8 +1,10 @@ +// 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.IO; -using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -195,7 +197,15 @@ namespace Microsoft.Extensions.Configuration.KeyPerFile.Test using (var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(250))) { - _ = Task.Run(() => { while (!cts.IsCancellationRequested) config.Reload(); }); + void ReloadLoop() + { + while (!cts.IsCancellationRequested) + { + config.Reload(); + } + } + + _ = Task.Run(ReloadLoop); while (!cts.IsCancellationRequested) { @@ -223,20 +233,11 @@ namespace Microsoft.Extensions.Configuration.KeyPerFile.Test _contents = new TestDirectoryContents(files); } - public IDirectoryContents GetDirectoryContents(string subpath) - { - return _contents; - } + public IDirectoryContents GetDirectoryContents(string subpath) => _contents; - public IFileInfo GetFileInfo(string subpath) - { - throw new NotImplementedException(); - } + public IFileInfo GetFileInfo(string subpath) => new TestFile("TestDirectory"); - public IChangeToken Watch(string filter) - { - throw new NotImplementedException(); - } + public IChangeToken Watch(string filter) => throw new NotImplementedException(); } class TestDirectoryContents : IDirectoryContents @@ -248,75 +249,33 @@ namespace Microsoft.Extensions.Configuration.KeyPerFile.Test _list = new List(files); } - public bool Exists - { - get - { - return true; - } - } + public bool Exists => true; - public IEnumerator GetEnumerator() - { - return _list.GetEnumerator(); - } + public IEnumerator GetEnumerator() => _list.GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } //TODO: Probably need a directory and file type. class TestFile : IFileInfo { - private string _name; - private string _contents; + private readonly string _name; + private readonly string _contents; - public bool Exists - { - get - { - return true; - } - } + public bool Exists => true; public bool IsDirectory { get; } - public DateTimeOffset LastModified - { - get - { - throw new NotImplementedException(); - } - } + public DateTimeOffset LastModified => throw new NotImplementedException(); - public long Length - { - get - { - throw new NotImplementedException(); - } - } + public long Length => throw new NotImplementedException(); - public string Name - { - get - { - return _name; - } - } + public string Name => _name; - public string PhysicalPath - { - get - { - throw new NotImplementedException(); - } - } + public string PhysicalPath => "Root/" + Name; public TestFile(string name) { @@ -337,7 +296,9 @@ namespace Microsoft.Extensions.Configuration.KeyPerFile.Test throw new InvalidOperationException("Cannot create stream from directory"); } - return new MemoryStream(Encoding.UTF8.GetBytes(_contents)); + return _contents == null + ? new MemoryStream() + : new MemoryStream(Encoding.UTF8.GetBytes(_contents)); } } } diff --git a/src/Configuration.KeyPerFile/test/Microsoft.Extensions.Configuration.KeyPerFile.Tests.csproj b/src/Configuration.KeyPerFile/test/Microsoft.Extensions.Configuration.KeyPerFile.Tests.csproj index 154fd5bb62..aee7e7a7bf 100644 --- a/src/Configuration.KeyPerFile/test/Microsoft.Extensions.Configuration.KeyPerFile.Tests.csproj +++ b/src/Configuration.KeyPerFile/test/Microsoft.Extensions.Configuration.KeyPerFile.Tests.csproj @@ -1,11 +1,14 @@  - netcoreapp2.2;net461 + netcoreapp3.0;net472 - + + + + diff --git a/src/FileProviders/Directory.Build.props b/src/FileProviders/Directory.Build.props index bf4410dcb7..709c47ddbd 100644 --- a/src/FileProviders/Directory.Build.props +++ b/src/FileProviders/Directory.Build.props @@ -2,7 +2,7 @@ - true + true files;filesystem diff --git a/src/FileProviders/Embedded/Directory.Build.props b/src/FileProviders/Embedded/Directory.Build.props deleted file mode 100644 index f25c1d90ce..0000000000 --- a/src/FileProviders/Embedded/Directory.Build.props +++ /dev/null @@ -1,7 +0,0 @@ - - - - - true - - diff --git a/src/FileProviders/Embedded/ref/Microsoft.Extensions.FileProviders.Embedded.csproj b/src/FileProviders/Embedded/ref/Microsoft.Extensions.FileProviders.Embedded.csproj new file mode 100644 index 0000000000..b8f2f33387 --- /dev/null +++ b/src/FileProviders/Embedded/ref/Microsoft.Extensions.FileProviders.Embedded.csproj @@ -0,0 +1,10 @@ + + + + netstandard2.0 + + + + + + diff --git a/src/FileProviders/Embedded/ref/Microsoft.Extensions.FileProviders.Embedded.netstandard2.0.cs b/src/FileProviders/Embedded/ref/Microsoft.Extensions.FileProviders.Embedded.netstandard2.0.cs new file mode 100644 index 0000000000..1596f191fd --- /dev/null +++ b/src/FileProviders/Embedded/ref/Microsoft.Extensions.FileProviders.Embedded.netstandard2.0.cs @@ -0,0 +1,39 @@ +// 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.Extensions.FileProviders +{ + public partial class EmbeddedFileProvider : Microsoft.Extensions.FileProviders.IFileProvider + { + public EmbeddedFileProvider(System.Reflection.Assembly assembly) { } + public EmbeddedFileProvider(System.Reflection.Assembly assembly, string baseNamespace) { } + public Microsoft.Extensions.FileProviders.IDirectoryContents GetDirectoryContents(string subpath) { throw null; } + public Microsoft.Extensions.FileProviders.IFileInfo GetFileInfo(string subpath) { throw null; } + public Microsoft.Extensions.Primitives.IChangeToken Watch(string pattern) { throw null; } + } + public partial class ManifestEmbeddedFileProvider : Microsoft.Extensions.FileProviders.IFileProvider + { + public ManifestEmbeddedFileProvider(System.Reflection.Assembly assembly) { } + public ManifestEmbeddedFileProvider(System.Reflection.Assembly assembly, string root) { } + public ManifestEmbeddedFileProvider(System.Reflection.Assembly assembly, string root, System.DateTimeOffset lastModified) { } + public ManifestEmbeddedFileProvider(System.Reflection.Assembly assembly, string root, string manifestName, System.DateTimeOffset lastModified) { } + public System.Reflection.Assembly Assembly { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public Microsoft.Extensions.FileProviders.IDirectoryContents GetDirectoryContents(string subpath) { throw null; } + public Microsoft.Extensions.FileProviders.IFileInfo GetFileInfo(string subpath) { throw null; } + public Microsoft.Extensions.Primitives.IChangeToken Watch(string filter) { throw null; } + } +} +namespace Microsoft.Extensions.FileProviders.Embedded +{ + public partial class EmbeddedResourceFileInfo : Microsoft.Extensions.FileProviders.IFileInfo + { + public EmbeddedResourceFileInfo(System.Reflection.Assembly assembly, string resourcePath, string name, System.DateTimeOffset lastModified) { } + public bool Exists { get { throw null; } } + public bool IsDirectory { get { throw null; } } + public System.DateTimeOffset LastModified { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public long Length { get { throw null; } } + public string Name { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public string PhysicalPath { get { throw null; } } + public System.IO.Stream CreateReadStream() { throw null; } + } +} diff --git a/src/FileProviders/Embedded/src/Microsoft.Extensions.FileProviders.Embedded.csproj b/src/FileProviders/Embedded/src/Microsoft.Extensions.FileProviders.Embedded.csproj index 191330625a..7d84c19f9d 100644 --- a/src/FileProviders/Embedded/src/Microsoft.Extensions.FileProviders.Embedded.csproj +++ b/src/FileProviders/Embedded/src/Microsoft.Extensions.FileProviders.Embedded.csproj @@ -5,8 +5,13 @@ File provider for files in embedded resources for Microsoft.Extensions.FileProviders. netstandard2.0 $(MSBuildProjectName).nuspec + true + + + + @@ -17,35 +22,22 @@ - - + - - id=$(PackageId); - version=$(PackageVersion); - authors=$(Authors); - description=$(Description); - tags=$(PackageTags.Replace(';', ' ')); - licenseUrl=$(PackageLicenseUrl); - projectUrl=$(PackageProjectUrl); - iconUrl=$(PackageIconUrl); - repositoryUrl=$(RepositoryUrl); - repositoryCommit=$(RepositoryCommit); - copyright=$(Copyright); - targetframework=$(TargetFramework); - AssemblyName=$(AssemblyName); - - OutputBinary=@(BuiltProjectOutputGroupOutput); - OutputSymbol=@(DebugSymbolsProjectOutputGroupOutput); - OutputDocumentation=@(DocumentationProjectOutputGroupOutput); - - - TaskAssemblyNetStandard=..\..\Manifest.MSBuildTask\src\bin\$(Configuration)\netstandard1.5\$(AssemblyName).Manifest.Task.dll; - TaskSymbolNetStandard=..\..\Manifest.MSBuildTask\src\bin\$(Configuration)\netstandard1.5\$(AssemblyName).Manifest.Task.pdb; - TaskAssemblyNet461=..\..\Manifest.MSBuildTask\src\bin\$(Configuration)\net461\$(AssemblyName).Manifest.Task.dll; - TaskSymbolNet461=..\..\Manifest.MSBuildTask\src\bin\$(Configuration)\net461\$(AssemblyName).Manifest.Task.pdb; - + $(PackageTags.Replace(';',' ')) + <_OutputBinary>@(BuiltProjectOutputGroupOutput) + <_OutputSymbol>@(DebugSymbolsProjectOutputGroupOutput) + <_OutputDocumentation>@(DocumentationProjectOutputGroupOutput) + + + + + + + + + + - diff --git a/src/FileProviders/Embedded/src/Microsoft.Extensions.FileProviders.Embedded.nuspec b/src/FileProviders/Embedded/src/Microsoft.Extensions.FileProviders.Embedded.nuspec index 0cc5ed823a..4a33eb6a95 100644 --- a/src/FileProviders/Embedded/src/Microsoft.Extensions.FileProviders.Embedded.nuspec +++ b/src/FileProviders/Embedded/src/Microsoft.Extensions.FileProviders.Embedded.nuspec @@ -1,17 +1,7 @@ - $id$ - $version$ - $authors$ - true - $licenseUrl$ - $projectUrl$ - $iconUrl$ - $description$ - $copyright$ - $tags$ - + $CommonMetadataElements$ @@ -25,9 +15,7 @@ - - - - + + - \ No newline at end of file + diff --git a/src/FileProviders/Embedded/src/Properties/AssemblyInfo.cs b/src/FileProviders/Embedded/src/Properties/AssemblyInfo.cs deleted file mode 100644 index 610a7fa706..0000000000 --- a/src/FileProviders/Embedded/src/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,4 +0,0 @@ - -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("Microsoft.Extensions.FileProviders.Embedded.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] \ No newline at end of file diff --git a/src/FileProviders/Embedded/src/baseline.netcore.json b/src/FileProviders/Embedded/src/baseline.netcore.json deleted file mode 100644 index 821969ea0b..0000000000 --- a/src/FileProviders/Embedded/src/baseline.netcore.json +++ /dev/null @@ -1,343 +0,0 @@ -{ - "AssemblyIdentity": "Microsoft.Extensions.FileProviders.Embedded, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", - "Types": [ - { - "Name": "Microsoft.Extensions.FileProviders.EmbeddedFileProvider", - "Visibility": "Public", - "Kind": "Class", - "ImplementedInterfaces": [ - "Microsoft.Extensions.FileProviders.IFileProvider" - ], - "Members": [ - { - "Kind": "Method", - "Name": "GetFileInfo", - "Parameters": [ - { - "Name": "subpath", - "Type": "System.String" - } - ], - "ReturnType": "Microsoft.Extensions.FileProviders.IFileInfo", - "Sealed": true, - "Virtual": true, - "ImplementedInterface": "Microsoft.Extensions.FileProviders.IFileProvider", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "GetDirectoryContents", - "Parameters": [ - { - "Name": "subpath", - "Type": "System.String" - } - ], - "ReturnType": "Microsoft.Extensions.FileProviders.IDirectoryContents", - "Sealed": true, - "Virtual": true, - "ImplementedInterface": "Microsoft.Extensions.FileProviders.IFileProvider", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Watch", - "Parameters": [ - { - "Name": "pattern", - "Type": "System.String" - } - ], - "ReturnType": "Microsoft.Extensions.Primitives.IChangeToken", - "Sealed": true, - "Virtual": true, - "ImplementedInterface": "Microsoft.Extensions.FileProviders.IFileProvider", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [ - { - "Name": "assembly", - "Type": "System.Reflection.Assembly" - } - ], - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [ - { - "Name": "assembly", - "Type": "System.Reflection.Assembly" - }, - { - "Name": "baseNamespace", - "Type": "System.String" - } - ], - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.Extensions.FileProviders.ManifestEmbeddedFileProvider", - "Visibility": "Public", - "Kind": "Class", - "ImplementedInterfaces": [ - "Microsoft.Extensions.FileProviders.IFileProvider" - ], - "Members": [ - { - "Kind": "Method", - "Name": "get_Assembly", - "Parameters": [], - "ReturnType": "System.Reflection.Assembly", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "GetDirectoryContents", - "Parameters": [ - { - "Name": "subpath", - "Type": "System.String" - } - ], - "ReturnType": "Microsoft.Extensions.FileProviders.IDirectoryContents", - "Sealed": true, - "Virtual": true, - "ImplementedInterface": "Microsoft.Extensions.FileProviders.IFileProvider", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "GetFileInfo", - "Parameters": [ - { - "Name": "subpath", - "Type": "System.String" - } - ], - "ReturnType": "Microsoft.Extensions.FileProviders.IFileInfo", - "Sealed": true, - "Virtual": true, - "ImplementedInterface": "Microsoft.Extensions.FileProviders.IFileProvider", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Watch", - "Parameters": [ - { - "Name": "filter", - "Type": "System.String" - } - ], - "ReturnType": "Microsoft.Extensions.Primitives.IChangeToken", - "Sealed": true, - "Virtual": true, - "ImplementedInterface": "Microsoft.Extensions.FileProviders.IFileProvider", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [ - { - "Name": "assembly", - "Type": "System.Reflection.Assembly" - } - ], - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [ - { - "Name": "assembly", - "Type": "System.Reflection.Assembly" - }, - { - "Name": "root", - "Type": "System.String" - } - ], - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [ - { - "Name": "assembly", - "Type": "System.Reflection.Assembly" - }, - { - "Name": "root", - "Type": "System.String" - }, - { - "Name": "lastModified", - "Type": "System.DateTimeOffset" - } - ], - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [ - { - "Name": "assembly", - "Type": "System.Reflection.Assembly" - }, - { - "Name": "root", - "Type": "System.String" - }, - { - "Name": "manifestName", - "Type": "System.String" - }, - { - "Name": "lastModified", - "Type": "System.DateTimeOffset" - } - ], - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.Extensions.FileProviders.Embedded.EmbeddedResourceFileInfo", - "Visibility": "Public", - "Kind": "Class", - "ImplementedInterfaces": [ - "Microsoft.Extensions.FileProviders.IFileInfo" - ], - "Members": [ - { - "Kind": "Method", - "Name": "get_Exists", - "Parameters": [], - "ReturnType": "System.Boolean", - "Sealed": true, - "Virtual": true, - "ImplementedInterface": "Microsoft.Extensions.FileProviders.IFileInfo", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "get_Length", - "Parameters": [], - "ReturnType": "System.Int64", - "Sealed": true, - "Virtual": true, - "ImplementedInterface": "Microsoft.Extensions.FileProviders.IFileInfo", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "get_PhysicalPath", - "Parameters": [], - "ReturnType": "System.String", - "Sealed": true, - "Virtual": true, - "ImplementedInterface": "Microsoft.Extensions.FileProviders.IFileInfo", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "get_Name", - "Parameters": [], - "ReturnType": "System.String", - "Sealed": true, - "Virtual": true, - "ImplementedInterface": "Microsoft.Extensions.FileProviders.IFileInfo", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "get_LastModified", - "Parameters": [], - "ReturnType": "System.DateTimeOffset", - "Sealed": true, - "Virtual": true, - "ImplementedInterface": "Microsoft.Extensions.FileProviders.IFileInfo", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "get_IsDirectory", - "Parameters": [], - "ReturnType": "System.Boolean", - "Sealed": true, - "Virtual": true, - "ImplementedInterface": "Microsoft.Extensions.FileProviders.IFileInfo", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "CreateReadStream", - "Parameters": [], - "ReturnType": "System.IO.Stream", - "Sealed": true, - "Virtual": true, - "ImplementedInterface": "Microsoft.Extensions.FileProviders.IFileInfo", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [ - { - "Name": "assembly", - "Type": "System.Reflection.Assembly" - }, - { - "Name": "resourcePath", - "Type": "System.String" - }, - { - "Name": "name", - "Type": "System.String" - }, - { - "Name": "lastModified", - "Type": "System.DateTimeOffset" - } - ], - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - } - ] -} \ No newline at end of file diff --git a/src/FileProviders/Embedded/src/build/netstandard2.0/Microsoft.Extensions.FileProviders.Embedded.props b/src/FileProviders/Embedded/src/build/netstandard2.0/Microsoft.Extensions.FileProviders.Embedded.props index e913e17321..aabbabc92f 100644 --- a/src/FileProviders/Embedded/src/build/netstandard2.0/Microsoft.Extensions.FileProviders.Embedded.props +++ b/src/FileProviders/Embedded/src/build/netstandard2.0/Microsoft.Extensions.FileProviders.Embedded.props @@ -5,9 +5,7 @@ - <_FileProviderTaskFolder Condition="'$(MSBuildRuntimeType)' == 'Core'">netstandard1.5 - <_FileProviderTaskFolder Condition="'$(MSBuildRuntimeType)' != 'Core'">net461 - <_FileProviderTaskAssembly>$(MSBuildThisFileDirectory)..\..\tasks\$(_FileProviderTaskFolder)\Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.dll + <_FileProviderTaskAssembly>$(MSBuildThisFileDirectory)..\..\tasks\netstandard2.0\Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.dll - netcoreapp2.2;net461 + netcoreapp3.0;net472 diff --git a/src/FileProviders/Manifest.MSBuildTask/src/Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.csproj b/src/FileProviders/Manifest.MSBuildTask/src/Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.csproj index e6c42b46f1..cdc4ffdcb0 100644 --- a/src/FileProviders/Manifest.MSBuildTask/src/Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.csproj +++ b/src/FileProviders/Manifest.MSBuildTask/src/Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.csproj @@ -3,24 +3,16 @@ MSBuild task to generate a manifest that can be used by Microsoft.Extensions.FileProviders.Embedded to preserve metadata of the files embedded in the assembly at compilation time. - netstandard1.5;net461 + netstandard2.0 false - false + true false false - - - - - - - - - - + + diff --git a/src/FileProviders/Manifest.MSBuildTask/test/Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.Test.csproj b/src/FileProviders/Manifest.MSBuildTask/test/Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.Tests.csproj similarity index 51% rename from src/FileProviders/Manifest.MSBuildTask/test/Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.Test.csproj rename to src/FileProviders/Manifest.MSBuildTask/test/Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.Tests.csproj index 8be54bf05d..ed68958fe8 100644 --- a/src/FileProviders/Manifest.MSBuildTask/test/Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.Test.csproj +++ b/src/FileProviders/Manifest.MSBuildTask/test/Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp2.2;net461 + netcoreapp3.0 @@ -9,16 +9,8 @@ - - - - - - - - diff --git a/src/HealthChecks/Abstractions/Directory.Build.props b/src/HealthChecks/Abstractions/Directory.Build.props deleted file mode 100644 index f25c1d90ce..0000000000 --- a/src/HealthChecks/Abstractions/Directory.Build.props +++ /dev/null @@ -1,7 +0,0 @@ - - - - - true - - diff --git a/src/HealthChecks/Abstractions/ref/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions.csproj b/src/HealthChecks/Abstractions/ref/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions.csproj new file mode 100644 index 0000000000..be23858955 --- /dev/null +++ b/src/HealthChecks/Abstractions/ref/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions.csproj @@ -0,0 +1,10 @@ + + + + netstandard2.0 + + + + + + diff --git a/src/HealthChecks/Abstractions/ref/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions.netstandard2.0.cs b/src/HealthChecks/Abstractions/ref/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions.netstandard2.0.cs new file mode 100644 index 0000000000..9ab497257e --- /dev/null +++ b/src/HealthChecks/Abstractions/ref/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions.netstandard2.0.cs @@ -0,0 +1,70 @@ +// 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.Extensions.Diagnostics.HealthChecks +{ + public sealed partial class HealthCheckContext + { + public HealthCheckContext() { } + public Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckRegistration Registration { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + } + public sealed partial class HealthCheckRegistration + { + public HealthCheckRegistration(string name, Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck instance, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus, System.Collections.Generic.IEnumerable tags) { } + public HealthCheckRegistration(string name, Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck instance, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus, System.Collections.Generic.IEnumerable tags, System.TimeSpan? timeout) { } + public HealthCheckRegistration(string name, System.Func factory, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus, System.Collections.Generic.IEnumerable tags) { } + public HealthCheckRegistration(string name, System.Func factory, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus, System.Collections.Generic.IEnumerable tags, System.TimeSpan? timeout) { } + public System.Func Factory { get { throw null; } set { } } + public Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus FailureStatus { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public string Name { get { throw null; } set { } } + public System.Collections.Generic.ISet Tags { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public System.TimeSpan Timeout { get { throw null; } set { } } + } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public partial struct HealthCheckResult + { + private object _dummy; + private int _dummyPrimitive; + public HealthCheckResult(Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus status, string description = null, System.Exception exception = null, System.Collections.Generic.IReadOnlyDictionary data = null) { throw null; } + public System.Collections.Generic.IReadOnlyDictionary Data { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public string Description { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public System.Exception Exception { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus Status { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public static Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult Degraded(string description = null, System.Exception exception = null, System.Collections.Generic.IReadOnlyDictionary data = null) { throw null; } + public static Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult Healthy(string description = null, System.Collections.Generic.IReadOnlyDictionary data = null) { throw null; } + public static Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult Unhealthy(string description = null, System.Exception exception = null, System.Collections.Generic.IReadOnlyDictionary data = null) { throw null; } + } + public sealed partial class HealthReport + { + public HealthReport(System.Collections.Generic.IReadOnlyDictionary entries, System.TimeSpan totalDuration) { } + public System.Collections.Generic.IReadOnlyDictionary Entries { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus Status { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public System.TimeSpan TotalDuration { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public partial struct HealthReportEntry + { + private object _dummy; + private int _dummyPrimitive; + public HealthReportEntry(Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus status, string description, System.TimeSpan duration, System.Exception exception, System.Collections.Generic.IReadOnlyDictionary data) { throw null; } + public System.Collections.Generic.IReadOnlyDictionary Data { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public string Description { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public System.TimeSpan Duration { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public System.Exception Exception { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus Status { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + } + public enum HealthStatus + { + Degraded = 1, + Healthy = 2, + Unhealthy = 0, + } + public partial interface IHealthCheck + { + System.Threading.Tasks.Task CheckHealthAsync(Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckContext context, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + } + public partial interface IHealthCheckPublisher + { + System.Threading.Tasks.Task PublishAsync(Microsoft.Extensions.Diagnostics.HealthChecks.HealthReport report, System.Threading.CancellationToken cancellationToken); + } +} diff --git a/src/HealthChecks/Abstractions/src/HealthCheckRegistration.cs b/src/HealthChecks/Abstractions/src/HealthCheckRegistration.cs index 9291c38846..8ee11e3195 100644 --- a/src/HealthChecks/Abstractions/src/HealthCheckRegistration.cs +++ b/src/HealthChecks/Abstractions/src/HealthCheckRegistration.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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; @@ -24,6 +24,22 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks { private Func _factory; private string _name; + private TimeSpan _timeout; + + /// + /// Creates a new for an existing instance. + /// + /// The health check name. + /// The instance. + /// + /// The that should be reported upon failure of the health check. If the provided value + /// is null, then will be reported. + /// + /// A list of tags that can be used for filtering health checks. + public HealthCheckRegistration(string name, IHealthCheck instance, HealthStatus? failureStatus, IEnumerable tags) + : this(name, instance, failureStatus, tags, default) + { + } /// /// Creates a new for an existing instance. @@ -35,7 +51,8 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks /// is null, then will be reported. /// /// A list of tags that can be used for filtering health checks. - public HealthCheckRegistration(string name, IHealthCheck instance, HealthStatus? failureStatus, IEnumerable tags) + /// An optional representing the timeout of the check. + public HealthCheckRegistration(string name, IHealthCheck instance, HealthStatus? failureStatus, IEnumerable tags, TimeSpan? timeout) { if (name == null) { @@ -47,10 +64,16 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks throw new ArgumentNullException(nameof(instance)); } + if (timeout <= TimeSpan.Zero && timeout != System.Threading.Timeout.InfiniteTimeSpan) + { + throw new ArgumentOutOfRangeException(nameof(timeout)); + } + Name = name; FailureStatus = failureStatus ?? HealthStatus.Unhealthy; Tags = new HashSet(tags ?? Array.Empty(), StringComparer.OrdinalIgnoreCase); Factory = (_) => instance; + Timeout = timeout ?? System.Threading.Timeout.InfiniteTimeSpan; } /// @@ -68,6 +91,27 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks Func factory, HealthStatus? failureStatus, IEnumerable tags) + : this(name, factory, failureStatus, tags, default) + { + } + + /// + /// Creates a new for an existing instance. + /// + /// The health check name. + /// A delegate used to create the instance. + /// + /// The that should be reported when the health check reports a failure. If the provided value + /// is null, then will be reported. + /// + /// A list of tags that can be used for filtering health checks. + /// An optional representing the timeout of the check. + public HealthCheckRegistration( + string name, + Func factory, + HealthStatus? failureStatus, + IEnumerable tags, + TimeSpan? timeout) { if (name == null) { @@ -79,10 +123,16 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks throw new ArgumentNullException(nameof(factory)); } + if (timeout <= TimeSpan.Zero && timeout != System.Threading.Timeout.InfiniteTimeSpan) + { + throw new ArgumentOutOfRangeException(nameof(timeout)); + } + Name = name; FailureStatus = failureStatus ?? HealthStatus.Unhealthy; Tags = new HashSet(tags ?? Array.Empty(), StringComparer.OrdinalIgnoreCase); Factory = factory; + Timeout = timeout ?? System.Threading.Timeout.InfiniteTimeSpan; } /// @@ -107,6 +157,23 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks /// public HealthStatus FailureStatus { get; set; } + /// + /// Gets or sets the timeout used for the test. + /// + public TimeSpan Timeout + { + get => _timeout; + set + { + if (value <= TimeSpan.Zero && value != System.Threading.Timeout.InfiniteTimeSpan) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + _timeout = value; + } + } + /// /// Gets or sets the health check name. /// diff --git a/src/HealthChecks/Abstractions/src/HealthCheckResult.cs b/src/HealthChecks/Abstractions/src/HealthCheckResult.cs index e01cb5aceb..7f4522da19 100644 --- a/src/HealthChecks/Abstractions/src/HealthCheckResult.cs +++ b/src/HealthChecks/Abstractions/src/HealthCheckResult.cs @@ -70,7 +70,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks /// A representing a degraged component. public static HealthCheckResult Degraded(string description = null, Exception exception = null, IReadOnlyDictionary data = null) { - return new HealthCheckResult(status: HealthStatus.Degraded, description, exception: null, data); + return new HealthCheckResult(status: HealthStatus.Degraded, description, exception: exception, data); } /// diff --git a/src/HealthChecks/Abstractions/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions.csproj b/src/HealthChecks/Abstractions/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions.csproj index b95d66f7b3..2bba5959a3 100644 --- a/src/HealthChecks/Abstractions/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions.csproj +++ b/src/HealthChecks/Abstractions/src/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions.csproj @@ -11,6 +11,7 @@ Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck $(NoWarn);CS1591 true diagnostics;healthchecks + true diff --git a/src/HealthChecks/Abstractions/src/baseline.netcore.json b/src/HealthChecks/Abstractions/src/baseline.netcore.json deleted file mode 100644 index 871db4c089..0000000000 --- a/src/HealthChecks/Abstractions/src/baseline.netcore.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "AssemblyIdentity": "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", - "Types": [ - ] -} \ No newline at end of file diff --git a/src/HealthChecks/HealthChecks/Directory.Build.props b/src/HealthChecks/HealthChecks/Directory.Build.props deleted file mode 100644 index f25c1d90ce..0000000000 --- a/src/HealthChecks/HealthChecks/Directory.Build.props +++ /dev/null @@ -1,7 +0,0 @@ - - - - - true - - diff --git a/src/HealthChecks/HealthChecks/ref/Microsoft.Extensions.Diagnostics.HealthChecks.csproj b/src/HealthChecks/HealthChecks/ref/Microsoft.Extensions.Diagnostics.HealthChecks.csproj new file mode 100644 index 0000000000..277e60910f --- /dev/null +++ b/src/HealthChecks/HealthChecks/ref/Microsoft.Extensions.Diagnostics.HealthChecks.csproj @@ -0,0 +1,12 @@ + + + + netstandard2.0 + + + + + + + + diff --git a/src/HealthChecks/HealthChecks/ref/Microsoft.Extensions.Diagnostics.HealthChecks.netstandard2.0.cs b/src/HealthChecks/HealthChecks/ref/Microsoft.Extensions.Diagnostics.HealthChecks.netstandard2.0.cs new file mode 100644 index 0000000000..a23961efdd --- /dev/null +++ b/src/HealthChecks/HealthChecks/ref/Microsoft.Extensions.Diagnostics.HealthChecks.netstandard2.0.cs @@ -0,0 +1,59 @@ +// 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.Extensions.DependencyInjection +{ + public static partial class HealthChecksBuilderAddCheckExtensions + { + public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck instance, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus, System.Collections.Generic.IEnumerable tags) { throw null; } + public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck instance, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus = default(Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus?), System.Collections.Generic.IEnumerable tags = null, System.TimeSpan? timeout = default(System.TimeSpan?)) { throw null; } + public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus, System.Collections.Generic.IEnumerable tags) where T : class, Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck { throw null; } + public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus = default(Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus?), System.Collections.Generic.IEnumerable tags = null, System.TimeSpan? timeout = default(System.TimeSpan?)) where T : class, Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck { throw null; } + public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddTypeActivatedCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus, System.Collections.Generic.IEnumerable tags, params object[] args) where T : class, Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck { throw null; } + public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddTypeActivatedCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus, System.Collections.Generic.IEnumerable tags, System.TimeSpan timeout, params object[] args) where T : class, Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck { throw null; } + public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddTypeActivatedCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus, params object[] args) where T : class, Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck { throw null; } + public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddTypeActivatedCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, params object[] args) where T : class, Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck { throw null; } + } + public static partial class HealthChecksBuilderDelegateExtensions + { + public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddAsyncCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, System.Func> check, System.Collections.Generic.IEnumerable tags) { throw null; } + public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddAsyncCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, System.Func> check, System.Collections.Generic.IEnumerable tags = null, System.TimeSpan? timeout = default(System.TimeSpan?)) { throw null; } + public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddAsyncCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, System.Func> check, System.Collections.Generic.IEnumerable tags) { throw null; } + public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddAsyncCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, System.Func> check, System.Collections.Generic.IEnumerable tags = null, System.TimeSpan? timeout = default(System.TimeSpan?)) { throw null; } + public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, System.Func check, System.Collections.Generic.IEnumerable tags) { throw null; } + public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, System.Func check, System.Collections.Generic.IEnumerable tags = null, System.TimeSpan? timeout = default(System.TimeSpan?)) { throw null; } + public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, System.Func check, System.Collections.Generic.IEnumerable tags) { throw null; } + public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, System.Func check, System.Collections.Generic.IEnumerable tags = null, System.TimeSpan? timeout = default(System.TimeSpan?)) { throw null; } + } + public static partial class HealthCheckServiceCollectionExtensions + { + public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddHealthChecks(this Microsoft.Extensions.DependencyInjection.IServiceCollection services) { throw null; } + } + public partial interface IHealthChecksBuilder + { + Microsoft.Extensions.DependencyInjection.IServiceCollection Services { get; } + Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder Add(Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckRegistration registration); + } +} +namespace Microsoft.Extensions.Diagnostics.HealthChecks +{ + public sealed partial class HealthCheckPublisherOptions + { + public HealthCheckPublisherOptions() { } + public System.TimeSpan Delay { get { throw null; } set { } } + public System.TimeSpan Period { get { throw null; } set { } } + public System.Func Predicate { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public System.TimeSpan Timeout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + } + public abstract partial class HealthCheckService + { + protected HealthCheckService() { } + public abstract System.Threading.Tasks.Task CheckHealthAsync(System.Func predicate, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + public System.Threading.Tasks.Task CheckHealthAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + } + public sealed partial class HealthCheckServiceOptions + { + public HealthCheckServiceOptions() { } + public System.Collections.Generic.ICollection Registrations { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + } +} diff --git a/src/HealthChecks/HealthChecks/src/DefaultHealthCheckService.cs b/src/HealthChecks/HealthChecks/src/DefaultHealthCheckService.cs index d5d71d9cb4..c2c9084e0a 100644 --- a/src/HealthChecks/HealthChecks/src/DefaultHealthCheckService.cs +++ b/src/HealthChecks/HealthChecks/src/DefaultHealthCheckService.cs @@ -40,74 +40,115 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks CancellationToken cancellationToken = default) { var registrations = _options.Value.Registrations; + if (predicate != null) + { + registrations = registrations.Where(predicate).ToArray(); + } + var totalTime = ValueStopwatch.StartNew(); + Log.HealthCheckProcessingBegin(_logger); + + var tasks = new Task[registrations.Count]; + var index = 0; using (var scope = _scopeFactory.CreateScope()) { - var context = new HealthCheckContext(); - var entries = new Dictionary(StringComparer.OrdinalIgnoreCase); - - var totalTime = ValueStopwatch.StartNew(); - Log.HealthCheckProcessingBegin(_logger); - foreach (var registration in registrations) { - if (predicate != null && !predicate(registration)) - { - continue; - } - - cancellationToken.ThrowIfCancellationRequested(); - - var healthCheck = registration.Factory(scope.ServiceProvider); - - // If the health check does things like make Database queries using EF or backend HTTP calls, - // it may be valuable to know that logs it generates are part of a health check. So we start a scope. - using (_logger.BeginScope(new HealthCheckLogScope(registration.Name))) - { - var stopwatch = ValueStopwatch.StartNew(); - context.Registration = registration; - - Log.HealthCheckBegin(_logger, registration); - - HealthReportEntry entry; - try - { - var result = await healthCheck.CheckHealthAsync(context, cancellationToken); - var duration = stopwatch.GetElapsedTime(); - - entry = new HealthReportEntry( - status: result.Status, - description: result.Description, - duration: duration, - exception: result.Exception, - data: result.Data); - - Log.HealthCheckEnd(_logger, registration, entry, duration); - Log.HealthCheckData(_logger, registration, entry); - } - - // Allow cancellation to propagate. - catch (Exception ex) when (ex as OperationCanceledException == null) - { - var duration = stopwatch.GetElapsedTime(); - entry = new HealthReportEntry( - status: HealthStatus.Unhealthy, - description: ex.Message, - duration: duration, - exception: ex, - data: null); - - Log.HealthCheckError(_logger, registration, ex, duration); - } - - entries[registration.Name] = entry; - } + tasks[index++] = Task.Run(() => RunCheckAsync(scope, registration, cancellationToken), cancellationToken); } - var totalElapsedTime = totalTime.GetElapsedTime(); - var report = new HealthReport(entries, totalElapsedTime); - Log.HealthCheckProcessingEnd(_logger, report.Status, totalElapsedTime); - return report; + await Task.WhenAll(tasks).ConfigureAwait(false); + } + + index = 0; + var entries = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var registration in registrations) + { + entries[registration.Name] = tasks[index++].Result; + } + + var totalElapsedTime = totalTime.GetElapsedTime(); + var report = new HealthReport(entries, totalElapsedTime); + Log.HealthCheckProcessingEnd(_logger, report.Status, totalElapsedTime); + return report; + } + + private async Task RunCheckAsync(IServiceScope scope, HealthCheckRegistration registration, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var healthCheck = registration.Factory(scope.ServiceProvider); + + // If the health check does things like make Database queries using EF or backend HTTP calls, + // it may be valuable to know that logs it generates are part of a health check. So we start a scope. + using (_logger.BeginScope(new HealthCheckLogScope(registration.Name))) + { + var stopwatch = ValueStopwatch.StartNew(); + var context = new HealthCheckContext { Registration = registration }; + + Log.HealthCheckBegin(_logger, registration); + + HealthReportEntry entry; + CancellationTokenSource timeoutCancellationTokenSource = null; + try + { + HealthCheckResult result; + + var checkCancellationToken = cancellationToken; + if (registration.Timeout > TimeSpan.Zero) + { + timeoutCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + timeoutCancellationTokenSource.CancelAfter(registration.Timeout); + checkCancellationToken = timeoutCancellationTokenSource.Token; + } + + result = await healthCheck.CheckHealthAsync(context, checkCancellationToken).ConfigureAwait(false); + + var duration = stopwatch.GetElapsedTime(); + + entry = new HealthReportEntry( + status: result.Status, + description: result.Description, + duration: duration, + exception: result.Exception, + data: result.Data); + + Log.HealthCheckEnd(_logger, registration, entry, duration); + Log.HealthCheckData(_logger, registration, entry); + } + catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested) + { + var duration = stopwatch.GetElapsedTime(); + entry = new HealthReportEntry( + status: HealthStatus.Unhealthy, + description: "A timeout occured while running check.", + duration: duration, + exception: ex, + data: null); + + Log.HealthCheckError(_logger, registration, ex, duration); + } + + // Allow cancellation to propagate if it's not a timeout. + catch (Exception ex) when (ex as OperationCanceledException == null) + { + var duration = stopwatch.GetElapsedTime(); + entry = new HealthReportEntry( + status: HealthStatus.Unhealthy, + description: ex.Message, + duration: duration, + exception: ex, + data: null); + + Log.HealthCheckError(_logger, registration, ex, duration); + } + + finally + { + timeoutCancellationTokenSource?.Dispose(); + } + + return entry; } } diff --git a/src/HealthChecks/HealthChecks/src/DependencyInjection/HealthCheckServiceCollectionExtensions.cs b/src/HealthChecks/HealthChecks/src/DependencyInjection/HealthCheckServiceCollectionExtensions.cs index d6df03d2ae..91ffa59449 100644 --- a/src/HealthChecks/HealthChecks/src/DependencyInjection/HealthCheckServiceCollectionExtensions.cs +++ b/src/HealthChecks/HealthChecks/src/DependencyInjection/HealthCheckServiceCollectionExtensions.cs @@ -26,7 +26,7 @@ namespace Microsoft.Extensions.DependencyInjection public static IHealthChecksBuilder AddHealthChecks(this IServiceCollection services) { services.TryAddSingleton(); - services.TryAddSingleton(); + services.TryAddEnumerable(ServiceDescriptor.Singleton()); return new HealthChecksBuilder(services); } } diff --git a/src/HealthChecks/HealthChecks/src/DependencyInjection/HealthChecksBuilderAddCheckExtensions.cs b/src/HealthChecks/HealthChecks/src/DependencyInjection/HealthChecksBuilderAddCheckExtensions.cs index 9508889054..51b7815438 100644 --- a/src/HealthChecks/HealthChecks/src/DependencyInjection/HealthChecksBuilderAddCheckExtensions.cs +++ b/src/HealthChecks/HealthChecks/src/DependencyInjection/HealthChecksBuilderAddCheckExtensions.cs @@ -24,12 +24,37 @@ namespace Microsoft.Extensions.DependencyInjection /// /// A list of tags that can be used to filter health checks. /// The . + // 2.0 BACKCOMPAT OVERLOAD -- DO NOT TOUCH + public static IHealthChecksBuilder AddCheck( + this IHealthChecksBuilder builder, + string name, + IHealthCheck instance, + HealthStatus? failureStatus, + IEnumerable tags) + { + return AddCheck(builder, name, instance, failureStatus, tags, default); + } + + /// + /// Adds a new health check with the specified name and implementation. + /// + /// The . + /// The name of the health check. + /// An instance. + /// + /// The that should be reported when the health check reports a failure. If the provided value + /// is null, then will be reported. + /// + /// A list of tags that can be used to filter health checks. + /// An optional representing the timeout of the check. + /// The . public static IHealthChecksBuilder AddCheck( this IHealthChecksBuilder builder, string name, IHealthCheck instance, HealthStatus? failureStatus = null, - IEnumerable tags = null) + IEnumerable tags = null, + TimeSpan? timeout = null) { if (builder == null) { @@ -46,7 +71,7 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentNullException(nameof(instance)); } - return builder.Add(new HealthCheckRegistration(name, instance, failureStatus, tags)); + return builder.Add(new HealthCheckRegistration(name, instance, failureStatus, tags, timeout)); } /// @@ -63,15 +88,45 @@ namespace Microsoft.Extensions.DependencyInjection /// The . /// /// This method will use to create the health check - /// instance when needed. If a service of type is registred in the dependency injection container - /// with any liftime it will be used. Otherwise an instance of type will be constructed with + /// instance when needed. If a service of type is registered in the dependency injection container + /// with any lifetime it will be used. Otherwise an instance of type will be constructed with + /// access to services from the dependency injection container. + /// + // 2.0 BACKCOMPAT OVERLOAD -- DO NOT TOUCH + public static IHealthChecksBuilder AddCheck( + this IHealthChecksBuilder builder, + string name, + HealthStatus? failureStatus, + IEnumerable tags) where T : class, IHealthCheck + { + return AddCheck(builder, name, failureStatus, tags, default); + } + + /// + /// Adds a new health check with the specified name and implementation. + /// + /// The health check implementation type. + /// The . + /// The name of the health check. + /// + /// The that should be reported when the health check reports a failure. If the provided value + /// is null, then will be reported. + /// + /// A list of tags that can be used to filter health checks. + /// An optional representing the timeout of the check. + /// The . + /// + /// This method will use to create the health check + /// instance when needed. If a service of type is registered in the dependency injection container + /// with any lifetime it will be used. Otherwise an instance of type will be constructed with /// access to services from the dependency injection container. /// public static IHealthChecksBuilder AddCheck( this IHealthChecksBuilder builder, string name, HealthStatus? failureStatus = null, - IEnumerable tags = null) where T : class, IHealthCheck + IEnumerable tags = null, + TimeSpan? timeout = null) where T : class, IHealthCheck { if (builder == null) { @@ -83,7 +138,7 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentNullException(nameof(name)); } - return builder.Add(new HealthCheckRegistration(name, s => ActivatorUtilities.GetServiceOrCreateInstance(s), failureStatus, tags)); + return builder.Add(new HealthCheckRegistration(name, s => ActivatorUtilities.GetServiceOrCreateInstance(s), failureStatus, tags, timeout)); } // NOTE: AddTypeActivatedCheck has overloads rather than default parameters values, because default parameter values don't @@ -113,7 +168,7 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentNullException(nameof(name)); } - return AddTypeActivatedCheck(builder, name, failureStatus: null, tags: null); + return AddTypeActivatedCheck(builder, name, failureStatus: null, tags: null, args); } /// @@ -148,7 +203,7 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentNullException(nameof(name)); } - return AddTypeActivatedCheck(builder, name, failureStatus, tags: null); + return AddTypeActivatedCheck(builder, name, failureStatus, tags: null, args); } /// @@ -187,5 +242,44 @@ namespace Microsoft.Extensions.DependencyInjection return builder.Add(new HealthCheckRegistration(name, s => ActivatorUtilities.CreateInstance(s, args), failureStatus, tags)); } + + /// + /// Adds a new type activated health check with the specified name and implementation. + /// + /// The health check implementation type. + /// The . + /// The name of the health check. + /// + /// The that should be reported when the health check reports a failure. If the provided value + /// is null, then will be reported. + /// + /// A list of tags that can be used to filter health checks. + /// Additional arguments to provide to the constructor. + /// A representing the timeout of the check. + /// The . + /// + /// This method will use to create the health check + /// instance when needed. Additional arguments can be provided to the constructor via . + /// + public static IHealthChecksBuilder AddTypeActivatedCheck( + this IHealthChecksBuilder builder, + string name, + HealthStatus? failureStatus, + IEnumerable tags, + TimeSpan timeout, + params object[] args) where T : class, IHealthCheck + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + return builder.Add(new HealthCheckRegistration(name, s => ActivatorUtilities.CreateInstance(s, args), failureStatus, tags, timeout)); + } } } diff --git a/src/HealthChecks/HealthChecks/src/DependencyInjection/HealthChecksBuilderDelegateExtensions.cs b/src/HealthChecks/HealthChecks/src/DependencyInjection/HealthChecksBuilderDelegateExtensions.cs index d7dfdd90ae..ba27ab5554 100644 --- a/src/HealthChecks/HealthChecks/src/DependencyInjection/HealthChecksBuilderDelegateExtensions.cs +++ b/src/HealthChecks/HealthChecks/src/DependencyInjection/HealthChecksBuilderDelegateExtensions.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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; @@ -22,11 +22,31 @@ namespace Microsoft.Extensions.DependencyInjection /// A list of tags that can be used to filter health checks. /// A delegate that provides the health check implementation. /// The . + // 2.0 BACKCOMPAT OVERLOAD -- DO NOT TOUCH public static IHealthChecksBuilder AddCheck( this IHealthChecksBuilder builder, string name, Func check, - IEnumerable tags = null) + IEnumerable tags) + { + return AddCheck(builder, name, check, tags, default); + } + + /// + /// Adds a new health check with the specified name and implementation. + /// + /// The . + /// The name of the health check. + /// A list of tags that can be used to filter health checks. + /// A delegate that provides the health check implementation. + /// An optional representing the timeout of the check. + /// The . + public static IHealthChecksBuilder AddCheck( + this IHealthChecksBuilder builder, + string name, + Func check, + IEnumerable tags = null, + TimeSpan? timeout = default) { if (builder == null) { @@ -44,7 +64,7 @@ namespace Microsoft.Extensions.DependencyInjection } var instance = new DelegateHealthCheck((ct) => Task.FromResult(check())); - return builder.Add(new HealthCheckRegistration(name, instance, failureStatus: null, tags)); + return builder.Add(new HealthCheckRegistration(name, instance, failureStatus: null, tags, timeout)); } /// @@ -55,11 +75,31 @@ namespace Microsoft.Extensions.DependencyInjection /// A list of tags that can be used to filter health checks. /// A delegate that provides the health check implementation. /// The . + // 2.0 BACKCOMPAT OVERLOAD -- DO NOT TOUCH public static IHealthChecksBuilder AddCheck( this IHealthChecksBuilder builder, string name, Func check, - IEnumerable tags = null) + IEnumerable tags) + { + return AddCheck(builder, name, check, tags, default); + } + + /// + /// Adds a new health check with the specified name and implementation. + /// + /// The . + /// The name of the health check. + /// A list of tags that can be used to filter health checks. + /// A delegate that provides the health check implementation. + /// An optional representing the timeout of the check. + /// The . + public static IHealthChecksBuilder AddCheck( + this IHealthChecksBuilder builder, + string name, + Func check, + IEnumerable tags = null, + TimeSpan? timeout = default) { if (builder == null) { @@ -77,7 +117,7 @@ namespace Microsoft.Extensions.DependencyInjection } var instance = new DelegateHealthCheck((ct) => Task.FromResult(check(ct))); - return builder.Add(new HealthCheckRegistration(name, instance, failureStatus: null, tags)); + return builder.Add(new HealthCheckRegistration(name, instance, failureStatus: null, tags, timeout)); } /// @@ -88,11 +128,31 @@ namespace Microsoft.Extensions.DependencyInjection /// A list of tags that can be used to filter health checks. /// A delegate that provides the health check implementation. /// The . + // 2.0 BACKCOMPAT OVERLOAD -- DO NOT TOUCH public static IHealthChecksBuilder AddAsyncCheck( this IHealthChecksBuilder builder, string name, Func> check, - IEnumerable tags = null) + IEnumerable tags) + { + return AddAsyncCheck(builder, name, check, tags, default); + } + + /// + /// Adds a new health check with the specified name and implementation. + /// + /// The . + /// The name of the health check. + /// A list of tags that can be used to filter health checks. + /// A delegate that provides the health check implementation. + /// An optional representing the timeout of the check. + /// The . + public static IHealthChecksBuilder AddAsyncCheck( + this IHealthChecksBuilder builder, + string name, + Func> check, + IEnumerable tags = null, + TimeSpan? timeout = default) { if (builder == null) { @@ -110,7 +170,7 @@ namespace Microsoft.Extensions.DependencyInjection } var instance = new DelegateHealthCheck((ct) => check()); - return builder.Add(new HealthCheckRegistration(name, instance, failureStatus: null, tags)); + return builder.Add(new HealthCheckRegistration(name, instance, failureStatus: null, tags, timeout)); } /// @@ -121,11 +181,31 @@ namespace Microsoft.Extensions.DependencyInjection /// A list of tags that can be used to filter health checks. /// A delegate that provides the health check implementation. /// The . + // 2.0 BACKCOMPAT OVERLOAD -- DO NOT TOUCH public static IHealthChecksBuilder AddAsyncCheck( this IHealthChecksBuilder builder, string name, Func> check, - IEnumerable tags = null) + IEnumerable tags) + { + return AddAsyncCheck(builder, name, check, tags, default); + } + + /// + /// Adds a new health check with the specified name and implementation. + /// + /// The . + /// The name of the health check. + /// A list of tags that can be used to filter health checks. + /// A delegate that provides the health check implementation. + /// An optional representing the timeout of the check. + /// The . + public static IHealthChecksBuilder AddAsyncCheck( + this IHealthChecksBuilder builder, + string name, + Func> check, + IEnumerable tags = null, + TimeSpan? timeout = default) { if (builder == null) { @@ -143,7 +223,7 @@ namespace Microsoft.Extensions.DependencyInjection } var instance = new DelegateHealthCheck((ct) => check(ct)); - return builder.Add(new HealthCheckRegistration(name, instance, failureStatus: null, tags)); + return builder.Add(new HealthCheckRegistration(name, instance, failureStatus: null, tags, timeout)); } } } diff --git a/src/HealthChecks/HealthChecks/src/HealthCheckPublisherOptions.cs b/src/HealthChecks/HealthChecks/src/HealthCheckPublisherOptions.cs index 1313718af8..6b7c8c3365 100644 --- a/src/HealthChecks/HealthChecks/src/HealthCheckPublisherOptions.cs +++ b/src/HealthChecks/HealthChecks/src/HealthCheckPublisherOptions.cs @@ -60,7 +60,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks throw new ArgumentException($"The {nameof(Period)} must not be infinite.", nameof(value)); } - _delay = value; + _period = value; } } diff --git a/src/HealthChecks/HealthChecks/src/Microsoft.Extensions.Diagnostics.HealthChecks.csproj b/src/HealthChecks/HealthChecks/src/Microsoft.Extensions.Diagnostics.HealthChecks.csproj index d0b1c97ef0..463e5b3632 100644 --- a/src/HealthChecks/HealthChecks/src/Microsoft.Extensions.Diagnostics.HealthChecks.csproj +++ b/src/HealthChecks/HealthChecks/src/Microsoft.Extensions.Diagnostics.HealthChecks.csproj @@ -10,8 +10,13 @@ Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder $(NoWarn);CS1591 true diagnostics;healthchecks + true + + + + diff --git a/src/HealthChecks/HealthChecks/src/Properties/AssemblyInfo.cs b/src/HealthChecks/HealthChecks/src/Properties/AssemblyInfo.cs deleted file mode 100644 index 13e969bfad..0000000000 --- a/src/HealthChecks/HealthChecks/src/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,3 +0,0 @@ -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("Microsoft.Extensions.Diagnostics.HealthChecks.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] \ No newline at end of file diff --git a/src/HealthChecks/HealthChecks/src/baseline.netcore.json b/src/HealthChecks/HealthChecks/src/baseline.netcore.json deleted file mode 100644 index cb2fe053f1..0000000000 --- a/src/HealthChecks/HealthChecks/src/baseline.netcore.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "AssemblyIdentity": "Microsoft.Extensions.Diagnostics.HealthChecks, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", - "Types": [ - ] -} \ No newline at end of file diff --git a/src/HealthChecks/HealthChecks/test/DefaultHealthCheckServiceTest.cs b/src/HealthChecks/HealthChecks/test/DefaultHealthCheckServiceTest.cs index 9ab991204e..38442edb93 100644 --- a/src/HealthChecks/HealthChecks/test/DefaultHealthCheckServiceTest.cs +++ b/src/HealthChecks/HealthChecks/test/DefaultHealthCheckServiceTest.cs @@ -6,7 +6,9 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Options; @@ -375,6 +377,113 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks }); } + [Fact] + public async Task CheckHealthAsync_ChecksAreRunInParallel() + { + // Arrange + var input1 = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var input2 = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var output1 = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var output2 = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var service = CreateHealthChecksService(b => + { + b.AddAsyncCheck("test1", + async () => + { + output1.SetResult(null); + await input1.Task; + return HealthCheckResult.Healthy(); + }); + b.AddAsyncCheck("test2", + async () => + { + output2.SetResult(null); + await input2.Task; + return HealthCheckResult.Healthy(); + }); + }); + + // Act + var checkHealthTask = service.CheckHealthAsync(); + await Task.WhenAll(output1.Task, output2.Task).TimeoutAfter(TimeSpan.FromSeconds(10)); + input1.SetResult(null); + input2.SetResult(null); + await checkHealthTask; + + // Assert + Assert.Collection(checkHealthTask.Result.Entries, + entry => + { + Assert.Equal("test1", entry.Key); + Assert.Equal(HealthStatus.Healthy, entry.Value.Status); + }, + entry => + { + Assert.Equal("test2", entry.Key); + Assert.Equal(HealthStatus.Healthy, entry.Value.Status); + }); + } + + [Fact] + public async Task CheckHealthAsync_TimeoutReturnsUnhealthy() + { + // Arrange + var service = CreateHealthChecksService(b => + { + b.AddAsyncCheck("timeout", async (ct) => + { + await Task.Delay(2000, ct); + return HealthCheckResult.Healthy(); + }, timeout: TimeSpan.FromMilliseconds(100)); + }); + + // Act + var results = await service.CheckHealthAsync(); + + // Assert + Assert.Collection( + results.Entries, + actual => + { + Assert.Equal("timeout", actual.Key); + Assert.Equal(HealthStatus.Unhealthy, actual.Value.Status); + }); + } + + [Fact] + public void CheckHealthAsync_WorksInSingleThreadedSyncContext() + { + // Arrange + var service = CreateHealthChecksService(b => + { + b.AddAsyncCheck("test", async () => + { + await Task.Delay(1).ConfigureAwait(false); + return HealthCheckResult.Healthy(); + }); + }); + + var hangs = true; + + // Act + using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3))) + { + var token = cts.Token; + token.Register(() => throw new OperationCanceledException(token)); + + SingleThreadedSynchronizationContext.Run(() => + { + // Act + service.CheckHealthAsync(token).GetAwaiter().GetResult(); + hangs = false; + }); + } + + // Assert + Assert.False(hangs); + } + private static DefaultHealthCheckService CreateHealthChecksService(Action configure) { var services = new ServiceCollection(); diff --git a/src/HealthChecks/HealthChecks/test/DependencyInjection/ServiceCollectionExtensionsTest.cs b/src/HealthChecks/HealthChecks/test/DependencyInjection/ServiceCollectionExtensionsTest.cs index 694a97628d..98371e9f13 100644 --- a/src/HealthChecks/HealthChecks/test/DependencyInjection/ServiceCollectionExtensionsTest.cs +++ b/src/HealthChecks/HealthChecks/test/DependencyInjection/ServiceCollectionExtensionsTest.cs @@ -2,6 +2,8 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Hosting; using Xunit; @@ -39,5 +41,56 @@ namespace Microsoft.Extensions.DependencyInjection Assert.Null(actual.ImplementationFactory); }); } + + [Fact] // see: https://github.com/aspnet/Extensions/issues/639 + public void AddHealthChecks_RegistersPublisherService_WhenOtherHostedServicesRegistered() + { + // Arrange + var services = new ServiceCollection(); + + // Act + services.AddSingleton(); + services.AddHealthChecks(); + + // Assert + Assert.Collection(services.OrderBy(s => s.ServiceType.FullName).ThenBy(s => s.ImplementationType.FullName), + actual => + { + Assert.Equal(ServiceLifetime.Singleton, actual.Lifetime); + Assert.Equal(typeof(HealthCheckService), actual.ServiceType); + Assert.Equal(typeof(DefaultHealthCheckService), actual.ImplementationType); + Assert.Null(actual.ImplementationInstance); + Assert.Null(actual.ImplementationFactory); + }, + actual => + { + Assert.Equal(ServiceLifetime.Singleton, actual.Lifetime); + Assert.Equal(typeof(IHostedService), actual.ServiceType); + Assert.Equal(typeof(DummyHostedService), actual.ImplementationType); + Assert.Null(actual.ImplementationInstance); + Assert.Null(actual.ImplementationFactory); + }, + actual => + { + Assert.Equal(ServiceLifetime.Singleton, actual.Lifetime); + Assert.Equal(typeof(IHostedService), actual.ServiceType); + Assert.Equal(typeof(HealthCheckPublisherHostedService), actual.ImplementationType); + Assert.Null(actual.ImplementationInstance); + Assert.Null(actual.ImplementationFactory); + }); + } + + private class DummyHostedService : IHostedService + { + public Task StartAsync(CancellationToken cancellationToken) + { + throw new System.NotImplementedException(); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + throw new System.NotImplementedException(); + } + } } } diff --git a/src/HealthChecks/HealthChecks/test/HealthCheckPublisherHostedServiceTest.cs b/src/HealthChecks/HealthChecks/test/HealthCheckPublisherHostedServiceTest.cs index 94687efcb8..099944a473 100644 --- a/src/HealthChecks/HealthChecks/test/HealthCheckPublisherHostedServiceTest.cs +++ b/src/HealthChecks/HealthChecks/test/HealthCheckPublisherHostedServiceTest.cs @@ -211,8 +211,8 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherProcessingBegin, entry.EventId); }, entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckProcessingBegin, entry.EventId); }, entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckBegin, entry.EventId); }, - entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckEnd, entry.EventId); }, - entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckBegin, entry.EventId); }, + entry => { Assert.Contains(entry.EventId, new[] { DefaultHealthCheckService.EventIds.HealthCheckBegin, DefaultHealthCheckService.EventIds.HealthCheckEnd }); }, + entry => { Assert.Contains(entry.EventId, new[] { DefaultHealthCheckService.EventIds.HealthCheckBegin, DefaultHealthCheckService.EventIds.HealthCheckEnd }); }, entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckEnd, entry.EventId); }, entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckProcessingEnd, entry.EventId); }, entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherBegin, entry.EventId); }, @@ -321,8 +321,8 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherProcessingBegin, entry.EventId); }, entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckProcessingBegin, entry.EventId); }, entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckBegin, entry.EventId); }, - entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckEnd, entry.EventId); }, - entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckBegin, entry.EventId); }, + entry => { Assert.Contains(entry.EventId, new[] { DefaultHealthCheckService.EventIds.HealthCheckBegin, DefaultHealthCheckService.EventIds.HealthCheckEnd }); }, + entry => { Assert.Contains(entry.EventId, new[] { DefaultHealthCheckService.EventIds.HealthCheckBegin, DefaultHealthCheckService.EventIds.HealthCheckEnd }); }, entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckEnd, entry.EventId); }, entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckProcessingEnd, entry.EventId); }, entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherBegin, entry.EventId); }, @@ -399,8 +399,8 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherProcessingBegin, entry.EventId); }, entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckProcessingBegin, entry.EventId); }, entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckBegin, entry.EventId); }, - entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckEnd, entry.EventId); }, - entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckBegin, entry.EventId); }, + entry => { Assert.Contains(entry.EventId, new[] { DefaultHealthCheckService.EventIds.HealthCheckBegin, DefaultHealthCheckService.EventIds.HealthCheckEnd }); }, + entry => { Assert.Contains(entry.EventId, new[] { DefaultHealthCheckService.EventIds.HealthCheckBegin, DefaultHealthCheckService.EventIds.HealthCheckEnd }); }, entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckEnd, entry.EventId); }, entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckProcessingEnd, entry.EventId); }, entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherBegin, entry.EventId); }, diff --git a/src/HealthChecks/HealthChecks/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests.csproj b/src/HealthChecks/HealthChecks/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests.csproj index 4ab29c46bf..08cd6a35f1 100644 --- a/src/HealthChecks/HealthChecks/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests.csproj +++ b/src/HealthChecks/HealthChecks/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests.csproj @@ -1,9 +1,9 @@  - + - netcoreapp2.2;net461 + netcoreapp3.0;net472 Microsoft.Extensions.Diagnostics.HealthChecks @@ -12,4 +12,8 @@ + + + + diff --git a/src/JSInterop/Microsoft.JSInterop.JS/src/Microsoft.JSInterop.JS.npmproj b/src/JSInterop/Microsoft.JSInterop.JS/src/Microsoft.JSInterop.JS.npmproj new file mode 100644 index 0000000000..e8d0554ff7 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop.JS/src/Microsoft.JSInterop.JS.npmproj @@ -0,0 +1,12 @@ + + + + + @dotnet/jsinterop + true + false + true + + + + diff --git a/src/JSInterop/Microsoft.JSInterop.JS/src/package-lock.json b/src/JSInterop/Microsoft.JSInterop.JS/src/package-lock.json new file mode 100644 index 0000000000..4c82321255 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop.JS/src/package-lock.json @@ -0,0 +1,348 @@ +{ + "name": "@dotnet/jsinterop", + "version": "3.0.0-dev", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + } + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "commander": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", + "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "js-yaml": { + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.1.tgz", + "integrity": "sha512-um46hB9wNOKlwkHgiuyEVAybXBjwFUV0Z/RaHJblRd9DXltue9FTYvzCr9ErQrK9Adz5MU4gHWVaNUfdmrC8qA==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "resolve": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz", + "integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dev": true, + "requires": { + "glob": "^7.0.5" + } + }, + "semver": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", + "dev": true + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "tslib": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", + "dev": true + }, + "tslint": { + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.12.1.tgz", + "integrity": "sha512-sfodBHOucFg6egff8d1BvuofoOQ/nOeYNfbp7LDlKBcLNrL3lmS5zoiDGyOMdT7YsEXAwWpTdAHwOGOc8eRZAw==", + "dev": true, + "requires": { + "babel-code-frame": "^6.22.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^3.2.0", + "glob": "^7.1.1", + "js-yaml": "^3.7.0", + "minimatch": "^3.0.4", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.8.0", + "tsutils": "^2.27.2" + } + }, + "tsutils": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, + "typescript": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz", + "integrity": "sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + } + } +} diff --git a/src/JSInterop/Microsoft.JSInterop.JS/src/package.json b/src/JSInterop/Microsoft.JSInterop.JS/src/package.json new file mode 100644 index 0000000000..8be950e16f --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop.JS/src/package.json @@ -0,0 +1,31 @@ +{ + "name": "@dotnet/jsinterop", + "version": "3.0.0-dev", + "description": "Provides abstractions and features for interop between .NET and JavaScript code.", + "main": "dist/Microsoft.JSInterop.js", + "types": "dist/Microsoft.JSInterop.d.js", + "scripts": { + "clean": "node node_modules/rimraf/bin.js ./dist", + "build": "npm run clean && npm run build:esm", + "build:lint": "node node_modules/tslint/bin/tslint -p ./tsconfig.json", + "build:esm": "node node_modules/typescript/bin/tsc --project ./tsconfig.json" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/aspnet/AspNetCore.git" + }, + "author": "Microsoft", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/aspnet/AspNetCore/issues" + }, + "homepage": "https://github.com/aspnet/Extensions/tree/master/src/JSInterop#readme", + "files": [ + "dist/**" + ], + "devDependencies": { + "rimraf": "^2.5.4", + "tslint": "^5.9.1", + "typescript": "^2.7.1" + } +} diff --git a/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts b/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts new file mode 100644 index 0000000000..4b5d409d0f --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts @@ -0,0 +1,287 @@ +// This is a single-file self-contained module to avoid the need for a Webpack build + +module DotNet { + (window as any).DotNet = DotNet; // Ensure reachable from anywhere + + export type JsonReviver = ((key: any, value: any) => any); + const jsonRevivers: JsonReviver[] = []; + + const pendingAsyncCalls: { [id: number]: PendingAsyncCall } = {}; + const cachedJSFunctions: { [identifier: string]: Function } = {}; + let nextAsyncCallId = 1; // Start at 1 because zero signals "no response needed" + + let dotNetDispatcher: DotNetCallDispatcher | null = null; + + /** + * Sets the specified .NET call dispatcher as the current instance so that it will be used + * for future invocations. + * + * @param dispatcher An object that can dispatch calls from JavaScript to a .NET runtime. + */ + export function attachDispatcher(dispatcher: DotNetCallDispatcher) { + dotNetDispatcher = dispatcher; + } + + /** + * Adds a JSON reviver callback that will be used when parsing arguments received from .NET. + * @param reviver The reviver to add. + */ + export function attachReviver(reviver: JsonReviver) { + jsonRevivers.push(reviver); + } + + /** + * Invokes the specified .NET public method synchronously. Not all hosting scenarios support + * synchronous invocation, so if possible use invokeMethodAsync instead. + * + * @param assemblyName The short name (without key/version or .dll extension) of the .NET assembly containing the method. + * @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier. + * @param args Arguments to pass to the method, each of which must be JSON-serializable. + * @returns The result of the operation. + */ + export function invokeMethod(assemblyName: string, methodIdentifier: string, ...args: any[]): T { + return invokePossibleInstanceMethod(assemblyName, methodIdentifier, null, args); + } + + /** + * Invokes the specified .NET public method asynchronously. + * + * @param assemblyName The short name (without key/version or .dll extension) of the .NET assembly containing the method. + * @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier. + * @param args Arguments to pass to the method, each of which must be JSON-serializable. + * @returns A promise representing the result of the operation. + */ + export function invokeMethodAsync(assemblyName: string, methodIdentifier: string, ...args: any[]): Promise { + return invokePossibleInstanceMethodAsync(assemblyName, methodIdentifier, null, args); + } + + function invokePossibleInstanceMethod(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, args: any[]): T { + const dispatcher = getRequiredDispatcher(); + if (dispatcher.invokeDotNetFromJS) { + const argsJson = JSON.stringify(args, argReplacer); + const resultJson = dispatcher.invokeDotNetFromJS(assemblyName, methodIdentifier, dotNetObjectId, argsJson); + return resultJson ? parseJsonWithRevivers(resultJson) : null; + } else { + throw new Error('The current dispatcher does not support synchronous calls from JS to .NET. Use invokeMethodAsync instead.'); + } + } + + function invokePossibleInstanceMethodAsync(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, args: any[]): Promise { + const asyncCallId = nextAsyncCallId++; + const resultPromise = new Promise((resolve, reject) => { + pendingAsyncCalls[asyncCallId] = { resolve, reject }; + }); + + try { + const argsJson = JSON.stringify(args, argReplacer); + getRequiredDispatcher().beginInvokeDotNetFromJS(asyncCallId, assemblyName, methodIdentifier, dotNetObjectId, argsJson); + } catch(ex) { + // Synchronous failure + completePendingCall(asyncCallId, false, ex); + } + + return resultPromise; + } + + function getRequiredDispatcher(): DotNetCallDispatcher { + if (dotNetDispatcher !== null) { + return dotNetDispatcher; + } + + throw new Error('No .NET call dispatcher has been set.'); + } + + function completePendingCall(asyncCallId: number, success: boolean, resultOrError: any) { + if (!pendingAsyncCalls.hasOwnProperty(asyncCallId)) { + throw new Error(`There is no pending async call with ID ${asyncCallId}.`); + } + + const asyncCall = pendingAsyncCalls[asyncCallId]; + delete pendingAsyncCalls[asyncCallId]; + if (success) { + asyncCall.resolve(resultOrError); + } else { + asyncCall.reject(resultOrError); + } + } + + interface PendingAsyncCall { + resolve: (value?: T | PromiseLike) => void; + reject: (reason?: any) => void; + } + + /** + * Represents the ability to dispatch calls from JavaScript to a .NET runtime. + */ + export interface DotNetCallDispatcher { + /** + * Optional. If implemented, invoked by the runtime to perform a synchronous call to a .NET method. + * + * @param assemblyName The short name (without key/version or .dll extension) of the .NET assembly holding the method to invoke. The value may be null when invoking instance methods. + * @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier. + * @param dotNetObjectId If given, the call will be to an instance method on the specified DotNetObject. Pass null or undefined to call static methods. + * @param argsJson JSON representation of arguments to pass to the method. + * @returns JSON representation of the result of the invocation. + */ + invokeDotNetFromJS?(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, argsJson: string): string | null; + + /** + * Invoked by the runtime to begin an asynchronous call to a .NET method. + * + * @param callId A value identifying the asynchronous operation. This value should be passed back in a later call from .NET to JS. + * @param assemblyName The short name (without key/version or .dll extension) of the .NET assembly holding the method to invoke. The value may be null when invoking instance methods. + * @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier. + * @param dotNetObjectId If given, the call will be to an instance method on the specified DotNetObject. Pass null to call static methods. + * @param argsJson JSON representation of arguments to pass to the method. + */ + beginInvokeDotNetFromJS(callId: number, assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, argsJson: string): void; + } + + /** + * Receives incoming calls from .NET and dispatches them to JavaScript. + */ + export const jsCallDispatcher = { + /** + * Finds the JavaScript function matching the specified identifier. + * + * @param identifier Identifies the globally-reachable function to be returned. + * @returns A Function instance. + */ + findJSFunction, + + /** + * Invokes the specified synchronous JavaScript function. + * + * @param identifier Identifies the globally-reachable function to invoke. + * @param argsJson JSON representation of arguments to be passed to the function. + * @returns JSON representation of the invocation result. + */ + invokeJSFromDotNet: (identifier: string, argsJson: string) => { + const result = findJSFunction(identifier).apply(null, parseJsonWithRevivers(argsJson)); + return result === null || result === undefined + ? null + : JSON.stringify(result, argReplacer); + }, + + /** + * Invokes the specified synchronous or asynchronous JavaScript function. + * + * @param asyncHandle A value identifying the asynchronous operation. This value will be passed back in a later call to endInvokeJSFromDotNet. + * @param identifier Identifies the globally-reachable function to invoke. + * @param argsJson JSON representation of arguments to be passed to the function. + */ + beginInvokeJSFromDotNet: (asyncHandle: number, identifier: string, argsJson: string): void => { + // Coerce synchronous functions into async ones, plus treat + // synchronous exceptions the same as async ones + const promise = new Promise(resolve => { + const synchronousResultOrPromise = findJSFunction(identifier).apply(null, parseJsonWithRevivers(argsJson)); + resolve(synchronousResultOrPromise); + }); + + // We only listen for a result if the caller wants to be notified about it + if (asyncHandle) { + // On completion, dispatch result back to .NET + // Not using "await" because it codegens a lot of boilerplate + promise.then( + result => getRequiredDispatcher().beginInvokeDotNetFromJS(0, 'Microsoft.JSInterop', 'DotNetDispatcher.EndInvoke', null, JSON.stringify([asyncHandle, true, result], argReplacer)), + error => getRequiredDispatcher().beginInvokeDotNetFromJS(0, 'Microsoft.JSInterop', 'DotNetDispatcher.EndInvoke', null, JSON.stringify([asyncHandle, false, formatError(error)])) + ); + } + }, + + /** + * Receives notification that an async call from JS to .NET has completed. + * @param asyncCallId The identifier supplied in an earlier call to beginInvokeDotNetFromJS. + * @param success A flag to indicate whether the operation completed successfully. + * @param resultOrExceptionMessage Either the operation result or an error message. + */ + endInvokeDotNetFromJS: (asyncCallId: string, success: boolean, resultOrExceptionMessage: any): void => { + const resultOrError = success ? resultOrExceptionMessage : new Error(resultOrExceptionMessage); + completePendingCall(parseInt(asyncCallId), success, resultOrError); + } + } + + function parseJsonWithRevivers(json: string): any { + return json ? JSON.parse(json, (key, initialValue) => { + // Invoke each reviver in order, passing the output from the previous reviver, + // so that each one gets a chance to transform the value + return jsonRevivers.reduce( + (latestValue, reviver) => reviver(key, latestValue), + initialValue + ); + }) : null; + } + + function formatError(error: any): string { + if (error instanceof Error) { + return `${error.message}\n${error.stack}`; + } else { + return error ? error.toString() : 'null'; + } + } + + function findJSFunction(identifier: string): Function { + if (cachedJSFunctions.hasOwnProperty(identifier)) { + return cachedJSFunctions[identifier]; + } + + let result: any = window; + let resultIdentifier = 'window'; + identifier.split('.').forEach(segment => { + if (segment in result) { + result = result[segment]; + resultIdentifier += '.' + segment; + } else { + throw new Error(`Could not find '${segment}' in '${resultIdentifier}'.`); + } + }); + + if (result instanceof Function) { + return result; + } else { + throw new Error(`The value '${resultIdentifier}' is not a function.`); + } + } + + class DotNetObject { + constructor(private _id: number) { + } + + public invokeMethod(methodIdentifier: string, ...args: any[]): T { + return invokePossibleInstanceMethod(null, methodIdentifier, this._id, args); + } + + public invokeMethodAsync(methodIdentifier: string, ...args: any[]): Promise { + return invokePossibleInstanceMethodAsync(null, methodIdentifier, this._id, args); + } + + public dispose() { + const promise = invokeMethodAsync( + 'Microsoft.JSInterop', + 'DotNetDispatcher.ReleaseDotNetObject', + this._id); + promise.catch(error => console.error(error)); + } + + public serializeAsArg() { + return `__dotNetObject:${this._id}`; + } + } + + const dotNetObjectValueFormat = /^__dotNetObject\:(\d+)$/; + attachReviver(function reviveDotNetObject(key: any, value: any) { + if (typeof value === 'string') { + const match = value.match(dotNetObjectValueFormat); + if (match) { + return new DotNetObject(parseInt(match[1])); + } + } + + // Unrecognized - let another reviver handle it + return value; + }); + + function argReplacer(key: string, value: any) { + return value instanceof DotNetObject ? value.serializeAsArg() : value; + } +} diff --git a/src/JSInterop/Microsoft.JSInterop.JS/src/tsconfig.json b/src/JSInterop/Microsoft.JSInterop.JS/src/tsconfig.json new file mode 100644 index 0000000000..f5a2b0e31a --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop.JS/src/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "noEmitOnError": true, + "removeComments": false, + "sourceMap": true, + "target": "es5", + "lib": ["es2015", "dom", "es2015.promise"], + "strict": true, + "declaration": true, + "outDir": "dist" + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "dist/**" + ] +} diff --git a/src/JSInterop/Microsoft.JSInterop.JS/src/tslint.json b/src/JSInterop/Microsoft.JSInterop.JS/src/tslint.json new file mode 100644 index 0000000000..5c38bef990 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop.JS/src/tslint.json @@ -0,0 +1,14 @@ +{ + "extends": "tslint:recommended", + "rules": { + "max-line-length": { "options": [300] }, + "member-ordering": false, + "interface-name": false, + "unified-signatures": false, + "max-classes-per-file": false, + "no-floating-promises": true, + "no-empty": false, + "no-bitwise": false, + "no-console": false + } +} diff --git a/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.csproj b/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.csproj new file mode 100644 index 0000000000..87fd913427 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.csproj @@ -0,0 +1,10 @@ + + + + netstandard2.0 + + + + + + diff --git a/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netstandard2.0.cs b/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netstandard2.0.cs new file mode 100644 index 0000000000..95b9b7956c --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netstandard2.0.cs @@ -0,0 +1,75 @@ +// 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.JSInterop +{ + public static partial class DotNetDispatcher + { + public static void BeginInvoke(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { } + [Microsoft.JSInterop.JSInvokableAttribute("DotNetDispatcher.EndInvoke")] + public static void EndInvoke(long asyncHandle, bool succeeded, Microsoft.JSInterop.Internal.JSAsyncCallResult result) { } + public static string Invoke(string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { throw null; } + [Microsoft.JSInterop.JSInvokableAttribute("DotNetDispatcher.ReleaseDotNetObject")] + public static void ReleaseDotNetObject(long dotNetObjectId) { } + } + public partial class DotNetObjectRef : System.IDisposable + { + public DotNetObjectRef(object value) { } + public object Value { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public void Dispose() { } + public void EnsureAttachedToJsRuntime(Microsoft.JSInterop.IJSRuntime runtime) { } + } + public partial interface IJSInProcessRuntime : Microsoft.JSInterop.IJSRuntime + { + T Invoke(string identifier, params object[] args); + } + public partial interface IJSRuntime + { + System.Threading.Tasks.Task InvokeAsync(string identifier, params object[] args); + void UntrackObjectRef(Microsoft.JSInterop.DotNetObjectRef dotNetObjectRef); + } + public partial class JSException : System.Exception + { + public JSException(string message) { } + } + public abstract partial class JSInProcessRuntimeBase : Microsoft.JSInterop.JSRuntimeBase, Microsoft.JSInterop.IJSInProcessRuntime, Microsoft.JSInterop.IJSRuntime + { + protected JSInProcessRuntimeBase() { } + protected abstract string InvokeJS(string identifier, string argsJson); + public T Invoke(string identifier, params object[] args) { throw null; } + } + [System.AttributeUsageAttribute(System.AttributeTargets.Method, AllowMultiple=true)] + public partial class JSInvokableAttribute : System.Attribute + { + public JSInvokableAttribute() { } + public JSInvokableAttribute(string identifier) { } + public string Identifier { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + } + public static partial class Json + { + public static T Deserialize(string json) { throw null; } + public static string Serialize(object value) { throw null; } + } + public static partial class JSRuntime + { + public static void SetCurrentJSRuntime(Microsoft.JSInterop.IJSRuntime instance) { } + } + public abstract partial class JSRuntimeBase : Microsoft.JSInterop.IJSRuntime + { + public JSRuntimeBase() { } + protected abstract void BeginInvokeJS(long asyncHandle, string identifier, string argsJson); + public System.Threading.Tasks.Task InvokeAsync(string identifier, params object[] args) { throw null; } + public void UntrackObjectRef(Microsoft.JSInterop.DotNetObjectRef dotNetObjectRef) { } + } +} +namespace Microsoft.JSInterop.Internal +{ + public partial interface ICustomArgSerializer + { + object ToJsonPrimitive(); + } + public partial class JSAsyncCallResult + { + internal JSAsyncCallResult() { } + } +} diff --git a/src/JSInterop/Microsoft.JSInterop/src/DotNetDispatcher.cs b/src/JSInterop/Microsoft.JSInterop/src/DotNetDispatcher.cs new file mode 100644 index 0000000000..ac936e670a --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/src/DotNetDispatcher.cs @@ -0,0 +1,286 @@ +// 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.JSInterop.Internal; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.ExceptionServices; +using System.Threading.Tasks; + +namespace Microsoft.JSInterop +{ + /// + /// Provides methods that receive incoming calls from JS to .NET. + /// + public static class DotNetDispatcher + { + private static ConcurrentDictionary> _cachedMethodsByAssembly + = new ConcurrentDictionary>(); + + /// + /// Receives a call from JS to .NET, locating and invoking the specified method. + /// + /// The assembly containing the method to be invoked. + /// The identifier of the method to be invoked. The method must be annotated with a matching this identifier string. + /// For instance method calls, identifies the target object. + /// A JSON representation of the parameters. + /// A JSON representation of the return value, or null. + public static string Invoke(string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) + { + // This method doesn't need [JSInvokable] because the platform is responsible for having + // some way to dispatch calls here. The logic inside here is the thing that checks whether + // the targeted method has [JSInvokable]. It is not itself subject to that restriction, + // because there would be nobody to police that. This method *is* the police. + + // DotNetDispatcher only works with JSRuntimeBase instances. + var jsRuntime = (JSRuntimeBase)JSRuntime.Current; + + var targetInstance = (object)null; + if (dotNetObjectId != default) + { + targetInstance = jsRuntime.ArgSerializerStrategy.FindDotNetObject(dotNetObjectId); + } + + var syncResult = InvokeSynchronously(assemblyName, methodIdentifier, targetInstance, argsJson); + return syncResult == null ? null : Json.Serialize(syncResult, jsRuntime.ArgSerializerStrategy); + } + + /// + /// Receives a call from JS to .NET, locating and invoking the specified method asynchronously. + /// + /// A value identifying the asynchronous call that should be passed back with the result, or null if no result notification is required. + /// The assembly containing the method to be invoked. + /// The identifier of the method to be invoked. The method must be annotated with a matching this identifier string. + /// For instance method calls, identifies the target object. + /// A JSON representation of the parameters. + /// A JSON representation of the return value, or null. + public static void BeginInvoke(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) + { + // This method doesn't need [JSInvokable] because the platform is responsible for having + // some way to dispatch calls here. The logic inside here is the thing that checks whether + // the targeted method has [JSInvokable]. It is not itself subject to that restriction, + // because there would be nobody to police that. This method *is* the police. + + // DotNetDispatcher only works with JSRuntimeBase instances. + // If the developer wants to use a totally custom IJSRuntime, then their JS-side + // code has to implement its own way of returning async results. + var jsRuntimeBaseInstance = (JSRuntimeBase)JSRuntime.Current; + + var targetInstance = dotNetObjectId == default + ? null + : jsRuntimeBaseInstance.ArgSerializerStrategy.FindDotNetObject(dotNetObjectId); + + // Using ExceptionDispatchInfo here throughout because we want to always preserve + // original stack traces. + object syncResult = null; + ExceptionDispatchInfo syncException = null; + + try + { + syncResult = InvokeSynchronously(assemblyName, methodIdentifier, targetInstance, argsJson); + } + catch (Exception ex) + { + syncException = ExceptionDispatchInfo.Capture(ex); + } + + // If there was no callId, the caller does not want to be notified about the result + if (callId == null) + { + return; + } + else if (syncException != null) + { + // Threw synchronously, let's respond. + jsRuntimeBaseInstance.EndInvokeDotNet(callId, false, syncException); + } + else if (syncResult is Task task) + { + // Returned a task - we need to continue that task and then report an exception + // or return the value. + task.ContinueWith(t => + { + if (t.Exception != null) + { + var exception = t.Exception.GetBaseException(); + jsRuntimeBaseInstance.EndInvokeDotNet(callId, false, ExceptionDispatchInfo.Capture(exception)); + } + + var result = TaskGenericsUtil.GetTaskResult(task); + jsRuntimeBaseInstance.EndInvokeDotNet(callId, true, result); + }, TaskScheduler.Current); + } + else + { + jsRuntimeBaseInstance.EndInvokeDotNet(callId, true, syncResult); + } + } + + private static object InvokeSynchronously(string assemblyName, string methodIdentifier, object targetInstance, string argsJson) + { + if (targetInstance != null) + { + if (assemblyName != null) + { + throw new ArgumentException($"For instance method calls, '{nameof(assemblyName)}' should be null. Value received: '{assemblyName}'."); + } + + assemblyName = targetInstance.GetType().Assembly.GetName().Name; + } + + var (methodInfo, parameterTypes) = GetCachedMethodInfo(assemblyName, methodIdentifier); + + // There's no direct way to say we want to deserialize as an array with heterogenous + // entry types (e.g., [string, int, bool]), so we need to deserialize in two phases. + // First we deserialize as object[], for which SimpleJson will supply JsonObject + // instances for nonprimitive values. + var suppliedArgs = (object[])null; + var suppliedArgsLength = 0; + if (argsJson != null) + { + suppliedArgs = Json.Deserialize(argsJson).ToArray(); + suppliedArgsLength = suppliedArgs.Length; + } + if (suppliedArgsLength != parameterTypes.Length) + { + throw new ArgumentException($"In call to '{methodIdentifier}', expected {parameterTypes.Length} parameters but received {suppliedArgsLength}."); + } + + // Second, convert each supplied value to the type expected by the method + var runtime = (JSRuntimeBase)JSRuntime.Current; + var serializerStrategy = runtime.ArgSerializerStrategy; + for (var i = 0; i < suppliedArgsLength; i++) + { + if (parameterTypes[i] == typeof(JSAsyncCallResult)) + { + // For JS async call results, we have to defer the deserialization until + // later when we know what type it's meant to be deserialized as + suppliedArgs[i] = new JSAsyncCallResult(suppliedArgs[i]); + } + else + { + suppliedArgs[i] = serializerStrategy.DeserializeObject( + suppliedArgs[i], parameterTypes[i]); + } + } + + try + { + return methodInfo.Invoke(targetInstance, suppliedArgs); + } + catch (TargetInvocationException tie) when (tie.InnerException != null) + { + ExceptionDispatchInfo.Capture(tie.InnerException).Throw(); + throw null; // unreachable + } + } + + /// + /// Receives notification that a call from .NET to JS has finished, marking the + /// associated as completed. + /// + /// The identifier for the function invocation. + /// A flag to indicate whether the invocation succeeded. + /// If is true, specifies the invocation result. If is false, gives the corresponding to the invocation failure. + [JSInvokable(nameof(DotNetDispatcher) + "." + nameof(EndInvoke))] + public static void EndInvoke(long asyncHandle, bool succeeded, JSAsyncCallResult result) + => ((JSRuntimeBase)JSRuntime.Current).EndInvokeJS(asyncHandle, succeeded, result.ResultOrException); + + /// + /// Releases the reference to the specified .NET object. This allows the .NET runtime + /// to garbage collect that object if there are no other references to it. + /// + /// To avoid leaking memory, the JavaScript side code must call this for every .NET + /// object it obtains a reference to. The exception is if that object is used for + /// the entire lifetime of a given user's session, in which case it is released + /// automatically when the JavaScript runtime is disposed. + /// + /// The identifier previously passed to JavaScript code. + [JSInvokable(nameof(DotNetDispatcher) + "." + nameof(ReleaseDotNetObject))] + public static void ReleaseDotNetObject(long dotNetObjectId) + { + // DotNetDispatcher only works with JSRuntimeBase instances. + var jsRuntime = (JSRuntimeBase)JSRuntime.Current; + jsRuntime.ArgSerializerStrategy.ReleaseDotNetObject(dotNetObjectId); + } + + private static (MethodInfo, Type[]) GetCachedMethodInfo(string assemblyName, string methodIdentifier) + { + if (string.IsNullOrWhiteSpace(assemblyName)) + { + throw new ArgumentException("Cannot be null, empty, or whitespace.", nameof(assemblyName)); + } + + if (string.IsNullOrWhiteSpace(methodIdentifier)) + { + throw new ArgumentException("Cannot be null, empty, or whitespace.", nameof(methodIdentifier)); + } + + var assemblyMethods = _cachedMethodsByAssembly.GetOrAdd(assemblyName, ScanAssemblyForCallableMethods); + if (assemblyMethods.TryGetValue(methodIdentifier, out var result)) + { + return result; + } + else + { + throw new ArgumentException($"The assembly '{assemblyName}' does not contain a public method with [{nameof(JSInvokableAttribute)}(\"{methodIdentifier}\")]."); + } + } + + private static IReadOnlyDictionary ScanAssemblyForCallableMethods(string assemblyName) + { + // TODO: Consider looking first for assembly-level attributes (i.e., if there are any, + // only use those) to avoid scanning, especially for framework assemblies. + var result = new Dictionary(); + var invokableMethods = GetRequiredLoadedAssembly(assemblyName) + .GetExportedTypes() + .SelectMany(type => type.GetMethods( + BindingFlags.Public | + BindingFlags.DeclaredOnly | + BindingFlags.Instance | + BindingFlags.Static)) + .Where(method => method.IsDefined(typeof(JSInvokableAttribute), inherit: false)); + foreach (var method in invokableMethods) + { + var identifier = method.GetCustomAttribute(false).Identifier ?? method.Name; + var parameterTypes = method.GetParameters().Select(p => p.ParameterType).ToArray(); + + try + { + result.Add(identifier, (method, parameterTypes)); + } + catch (ArgumentException) + { + if (result.ContainsKey(identifier)) + { + throw new InvalidOperationException($"The assembly '{assemblyName}' contains more than one " + + $"[JSInvokable] method with identifier '{identifier}'. All [JSInvokable] methods within the same " + + $"assembly must have different identifiers. You can pass a custom identifier as a parameter to " + + $"the [JSInvokable] attribute."); + } + else + { + throw; + } + } + } + + return result; + } + + private static Assembly GetRequiredLoadedAssembly(string assemblyName) + { + // We don't want to load assemblies on demand here, because we don't necessarily trust + // "assemblyName" to be something the developer intended to load. So only pick from the + // set of already-loaded assemblies. + // In some edge cases this might force developers to explicitly call something on the + // target assembly (from .NET) before they can invoke its allowed methods from JS. + var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); + return loadedAssemblies.FirstOrDefault(a => a.GetName().Name.Equals(assemblyName, StringComparison.Ordinal)) + ?? throw new ArgumentException($"There is no loaded assembly with the name '{assemblyName}'."); + } + } +} diff --git a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRef.cs b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRef.cs new file mode 100644 index 0000000000..aa62bee341 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRef.cs @@ -0,0 +1,66 @@ +// 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.Threading; + +namespace Microsoft.JSInterop +{ + /// + /// Wraps a JS interop argument, indicating that the value should not be serialized as JSON + /// but instead should be passed as a reference. + /// + /// To avoid leaking memory, the reference must later be disposed by JS code or by .NET code. + /// + public class DotNetObjectRef : IDisposable + { + /// + /// Gets the object instance represented by this wrapper. + /// + public object Value { get; } + + // We track an associated IJSRuntime purely so that this class can be IDisposable + // in the normal way. Developers are more likely to use objectRef.Dispose() than + // some less familiar API such as JSRuntime.Current.UntrackObjectRef(objectRef). + private IJSRuntime _attachedToRuntime; + + /// + /// Constructs an instance of . + /// + /// The value being wrapped. + public DotNetObjectRef(object value) + { + Value = value; + } + + /// + /// Ensures the is associated with the specified . + /// Developers do not normally need to invoke this manually, since it is called automatically by + /// framework code. + /// + /// The . + public void EnsureAttachedToJsRuntime(IJSRuntime runtime) + { + // The reason we populate _attachedToRuntime here rather than in the constructor + // is to ensure developers can't accidentally try to reuse DotNetObjectRef across + // different IJSRuntime instances. This method gets called as part of serializing + // the DotNetObjectRef during an interop call. + + var existingRuntime = Interlocked.CompareExchange(ref _attachedToRuntime, runtime, null); + if (existingRuntime != null && existingRuntime != runtime) + { + throw new InvalidOperationException($"The {nameof(DotNetObjectRef)} is already associated with a different {nameof(IJSRuntime)}. Do not attempt to re-use {nameof(DotNetObjectRef)} instances with multiple {nameof(IJSRuntime)} instances."); + } + } + + /// + /// Stops tracking this object reference, allowing it to be garbage collected + /// (if there are no other references to it). Once the instance is disposed, it + /// can no longer be used in interop calls from JavaScript code. + /// + public void Dispose() + { + _attachedToRuntime?.UntrackObjectRef(this); + } + } +} diff --git a/src/JSInterop/Microsoft.JSInterop/src/ICustomArgSerializer.cs b/src/JSInterop/Microsoft.JSInterop/src/ICustomArgSerializer.cs new file mode 100644 index 0000000000..f4012af8e9 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/src/ICustomArgSerializer.cs @@ -0,0 +1,22 @@ +// 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.JSInterop.Internal +{ + // This is "soft" internal because we're trying to avoid expanding JsonUtil into a sophisticated + // API. Developers who want that would be better served by using a different JSON package + // instead. Also the perf implications of the ICustomArgSerializer approach aren't ideal + // (it forces structs to be boxed, and returning a dictionary means lots more allocations + // and boxing of any value-typed properties). + + /// + /// Internal. Intended for framework use only. + /// + public interface ICustomArgSerializer + { + /// + /// Internal. Intended for framework use only. + /// + object ToJsonPrimitive(); + } +} diff --git a/src/JSInterop/Microsoft.JSInterop/src/IJSInProcessRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/IJSInProcessRuntime.cs new file mode 100644 index 0000000000..cae5126db2 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/src/IJSInProcessRuntime.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. + +namespace Microsoft.JSInterop +{ + /// + /// Represents an instance of a JavaScript runtime to which calls may be dispatched. + /// + public interface IJSInProcessRuntime : IJSRuntime + { + /// + /// Invokes the specified JavaScript function synchronously. + /// + /// The JSON-serializable return type. + /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. + /// JSON-serializable arguments. + /// An instance of obtained by JSON-deserializing the return value. + T Invoke(string identifier, params object[] args); + } +} diff --git a/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs new file mode 100644 index 0000000000..b56d1f0089 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs @@ -0,0 +1,32 @@ +// 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.Threading.Tasks; + +namespace Microsoft.JSInterop +{ + /// + /// Represents an instance of a JavaScript runtime to which calls may be dispatched. + /// + public interface IJSRuntime + { + /// + /// Invokes the specified JavaScript function asynchronously. + /// + /// The JSON-serializable return type. + /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. + /// JSON-serializable arguments. + /// An instance of obtained by JSON-deserializing the return value. + Task InvokeAsync(string identifier, params object[] args); + + /// + /// Stops tracking the .NET object represented by the . + /// This allows it to be garbage collected (if nothing else holds a reference to it) + /// and means the JS-side code can no longer invoke methods on the instance or pass + /// it as an argument to subsequent calls. + /// + /// The reference to stop tracking. + /// This method is called automatically by . + void UntrackObjectRef(DotNetObjectRef dotNetObjectRef); + } +} diff --git a/src/JSInterop/Microsoft.JSInterop/src/InteropArgSerializerStrategy.cs b/src/JSInterop/Microsoft.JSInterop/src/InteropArgSerializerStrategy.cs new file mode 100644 index 0000000000..663c1cf85a --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/src/InteropArgSerializerStrategy.cs @@ -0,0 +1,121 @@ +// 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.JSInterop.Internal; +using SimpleJson; +using System; +using System.Collections.Generic; + +namespace Microsoft.JSInterop +{ + internal class InteropArgSerializerStrategy : PocoJsonSerializerStrategy + { + private readonly JSRuntimeBase _jsRuntime; + private const string _dotNetObjectPrefix = "__dotNetObject:"; + private object _storageLock = new object(); + private long _nextId = 1; // Start at 1, because 0 signals "no object" + private Dictionary _trackedRefsById = new Dictionary(); + private Dictionary _trackedIdsByRef = new Dictionary(); + + public InteropArgSerializerStrategy(JSRuntimeBase jsRuntime) + { + _jsRuntime = jsRuntime ?? throw new ArgumentNullException(nameof(jsRuntime)); + } + + protected override bool TrySerializeKnownTypes(object input, out object output) + { + switch (input) + { + case DotNetObjectRef marshalByRefValue: + EnsureDotNetObjectTracked(marshalByRefValue, out var id); + + // Special value format recognized by the code in Microsoft.JSInterop.js + // If we have to make it more clash-resistant, we can do + output = _dotNetObjectPrefix + id; + + return true; + + case ICustomArgSerializer customArgSerializer: + output = customArgSerializer.ToJsonPrimitive(); + return true; + + default: + return base.TrySerializeKnownTypes(input, out output); + } + } + + public override object DeserializeObject(object value, Type type) + { + if (value is string valueString) + { + if (valueString.StartsWith(_dotNetObjectPrefix)) + { + var dotNetObjectId = long.Parse(valueString.Substring(_dotNetObjectPrefix.Length)); + return FindDotNetObject(dotNetObjectId); + } + } + + return base.DeserializeObject(value, type); + } + + public object FindDotNetObject(long dotNetObjectId) + { + lock (_storageLock) + { + return _trackedRefsById.TryGetValue(dotNetObjectId, out var dotNetObjectRef) + ? dotNetObjectRef.Value + : throw new ArgumentException($"There is no tracked object with id '{dotNetObjectId}'. Perhaps the reference was already released.", nameof(dotNetObjectId)); + } + } + + /// + /// Stops tracking the specified .NET object reference. + /// This overload is typically invoked from JS code via JS interop. + /// + /// The ID of the . + public void ReleaseDotNetObject(long dotNetObjectId) + { + lock (_storageLock) + { + if (_trackedRefsById.TryGetValue(dotNetObjectId, out var dotNetObjectRef)) + { + _trackedRefsById.Remove(dotNetObjectId); + _trackedIdsByRef.Remove(dotNetObjectRef); + } + } + } + + /// + /// Stops tracking the specified .NET object reference. + /// This overload is typically invoked from .NET code by . + /// + /// The . + public void ReleaseDotNetObject(DotNetObjectRef dotNetObjectRef) + { + lock (_storageLock) + { + if (_trackedIdsByRef.TryGetValue(dotNetObjectRef, out var dotNetObjectId)) + { + _trackedRefsById.Remove(dotNetObjectId); + _trackedIdsByRef.Remove(dotNetObjectRef); + } + } + } + + private void EnsureDotNetObjectTracked(DotNetObjectRef dotNetObjectRef, out long dotNetObjectId) + { + dotNetObjectRef.EnsureAttachedToJsRuntime(_jsRuntime); + + lock (_storageLock) + { + // Assign an ID only if it doesn't already have one + if (!_trackedIdsByRef.TryGetValue(dotNetObjectRef, out dotNetObjectId)) + { + dotNetObjectId = _nextId++; + _trackedRefsById.Add(dotNetObjectId, dotNetObjectRef); + _trackedIdsByRef.Add(dotNetObjectRef, dotNetObjectId); + } + } + } + } +} diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSAsyncCallResult.cs b/src/JSInterop/Microsoft.JSInterop/src/JSAsyncCallResult.cs new file mode 100644 index 0000000000..d46517eddc --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/src/JSAsyncCallResult.cs @@ -0,0 +1,36 @@ +// 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.JSInterop.Internal +{ + // This type takes care of a special case in handling the result of an async call from + // .NET to JS. The information about what type the result should be exists only on the + // corresponding TaskCompletionSource. We don't have that information at the time + // that we deserialize the incoming argsJson before calling DotNetDispatcher.EndInvoke. + // Declaring the EndInvoke parameter type as JSAsyncCallResult defers the deserialization + // until later when we have access to the TaskCompletionSource. + // + // There's no reason why developers would need anything similar to this in user code, + // because this is the mechanism by which we resolve the incoming argsJson to the correct + // user types before completing calls. + // + // It's marked as 'public' only because it has to be for use as an argument on a + // [JSInvokable] method. + + /// + /// Intended for framework use only. + /// + public class JSAsyncCallResult + { + internal object ResultOrException { get; } + + /// + /// Constructs an instance of . + /// + /// The result of the call. + internal JSAsyncCallResult(object resultOrException) + { + ResultOrException = resultOrException; + } + } +} diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSException.cs b/src/JSInterop/Microsoft.JSInterop/src/JSException.cs new file mode 100644 index 0000000000..2929f69311 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/src/JSException.cs @@ -0,0 +1,21 @@ +// 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.JSInterop +{ + /// + /// Represents errors that occur during an interop call from .NET to JavaScript. + /// + public class JSException : Exception + { + /// + /// Constructs an instance of . + /// + /// The exception message. + public JSException(string message) : base(message) + { + } + } +} diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntimeBase.cs b/src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntimeBase.cs new file mode 100644 index 0000000000..49a47d0595 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntimeBase.cs @@ -0,0 +1,32 @@ +// 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.JSInterop +{ + /// + /// Abstract base class for an in-process JavaScript runtime. + /// + public abstract class JSInProcessRuntimeBase : JSRuntimeBase, IJSInProcessRuntime + { + /// + /// Invokes the specified JavaScript function synchronously. + /// + /// The JSON-serializable return type. + /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. + /// JSON-serializable arguments. + /// An instance of obtained by JSON-deserializing the return value. + public T Invoke(string identifier, params object[] args) + { + var resultJson = InvokeJS(identifier, Json.Serialize(args, ArgSerializerStrategy)); + return Json.Deserialize(resultJson, ArgSerializerStrategy); + } + + /// + /// Performs a synchronous function invocation. + /// + /// The identifier for the function to invoke. + /// A JSON representation of the arguments. + /// A JSON representation of the result. + protected abstract string InvokeJS(string identifier, string argsJson); + } +} diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSInvokableAttribute.cs b/src/JSInterop/Microsoft.JSInterop/src/JSInvokableAttribute.cs new file mode 100644 index 0000000000..e037078cba --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/src/JSInvokableAttribute.cs @@ -0,0 +1,48 @@ +// 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.JSInterop +{ + /// + /// Identifies a .NET method as allowing invocation from JavaScript code. + /// Any method marked with this attribute may receive arbitrary parameter values + /// from untrusted callers. All inputs should be validated carefully. + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + public class JSInvokableAttribute : Attribute + { + /// + /// Gets the identifier for the method. The identifier must be unique within the scope + /// of an assembly. + /// + /// If not set, the identifier is taken from the name of the method. In this case the + /// method name must be unique within the assembly. + /// + public string Identifier { get; } + + /// + /// Constructs an instance of without setting + /// an identifier for the method. + /// + public JSInvokableAttribute() + { + } + + /// + /// Constructs an instance of using the specified + /// identifier. + /// + /// An identifier for the method, which must be unique within the scope of the assembly. + public JSInvokableAttribute(string identifier) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException("Cannot be null or empty", nameof(identifier)); + } + + Identifier = identifier; + } + } +} diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs new file mode 100644 index 0000000000..ae097ca68e --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs @@ -0,0 +1,30 @@ +// 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.Threading; + +namespace Microsoft.JSInterop +{ + /// + /// Provides mechanisms for accessing the current . + /// + public static class JSRuntime + { + private static readonly AsyncLocal _currentJSRuntime = new AsyncLocal(); + + internal static IJSRuntime Current => _currentJSRuntime.Value; + + /// + /// Sets the current JS runtime to the supplied instance. + /// + /// This is intended for framework use. Developers should not normally need to call this method. + /// + /// The new current . + public static void SetCurrentJSRuntime(IJSRuntime instance) + { + _currentJSRuntime.Value = instance + ?? throw new ArgumentNullException(nameof(instance)); + } + } +} diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeBase.cs b/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeBase.cs new file mode 100644 index 0000000000..d18bc7f4fe --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeBase.cs @@ -0,0 +1,121 @@ +// 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.Runtime.ExceptionServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.JSInterop +{ + /// + /// Abstract base class for a JavaScript runtime. + /// + public abstract class JSRuntimeBase : IJSRuntime + { + private long _nextPendingTaskId = 1; // Start at 1 because zero signals "no response needed" + private readonly ConcurrentDictionary _pendingTasks + = new ConcurrentDictionary(); + + internal InteropArgSerializerStrategy ArgSerializerStrategy { get; } + + /// + /// Constructs an instance of . + /// + public JSRuntimeBase() + { + ArgSerializerStrategy = new InteropArgSerializerStrategy(this); + } + + /// + public void UntrackObjectRef(DotNetObjectRef dotNetObjectRef) + => ArgSerializerStrategy.ReleaseDotNetObject(dotNetObjectRef); + + /// + /// Invokes the specified JavaScript function asynchronously. + /// + /// The JSON-serializable return type. + /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. + /// JSON-serializable arguments. + /// An instance of obtained by JSON-deserializing the return value. + public Task InvokeAsync(string identifier, params object[] args) + { + // We might consider also adding a default timeout here in case we don't want to + // risk a memory leak in the scenario where the JS-side code is failing to complete + // the operation. + + var taskId = Interlocked.Increment(ref _nextPendingTaskId); + var tcs = new TaskCompletionSource(); + _pendingTasks[taskId] = tcs; + + try + { + var argsJson = args?.Length > 0 + ? Json.Serialize(args, ArgSerializerStrategy) + : null; + BeginInvokeJS(taskId, identifier, argsJson); + return tcs.Task; + } + catch + { + _pendingTasks.TryRemove(taskId, out _); + throw; + } + } + + /// + /// Begins an asynchronous function invocation. + /// + /// The identifier for the function invocation, or zero if no async callback is required. + /// The identifier for the function to invoke. + /// A JSON representation of the arguments. + protected abstract void BeginInvokeJS(long asyncHandle, string identifier, string argsJson); + + internal void EndInvokeDotNet(string callId, bool success, object resultOrException) + { + // For failures, the common case is to call EndInvokeDotNet with the Exception object. + // For these we'll serialize as something that's useful to receive on the JS side. + // If the value is not an Exception, we'll just rely on it being directly JSON-serializable. + if (!success && resultOrException is Exception) + { + resultOrException = resultOrException.ToString(); + } + else if (!success && resultOrException is ExceptionDispatchInfo edi) + { + resultOrException = edi.SourceException.ToString(); + } + + // We pass 0 as the async handle because we don't want the JS-side code to + // send back any notification (we're just providing a result for an existing async call) + BeginInvokeJS(0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", Json.Serialize(new[] { + callId, + success, + resultOrException + }, ArgSerializerStrategy)); + } + + internal void EndInvokeJS(long asyncHandle, bool succeeded, object resultOrException) + { + if (!_pendingTasks.TryRemove(asyncHandle, out var tcs)) + { + throw new ArgumentException($"There is no pending task with handle '{asyncHandle}'."); + } + + if (succeeded) + { + var resultType = TaskGenericsUtil.GetTaskCompletionSourceResultType(tcs); + if (resultOrException is SimpleJson.JsonObject || resultOrException is SimpleJson.JsonArray) + { + resultOrException = ArgSerializerStrategy.DeserializeObject(resultOrException, resultType); + } + + TaskGenericsUtil.SetTaskCompletionSourceResult(tcs, resultOrException); + } + else + { + TaskGenericsUtil.SetTaskCompletionSourceException(tcs, new JSException(resultOrException.ToString())); + } + } + } +} diff --git a/src/JSInterop/Microsoft.JSInterop/src/Json/CamelCase.cs b/src/JSInterop/Microsoft.JSInterop/src/Json/CamelCase.cs new file mode 100644 index 0000000000..8caae1387b --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/src/Json/CamelCase.cs @@ -0,0 +1,59 @@ +// 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.JSInterop +{ + internal static class CamelCase + { + public static string MemberNameToCamelCase(string value) + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentException( + $"The value '{value ?? "null"}' is not a valid member name.", + nameof(value)); + } + + // If we don't need to modify the value, bail out without creating a char array + if (!char.IsUpper(value[0])) + { + return value; + } + + // We have to modify at least one character + var chars = value.ToCharArray(); + + var length = chars.Length; + if (length < 2 || !char.IsUpper(chars[1])) + { + // Only the first character needs to be modified + // Note that this branch is functionally necessary, because the 'else' branch below + // never looks at char[1]. It's always looking at the n+2 character. + chars[0] = char.ToLowerInvariant(chars[0]); + } + else + { + // If chars[0] and chars[1] are both upper, then we'll lowercase the first char plus + // any consecutive uppercase ones, stopping if we find any char that is followed by a + // non-uppercase one + var i = 0; + while (i < length) + { + chars[i] = char.ToLowerInvariant(chars[i]); + + i++; + + // If the next-plus-one char isn't also uppercase, then we're now on the last uppercase, so stop + if (i < length - 1 && !char.IsUpper(chars[i + 1])) + { + break; + } + } + } + + return new string(chars); + } + } +} diff --git a/src/JSInterop/Microsoft.JSInterop/src/Json/Json.cs b/src/JSInterop/Microsoft.JSInterop/src/Json/Json.cs new file mode 100644 index 0000000000..7275dfe427 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/src/Json/Json.cs @@ -0,0 +1,39 @@ +// 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.JSInterop +{ + /// + /// Provides mechanisms for converting between .NET objects and JSON strings for use + /// when making calls to JavaScript functions via . + /// + /// Warning: This is not intended as a general-purpose JSON library. It is only intended + /// for use when making calls via . Eventually its implementation + /// will be replaced by something more general-purpose. + /// + public static class Json + { + /// + /// Serializes the value as a JSON string. + /// + /// The value to serialize. + /// The JSON string. + public static string Serialize(object value) + => SimpleJson.SimpleJson.SerializeObject(value); + + internal static string Serialize(object value, SimpleJson.IJsonSerializerStrategy serializerStrategy) + => SimpleJson.SimpleJson.SerializeObject(value, serializerStrategy); + + /// + /// Deserializes the JSON string, creating an object of the specified generic type. + /// + /// The type of object to create. + /// The JSON string. + /// An object of the specified type. + public static T Deserialize(string json) + => SimpleJson.SimpleJson.DeserializeObject(json); + + internal static T Deserialize(string json, SimpleJson.IJsonSerializerStrategy serializerStrategy) + => SimpleJson.SimpleJson.DeserializeObject(json, serializerStrategy); + } +} diff --git a/src/JSInterop/Microsoft.JSInterop/src/Json/SimpleJson/README.txt b/src/JSInterop/Microsoft.JSInterop/src/Json/SimpleJson/README.txt new file mode 100644 index 0000000000..5e58eb7106 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/src/Json/SimpleJson/README.txt @@ -0,0 +1,24 @@ +SimpleJson is from https://github.com/facebook-csharp-sdk/simple-json + +LICENSE (from https://github.com/facebook-csharp-sdk/simple-json/blob/08b6871e8f63e866810d25e7a03c48502c9a234b/LICENSE.txt): +===== +Copyright (c) 2011, The Outercurve Foundation + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/src/JSInterop/Microsoft.JSInterop/src/Json/SimpleJson/SimpleJson.cs b/src/JSInterop/Microsoft.JSInterop/src/Json/SimpleJson/SimpleJson.cs new file mode 100644 index 0000000000..d12c6fae30 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/src/Json/SimpleJson/SimpleJson.cs @@ -0,0 +1,2201 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) 2011, The Outercurve Foundation. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.opensource.org/licenses/mit-license.php +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Nathan Totten (ntotten.com), Jim Zimmerman (jimzimmerman.com) and Prabir Shrestha (prabir.me) +// https://github.com/facebook-csharp-sdk/simple-json +//----------------------------------------------------------------------- + +// VERSION: + +// NOTE: uncomment the following line to make SimpleJson class internal. +#define SIMPLE_JSON_INTERNAL + +// NOTE: uncomment the following line to make JsonArray and JsonObject class internal. +#define SIMPLE_JSON_OBJARRAYINTERNAL + +// NOTE: uncomment the following line to enable dynamic support. +//#define SIMPLE_JSON_DYNAMIC + +// NOTE: uncomment the following line to enable DataContract support. +//#define SIMPLE_JSON_DATACONTRACT + +// NOTE: uncomment the following line to enable IReadOnlyCollection and IReadOnlyList support. +//#define SIMPLE_JSON_READONLY_COLLECTIONS + +// NOTE: uncomment the following line to disable linq expressions/compiled lambda (better performance) instead of method.invoke(). +// define if you are using .net framework <= 3.0 or < WP7.5 +#define SIMPLE_JSON_NO_LINQ_EXPRESSION + +// NOTE: uncomment the following line if you are compiling under Window Metro style application/library. +// usually already defined in properties +//#define NETFX_CORE; + +// If you are targetting WinStore, WP8 and NET4.5+ PCL make sure to #define SIMPLE_JSON_TYPEINFO; + +// original json parsing code from http://techblog.procurios.nl/k/618/news/view/14605/14863/How-do-I-write-my-own-parser-for-JSON.html + +#if NETFX_CORE +#define SIMPLE_JSON_TYPEINFO +#endif + +using System; +using System.CodeDom.Compiler; +using System.Collections; +using System.Collections.Generic; +#if !SIMPLE_JSON_NO_LINQ_EXPRESSION +using System.Linq.Expressions; +#endif +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +#if SIMPLE_JSON_DYNAMIC +using System.Dynamic; +#endif +using System.Globalization; +using System.Reflection; +using System.Runtime.Serialization; +using System.Text; +using Microsoft.JSInterop; +using SimpleJson.Reflection; + +// ReSharper disable LoopCanBeConvertedToQuery +// ReSharper disable RedundantExplicitArrayCreation +// ReSharper disable SuggestUseVarKeywordEvident +namespace SimpleJson +{ + /// + /// Represents the json array. + /// + [GeneratedCode("simple-json", "1.0.0")] + [EditorBrowsable(EditorBrowsableState.Never)] + [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")] +#if SIMPLE_JSON_OBJARRAYINTERNAL + internal +#else + public +#endif + class JsonArray : List + { + /// + /// Initializes a new instance of the class. + /// + public JsonArray() { } + + /// + /// Initializes a new instance of the class. + /// + /// The capacity of the json array. + public JsonArray(int capacity) : base(capacity) { } + + /// + /// The json representation of the array. + /// + /// The json representation of the array. + public override string ToString() + { + return SimpleJson.SerializeObject(this) ?? string.Empty; + } + } + + /// + /// Represents the json object. + /// + [GeneratedCode("simple-json", "1.0.0")] + [EditorBrowsable(EditorBrowsableState.Never)] + [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")] +#if SIMPLE_JSON_OBJARRAYINTERNAL + internal +#else + public +#endif + class JsonObject : +#if SIMPLE_JSON_DYNAMIC + DynamicObject, +#endif + IDictionary + { + /// + /// The internal member dictionary. + /// + private readonly Dictionary _members; + + /// + /// Initializes a new instance of . + /// + public JsonObject() + { + _members = new Dictionary(); + } + + /// + /// Initializes a new instance of . + /// + /// The implementation to use when comparing keys, or null to use the default for the type of the key. + public JsonObject(IEqualityComparer comparer) + { + _members = new Dictionary(comparer); + } + + /// + /// Gets the at the specified index. + /// + /// + public object this[int index] + { + get { return GetAtIndex(_members, index); } + } + + internal static object GetAtIndex(IDictionary obj, int index) + { + if (obj == null) + throw new ArgumentNullException("obj"); + if (index >= obj.Count) + throw new ArgumentOutOfRangeException("index"); + int i = 0; + foreach (KeyValuePair o in obj) + if (i++ == index) return o.Value; + return null; + } + + /// + /// Adds the specified key. + /// + /// The key. + /// The value. + public void Add(string key, object value) + { + _members.Add(key, value); + } + + /// + /// Determines whether the specified key contains key. + /// + /// The key. + /// + /// true if the specified key contains key; otherwise, false. + /// + public bool ContainsKey(string key) + { + return _members.ContainsKey(key); + } + + /// + /// Gets the keys. + /// + /// The keys. + public ICollection Keys + { + get { return _members.Keys; } + } + + /// + /// Removes the specified key. + /// + /// The key. + /// + public bool Remove(string key) + { + return _members.Remove(key); + } + + /// + /// Tries the get value. + /// + /// The key. + /// The value. + /// + public bool TryGetValue(string key, out object value) + { + return _members.TryGetValue(key, out value); + } + + /// + /// Gets the values. + /// + /// The values. + public ICollection Values + { + get { return _members.Values; } + } + + /// + /// Gets or sets the with the specified key. + /// + /// + public object this[string key] + { + get { return _members[key]; } + set { _members[key] = value; } + } + + /// + /// Adds the specified item. + /// + /// The item. + public void Add(KeyValuePair item) + { + _members.Add(item.Key, item.Value); + } + + /// + /// Clears this instance. + /// + public void Clear() + { + _members.Clear(); + } + + /// + /// Determines whether [contains] [the specified item]. + /// + /// The item. + /// + /// true if [contains] [the specified item]; otherwise, false. + /// + public bool Contains(KeyValuePair item) + { + return _members.ContainsKey(item.Key) && _members[item.Key] == item.Value; + } + + /// + /// Copies to. + /// + /// The array. + /// Index of the array. + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + if (array == null) throw new ArgumentNullException("array"); + int num = Count; + foreach (KeyValuePair kvp in this) + { + array[arrayIndex++] = kvp; + if (--num <= 0) + return; + } + } + + /// + /// Gets the count. + /// + /// The count. + public int Count + { + get { return _members.Count; } + } + + /// + /// Gets a value indicating whether this instance is read only. + /// + /// + /// true if this instance is read only; otherwise, false. + /// + public bool IsReadOnly + { + get { return false; } + } + + /// + /// Removes the specified item. + /// + /// The item. + /// + public bool Remove(KeyValuePair item) + { + return _members.Remove(item.Key); + } + + /// + /// Gets the enumerator. + /// + /// + public IEnumerator> GetEnumerator() + { + return _members.GetEnumerator(); + } + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// + /// An object that can be used to iterate through the collection. + /// + IEnumerator IEnumerable.GetEnumerator() + { + return _members.GetEnumerator(); + } + + /// + /// Returns a json that represents the current . + /// + /// + /// A json that represents the current . + /// + public override string ToString() + { + return SimpleJson.SerializeObject(this); + } + +#if SIMPLE_JSON_DYNAMIC + /// + /// Provides implementation for type conversion operations. Classes derived from the class can override this method to specify dynamic behavior for operations that convert an object from one type to another. + /// + /// Provides information about the conversion operation. The binder.Type property provides the type to which the object must be converted. For example, for the statement (String)sampleObject in C# (CType(sampleObject, Type) in Visual Basic), where sampleObject is an instance of the class derived from the class, binder.Type returns the type. The binder.Explicit property provides information about the kind of conversion that occurs. It returns true for explicit conversion and false for implicit conversion. + /// The result of the type conversion operation. + /// + /// Alwasy returns true. + /// + public override bool TryConvert(ConvertBinder binder, out object result) + { + // + if (binder == null) + throw new ArgumentNullException("binder"); + // + Type targetType = binder.Type; + + if ((targetType == typeof(IEnumerable)) || + (targetType == typeof(IEnumerable>)) || + (targetType == typeof(IDictionary)) || + (targetType == typeof(IDictionary))) + { + result = this; + return true; + } + + return base.TryConvert(binder, out result); + } + + /// + /// Provides the implementation for operations that delete an object member. This method is not intended for use in C# or Visual Basic. + /// + /// Provides information about the deletion. + /// + /// Alwasy returns true. + /// + public override bool TryDeleteMember(DeleteMemberBinder binder) + { + // + if (binder == null) + throw new ArgumentNullException("binder"); + // + return _members.Remove(binder.Name); + } + + /// + /// Provides the implementation for operations that get a value by index. Classes derived from the class can override this method to specify dynamic behavior for indexing operations. + /// + /// Provides information about the operation. + /// The indexes that are used in the operation. For example, for the sampleObject[3] operation in C# (sampleObject(3) in Visual Basic), where sampleObject is derived from the DynamicObject class, is equal to 3. + /// The result of the index operation. + /// + /// Alwasy returns true. + /// + public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result) + { + if (indexes == null) throw new ArgumentNullException("indexes"); + if (indexes.Length == 1) + { + result = ((IDictionary)this)[(string)indexes[0]]; + return true; + } + result = null; + return true; + } + + /// + /// Provides the implementation for operations that get member values. Classes derived from the class can override this method to specify dynamic behavior for operations such as getting a value for a property. + /// + /// Provides information about the object that called the dynamic operation. The binder.Name property provides the name of the member on which the dynamic operation is performed. For example, for the Console.WriteLine(sampleObject.SampleProperty) statement, where sampleObject is an instance of the class derived from the class, binder.Name returns "SampleProperty". The binder.IgnoreCase property specifies whether the member name is case-sensitive. + /// The result of the get operation. For example, if the method is called for a property, you can assign the property value to . + /// + /// Alwasy returns true. + /// + public override bool TryGetMember(GetMemberBinder binder, out object result) + { + object value; + if (_members.TryGetValue(binder.Name, out value)) + { + result = value; + return true; + } + result = null; + return true; + } + + /// + /// Provides the implementation for operations that set a value by index. Classes derived from the class can override this method to specify dynamic behavior for operations that access objects by a specified index. + /// + /// Provides information about the operation. + /// The indexes that are used in the operation. For example, for the sampleObject[3] = 10 operation in C# (sampleObject(3) = 10 in Visual Basic), where sampleObject is derived from the class, is equal to 3. + /// The value to set to the object that has the specified index. For example, for the sampleObject[3] = 10 operation in C# (sampleObject(3) = 10 in Visual Basic), where sampleObject is derived from the class, is equal to 10. + /// + /// true if the operation is successful; otherwise, false. If this method returns false, the run-time binder of the language determines the behavior. (In most cases, a language-specific run-time exception is thrown. + /// + public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object value) + { + if (indexes == null) throw new ArgumentNullException("indexes"); + if (indexes.Length == 1) + { + ((IDictionary)this)[(string)indexes[0]] = value; + return true; + } + return base.TrySetIndex(binder, indexes, value); + } + + /// + /// Provides the implementation for operations that set member values. Classes derived from the class can override this method to specify dynamic behavior for operations such as setting a value for a property. + /// + /// Provides information about the object that called the dynamic operation. The binder.Name property provides the name of the member to which the value is being assigned. For example, for the statement sampleObject.SampleProperty = "Test", where sampleObject is an instance of the class derived from the class, binder.Name returns "SampleProperty". The binder.IgnoreCase property specifies whether the member name is case-sensitive. + /// The value to set to the member. For example, for sampleObject.SampleProperty = "Test", where sampleObject is an instance of the class derived from the class, the is "Test". + /// + /// true if the operation is successful; otherwise, false. If this method returns false, the run-time binder of the language determines the behavior. (In most cases, a language-specific run-time exception is thrown.) + /// + public override bool TrySetMember(SetMemberBinder binder, object value) + { + // + if (binder == null) + throw new ArgumentNullException("binder"); + // + _members[binder.Name] = value; + return true; + } + + /// + /// Returns the enumeration of all dynamic member names. + /// + /// + /// A sequence that contains dynamic member names. + /// + public override IEnumerable GetDynamicMemberNames() + { + foreach (var key in Keys) + yield return key; + } +#endif + } +} + +namespace SimpleJson +{ + /// + /// This class encodes and decodes JSON strings. + /// Spec. details, see http://www.json.org/ + /// + /// JSON uses Arrays and Objects. These correspond here to the datatypes JsonArray(IList<object>) and JsonObject(IDictionary<string,object>). + /// All numbers are parsed to doubles. + /// + [GeneratedCode("simple-json", "1.0.0")] +#if SIMPLE_JSON_INTERNAL + internal +#else + public +#endif + static class SimpleJson + { + private const int TOKEN_NONE = 0; + private const int TOKEN_CURLY_OPEN = 1; + private const int TOKEN_CURLY_CLOSE = 2; + private const int TOKEN_SQUARED_OPEN = 3; + private const int TOKEN_SQUARED_CLOSE = 4; + private const int TOKEN_COLON = 5; + private const int TOKEN_COMMA = 6; + private const int TOKEN_STRING = 7; + private const int TOKEN_NUMBER = 8; + private const int TOKEN_TRUE = 9; + private const int TOKEN_FALSE = 10; + private const int TOKEN_NULL = 11; + private const int BUILDER_CAPACITY = 2000; + + private static readonly char[] EscapeTable; + private static readonly char[] EscapeCharacters = new char[] { '"', '\\', '\b', '\f', '\n', '\r', '\t' }; + private static readonly string EscapeCharactersString = new string(EscapeCharacters); + + static SimpleJson() + { + EscapeTable = new char[93]; + EscapeTable['"'] = '"'; + EscapeTable['\\'] = '\\'; + EscapeTable['\b'] = 'b'; + EscapeTable['\f'] = 'f'; + EscapeTable['\n'] = 'n'; + EscapeTable['\r'] = 'r'; + EscapeTable['\t'] = 't'; + } + + /// + /// Parses the string json into a value + /// + /// A JSON string. + /// An IList<object>, a IDictionary<string,object>, a double, a string, null, true, or false + public static object DeserializeObject(string json) + { + object obj; + if (TryDeserializeObject(json, out obj)) + return obj; + throw new SerializationException("Invalid JSON string"); + } + + /// + /// Try parsing the json string into a value. + /// + /// + /// A JSON string. + /// + /// + /// The object. + /// + /// + /// Returns true if successful otherwise false. + /// + [SuppressMessage("Microsoft.Design", "CA1007:UseGenericsWhereAppropriate", Justification="Need to support .NET 2")] + public static bool TryDeserializeObject(string json, out object obj) + { + bool success = true; + if (json != null) + { + char[] charArray = json.ToCharArray(); + int index = 0; + obj = ParseValue(charArray, ref index, ref success); + } + else + obj = null; + + return success; + } + + public static object DeserializeObject(string json, Type type, IJsonSerializerStrategy jsonSerializerStrategy) + { + object jsonObject = DeserializeObject(json); + return type == null || jsonObject != null && ReflectionUtils.IsAssignableFrom(jsonObject.GetType(), type) + ? jsonObject + : (jsonSerializerStrategy ?? CurrentJsonSerializerStrategy).DeserializeObject(jsonObject, type); + } + + public static object DeserializeObject(string json, Type type) + { + return DeserializeObject(json, type, null); + } + + public static T DeserializeObject(string json, IJsonSerializerStrategy jsonSerializerStrategy) + { + return (T)DeserializeObject(json, typeof(T), jsonSerializerStrategy); + } + + public static T DeserializeObject(string json) + { + return (T)DeserializeObject(json, typeof(T), null); + } + + /// + /// Converts a IDictionary<string,object> / IList<object> object into a JSON string + /// + /// A IDictionary<string,object> / IList<object> + /// Serializer strategy to use + /// A JSON encoded string, or null if object 'json' is not serializable + public static string SerializeObject(object json, IJsonSerializerStrategy jsonSerializerStrategy) + { + StringBuilder builder = new StringBuilder(BUILDER_CAPACITY); + bool success = SerializeValue(jsonSerializerStrategy, json, builder); + return (success ? builder.ToString() : null); + } + + public static string SerializeObject(object json) + { + return SerializeObject(json, CurrentJsonSerializerStrategy); + } + + public static string EscapeToJavascriptString(string jsonString) + { + if (string.IsNullOrEmpty(jsonString)) + return jsonString; + + StringBuilder sb = new StringBuilder(); + char c; + + for (int i = 0; i < jsonString.Length; ) + { + c = jsonString[i++]; + + if (c == '\\') + { + int remainingLength = jsonString.Length - i; + if (remainingLength >= 2) + { + char lookahead = jsonString[i]; + if (lookahead == '\\') + { + sb.Append('\\'); + ++i; + } + else if (lookahead == '"') + { + sb.Append("\""); + ++i; + } + else if (lookahead == 't') + { + sb.Append('\t'); + ++i; + } + else if (lookahead == 'b') + { + sb.Append('\b'); + ++i; + } + else if (lookahead == 'n') + { + sb.Append('\n'); + ++i; + } + else if (lookahead == 'r') + { + sb.Append('\r'); + ++i; + } + } + } + else + { + sb.Append(c); + } + } + return sb.ToString(); + } + + static IDictionary ParseObject(char[] json, ref int index, ref bool success) + { + IDictionary table = new JsonObject(); + int token; + + // { + NextToken(json, ref index); + + bool done = false; + while (!done) + { + token = LookAhead(json, index); + if (token == TOKEN_NONE) + { + success = false; + return null; + } + else if (token == TOKEN_COMMA) + NextToken(json, ref index); + else if (token == TOKEN_CURLY_CLOSE) + { + NextToken(json, ref index); + return table; + } + else + { + // name + string name = ParseString(json, ref index, ref success); + if (!success) + { + success = false; + return null; + } + // : + token = NextToken(json, ref index); + if (token != TOKEN_COLON) + { + success = false; + return null; + } + // value + object value = ParseValue(json, ref index, ref success); + if (!success) + { + success = false; + return null; + } + table[name] = value; + } + } + return table; + } + + static JsonArray ParseArray(char[] json, ref int index, ref bool success) + { + JsonArray array = new JsonArray(); + + // [ + NextToken(json, ref index); + + bool done = false; + while (!done) + { + int token = LookAhead(json, index); + if (token == TOKEN_NONE) + { + success = false; + return null; + } + else if (token == TOKEN_COMMA) + NextToken(json, ref index); + else if (token == TOKEN_SQUARED_CLOSE) + { + NextToken(json, ref index); + break; + } + else + { + object value = ParseValue(json, ref index, ref success); + if (!success) + return null; + array.Add(value); + } + } + return array; + } + + static object ParseValue(char[] json, ref int index, ref bool success) + { + switch (LookAhead(json, index)) + { + case TOKEN_STRING: + return ParseString(json, ref index, ref success); + case TOKEN_NUMBER: + return ParseNumber(json, ref index, ref success); + case TOKEN_CURLY_OPEN: + return ParseObject(json, ref index, ref success); + case TOKEN_SQUARED_OPEN: + return ParseArray(json, ref index, ref success); + case TOKEN_TRUE: + NextToken(json, ref index); + return true; + case TOKEN_FALSE: + NextToken(json, ref index); + return false; + case TOKEN_NULL: + NextToken(json, ref index); + return null; + case TOKEN_NONE: + break; + } + success = false; + return null; + } + + static string ParseString(char[] json, ref int index, ref bool success) + { + StringBuilder s = new StringBuilder(BUILDER_CAPACITY); + char c; + + EatWhitespace(json, ref index); + + // " + c = json[index++]; + bool complete = false; + while (!complete) + { + if (index == json.Length) + break; + + c = json[index++]; + if (c == '"') + { + complete = true; + break; + } + else if (c == '\\') + { + if (index == json.Length) + break; + c = json[index++]; + if (c == '"') + s.Append('"'); + else if (c == '\\') + s.Append('\\'); + else if (c == '/') + s.Append('/'); + else if (c == 'b') + s.Append('\b'); + else if (c == 'f') + s.Append('\f'); + else if (c == 'n') + s.Append('\n'); + else if (c == 'r') + s.Append('\r'); + else if (c == 't') + s.Append('\t'); + else if (c == 'u') + { + int remainingLength = json.Length - index; + if (remainingLength >= 4) + { + // parse the 32 bit hex into an integer codepoint + uint codePoint; + if (!(success = UInt32.TryParse(new string(json, index, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out codePoint))) + return ""; + + // convert the integer codepoint to a unicode char and add to string + if (0xD800 <= codePoint && codePoint <= 0xDBFF) // if high surrogate + { + index += 4; // skip 4 chars + remainingLength = json.Length - index; + if (remainingLength >= 6) + { + uint lowCodePoint; + if (new string(json, index, 2) == "\\u" && UInt32.TryParse(new string(json, index + 2, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out lowCodePoint)) + { + if (0xDC00 <= lowCodePoint && lowCodePoint <= 0xDFFF) // if low surrogate + { + s.Append((char)codePoint); + s.Append((char)lowCodePoint); + index += 6; // skip 6 chars + continue; + } + } + } + success = false; // invalid surrogate pair + return ""; + } + s.Append(ConvertFromUtf32((int)codePoint)); + // skip 4 chars + index += 4; + } + else + break; + } + } + else + s.Append(c); + } + if (!complete) + { + success = false; + return null; + } + return s.ToString(); + } + + private static string ConvertFromUtf32(int utf32) + { + // http://www.java2s.com/Open-Source/CSharp/2.6.4-mono-.net-core/System/System/Char.cs.htm + if (utf32 < 0 || utf32 > 0x10FFFF) + throw new ArgumentOutOfRangeException("utf32", "The argument must be from 0 to 0x10FFFF."); + if (0xD800 <= utf32 && utf32 <= 0xDFFF) + throw new ArgumentOutOfRangeException("utf32", "The argument must not be in surrogate pair range."); + if (utf32 < 0x10000) + return new string((char)utf32, 1); + utf32 -= 0x10000; + return new string(new char[] { (char)((utf32 >> 10) + 0xD800), (char)(utf32 % 0x0400 + 0xDC00) }); + } + + static object ParseNumber(char[] json, ref int index, ref bool success) + { + EatWhitespace(json, ref index); + int lastIndex = GetLastIndexOfNumber(json, index); + int charLength = (lastIndex - index) + 1; + object returnNumber; + string str = new string(json, index, charLength); + if (str.IndexOf(".", StringComparison.OrdinalIgnoreCase) != -1 || str.IndexOf("e", StringComparison.OrdinalIgnoreCase) != -1) + { + double number; + success = double.TryParse(new string(json, index, charLength), NumberStyles.Any, CultureInfo.InvariantCulture, out number); + returnNumber = number; + } + else + { + long number; + success = long.TryParse(new string(json, index, charLength), NumberStyles.Any, CultureInfo.InvariantCulture, out number); + returnNumber = number; + } + index = lastIndex + 1; + return returnNumber; + } + + static int GetLastIndexOfNumber(char[] json, int index) + { + int lastIndex; + for (lastIndex = index; lastIndex < json.Length; lastIndex++) + if ("0123456789+-.eE".IndexOf(json[lastIndex]) == -1) break; + return lastIndex - 1; + } + + static void EatWhitespace(char[] json, ref int index) + { + for (; index < json.Length; index++) { + switch (json[index]) { + case ' ': + case '\t': + case '\n': + case '\r': + case '\b': + case '\f': + break; + default: + return; + } + } + } + + static int LookAhead(char[] json, int index) + { + int saveIndex = index; + return NextToken(json, ref saveIndex); + } + + [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")] + static int NextToken(char[] json, ref int index) + { + EatWhitespace(json, ref index); + if (index == json.Length) + return TOKEN_NONE; + char c = json[index]; + index++; + switch (c) + { + case '{': + return TOKEN_CURLY_OPEN; + case '}': + return TOKEN_CURLY_CLOSE; + case '[': + return TOKEN_SQUARED_OPEN; + case ']': + return TOKEN_SQUARED_CLOSE; + case ',': + return TOKEN_COMMA; + case '"': + return TOKEN_STRING; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case '-': + return TOKEN_NUMBER; + case ':': + return TOKEN_COLON; + } + index--; + int remainingLength = json.Length - index; + // false + if (remainingLength >= 5) + { + if (json[index] == 'f' && json[index + 1] == 'a' && json[index + 2] == 'l' && json[index + 3] == 's' && json[index + 4] == 'e') + { + index += 5; + return TOKEN_FALSE; + } + } + // true + if (remainingLength >= 4) + { + if (json[index] == 't' && json[index + 1] == 'r' && json[index + 2] == 'u' && json[index + 3] == 'e') + { + index += 4; + return TOKEN_TRUE; + } + } + // null + if (remainingLength >= 4) + { + if (json[index] == 'n' && json[index + 1] == 'u' && json[index + 2] == 'l' && json[index + 3] == 'l') + { + index += 4; + return TOKEN_NULL; + } + } + return TOKEN_NONE; + } + + static bool SerializeValue(IJsonSerializerStrategy jsonSerializerStrategy, object value, StringBuilder builder) + { + bool success = true; + string stringValue = value as string; + if (stringValue != null) + success = SerializeString(stringValue, builder); + else + { + IDictionary dict = value as IDictionary; + if (dict != null) + { + success = SerializeObject(jsonSerializerStrategy, dict.Keys, dict.Values, builder); + } + else + { + IDictionary stringDictionary = value as IDictionary; + if (stringDictionary != null) + { + success = SerializeObject(jsonSerializerStrategy, stringDictionary.Keys, stringDictionary.Values, builder); + } + else + { + IEnumerable enumerableValue = value as IEnumerable; + if (enumerableValue != null) + success = SerializeArray(jsonSerializerStrategy, enumerableValue, builder); + else if (IsNumeric(value)) + success = SerializeNumber(value, builder); + else if (value is bool) + builder.Append((bool)value ? "true" : "false"); + else if (value == null) + builder.Append("null"); + else + { + object serializedObject; + success = jsonSerializerStrategy.TrySerializeNonPrimitiveObject(value, out serializedObject); + if (success) + SerializeValue(jsonSerializerStrategy, serializedObject, builder); + } + } + } + } + return success; + } + + static bool SerializeObject(IJsonSerializerStrategy jsonSerializerStrategy, IEnumerable keys, IEnumerable values, StringBuilder builder) + { + builder.Append("{"); + IEnumerator ke = keys.GetEnumerator(); + IEnumerator ve = values.GetEnumerator(); + bool first = true; + while (ke.MoveNext() && ve.MoveNext()) + { + object key = ke.Current; + object value = ve.Current; + if (!first) + builder.Append(","); + string stringKey = key as string; + if (stringKey != null) + SerializeString(stringKey, builder); + else + if (!SerializeValue(jsonSerializerStrategy, value, builder)) return false; + builder.Append(":"); + if (!SerializeValue(jsonSerializerStrategy, value, builder)) + return false; + first = false; + } + builder.Append("}"); + return true; + } + + static bool SerializeArray(IJsonSerializerStrategy jsonSerializerStrategy, IEnumerable anArray, StringBuilder builder) + { + builder.Append("["); + bool first = true; + foreach (object value in anArray) + { + if (!first) + builder.Append(","); + if (!SerializeValue(jsonSerializerStrategy, value, builder)) + return false; + first = false; + } + builder.Append("]"); + return true; + } + + static bool SerializeString(string aString, StringBuilder builder) + { + // Happy path if there's nothing to be escaped. IndexOfAny is highly optimized (and unmanaged) + if (aString.IndexOfAny(EscapeCharacters) == -1) + { + builder.Append('"'); + builder.Append(aString); + builder.Append('"'); + + return true; + } + + builder.Append('"'); + int safeCharacterCount = 0; + char[] charArray = aString.ToCharArray(); + + for (int i = 0; i < charArray.Length; i++) + { + char c = charArray[i]; + + // Non ascii characters are fine, buffer them up and send them to the builder + // in larger chunks if possible. The escape table is a 1:1 translation table + // with \0 [default(char)] denoting a safe character. + if (c >= EscapeTable.Length || EscapeTable[c] == default(char)) + { + safeCharacterCount++; + } + else + { + if (safeCharacterCount > 0) + { + builder.Append(charArray, i - safeCharacterCount, safeCharacterCount); + safeCharacterCount = 0; + } + + builder.Append('\\'); + builder.Append(EscapeTable[c]); + } + } + + if (safeCharacterCount > 0) + { + builder.Append(charArray, charArray.Length - safeCharacterCount, safeCharacterCount); + } + + builder.Append('"'); + return true; + } + + static bool SerializeNumber(object number, StringBuilder builder) + { + if (number is long) + builder.Append(((long)number).ToString(CultureInfo.InvariantCulture)); + else if (number is ulong) + builder.Append(((ulong)number).ToString(CultureInfo.InvariantCulture)); + else if (number is int) + builder.Append(((int)number).ToString(CultureInfo.InvariantCulture)); + else if (number is uint) + builder.Append(((uint)number).ToString(CultureInfo.InvariantCulture)); + else if (number is decimal) + builder.Append(((decimal)number).ToString(CultureInfo.InvariantCulture)); + else if (number is float) + builder.Append(((float)number).ToString(CultureInfo.InvariantCulture)); + else + builder.Append(Convert.ToDouble(number, CultureInfo.InvariantCulture).ToString("r", CultureInfo.InvariantCulture)); + return true; + } + + /// + /// Determines if a given object is numeric in any way + /// (can be integer, double, null, etc). + /// + static bool IsNumeric(object value) + { + if (value is sbyte) return true; + if (value is byte) return true; + if (value is short) return true; + if (value is ushort) return true; + if (value is int) return true; + if (value is uint) return true; + if (value is long) return true; + if (value is ulong) return true; + if (value is float) return true; + if (value is double) return true; + if (value is decimal) return true; + return false; + } + + private static IJsonSerializerStrategy _currentJsonSerializerStrategy; + public static IJsonSerializerStrategy CurrentJsonSerializerStrategy + { + get + { + return _currentJsonSerializerStrategy ?? + (_currentJsonSerializerStrategy = +#if SIMPLE_JSON_DATACONTRACT + DataContractJsonSerializerStrategy +#else + PocoJsonSerializerStrategy +#endif +); + } + set + { + _currentJsonSerializerStrategy = value; + } + } + + private static PocoJsonSerializerStrategy _pocoJsonSerializerStrategy; + [EditorBrowsable(EditorBrowsableState.Advanced)] + public static PocoJsonSerializerStrategy PocoJsonSerializerStrategy + { + get + { + return _pocoJsonSerializerStrategy ?? (_pocoJsonSerializerStrategy = new PocoJsonSerializerStrategy()); + } + } + +#if SIMPLE_JSON_DATACONTRACT + + private static DataContractJsonSerializerStrategy _dataContractJsonSerializerStrategy; + [System.ComponentModel.EditorBrowsable(EditorBrowsableState.Advanced)] + public static DataContractJsonSerializerStrategy DataContractJsonSerializerStrategy + { + get + { + return _dataContractJsonSerializerStrategy ?? (_dataContractJsonSerializerStrategy = new DataContractJsonSerializerStrategy()); + } + } + +#endif + } + + [GeneratedCode("simple-json", "1.0.0")] +#if SIMPLE_JSON_INTERNAL + internal +#else + public +#endif + interface IJsonSerializerStrategy + { + [SuppressMessage("Microsoft.Design", "CA1007:UseGenericsWhereAppropriate", Justification="Need to support .NET 2")] + bool TrySerializeNonPrimitiveObject(object input, out object output); + object DeserializeObject(object value, Type type); + } + + [GeneratedCode("simple-json", "1.0.0")] +#if SIMPLE_JSON_INTERNAL + internal +#else + public +#endif + class PocoJsonSerializerStrategy : IJsonSerializerStrategy + { + internal IDictionary ConstructorCache; + internal IDictionary> GetCache; + internal IDictionary>> SetCache; + + internal static readonly Type[] EmptyTypes = new Type[0]; + internal static readonly Type[] ArrayConstructorParameterTypes = new Type[] { typeof(int) }; + + private static readonly string[] Iso8601Format = new string[] + { + @"yyyy-MM-dd\THH:mm:ss.FFFFFFF\Z", + @"yyyy-MM-dd\THH:mm:ss\Z", + @"yyyy-MM-dd\THH:mm:ssK" + }; + + public PocoJsonSerializerStrategy() + { + ConstructorCache = new ReflectionUtils.ThreadSafeDictionary(ConstructorDelegateFactory); + GetCache = new ReflectionUtils.ThreadSafeDictionary>(GetterValueFactory); + SetCache = new ReflectionUtils.ThreadSafeDictionary>>(SetterValueFactory); + } + + protected virtual string MapClrMemberNameToJsonFieldName(string clrPropertyName) + { + return CamelCase.MemberNameToCamelCase(clrPropertyName); + } + + internal virtual ReflectionUtils.ConstructorDelegate ConstructorDelegateFactory(Type key) + { + // We need List(int) constructor so that DeserializeObject method will work for generating IList-declared values + var needsCapacityArgument = key.IsArray || key.IsConstructedGenericType && key.GetGenericTypeDefinition() == typeof(List<>); + return ReflectionUtils.GetConstructor(key, needsCapacityArgument ? ArrayConstructorParameterTypes : EmptyTypes); + } + + internal virtual IDictionary GetterValueFactory(Type type) + { + IDictionary result = new Dictionary(); + foreach (PropertyInfo propertyInfo in ReflectionUtils.GetProperties(type)) + { + if (propertyInfo.CanRead) + { + MethodInfo getMethod = ReflectionUtils.GetGetterMethodInfo(propertyInfo); + if (getMethod.IsStatic || !getMethod.IsPublic) + continue; + result[MapClrMemberNameToJsonFieldName(propertyInfo.Name)] = ReflectionUtils.GetGetMethod(propertyInfo); + } + } + foreach (FieldInfo fieldInfo in ReflectionUtils.GetFields(type)) + { + if (fieldInfo.IsStatic || !fieldInfo.IsPublic) + continue; + result[MapClrMemberNameToJsonFieldName(fieldInfo.Name)] = ReflectionUtils.GetGetMethod(fieldInfo); + } + return result; + } + + internal virtual IDictionary> SetterValueFactory(Type type) + { + // BLAZOR-SPECIFIC MODIFICATION FROM STOCK SIMPLEJSON: + // + // For incoming keys we match case-insensitively. But if two .NET properties differ only by case, + // it's ambiguous which should be used: the one that matches the incoming JSON exactly, or the + // one that uses 'correct' PascalCase corresponding to the incoming camelCase? What if neither + // meets these descriptions? + // + // To resolve this: + // - If multiple public properties differ only by case, we throw + // - If multiple public fields differ only by case, we throw + // - If there's a public property and a public field that differ only by case, we prefer the property + // This unambiguously selects one member, and that's what we'll use. + + IDictionary> result = new Dictionary>(StringComparer.OrdinalIgnoreCase); + foreach (PropertyInfo propertyInfo in ReflectionUtils.GetProperties(type)) + { + if (propertyInfo.CanWrite) + { + MethodInfo setMethod = ReflectionUtils.GetSetterMethodInfo(propertyInfo); + if (setMethod.IsStatic) + continue; + if (result.ContainsKey(propertyInfo.Name)) + { + throw new InvalidOperationException($"The type '{type.FullName}' contains multiple public properties with names case-insensitively matching '{propertyInfo.Name.ToLowerInvariant()}'. Such types cannot be used for JSON deserialization."); + } + result[propertyInfo.Name] = new KeyValuePair(propertyInfo.PropertyType, ReflectionUtils.GetSetMethod(propertyInfo)); + } + } + + IDictionary> fieldResult = new Dictionary>(StringComparer.OrdinalIgnoreCase); + foreach (FieldInfo fieldInfo in ReflectionUtils.GetFields(type)) + { + if (fieldInfo.IsInitOnly || fieldInfo.IsStatic || !fieldInfo.IsPublic) + continue; + if (fieldResult.ContainsKey(fieldInfo.Name)) + { + throw new InvalidOperationException($"The type '{type.FullName}' contains multiple public fields with names case-insensitively matching '{fieldInfo.Name.ToLowerInvariant()}'. Such types cannot be used for JSON deserialization."); + } + fieldResult[fieldInfo.Name] = new KeyValuePair(fieldInfo.FieldType, ReflectionUtils.GetSetMethod(fieldInfo)); + if (!result.ContainsKey(fieldInfo.Name)) + { + result[fieldInfo.Name] = fieldResult[fieldInfo.Name]; + } + } + + return result; + } + + public virtual bool TrySerializeNonPrimitiveObject(object input, out object output) + { + return TrySerializeKnownTypes(input, out output) || TrySerializeUnknownTypes(input, out output); + } + + [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")] + public virtual object DeserializeObject(object value, Type type) + { + if (type == null) throw new ArgumentNullException("type"); + string str = value as string; + + if (type == typeof (Guid) && string.IsNullOrEmpty(str)) + return default(Guid); + + if (type.IsEnum) + { + type = type.GetEnumUnderlyingType(); + } + + if (value == null) + return null; + + object obj = null; + + if (str != null) + { + if (str.Length != 0) // We know it can't be null now. + { + if (type == typeof(TimeSpan) || (ReflectionUtils.IsNullableType(type) && Nullable.GetUnderlyingType(type) == typeof(TimeSpan))) + return TimeSpan.ParseExact(str, "c", CultureInfo.InvariantCulture); + if (type == typeof(DateTime) || (ReflectionUtils.IsNullableType(type) && Nullable.GetUnderlyingType(type) == typeof(DateTime))) + return DateTime.TryParseExact(str, Iso8601Format, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var result) + ? result : DateTime.Parse(str, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind); + if (type == typeof(DateTimeOffset) || (ReflectionUtils.IsNullableType(type) && Nullable.GetUnderlyingType(type) == typeof(DateTimeOffset))) + return DateTimeOffset.TryParseExact(str, Iso8601Format, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var result) + ? result : DateTimeOffset.Parse(str, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind); + if (type == typeof(Guid) || (ReflectionUtils.IsNullableType(type) && Nullable.GetUnderlyingType(type) == typeof(Guid))) + return new Guid(str); + if (type == typeof(Uri)) + { + bool isValid = Uri.IsWellFormedUriString(str, UriKind.RelativeOrAbsolute); + + Uri result; + if (isValid && Uri.TryCreate(str, UriKind.RelativeOrAbsolute, out result)) + return result; + + return null; + } + + if (type == typeof(string)) + return str; + + return Convert.ChangeType(str, type, CultureInfo.InvariantCulture); + } + else + { + if (type == typeof(Guid)) + obj = default(Guid); + else if (ReflectionUtils.IsNullableType(type) && Nullable.GetUnderlyingType(type) == typeof(Guid)) + obj = null; + else + obj = str; + } + // Empty string case + if (!ReflectionUtils.IsNullableType(type) && Nullable.GetUnderlyingType(type) == typeof(Guid)) + return str; + } + else if (value is bool) + return value; + + bool valueIsLong = value is long; + bool valueIsDouble = value is double; + if ((valueIsLong && type == typeof(long)) || (valueIsDouble && type == typeof(double))) + return value; + if ((valueIsDouble && type != typeof(double)) || (valueIsLong && type != typeof(long))) + { + obj = type == typeof(int) || type == typeof(long) || type == typeof(double) || type == typeof(float) || type == typeof(bool) || type == typeof(decimal) || type == typeof(byte) || type == typeof(short) + ? Convert.ChangeType(value, type, CultureInfo.InvariantCulture) + : value; + } + else + { + IDictionary objects = value as IDictionary; + if (objects != null) + { + IDictionary jsonObject = objects; + + if (ReflectionUtils.IsTypeDictionary(type)) + { + // if dictionary then + Type[] types = ReflectionUtils.GetGenericTypeArguments(type); + Type keyType = types[0]; + Type valueType = types[1]; + + Type genericType = typeof(Dictionary<,>).MakeGenericType(keyType, valueType); + + IDictionary dict = (IDictionary)ConstructorCache[genericType](); + + foreach (KeyValuePair kvp in jsonObject) + dict.Add(kvp.Key, DeserializeObject(kvp.Value, valueType)); + + obj = dict; + } + else + { + if (type == typeof(object)) + obj = value; + else + { + var constructorDelegate = ConstructorCache[type] + ?? throw new InvalidOperationException($"Cannot deserialize JSON into type '{type.FullName}' because it does not have a public parameterless constructor."); + obj = constructorDelegate(); + + var setterCache = SetCache[type]; + foreach (var jsonKeyValuePair in jsonObject) + { + if (setterCache.TryGetValue(jsonKeyValuePair.Key, out var setter)) + { + var jsonValue = DeserializeObject(jsonKeyValuePair.Value, setter.Key); + setter.Value(obj, jsonValue); + } + } + } + } + } + else + { + IList valueAsList = value as IList; + if (valueAsList != null) + { + IList jsonObject = valueAsList; + IList list = null; + + if (type.IsArray) + { + list = (IList)ConstructorCache[type](jsonObject.Count); + int i = 0; + foreach (object o in jsonObject) + list[i++] = DeserializeObject(o, type.GetElementType()); + } + else if (ReflectionUtils.IsTypeGenericCollectionInterface(type) || ReflectionUtils.IsAssignableFrom(typeof(IList), type)) + { + Type innerType = ReflectionUtils.GetGenericListElementType(type); + list = (IList)(ConstructorCache[type] ?? ConstructorCache[typeof(List<>).MakeGenericType(innerType)])(jsonObject.Count); + foreach (object o in jsonObject) + list.Add(DeserializeObject(o, innerType)); + } + obj = list; + } + } + return obj; + } + if (ReflectionUtils.IsNullableType(type)) + { + // For nullable enums serialized as numbers + if (Nullable.GetUnderlyingType(type).IsEnum) + { + return Enum.ToObject(Nullable.GetUnderlyingType(type), value); + } + + return ReflectionUtils.ToNullableType(obj, type); + } + + return obj; + } + + protected virtual object SerializeEnum(Enum p) + { + return Convert.ToDouble(p, CultureInfo.InvariantCulture); + } + + [SuppressMessage("Microsoft.Design", "CA1007:UseGenericsWhereAppropriate", Justification="Need to support .NET 2")] + protected virtual bool TrySerializeKnownTypes(object input, out object output) + { + bool returnValue = true; + if (input is DateTime) + output = ((DateTime)input).ToUniversalTime().ToString(Iso8601Format[0], CultureInfo.InvariantCulture); + else if (input is DateTimeOffset) + output = ((DateTimeOffset)input).ToString("o"); + else if (input is Guid) + output = ((Guid)input).ToString("D"); + else if (input is Uri) + output = input.ToString(); + else if (input is TimeSpan) + output = ((TimeSpan)input).ToString("c"); + else + { + Enum inputEnum = input as Enum; + if (inputEnum != null) + output = SerializeEnum(inputEnum); + else + { + returnValue = false; + output = null; + } + } + return returnValue; + } + [SuppressMessage("Microsoft.Design", "CA1007:UseGenericsWhereAppropriate", Justification="Need to support .NET 2")] + protected virtual bool TrySerializeUnknownTypes(object input, out object output) + { + if (input == null) throw new ArgumentNullException("input"); + output = null; + Type type = input.GetType(); + if (type.FullName == null) + return false; + IDictionary obj = new JsonObject(); + IDictionary getters = GetCache[type]; + foreach (KeyValuePair getter in getters) + { + if (getter.Value != null) + obj.Add(MapClrMemberNameToJsonFieldName(getter.Key), getter.Value(input)); + } + output = obj; + return true; + } + } + +#if SIMPLE_JSON_DATACONTRACT + [GeneratedCode("simple-json", "1.0.0")] +#if SIMPLE_JSON_INTERNAL + internal +#else + public +#endif + class DataContractJsonSerializerStrategy : PocoJsonSerializerStrategy + { + public DataContractJsonSerializerStrategy() + { + GetCache = new ReflectionUtils.ThreadSafeDictionary>(GetterValueFactory); + SetCache = new ReflectionUtils.ThreadSafeDictionary>>(SetterValueFactory); + } + + internal override IDictionary GetterValueFactory(Type type) + { + bool hasDataContract = ReflectionUtils.GetAttribute(type, typeof(DataContractAttribute)) != null; + if (!hasDataContract) + return base.GetterValueFactory(type); + string jsonKey; + IDictionary result = new Dictionary(); + foreach (PropertyInfo propertyInfo in ReflectionUtils.GetProperties(type)) + { + if (propertyInfo.CanRead) + { + MethodInfo getMethod = ReflectionUtils.GetGetterMethodInfo(propertyInfo); + if (!getMethod.IsStatic && CanAdd(propertyInfo, out jsonKey)) + result[jsonKey] = ReflectionUtils.GetGetMethod(propertyInfo); + } + } + foreach (FieldInfo fieldInfo in ReflectionUtils.GetFields(type)) + { + if (!fieldInfo.IsStatic && CanAdd(fieldInfo, out jsonKey)) + result[jsonKey] = ReflectionUtils.GetGetMethod(fieldInfo); + } + return result; + } + + internal override IDictionary> SetterValueFactory(Type type) + { + bool hasDataContract = ReflectionUtils.GetAttribute(type, typeof(DataContractAttribute)) != null; + if (!hasDataContract) + return base.SetterValueFactory(type); + string jsonKey; + IDictionary> result = new Dictionary>(); + foreach (PropertyInfo propertyInfo in ReflectionUtils.GetProperties(type)) + { + if (propertyInfo.CanWrite) + { + MethodInfo setMethod = ReflectionUtils.GetSetterMethodInfo(propertyInfo); + if (!setMethod.IsStatic && CanAdd(propertyInfo, out jsonKey)) + result[jsonKey] = new KeyValuePair(propertyInfo.PropertyType, ReflectionUtils.GetSetMethod(propertyInfo)); + } + } + foreach (FieldInfo fieldInfo in ReflectionUtils.GetFields(type)) + { + if (!fieldInfo.IsInitOnly && !fieldInfo.IsStatic && CanAdd(fieldInfo, out jsonKey)) + result[jsonKey] = new KeyValuePair(fieldInfo.FieldType, ReflectionUtils.GetSetMethod(fieldInfo)); + } + // todo implement sorting for DATACONTRACT. + return result; + } + + private static bool CanAdd(MemberInfo info, out string jsonKey) + { + jsonKey = null; + if (ReflectionUtils.GetAttribute(info, typeof(IgnoreDataMemberAttribute)) != null) + return false; + DataMemberAttribute dataMemberAttribute = (DataMemberAttribute)ReflectionUtils.GetAttribute(info, typeof(DataMemberAttribute)); + if (dataMemberAttribute == null) + return false; + jsonKey = string.IsNullOrEmpty(dataMemberAttribute.Name) ? info.Name : dataMemberAttribute.Name; + return true; + } + } + +#endif + + namespace Reflection + { + // This class is meant to be copied into other libraries. So we want to exclude it from Code Analysis rules + // that might be in place in the target project. + [GeneratedCode("reflection-utils", "1.0.0")] +#if SIMPLE_JSON_REFLECTION_UTILS_PUBLIC + public +#else + internal +#endif + class ReflectionUtils + { + private static readonly object[] EmptyObjects = new object[] { }; + + public delegate object GetDelegate(object source); + public delegate void SetDelegate(object source, object value); + public delegate object ConstructorDelegate(params object[] args); + + public delegate TValue ThreadSafeDictionaryValueFactory(TKey key); + +#if SIMPLE_JSON_TYPEINFO + public static TypeInfo GetTypeInfo(Type type) + { + return type.GetTypeInfo(); + } +#else + public static Type GetTypeInfo(Type type) + { + return type; + } +#endif + + public static Attribute GetAttribute(MemberInfo info, Type type) + { +#if SIMPLE_JSON_TYPEINFO + if (info == null || type == null || !info.IsDefined(type)) + return null; + return info.GetCustomAttribute(type); +#else + if (info == null || type == null || !Attribute.IsDefined(info, type)) + return null; + return Attribute.GetCustomAttribute(info, type); +#endif + } + + public static Type GetGenericListElementType(Type type) + { + IEnumerable interfaces; +#if SIMPLE_JSON_TYPEINFO + interfaces = type.GetTypeInfo().ImplementedInterfaces; +#else + interfaces = type.GetInterfaces(); +#endif + foreach (Type implementedInterface in interfaces) + { + if (IsTypeGeneric(implementedInterface) && + implementedInterface.GetGenericTypeDefinition() == typeof (IList<>)) + { + return GetGenericTypeArguments(implementedInterface)[0]; + } + } + return GetGenericTypeArguments(type)[0]; + } + + public static Attribute GetAttribute(Type objectType, Type attributeType) + { + +#if SIMPLE_JSON_TYPEINFO + if (objectType == null || attributeType == null || !objectType.GetTypeInfo().IsDefined(attributeType)) + return null; + return objectType.GetTypeInfo().GetCustomAttribute(attributeType); +#else + if (objectType == null || attributeType == null || !Attribute.IsDefined(objectType, attributeType)) + return null; + return Attribute.GetCustomAttribute(objectType, attributeType); +#endif + } + + public static Type[] GetGenericTypeArguments(Type type) + { +#if SIMPLE_JSON_TYPEINFO + return type.GetTypeInfo().GenericTypeArguments; +#else + return type.GetGenericArguments(); +#endif + } + + public static bool IsTypeGeneric(Type type) + { + return GetTypeInfo(type).IsGenericType; + } + + public static bool IsTypeGenericCollectionInterface(Type type) + { + if (!IsTypeGeneric(type)) + return false; + + Type genericDefinition = type.GetGenericTypeDefinition(); + + return (genericDefinition == typeof(IList<>) + || genericDefinition == typeof(ICollection<>) + || genericDefinition == typeof(IEnumerable<>) +#if SIMPLE_JSON_READONLY_COLLECTIONS + || genericDefinition == typeof(IReadOnlyCollection<>) + || genericDefinition == typeof(IReadOnlyList<>) +#endif + ); + } + + public static bool IsAssignableFrom(Type type1, Type type2) + { + return GetTypeInfo(type1).IsAssignableFrom(GetTypeInfo(type2)); + } + + public static bool IsTypeDictionary(Type type) + { +#if SIMPLE_JSON_TYPEINFO + if (typeof(IDictionary<,>).GetTypeInfo().IsAssignableFrom(type.GetTypeInfo())) + return true; +#else + if (typeof(System.Collections.IDictionary).IsAssignableFrom(type)) + return true; +#endif + if (!GetTypeInfo(type).IsGenericType) + return false; + + Type genericDefinition = type.GetGenericTypeDefinition(); + return genericDefinition == typeof(IDictionary<,>); + } + + public static bool IsNullableType(Type type) + { + return GetTypeInfo(type).IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); + } + + public static object ToNullableType(object obj, Type nullableType) + { + return obj == null ? null : Convert.ChangeType(obj, Nullable.GetUnderlyingType(nullableType), CultureInfo.InvariantCulture); + } + + public static bool IsValueType(Type type) + { + return GetTypeInfo(type).IsValueType; + } + + public static IEnumerable GetConstructors(Type type) + { +#if SIMPLE_JSON_TYPEINFO + return type.GetTypeInfo().DeclaredConstructors; +#else + const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + return type.GetConstructors(flags); +#endif + } + + public static ConstructorInfo GetConstructorInfo(Type type, params Type[] argsType) + { + IEnumerable constructorInfos = GetConstructors(type); + int i; + bool matches; + foreach (ConstructorInfo constructorInfo in constructorInfos) + { + ParameterInfo[] parameters = constructorInfo.GetParameters(); + if (argsType.Length != parameters.Length) + continue; + + i = 0; + matches = true; + foreach (ParameterInfo parameterInfo in constructorInfo.GetParameters()) + { + if (parameterInfo.ParameterType != argsType[i]) + { + matches = false; + break; + } + } + + if (matches) + return constructorInfo; + } + + return null; + } + + public static IEnumerable GetProperties(Type type) + { +#if SIMPLE_JSON_TYPEINFO + return type.GetRuntimeProperties(); +#else + return type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); +#endif + } + + public static IEnumerable GetFields(Type type) + { +#if SIMPLE_JSON_TYPEINFO + return type.GetRuntimeFields(); +#else + return type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); +#endif + } + + public static MethodInfo GetGetterMethodInfo(PropertyInfo propertyInfo) + { +#if SIMPLE_JSON_TYPEINFO + return propertyInfo.GetMethod; +#else + return propertyInfo.GetGetMethod(true); +#endif + } + + public static MethodInfo GetSetterMethodInfo(PropertyInfo propertyInfo) + { +#if SIMPLE_JSON_TYPEINFO + return propertyInfo.SetMethod; +#else + return propertyInfo.GetSetMethod(true); +#endif + } + + public static ConstructorDelegate GetConstructor(ConstructorInfo constructorInfo) + { +#if SIMPLE_JSON_NO_LINQ_EXPRESSION + return GetConstructorByReflection(constructorInfo); +#else + return GetConstructorByExpression(constructorInfo); +#endif + } + + public static ConstructorDelegate GetConstructor(Type type, params Type[] argsType) + { +#if SIMPLE_JSON_NO_LINQ_EXPRESSION + return GetConstructorByReflection(type, argsType); +#else + return GetConstructorByExpression(type, argsType); +#endif + } + + public static ConstructorDelegate GetConstructorByReflection(ConstructorInfo constructorInfo) + { + return delegate(object[] args) { return constructorInfo.Invoke(args); }; + } + + public static ConstructorDelegate GetConstructorByReflection(Type type, params Type[] argsType) + { + ConstructorInfo constructorInfo = GetConstructorInfo(type, argsType); + + if (constructorInfo == null && argsType.Length == 0 && type.IsValueType) + { + // If it's a struct, then parameterless constructors are implicit + // We can always call Activator.CreateInstance in lieu of a zero-arg constructor + return args => Activator.CreateInstance(type); + } + + return constructorInfo == null ? null : GetConstructorByReflection(constructorInfo); + } + +#if !SIMPLE_JSON_NO_LINQ_EXPRESSION + + public static ConstructorDelegate GetConstructorByExpression(ConstructorInfo constructorInfo) + { + ParameterInfo[] paramsInfo = constructorInfo.GetParameters(); + ParameterExpression param = Expression.Parameter(typeof(object[]), "args"); + Expression[] argsExp = new Expression[paramsInfo.Length]; + for (int i = 0; i < paramsInfo.Length; i++) + { + Expression index = Expression.Constant(i); + Type paramType = paramsInfo[i].ParameterType; + Expression paramAccessorExp = Expression.ArrayIndex(param, index); + Expression paramCastExp = Expression.Convert(paramAccessorExp, paramType); + argsExp[i] = paramCastExp; + } + NewExpression newExp = Expression.New(constructorInfo, argsExp); + Expression> lambda = Expression.Lambda>(newExp, param); + Func compiledLambda = lambda.Compile(); + return delegate(object[] args) { return compiledLambda(args); }; + } + + public static ConstructorDelegate GetConstructorByExpression(Type type, params Type[] argsType) + { + ConstructorInfo constructorInfo = GetConstructorInfo(type, argsType); + return constructorInfo == null ? null : GetConstructorByExpression(constructorInfo); + } + +#endif + + public static GetDelegate GetGetMethod(PropertyInfo propertyInfo) + { +#if SIMPLE_JSON_NO_LINQ_EXPRESSION + return GetGetMethodByReflection(propertyInfo); +#else + return GetGetMethodByExpression(propertyInfo); +#endif + } + + public static GetDelegate GetGetMethod(FieldInfo fieldInfo) + { +#if SIMPLE_JSON_NO_LINQ_EXPRESSION + return GetGetMethodByReflection(fieldInfo); +#else + return GetGetMethodByExpression(fieldInfo); +#endif + } + + public static GetDelegate GetGetMethodByReflection(PropertyInfo propertyInfo) + { + MethodInfo methodInfo = GetGetterMethodInfo(propertyInfo); + return delegate(object source) { return methodInfo.Invoke(source, EmptyObjects); }; + } + + public static GetDelegate GetGetMethodByReflection(FieldInfo fieldInfo) + { + return delegate(object source) { return fieldInfo.GetValue(source); }; + } + +#if !SIMPLE_JSON_NO_LINQ_EXPRESSION + + public static GetDelegate GetGetMethodByExpression(PropertyInfo propertyInfo) + { + MethodInfo getMethodInfo = GetGetterMethodInfo(propertyInfo); + ParameterExpression instance = Expression.Parameter(typeof(object), "instance"); + UnaryExpression instanceCast = (!IsValueType(propertyInfo.DeclaringType)) ? Expression.TypeAs(instance, propertyInfo.DeclaringType) : Expression.Convert(instance, propertyInfo.DeclaringType); + Func compiled = Expression.Lambda>(Expression.TypeAs(Expression.Call(instanceCast, getMethodInfo), typeof(object)), instance).Compile(); + return delegate(object source) { return compiled(source); }; + } + + public static GetDelegate GetGetMethodByExpression(FieldInfo fieldInfo) + { + ParameterExpression instance = Expression.Parameter(typeof(object), "instance"); + MemberExpression member = Expression.Field(Expression.Convert(instance, fieldInfo.DeclaringType), fieldInfo); + GetDelegate compiled = Expression.Lambda(Expression.Convert(member, typeof(object)), instance).Compile(); + return delegate(object source) { return compiled(source); }; + } + +#endif + + public static SetDelegate GetSetMethod(PropertyInfo propertyInfo) + { +#if SIMPLE_JSON_NO_LINQ_EXPRESSION + return GetSetMethodByReflection(propertyInfo); +#else + return GetSetMethodByExpression(propertyInfo); +#endif + } + + public static SetDelegate GetSetMethod(FieldInfo fieldInfo) + { +#if SIMPLE_JSON_NO_LINQ_EXPRESSION + return GetSetMethodByReflection(fieldInfo); +#else + return GetSetMethodByExpression(fieldInfo); +#endif + } + + public static SetDelegate GetSetMethodByReflection(PropertyInfo propertyInfo) + { + MethodInfo methodInfo = GetSetterMethodInfo(propertyInfo); + return delegate(object source, object value) { methodInfo.Invoke(source, new object[] { value }); }; + } + + public static SetDelegate GetSetMethodByReflection(FieldInfo fieldInfo) + { + return delegate(object source, object value) { fieldInfo.SetValue(source, value); }; + } + +#if !SIMPLE_JSON_NO_LINQ_EXPRESSION + + public static SetDelegate GetSetMethodByExpression(PropertyInfo propertyInfo) + { + MethodInfo setMethodInfo = GetSetterMethodInfo(propertyInfo); + ParameterExpression instance = Expression.Parameter(typeof(object), "instance"); + ParameterExpression value = Expression.Parameter(typeof(object), "value"); + UnaryExpression instanceCast = (!IsValueType(propertyInfo.DeclaringType)) ? Expression.TypeAs(instance, propertyInfo.DeclaringType) : Expression.Convert(instance, propertyInfo.DeclaringType); + UnaryExpression valueCast = (!IsValueType(propertyInfo.PropertyType)) ? Expression.TypeAs(value, propertyInfo.PropertyType) : Expression.Convert(value, propertyInfo.PropertyType); + Action compiled = Expression.Lambda>(Expression.Call(instanceCast, setMethodInfo, valueCast), new ParameterExpression[] { instance, value }).Compile(); + return delegate(object source, object val) { compiled(source, val); }; + } + + public static SetDelegate GetSetMethodByExpression(FieldInfo fieldInfo) + { + ParameterExpression instance = Expression.Parameter(typeof(object), "instance"); + ParameterExpression value = Expression.Parameter(typeof(object), "value"); + Action compiled = Expression.Lambda>( + Assign(Expression.Field(Expression.Convert(instance, fieldInfo.DeclaringType), fieldInfo), Expression.Convert(value, fieldInfo.FieldType)), instance, value).Compile(); + return delegate(object source, object val) { compiled(source, val); }; + } + + public static BinaryExpression Assign(Expression left, Expression right) + { +#if SIMPLE_JSON_TYPEINFO + return Expression.Assign(left, right); +#else + MethodInfo assign = typeof(Assigner<>).MakeGenericType(left.Type).GetMethod("Assign"); + BinaryExpression assignExpr = Expression.Add(left, right, assign); + return assignExpr; +#endif + } + + private static class Assigner + { + public static T Assign(ref T left, T right) + { + return (left = right); + } + } + +#endif + + public sealed class ThreadSafeDictionary : IDictionary + { + private readonly object _lock = new object(); + private readonly ThreadSafeDictionaryValueFactory _valueFactory; + private Dictionary _dictionary; + + public ThreadSafeDictionary(ThreadSafeDictionaryValueFactory valueFactory) + { + _valueFactory = valueFactory; + } + + private TValue Get(TKey key) + { + if (_dictionary == null) + return AddValue(key); + TValue value; + if (!_dictionary.TryGetValue(key, out value)) + return AddValue(key); + return value; + } + + private TValue AddValue(TKey key) + { + TValue value = _valueFactory(key); + lock (_lock) + { + if (_dictionary == null) + { + _dictionary = new Dictionary(); + _dictionary[key] = value; + } + else + { + TValue val; + if (_dictionary.TryGetValue(key, out val)) + return val; + Dictionary dict = new Dictionary(_dictionary); + dict[key] = value; + _dictionary = dict; + } + } + return value; + } + + public void Add(TKey key, TValue value) + { + throw new NotImplementedException(); + } + + public bool ContainsKey(TKey key) + { + return _dictionary.ContainsKey(key); + } + + public ICollection Keys + { + get { return _dictionary.Keys; } + } + + public bool Remove(TKey key) + { + throw new NotImplementedException(); + } + + public bool TryGetValue(TKey key, out TValue value) + { + value = this[key]; + return true; + } + + public ICollection Values + { + get { return _dictionary.Values; } + } + + public TValue this[TKey key] + { + get { return Get(key); } + set { throw new NotImplementedException(); } + } + + public void Add(KeyValuePair item) + { + throw new NotImplementedException(); + } + + public void Clear() + { + throw new NotImplementedException(); + } + + public bool Contains(KeyValuePair item) + { + throw new NotImplementedException(); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + throw new NotImplementedException(); + } + + public int Count + { + get { return _dictionary.Count; } + } + + public bool IsReadOnly + { + get { throw new NotImplementedException(); } + } + + public bool Remove(KeyValuePair item) + { + throw new NotImplementedException(); + } + + public IEnumerator> GetEnumerator() + { + return _dictionary.GetEnumerator(); + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return _dictionary.GetEnumerator(); + } + } + + } + } +} +// ReSharper restore LoopCanBeConvertedToQuery +// ReSharper restore RedundantExplicitArrayCreation +// ReSharper restore SuggestUseVarKeywordEvident diff --git a/src/JSInterop/Microsoft.JSInterop/src/Microsoft.JSInterop.csproj b/src/JSInterop/Microsoft.JSInterop/src/Microsoft.JSInterop.csproj new file mode 100644 index 0000000000..f92b8d457d --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/src/Microsoft.JSInterop.csproj @@ -0,0 +1,15 @@ + + + + netstandard2.0 + Abstractions and features for interop between .NET and JavaScript code. + javascript;interop + true + true + + + + + + + diff --git a/src/JSInterop/Microsoft.JSInterop/src/TaskGenericsUtil.cs b/src/JSInterop/Microsoft.JSInterop/src/TaskGenericsUtil.cs new file mode 100644 index 0000000000..734e9863b8 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/src/TaskGenericsUtil.cs @@ -0,0 +1,116 @@ +// 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.Linq; +using System.Threading.Tasks; + +namespace Microsoft.JSInterop +{ + internal static class TaskGenericsUtil + { + private static ConcurrentDictionary _cachedResultGetters + = new ConcurrentDictionary(); + + private static ConcurrentDictionary _cachedResultSetters + = new ConcurrentDictionary(); + + public static void SetTaskCompletionSourceResult(object taskCompletionSource, object result) + => CreateResultSetter(taskCompletionSource).SetResult(taskCompletionSource, result); + + public static void SetTaskCompletionSourceException(object taskCompletionSource, Exception exception) + => CreateResultSetter(taskCompletionSource).SetException(taskCompletionSource, exception); + + public static Type GetTaskCompletionSourceResultType(object taskCompletionSource) + => CreateResultSetter(taskCompletionSource).ResultType; + + public static object GetTaskResult(Task task) + { + var getter = _cachedResultGetters.GetOrAdd(task.GetType(), taskInstanceType => + { + var resultType = GetTaskResultType(taskInstanceType); + return resultType == null + ? new VoidTaskResultGetter() + : (ITaskResultGetter)Activator.CreateInstance( + typeof(TaskResultGetter<>).MakeGenericType(resultType)); + }); + return getter.GetResult(task); + } + + private static Type GetTaskResultType(Type taskType) + { + // It might be something derived from Task or Task, so we have to scan + // up the inheritance hierarchy to find the Task or Task + while (taskType != typeof(Task) && + (!taskType.IsGenericType || taskType.GetGenericTypeDefinition() != typeof(Task<>))) + { + taskType = taskType.BaseType + ?? throw new ArgumentException($"The type '{taskType.FullName}' is not inherited from '{typeof(Task).FullName}'."); + } + + return taskType.IsGenericType + ? taskType.GetGenericArguments().Single() + : null; + } + + interface ITcsResultSetter + { + Type ResultType { get; } + void SetResult(object taskCompletionSource, object result); + void SetException(object taskCompletionSource, Exception exception); + } + + private interface ITaskResultGetter + { + object GetResult(Task task); + } + + private class TaskResultGetter : ITaskResultGetter + { + public object GetResult(Task task) => ((Task)task).Result; + } + + private class VoidTaskResultGetter : ITaskResultGetter + { + public object GetResult(Task task) + { + task.Wait(); // Throw if the task failed + return null; + } + } + + private class TcsResultSetter : ITcsResultSetter + { + public Type ResultType => typeof(T); + + public void SetResult(object tcs, object result) + { + var typedTcs = (TaskCompletionSource)tcs; + + // If necessary, attempt a cast + var typedResult = result is T resultT + ? resultT + : (T)Convert.ChangeType(result, typeof(T)); + + typedTcs.SetResult(typedResult); + } + + public void SetException(object tcs, Exception exception) + { + var typedTcs = (TaskCompletionSource)tcs; + typedTcs.SetException(exception); + } + } + + private static ITcsResultSetter CreateResultSetter(object taskCompletionSource) + { + return _cachedResultSetters.GetOrAdd(taskCompletionSource.GetType(), tcsType => + { + var resultType = tcsType.GetGenericArguments().Single(); + return (ITcsResultSetter)Activator.CreateInstance( + typeof(TcsResultSetter<>).MakeGenericType(resultType)); + }); + } + } +} diff --git a/src/JSInterop/Microsoft.JSInterop/test/DotNetDispatcherTest.cs b/src/JSInterop/Microsoft.JSInterop/test/DotNetDispatcherTest.cs new file mode 100644 index 0000000000..e3f91a6fd0 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/test/DotNetDispatcherTest.cs @@ -0,0 +1,508 @@ +// 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.Threading.Tasks; +using Xunit; + +namespace Microsoft.JSInterop.Tests +{ + public class DotNetDispatcherTest + { + private readonly static string thisAssemblyName + = typeof(DotNetDispatcherTest).Assembly.GetName().Name; + private readonly TestJSRuntime jsRuntime + = new TestJSRuntime(); + + [Fact] + public void CannotInvokeWithEmptyAssemblyName() + { + var ex = Assert.Throws(() => + { + DotNetDispatcher.Invoke(" ", "SomeMethod", default, "[]"); + }); + + Assert.StartsWith("Cannot be null, empty, or whitespace.", ex.Message); + Assert.Equal("assemblyName", ex.ParamName); + } + + [Fact] + public void CannotInvokeWithEmptyMethodIdentifier() + { + var ex = Assert.Throws(() => + { + DotNetDispatcher.Invoke("SomeAssembly", " ", default, "[]"); + }); + + Assert.StartsWith("Cannot be null, empty, or whitespace.", ex.Message); + Assert.Equal("methodIdentifier", ex.ParamName); + } + + [Fact] + public void CannotInvokeMethodsOnUnloadedAssembly() + { + var assemblyName = "Some.Fake.Assembly"; + var ex = Assert.Throws(() => + { + DotNetDispatcher.Invoke(assemblyName, "SomeMethod", default, null); + }); + + Assert.Equal($"There is no loaded assembly with the name '{assemblyName}'.", ex.Message); + } + + // Note: Currently it's also not possible to invoke generic methods. + // That's not something determined by DotNetDispatcher, but rather by the fact that we + // don't close over the generics in the reflection code. + // Not defining this behavior through unit tests because the default outcome is + // fine (an exception stating what info is missing). + + [Theory] + [InlineData("MethodOnInternalType")] + [InlineData("PrivateMethod")] + [InlineData("ProtectedMethod")] + [InlineData("StaticMethodWithoutAttribute")] // That's not really its identifier; just making the point that there's no way to invoke it + [InlineData("InstanceMethodWithoutAttribute")] // That's not really its identifier; just making the point that there's no way to invoke it + public void CannotInvokeUnsuitableMethods(string methodIdentifier) + { + var ex = Assert.Throws(() => + { + DotNetDispatcher.Invoke(thisAssemblyName, methodIdentifier, default, null); + }); + + Assert.Equal($"The assembly '{thisAssemblyName}' does not contain a public method with [JSInvokableAttribute(\"{methodIdentifier}\")].", ex.Message); + } + + [Fact(Skip = "https://github.com/aspnet/AspNetCore-Internal/issues/1733")] + public Task CanInvokeStaticVoidMethod() => WithJSRuntime(jsRuntime => + { + // Arrange/Act + SomePublicType.DidInvokeMyInvocableStaticVoid = false; + var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticVoid", default, null); + + // Assert + Assert.Null(resultJson); + Assert.True(SomePublicType.DidInvokeMyInvocableStaticVoid); + }); + + [Fact] + public Task CanInvokeStaticNonVoidMethod() => WithJSRuntime(jsRuntime => + { + // Arrange/Act + var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticNonVoid", default, null); + var result = Json.Deserialize(resultJson); + + // Assert + Assert.Equal("Test", result.StringVal); + Assert.Equal(123, result.IntVal); + }); + + [Fact] + public Task CanInvokeStaticNonVoidMethodWithoutCustomIdentifier() => WithJSRuntime(jsRuntime => + { + // Arrange/Act + var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, nameof(SomePublicType.InvokableMethodWithoutCustomIdentifier), default, null); + var result = Json.Deserialize(resultJson); + + // Assert + Assert.Equal("InvokableMethodWithoutCustomIdentifier", result.StringVal); + Assert.Equal(456, result.IntVal); + }); + + [Fact(Skip = "https://github.com/aspnet/AspNetCore-Internal/issues/1733")] + public Task CanInvokeStaticWithParams() => WithJSRuntime(jsRuntime => + { + // Arrange: Track a .NET object to use as an arg + var arg3 = new TestDTO { IntVal = 999, StringVal = "My string" }; + jsRuntime.Invoke("unimportant", new DotNetObjectRef(arg3)); + + // Arrange: Remaining args + var argsJson = Json.Serialize(new object[] { + new TestDTO { StringVal = "Another string", IntVal = 456 }, + new[] { 100, 200 }, + "__dotNetObject:1" + }); + + // Act + var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticWithParams", default, argsJson); + var result = Json.Deserialize(resultJson); + + // Assert: First result value marshalled via JSON + var resultDto1 = (TestDTO)jsRuntime.ArgSerializerStrategy.DeserializeObject(result[0], typeof(TestDTO)); + Assert.Equal("ANOTHER STRING", resultDto1.StringVal); + Assert.Equal(756, resultDto1.IntVal); + + // Assert: Second result value marshalled by ref + var resultDto2Ref = (string)result[1]; + Assert.Equal("__dotNetObject:2", resultDto2Ref); + var resultDto2 = (TestDTO)jsRuntime.ArgSerializerStrategy.FindDotNetObject(2); + Assert.Equal("MY STRING", resultDto2.StringVal); + Assert.Equal(1299, resultDto2.IntVal); + }); + + [Fact(Skip = "https://github.com/aspnet/AspNetCore-Internal/issues/1733")] + public Task CanInvokeInstanceVoidMethod() => WithJSRuntime(jsRuntime => + { + // Arrange: Track some instance + var targetInstance = new SomePublicType(); + jsRuntime.Invoke("unimportant", new DotNetObjectRef(targetInstance)); + + // Act + var resultJson = DotNetDispatcher.Invoke(null, "InvokableInstanceVoid", 1, null); + + // Assert + Assert.Null(resultJson); + Assert.True(targetInstance.DidInvokeMyInvocableInstanceVoid); + }); + + [Fact(Skip = "https://github.com/aspnet/AspNetCore-Internal/issues/1733")] + public Task CanInvokeBaseInstanceVoidMethod() => WithJSRuntime(jsRuntime => + { + // Arrange: Track some instance + var targetInstance = new DerivedClass(); + jsRuntime.Invoke("unimportant", new DotNetObjectRef(targetInstance)); + + // Act + var resultJson = DotNetDispatcher.Invoke(null, "BaseClassInvokableInstanceVoid", 1, null); + + // Assert + Assert.Null(resultJson); + Assert.True(targetInstance.DidInvokeMyBaseClassInvocableInstanceVoid); + }); + + [Fact] + public Task CannotUseDotNetObjectRefAfterDisposal() => WithJSRuntime(jsRuntime => + { + // This test addresses the case where the developer calls objectRef.Dispose() + // from .NET code, as opposed to .dispose() from JS code + + // Arrange: Track some instance, then dispose it + var targetInstance = new SomePublicType(); + var objectRef = new DotNetObjectRef(targetInstance); + jsRuntime.Invoke("unimportant", objectRef); + objectRef.Dispose(); + + // Act/Assert + var ex = Assert.Throws( + () => DotNetDispatcher.Invoke(null, "InvokableInstanceVoid", 1, null)); + Assert.StartsWith("There is no tracked object with id '1'.", ex.Message); + }); + + [Fact] + public Task CannotUseDotNetObjectRefAfterReleaseDotNetObject() => WithJSRuntime(jsRuntime => + { + // This test addresses the case where the developer calls .dispose() + // from JS code, as opposed to objectRef.Dispose() from .NET code + + // Arrange: Track some instance, then dispose it + var targetInstance = new SomePublicType(); + var objectRef = new DotNetObjectRef(targetInstance); + jsRuntime.Invoke("unimportant", objectRef); + DotNetDispatcher.ReleaseDotNetObject(1); + + // Act/Assert + var ex = Assert.Throws( + () => DotNetDispatcher.Invoke(null, "InvokableInstanceVoid", 1, null)); + Assert.StartsWith("There is no tracked object with id '1'.", ex.Message); + }); + + [Fact(Skip = "https://github.com/aspnet/AspNetCore-Internal/issues/1733")] + public Task CanInvokeInstanceMethodWithParams() => WithJSRuntime(jsRuntime => + { + // Arrange: Track some instance plus another object we'll pass as a param + var targetInstance = new SomePublicType(); + var arg2 = new TestDTO { IntVal = 1234, StringVal = "My string" }; + jsRuntime.Invoke("unimportant", + new DotNetObjectRef(targetInstance), + new DotNetObjectRef(arg2)); + var argsJson = "[\"myvalue\",\"__dotNetObject:2\"]"; + + // Act + var resultJson = DotNetDispatcher.Invoke(null, "InvokableInstanceMethod", 1, argsJson); + + // Assert + Assert.Equal("[\"You passed myvalue\",\"__dotNetObject:3\"]", resultJson); + var resultDto = (TestDTO)jsRuntime.ArgSerializerStrategy.FindDotNetObject(3); + Assert.Equal(1235, resultDto.IntVal); + Assert.Equal("MY STRING", resultDto.StringVal); + }); + + [Fact] + public void CannotInvokeWithIncorrectNumberOfParams() + { + // Arrange + var argsJson = Json.Serialize(new object[] { 1, 2, 3, 4 }); + + // Act/Assert + var ex = Assert.Throws(() => + { + DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticWithParams", default, argsJson); + }); + + Assert.Equal("In call to 'InvocableStaticWithParams', expected 3 parameters but received 4.", ex.Message); + } + + [Fact(Skip = "https://github.com/aspnet/AspNetCore-Internal/issues/1733")] + public Task CanInvokeAsyncMethod() => WithJSRuntime(async jsRuntime => + { + // Arrange: Track some instance plus another object we'll pass as a param + var targetInstance = new SomePublicType(); + var arg2 = new TestDTO { IntVal = 1234, StringVal = "My string" }; + jsRuntime.Invoke("unimportant", new DotNetObjectRef(targetInstance), new DotNetObjectRef(arg2)); + + // Arrange: all args + var argsJson = Json.Serialize(new object[] + { + new TestDTO { IntVal = 1000, StringVal = "String via JSON" }, + "__dotNetObject:2" + }); + + // Act + var callId = "123"; + var resultTask = jsRuntime.NextInvocationTask; + DotNetDispatcher.BeginInvoke(callId, null, "InvokableAsyncMethod", 1, argsJson); + await resultTask; + var result = Json.Deserialize(jsRuntime.LastInvocationArgsJson); + var resultValue = (SimpleJson.JsonArray)result[2]; + + // Assert: Correct info to complete the async call + Assert.Equal(0, jsRuntime.LastInvocationAsyncHandle); // 0 because it doesn't want a further callback from JS to .NET + Assert.Equal("DotNet.jsCallDispatcher.endInvokeDotNetFromJS", jsRuntime.LastInvocationIdentifier); + Assert.Equal(3, result.Count); + Assert.Equal(callId, result[0]); + Assert.True((bool)result[1]); // Success flag + + // Assert: First result value marshalled via JSON + var resultDto1 = (TestDTO)jsRuntime.ArgSerializerStrategy.DeserializeObject(resultValue[0], typeof(TestDTO)); + Assert.Equal("STRING VIA JSON", resultDto1.StringVal); + Assert.Equal(2000, resultDto1.IntVal); + + // Assert: Second result value marshalled by ref + var resultDto2Ref = (string)resultValue[1]; + Assert.Equal("__dotNetObject:3", resultDto2Ref); + var resultDto2 = (TestDTO)jsRuntime.ArgSerializerStrategy.FindDotNetObject(3); + Assert.Equal("MY STRING", resultDto2.StringVal); + Assert.Equal(2468, resultDto2.IntVal); + }); + + + [Fact] + public Task CanInvokeSyncThrowingMethod() => WithJSRuntime(async jsRuntime => + { + // Arrange + + // Act + var callId = "123"; + var resultTask = jsRuntime.NextInvocationTask; + DotNetDispatcher.BeginInvoke(callId, thisAssemblyName, nameof(ThrowingClass.ThrowingMethod), default, default); + + await resultTask; // This won't throw, it sets properties on the jsRuntime. + + // Assert + var result = Json.Deserialize(jsRuntime.LastInvocationArgsJson); + Assert.Equal(callId, result[0]); + Assert.False((bool)result[1]); // Fails + + // Make sure the method that threw the exception shows up in the call stack + // https://github.com/aspnet/AspNetCore/issues/8612 + var exception = (string)result[2]; + Assert.Contains(nameof(ThrowingClass.ThrowingMethod), exception); + }); + + [Fact] + public Task CanInvokeAsyncThrowingMethod() => WithJSRuntime(async jsRuntime => + { + // Arrange + + // Act + var callId = "123"; + var resultTask = jsRuntime.NextInvocationTask; + DotNetDispatcher.BeginInvoke(callId, thisAssemblyName, nameof(ThrowingClass.AsyncThrowingMethod), default, default); + + await resultTask; // This won't throw, it sets properties on the jsRuntime. + + // Assert + var result = Json.Deserialize(jsRuntime.LastInvocationArgsJson); + Assert.Equal(callId, result[0]); + Assert.False((bool)result[1]); // Fails + + // Make sure the method that threw the exception shows up in the call stack + // https://github.com/aspnet/AspNetCore/issues/8612 + var exception = (string)result[2]; + Assert.Contains(nameof(ThrowingClass.AsyncThrowingMethod), exception); + }); + + + Task WithJSRuntime(Action testCode) + { + return WithJSRuntime(jsRuntime => + { + testCode(jsRuntime); + return Task.CompletedTask; + }); + } + + async Task WithJSRuntime(Func testCode) + { + // Since the tests rely on the asynclocal JSRuntime.Current, ensure we + // are on a distinct async context with a non-null JSRuntime.Current + await Task.Yield(); + + var runtime = new TestJSRuntime(); + JSRuntime.SetCurrentJSRuntime(runtime); + await testCode(runtime); + } + + internal class SomeInteralType + { + [JSInvokable("MethodOnInternalType")] public void MyMethod() { } + } + + public class SomePublicType + { + public static bool DidInvokeMyInvocableStaticVoid; + public bool DidInvokeMyInvocableInstanceVoid; + + [JSInvokable("PrivateMethod")] private static void MyPrivateMethod() { } + [JSInvokable("ProtectedMethod")] protected static void MyProtectedMethod() { } + protected static void StaticMethodWithoutAttribute() { } + protected static void InstanceMethodWithoutAttribute() { } + + [JSInvokable("InvocableStaticVoid")] + public static void MyInvocableVoid() + { + DidInvokeMyInvocableStaticVoid = true; + } + + [JSInvokable("InvocableStaticNonVoid")] + public static object MyInvocableNonVoid() + => new TestDTO { StringVal = "Test", IntVal = 123 }; + + [JSInvokable("InvocableStaticWithParams")] + public static object[] MyInvocableWithParams(TestDTO dtoViaJson, int[] incrementAmounts, TestDTO dtoByRef) + => new object[] + { + new TestDTO // Return via JSON marshalling + { + StringVal = dtoViaJson.StringVal.ToUpperInvariant(), + IntVal = dtoViaJson.IntVal + incrementAmounts.Sum() + }, + new DotNetObjectRef(new TestDTO // Return by ref + { + StringVal = dtoByRef.StringVal.ToUpperInvariant(), + IntVal = dtoByRef.IntVal + incrementAmounts.Sum() + }) + }; + + [JSInvokable] + public static TestDTO InvokableMethodWithoutCustomIdentifier() + => new TestDTO { StringVal = "InvokableMethodWithoutCustomIdentifier", IntVal = 456 }; + + [JSInvokable] + public void InvokableInstanceVoid() + { + DidInvokeMyInvocableInstanceVoid = true; + } + + [JSInvokable] + public object[] InvokableInstanceMethod(string someString, TestDTO someDTO) + { + // Returning an array to make the point that object references + // can be embedded anywhere in the result + return new object[] + { + $"You passed {someString}", + new DotNetObjectRef(new TestDTO + { + IntVal = someDTO.IntVal + 1, + StringVal = someDTO.StringVal.ToUpperInvariant() + }) + }; + } + + [JSInvokable] + public async Task InvokableAsyncMethod(TestDTO dtoViaJson, TestDTO dtoByRef) + { + await Task.Delay(50); + return new object[] + { + new TestDTO // Return via JSON + { + StringVal = dtoViaJson.StringVal.ToUpperInvariant(), + IntVal = dtoViaJson.IntVal * 2, + }, + new DotNetObjectRef(new TestDTO // Return by ref + { + StringVal = dtoByRef.StringVal.ToUpperInvariant(), + IntVal = dtoByRef.IntVal * 2, + }) + }; + } + } + + public class BaseClass + { + public bool DidInvokeMyBaseClassInvocableInstanceVoid; + + [JSInvokable] + public void BaseClassInvokableInstanceVoid() + { + DidInvokeMyBaseClassInvocableInstanceVoid = true; + } + } + + public class DerivedClass : BaseClass + { + } + + public class TestDTO + { + public string StringVal { get; set; } + public int IntVal { get; set; } + } + + public class ThrowingClass + { + [JSInvokable] + public static string ThrowingMethod() + { + throw new InvalidTimeZoneException(); + } + + [JSInvokable] + public static async Task AsyncThrowingMethod() + { + await Task.Yield(); + throw new InvalidTimeZoneException(); + } + } + + public class TestJSRuntime : JSInProcessRuntimeBase + { + private TaskCompletionSource _nextInvocationTcs = new TaskCompletionSource(); + public Task NextInvocationTask => _nextInvocationTcs.Task; + public long LastInvocationAsyncHandle { get; private set; } + public string LastInvocationIdentifier { get; private set; } + public string LastInvocationArgsJson { get; private set; } + + protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson) + { + LastInvocationAsyncHandle = asyncHandle; + LastInvocationIdentifier = identifier; + LastInvocationArgsJson = argsJson; + _nextInvocationTcs.SetResult(null); + _nextInvocationTcs = new TaskCompletionSource(); + } + + protected override string InvokeJS(string identifier, string argsJson) + { + LastInvocationAsyncHandle = default; + LastInvocationIdentifier = identifier; + LastInvocationArgsJson = argsJson; + _nextInvocationTcs.SetResult(null); + _nextInvocationTcs = new TaskCompletionSource(); + return null; + } + } + } +} diff --git a/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectRefTest.cs b/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectRefTest.cs new file mode 100644 index 0000000000..1bdec6d465 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectRefTest.cs @@ -0,0 +1,68 @@ +// 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.Threading.Tasks; +using Xunit; + +namespace Microsoft.JSInterop.Tests +{ + public class DotNetObjectRefTest + { + [Fact] + public void CanAccessValue() + { + var obj = new object(); + Assert.Same(obj, new DotNetObjectRef(obj).Value); + } + + [Fact] + public void CanAssociateWithSameRuntimeMultipleTimes() + { + var objRef = new DotNetObjectRef(new object()); + var jsRuntime = new TestJsRuntime(); + objRef.EnsureAttachedToJsRuntime(jsRuntime); + objRef.EnsureAttachedToJsRuntime(jsRuntime); + } + + [Fact] + public void CannotAssociateWithDifferentRuntimes() + { + var objRef = new DotNetObjectRef(new object()); + var jsRuntime1 = new TestJsRuntime(); + var jsRuntime2 = new TestJsRuntime(); + objRef.EnsureAttachedToJsRuntime(jsRuntime1); + + var ex = Assert.Throws( + () => objRef.EnsureAttachedToJsRuntime(jsRuntime2)); + Assert.Contains("Do not attempt to re-use", ex.Message); + } + + [Fact] + public void NotifiesAssociatedJsRuntimeOfDisposal() + { + // Arrange + var objRef = new DotNetObjectRef(new object()); + var jsRuntime = new TestJsRuntime(); + objRef.EnsureAttachedToJsRuntime(jsRuntime); + + // Act + objRef.Dispose(); + + // Assert + Assert.Equal(new[] { objRef }, jsRuntime.UntrackedRefs); + } + + class TestJsRuntime : IJSRuntime + { + public List UntrackedRefs = new List(); + + public Task InvokeAsync(string identifier, params object[] args) + => throw new NotImplementedException(); + + public void UntrackObjectRef(DotNetObjectRef dotNetObjectRef) + => UntrackedRefs.Add(dotNetObjectRef); + } + } +} diff --git a/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeBaseTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeBaseTest.cs new file mode 100644 index 0000000000..a13d53677a --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeBaseTest.cs @@ -0,0 +1,117 @@ +// 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.JSInterop.Tests +{ + public class JSInProcessRuntimeBaseTest + { + [Fact(Skip = "https://github.com/aspnet/AspNetCore-Internal/issues/1807#issuecomment-470756811")] + public void DispatchesSyncCallsAndDeserializesResults() + { + // Arrange + var runtime = new TestJSInProcessRuntime + { + NextResultJson = Json.Serialize( + new TestDTO { IntValue = 123, StringValue = "Hello" }) + }; + + // Act + var syncResult = runtime.Invoke("test identifier 1", "arg1", 123, true); + var call = runtime.InvokeCalls.Single(); + + // Assert + Assert.Equal(123, syncResult.IntValue); + Assert.Equal("Hello", syncResult.StringValue); + Assert.Equal("test identifier 1", call.Identifier); + Assert.Equal("[\"arg1\",123,true]", call.ArgsJson); + } + + [Fact] + public void SerializesDotNetObjectWrappersInKnownFormat() + { + // Arrange + var runtime = new TestJSInProcessRuntime { NextResultJson = null }; + var obj1 = new object(); + var obj2 = new object(); + var obj3 = new object(); + + // Act + // Showing we can pass the DotNetObject either as top-level args or nested + var syncResult = runtime.Invoke("test identifier", + new DotNetObjectRef(obj1), + new Dictionary + { + { "obj2", new DotNetObjectRef(obj2) }, + { "obj3", new DotNetObjectRef(obj3) } + }); + + // Assert: Handles null result string + Assert.Null(syncResult); + + // Assert: Serialized as expected + var call = runtime.InvokeCalls.Single(); + Assert.Equal("test identifier", call.Identifier); + Assert.Equal("[\"__dotNetObject:1\",{\"obj2\":\"__dotNetObject:2\",\"obj3\":\"__dotNetObject:3\"}]", call.ArgsJson); + + // Assert: Objects were tracked + Assert.Same(obj1, runtime.ArgSerializerStrategy.FindDotNetObject(1)); + Assert.Same(obj2, runtime.ArgSerializerStrategy.FindDotNetObject(2)); + Assert.Same(obj3, runtime.ArgSerializerStrategy.FindDotNetObject(3)); + } + + [Fact] + public void SyncCallResultCanIncludeDotNetObjects() + { + // Arrange + var runtime = new TestJSInProcessRuntime + { + NextResultJson = "[\"__dotNetObject:2\",\"__dotNetObject:1\"]" + }; + var obj1 = new object(); + var obj2 = new object(); + + // Act + var syncResult = runtime.Invoke("test identifier", + new DotNetObjectRef(obj1), + "some other arg", + new DotNetObjectRef(obj2)); + var call = runtime.InvokeCalls.Single(); + + // Assert + Assert.Equal(new[] { obj2, obj1 }, syncResult); + } + + class TestDTO + { + public int IntValue { get; set; } + public string StringValue { get; set; } + } + + class TestJSInProcessRuntime : JSInProcessRuntimeBase + { + public List InvokeCalls { get; set; } = new List(); + + public string NextResultJson { get; set; } + + protected override string InvokeJS(string identifier, string argsJson) + { + InvokeCalls.Add(new InvokeArgs { Identifier = identifier, ArgsJson = argsJson }); + return NextResultJson; + } + + public class InvokeArgs + { + public string Identifier { get; set; } + public string ArgsJson { get; set; } + } + + protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson) + => throw new NotImplementedException("This test only covers sync calls"); + } + } +} diff --git a/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeBaseTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeBaseTest.cs new file mode 100644 index 0000000000..ab048e812f --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeBaseTest.cs @@ -0,0 +1,191 @@ +// 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.JSInterop.Internal; +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Microsoft.JSInterop.Tests +{ + public class JSRuntimeBaseTest + { + [Fact] + public void DispatchesAsyncCallsWithDistinctAsyncHandles() + { + // Arrange + var runtime = new TestJSRuntime(); + + // Act + runtime.InvokeAsync("test identifier 1", "arg1", 123, true); + runtime.InvokeAsync("test identifier 2", "some other arg"); + + // Assert + Assert.Collection(runtime.BeginInvokeCalls, + call => + { + Assert.Equal("test identifier 1", call.Identifier); + Assert.Equal("[\"arg1\",123,true]", call.ArgsJson); + }, + call => + { + Assert.Equal("test identifier 2", call.Identifier); + Assert.Equal("[\"some other arg\"]", call.ArgsJson); + Assert.NotEqual(runtime.BeginInvokeCalls[0].AsyncHandle, call.AsyncHandle); + }); + } + + [Fact] + public void CanCompleteAsyncCallsAsSuccess() + { + // Arrange + var runtime = new TestJSRuntime(); + + // Act/Assert: Tasks not initially completed + var unrelatedTask = runtime.InvokeAsync("unrelated call", Array.Empty()); + var task = runtime.InvokeAsync("test identifier", Array.Empty()); + Assert.False(unrelatedTask.IsCompleted); + Assert.False(task.IsCompleted); + + // Act/Assert: Task can be completed + runtime.OnEndInvoke( + runtime.BeginInvokeCalls[1].AsyncHandle, + /* succeeded: */ true, + "my result"); + Assert.False(unrelatedTask.IsCompleted); + Assert.True(task.IsCompleted); + Assert.Equal("my result", task.Result); + } + + [Fact] + public void CanCompleteAsyncCallsAsFailure() + { + // Arrange + var runtime = new TestJSRuntime(); + + // Act/Assert: Tasks not initially completed + var unrelatedTask = runtime.InvokeAsync("unrelated call", Array.Empty()); + var task = runtime.InvokeAsync("test identifier", Array.Empty()); + Assert.False(unrelatedTask.IsCompleted); + Assert.False(task.IsCompleted); + + // Act/Assert: Task can be failed + runtime.OnEndInvoke( + runtime.BeginInvokeCalls[1].AsyncHandle, + /* succeeded: */ false, + "This is a test exception"); + Assert.False(unrelatedTask.IsCompleted); + Assert.True(task.IsCompleted); + + Assert.IsType(task.Exception); + Assert.IsType(task.Exception.InnerException); + Assert.Equal("This is a test exception", ((JSException)task.Exception.InnerException).Message); + } + + [Fact] + public void CannotCompleteSameAsyncCallMoreThanOnce() + { + // Arrange + var runtime = new TestJSRuntime(); + + // Act/Assert + runtime.InvokeAsync("test identifier", Array.Empty()); + var asyncHandle = runtime.BeginInvokeCalls[0].AsyncHandle; + runtime.OnEndInvoke(asyncHandle, true, null); + var ex = Assert.Throws(() => + { + // Second "end invoke" will fail + runtime.OnEndInvoke(asyncHandle, true, null); + }); + Assert.Equal($"There is no pending task with handle '{asyncHandle}'.", ex.Message); + } + + [Fact] + public void SerializesDotNetObjectWrappersInKnownFormat() + { + // Arrange + var runtime = new TestJSRuntime(); + var obj1 = new object(); + var obj2 = new object(); + var obj3 = new object(); + + // Act + // Showing we can pass the DotNetObject either as top-level args or nested + var obj1Ref = new DotNetObjectRef(obj1); + var obj1DifferentRef = new DotNetObjectRef(obj1); + runtime.InvokeAsync("test identifier", + obj1Ref, + new Dictionary + { + { "obj2", new DotNetObjectRef(obj2) }, + { "obj3", new DotNetObjectRef(obj3) }, + { "obj1SameRef", obj1Ref }, + { "obj1DifferentRef", obj1DifferentRef }, + }); + + // Assert: Serialized as expected + var call = runtime.BeginInvokeCalls.Single(); + Assert.Equal("test identifier", call.Identifier); + Assert.Equal("[\"__dotNetObject:1\",{\"obj2\":\"__dotNetObject:2\",\"obj3\":\"__dotNetObject:3\",\"obj1SameRef\":\"__dotNetObject:1\",\"obj1DifferentRef\":\"__dotNetObject:4\"}]", call.ArgsJson); + + // Assert: Objects were tracked + Assert.Same(obj1, runtime.ArgSerializerStrategy.FindDotNetObject(1)); + Assert.Same(obj2, runtime.ArgSerializerStrategy.FindDotNetObject(2)); + Assert.Same(obj3, runtime.ArgSerializerStrategy.FindDotNetObject(3)); + Assert.Same(obj1, runtime.ArgSerializerStrategy.FindDotNetObject(4)); + } + + [Fact] + public void SupportsCustomSerializationForArguments() + { + // Arrange + var runtime = new TestJSRuntime(); + + // Arrange/Act + runtime.InvokeAsync("test identifier", + new WithCustomArgSerializer()); + + // Asssert + var call = runtime.BeginInvokeCalls.Single(); + Assert.Equal("[{\"key1\":\"value1\",\"key2\":123}]", call.ArgsJson); + } + + class TestJSRuntime : JSRuntimeBase + { + public List BeginInvokeCalls = new List(); + + public class BeginInvokeAsyncArgs + { + public long AsyncHandle { get; set; } + public string Identifier { get; set; } + public string ArgsJson { get; set; } + } + + protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson) + { + BeginInvokeCalls.Add(new BeginInvokeAsyncArgs + { + AsyncHandle = asyncHandle, + Identifier = identifier, + ArgsJson = argsJson, + }); + } + + public void OnEndInvoke(long asyncHandle, bool succeeded, object resultOrException) + => EndInvokeJS(asyncHandle, succeeded, resultOrException); + } + + class WithCustomArgSerializer : ICustomArgSerializer + { + public object ToJsonPrimitive() + { + return new Dictionary + { + { "key1", "value1" }, + { "key2", 123 }, + }; + } + } + } +} diff --git a/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs new file mode 100644 index 0000000000..b8a1c363dc --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs @@ -0,0 +1,37 @@ +// 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.Threading.Tasks; +using Xunit; + +namespace Microsoft.JSInterop.Tests +{ + public class JSRuntimeTest + { + [Fact] + public async Task CanHaveDistinctJSRuntimeInstancesInEachAsyncContext() + { + var tasks = Enumerable.Range(0, 20).Select(async _ => + { + var jsRuntime = new FakeJSRuntime(); + JSRuntime.SetCurrentJSRuntime(jsRuntime); + await Task.Delay(50).ConfigureAwait(false); + Assert.Same(jsRuntime, JSRuntime.Current); + }); + + await Task.WhenAll(tasks); + Assert.Null(JSRuntime.Current); + } + + private class FakeJSRuntime : IJSRuntime + { + public Task InvokeAsync(string identifier, params object[] args) + => throw new NotImplementedException(); + + public void UntrackObjectRef(DotNetObjectRef dotNetObjectRef) + => throw new NotImplementedException(); + } + } +} diff --git a/src/JSInterop/Microsoft.JSInterop/test/JsonUtilTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JsonUtilTest.cs new file mode 100644 index 0000000000..2b239faab9 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/test/JsonUtilTest.cs @@ -0,0 +1,349 @@ +// 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.JSInterop.Internal; +using System; +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.JSInterop.Tests +{ + public class JsonUtilTest + { + // It's not useful to have a complete set of behavior specifications for + // what the JSON serializer/deserializer does in all cases here. We merely + // expose a simple wrapper over a third-party library that maintains its + // own specs and tests. + // + // We should only add tests here to cover behaviors that Blazor itself + // depends on. + + [Theory] + [InlineData(null, "null")] + [InlineData("My string", "\"My string\"")] + [InlineData(123, "123")] + [InlineData(123.456f, "123.456")] + [InlineData(123.456d, "123.456")] + [InlineData(true, "true")] + public void CanSerializePrimitivesToJson(object value, string expectedJson) + { + Assert.Equal(expectedJson, Json.Serialize(value)); + } + + [Theory] + [InlineData("null", null)] + [InlineData("\"My string\"", "My string")] + [InlineData("123", 123L)] // Would also accept 123 as a System.Int32, but Int64 is fine as a default + [InlineData("123.456", 123.456d)] + [InlineData("true", true)] + public void CanDeserializePrimitivesFromJson(string json, object expectedValue) + { + Assert.Equal(expectedValue, Json.Deserialize(json)); + } + + [Fact] + public void CanSerializeClassToJson() + { + // Arrange + var person = new Person + { + Id = 1844, + Name = "Athos", + Pets = new[] { "Aramis", "Porthos", "D'Artagnan" }, + Hobby = Hobbies.Swordfighting, + SecondaryHobby = Hobbies.Reading, + Nicknames = new List { "Comte de la Fère", "Armand" }, + BirthInstant = new DateTimeOffset(1825, 8, 6, 18, 45, 21, TimeSpan.FromHours(-6)), + Age = new TimeSpan(7665, 1, 30, 0), + Allergies = new Dictionary { { "Ducks", true }, { "Geese", false } }, + }; + + // Act/Assert + Assert.Equal( + "{\"id\":1844,\"name\":\"Athos\",\"pets\":[\"Aramis\",\"Porthos\",\"D'Artagnan\"],\"hobby\":2,\"secondaryHobby\":1,\"nullHobby\":null,\"nicknames\":[\"Comte de la Fère\",\"Armand\"],\"birthInstant\":\"1825-08-06T18:45:21.0000000-06:00\",\"age\":\"7665.01:30:00\",\"allergies\":{\"Ducks\":true,\"Geese\":false}}", + Json.Serialize(person)); + } + + [Fact] + public void CanDeserializeClassFromJson() + { + // Arrange + var json = "{\"id\":1844,\"name\":\"Athos\",\"pets\":[\"Aramis\",\"Porthos\",\"D'Artagnan\"],\"hobby\":2,\"secondaryHobby\":1,\"nullHobby\":null,\"nicknames\":[\"Comte de la Fère\",\"Armand\"],\"birthInstant\":\"1825-08-06T18:45:21.0000000-06:00\",\"age\":\"7665.01:30:00\",\"allergies\":{\"Ducks\":true,\"Geese\":false}}"; + + // Act + var person = Json.Deserialize(json); + + // Assert + Assert.Equal(1844, person.Id); + Assert.Equal("Athos", person.Name); + Assert.Equal(new[] { "Aramis", "Porthos", "D'Artagnan" }, person.Pets); + Assert.Equal(Hobbies.Swordfighting, person.Hobby); + Assert.Equal(Hobbies.Reading, person.SecondaryHobby); + Assert.Null(person.NullHobby); + Assert.Equal(new[] { "Comte de la Fère", "Armand" }, person.Nicknames); + Assert.Equal(new DateTimeOffset(1825, 8, 6, 18, 45, 21, TimeSpan.FromHours(-6)), person.BirthInstant); + Assert.Equal(new TimeSpan(7665, 1, 30, 0), person.Age); + Assert.Equal(new Dictionary { { "Ducks", true }, { "Geese", false } }, person.Allergies); + } + + [Fact] + public void CanDeserializeWithCaseInsensitiveKeys() + { + // Arrange + var json = "{\"ID\":1844,\"NamE\":\"Athos\"}"; + + // Act + var person = Json.Deserialize(json); + + // Assert + Assert.Equal(1844, person.Id); + Assert.Equal("Athos", person.Name); + } + + [Fact] + public void DeserializationPrefersPropertiesOverFields() + { + // Arrange + var json = "{\"member1\":\"Hello\"}"; + + // Act + var person = Json.Deserialize(json); + + // Assert + Assert.Equal("Hello", person.Member1); + Assert.Null(person.member1); + } + + [Fact] + public void CanSerializeStructToJson() + { + // Arrange + var commandResult = new SimpleStruct + { + StringProperty = "Test", + BoolProperty = true, + NullableIntProperty = 1 + }; + + // Act + var result = Json.Serialize(commandResult); + + // Assert + Assert.Equal("{\"stringProperty\":\"Test\",\"boolProperty\":true,\"nullableIntProperty\":1}", result); + } + + [Fact] + public void CanDeserializeStructFromJson() + { + // Arrange + var json = "{\"stringProperty\":\"Test\",\"boolProperty\":true,\"nullableIntProperty\":1}"; + + //Act + var simpleError = Json.Deserialize(json); + + // Assert + Assert.Equal("Test", simpleError.StringProperty); + Assert.True(simpleError.BoolProperty); + Assert.Equal(1, simpleError.NullableIntProperty); + } + + [Fact] + public void CanCreateInstanceOfClassWithPrivateConstructor() + { + // Arrange + var expectedName = "NameValue"; + var json = $"{{\"Name\":\"{expectedName}\"}}"; + + // Act + var instance = Json.Deserialize(json); + + // Assert + Assert.Equal(expectedName, instance.Name); + } + + [Fact] + public void CanSetValueOfPublicPropertiesWithNonPublicSetters() + { + // Arrange + var expectedPrivateValue = "PrivateValue"; + var expectedProtectedValue = "ProtectedValue"; + var expectedInternalValue = "InternalValue"; + + var json = "{" + + $"\"PrivateSetter\":\"{expectedPrivateValue}\"," + + $"\"ProtectedSetter\":\"{expectedProtectedValue}\"," + + $"\"InternalSetter\":\"{expectedInternalValue}\"," + + "}"; + + // Act + var instance = Json.Deserialize(json); + + // Assert + Assert.Equal(expectedPrivateValue, instance.PrivateSetter); + Assert.Equal(expectedProtectedValue, instance.ProtectedSetter); + Assert.Equal(expectedInternalValue, instance.InternalSetter); + } + + [Fact] + public void RejectsTypesWithAmbiguouslyNamedProperties() + { + var ex = Assert.Throws(() => + { + Json.Deserialize("{}"); + }); + + Assert.Equal($"The type '{typeof(ClashingProperties).FullName}' contains multiple public properties " + + $"with names case-insensitively matching '{nameof(ClashingProperties.PROP1).ToLowerInvariant()}'. " + + $"Such types cannot be used for JSON deserialization.", + ex.Message); + } + + [Fact] + public void RejectsTypesWithAmbiguouslyNamedFields() + { + var ex = Assert.Throws(() => + { + Json.Deserialize("{}"); + }); + + Assert.Equal($"The type '{typeof(ClashingFields).FullName}' contains multiple public fields " + + $"with names case-insensitively matching '{nameof(ClashingFields.Field1).ToLowerInvariant()}'. " + + $"Such types cannot be used for JSON deserialization.", + ex.Message); + } + + [Fact] + public void NonEmptyConstructorThrowsUsefulException() + { + // Arrange + var json = "{\"Property\":1}"; + var type = typeof(NonEmptyConstructorPoco); + + // Act + var exception = Assert.Throws(() => + { + Json.Deserialize(json); + }); + + // Assert + Assert.Equal( + $"Cannot deserialize JSON into type '{type.FullName}' because it does not have a public parameterless constructor.", + exception.Message); + } + + // Test cases based on https://github.com/JamesNK/Newtonsoft.Json/blob/122afba9908832bd5ac207164ee6c303bfd65cf1/Src/Newtonsoft.Json.Tests/Utilities/StringUtilsTests.cs#L41 + // The only difference is that our logic doesn't have to handle space-separated words, + // because we're only use this for camelcasing .NET member names + // + // Not all of the following cases are really valid .NET member names, but we have no reason + // to implement more logic to detect invalid member names besides the basics (null or empty). + [Theory] + [InlineData("URLValue", "urlValue")] + [InlineData("URL", "url")] + [InlineData("ID", "id")] + [InlineData("I", "i")] + [InlineData("Person", "person")] + [InlineData("xPhone", "xPhone")] + [InlineData("XPhone", "xPhone")] + [InlineData("X_Phone", "x_Phone")] + [InlineData("X__Phone", "x__Phone")] + [InlineData("IsCIA", "isCIA")] + [InlineData("VmQ", "vmQ")] + [InlineData("Xml2Json", "xml2Json")] + [InlineData("SnAkEcAsE", "snAkEcAsE")] + [InlineData("SnA__kEcAsE", "snA__kEcAsE")] + [InlineData("already_snake_case_", "already_snake_case_")] + [InlineData("IsJSONProperty", "isJSONProperty")] + [InlineData("SHOUTING_CASE", "shoutinG_CASE")] + [InlineData("9999-12-31T23:59:59.9999999Z", "9999-12-31T23:59:59.9999999Z")] + [InlineData("Hi!! This is text. Time to test.", "hi!! This is text. Time to test.")] + [InlineData("BUILDING", "building")] + [InlineData("BUILDINGProperty", "buildingProperty")] + public void MemberNameToCamelCase_Valid(string input, string expectedOutput) + { + Assert.Equal(expectedOutput, CamelCase.MemberNameToCamelCase(input)); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + public void MemberNameToCamelCase_Invalid(string input) + { + var ex = Assert.Throws(() => + CamelCase.MemberNameToCamelCase(input)); + Assert.Equal("value", ex.ParamName); + Assert.StartsWith($"The value '{input ?? "null"}' is not a valid member name.", ex.Message); + } + + class NonEmptyConstructorPoco + { + public NonEmptyConstructorPoco(int parameter) { } + + public int Property { get; set; } + } + + struct SimpleStruct + { + public string StringProperty { get; set; } + public bool BoolProperty { get; set; } + public int? NullableIntProperty { get; set; } + } + + class Person + { + public int Id { get; set; } + public string Name { get; set; } + public string[] Pets { get; set; } + public Hobbies Hobby { get; set; } + public Hobbies? SecondaryHobby { get; set; } + public Hobbies? NullHobby { get; set; } + public IList Nicknames { get; set; } + public DateTimeOffset BirthInstant { get; set; } + public TimeSpan Age { get; set; } + public IDictionary Allergies { get; set; } + } + + enum Hobbies { Reading = 1, Swordfighting = 2 } + +#pragma warning disable 0649 + class ClashingProperties + { + public string Prop1 { get; set; } + public int PROP1 { get; set; } + } + + class ClashingFields + { + public string Field1; + public int field1; + } + + class PrefersPropertiesOverFields + { + public string member1; + public string Member1 { get; set; } + } +#pragma warning restore 0649 + + class PrivateConstructor + { + public string Name { get; set; } + + private PrivateConstructor() + { + } + + public PrivateConstructor(string name) + { + Name = name; + } + } + + class NonPublicSetterOnPublicProperty + { + public string PrivateSetter { get; private set; } + public string ProtectedSetter { get; protected set; } + public string InternalSetter { get; internal set; } + } + } +} diff --git a/src/JSInterop/Microsoft.JSInterop/test/Microsoft.JSInterop.Tests.csproj b/src/JSInterop/Microsoft.JSInterop/test/Microsoft.JSInterop.Tests.csproj new file mode 100644 index 0000000000..a39b082180 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/test/Microsoft.JSInterop.Tests.csproj @@ -0,0 +1,15 @@ + + + + netcoreapp3.0 + + + + + + + + + + + diff --git a/src/JSInterop/Microsoft.JSInterop/test/xunit.runner.json b/src/JSInterop/Microsoft.JSInterop/test/xunit.runner.json new file mode 100644 index 0000000000..0f2ad9f769 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/test/xunit.runner.json @@ -0,0 +1,3 @@ +{ + "shadowCopy": true +} diff --git a/src/JSInterop/Mono.WebAssembly.Interop/ref/Mono.WebAssembly.Interop.csproj b/src/JSInterop/Mono.WebAssembly.Interop/ref/Mono.WebAssembly.Interop.csproj new file mode 100644 index 0000000000..a1ba605705 --- /dev/null +++ b/src/JSInterop/Mono.WebAssembly.Interop/ref/Mono.WebAssembly.Interop.csproj @@ -0,0 +1,10 @@ + + + + netstandard2.0 + + + + + + diff --git a/src/JSInterop/Mono.WebAssembly.Interop/ref/Mono.WebAssembly.Interop.netstandard2.0.cs b/src/JSInterop/Mono.WebAssembly.Interop/ref/Mono.WebAssembly.Interop.netstandard2.0.cs new file mode 100644 index 0000000000..033e97237a --- /dev/null +++ b/src/JSInterop/Mono.WebAssembly.Interop/ref/Mono.WebAssembly.Interop.netstandard2.0.cs @@ -0,0 +1,16 @@ +// 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 Mono.WebAssembly.Interop +{ + public partial class MonoWebAssemblyJSRuntime : Microsoft.JSInterop.JSInProcessRuntimeBase + { + public MonoWebAssemblyJSRuntime() { } + protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson) { } + protected override string InvokeJS(string identifier, string argsJson) { throw null; } + public TRes InvokeUnmarshalled(string identifier) { throw null; } + public TRes InvokeUnmarshalled(string identifier, T0 arg0) { throw null; } + public TRes InvokeUnmarshalled(string identifier, T0 arg0, T1 arg1) { throw null; } + public TRes InvokeUnmarshalled(string identifier, T0 arg0, T1 arg1, T2 arg2) { throw null; } + } +} diff --git a/src/JSInterop/Mono.WebAssembly.Interop/src/InternalCalls.cs b/src/JSInterop/Mono.WebAssembly.Interop/src/InternalCalls.cs new file mode 100644 index 0000000000..60c0cdc429 --- /dev/null +++ b/src/JSInterop/Mono.WebAssembly.Interop/src/InternalCalls.cs @@ -0,0 +1,25 @@ +// 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; + +namespace WebAssembly.JSInterop +{ + /// + /// Methods that map to the functions compiled into the Mono WebAssembly runtime, + /// as defined by 'mono_add_internal_call' calls in driver.c + /// + internal class InternalCalls + { + // The exact namespace, type, and method names must match the corresponding entries + // in driver.c in the Mono distribution + + // We're passing asyncHandle by ref not because we want it to be writable, but so it gets + // passed as a pointer (4 bytes). We can pass 4-byte values, but not 8-byte ones. + [MethodImpl(MethodImplOptions.InternalCall)] + public static extern string InvokeJSMarshalled(out string exception, ref long asyncHandle, string functionIdentifier, string argsJson); + + [MethodImpl(MethodImplOptions.InternalCall)] + public static extern TRes InvokeJSUnmarshalled(out string exception, string functionIdentifier, T0 arg0, T1 arg1, T2 arg2); + } +} diff --git a/src/JSInterop/Mono.WebAssembly.Interop/src/Mono.WebAssembly.Interop.csproj b/src/JSInterop/Mono.WebAssembly.Interop/src/Mono.WebAssembly.Interop.csproj new file mode 100644 index 0000000000..413d084e48 --- /dev/null +++ b/src/JSInterop/Mono.WebAssembly.Interop/src/Mono.WebAssembly.Interop.csproj @@ -0,0 +1,15 @@ + + + + netstandard2.0 + Abstractions and features for interop between Mono WebAssembly and JavaScript code. + wasm;javascript;interop + true + true + + + + + + + diff --git a/src/JSInterop/Mono.WebAssembly.Interop/src/MonoWebAssemblyJSRuntime.cs b/src/JSInterop/Mono.WebAssembly.Interop/src/MonoWebAssemblyJSRuntime.cs new file mode 100644 index 0000000000..9a502b8bc8 --- /dev/null +++ b/src/JSInterop/Mono.WebAssembly.Interop/src/MonoWebAssemblyJSRuntime.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 Microsoft.JSInterop; +using WebAssembly.JSInterop; + +namespace Mono.WebAssembly.Interop +{ + /// + /// Provides methods for invoking JavaScript functions for applications running + /// on the Mono WebAssembly runtime. + /// + public class MonoWebAssemblyJSRuntime : JSInProcessRuntimeBase + { + /// + protected override string InvokeJS(string identifier, string argsJson) + { + var noAsyncHandle = default(long); + var result = InternalCalls.InvokeJSMarshalled(out var exception, ref noAsyncHandle, identifier, argsJson); + return exception != null + ? throw new JSException(exception) + : result; + } + + /// + protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson) + { + InternalCalls.InvokeJSMarshalled(out _, ref asyncHandle, identifier, argsJson); + } + + // Invoked via Mono's JS interop mechanism (invoke_method) + private static string InvokeDotNet(string assemblyName, string methodIdentifier, string dotNetObjectId, string argsJson) + => DotNetDispatcher.Invoke(assemblyName, methodIdentifier, dotNetObjectId == null ? default : long.Parse(dotNetObjectId), argsJson); + + // Invoked via Mono's JS interop mechanism (invoke_method) + private static void BeginInvokeDotNet(string callId, string assemblyNameOrDotNetObjectId, string methodIdentifier, string argsJson) + { + // Figure out whether 'assemblyNameOrDotNetObjectId' is the assembly name or the instance ID + // We only need one for any given call. This helps to work around the limitation that we can + // only pass a maximum of 4 args in a call from JS to Mono WebAssembly. + string assemblyName; + long dotNetObjectId; + if (char.IsDigit(assemblyNameOrDotNetObjectId[0])) + { + dotNetObjectId = long.Parse(assemblyNameOrDotNetObjectId); + assemblyName = null; + } + else + { + dotNetObjectId = default; + assemblyName = assemblyNameOrDotNetObjectId; + } + + DotNetDispatcher.BeginInvoke(callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson); + } + + #region Custom MonoWebAssemblyJSRuntime methods + + /// + /// Invokes the JavaScript function registered with the specified identifier. + /// + /// The .NET type corresponding to the function's return value type. + /// The identifier used when registering the target function. + /// The result of the function invocation. + public TRes InvokeUnmarshalled(string identifier) + => InvokeUnmarshalled(identifier, null, null, null); + + /// + /// Invokes the JavaScript function registered with the specified identifier. + /// + /// The type of the first argument. + /// The .NET type corresponding to the function's return value type. + /// The identifier used when registering the target function. + /// The first argument. + /// The result of the function invocation. + public TRes InvokeUnmarshalled(string identifier, T0 arg0) + => InvokeUnmarshalled(identifier, arg0, null, null); + + /// + /// Invokes the JavaScript function registered with the specified identifier. + /// + /// The type of the first argument. + /// The type of the second argument. + /// The .NET type corresponding to the function's return value type. + /// The identifier used when registering the target function. + /// The first argument. + /// The second argument. + /// The result of the function invocation. + public TRes InvokeUnmarshalled(string identifier, T0 arg0, T1 arg1) + => InvokeUnmarshalled(identifier, arg0, arg1, null); + + /// + /// Invokes the JavaScript function registered with the specified identifier. + /// + /// The type of the first argument. + /// The type of the second argument. + /// The type of the third argument. + /// The .NET type corresponding to the function's return value type. + /// The identifier used when registering the target function. + /// The first argument. + /// The second argument. + /// The third argument. + /// The result of the function invocation. + public TRes InvokeUnmarshalled(string identifier, T0 arg0, T1 arg1, T2 arg2) + { + var result = InternalCalls.InvokeJSUnmarshalled(out var exception, identifier, arg0, arg1, arg2); + return exception != null + ? throw new JSException(exception) + : result; + } + + #endregion + } +} diff --git a/src/JSInterop/README.md b/src/JSInterop/README.md new file mode 100644 index 0000000000..dcdaf7618e --- /dev/null +++ b/src/JSInterop/README.md @@ -0,0 +1,8 @@ +# jsinterop + +This repo is for `Microsoft.JSInterop`, a package that provides abstractions and features for interop between .NET and JavaScript code. + +## Usage + +The primary use case is for applications built with Mono WebAssembly or Blazor. It's not expected that developers will typically use these libraries separately from Mono WebAssembly, Blazor, or a similar technology. + diff --git a/src/Localization/Abstractions/ref/Microsoft.Extensions.Localization.Abstractions.csproj b/src/Localization/Abstractions/ref/Microsoft.Extensions.Localization.Abstractions.csproj new file mode 100644 index 0000000000..0608c06147 --- /dev/null +++ b/src/Localization/Abstractions/ref/Microsoft.Extensions.Localization.Abstractions.csproj @@ -0,0 +1,10 @@ + + + + netstandard2.0 + + + + + + diff --git a/src/Localization/Abstractions/ref/Microsoft.Extensions.Localization.Abstractions.netstandard2.0.cs b/src/Localization/Abstractions/ref/Microsoft.Extensions.Localization.Abstractions.netstandard2.0.cs new file mode 100644 index 0000000000..174cac28e5 --- /dev/null +++ b/src/Localization/Abstractions/ref/Microsoft.Extensions.Localization.Abstractions.netstandard2.0.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. + +namespace Microsoft.Extensions.Localization +{ + public partial interface IStringLocalizer + { + Microsoft.Extensions.Localization.LocalizedString this[string name] { get; } + Microsoft.Extensions.Localization.LocalizedString this[string name, params object[] arguments] { get; } + System.Collections.Generic.IEnumerable GetAllStrings(bool includeParentCultures); + [System.ObsoleteAttribute("This method is obsolete. Use `CurrentCulture` and `CurrentUICulture` instead.")] + Microsoft.Extensions.Localization.IStringLocalizer WithCulture(System.Globalization.CultureInfo culture); + } + public partial interface IStringLocalizerFactory + { + Microsoft.Extensions.Localization.IStringLocalizer Create(string baseName, string location); + Microsoft.Extensions.Localization.IStringLocalizer Create(System.Type resourceSource); + } + public partial interface IStringLocalizer : Microsoft.Extensions.Localization.IStringLocalizer + { + } + public partial class LocalizedString + { + public LocalizedString(string name, string value) { } + public LocalizedString(string name, string value, bool resourceNotFound) { } + public LocalizedString(string name, string value, bool resourceNotFound, string searchedLocation) { } + public string Name { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public bool ResourceNotFound { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public string SearchedLocation { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public string Value { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public static implicit operator string (Microsoft.Extensions.Localization.LocalizedString localizedString) { throw null; } + public override string ToString() { throw null; } + } + public static partial class StringLocalizerExtensions + { + public static System.Collections.Generic.IEnumerable GetAllStrings(this Microsoft.Extensions.Localization.IStringLocalizer stringLocalizer) { throw null; } + public static Microsoft.Extensions.Localization.LocalizedString GetString(this Microsoft.Extensions.Localization.IStringLocalizer stringLocalizer, string name) { throw null; } + public static Microsoft.Extensions.Localization.LocalizedString GetString(this Microsoft.Extensions.Localization.IStringLocalizer stringLocalizer, string name, params object[] arguments) { throw null; } + } + public partial class StringLocalizer : Microsoft.Extensions.Localization.IStringLocalizer, Microsoft.Extensions.Localization.IStringLocalizer + { + public StringLocalizer(Microsoft.Extensions.Localization.IStringLocalizerFactory factory) { } + public virtual Microsoft.Extensions.Localization.LocalizedString this[string name] { get { throw null; } } + public virtual Microsoft.Extensions.Localization.LocalizedString this[string name, params object[] arguments] { get { throw null; } } + public System.Collections.Generic.IEnumerable GetAllStrings(bool includeParentCultures) { throw null; } + [System.ObsoleteAttribute("This method is obsolete. Use `CurrentCulture` and `CurrentUICulture` instead.")] + public virtual Microsoft.Extensions.Localization.IStringLocalizer WithCulture(System.Globalization.CultureInfo culture) { throw null; } + } +} diff --git a/src/Localization/Abstractions/src/IStringLocalizer.cs b/src/Localization/Abstractions/src/IStringLocalizer.cs index 0e1145bbca..cabdf434ac 100644 --- a/src/Localization/Abstractions/src/IStringLocalizer.cs +++ b/src/Localization/Abstractions/src/IStringLocalizer.cs @@ -1,6 +1,7 @@ -// 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. +// 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.Globalization; @@ -40,6 +41,7 @@ namespace Microsoft.Extensions.Localization /// /// The to use. /// A culture-specific . + [Obsolete("This method is obsolete. Use `CurrentCulture` and `CurrentUICulture` instead.")] IStringLocalizer WithCulture(CultureInfo culture); } -} \ No newline at end of file +} diff --git a/src/Localization/Abstractions/src/IStringLocalizerOfT.cs b/src/Localization/Abstractions/src/IStringLocalizerOfT.cs index 695678a900..bdc2a1c7b7 100644 --- a/src/Localization/Abstractions/src/IStringLocalizerOfT.cs +++ b/src/Localization/Abstractions/src/IStringLocalizerOfT.cs @@ -7,8 +7,8 @@ namespace Microsoft.Extensions.Localization /// Represents an that provides strings for . /// /// The to provide strings for. - public interface IStringLocalizer : IStringLocalizer + public interface IStringLocalizer : IStringLocalizer { } -} \ No newline at end of file +} diff --git a/src/Localization/Abstractions/src/Microsoft.Extensions.Localization.Abstractions.csproj b/src/Localization/Abstractions/src/Microsoft.Extensions.Localization.Abstractions.csproj index 8508eb071a..33f58b6358 100644 --- a/src/Localization/Abstractions/src/Microsoft.Extensions.Localization.Abstractions.csproj +++ b/src/Localization/Abstractions/src/Microsoft.Extensions.Localization.Abstractions.csproj @@ -10,6 +10,7 @@ Microsoft.Extensions.Localization.IStringLocalizer<T> $(NoWarn);CS1591 true localization + true diff --git a/src/Localization/Abstractions/src/StringLocalizerOfT.cs b/src/Localization/Abstractions/src/StringLocalizerOfT.cs index 131c1126ec..4190ca14ff 100644 --- a/src/Localization/Abstractions/src/StringLocalizerOfT.cs +++ b/src/Localization/Abstractions/src/StringLocalizerOfT.cs @@ -1,5 +1,5 @@ -// 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. +// 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; @@ -30,6 +30,7 @@ namespace Microsoft.Extensions.Localization } /// + [Obsolete("This method is obsolete. Use `CurrentCulture` and `CurrentUICulture` instead.")] public virtual IStringLocalizer WithCulture(CultureInfo culture) => _localizer.WithCulture(culture); /// @@ -64,4 +65,4 @@ namespace Microsoft.Extensions.Localization public IEnumerable GetAllStrings(bool includeParentCultures) => _localizer.GetAllStrings(includeParentCultures); } -} \ No newline at end of file +} diff --git a/src/Localization/Abstractions/src/baseline.netcore.json b/src/Localization/Abstractions/src/baseline.netcore.json deleted file mode 100644 index 02ba71db8e..0000000000 --- a/src/Localization/Abstractions/src/baseline.netcore.json +++ /dev/null @@ -1,413 +0,0 @@ -{ - "AssemblyIdentity": "Microsoft.Extensions.Localization.Abstractions, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", - "Types": [ - { - "Name": "Microsoft.Extensions.Localization.IStringLocalizer", - "Visibility": "Public", - "Kind": "Interface", - "Abstract": true, - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "get_Item", - "Parameters": [ - { - "Name": "name", - "Type": "System.String" - } - ], - "ReturnType": "Microsoft.Extensions.Localization.LocalizedString", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "get_Item", - "Parameters": [ - { - "Name": "name", - "Type": "System.String" - }, - { - "Name": "arguments", - "Type": "System.Object[]", - "IsParams": true - } - ], - "ReturnType": "Microsoft.Extensions.Localization.LocalizedString", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "GetAllStrings", - "Parameters": [ - { - "Name": "includeParentCultures", - "Type": "System.Boolean" - } - ], - "ReturnType": "System.Collections.Generic.IEnumerable", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "WithCulture", - "Parameters": [ - { - "Name": "culture", - "Type": "System.Globalization.CultureInfo" - } - ], - "ReturnType": "Microsoft.Extensions.Localization.IStringLocalizer", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.Extensions.Localization.IStringLocalizerFactory", - "Visibility": "Public", - "Kind": "Interface", - "Abstract": true, - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "Create", - "Parameters": [ - { - "Name": "resourceSource", - "Type": "System.Type" - } - ], - "ReturnType": "Microsoft.Extensions.Localization.IStringLocalizer", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Create", - "Parameters": [ - { - "Name": "baseName", - "Type": "System.String" - }, - { - "Name": "location", - "Type": "System.String" - } - ], - "ReturnType": "Microsoft.Extensions.Localization.IStringLocalizer", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.Extensions.Localization.IStringLocalizer", - "Visibility": "Public", - "Kind": "Interface", - "Abstract": true, - "ImplementedInterfaces": [ - "Microsoft.Extensions.Localization.IStringLocalizer" - ], - "Members": [], - "GenericParameters": [ - { - "ParameterName": "T", - "ParameterPosition": 0, - "BaseTypeOrInterfaces": [] - } - ] - }, - { - "Name": "Microsoft.Extensions.Localization.LocalizedString", - "Visibility": "Public", - "Kind": "Class", - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "op_Implicit", - "Parameters": [ - { - "Name": "localizedString", - "Type": "Microsoft.Extensions.Localization.LocalizedString" - } - ], - "ReturnType": "System.String", - "Static": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "get_Name", - "Parameters": [], - "ReturnType": "System.String", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "get_Value", - "Parameters": [], - "ReturnType": "System.String", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "get_ResourceNotFound", - "Parameters": [], - "ReturnType": "System.Boolean", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "get_SearchedLocation", - "Parameters": [], - "ReturnType": "System.String", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "ToString", - "Parameters": [], - "ReturnType": "System.String", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [ - { - "Name": "name", - "Type": "System.String" - }, - { - "Name": "value", - "Type": "System.String" - } - ], - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [ - { - "Name": "name", - "Type": "System.String" - }, - { - "Name": "value", - "Type": "System.String" - }, - { - "Name": "resourceNotFound", - "Type": "System.Boolean" - } - ], - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [ - { - "Name": "name", - "Type": "System.String" - }, - { - "Name": "value", - "Type": "System.String" - }, - { - "Name": "resourceNotFound", - "Type": "System.Boolean" - }, - { - "Name": "searchedLocation", - "Type": "System.String" - } - ], - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.Extensions.Localization.StringLocalizerExtensions", - "Visibility": "Public", - "Kind": "Class", - "Abstract": true, - "Static": true, - "Sealed": true, - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "GetString", - "Parameters": [ - { - "Name": "stringLocalizer", - "Type": "Microsoft.Extensions.Localization.IStringLocalizer" - }, - { - "Name": "name", - "Type": "System.String" - } - ], - "ReturnType": "Microsoft.Extensions.Localization.LocalizedString", - "Static": true, - "Extension": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "GetString", - "Parameters": [ - { - "Name": "stringLocalizer", - "Type": "Microsoft.Extensions.Localization.IStringLocalizer" - }, - { - "Name": "name", - "Type": "System.String" - }, - { - "Name": "arguments", - "Type": "System.Object[]", - "IsParams": true - } - ], - "ReturnType": "Microsoft.Extensions.Localization.LocalizedString", - "Static": true, - "Extension": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "GetAllStrings", - "Parameters": [ - { - "Name": "stringLocalizer", - "Type": "Microsoft.Extensions.Localization.IStringLocalizer" - } - ], - "ReturnType": "System.Collections.Generic.IEnumerable", - "Static": true, - "Extension": true, - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.Extensions.Localization.StringLocalizer", - "Visibility": "Public", - "Kind": "Class", - "ImplementedInterfaces": [ - "Microsoft.Extensions.Localization.IStringLocalizer" - ], - "Members": [ - { - "Kind": "Method", - "Name": "get_Item", - "Parameters": [ - { - "Name": "name", - "Type": "System.String" - } - ], - "ReturnType": "Microsoft.Extensions.Localization.LocalizedString", - "Virtual": true, - "ImplementedInterface": "Microsoft.Extensions.Localization.IStringLocalizer", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "get_Item", - "Parameters": [ - { - "Name": "name", - "Type": "System.String" - }, - { - "Name": "arguments", - "Type": "System.Object[]", - "IsParams": true - } - ], - "ReturnType": "Microsoft.Extensions.Localization.LocalizedString", - "Virtual": true, - "ImplementedInterface": "Microsoft.Extensions.Localization.IStringLocalizer", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "GetAllStrings", - "Parameters": [ - { - "Name": "includeParentCultures", - "Type": "System.Boolean" - } - ], - "ReturnType": "System.Collections.Generic.IEnumerable", - "Sealed": true, - "Virtual": true, - "ImplementedInterface": "Microsoft.Extensions.Localization.IStringLocalizer", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "WithCulture", - "Parameters": [ - { - "Name": "culture", - "Type": "System.Globalization.CultureInfo" - } - ], - "ReturnType": "Microsoft.Extensions.Localization.IStringLocalizer", - "Virtual": true, - "ImplementedInterface": "Microsoft.Extensions.Localization.IStringLocalizer", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [ - { - "Name": "factory", - "Type": "Microsoft.Extensions.Localization.IStringLocalizerFactory" - } - ], - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [ - { - "ParameterName": "TResourceSource", - "ParameterPosition": 0, - "BaseTypeOrInterfaces": [] - } - ] - } - ] -} \ No newline at end of file diff --git a/src/Localization/Localization/ref/Microsoft.Extensions.Localization.csproj b/src/Localization/Localization/ref/Microsoft.Extensions.Localization.csproj new file mode 100644 index 0000000000..d628c33a54 --- /dev/null +++ b/src/Localization/Localization/ref/Microsoft.Extensions.Localization.csproj @@ -0,0 +1,13 @@ + + + + netstandard2.0 + + + + + + + + + diff --git a/src/Localization/Localization/ref/Microsoft.Extensions.Localization.netstandard2.0.cs b/src/Localization/Localization/ref/Microsoft.Extensions.Localization.netstandard2.0.cs new file mode 100644 index 0000000000..80175da718 --- /dev/null +++ b/src/Localization/Localization/ref/Microsoft.Extensions.Localization.netstandard2.0.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. + +namespace Microsoft.Extensions.DependencyInjection +{ + public static partial class LocalizationServiceCollectionExtensions + { + public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddLocalization(this Microsoft.Extensions.DependencyInjection.IServiceCollection services) { throw null; } + public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddLocalization(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action setupAction) { throw null; } + } +} +namespace Microsoft.Extensions.Localization +{ + public partial interface IResourceNamesCache + { + System.Collections.Generic.IList GetOrAdd(string name, System.Func> valueFactory); + } + public partial class LocalizationOptions + { + public LocalizationOptions() { } + public string ResourcesPath { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + } + [System.AttributeUsageAttribute(System.AttributeTargets.Assembly, AllowMultiple=false, Inherited=false)] + public partial class ResourceLocationAttribute : System.Attribute + { + public ResourceLocationAttribute(string resourceLocation) { } + public string ResourceLocation { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + } + public partial class ResourceManagerStringLocalizer : Microsoft.Extensions.Localization.IStringLocalizer + { + public ResourceManagerStringLocalizer(System.Resources.ResourceManager resourceManager, Microsoft.Extensions.Localization.Internal.AssemblyWrapper resourceAssemblyWrapper, string baseName, Microsoft.Extensions.Localization.IResourceNamesCache resourceNamesCache, Microsoft.Extensions.Logging.ILogger logger) { } + public ResourceManagerStringLocalizer(System.Resources.ResourceManager resourceManager, Microsoft.Extensions.Localization.Internal.IResourceStringProvider resourceStringProvider, string baseName, Microsoft.Extensions.Localization.IResourceNamesCache resourceNamesCache, Microsoft.Extensions.Logging.ILogger logger) { } + public ResourceManagerStringLocalizer(System.Resources.ResourceManager resourceManager, System.Reflection.Assembly resourceAssembly, string baseName, Microsoft.Extensions.Localization.IResourceNamesCache resourceNamesCache, Microsoft.Extensions.Logging.ILogger logger) { } + public virtual Microsoft.Extensions.Localization.LocalizedString this[string name] { get { throw null; } } + public virtual Microsoft.Extensions.Localization.LocalizedString this[string name, params object[] arguments] { get { throw null; } } + public virtual System.Collections.Generic.IEnumerable GetAllStrings(bool includeParentCultures) { throw null; } + protected System.Collections.Generic.IEnumerable GetAllStrings(bool includeParentCultures, System.Globalization.CultureInfo culture) { throw null; } + protected string GetStringSafely(string name, System.Globalization.CultureInfo culture) { throw null; } + [System.ObsoleteAttribute("This method is obsolete. Use `CurrentCulture` and `CurrentUICulture` instead.")] + public Microsoft.Extensions.Localization.IStringLocalizer WithCulture(System.Globalization.CultureInfo culture) { throw null; } + } + public partial class ResourceManagerStringLocalizerFactory : Microsoft.Extensions.Localization.IStringLocalizerFactory + { + public ResourceManagerStringLocalizerFactory(Microsoft.Extensions.Options.IOptions localizationOptions, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) { } + public Microsoft.Extensions.Localization.IStringLocalizer Create(string baseName, string location) { throw null; } + public Microsoft.Extensions.Localization.IStringLocalizer Create(System.Type resourceSource) { throw null; } + protected virtual Microsoft.Extensions.Localization.ResourceManagerStringLocalizer CreateResourceManagerStringLocalizer(System.Reflection.Assembly assembly, string baseName) { throw null; } + protected virtual Microsoft.Extensions.Localization.ResourceLocationAttribute GetResourceLocationAttribute(System.Reflection.Assembly assembly) { throw null; } + protected virtual string GetResourcePrefix(System.Reflection.TypeInfo typeInfo) { throw null; } + protected virtual string GetResourcePrefix(System.Reflection.TypeInfo typeInfo, string baseNamespace, string resourcesRelativePath) { throw null; } + protected virtual string GetResourcePrefix(string baseResourceName, string baseNamespace) { throw null; } + protected virtual string GetResourcePrefix(string location, string baseName, string resourceLocation) { throw null; } + protected virtual Microsoft.Extensions.Localization.RootNamespaceAttribute GetRootNamespaceAttribute(System.Reflection.Assembly assembly) { throw null; } + } + [System.ObsoleteAttribute("This method is obsolete. Use `CurrentCulture` and `CurrentUICulture` instead.")] + public partial class ResourceManagerWithCultureStringLocalizer : Microsoft.Extensions.Localization.ResourceManagerStringLocalizer + { + public ResourceManagerWithCultureStringLocalizer(System.Resources.ResourceManager resourceManager, System.Reflection.Assembly resourceAssembly, string baseName, Microsoft.Extensions.Localization.IResourceNamesCache resourceNamesCache, System.Globalization.CultureInfo culture, Microsoft.Extensions.Logging.ILogger logger) : base (default(System.Resources.ResourceManager), default(System.Reflection.Assembly), default(string), default(Microsoft.Extensions.Localization.IResourceNamesCache), default(Microsoft.Extensions.Logging.ILogger)) { } + public override Microsoft.Extensions.Localization.LocalizedString this[string name] { get { throw null; } } + public override Microsoft.Extensions.Localization.LocalizedString this[string name, params object[] arguments] { get { throw null; } } + public override System.Collections.Generic.IEnumerable GetAllStrings(bool includeParentCultures) { throw null; } + } + public partial class ResourceNamesCache : Microsoft.Extensions.Localization.IResourceNamesCache + { + public ResourceNamesCache() { } + public System.Collections.Generic.IList GetOrAdd(string name, System.Func> valueFactory) { throw null; } + } + [System.AttributeUsageAttribute(System.AttributeTargets.Assembly, AllowMultiple=false, Inherited=false)] + public partial class RootNamespaceAttribute : System.Attribute + { + public RootNamespaceAttribute(string rootNamespace) { } + public string RootNamespace { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + } +} +namespace Microsoft.Extensions.Localization.Internal +{ + public partial class AssemblyWrapper + { + public AssemblyWrapper(System.Reflection.Assembly assembly) { } + public System.Reflection.Assembly Assembly { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public virtual string FullName { get { throw null; } } + public virtual System.IO.Stream GetManifestResourceStream(string name) { throw null; } + } + public partial interface IResourceStringProvider + { + System.Collections.Generic.IList GetAllResourceStrings(System.Globalization.CultureInfo culture, bool throwOnMissing); + } + public partial class ResourceManagerStringProvider : Microsoft.Extensions.Localization.Internal.IResourceStringProvider + { + public ResourceManagerStringProvider(Microsoft.Extensions.Localization.IResourceNamesCache resourceCache, System.Resources.ResourceManager resourceManager, System.Reflection.Assembly assembly, string baseName) { } + public System.Collections.Generic.IList GetAllResourceStrings(System.Globalization.CultureInfo culture, bool throwOnMissing) { throw null; } + } +} diff --git a/src/Localization/Localization/src/Internal/ResourceManagerStringLocalizerLoggerExtensions.cs b/src/Localization/Localization/src/Internal/ResourceManagerStringLocalizerLoggerExtensions.cs index 456e07009e..63f40536ca 100644 --- a/src/Localization/Localization/src/Internal/ResourceManagerStringLocalizerLoggerExtensions.cs +++ b/src/Localization/Localization/src/Internal/ResourceManagerStringLocalizerLoggerExtensions.cs @@ -15,7 +15,7 @@ namespace Microsoft.Extensions.Localization.Internal { _searchedLocation = LoggerMessage.Define( LogLevel.Debug, - 1, + new EventId(1, "SearchedLocation"), $"{nameof(ResourceManagerStringLocalizer)} searched for '{{Key}}' in '{{LocationSearched}}' with culture '{{Culture}}'."); } diff --git a/src/Localization/Localization/src/Microsoft.Extensions.Localization.csproj b/src/Localization/Localization/src/Microsoft.Extensions.Localization.csproj index 73365a15eb..f16cbd9dab 100644 --- a/src/Localization/Localization/src/Microsoft.Extensions.Localization.csproj +++ b/src/Localization/Localization/src/Microsoft.Extensions.Localization.csproj @@ -7,6 +7,7 @@ $(NoWarn);CS1591 true localization + true @@ -16,4 +17,8 @@ + + + + diff --git a/src/Localization/Localization/src/Properties/AssemblyInfo.cs b/src/Localization/Localization/src/Properties/AssemblyInfo.cs deleted file mode 100644 index 3e297b801e..0000000000 --- a/src/Localization/Localization/src/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,6 +0,0 @@ -// 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.Extensions.Localization.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Localization/Localization/src/ResourceManagerStringLocalizer.cs b/src/Localization/Localization/src/ResourceManagerStringLocalizer.cs index e2e1a3f234..90f8e077b4 100644 --- a/src/Localization/Localization/src/ResourceManagerStringLocalizer.cs +++ b/src/Localization/Localization/src/ResourceManagerStringLocalizer.cs @@ -1,5 +1,5 @@ -// 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. +// 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; @@ -151,6 +151,7 @@ namespace Microsoft.Extensions.Localization /// /// The to use. /// A culture-specific . + [Obsolete("This method is obsolete. Use `CurrentCulture` and `CurrentUICulture` instead.")] public IStringLocalizer WithCulture(CultureInfo culture) { return culture == null @@ -271,4 +272,4 @@ namespace Microsoft.Extensions.Localization return resourceNames; } } -} \ No newline at end of file +} diff --git a/src/Localization/Localization/src/ResourceManagerWithCultureStringLocalizer.cs b/src/Localization/Localization/src/ResourceManagerWithCultureStringLocalizer.cs index 65b6ae242c..2bc51289da 100644 --- a/src/Localization/Localization/src/ResourceManagerWithCultureStringLocalizer.cs +++ b/src/Localization/Localization/src/ResourceManagerWithCultureStringLocalizer.cs @@ -1,5 +1,5 @@ -// 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. +// 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; @@ -15,6 +15,7 @@ namespace Microsoft.Extensions.Localization /// An that uses the and /// to provide localized strings for a specific . /// + [Obsolete("This method is obsolete. Use `CurrentCulture` and `CurrentUICulture` instead.")] public class ResourceManagerWithCultureStringLocalizer : ResourceManagerStringLocalizer { private readonly string _resourceBaseName; @@ -161,4 +162,4 @@ namespace Microsoft.Extensions.Localization public override IEnumerable GetAllStrings(bool includeParentCultures) => GetAllStrings(includeParentCultures, _culture); } -} \ No newline at end of file +} diff --git a/src/Localization/Localization/src/baseline.netcore.json b/src/Localization/Localization/src/baseline.netcore.json deleted file mode 100644 index 860db76899..0000000000 --- a/src/Localization/Localization/src/baseline.netcore.json +++ /dev/null @@ -1,687 +0,0 @@ -{ - "AssemblyIdentity": "Microsoft.Extensions.Localization, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", - "Types": [ - { - "Name": "Microsoft.Extensions.DependencyInjection.LocalizationServiceCollectionExtensions", - "Visibility": "Public", - "Kind": "Class", - "Abstract": true, - "Static": true, - "Sealed": true, - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "AddLocalization", - "Parameters": [ - { - "Name": "services", - "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" - } - ], - "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection", - "Static": true, - "Extension": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "AddLocalization", - "Parameters": [ - { - "Name": "services", - "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" - }, - { - "Name": "setupAction", - "Type": "System.Action" - } - ], - "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection", - "Static": true, - "Extension": true, - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.Extensions.Localization.IResourceNamesCache", - "Visibility": "Public", - "Kind": "Interface", - "Abstract": true, - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "GetOrAdd", - "Parameters": [ - { - "Name": "name", - "Type": "System.String" - }, - { - "Name": "valueFactory", - "Type": "System.Func>" - } - ], - "ReturnType": "System.Collections.Generic.IList", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.Extensions.Localization.LocalizationOptions", - "Visibility": "Public", - "Kind": "Class", - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "get_ResourcesPath", - "Parameters": [], - "ReturnType": "System.String", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "set_ResourcesPath", - "Parameters": [ - { - "Name": "value", - "Type": "System.String" - } - ], - "ReturnType": "System.Void", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [], - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.Extensions.Localization.ResourceLocationAttribute", - "Visibility": "Public", - "Kind": "Class", - "BaseType": "System.Attribute", - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "get_ResourceLocation", - "Parameters": [], - "ReturnType": "System.String", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [ - { - "Name": "resourceLocation", - "Type": "System.String" - } - ], - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.Extensions.Localization.ResourceManagerStringLocalizer", - "Visibility": "Public", - "Kind": "Class", - "ImplementedInterfaces": [ - "Microsoft.Extensions.Localization.IStringLocalizer" - ], - "Members": [ - { - "Kind": "Method", - "Name": "get_Item", - "Parameters": [ - { - "Name": "name", - "Type": "System.String" - } - ], - "ReturnType": "Microsoft.Extensions.Localization.LocalizedString", - "Virtual": true, - "ImplementedInterface": "Microsoft.Extensions.Localization.IStringLocalizer", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "get_Item", - "Parameters": [ - { - "Name": "name", - "Type": "System.String" - }, - { - "Name": "arguments", - "Type": "System.Object[]", - "IsParams": true - } - ], - "ReturnType": "Microsoft.Extensions.Localization.LocalizedString", - "Virtual": true, - "ImplementedInterface": "Microsoft.Extensions.Localization.IStringLocalizer", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "WithCulture", - "Parameters": [ - { - "Name": "culture", - "Type": "System.Globalization.CultureInfo" - } - ], - "ReturnType": "Microsoft.Extensions.Localization.IStringLocalizer", - "Sealed": true, - "Virtual": true, - "ImplementedInterface": "Microsoft.Extensions.Localization.IStringLocalizer", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "GetAllStrings", - "Parameters": [ - { - "Name": "includeParentCultures", - "Type": "System.Boolean" - } - ], - "ReturnType": "System.Collections.Generic.IEnumerable", - "Virtual": true, - "ImplementedInterface": "Microsoft.Extensions.Localization.IStringLocalizer", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "GetAllStrings", - "Parameters": [ - { - "Name": "includeParentCultures", - "Type": "System.Boolean" - }, - { - "Name": "culture", - "Type": "System.Globalization.CultureInfo" - } - ], - "ReturnType": "System.Collections.Generic.IEnumerable", - "Visibility": "Protected", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "GetStringSafely", - "Parameters": [ - { - "Name": "name", - "Type": "System.String" - }, - { - "Name": "culture", - "Type": "System.Globalization.CultureInfo" - } - ], - "ReturnType": "System.String", - "Visibility": "Protected", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [ - { - "Name": "resourceManager", - "Type": "System.Resources.ResourceManager" - }, - { - "Name": "resourceAssembly", - "Type": "System.Reflection.Assembly" - }, - { - "Name": "baseName", - "Type": "System.String" - }, - { - "Name": "resourceNamesCache", - "Type": "Microsoft.Extensions.Localization.IResourceNamesCache" - }, - { - "Name": "logger", - "Type": "Microsoft.Extensions.Logging.ILogger" - } - ], - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [ - { - "Name": "resourceManager", - "Type": "System.Resources.ResourceManager" - }, - { - "Name": "resourceAssemblyWrapper", - "Type": "Microsoft.Extensions.Localization.Internal.AssemblyWrapper" - }, - { - "Name": "baseName", - "Type": "System.String" - }, - { - "Name": "resourceNamesCache", - "Type": "Microsoft.Extensions.Localization.IResourceNamesCache" - }, - { - "Name": "logger", - "Type": "Microsoft.Extensions.Logging.ILogger" - } - ], - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [ - { - "Name": "resourceManager", - "Type": "System.Resources.ResourceManager" - }, - { - "Name": "resourceStringProvider", - "Type": "Microsoft.Extensions.Localization.Internal.IResourceStringProvider" - }, - { - "Name": "baseName", - "Type": "System.String" - }, - { - "Name": "resourceNamesCache", - "Type": "Microsoft.Extensions.Localization.IResourceNamesCache" - }, - { - "Name": "logger", - "Type": "Microsoft.Extensions.Logging.ILogger" - } - ], - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.Extensions.Localization.ResourceManagerStringLocalizerFactory", - "Visibility": "Public", - "Kind": "Class", - "ImplementedInterfaces": [ - "Microsoft.Extensions.Localization.IStringLocalizerFactory" - ], - "Members": [ - { - "Kind": "Method", - "Name": "GetResourcePrefix", - "Parameters": [ - { - "Name": "typeInfo", - "Type": "System.Reflection.TypeInfo" - } - ], - "ReturnType": "System.String", - "Virtual": true, - "Visibility": "Protected", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "GetResourcePrefix", - "Parameters": [ - { - "Name": "typeInfo", - "Type": "System.Reflection.TypeInfo" - }, - { - "Name": "baseNamespace", - "Type": "System.String" - }, - { - "Name": "resourcesRelativePath", - "Type": "System.String" - } - ], - "ReturnType": "System.String", - "Virtual": true, - "Visibility": "Protected", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "GetResourcePrefix", - "Parameters": [ - { - "Name": "baseResourceName", - "Type": "System.String" - }, - { - "Name": "baseNamespace", - "Type": "System.String" - } - ], - "ReturnType": "System.String", - "Virtual": true, - "Visibility": "Protected", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Create", - "Parameters": [ - { - "Name": "resourceSource", - "Type": "System.Type" - } - ], - "ReturnType": "Microsoft.Extensions.Localization.IStringLocalizer", - "Sealed": true, - "Virtual": true, - "ImplementedInterface": "Microsoft.Extensions.Localization.IStringLocalizerFactory", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Create", - "Parameters": [ - { - "Name": "baseName", - "Type": "System.String" - }, - { - "Name": "location", - "Type": "System.String" - } - ], - "ReturnType": "Microsoft.Extensions.Localization.IStringLocalizer", - "Sealed": true, - "Virtual": true, - "ImplementedInterface": "Microsoft.Extensions.Localization.IStringLocalizerFactory", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "CreateResourceManagerStringLocalizer", - "Parameters": [ - { - "Name": "assembly", - "Type": "System.Reflection.Assembly" - }, - { - "Name": "baseName", - "Type": "System.String" - } - ], - "ReturnType": "Microsoft.Extensions.Localization.ResourceManagerStringLocalizer", - "Virtual": true, - "Visibility": "Protected", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "GetResourcePrefix", - "Parameters": [ - { - "Name": "location", - "Type": "System.String" - }, - { - "Name": "baseName", - "Type": "System.String" - }, - { - "Name": "resourceLocation", - "Type": "System.String" - } - ], - "ReturnType": "System.String", - "Virtual": true, - "Visibility": "Protected", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "GetResourceLocationAttribute", - "Parameters": [ - { - "Name": "assembly", - "Type": "System.Reflection.Assembly" - } - ], - "ReturnType": "Microsoft.Extensions.Localization.ResourceLocationAttribute", - "Virtual": true, - "Visibility": "Protected", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "GetRootNamespaceAttribute", - "Parameters": [ - { - "Name": "assembly", - "Type": "System.Reflection.Assembly" - } - ], - "ReturnType": "Microsoft.Extensions.Localization.RootNamespaceAttribute", - "Virtual": true, - "Visibility": "Protected", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [ - { - "Name": "localizationOptions", - "Type": "Microsoft.Extensions.Options.IOptions" - }, - { - "Name": "loggerFactory", - "Type": "Microsoft.Extensions.Logging.ILoggerFactory" - } - ], - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.Extensions.Localization.ResourceManagerWithCultureStringLocalizer", - "Visibility": "Public", - "Kind": "Class", - "BaseType": "Microsoft.Extensions.Localization.ResourceManagerStringLocalizer", - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "get_Item", - "Parameters": [ - { - "Name": "name", - "Type": "System.String" - } - ], - "ReturnType": "Microsoft.Extensions.Localization.LocalizedString", - "Virtual": true, - "Override": true, - "ImplementedInterface": "Microsoft.Extensions.Localization.IStringLocalizer", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "get_Item", - "Parameters": [ - { - "Name": "name", - "Type": "System.String" - }, - { - "Name": "arguments", - "Type": "System.Object[]", - "IsParams": true - } - ], - "ReturnType": "Microsoft.Extensions.Localization.LocalizedString", - "Virtual": true, - "Override": true, - "ImplementedInterface": "Microsoft.Extensions.Localization.IStringLocalizer", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "GetAllStrings", - "Parameters": [ - { - "Name": "includeParentCultures", - "Type": "System.Boolean" - } - ], - "ReturnType": "System.Collections.Generic.IEnumerable", - "Virtual": true, - "Override": true, - "ImplementedInterface": "Microsoft.Extensions.Localization.IStringLocalizer", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [ - { - "Name": "resourceManager", - "Type": "System.Resources.ResourceManager" - }, - { - "Name": "resourceAssembly", - "Type": "System.Reflection.Assembly" - }, - { - "Name": "baseName", - "Type": "System.String" - }, - { - "Name": "resourceNamesCache", - "Type": "Microsoft.Extensions.Localization.IResourceNamesCache" - }, - { - "Name": "culture", - "Type": "System.Globalization.CultureInfo" - }, - { - "Name": "logger", - "Type": "Microsoft.Extensions.Logging.ILogger" - } - ], - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.Extensions.Localization.ResourceNamesCache", - "Visibility": "Public", - "Kind": "Class", - "ImplementedInterfaces": [ - "Microsoft.Extensions.Localization.IResourceNamesCache" - ], - "Members": [ - { - "Kind": "Method", - "Name": "GetOrAdd", - "Parameters": [ - { - "Name": "name", - "Type": "System.String" - }, - { - "Name": "valueFactory", - "Type": "System.Func>" - } - ], - "ReturnType": "System.Collections.Generic.IList", - "Sealed": true, - "Virtual": true, - "ImplementedInterface": "Microsoft.Extensions.Localization.IResourceNamesCache", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [], - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.Extensions.Localization.RootNamespaceAttribute", - "Visibility": "Public", - "Kind": "Class", - "BaseType": "System.Attribute", - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "get_RootNamespace", - "Parameters": [], - "ReturnType": "System.String", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [ - { - "Name": "rootNamespace", - "Type": "System.String" - } - ], - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - } - ] -} \ No newline at end of file diff --git a/src/Localization/Localization/test/Microsoft.Extensions.Localization.Tests.csproj b/src/Localization/Localization/test/Microsoft.Extensions.Localization.Tests.csproj index bdd50d8dd8..32c6263ac3 100644 --- a/src/Localization/Localization/test/Microsoft.Extensions.Localization.Tests.csproj +++ b/src/Localization/Localization/test/Microsoft.Extensions.Localization.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp2.2;net461 + netcoreapp3.0;net472 diff --git a/src/Localization/Localization/test/ResourceManagerStringLocalizerTest.cs b/src/Localization/Localization/test/ResourceManagerStringLocalizerTest.cs index ff7bfa9933..a82ce9d1d3 100644 --- a/src/Localization/Localization/test/ResourceManagerStringLocalizerTest.cs +++ b/src/Localization/Localization/test/ResourceManagerStringLocalizerTest.cs @@ -182,12 +182,11 @@ namespace Microsoft.Extensions.Localization var resourceManager = new TestResourceManager(baseName, resourceAssembly); var logger = Logger; - var localizer = new ResourceManagerWithCultureStringLocalizer( + var localizer = new ResourceManagerStringLocalizer( resourceManager, resourceAssembly.Assembly, baseName, resourceNamesCache, - CultureInfo.CurrentCulture, logger); // Act & Assert @@ -291,7 +290,7 @@ namespace Microsoft.Extensions.Localization public override Stream GetManifestResourceStream(string name) { ManifestResourceStreamCallCount++; - + return HasResources ? MakeResourceStream() : null; } } diff --git a/src/Localization/Localization/test/StringLocalizerOfTTest.cs b/src/Localization/Localization/test/StringLocalizerOfTTest.cs new file mode 100644 index 0000000000..ce06e74d1c --- /dev/null +++ b/src/Localization/Localization/test/StringLocalizerOfTTest.cs @@ -0,0 +1,159 @@ +// 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.Globalization; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Localization +{ + public class StringLocalizerOfTTest + { + [Fact] + public void Constructor_ThrowsAnExceptionForNullFactory() + { + // Arrange, act and assert + var exception = Assert.Throws( + () => new StringLocalizer(factory: null)); + + Assert.Equal("factory", exception.ParamName); + } + + [Fact] + public void Constructor_ResolvesLocalizerFromFactory() + { + // Arrange + var factory = new Mock(); + + // Act + _ = new StringLocalizer(factory.Object); + + // Assert + factory.Verify(mock => mock.Create(typeof(object)), Times.Once()); + } + + [Fact] + public void WithCulture_InvokesWithCultureFromInnerLocalizer() + { + // Arrange + var factory = new Mock(); + var innerLocalizer = new Mock(); + factory.Setup(mock => mock.Create(typeof(object))) + .Returns(innerLocalizer.Object); + + var localizer = new StringLocalizer(factory.Object); + + // Act +#pragma warning disable CS0618 + localizer.WithCulture(CultureInfo.GetCultureInfo("fr-FR")); +#pragma warning restore CS0618 + + // Assert +#pragma warning disable CS0618 + innerLocalizer.Verify(mock => mock.WithCulture(CultureInfo.GetCultureInfo("fr-FR")), Times.Once()); +#pragma warning restore CS0618 + } + + [Fact] + public void Indexer_ThrowsAnExceptionForNullName() + { + // Arrange + var factory = new Mock(); + var innerLocalizer = new Mock(); + factory.Setup(mock => mock.Create(typeof(object))) + .Returns(innerLocalizer.Object); + + var localizer = new StringLocalizer(factory.Object); + + // Act and assert + var exception = Assert.Throws(() => localizer[name: null]); + + Assert.Equal("name", exception.ParamName); + } + + [Fact] + public void Indexer_InvokesIndexerFromInnerLocalizer() + { + // Arrange + var factory = new Mock(); + var innerLocalizer = new Mock(); + factory.Setup(mock => mock.Create(typeof(object))) + .Returns(innerLocalizer.Object); + + var localizer = new StringLocalizer(factory.Object); + + // Act + _ = localizer["Hello world"]; + + // Assert + innerLocalizer.Verify(mock => mock["Hello world"], Times.Once()); + } + + [Fact] + public void Indexer_ThrowsAnExceptionForNullName_WithArguments() + { + // Arrange + var factory = new Mock(); + var innerLocalizer = new Mock(); + factory.Setup(mock => mock.Create(typeof(object))) + .Returns(innerLocalizer.Object); + + var localizer = new StringLocalizer(factory.Object); + + // Act and assert + var exception = Assert.Throws(() => localizer[name: null]); + + Assert.Equal("name", exception.ParamName); + } + + [Fact] + public void Indexer_InvokesIndexerFromInnerLocalizer_WithArguments() + { + // Arrange + var factory = new Mock(); + var innerLocalizer = new Mock(); + factory.Setup(mock => mock.Create(typeof(object))) + .Returns(innerLocalizer.Object); + + var localizer = new StringLocalizer(factory.Object); + + // Act + _ = localizer["Welcome, {0}", "Bob"]; + + // Assert + innerLocalizer.Verify(mock => mock["Welcome, {0}", "Bob"], Times.Once()); + } + + [Fact] + public void GetAllStrings_InvokesGetAllStringsFromInnerLocalizer() + { + // Arrange + var factory = new Mock(); + var innerLocalizer = new Mock(); + factory.Setup(mock => mock.Create(typeof(object))) + .Returns(innerLocalizer.Object); + + var localizer = new StringLocalizer(factory.Object); + + // Act + localizer.GetAllStrings(includeParentCultures: true); + + // Assert + innerLocalizer.Verify(mock => mock.GetAllStrings(true), Times.Once()); + } + + [Fact] + public void StringLocalizer_CanBeCastToBaseType() + { + // Arrange and act + IStringLocalizer localizer = new StringLocalizer(Mock.Of()); + + // Assert + Assert.NotNull(localizer); + } + + private class BaseType { } + private class DerivedType : BaseType { } + } +} diff --git a/src/Localization/README.md b/src/Localization/README.md new file mode 100644 index 0000000000..dc6894b6e0 --- /dev/null +++ b/src/Localization/README.md @@ -0,0 +1,6 @@ +Localization +============ + +These projects provide abstractions for localizing resources in .NET applications. + +The ASP.NET Core implementation of localization can be found in https://github.com/aspnet/AspNetCore/tree/master/src/Middleware/Localization. diff --git a/src/ObjectPool/Directory.Build.props b/src/ObjectPool/Directory.Build.props deleted file mode 100644 index f25c1d90ce..0000000000 --- a/src/ObjectPool/Directory.Build.props +++ /dev/null @@ -1,7 +0,0 @@ - - - - - true - - diff --git a/src/ObjectPool/ref/Microsoft.Extensions.ObjectPool.csproj b/src/ObjectPool/ref/Microsoft.Extensions.ObjectPool.csproj new file mode 100644 index 0000000000..1fbb81a9ca --- /dev/null +++ b/src/ObjectPool/ref/Microsoft.Extensions.ObjectPool.csproj @@ -0,0 +1,10 @@ + + + + netstandard2.0 + + + + + + diff --git a/src/ObjectPool/ref/Microsoft.Extensions.ObjectPool.netstandard2.0.cs b/src/ObjectPool/ref/Microsoft.Extensions.ObjectPool.netstandard2.0.cs new file mode 100644 index 0000000000..083aaf14ef --- /dev/null +++ b/src/ObjectPool/ref/Microsoft.Extensions.ObjectPool.netstandard2.0.cs @@ -0,0 +1,76 @@ +// 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.Extensions.ObjectPool +{ + public partial class DefaultObjectPoolProvider : Microsoft.Extensions.ObjectPool.ObjectPoolProvider + { + public DefaultObjectPoolProvider() { } + public int MaximumRetained { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public override Microsoft.Extensions.ObjectPool.ObjectPool Create(Microsoft.Extensions.ObjectPool.IPooledObjectPolicy policy) { throw null; } + } + public partial class DefaultObjectPool : Microsoft.Extensions.ObjectPool.ObjectPool where T : class + { + public DefaultObjectPool(Microsoft.Extensions.ObjectPool.IPooledObjectPolicy policy) { } + public DefaultObjectPool(Microsoft.Extensions.ObjectPool.IPooledObjectPolicy policy, int maximumRetained) { } + public override T Get() { throw null; } + public override void Return(T obj) { } + } + public partial class DefaultPooledObjectPolicy : Microsoft.Extensions.ObjectPool.PooledObjectPolicy where T : class, new() + { + public DefaultPooledObjectPolicy() { } + public override T Create() { throw null; } + public override bool Return(T obj) { throw null; } + } + public partial interface IPooledObjectPolicy + { + T Create(); + bool Return(T obj); + } + public partial class LeakTrackingObjectPoolProvider : Microsoft.Extensions.ObjectPool.ObjectPoolProvider + { + public LeakTrackingObjectPoolProvider(Microsoft.Extensions.ObjectPool.ObjectPoolProvider inner) { } + public override Microsoft.Extensions.ObjectPool.ObjectPool Create(Microsoft.Extensions.ObjectPool.IPooledObjectPolicy policy) { throw null; } + } + public partial class LeakTrackingObjectPool : Microsoft.Extensions.ObjectPool.ObjectPool where T : class + { + public LeakTrackingObjectPool(Microsoft.Extensions.ObjectPool.ObjectPool inner) { } + public override T Get() { throw null; } + public override void Return(T obj) { } + } + public static partial class ObjectPool + { + public static Microsoft.Extensions.ObjectPool.ObjectPool Create(Microsoft.Extensions.ObjectPool.IPooledObjectPolicy policy = null) where T : class, new() { throw null; } + } + public abstract partial class ObjectPoolProvider + { + protected ObjectPoolProvider() { } + public Microsoft.Extensions.ObjectPool.ObjectPool Create() where T : class, new() { throw null; } + public abstract Microsoft.Extensions.ObjectPool.ObjectPool Create(Microsoft.Extensions.ObjectPool.IPooledObjectPolicy policy) where T : class; + } + public static partial class ObjectPoolProviderExtensions + { + public static Microsoft.Extensions.ObjectPool.ObjectPool CreateStringBuilderPool(this Microsoft.Extensions.ObjectPool.ObjectPoolProvider provider) { throw null; } + public static Microsoft.Extensions.ObjectPool.ObjectPool CreateStringBuilderPool(this Microsoft.Extensions.ObjectPool.ObjectPoolProvider provider, int initialCapacity, int maximumRetainedCapacity) { throw null; } + } + public abstract partial class ObjectPool where T : class + { + protected ObjectPool() { } + public abstract T Get(); + public abstract void Return(T obj); + } + public abstract partial class PooledObjectPolicy : Microsoft.Extensions.ObjectPool.IPooledObjectPolicy + { + protected PooledObjectPolicy() { } + public abstract T Create(); + public abstract bool Return(T obj); + } + public partial class StringBuilderPooledObjectPolicy : Microsoft.Extensions.ObjectPool.PooledObjectPolicy + { + public StringBuilderPooledObjectPolicy() { } + public int InitialCapacity { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public int MaximumRetainedCapacity { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public override System.Text.StringBuilder Create() { throw null; } + public override bool Return(System.Text.StringBuilder obj) { throw null; } + } +} diff --git a/src/ObjectPool/src/DefaultObjectPool.cs b/src/ObjectPool/src/DefaultObjectPool.cs index dcd7f1c715..070508a014 100644 --- a/src/ObjectPool/src/DefaultObjectPool.cs +++ b/src/ObjectPool/src/DefaultObjectPool.cs @@ -10,13 +10,13 @@ namespace Microsoft.Extensions.ObjectPool { public class DefaultObjectPool : ObjectPool where T : class { - private readonly ObjectWrapper[] _items; - private readonly IPooledObjectPolicy _policy; - private readonly bool _isDefaultPolicy; - private T _firstItem; + private protected readonly ObjectWrapper[] _items; + private protected readonly IPooledObjectPolicy _policy; + private protected readonly bool _isDefaultPolicy; + private protected T _firstItem; // This class was introduced in 2.1 to avoid the interface call where possible - private readonly PooledObjectPolicy _fastPolicy; + private protected readonly PooledObjectPolicy _fastPolicy; public DefaultObjectPool(IPooledObjectPolicy policy) : this(policy, Environment.ProcessorCount * 2) @@ -45,28 +45,22 @@ namespace Microsoft.Extensions.ObjectPool var item = _firstItem; if (item == null || Interlocked.CompareExchange(ref _firstItem, null, item) != item) { - item = GetViaScan(); + var items = _items; + for (var i = 0; i < items.Length; i++) + { + item = items[i].Element; + if (item != null && Interlocked.CompareExchange(ref items[i].Element, null, item) == item) + { + return item; + } + } + + item = Create(); } return item; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private T GetViaScan() - { - var items = _items; - for (var i = 0; i < items.Length; i++) - { - var item = items[i].Element; - if (item != null && Interlocked.CompareExchange(ref items[i].Element, null, item) == item) - { - return item; - } - } - - return Create(); - } - // Non-inline to improve its code quality as uncommon path [MethodImpl(MethodImplOptions.NoInlining)] private T Create() => _fastPolicy?.Create() ?? _policy.Create(); @@ -77,23 +71,17 @@ namespace Microsoft.Extensions.ObjectPool { if (_firstItem != null || Interlocked.CompareExchange(ref _firstItem, obj, null) != null) { - ReturnViaScan(obj); + var items = _items; + for (var i = 0; i < items.Length && Interlocked.CompareExchange(ref items[i].Element, obj, null) != null; ++i) + { + } } } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void ReturnViaScan(T obj) - { - var items = _items; - for (var i = 0; i < items.Length && Interlocked.CompareExchange(ref items[i].Element, obj, null) != null; ++i) - { - } - } - // PERF: the struct wrapper avoids array-covariance-checks from the runtime when assigning to elements of the array. [DebuggerDisplay("{Element}")] - private struct ObjectWrapper + private protected struct ObjectWrapper { public T Element; } diff --git a/src/ObjectPool/src/DefaultObjectPoolProvider.cs b/src/ObjectPool/src/DefaultObjectPoolProvider.cs index fb3c4bfa7e..2e7767ab35 100644 --- a/src/ObjectPool/src/DefaultObjectPoolProvider.cs +++ b/src/ObjectPool/src/DefaultObjectPoolProvider.cs @@ -11,6 +11,16 @@ namespace Microsoft.Extensions.ObjectPool public override ObjectPool Create(IPooledObjectPolicy policy) { + if (policy == null) + { + throw new ArgumentNullException(nameof(policy)); + } + + if (typeof(IDisposable).IsAssignableFrom(typeof(T))) + { + return new DisposableObjectPool(policy, MaximumRetained); + } + return new DefaultObjectPool(policy, MaximumRetained); } } diff --git a/src/ObjectPool/src/DisposableObjectPool.cs b/src/ObjectPool/src/DisposableObjectPool.cs new file mode 100644 index 0000000000..17ada443e5 --- /dev/null +++ b/src/ObjectPool/src/DisposableObjectPool.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; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace Microsoft.Extensions.ObjectPool +{ + internal sealed class DisposableObjectPool : DefaultObjectPool, IDisposable where T : class + { + private volatile bool _isDisposed; + + public DisposableObjectPool(IPooledObjectPolicy policy) + : base(policy) + { + } + + public DisposableObjectPool(IPooledObjectPolicy policy, int maximumRetained) + : base(policy, maximumRetained) + { + } + + public override T Get() + { + if (_isDisposed) + { + ThrowObjectDisposedException(); + } + + return base.Get(); + + void ThrowObjectDisposedException() + { + throw new ObjectDisposedException(GetType().Name); + } + } + + public override void Return(T obj) + { + // When the pool is disposed or the obj is not returned to the pool, dispose it + if (_isDisposed || !ReturnCore(obj)) + { + DisposeItem(obj); + } + } + + private bool ReturnCore(T obj) + { + bool returnedTooPool = false; + + if (_isDefaultPolicy || (_fastPolicy?.Return(obj) ?? _policy.Return(obj))) + { + if (_firstItem == null && Interlocked.CompareExchange(ref _firstItem, obj, null) == null) + { + returnedTooPool = true; + } + else + { + var items = _items; + for (var i = 0; i < items.Length && !(returnedTooPool = Interlocked.CompareExchange(ref items[i].Element, obj, null) == null); i++) + { + } + } + } + + return returnedTooPool; + } + + public void Dispose() + { + _isDisposed = true; + + DisposeItem(_firstItem); + _firstItem = null; + + ObjectWrapper[] items = _items; + for (var i = 0; i < items.Length; i++) + { + DisposeItem(items[i].Element); + items[i].Element = null; + } + } + + private void DisposeItem(T item) + { + if (item is IDisposable disposable) + { + disposable.Dispose(); + } + } + } +} diff --git a/src/ObjectPool/src/Microsoft.Extensions.ObjectPool.csproj b/src/ObjectPool/src/Microsoft.Extensions.ObjectPool.csproj index cb42c5615a..71e9abed79 100644 --- a/src/ObjectPool/src/Microsoft.Extensions.ObjectPool.csproj +++ b/src/ObjectPool/src/Microsoft.Extensions.ObjectPool.csproj @@ -6,6 +6,7 @@ $(NoWarn);CS1591 true pooling + true diff --git a/src/ObjectPool/src/ObjectPool.cs b/src/ObjectPool/src/ObjectPool.cs index 8cf52c9195..691beae60c 100644 --- a/src/ObjectPool/src/ObjectPool.cs +++ b/src/ObjectPool/src/ObjectPool.cs @@ -9,4 +9,13 @@ namespace Microsoft.Extensions.ObjectPool public abstract void Return(T obj); } + + public static class ObjectPool + { + public static ObjectPool Create(IPooledObjectPolicy policy = null) where T : class, new() + { + var provider = new DefaultObjectPoolProvider(); + return provider.Create(policy ?? new DefaultPooledObjectPolicy()); + } + } } diff --git a/src/ObjectPool/src/Properties/AssemblyInfo.cs b/src/ObjectPool/src/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..121f8990b1 --- /dev/null +++ b/src/ObjectPool/src/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.Extensions.ObjectPool.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/ObjectPool/src/baseline.netcore.json b/src/ObjectPool/src/baseline.netcore.json deleted file mode 100644 index 253c1f6b66..0000000000 --- a/src/ObjectPool/src/baseline.netcore.json +++ /dev/null @@ -1,612 +0,0 @@ -{ - "AssemblyIdentity": "Microsoft.Extensions.ObjectPool, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", - "Types": [ - { - "Name": "Microsoft.Extensions.ObjectPool.DefaultObjectPool", - "Visibility": "Public", - "Kind": "Class", - "BaseType": "Microsoft.Extensions.ObjectPool.ObjectPool", - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "Get", - "Parameters": [], - "ReturnType": "T0", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Return", - "Parameters": [ - { - "Name": "obj", - "Type": "T0" - } - ], - "ReturnType": "System.Void", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [ - { - "Name": "policy", - "Type": "Microsoft.Extensions.ObjectPool.IPooledObjectPolicy" - } - ], - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [ - { - "Name": "policy", - "Type": "Microsoft.Extensions.ObjectPool.IPooledObjectPolicy" - }, - { - "Name": "maximumRetained", - "Type": "System.Int32" - } - ], - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [ - { - "ParameterName": "T", - "ParameterPosition": 0, - "Class": true, - "BaseTypeOrInterfaces": [] - } - ] - }, - { - "Name": "Microsoft.Extensions.ObjectPool.DefaultObjectPoolProvider", - "Visibility": "Public", - "Kind": "Class", - "BaseType": "Microsoft.Extensions.ObjectPool.ObjectPoolProvider", - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "get_MaximumRetained", - "Parameters": [], - "ReturnType": "System.Int32", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "set_MaximumRetained", - "Parameters": [ - { - "Name": "value", - "Type": "System.Int32" - } - ], - "ReturnType": "System.Void", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Create", - "Parameters": [ - { - "Name": "policy", - "Type": "Microsoft.Extensions.ObjectPool.IPooledObjectPolicy" - } - ], - "ReturnType": "Microsoft.Extensions.ObjectPool.ObjectPool", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [ - { - "ParameterName": "T", - "ParameterPosition": 0, - "Class": true, - "BaseTypeOrInterfaces": [] - } - ] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [], - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.Extensions.ObjectPool.DefaultPooledObjectPolicy", - "Visibility": "Public", - "Kind": "Class", - "BaseType": "Microsoft.Extensions.ObjectPool.PooledObjectPolicy", - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "Create", - "Parameters": [], - "ReturnType": "T0", - "Virtual": true, - "Override": true, - "ImplementedInterface": "Microsoft.Extensions.ObjectPool.IPooledObjectPolicy", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Return", - "Parameters": [ - { - "Name": "obj", - "Type": "T0" - } - ], - "ReturnType": "System.Boolean", - "Virtual": true, - "Override": true, - "ImplementedInterface": "Microsoft.Extensions.ObjectPool.IPooledObjectPolicy", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [], - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [ - { - "ParameterName": "T", - "ParameterPosition": 0, - "New": true, - "Class": true, - "BaseTypeOrInterfaces": [] - } - ] - }, - { - "Name": "Microsoft.Extensions.ObjectPool.IPooledObjectPolicy", - "Visibility": "Public", - "Kind": "Interface", - "Abstract": true, - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "Create", - "Parameters": [], - "ReturnType": "T0", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Return", - "Parameters": [ - { - "Name": "obj", - "Type": "T0" - } - ], - "ReturnType": "System.Boolean", - "GenericParameter": [] - } - ], - "GenericParameters": [ - { - "ParameterName": "T", - "ParameterPosition": 0, - "BaseTypeOrInterfaces": [] - } - ] - }, - { - "Name": "Microsoft.Extensions.ObjectPool.LeakTrackingObjectPool", - "Visibility": "Public", - "Kind": "Class", - "BaseType": "Microsoft.Extensions.ObjectPool.ObjectPool", - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "Get", - "Parameters": [], - "ReturnType": "T0", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Return", - "Parameters": [ - { - "Name": "obj", - "Type": "T0" - } - ], - "ReturnType": "System.Void", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [ - { - "Name": "inner", - "Type": "Microsoft.Extensions.ObjectPool.ObjectPool" - } - ], - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [ - { - "ParameterName": "T", - "ParameterPosition": 0, - "Class": true, - "BaseTypeOrInterfaces": [] - } - ] - }, - { - "Name": "Microsoft.Extensions.ObjectPool.LeakTrackingObjectPoolProvider", - "Visibility": "Public", - "Kind": "Class", - "BaseType": "Microsoft.Extensions.ObjectPool.ObjectPoolProvider", - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "Create", - "Parameters": [ - { - "Name": "policy", - "Type": "Microsoft.Extensions.ObjectPool.IPooledObjectPolicy" - } - ], - "ReturnType": "Microsoft.Extensions.ObjectPool.ObjectPool", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [ - { - "ParameterName": "T", - "ParameterPosition": 0, - "Class": true, - "BaseTypeOrInterfaces": [] - } - ] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [ - { - "Name": "inner", - "Type": "Microsoft.Extensions.ObjectPool.ObjectPoolProvider" - } - ], - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.Extensions.ObjectPool.ObjectPool", - "Visibility": "Public", - "Kind": "Class", - "Abstract": true, - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "Get", - "Parameters": [], - "ReturnType": "T0", - "Virtual": true, - "Abstract": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Return", - "Parameters": [ - { - "Name": "obj", - "Type": "T0" - } - ], - "ReturnType": "System.Void", - "Virtual": true, - "Abstract": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [], - "Visibility": "Protected", - "GenericParameter": [] - } - ], - "GenericParameters": [ - { - "ParameterName": "T", - "ParameterPosition": 0, - "Class": true, - "BaseTypeOrInterfaces": [] - } - ] - }, - { - "Name": "Microsoft.Extensions.ObjectPool.ObjectPoolProvider", - "Visibility": "Public", - "Kind": "Class", - "Abstract": true, - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "Create", - "Parameters": [ - { - "Name": "policy", - "Type": "Microsoft.Extensions.ObjectPool.IPooledObjectPolicy" - } - ], - "ReturnType": "Microsoft.Extensions.ObjectPool.ObjectPool", - "Virtual": true, - "Abstract": true, - "Visibility": "Public", - "GenericParameter": [ - { - "ParameterName": "T", - "ParameterPosition": 0, - "Class": true, - "BaseTypeOrInterfaces": [] - } - ] - }, - { - "Kind": "Method", - "Name": "Create", - "Parameters": [], - "ReturnType": "Microsoft.Extensions.ObjectPool.ObjectPool", - "Visibility": "Public", - "GenericParameter": [ - { - "ParameterName": "T", - "ParameterPosition": 0, - "New": true, - "Class": true, - "BaseTypeOrInterfaces": [] - } - ] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [], - "Visibility": "Protected", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.Extensions.ObjectPool.ObjectPoolProviderExtensions", - "Visibility": "Public", - "Kind": "Class", - "Abstract": true, - "Static": true, - "Sealed": true, - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "CreateStringBuilderPool", - "Parameters": [ - { - "Name": "provider", - "Type": "Microsoft.Extensions.ObjectPool.ObjectPoolProvider" - } - ], - "ReturnType": "Microsoft.Extensions.ObjectPool.ObjectPool", - "Static": true, - "Extension": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "CreateStringBuilderPool", - "Parameters": [ - { - "Name": "provider", - "Type": "Microsoft.Extensions.ObjectPool.ObjectPoolProvider" - }, - { - "Name": "initialCapacity", - "Type": "System.Int32" - }, - { - "Name": "maximumRetainedCapacity", - "Type": "System.Int32" - } - ], - "ReturnType": "Microsoft.Extensions.ObjectPool.ObjectPool", - "Static": true, - "Extension": true, - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.Extensions.ObjectPool.PooledObjectPolicy", - "Visibility": "Public", - "Kind": "Class", - "Abstract": true, - "ImplementedInterfaces": [ - "Microsoft.Extensions.ObjectPool.IPooledObjectPolicy" - ], - "Members": [ - { - "Kind": "Method", - "Name": "Create", - "Parameters": [], - "ReturnType": "T0", - "Virtual": true, - "Abstract": true, - "ImplementedInterface": "Microsoft.Extensions.ObjectPool.IPooledObjectPolicy", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Return", - "Parameters": [ - { - "Name": "obj", - "Type": "T0" - } - ], - "ReturnType": "System.Boolean", - "Virtual": true, - "Abstract": true, - "ImplementedInterface": "Microsoft.Extensions.ObjectPool.IPooledObjectPolicy", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [], - "Visibility": "Protected", - "GenericParameter": [] - } - ], - "GenericParameters": [ - { - "ParameterName": "T", - "ParameterPosition": 0, - "BaseTypeOrInterfaces": [] - } - ] - }, - { - "Name": "Microsoft.Extensions.ObjectPool.StringBuilderPooledObjectPolicy", - "Visibility": "Public", - "Kind": "Class", - "BaseType": "Microsoft.Extensions.ObjectPool.PooledObjectPolicy", - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "Create", - "Parameters": [], - "ReturnType": "System.Text.StringBuilder", - "Virtual": true, - "Override": true, - "ImplementedInterface": "Microsoft.Extensions.ObjectPool.IPooledObjectPolicy", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Return", - "Parameters": [ - { - "Name": "obj", - "Type": "System.Text.StringBuilder" - } - ], - "ReturnType": "System.Boolean", - "Virtual": true, - "Override": true, - "ImplementedInterface": "Microsoft.Extensions.ObjectPool.IPooledObjectPolicy", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "get_InitialCapacity", - "Parameters": [], - "ReturnType": "System.Int32", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "set_InitialCapacity", - "Parameters": [ - { - "Name": "value", - "Type": "System.Int32" - } - ], - "ReturnType": "System.Void", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "get_MaximumRetainedCapacity", - "Parameters": [], - "ReturnType": "System.Int32", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "set_MaximumRetainedCapacity", - "Parameters": [ - { - "Name": "value", - "Type": "System.Int32" - } - ], - "ReturnType": "System.Void", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [], - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - } - ] -} \ No newline at end of file diff --git a/src/ObjectPool/test/DefaultObjectPoolProviderTest.cs b/src/ObjectPool/test/DefaultObjectPoolProviderTest.cs new file mode 100644 index 0000000000..7096b60b34 --- /dev/null +++ b/src/ObjectPool/test/DefaultObjectPoolProviderTest.cs @@ -0,0 +1,44 @@ +// 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 Xunit; + +namespace Microsoft.Extensions.ObjectPool +{ + public class DefaultObjectPoolProviderTest + { + [Fact] + public void DefaultObjectPoolProvider_CreateForObject_DefaultObjectPoolReturned() + { + // Arrange + var provider = new DefaultObjectPoolProvider(); + + // Act + var pool = provider.Create(); + + // Assert + Assert.IsType>(pool); + } + + [Fact] + public void DefaultObjectPoolProvider_CreateForIDisposable_DisposableObjectPoolReturned() + { + // Arrange + var provider = new DefaultObjectPoolProvider(); + + // Act + var pool = provider.Create(); + + // Assert + Assert.IsType>(pool); + } + + private class DisposableObject : IDisposable + { + public bool IsDisposed { get; private set; } + + public void Dispose() => IsDisposed = true; + } + } +} diff --git a/src/ObjectPool/test/DisposableObjectPoolTest.cs b/src/ObjectPool/test/DisposableObjectPoolTest.cs new file mode 100644 index 0000000000..3bcefbaf66 --- /dev/null +++ b/src/ObjectPool/test/DisposableObjectPoolTest.cs @@ -0,0 +1,145 @@ +// 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 Xunit; + +namespace Microsoft.Extensions.ObjectPool +{ + public class DisposableObjectPoolTest + { + [Fact] + public void DisposableObjectPoolWithDefaultPolicy_GetAnd_ReturnObject_SameInstance() + { + // Arrange + var pool = new DisposableObjectPool(new DefaultPooledObjectPolicy()); + + var obj1 = pool.Get(); + pool.Return(obj1); + + // Act + var obj2 = pool.Get(); + + // Assert + Assert.Same(obj1, obj2); + } + + [Fact] + public void DisposableObjectPool_GetAndReturnObject_SameInstance() + { + // Arrange + var pool = new DisposableObjectPool>(new ListPolicy()); + + var list1 = pool.Get(); + pool.Return(list1); + + // Act + var list2 = pool.Get(); + + // Assert + Assert.Same(list1, list2); + } + + [Fact] + public void DisposableObjectPool_Return_RejectedByPolicy() + { + // Arrange + var pool = new DisposableObjectPool>(new ListPolicy()); + var list1 = pool.Get(); + list1.Capacity = 20; + + // Act + pool.Return(list1); + var list2 = pool.Get(); + + // Assert + Assert.NotSame(list1, list2); + } + + [Fact] + public void DisposableObjectPoolWithOneElement_Dispose_ObjectDisposed() + { + // Arrange + var pool = new DisposableObjectPool(new DefaultPooledObjectPolicy()); + var obj = pool.Get(); + pool.Return(obj); + + // Act + pool.Dispose(); + + // Assert + Assert.True(obj.IsDisposed); + } + + [Fact] + public void DisposableObjectPoolWithTwoElements_Dispose_ObjectsDisposed() + { + // Arrange + var pool = new DisposableObjectPool(new DefaultPooledObjectPolicy()); + var obj1 = pool.Get(); + var obj2 = pool.Get(); + pool.Return(obj1); + pool.Return(obj2); + + // Act + pool.Dispose(); + + // Assert + Assert.True(obj1.IsDisposed); + Assert.True(obj2.IsDisposed); + } + + [Fact] + public void DisposableObjectPool_DisposeAndGet_ThrowsObjectDisposed() + { + // Arrange + var pool = new DisposableObjectPool(new DefaultPooledObjectPolicy()); + var obj1 = pool.Get(); + var obj2 = pool.Get(); + pool.Return(obj1); + pool.Return(obj2); + + // Act + pool.Dispose(); + + // Assert + Assert.Throws(() => pool.Get()); + } + + [Fact] + public void DisposableObjectPool_DisposeAndReturn_DisposesObject() + { + // Arrange + var pool = new DisposableObjectPool(new DefaultPooledObjectPolicy()); + var obj = pool.Get(); + + // Act + pool.Dispose(); + pool.Return(obj); + + // Assert + Assert.True(obj.IsDisposed); + } + + private class ListPolicy : IPooledObjectPolicy> + { + public List Create() + { + return new List(17); + } + + public bool Return(List obj) + { + return obj.Capacity == 17; + } + } + + private class DisposableObject : IDisposable + { + public bool IsDisposed { get; private set; } + + public void Dispose() => IsDisposed = true; + } + } +} diff --git a/src/ObjectPool/test/Microsoft.Extensions.ObjectPool.Tests.csproj b/src/ObjectPool/test/Microsoft.Extensions.ObjectPool.Tests.csproj index 72d2dc93fd..8c14917a65 100644 --- a/src/ObjectPool/test/Microsoft.Extensions.ObjectPool.Tests.csproj +++ b/src/ObjectPool/test/Microsoft.Extensions.ObjectPool.Tests.csproj @@ -1,7 +1,7 @@ - netcoreapp2.2;net461 + netcoreapp3.0;net472 diff --git a/src/ObjectPool/test/ThreadingTest.cs b/src/ObjectPool/test/ThreadingTest.cs index 541bc5ffd4..dbab7a5301 100644 --- a/src/ObjectPool/test/ThreadingTest.cs +++ b/src/ObjectPool/test/ThreadingTest.cs @@ -13,10 +13,22 @@ namespace Microsoft.Extensions.ObjectPool private bool _foundError; [Fact] - public void RunThreadingTest() + public void DefaultObjectPool_RunThreadingTest() + { + _pool = new DefaultObjectPool(new DefaultPooledObjectPolicy(), 10); + RunThreadingTest(); + } + + [Fact] + public void DisposableObjectPool_RunThreadingTest() + { + _pool = new DisposableObjectPool(new DefaultPooledObjectPolicy(), 10); + RunThreadingTest(); + } + + private void RunThreadingTest() { _cts = new CancellationTokenSource(); - _pool = new DefaultObjectPool(new DefaultPooledObjectPolicy(), 10); var threads = new Thread[8]; for (var i = 0; i < threads.Length; i++) @@ -66,7 +78,7 @@ namespace Microsoft.Extensions.ObjectPool _pool.Return(obj2); } } - + private class Item { public int i = 0; diff --git a/src/Shared/ActivatorUtilities/sharedsources.props b/src/Shared/ActivatorUtilities/sharedsources.props index b35fe34b10..f754677531 100644 --- a/src/Shared/ActivatorUtilities/sharedsources.props +++ b/src/Shared/ActivatorUtilities/sharedsources.props @@ -1,4 +1,8 @@ + + false + + true diff --git a/src/Shared/BenchmarkRunner/DefaultCoreConfig.cs b/src/Shared/BenchmarkRunner/DefaultCoreConfig.cs index 5e2bafd506..a61833ab26 100644 --- a/src/Shared/BenchmarkRunner/DefaultCoreConfig.cs +++ b/src/Shared/BenchmarkRunner/DefaultCoreConfig.cs @@ -31,7 +31,7 @@ namespace BenchmarkDotNet.Attributes #if NETCOREAPP2_1 .With(CsProjCoreToolchain.From(NetCoreAppSettings.NetCoreApp21)) #else - .With(CsProjCoreToolchain.From(new NetCoreAppSettings("netcoreapp2.2", null, ".NET Core 2.2"))) + .With(CsProjCoreToolchain.From(new NetCoreAppSettings("netcoreapp3.0", null, ".NET Core 3.0"))) #endif .With(new GcMode { Server = true }) .With(RunStrategy.Throughput)); diff --git a/src/Shared/BenchmarkRunner/DefaultCoreValidationConfig.cs b/src/Shared/BenchmarkRunner/DefaultCoreValidationConfig.cs index 95fc725564..5a90929cff 100644 --- a/src/Shared/BenchmarkRunner/DefaultCoreValidationConfig.cs +++ b/src/Shared/BenchmarkRunner/DefaultCoreValidationConfig.cs @@ -1,11 +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; -using System.Linq; -using System.Reflection; -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Running; using BenchmarkDotNet.Configs; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Loggers; diff --git a/src/Shared/BenchmarkRunner/Directory.Build.props b/src/Shared/BenchmarkRunner/Directory.Build.props deleted file mode 100644 index d2f65e8d3d..0000000000 --- a/src/Shared/BenchmarkRunner/Directory.Build.props +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - Microsoft.AspNetCore.BenchmarkRunner.Sources - - diff --git a/src/Shared/CommandLineUtils/Microsoft.Extensions.CommandLineUtils.Sources.shproj b/src/Shared/CommandLineUtils/Microsoft.Extensions.CommandLineUtils.Sources.shproj deleted file mode 100644 index c728fe1012..0000000000 --- a/src/Shared/CommandLineUtils/Microsoft.Extensions.CommandLineUtils.Sources.shproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - 00947d4a-c20e-46e3-90c3-6cd6bc87ee72 - 14.0 - - - - - - - - diff --git a/src/Shared/HostFactoryResolver/HostFactoryResolver.cs b/src/Shared/HostFactoryResolver/HostFactoryResolver.cs new file mode 100644 index 0000000000..cb9f811237 --- /dev/null +++ b/src/Shared/HostFactoryResolver/HostFactoryResolver.cs @@ -0,0 +1,112 @@ +// 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; + +namespace Microsoft.Extensions.Hosting +{ + internal class HostFactoryResolver + { + public static readonly string BuildWebHost = nameof(BuildWebHost); + public static readonly string CreateWebHostBuilder = nameof(CreateWebHostBuilder); + public static readonly string CreateHostBuilder = nameof(CreateHostBuilder); + + public static Func ResolveWebHostFactory(Assembly assembly) + { + return ResolveFactory(assembly, BuildWebHost); + } + + public static Func ResolveWebHostBuilderFactory(Assembly assembly) + { + return ResolveFactory(assembly, CreateWebHostBuilder); + } + + public static Func ResolveHostBuilderFactory(Assembly assembly) + { + return ResolveFactory(assembly, CreateHostBuilder); + } + + private static Func ResolveFactory(Assembly assembly, string name) + { + var programType = assembly?.EntryPoint?.DeclaringType; + if (programType == null) + { + return null; + } + + var factory = programType.GetTypeInfo().GetDeclaredMethod(name); + if (!IsFactory(factory)) + { + return null; + } + + return args => (T)factory.Invoke(null, new object[] { args }); + } + + // TReturn Factory(string[] args); + private static bool IsFactory(MethodInfo factory) + { + return factory != null + && typeof(TReturn).IsAssignableFrom(factory.ReturnType) + && factory.GetParameters().Length == 1 + && typeof(string[]).Equals(factory.GetParameters()[0].ParameterType); + } + + // Used by EF tooling without any Hosting references. Looses some return type safety checks. + public static Func ResolveServiceProviderFactory(Assembly assembly) + { + // Prefer the older patterns by default for back compat. + var webHostFactory = ResolveWebHostFactory(assembly); + if (webHostFactory != null) + { + return args => + { + var webHost = webHostFactory(args); + return GetServiceProvider(webHost); + }; + } + + var webHostBuilderFactory = ResolveWebHostBuilderFactory(assembly); + if (webHostBuilderFactory != null) + { + return args => + { + var webHostBuilder = webHostBuilderFactory(args); + var webHost = Build(webHostBuilder); + return GetServiceProvider(webHost); + }; + } + + var hostBuilderFactory = ResolveHostBuilderFactory(assembly); + if (hostBuilderFactory != null) + { + return args => + { + var hostBuilder = hostBuilderFactory(args); + var host = Build(hostBuilder); + return GetServiceProvider(host); + }; + } + + return null; + } + + private static object Build(object builder) + { + var buildMethod = builder.GetType().GetMethod("Build"); + return buildMethod?.Invoke(builder, Array.Empty()); + } + + private static IServiceProvider GetServiceProvider(object host) + { + if (host == null) + { + return null; + } + var hostType = host.GetType(); + var servicesProperty = hostType.GetTypeInfo().GetDeclaredProperty("Services"); + return (IServiceProvider)servicesProperty.GetValue(host); + } + } +} diff --git a/src/Shared/ParameterDefaultValue/ParameterDefaultValue.cs b/src/Shared/ParameterDefaultValue/ParameterDefaultValue.cs index a71bad37b1..dc635bb789 100644 --- a/src/Shared/ParameterDefaultValue/ParameterDefaultValue.cs +++ b/src/Shared/ParameterDefaultValue/ParameterDefaultValue.cs @@ -8,6 +8,8 @@ namespace Microsoft.Extensions.Internal { internal class ParameterDefaultValue { + private static readonly Type _nullable = typeof(Nullable<>); + public static bool TryGetDefaultValue(ParameterInfo parameter, out object defaultValue) { bool hasDefaultValue; @@ -39,6 +41,19 @@ namespace Microsoft.Extensions.Internal { defaultValue = Activator.CreateInstance(parameter.ParameterType); } + + // Handle nullable enums + if (defaultValue != null && + parameter.ParameterType.IsGenericType && + parameter.ParameterType.GetGenericTypeDefinition() == _nullable + ) + { + var underlyingType = Nullable.GetUnderlyingType(parameter.ParameterType); + if (underlyingType != null && underlyingType.IsEnum) + { + defaultValue = Enum.ToObject(underlyingType, defaultValue); + } + } } return hasDefaultValue; diff --git a/src/Shared/TypeNameHelper/TypeNameHelper.cs b/src/Shared/TypeNameHelper/TypeNameHelper.cs index 1cc7468646..3994a074b6 100644 --- a/src/Shared/TypeNameHelper/TypeNameHelper.cs +++ b/src/Shared/TypeNameHelper/TypeNameHelper.cs @@ -9,6 +9,8 @@ namespace Microsoft.Extensions.Internal { internal class TypeNameHelper { + private const char DefaultNestedTypeDelimiter = '+'; + private static readonly Dictionary _builtInTypeNames = new Dictionary { { typeof(void), "void" }, @@ -40,15 +42,17 @@ namespace Microsoft.Extensions.Internal /// The . /// true to print a fully qualified name. /// true to include generic parameter names. + /// true to include generic parameters. + /// Character to use as a delimiter in nested type names /// The pretty printed type name. - public static string GetTypeDisplayName(Type type, bool fullName = true, bool includeGenericParameterNames = false) + public static string GetTypeDisplayName(Type type, bool fullName = true, bool includeGenericParameterNames = false, bool includeGenericParameters = true, char nestedTypeDelimiter = DefaultNestedTypeDelimiter) { var builder = new StringBuilder(); - ProcessType(builder, type, new DisplayNameOptions(fullName, includeGenericParameterNames)); + ProcessType(builder, type, new DisplayNameOptions(fullName, includeGenericParameterNames, includeGenericParameters, nestedTypeDelimiter)); return builder.ToString(); } - private static void ProcessType(StringBuilder builder, Type type, DisplayNameOptions options) + private static void ProcessType(StringBuilder builder, Type type, in DisplayNameOptions options) { if (type.IsGenericType) { @@ -72,11 +76,17 @@ namespace Microsoft.Extensions.Internal } else { - builder.Append(options.FullName ? type.FullName : type.Name); + var name = options.FullName ? type.FullName : type.Name; + builder.Append(name); + + if (options.NestedTypeDelimiter != DefaultNestedTypeDelimiter) + { + builder.Replace(DefaultNestedTypeDelimiter, options.NestedTypeDelimiter, builder.Length - name.Length, name.Length); + } } } - private static void ProcessArrayType(StringBuilder builder, Type type, DisplayNameOptions options) + private static void ProcessArrayType(StringBuilder builder, Type type, in DisplayNameOptions options) { var innerType = type; while (innerType.IsArray) @@ -95,7 +105,7 @@ namespace Microsoft.Extensions.Internal } } - private static void ProcessGenericType(StringBuilder builder, Type type, Type[] genericArguments, int length, DisplayNameOptions options) + private static void ProcessGenericType(StringBuilder builder, Type type, Type[] genericArguments, int length, in DisplayNameOptions options) { var offset = 0; if (type.IsNested) @@ -108,7 +118,7 @@ namespace Microsoft.Extensions.Internal if (type.IsNested) { ProcessGenericType(builder, type.DeclaringType, genericArguments, offset, options); - builder.Append('+'); + builder.Append(options.NestedTypeDelimiter); } else if (!string.IsNullOrEmpty(type.Namespace)) { @@ -126,35 +136,44 @@ namespace Microsoft.Extensions.Internal builder.Append(type.Name, 0, genericPartIndex); - builder.Append('<'); - for (var i = offset; i < length; i++) + if (options.IncludeGenericParameters) { - ProcessType(builder, genericArguments[i], options); - if (i + 1 == length) + builder.Append('<'); + for (var i = offset; i < length; i++) { - continue; - } + ProcessType(builder, genericArguments[i], options); + if (i + 1 == length) + { + continue; + } - builder.Append(','); - if (options.IncludeGenericParameterNames || !genericArguments[i + 1].IsGenericParameter) - { - builder.Append(' '); + builder.Append(','); + if (options.IncludeGenericParameterNames || !genericArguments[i + 1].IsGenericParameter) + { + builder.Append(' '); + } } + builder.Append('>'); } - builder.Append('>'); } - private struct DisplayNameOptions + private readonly struct DisplayNameOptions { - public DisplayNameOptions(bool fullName, bool includeGenericParameterNames) + public DisplayNameOptions(bool fullName, bool includeGenericParameterNames, bool includeGenericParameters, char nestedTypeDelimiter) { FullName = fullName; + IncludeGenericParameters = includeGenericParameters; IncludeGenericParameterNames = includeGenericParameterNames; + NestedTypeDelimiter = nestedTypeDelimiter; } public bool FullName { get; } + public bool IncludeGenericParameters { get; } + public bool IncludeGenericParameterNames { get; } + + public char NestedTypeDelimiter { get; } } } } diff --git a/src/Shared/test/Shared.Tests/DotNetMuxerTests.cs b/src/Shared/test/Shared.Tests/DotNetMuxerTests.cs index 92e06a8f70..2f412e292e 100644 --- a/src/Shared/test/Shared.Tests/DotNetMuxerTests.cs +++ b/src/Shared/test/Shared.Tests/DotNetMuxerTests.cs @@ -1,7 +1,7 @@ // 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_2 +#if NETCOREAPP3_0 using System.IO; using System.Runtime.InteropServices; using Xunit; diff --git a/src/Shared/test/Shared.Tests/HostFactoryResolverTests.cs b/src/Shared/test/Shared.Tests/HostFactoryResolverTests.cs new file mode 100644 index 0000000000..a26fb7b133 --- /dev/null +++ b/src/Shared/test/Shared.Tests/HostFactoryResolverTests.cs @@ -0,0 +1,116 @@ +// 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 MockHostTypes; +using System; +using Xunit; + +namespace Microsoft.Extensions.Hosting.Tests +{ + public class HostFactoryResolverTests + { + [Fact] + public void BuildWebHostPattern_CanFindWebHost() + { + var factory = HostFactoryResolver.ResolveWebHostFactory(typeof(BuildWebHostPatternTestSite.Program).Assembly); + + Assert.NotNull(factory); + Assert.IsAssignableFrom(factory(Array.Empty())); + } + + [Fact] + public void BuildWebHostPattern_CanFindServiceProvider() + { + var factory = HostFactoryResolver.ResolveServiceProviderFactory(typeof(BuildWebHostPatternTestSite.Program).Assembly); + + Assert.NotNull(factory); + Assert.IsAssignableFrom(factory(Array.Empty())); + } + + [Fact] + public void BuildWebHostPattern__Invalid_CantFindWebHost() + { + var factory = HostFactoryResolver.ResolveWebHostFactory(typeof(BuildWebHostInvalidSignature.Program).Assembly); + + Assert.Null(factory); + } + + [Fact] + public void BuildWebHostPattern__Invalid_CantFindServiceProvider() + { + var factory = HostFactoryResolver.ResolveServiceProviderFactory(typeof(BuildWebHostInvalidSignature.Program).Assembly); + + Assert.Null(factory); + } + + [Fact] + public void CreateWebHostBuilderPattern_CanFindWebHostBuilder() + { + var factory = HostFactoryResolver.ResolveWebHostBuilderFactory(typeof(CreateWebHostBuilderPatternTestSite.Program).Assembly); + + Assert.NotNull(factory); + Assert.IsAssignableFrom(factory(Array.Empty())); + } + + [Fact] + public void CreateWebHostBuilderPattern_CanFindServiceProvider() + { + var factory = HostFactoryResolver.ResolveServiceProviderFactory(typeof(CreateWebHostBuilderPatternTestSite.Program).Assembly); + + Assert.NotNull(factory); + Assert.IsAssignableFrom(factory(Array.Empty())); + } + + [Fact] + public void CreateWebHostBuilderPattern__Invalid_CantFindWebHostBuilder() + { + var factory = HostFactoryResolver.ResolveWebHostBuilderFactory(typeof(CreateWebHostBuilderInvalidSignature.Program).Assembly); + + Assert.Null(factory); + } + + [Fact] + public void CreateWebHostBuilderPattern__InvalidReturnType_CanFindServiceProvider() + { + var factory = HostFactoryResolver.ResolveServiceProviderFactory(typeof(CreateWebHostBuilderInvalidSignature.Program).Assembly); + + Assert.NotNull(factory); + Assert.Null(factory(Array.Empty())); + + } + + [Fact] + public void CreateHostBuilderPattern_CanFindHostBuilder() + { + var factory = HostFactoryResolver.ResolveHostBuilderFactory(typeof(CreateHostBuilderPatternTestSite.Program).Assembly); + + Assert.NotNull(factory); + Assert.IsAssignableFrom(factory(Array.Empty())); + } + + [Fact] + public void CreateHostBuilderPattern_CanFindServiceProvider() + { + var factory = HostFactoryResolver.ResolveServiceProviderFactory(typeof(CreateHostBuilderPatternTestSite.Program).Assembly); + + Assert.NotNull(factory); + Assert.IsAssignableFrom(factory(Array.Empty())); + } + + [Fact] + public void CreateHostBuilderPattern__Invalid_CantFindHostBuilder() + { + var factory = HostFactoryResolver.ResolveHostBuilderFactory(typeof(CreateHostBuilderInvalidSignature.Program).Assembly); + + Assert.Null(factory); + } + + [Fact] + public void CreateHostBuilderPattern__Invalid_CantFindServiceProvider() + { + var factory = HostFactoryResolver.ResolveServiceProviderFactory(typeof(CreateHostBuilderInvalidSignature.Program).Assembly); + + Assert.Null(factory); + } + } +} 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 cb21bfa1a6..222c88bc67 100644 --- a/src/Shared/test/Shared.Tests/Microsoft.AspNetCore.Shared.Tests.csproj +++ b/src/Shared/test/Shared.Tests/Microsoft.AspNetCore.Shared.Tests.csproj @@ -1,7 +1,7 @@ - netcoreapp2.2;net461 + netcoreapp3.0;net472 true @@ -13,6 +13,16 @@ " /> + + + + + + + + + + diff --git a/src/Shared/test/Shared.Tests/SingleThreadedSynchronizationContext.cs b/src/Shared/test/Shared.Tests/SingleThreadedSynchronizationContext.cs new file mode 100644 index 0000000000..77312e0a05 --- /dev/null +++ b/src/Shared/test/Shared.Tests/SingleThreadedSynchronizationContext.cs @@ -0,0 +1,45 @@ +// 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.Threading; + +namespace Microsoft.Extensions.Internal +{ + internal class SingleThreadedSynchronizationContext : SynchronizationContext + { + private readonly BlockingCollection<(SendOrPostCallback Callback, object State)> _queue = new BlockingCollection<(SendOrPostCallback Callback, object State)>(); + + public override void Send(SendOrPostCallback d, object state) // Sync operations + { + throw new NotSupportedException($"{nameof(SingleThreadedSynchronizationContext)} does not support synchronous operations."); + } + + public override void Post(SendOrPostCallback d, object state) // Async operations + { + _queue.Add((d, state)); + } + + public static void Run(Action action) + { + var previous = Current; + var context = new SingleThreadedSynchronizationContext(); + SetSynchronizationContext(context); + try + { + action(); + + while (context._queue.TryTake(out var item)) + { + item.Callback(item.State); + } + } + finally + { + context._queue.CompleteAdding(); + SetSynchronizationContext(previous); + } + } + } +} diff --git a/src/Shared/test/Shared.Tests/TypeNameHelperTest.cs b/src/Shared/test/Shared.Tests/TypeNameHelperTest.cs index b7f4285bdc..bd29f647d1 100644 --- a/src/Shared/test/Shared.Tests/TypeNameHelperTest.cs +++ b/src/Shared/test/Shared.Tests/TypeNameHelperTest.cs @@ -216,6 +216,47 @@ namespace Microsoft.Extensions.Internal Assert.Equal(expected, actual); } + public static TheoryData FullTypeNameData + { + get + { + return new TheoryData + { + // Predefined Types + { typeof(int), "int" }, + { typeof(List), "System.Collections.Generic.List" }, + { typeof(Dictionary), "System.Collections.Generic.Dictionary" }, + { typeof(Dictionary>), "System.Collections.Generic.Dictionary" }, + { typeof(List>), "System.Collections.Generic.List" }, + + // Classes inside NonGeneric class + { typeof(A), "Microsoft.Extensions.Internal.TypeNameHelperTest.A" }, + { typeof(B), "Microsoft.Extensions.Internal.TypeNameHelperTest.B" }, + { typeof(C), "Microsoft.Extensions.Internal.TypeNameHelperTest.C" }, + { typeof(C>), "Microsoft.Extensions.Internal.TypeNameHelperTest.C" }, + { typeof(B>), "Microsoft.Extensions.Internal.TypeNameHelperTest.B" }, + + // Classes inside Generic class + { typeof(Outer.D), "Microsoft.Extensions.Internal.TypeNameHelperTest.Outer.D" }, + { typeof(Outer.E), "Microsoft.Extensions.Internal.TypeNameHelperTest.Outer.E" }, + { typeof(Outer.F), "Microsoft.Extensions.Internal.TypeNameHelperTest.Outer.F" }, + { typeof(Outer.F.E>),"Microsoft.Extensions.Internal.TypeNameHelperTest.Outer.F" }, + { typeof(Outer.E.E>), "Microsoft.Extensions.Internal.TypeNameHelperTest.Outer.E" } + }; + } + } + + [Theory] + [MemberData(nameof(FullTypeNameData))] + public void Can_PrettyPrint_FullTypeName_WithoutGenericParametersAndNestedTypeDelimiter(Type type, string expectedTypeName) + { + // Arrange & Act + var displayName = TypeNameHelper.GetTypeDisplayName(type, fullName: true, includeGenericParameters: false, nestedTypeDelimiter: '.'); + + // Assert + Assert.Equal(expectedTypeName, displayName); + } + private class A { } private class B { } diff --git a/src/Shared/test/testassets/BuildWebHostInvalidSignature/BuildWebHostInvalidSignature.csproj b/src/Shared/test/testassets/BuildWebHostInvalidSignature/BuildWebHostInvalidSignature.csproj new file mode 100644 index 0000000000..6368289f65 --- /dev/null +++ b/src/Shared/test/testassets/BuildWebHostInvalidSignature/BuildWebHostInvalidSignature.csproj @@ -0,0 +1,12 @@ + + + + netcoreapp3.0;net472 + Exe + + + + + + + diff --git a/src/Shared/test/testassets/BuildWebHostInvalidSignature/Program.cs b/src/Shared/test/testassets/BuildWebHostInvalidSignature/Program.cs new file mode 100644 index 0000000000..ba9e3dab6a --- /dev/null +++ b/src/Shared/test/testassets/BuildWebHostInvalidSignature/Program.cs @@ -0,0 +1,17 @@ +// 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 MockHostTypes; + +namespace BuildWebHostInvalidSignature +{ + public class Program + { + static void Main(string[] args) + { + } + + // Missing string[] args + public static IWebHost BuildWebHost() => null; + } +} diff --git a/src/Shared/test/testassets/BuildWebHostPatternTestSite/BuildWebHostPatternTestSite.csproj b/src/Shared/test/testassets/BuildWebHostPatternTestSite/BuildWebHostPatternTestSite.csproj new file mode 100644 index 0000000000..6368289f65 --- /dev/null +++ b/src/Shared/test/testassets/BuildWebHostPatternTestSite/BuildWebHostPatternTestSite.csproj @@ -0,0 +1,12 @@ + + + + netcoreapp3.0;net472 + Exe + + + + + + + diff --git a/src/Shared/test/testassets/BuildWebHostPatternTestSite/Program.cs b/src/Shared/test/testassets/BuildWebHostPatternTestSite/Program.cs new file mode 100644 index 0000000000..b1d0655e4d --- /dev/null +++ b/src/Shared/test/testassets/BuildWebHostPatternTestSite/Program.cs @@ -0,0 +1,16 @@ +// 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 MockHostTypes; + +namespace BuildWebHostPatternTestSite +{ + public class Program + { + static void Main(string[] args) + { + } + + public static IWebHost BuildWebHost(string[] args) => new WebHost(); + } +} diff --git a/src/Shared/test/testassets/CreateHostBuilderInvalidSignature/CreateHostBuilderInvalidSignature.csproj b/src/Shared/test/testassets/CreateHostBuilderInvalidSignature/CreateHostBuilderInvalidSignature.csproj new file mode 100644 index 0000000000..6368289f65 --- /dev/null +++ b/src/Shared/test/testassets/CreateHostBuilderInvalidSignature/CreateHostBuilderInvalidSignature.csproj @@ -0,0 +1,12 @@ + + + + netcoreapp3.0;net472 + Exe + + + + + + + diff --git a/src/Shared/test/testassets/CreateHostBuilderInvalidSignature/Program.cs b/src/Shared/test/testassets/CreateHostBuilderInvalidSignature/Program.cs new file mode 100644 index 0000000000..8451301a20 --- /dev/null +++ b/src/Shared/test/testassets/CreateHostBuilderInvalidSignature/Program.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 MockHostTypes; + +namespace CreateHostBuilderInvalidSignature +{ + public class Program + { + public static void Main(string[] args) + { + var webHost = CreateHostBuilder(null, args).Build(); + } + + // Extra parameter + private static IHostBuilder CreateHostBuilder(object extraParam, string[] args) => null; + } +} diff --git a/src/Shared/test/testassets/CreateHostBuilderPatternTestSite/CreateHostBuilderPatternTestSite.csproj b/src/Shared/test/testassets/CreateHostBuilderPatternTestSite/CreateHostBuilderPatternTestSite.csproj new file mode 100644 index 0000000000..6368289f65 --- /dev/null +++ b/src/Shared/test/testassets/CreateHostBuilderPatternTestSite/CreateHostBuilderPatternTestSite.csproj @@ -0,0 +1,12 @@ + + + + netcoreapp3.0;net472 + Exe + + + + + + + diff --git a/src/Shared/test/testassets/CreateHostBuilderPatternTestSite/Program.cs b/src/Shared/test/testassets/CreateHostBuilderPatternTestSite/Program.cs new file mode 100644 index 0000000000..70edf16097 --- /dev/null +++ b/src/Shared/test/testassets/CreateHostBuilderPatternTestSite/Program.cs @@ -0,0 +1,19 @@ +// 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 MockHostTypes; + +namespace CreateHostBuilderPatternTestSite +{ + public class Program + { + public static void Main(string[] args) + { + var webHost = CreateHostBuilder(args).Build(); + } + + // Do not change the signature of this method. It's used for tests. + private static HostBuilder CreateHostBuilder(string[] args) => + new HostBuilder(); + } +} diff --git a/src/Shared/test/testassets/CreateWebHostBuilderInvalidSignature/CreateWebHostBuilderInvalidSignature.csproj b/src/Shared/test/testassets/CreateWebHostBuilderInvalidSignature/CreateWebHostBuilderInvalidSignature.csproj new file mode 100644 index 0000000000..6368289f65 --- /dev/null +++ b/src/Shared/test/testassets/CreateWebHostBuilderInvalidSignature/CreateWebHostBuilderInvalidSignature.csproj @@ -0,0 +1,12 @@ + + + + netcoreapp3.0;net472 + Exe + + + + + + + diff --git a/src/Shared/test/testassets/CreateWebHostBuilderInvalidSignature/Program.cs b/src/Shared/test/testassets/CreateWebHostBuilderInvalidSignature/Program.cs new file mode 100644 index 0000000000..1533acbf57 --- /dev/null +++ b/src/Shared/test/testassets/CreateWebHostBuilderInvalidSignature/Program.cs @@ -0,0 +1,17 @@ +// 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 MockHostTypes; + +namespace CreateWebHostBuilderInvalidSignature +{ + public class Program + { + static void Main(string[] args) + { + } + + // Wrong return type + public static IWebHost CreateWebHostBuilder(string[] args) => new WebHost(); + } +} diff --git a/src/Shared/test/testassets/CreateWebHostBuilderPatternTestSite/CreateWebHostBuilderPatternTestSite.csproj b/src/Shared/test/testassets/CreateWebHostBuilderPatternTestSite/CreateWebHostBuilderPatternTestSite.csproj new file mode 100644 index 0000000000..6368289f65 --- /dev/null +++ b/src/Shared/test/testassets/CreateWebHostBuilderPatternTestSite/CreateWebHostBuilderPatternTestSite.csproj @@ -0,0 +1,12 @@ + + + + netcoreapp3.0;net472 + Exe + + + + + + + diff --git a/src/Shared/test/testassets/CreateWebHostBuilderPatternTestSite/Program.cs b/src/Shared/test/testassets/CreateWebHostBuilderPatternTestSite/Program.cs new file mode 100644 index 0000000000..caab3cb224 --- /dev/null +++ b/src/Shared/test/testassets/CreateWebHostBuilderPatternTestSite/Program.cs @@ -0,0 +1,19 @@ +// 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 MockHostTypes; + +namespace CreateWebHostBuilderPatternTestSite +{ + public class Program + { + public static void Main(string[] args) + { + var webHost = CreateWebHostBuilder(args).Build(); + } + + // Do not change the signature of this method. It's used for tests. + private static IWebHostBuilder CreateWebHostBuilder(string[] args) => + new WebHostBuilder(); + } +} diff --git a/src/Shared/test/testassets/MockHostTypes/Host.cs b/src/Shared/test/testassets/MockHostTypes/Host.cs new file mode 100644 index 0000000000..412ab63ef3 --- /dev/null +++ b/src/Shared/test/testassets/MockHostTypes/Host.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. + +using System; + +namespace MockHostTypes +{ + public class Host : IHost + { + public IServiceProvider Services { get; } = new ServiceProvider(); + } +} diff --git a/src/Shared/test/testassets/MockHostTypes/HostBuilder.cs b/src/Shared/test/testassets/MockHostTypes/HostBuilder.cs new file mode 100644 index 0000000000..eb62e9a4b1 --- /dev/null +++ b/src/Shared/test/testassets/MockHostTypes/HostBuilder.cs @@ -0,0 +1,10 @@ +// 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 MockHostTypes +{ + public class HostBuilder : IHostBuilder + { + public IHost Build() => new Host(); + } +} diff --git a/src/Shared/test/testassets/MockHostTypes/IHost.cs b/src/Shared/test/testassets/MockHostTypes/IHost.cs new file mode 100644 index 0000000000..27c6dbaf71 --- /dev/null +++ b/src/Shared/test/testassets/MockHostTypes/IHost.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. + +using System; + +namespace MockHostTypes +{ + public interface IHost + { + IServiceProvider Services { get; } + } +} diff --git a/src/Shared/test/testassets/MockHostTypes/IHostBuilder.cs b/src/Shared/test/testassets/MockHostTypes/IHostBuilder.cs new file mode 100644 index 0000000000..2053b52106 --- /dev/null +++ b/src/Shared/test/testassets/MockHostTypes/IHostBuilder.cs @@ -0,0 +1,10 @@ +// 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 MockHostTypes +{ + public interface IHostBuilder + { + IHost Build(); + } +} diff --git a/src/Shared/test/testassets/MockHostTypes/IWebHost.cs b/src/Shared/test/testassets/MockHostTypes/IWebHost.cs new file mode 100644 index 0000000000..f93bba440c --- /dev/null +++ b/src/Shared/test/testassets/MockHostTypes/IWebHost.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. + +using System; + +namespace MockHostTypes +{ + public interface IWebHost + { + IServiceProvider Services { get; } + } +} diff --git a/src/Shared/test/testassets/MockHostTypes/IWebHostBuilder.cs b/src/Shared/test/testassets/MockHostTypes/IWebHostBuilder.cs new file mode 100644 index 0000000000..1159ae103e --- /dev/null +++ b/src/Shared/test/testassets/MockHostTypes/IWebHostBuilder.cs @@ -0,0 +1,10 @@ +// 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 MockHostTypes +{ + public interface IWebHostBuilder + { + IWebHost Build(); + } +} diff --git a/src/Shared/test/testassets/MockHostTypes/MockHostTypes.csproj b/src/Shared/test/testassets/MockHostTypes/MockHostTypes.csproj new file mode 100644 index 0000000000..3272f8d93a --- /dev/null +++ b/src/Shared/test/testassets/MockHostTypes/MockHostTypes.csproj @@ -0,0 +1,7 @@ + + + + netcoreapp3.0;net472 + + + diff --git a/src/Shared/test/testassets/MockHostTypes/ServiceProvider.cs b/src/Shared/test/testassets/MockHostTypes/ServiceProvider.cs new file mode 100644 index 0000000000..7b550c9d32 --- /dev/null +++ b/src/Shared/test/testassets/MockHostTypes/ServiceProvider.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. + +using System; + +namespace MockHostTypes +{ + public class ServiceProvider : IServiceProvider + { + public object GetService(Type serviceType) => null; + } +} diff --git a/src/Shared/test/testassets/MockHostTypes/WebHost.cs b/src/Shared/test/testassets/MockHostTypes/WebHost.cs new file mode 100644 index 0000000000..77d3d58ca4 --- /dev/null +++ b/src/Shared/test/testassets/MockHostTypes/WebHost.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. + +using System; + +namespace MockHostTypes +{ + public class WebHost : IWebHost + { + public IServiceProvider Services { get; } = new ServiceProvider(); + } +} diff --git a/src/Shared/test/testassets/MockHostTypes/WebHostBuilder.cs b/src/Shared/test/testassets/MockHostTypes/WebHostBuilder.cs new file mode 100644 index 0000000000..216fb28d60 --- /dev/null +++ b/src/Shared/test/testassets/MockHostTypes/WebHostBuilder.cs @@ -0,0 +1,10 @@ +// 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 MockHostTypes +{ + public class WebHostBuilder : IWebHostBuilder + { + public IWebHost Build() => new WebHost(); + } +} diff --git a/src/Testing/src/FlakyOn.cs b/src/Testing/src/FlakyOn.cs new file mode 100644 index 0000000000..81d9299043 --- /dev/null +++ b/src/Testing/src/FlakyOn.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.AspNetCore.Testing +{ + public static class FlakyOn + { + public const string All = "All"; + + public static class Helix + { + public const string All = QueuePrefix + "All"; + + public const string Fedora28Amd64 = QueuePrefix + HelixQueues.Fedora28Amd64; + public const string Fedora27Amd64 = QueuePrefix + HelixQueues.Fedora27Amd64; + public const string Redhat7Amd64 = QueuePrefix + HelixQueues.Redhat7Amd64; + public const string Debian9Amd64 = QueuePrefix + HelixQueues.Debian9Amd64; + public const string Debian8Amd64 = QueuePrefix + HelixQueues.Debian8Amd64; + public const string Centos7Amd64 = QueuePrefix + HelixQueues.Centos7Amd64; + public const string Ubuntu1604Amd64 = QueuePrefix + HelixQueues.Ubuntu1604Amd64; + public const string Ubuntu1810Amd64 = QueuePrefix + HelixQueues.Ubuntu1810Amd64; + public const string macOS1012Amd64 = QueuePrefix + HelixQueues.macOS1012Amd64; + public const string Windows10Amd64 = QueuePrefix + HelixQueues.Windows10Amd64; + + private const string Prefix = "Helix:"; + private const string QueuePrefix = Prefix + "Queue:"; + } + + public static class AzP + { + public const string All = Prefix + "All"; + public const string Windows = OsPrefix + "Windows_NT"; + public const string macOS = OsPrefix + "Darwin"; + public const string Linux = OsPrefix + "Linux"; + + private const string Prefix = "AzP:"; + private const string OsPrefix = Prefix + "OS:"; + } + } +} diff --git a/src/Testing/src/HelixQueues.cs b/src/Testing/src/HelixQueues.cs new file mode 100644 index 0000000000..ef5e4d1f5a --- /dev/null +++ b/src/Testing/src/HelixQueues.cs @@ -0,0 +1,18 @@ +namespace Microsoft.AspNetCore.Testing +{ + public static class HelixQueues + { + public const string Fedora28Amd64 = "Fedora.28." + Amd64Suffix; + public const string Fedora27Amd64 = "Fedora.27." + Amd64Suffix; + public const string Redhat7Amd64 = "Redhat.7." + Amd64Suffix; + public const string Debian9Amd64 = "Debian.9." + Amd64Suffix; + public const string Debian8Amd64 = "Debian.8." + Amd64Suffix; + public const string Centos7Amd64 = "Centos.7." + Amd64Suffix; + public const string Ubuntu1604Amd64 = "Ubuntu.1604." + Amd64Suffix; + public const string Ubuntu1810Amd64 = "Ubuntu.1810." + Amd64Suffix; + public const string macOS1012Amd64 = "OSX.1012." + Amd64Suffix; + public const string Windows10Amd64 = "Windows.10.Amd64.ClientRS4.VS2017.Open"; // Doesn't have the default suffix! + + private const string Amd64Suffix = "Amd64.Open"; + } +} diff --git a/src/Testing/src/Microsoft.AspNetCore.Testing.csproj b/src/Testing/src/Microsoft.AspNetCore.Testing.csproj index 64e0b3c4e1..9da267c419 100644 --- a/src/Testing/src/Microsoft.AspNetCore.Testing.csproj +++ b/src/Testing/src/Microsoft.AspNetCore.Testing.csproj @@ -6,15 +6,30 @@ $(NoWarn);CS1591 true aspnetcore - false + + false true + false true + + + + + + diff --git a/src/Testing/src/Properties/AssemblyInfo.cs b/src/Testing/src/Properties/AssemblyInfo.cs deleted file mode 100644 index 0212e111ee..0000000000 --- a/src/Testing/src/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,6 +0,0 @@ -// 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.Testing.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Testing/src/TaskExtensions.cs b/src/Testing/src/TaskExtensions.cs index 83130aeae4..f99bf7361a 100644 --- a/src/Testing/src/TaskExtensions.cs +++ b/src/Testing/src/TaskExtensions.cs @@ -1,7 +1,8 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -12,10 +13,11 @@ namespace Microsoft.AspNetCore.Testing { public static async Task TimeoutAfter(this Task task, TimeSpan timeout, [CallerFilePath] string filePath = null, - [CallerLineNumber] int lineNumber = default(int)) + [CallerLineNumber] int lineNumber = default) { // Don't create a timer if the task is already completed - if (task.IsCompleted) + // or the debugger is attached + if (task.IsCompleted || Debugger.IsAttached) { return await task; } @@ -28,17 +30,17 @@ namespace Microsoft.AspNetCore.Testing } else { - throw new TimeoutException( - CreateMessage(timeout, filePath, lineNumber)); + throw new TimeoutException(CreateMessage(timeout, filePath, lineNumber)); } } public static async Task TimeoutAfter(this Task task, TimeSpan timeout, [CallerFilePath] string filePath = null, - [CallerLineNumber] int lineNumber = default(int)) + [CallerLineNumber] int lineNumber = default) { // Don't create a timer if the task is already completed - if (task.IsCompleted) + // or the debugger is attached + if (task.IsCompleted || Debugger.IsAttached) { await task; return; diff --git a/src/Testing/src/TestPathUtilities.cs b/src/Testing/src/TestPathUtilities.cs index ebd10897c3..f982471f39 100644 --- a/src/Testing/src/TestPathUtilities.cs +++ b/src/Testing/src/TestPathUtilities.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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; @@ -8,6 +8,11 @@ namespace Microsoft.AspNetCore.Testing { public class TestPathUtilities { + public static string GetRepoRootDirectory() + { + return GetSolutionRootDirectory("Extensions"); + } + public static string GetSolutionRootDirectory(string solution) { var applicationBasePath = AppContext.BaseDirectory; diff --git a/src/Testing/src/xunit/ConditionalFactAttribute.cs b/src/Testing/src/xunit/ConditionalFactAttribute.cs index 7448b48d8c..ce37df2e56 100644 --- a/src/Testing/src/xunit/ConditionalFactAttribute.cs +++ b/src/Testing/src/xunit/ConditionalFactAttribute.cs @@ -12,4 +12,4 @@ namespace Microsoft.AspNetCore.Testing.xunit public class ConditionalFactAttribute : FactAttribute { } -} \ No newline at end of file +} diff --git a/src/Testing/src/xunit/ConditionalFactDiscoverer.cs b/src/Testing/src/xunit/ConditionalFactDiscoverer.cs index 819373fa31..cf49b29e5a 100644 --- a/src/Testing/src/xunit/ConditionalFactDiscoverer.cs +++ b/src/Testing/src/xunit/ConditionalFactDiscoverer.cs @@ -20,8 +20,8 @@ namespace Microsoft.AspNetCore.Testing.xunit { var skipReason = testMethod.EvaluateSkipConditions(); return skipReason != null - ? new SkippedTestCase(skipReason, _diagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), testMethod) + ? new SkippedTestCase(skipReason, _diagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), TestMethodDisplayOptions.None, testMethod) : base.CreateTestCase(discoveryOptions, testMethod, factAttribute); } } -} \ No newline at end of file +} diff --git a/src/Testing/src/xunit/ConditionalTheoryAttribute.cs b/src/Testing/src/xunit/ConditionalTheoryAttribute.cs index 9249078cc5..fe45f2ffc6 100644 --- a/src/Testing/src/xunit/ConditionalTheoryAttribute.cs +++ b/src/Testing/src/xunit/ConditionalTheoryAttribute.cs @@ -12,4 +12,4 @@ namespace Microsoft.AspNetCore.Testing.xunit public class ConditionalTheoryAttribute : TheoryAttribute { } -} \ No newline at end of file +} diff --git a/src/Testing/src/xunit/ConditionalTheoryDiscoverer.cs b/src/Testing/src/xunit/ConditionalTheoryDiscoverer.cs index d24421f5cd..9e413cd580 100644 --- a/src/Testing/src/xunit/ConditionalTheoryDiscoverer.cs +++ b/src/Testing/src/xunit/ConditionalTheoryDiscoverer.cs @@ -14,11 +14,30 @@ namespace Microsoft.AspNetCore.Testing.xunit { } + private sealed class OptionsWithPreEnumerationEnabled : ITestFrameworkDiscoveryOptions + { + private const string PreEnumerateTheories = "xunit.discovery.PreEnumerateTheories"; + + private readonly ITestFrameworkDiscoveryOptions _original; + + public OptionsWithPreEnumerationEnabled(ITestFrameworkDiscoveryOptions original) + => _original = original; + + public TValue GetValue(string name) + => (name == PreEnumerateTheories) ? (TValue)(object)true : _original.GetValue(name); + + public void SetValue(string name, TValue value) + => _original.SetValue(name, value); + } + + public override IEnumerable Discover(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute) + => base.Discover(new OptionsWithPreEnumerationEnabled(discoveryOptions), testMethod, theoryAttribute); + protected override IEnumerable CreateTestCasesForTheory(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute) { var skipReason = testMethod.EvaluateSkipConditions(); return skipReason != null - ? new[] { new SkippedTestCase(skipReason, DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), testMethod) } + ? new[] { new SkippedTestCase(skipReason, DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), TestMethodDisplayOptions.None, testMethod) } : base.CreateTestCasesForTheory(discoveryOptions, testMethod, theoryAttribute); } @@ -44,4 +63,4 @@ namespace Microsoft.AspNetCore.Testing.xunit : base.CreateTestCasesForDataRow(discoveryOptions, testMethod, theoryAttribute, dataRow); } } -} \ No newline at end of file +} diff --git a/src/Testing/src/xunit/FlakyAttribute.cs b/src/Testing/src/xunit/FlakyAttribute.cs new file mode 100644 index 0000000000..f58026c7ca --- /dev/null +++ b/src/Testing/src/xunit/FlakyAttribute.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using Xunit.Sdk; + +namespace Microsoft.AspNetCore.Testing.xunit +{ + /// + /// Marks a test as "Flaky" so that the build will sequester it and ignore failures. + /// + /// + /// + /// This attribute works by applying xUnit.net "Traits" based on the criteria specified in the attribute + /// properties. Once these traits are applied, build scripts can include/exclude tests based on them. + /// + /// + /// All flakiness-related traits start with Flaky: and are grouped first by the process running the tests: Azure Pipelines (AzP) or Helix. + /// Then there is a segment specifying the "selector" which indicates where the test is flaky. Finally a segment specifying the value of that selector. + /// The value of these traits is always either "true" or the trait is not present. We encode the entire selector in the name of the trait because xUnit.net only + /// provides "==" and "!=" operators for traits, there is no way to check if a trait "contains" or "does not contain" a value. VSTest does support "contains" checks + /// but does not appear to support "does not contain" checks. Using this pattern means we can use simple "==" and "!=" checks to either only run flaky tests, or exclude + /// flaky tests. + /// + /// + /// + /// + /// [Fact] + /// [Flaky("...", HelixQueues.Fedora28Amd64, AzurePipelines.macOS)] + /// public void FlakyTest() + /// { + /// // Flakiness + /// } + /// + /// + /// + /// The above example generates the following facets: + /// + /// + /// + /// + /// Flaky:Helix:Queue:Fedora.28.Amd64.Open = true + /// + /// + /// Flaky:AzP:OS:Darwin = true + /// + /// + /// + /// + /// Given the above attribute, the Azure Pipelines macOS run can easily filter this test out by passing -notrait "Flaky:AzP:OS:all=true" -notrait "Flaky:AzP:OS:Darwin=true" + /// to xunit.console.exe. Similarly, it can run only flaky tests using -trait "Flaky:AzP:OS:all=true" -trait "Flaky:AzP:OS:Darwin=true" + /// + /// + [TraitDiscoverer("Microsoft.AspNetCore.Testing.xunit.FlakyTestDiscoverer", "Microsoft.AspNetCore.Testing")] + [AttributeUsage(AttributeTargets.Method)] + public sealed class FlakyAttribute : Attribute, ITraitAttribute + { + /// + /// Gets a URL to a GitHub issue tracking this flaky test. + /// + public string GitHubIssueUrl { get; } + + public IReadOnlyList Filters { get; } + + /// + /// Initializes a new instance of the class with the specified and a list of . If no + /// filters are provided, the test is considered flaky in all environments. + /// + /// + /// At least one filter is required. + /// + /// The URL to a GitHub issue tracking this flaky test. + /// The first filter that indicates where the test is flaky. Use a value from . + /// A list of additional filters that define where this test is flaky. Use values in . + public FlakyAttribute(string gitHubIssueUrl, string firstFilter, params string[] additionalFilters) + { + if(string.IsNullOrEmpty(gitHubIssueUrl)) + { + throw new ArgumentNullException(nameof(gitHubIssueUrl)); + } + + if(string.IsNullOrEmpty(firstFilter)) + { + throw new ArgumentNullException(nameof(firstFilter)); + } + + GitHubIssueUrl = gitHubIssueUrl; + var filters = new List(); + filters.Add(firstFilter); + filters.AddRange(additionalFilters); + Filters = filters; + } + } +} diff --git a/src/Testing/src/xunit/FlakyTestDiscoverer.cs b/src/Testing/src/xunit/FlakyTestDiscoverer.cs new file mode 100644 index 0000000000..344b9b2378 --- /dev/null +++ b/src/Testing/src/xunit/FlakyTestDiscoverer.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Microsoft.AspNetCore.Testing.xunit +{ + public class FlakyTestDiscoverer : ITraitDiscoverer + { + public IEnumerable> GetTraits(IAttributeInfo traitAttribute) + { + if (traitAttribute is ReflectionAttributeInfo attribute && attribute.Attribute is FlakyAttribute flakyAttribute) + { + return GetTraitsCore(flakyAttribute); + } + else + { + throw new InvalidOperationException("The 'Flaky' attribute is only supported via reflection."); + } + } + + private IEnumerable> GetTraitsCore(FlakyAttribute attribute) + { + if (attribute.Filters.Count > 0) + { + foreach (var filter in attribute.Filters) + { + yield return new KeyValuePair($"Flaky:{filter}", "true"); + } + } + else + { + yield return new KeyValuePair($"Flaky:All", "true"); + } + } + } +} diff --git a/src/Testing/src/xunit/SkippedTestCase.cs b/src/Testing/src/xunit/SkippedTestCase.cs index c2e15fa640..1c25c507b9 100644 --- a/src/Testing/src/xunit/SkippedTestCase.cs +++ b/src/Testing/src/xunit/SkippedTestCase.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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; @@ -16,8 +16,14 @@ namespace Microsoft.AspNetCore.Testing.xunit { } - public SkippedTestCase(string skipReason, IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, ITestMethod testMethod, object[] testMethodArguments = null) - : base(diagnosticMessageSink, defaultMethodDisplay, testMethod, testMethodArguments) + public SkippedTestCase( + string skipReason, + IMessageSink diagnosticMessageSink, + TestMethodDisplay defaultMethodDisplay, + TestMethodDisplayOptions defaultMethodDisplayOptions, + ITestMethod testMethod, + object[] testMethodArguments = null) + : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod, testMethodArguments) { _skipReason = skipReason; } @@ -37,4 +43,4 @@ namespace Microsoft.AspNetCore.Testing.xunit data.AddValue(nameof(_skipReason), _skipReason); } } -} \ No newline at end of file +} diff --git a/src/Testing/test/ConditionalFactTest.cs b/src/Testing/test/ConditionalFactTest.cs index a04eb1731d..9c5c6d037d 100644 --- a/src/Testing/test/ConditionalFactTest.cs +++ b/src/Testing/test/ConditionalFactTest.cs @@ -29,14 +29,14 @@ namespace Microsoft.AspNetCore.Testing Assert.True(false, "This test should always be skipped."); } -#if NETCOREAPP2_2 +#if NETCOREAPP3_0 [ConditionalFact] [FrameworkSkipCondition(RuntimeFrameworks.CLR)] public void ThisTestMustRunOnCoreCLR() { Asserter.TestRan = true; } -#elif NET461 || NET46 +#elif NET472 [ConditionalFact] [FrameworkSkipCondition(RuntimeFrameworks.CoreCLR)] public void ThisTestMustRunOnCLR() @@ -57,4 +57,4 @@ namespace Microsoft.AspNetCore.Testing } } } -} \ No newline at end of file +} diff --git a/src/Testing/test/ConditionalTheoryTest.cs b/src/Testing/test/ConditionalTheoryTest.cs index 1181f1365a..d824eb61b4 100644 --- a/src/Testing/test/ConditionalTheoryTest.cs +++ b/src/Testing/test/ConditionalTheoryTest.cs @@ -81,7 +81,7 @@ namespace Microsoft.AspNetCore.Testing Assert.True(true); } -#if NETCOREAPP2_2 +#if NETCOREAPP3_0 [ConditionalTheory] [FrameworkSkipCondition(RuntimeFrameworks.CLR)] [MemberData(nameof(GetInts))] @@ -89,7 +89,7 @@ namespace Microsoft.AspNetCore.Testing { Asserter.TestRan = true; } -#elif NET461 || NET46 +#elif NET472 [ConditionalTheory] [FrameworkSkipCondition(RuntimeFrameworks.CoreCLR)] [MemberData(nameof(GetInts))] diff --git a/src/Testing/test/FlakyAttributeTest.cs b/src/Testing/test/FlakyAttributeTest.cs new file mode 100644 index 0000000000..7837bd8711 --- /dev/null +++ b/src/Testing/test/FlakyAttributeTest.cs @@ -0,0 +1,97 @@ +using Microsoft.AspNetCore.Testing.xunit; +using System; +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.AspNetCore.Testing.Tests +{ + public class FlakyAttributeTest + { + [Fact] + [Flaky("http://example.com", FlakyOn.All)] + public void AlwaysFlakyInCI() + { + if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("HELIX")) || !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("AGENT_OS"))) + { + throw new Exception("Flaky!"); + } + } + + [Fact] + [Flaky("http://example.com", FlakyOn.Helix.All)] + public void FlakyInHelixOnly() + { + if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("HELIX"))) + { + throw new Exception("Flaky on Helix!"); + } + } + + [Fact] + [Flaky("http://example.com", FlakyOn.Helix.macOS1012Amd64, FlakyOn.Helix.Fedora28Amd64)] + public void FlakyInSpecificHelixQueue() + { + // Today we don't run Extensions tests on Helix, but this test should light up when we do. + var queueName = Environment.GetEnvironmentVariable("HELIX"); + if (!string.IsNullOrEmpty(queueName)) + { + var failingQueues = new HashSet(StringComparer.OrdinalIgnoreCase) { HelixQueues.macOS1012Amd64, HelixQueues.Fedora28Amd64 }; + if (failingQueues.Contains(queueName)) + { + throw new Exception($"Flaky on Helix Queue '{queueName}' !"); + } + } + } + + [Fact] + [Flaky("http://example.com", FlakyOn.AzP.All)] + public void FlakyInAzPOnly() + { + if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("AGENT_OS"))) + { + throw new Exception("Flaky on AzP!"); + } + } + + [Fact] + [Flaky("http://example.com", FlakyOn.AzP.Windows)] + public void FlakyInAzPWindowsOnly() + { + if (string.Equals(Environment.GetEnvironmentVariable("AGENT_OS"), "Windows_NT")) + { + throw new Exception("Flaky on AzP Windows!"); + } + } + + [Fact] + [Flaky("http://example.com", FlakyOn.AzP.macOS)] + public void FlakyInAzPmacOSOnly() + { + if (string.Equals(Environment.GetEnvironmentVariable("AGENT_OS"), "Darwin")) + { + throw new Exception("Flaky on AzP macOS!"); + } + } + + [Fact] + [Flaky("http://example.com", FlakyOn.AzP.Linux)] + public void FlakyInAzPLinuxOnly() + { + if (string.Equals(Environment.GetEnvironmentVariable("AGENT_OS"), "Linux")) + { + throw new Exception("Flaky on AzP Linux!"); + } + } + + [Fact] + [Flaky("http://example.com", FlakyOn.AzP.Linux, FlakyOn.AzP.macOS)] + public void FlakyInAzPNonWindowsOnly() + { + var agentOs = Environment.GetEnvironmentVariable("AGENT_OS"); + if (string.Equals(agentOs, "Linux") || string.Equals(agentOs, "Darwin")) + { + throw new Exception("Flaky on AzP non-Windows!"); + } + } + } +} diff --git a/src/Testing/test/HttpClientSlimTest.cs b/src/Testing/test/HttpClientSlimTest.cs index 42b19ece08..ede48243e5 100644 --- a/src/Testing/test/HttpClientSlimTest.cs +++ b/src/Testing/test/HttpClientSlimTest.cs @@ -4,7 +4,6 @@ using System; using System.Net; using System.Net.Http; -using System.Net.Sockets; using System.Text; using System.Threading.Tasks; using Xunit; @@ -13,7 +12,7 @@ namespace Microsoft.AspNetCore.Testing { public class HttpClientSlimTest { - private static byte[] _defaultResponse = Encoding.ASCII.GetBytes("test"); + private static readonly byte[] _defaultResponse = Encoding.ASCII.GetBytes("test"); [Fact] public async Task GetStringAsyncHttp() @@ -79,7 +78,7 @@ namespace Microsoft.AspNetCore.Testing // HttpListener doesn't support requesting port 0 (dynamic). // Requesting port 0 from Sockets and then passing that to HttpListener is racy. // Just keep trying until we find a free one. - address = $"http://127.0.0.1:{random.Next(1024, ushort.MaxValue)}/"; + address = $"http://localhost:{random.Next(1024, ushort.MaxValue)}/"; listener.Prefixes.Add(address); listener.Start(); break; diff --git a/src/Testing/test/Microsoft.AspNetCore.Testing.Tests.csproj b/src/Testing/test/Microsoft.AspNetCore.Testing.Tests.csproj index 6d9a9a060c..acfb34b320 100644 --- a/src/Testing/test/Microsoft.AspNetCore.Testing.Tests.csproj +++ b/src/Testing/test/Microsoft.AspNetCore.Testing.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp2.2;net461 + netcoreapp3.0;net472 $(NoWarn);xUnit1004 @@ -24,5 +24,4 @@ - diff --git a/src/Testing/test/TestPathUtilitiesTest.cs b/src/Testing/test/TestPathUtilitiesTest.cs index 0c9a7c5ee4..c77194a548 100644 --- a/src/Testing/test/TestPathUtilitiesTest.cs +++ b/src/Testing/test/TestPathUtilitiesTest.cs @@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Testing // Testing\test\Microsoft.AspNetCore.Testing.Tests\bin\Debug\netcoreapp2.0 // Testing\test\Microsoft.AspNetCore.Testing.Tests\bin\Debug\net461 // Testing\test\Microsoft.AspNetCore.Testing.Tests\bin\Debug\net46 - var expectedPath = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "..", "..", "..")); + var expectedPath = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "..")); Assert.Equal(expectedPath, TestPathUtilities.GetSolutionRootDirectory("Extensions")); } diff --git a/src/WebEncoders/Directory.Build.props b/src/WebEncoders/Directory.Build.props index f25c1d90ce..81557e1bca 100644 --- a/src/WebEncoders/Directory.Build.props +++ b/src/WebEncoders/Directory.Build.props @@ -2,6 +2,7 @@ - true + true + diff --git a/src/WebEncoders/ref/Microsoft.Extensions.WebEncoders.csproj b/src/WebEncoders/ref/Microsoft.Extensions.WebEncoders.csproj new file mode 100644 index 0000000000..9dea660cb0 --- /dev/null +++ b/src/WebEncoders/ref/Microsoft.Extensions.WebEncoders.csproj @@ -0,0 +1,12 @@ + + + + netstandard2.0 + + + + + + + + diff --git a/src/WebEncoders/ref/Microsoft.Extensions.WebEncoders.netstandard2.0.cs b/src/WebEncoders/ref/Microsoft.Extensions.WebEncoders.netstandard2.0.cs new file mode 100644 index 0000000000..18cdcbdfa3 --- /dev/null +++ b/src/WebEncoders/ref/Microsoft.Extensions.WebEncoders.netstandard2.0.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. + +namespace Microsoft.Extensions.DependencyInjection +{ + public static partial class EncoderServiceCollectionExtensions + { + public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddWebEncoders(this Microsoft.Extensions.DependencyInjection.IServiceCollection services) { throw null; } + public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddWebEncoders(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action setupAction) { throw null; } + } +} +namespace Microsoft.Extensions.WebEncoders +{ + public sealed partial class WebEncoderOptions + { + public WebEncoderOptions() { } + public System.Text.Encodings.Web.TextEncoderSettings TextEncoderSettings { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + } +} +namespace Microsoft.Extensions.WebEncoders.Testing +{ + public sealed partial class HtmlTestEncoder : System.Text.Encodings.Web.HtmlEncoder + { + public HtmlTestEncoder() { } + public override int MaxOutputCharactersPerInputCharacter { get { throw null; } } + public override void Encode(System.IO.TextWriter output, char[] value, int startIndex, int characterCount) { } + public override void Encode(System.IO.TextWriter output, string value, int startIndex, int characterCount) { } + public override string Encode(string value) { throw null; } + public unsafe override int FindFirstCharacterToEncode(char* text, int textLength) { throw null; } + public unsafe override bool TryEncodeUnicodeScalar(int unicodeScalar, char* buffer, int bufferLength, out int numberOfCharactersWritten) { throw null; } + public override bool WillEncode(int unicodeScalar) { throw null; } + } + public partial class JavaScriptTestEncoder : System.Text.Encodings.Web.JavaScriptEncoder + { + public JavaScriptTestEncoder() { } + public override int MaxOutputCharactersPerInputCharacter { get { throw null; } } + public override void Encode(System.IO.TextWriter output, char[] value, int startIndex, int characterCount) { } + public override void Encode(System.IO.TextWriter output, string value, int startIndex, int characterCount) { } + public override string Encode(string value) { throw null; } + public unsafe override int FindFirstCharacterToEncode(char* text, int textLength) { throw null; } + public unsafe override bool TryEncodeUnicodeScalar(int unicodeScalar, char* buffer, int bufferLength, out int numberOfCharactersWritten) { throw null; } + public override bool WillEncode(int unicodeScalar) { throw null; } + } + public partial class UrlTestEncoder : System.Text.Encodings.Web.UrlEncoder + { + public UrlTestEncoder() { } + public override int MaxOutputCharactersPerInputCharacter { get { throw null; } } + public override void Encode(System.IO.TextWriter output, char[] value, int startIndex, int characterCount) { } + public override void Encode(System.IO.TextWriter output, string value, int startIndex, int characterCount) { } + public override string Encode(string value) { throw null; } + public unsafe override int FindFirstCharacterToEncode(char* text, int textLength) { throw null; } + public unsafe override bool TryEncodeUnicodeScalar(int unicodeScalar, char* buffer, int bufferLength, out int numberOfCharactersWritten) { throw null; } + public override bool WillEncode(int unicodeScalar) { throw null; } + } +} diff --git a/src/WebEncoders/src/Microsoft.Extensions.WebEncoders.csproj b/src/WebEncoders/src/Microsoft.Extensions.WebEncoders.csproj index 18f96d9412..8f60f8f983 100644 --- a/src/WebEncoders/src/Microsoft.Extensions.WebEncoders.csproj +++ b/src/WebEncoders/src/Microsoft.Extensions.WebEncoders.csproj @@ -4,9 +4,9 @@ Contains registration and configuration APIs to add the core framework encoders to a dependency injection container. netstandard2.0 $(NoWarn);CS1591 - true true aspnetcore + true diff --git a/src/WebEncoders/src/baseline.netcore.json b/src/WebEncoders/src/baseline.netcore.json deleted file mode 100644 index 6da0ae0754..0000000000 --- a/src/WebEncoders/src/baseline.netcore.json +++ /dev/null @@ -1,564 +0,0 @@ -{ - "AssemblyIdentity": "Microsoft.Extensions.WebEncoders, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", - "Types": [ - { - "Name": "Microsoft.Extensions.WebEncoders.WebEncoderOptions", - "Visibility": "Public", - "Kind": "Class", - "Sealed": true, - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "get_TextEncoderSettings", - "Parameters": [], - "ReturnType": "System.Text.Encodings.Web.TextEncoderSettings", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "set_TextEncoderSettings", - "Parameters": [ - { - "Name": "value", - "Type": "System.Text.Encodings.Web.TextEncoderSettings" - } - ], - "ReturnType": "System.Void", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [], - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.Extensions.WebEncoders.Testing.HtmlTestEncoder", - "Visibility": "Public", - "Kind": "Class", - "Sealed": true, - "BaseType": "System.Text.Encodings.Web.HtmlEncoder", - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "get_MaxOutputCharactersPerInputCharacter", - "Parameters": [], - "ReturnType": "System.Int32", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Encode", - "Parameters": [ - { - "Name": "value", - "Type": "System.String" - } - ], - "ReturnType": "System.String", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Encode", - "Parameters": [ - { - "Name": "output", - "Type": "System.IO.TextWriter" - }, - { - "Name": "value", - "Type": "System.Char[]" - }, - { - "Name": "startIndex", - "Type": "System.Int32" - }, - { - "Name": "characterCount", - "Type": "System.Int32" - } - ], - "ReturnType": "System.Void", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Encode", - "Parameters": [ - { - "Name": "output", - "Type": "System.IO.TextWriter" - }, - { - "Name": "value", - "Type": "System.String" - }, - { - "Name": "startIndex", - "Type": "System.Int32" - }, - { - "Name": "characterCount", - "Type": "System.Int32" - } - ], - "ReturnType": "System.Void", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "WillEncode", - "Parameters": [ - { - "Name": "unicodeScalar", - "Type": "System.Int32" - } - ], - "ReturnType": "System.Boolean", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "FindFirstCharacterToEncode", - "Parameters": [ - { - "Name": "text", - "Type": "System.Char*" - }, - { - "Name": "textLength", - "Type": "System.Int32" - } - ], - "ReturnType": "System.Int32", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "TryEncodeUnicodeScalar", - "Parameters": [ - { - "Name": "unicodeScalar", - "Type": "System.Int32" - }, - { - "Name": "buffer", - "Type": "System.Char*" - }, - { - "Name": "bufferLength", - "Type": "System.Int32" - }, - { - "Name": "numberOfCharactersWritten", - "Type": "System.Int32", - "Direction": "Out" - } - ], - "ReturnType": "System.Boolean", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [], - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.Extensions.WebEncoders.Testing.JavaScriptTestEncoder", - "Visibility": "Public", - "Kind": "Class", - "BaseType": "System.Text.Encodings.Web.JavaScriptEncoder", - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "get_MaxOutputCharactersPerInputCharacter", - "Parameters": [], - "ReturnType": "System.Int32", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Encode", - "Parameters": [ - { - "Name": "value", - "Type": "System.String" - } - ], - "ReturnType": "System.String", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Encode", - "Parameters": [ - { - "Name": "output", - "Type": "System.IO.TextWriter" - }, - { - "Name": "value", - "Type": "System.Char[]" - }, - { - "Name": "startIndex", - "Type": "System.Int32" - }, - { - "Name": "characterCount", - "Type": "System.Int32" - } - ], - "ReturnType": "System.Void", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Encode", - "Parameters": [ - { - "Name": "output", - "Type": "System.IO.TextWriter" - }, - { - "Name": "value", - "Type": "System.String" - }, - { - "Name": "startIndex", - "Type": "System.Int32" - }, - { - "Name": "characterCount", - "Type": "System.Int32" - } - ], - "ReturnType": "System.Void", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "WillEncode", - "Parameters": [ - { - "Name": "unicodeScalar", - "Type": "System.Int32" - } - ], - "ReturnType": "System.Boolean", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "FindFirstCharacterToEncode", - "Parameters": [ - { - "Name": "text", - "Type": "System.Char*" - }, - { - "Name": "textLength", - "Type": "System.Int32" - } - ], - "ReturnType": "System.Int32", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "TryEncodeUnicodeScalar", - "Parameters": [ - { - "Name": "unicodeScalar", - "Type": "System.Int32" - }, - { - "Name": "buffer", - "Type": "System.Char*" - }, - { - "Name": "bufferLength", - "Type": "System.Int32" - }, - { - "Name": "numberOfCharactersWritten", - "Type": "System.Int32", - "Direction": "Out" - } - ], - "ReturnType": "System.Boolean", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [], - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.Extensions.WebEncoders.Testing.UrlTestEncoder", - "Visibility": "Public", - "Kind": "Class", - "BaseType": "System.Text.Encodings.Web.UrlEncoder", - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "get_MaxOutputCharactersPerInputCharacter", - "Parameters": [], - "ReturnType": "System.Int32", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Encode", - "Parameters": [ - { - "Name": "value", - "Type": "System.String" - } - ], - "ReturnType": "System.String", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Encode", - "Parameters": [ - { - "Name": "output", - "Type": "System.IO.TextWriter" - }, - { - "Name": "value", - "Type": "System.Char[]" - }, - { - "Name": "startIndex", - "Type": "System.Int32" - }, - { - "Name": "characterCount", - "Type": "System.Int32" - } - ], - "ReturnType": "System.Void", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Encode", - "Parameters": [ - { - "Name": "output", - "Type": "System.IO.TextWriter" - }, - { - "Name": "value", - "Type": "System.String" - }, - { - "Name": "startIndex", - "Type": "System.Int32" - }, - { - "Name": "characterCount", - "Type": "System.Int32" - } - ], - "ReturnType": "System.Void", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "WillEncode", - "Parameters": [ - { - "Name": "unicodeScalar", - "Type": "System.Int32" - } - ], - "ReturnType": "System.Boolean", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "FindFirstCharacterToEncode", - "Parameters": [ - { - "Name": "text", - "Type": "System.Char*" - }, - { - "Name": "textLength", - "Type": "System.Int32" - } - ], - "ReturnType": "System.Int32", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "TryEncodeUnicodeScalar", - "Parameters": [ - { - "Name": "unicodeScalar", - "Type": "System.Int32" - }, - { - "Name": "buffer", - "Type": "System.Char*" - }, - { - "Name": "bufferLength", - "Type": "System.Int32" - }, - { - "Name": "numberOfCharactersWritten", - "Type": "System.Int32", - "Direction": "Out" - } - ], - "ReturnType": "System.Boolean", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [], - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.Extensions.DependencyInjection.EncoderServiceCollectionExtensions", - "Visibility": "Public", - "Kind": "Class", - "Abstract": true, - "Static": true, - "Sealed": true, - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "AddWebEncoders", - "Parameters": [ - { - "Name": "services", - "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" - } - ], - "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection", - "Static": true, - "Extension": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "AddWebEncoders", - "Parameters": [ - { - "Name": "services", - "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" - }, - { - "Name": "setupAction", - "Type": "System.Action" - } - ], - "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection", - "Static": true, - "Extension": true, - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - } - ] -} \ No newline at end of file diff --git a/src/WebEncoders/test/Microsoft.Extensions.WebEncoders.Tests.csproj b/src/WebEncoders/test/Microsoft.Extensions.WebEncoders.Tests.csproj index 659e0ac40f..5a28d38065 100755 --- a/src/WebEncoders/test/Microsoft.Extensions.WebEncoders.Tests.csproj +++ b/src/WebEncoders/test/Microsoft.Extensions.WebEncoders.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp2.2;net461 + netcoreapp3.0;net472