Merge branch 'master' of dotnet/extensions
This commit is contained in:
commit
25b41ef79f
|
|
@ -0,0 +1,9 @@
|
|||
<Project>
|
||||
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.props))\Directory.Build.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<GenerateDocumentationFile Condition=" '$(IsUnitTestProject)' != 'true' AND '$(IsSampleProject)' != 'true' ">true</GenerateDocumentationFile>
|
||||
<PackageTags>configuration</PackageTags>
|
||||
<NoWarn>$(NoWarn);PKG0001</NoWarn>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<!-- This file is automatically generated. -->
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;$(DefaultNetCoreTargetFramework)</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
|
||||
<Compile Include="Microsoft.Extensions.Configuration.KeyPerFile.netstandard2.0.cs" />
|
||||
<Reference Include="Microsoft.Extensions.Configuration" />
|
||||
<Reference Include="Microsoft.Extensions.FileProviders.Physical" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)' == '$(DefaultNetCoreTargetFramework)'">
|
||||
<Compile Include="Microsoft.Extensions.Configuration.KeyPerFile.netcoreapp.cs" />
|
||||
<Reference Include="Microsoft.Extensions.Configuration" />
|
||||
<Reference Include="Microsoft.Extensions.FileProviders.Physical" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
namespace Microsoft.Extensions.Configuration
|
||||
{
|
||||
public static partial class KeyPerFileConfigurationBuilderExtensions
|
||||
{
|
||||
public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddKeyPerFile(this Microsoft.Extensions.Configuration.IConfigurationBuilder builder, System.Action<Microsoft.Extensions.Configuration.KeyPerFile.KeyPerFileConfigurationSource> configureSource) { throw null; }
|
||||
public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddKeyPerFile(this Microsoft.Extensions.Configuration.IConfigurationBuilder builder, string directoryPath) { throw null; }
|
||||
public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddKeyPerFile(this Microsoft.Extensions.Configuration.IConfigurationBuilder builder, string directoryPath, bool optional) { throw null; }
|
||||
public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddKeyPerFile(this Microsoft.Extensions.Configuration.IConfigurationBuilder builder, string directoryPath, bool optional, bool reloadOnChange) { throw null; }
|
||||
}
|
||||
}
|
||||
namespace Microsoft.Extensions.Configuration.KeyPerFile
|
||||
{
|
||||
public partial class KeyPerFileConfigurationProvider : Microsoft.Extensions.Configuration.ConfigurationProvider, System.IDisposable
|
||||
{
|
||||
public KeyPerFileConfigurationProvider(Microsoft.Extensions.Configuration.KeyPerFile.KeyPerFileConfigurationSource source) { }
|
||||
public void Dispose() { }
|
||||
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<string, bool> 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 int ReloadDelay { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
|
||||
public bool ReloadOnChange { [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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
namespace Microsoft.Extensions.Configuration
|
||||
{
|
||||
public static partial class KeyPerFileConfigurationBuilderExtensions
|
||||
{
|
||||
public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddKeyPerFile(this Microsoft.Extensions.Configuration.IConfigurationBuilder builder, System.Action<Microsoft.Extensions.Configuration.KeyPerFile.KeyPerFileConfigurationSource> configureSource) { throw null; }
|
||||
public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddKeyPerFile(this Microsoft.Extensions.Configuration.IConfigurationBuilder builder, string directoryPath) { throw null; }
|
||||
public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddKeyPerFile(this Microsoft.Extensions.Configuration.IConfigurationBuilder builder, string directoryPath, bool optional) { throw null; }
|
||||
public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddKeyPerFile(this Microsoft.Extensions.Configuration.IConfigurationBuilder builder, string directoryPath, bool optional, bool reloadOnChange) { throw null; }
|
||||
}
|
||||
}
|
||||
namespace Microsoft.Extensions.Configuration.KeyPerFile
|
||||
{
|
||||
public partial class KeyPerFileConfigurationProvider : Microsoft.Extensions.Configuration.ConfigurationProvider, System.IDisposable
|
||||
{
|
||||
public KeyPerFileConfigurationProvider(Microsoft.Extensions.Configuration.KeyPerFile.KeyPerFileConfigurationSource source) { }
|
||||
public void Dispose() { }
|
||||
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<string, bool> 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 int ReloadDelay { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
|
||||
public bool ReloadOnChange { [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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using Microsoft.Extensions.Configuration.KeyPerFile;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
|
||||
namespace Microsoft.Extensions.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for registering <see cref="KeyPerFileConfigurationProvider"/> with <see cref="IConfigurationBuilder"/>.
|
||||
/// </summary>
|
||||
public static class KeyPerFileConfigurationBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds configuration using files from a directory. File names are used as the key,
|
||||
/// file contents are used as the value.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IConfigurationBuilder"/> to add to.</param>
|
||||
/// <param name="directoryPath">The path to the directory.</param>
|
||||
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
|
||||
public static IConfigurationBuilder AddKeyPerFile(this IConfigurationBuilder builder, string directoryPath)
|
||||
=> builder.AddKeyPerFile(directoryPath, optional: false, reloadOnChange: false);
|
||||
|
||||
/// <summary>
|
||||
/// Adds configuration using files from a directory. File names are used as the key,
|
||||
/// file contents are used as the value.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IConfigurationBuilder"/> to add to.</param>
|
||||
/// <param name="directoryPath">The path to the directory.</param>
|
||||
/// <param name="optional">Whether the directory is optional.</param>
|
||||
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
|
||||
public static IConfigurationBuilder AddKeyPerFile(this IConfigurationBuilder builder, string directoryPath, bool optional)
|
||||
=> builder.AddKeyPerFile(directoryPath, optional, reloadOnChange: false);
|
||||
|
||||
/// <summary>
|
||||
/// Adds configuration using files from a directory. File names are used as the key,
|
||||
/// file contents are used as the value.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IConfigurationBuilder"/> to add to.</param>
|
||||
/// <param name="directoryPath">The path to the directory.</param>
|
||||
/// <param name="optional">Whether the directory is optional.</param>
|
||||
/// <param name="reloadOnChange">Whether the configuration should be reloaded if the files are changed, added or removed.</param>
|
||||
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
|
||||
public static IConfigurationBuilder AddKeyPerFile(this IConfigurationBuilder builder, string directoryPath, bool optional, bool reloadOnChange)
|
||||
=> builder.AddKeyPerFile(source =>
|
||||
{
|
||||
// Only try to set the file provider if its not optional or the directory exists
|
||||
if (!optional || Directory.Exists(directoryPath))
|
||||
{
|
||||
source.FileProvider = new PhysicalFileProvider(directoryPath);
|
||||
}
|
||||
source.Optional = optional;
|
||||
source.ReloadOnChange = reloadOnChange;
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Adds configuration using files from a directory. File names are used as the key,
|
||||
/// file contents are used as the value.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IConfigurationBuilder"/> to add to.</param>
|
||||
/// <param name="configureSource">Configures the source.</param>
|
||||
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
|
||||
public static IConfigurationBuilder AddKeyPerFile(this IConfigurationBuilder builder, Action<KeyPerFileConfigurationSource> configureSource)
|
||||
=> builder.Add(configureSource);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace Microsoft.Extensions.Configuration.KeyPerFile
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="ConfigurationProvider"/> that uses a directory's files as configuration key/values.
|
||||
/// </summary>
|
||||
public class KeyPerFileConfigurationProvider : ConfigurationProvider, IDisposable
|
||||
{
|
||||
private readonly IDisposable _changeTokenRegistration;
|
||||
|
||||
KeyPerFileConfigurationSource Source { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance.
|
||||
/// </summary>
|
||||
/// <param name="source">The settings.</param>
|
||||
public KeyPerFileConfigurationProvider(KeyPerFileConfigurationSource source)
|
||||
{
|
||||
Source = source ?? throw new ArgumentNullException(nameof(source));
|
||||
|
||||
if (Source.ReloadOnChange && Source.FileProvider != null)
|
||||
{
|
||||
_changeTokenRegistration = ChangeToken.OnChange(
|
||||
() => Source.FileProvider.Watch("*"),
|
||||
() =>
|
||||
{
|
||||
Thread.Sleep(Source.ReloadDelay);
|
||||
Load(reload: true);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static string NormalizeKey(string key)
|
||||
=> key.Replace("__", ConfigurationPath.KeyDelimiter);
|
||||
|
||||
private static string TrimNewLine(string value)
|
||||
=> value.EndsWith(Environment.NewLine)
|
||||
? value.Substring(0, value.Length - Environment.NewLine.Length)
|
||||
: value;
|
||||
|
||||
/// <summary>
|
||||
/// Loads the configuration values.
|
||||
/// </summary>
|
||||
public override void Load()
|
||||
{
|
||||
Load(reload: false);
|
||||
}
|
||||
|
||||
private void Load(bool reload)
|
||||
{
|
||||
var data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (Source.FileProvider == null)
|
||||
{
|
||||
if (Source.Optional || reload) // Always optional on reload
|
||||
{
|
||||
Data = data;
|
||||
return;
|
||||
}
|
||||
|
||||
throw new DirectoryNotFoundException("A non-null file provider for the directory is required when this source is not optional.");
|
||||
}
|
||||
|
||||
var directory = Source.FileProvider.GetDirectoryContents("/");
|
||||
if (!directory.Exists)
|
||||
{
|
||||
if (Source.Optional || reload) // Always optional on reload
|
||||
{
|
||||
Data = data;
|
||||
return;
|
||||
}
|
||||
throw new DirectoryNotFoundException("The root directory for the FileProvider doesn't exist and is not optional.");
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var file in directory)
|
||||
{
|
||||
if (file.IsDirectory)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
using var stream = file.CreateReadStream();
|
||||
using var streamReader = new StreamReader(stream);
|
||||
|
||||
if (Source.IgnoreCondition == null || !Source.IgnoreCondition(file.Name))
|
||||
{
|
||||
data.Add(NormalizeKey(file.Name), TrimNewLine(streamReader.ReadToEnd()));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Data = data;
|
||||
}
|
||||
|
||||
private string GetDirectoryName()
|
||||
=> Source.FileProvider?.GetFileInfo("/")?.PhysicalPath ?? "<Unknown>";
|
||||
|
||||
/// <summary>
|
||||
/// Generates a string representing this provider name and relevant details.
|
||||
/// </summary>
|
||||
/// <returns> The configuration name. </returns>
|
||||
public override string ToString()
|
||||
=> $"{GetType().Name} for files in '{GetDirectoryName()}' ({(Source.Optional ? "Optional" : "Required")})";
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
_changeTokenRegistration?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
|
||||
namespace Microsoft.Extensions.Configuration.KeyPerFile
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="IConfigurationSource"/> used to configure <see cref="KeyPerFileConfigurationProvider"/>.
|
||||
/// </summary>
|
||||
public class KeyPerFileConfigurationSource : IConfigurationSource
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructor;
|
||||
/// </summary>
|
||||
public KeyPerFileConfigurationSource()
|
||||
=> IgnoreCondition = s => IgnorePrefix != null && s.StartsWith(IgnorePrefix);
|
||||
|
||||
/// <summary>
|
||||
/// The FileProvider whos root "/" directory files will be used as configuration data.
|
||||
/// </summary>
|
||||
public IFileProvider FileProvider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Files that start with this prefix will be excluded.
|
||||
/// Defaults to "ignore.".
|
||||
/// </summary>
|
||||
public string IgnorePrefix { get; set; } = "ignore.";
|
||||
|
||||
/// <summary>
|
||||
/// Used to determine if a file should be ignored using its name.
|
||||
/// Defaults to using the IgnorePrefix.
|
||||
/// </summary>
|
||||
public Func<string, bool> IgnoreCondition { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If false, will throw if the directory doesn't exist.
|
||||
/// </summary>
|
||||
public bool Optional { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the source will be loaded if the underlying file changes.
|
||||
/// </summary>
|
||||
public bool ReloadOnChange { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of milliseconds that reload will wait before calling Load. This helps
|
||||
/// avoid triggering reload before a file is completely written. Default is 250.
|
||||
/// </summary>
|
||||
public int ReloadDelay { get; set; } = 250;
|
||||
|
||||
/// <summary>
|
||||
/// Builds the <see cref="KeyPerFileConfigurationProvider"/> for this source.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IConfigurationBuilder"/>.</param>
|
||||
/// <returns>A <see cref="KeyPerFileConfigurationProvider"/></returns>
|
||||
public IConfigurationProvider Build(IConfigurationBuilder builder)
|
||||
=> new KeyPerFileConfigurationProvider(this);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>Configuration provider that uses files in a directory for Microsoft.Extensions.Configuration.</Description>
|
||||
<TargetFrameworks>netstandard2.0;$(DefaultNetCoreTargetFramework)</TargetFrameworks>
|
||||
<TargetFrameworks Condition="'$(DotNetBuildFromSource)' == 'true'">$(DefaultNetCoreTargetFramework)</TargetFrameworks>
|
||||
<IsPackable>true</IsPackable>
|
||||
<IsShipping>true</IsShipping>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.Extensions.Configuration" />
|
||||
<Reference Include="Microsoft.Extensions.FileProviders.Physical" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
|
||||
This is a configuration provider that uses a directory's files as data. A file's name is the key and the contents are the value.
|
||||
|
|
@ -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<IFileInfo>();
|
||||
SectionToTestFiles(testFiles, "", testConfig);
|
||||
|
||||
var provider = new KeyPerFileConfigurationProvider(
|
||||
new KeyPerFileConfigurationSource
|
||||
{
|
||||
Optional = true,
|
||||
FileProvider = new TestFileProvider(testFiles.ToArray())
|
||||
});
|
||||
|
||||
return (provider, () => { });
|
||||
}
|
||||
|
||||
private void SectionToTestFiles(List<IFileInfo> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<AsOptions>();
|
||||
|
||||
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<KeyValuePair<string, string>>();
|
||||
SectionToValues(testConfig, "", values);
|
||||
|
||||
return (new MemoryConfigurationProvider(
|
||||
new MemoryConfigurationSource
|
||||
{
|
||||
InitialData = values
|
||||
}),
|
||||
() => { });
|
||||
}
|
||||
|
||||
protected static void SectionToValues(
|
||||
TestSection section,
|
||||
string sectionName,
|
||||
IList<KeyValuePair<string, string>> values)
|
||||
{
|
||||
foreach (var tuple in section.Values.SelectMany(e => e.Value.Expand(e.Key)))
|
||||
{
|
||||
values.Add(new KeyValuePair<string, string>(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")}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,416 @@
|
|||
// 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.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Extensions.Configuration.KeyPerFile.Test
|
||||
{
|
||||
public class KeyPerFileTests
|
||||
{
|
||||
[Fact]
|
||||
public void DoesNotThrowWhenOptionalAndNoSecrets()
|
||||
{
|
||||
new ConfigurationBuilder().AddKeyPerFile(o => o.Optional = true).Build();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DoesNotThrowWhenOptionalAndDirectoryDoesntExist()
|
||||
{
|
||||
new ConfigurationBuilder().AddKeyPerFile("nonexistent", true).Build();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ThrowsWhenNotOptionalAndDirectoryDoesntExist()
|
||||
{
|
||||
var e = Assert.Throws<ArgumentException>(() => new ConfigurationBuilder().AddKeyPerFile("nonexistent", false).Build());
|
||||
Assert.Contains("The path must be absolute.", e.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanLoadMultipleSecrets()
|
||||
{
|
||||
var testFileProvider = new TestFileProvider(
|
||||
new TestFile("Secret1", "SecretValue1"),
|
||||
new TestFile("Secret2", "SecretValue2"));
|
||||
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddKeyPerFile(o => o.FileProvider = testFileProvider)
|
||||
.Build();
|
||||
|
||||
Assert.Equal("SecretValue1", config["Secret1"]);
|
||||
Assert.Equal("SecretValue2", config["Secret2"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanLoadMultipleSecretsWithDirectory()
|
||||
{
|
||||
var testFileProvider = new TestFileProvider(
|
||||
new TestFile("Secret1", "SecretValue1"),
|
||||
new TestFile("Secret2", "SecretValue2"),
|
||||
new TestFile("directory"));
|
||||
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddKeyPerFile(o => o.FileProvider = testFileProvider)
|
||||
.Build();
|
||||
|
||||
Assert.Equal("SecretValue1", config["Secret1"]);
|
||||
Assert.Equal("SecretValue2", config["Secret2"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanLoadNestedKeys()
|
||||
{
|
||||
var testFileProvider = new TestFileProvider(
|
||||
new TestFile("Secret0__Secret1__Secret2__Key", "SecretValue2"),
|
||||
new TestFile("Secret0__Secret1__Key", "SecretValue1"),
|
||||
new TestFile("Secret0__Key", "SecretValue0"));
|
||||
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddKeyPerFile(o => o.FileProvider = testFileProvider)
|
||||
.Build();
|
||||
|
||||
Assert.Equal("SecretValue0", config["Secret0:Key"]);
|
||||
Assert.Equal("SecretValue1", config["Secret0:Secret1:Key"]);
|
||||
Assert.Equal("SecretValue2", config["Secret0:Secret1:Secret2:Key"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanIgnoreFilesWithDefault()
|
||||
{
|
||||
var testFileProvider = new TestFileProvider(
|
||||
new TestFile("ignore.Secret0", "SecretValue0"),
|
||||
new TestFile("ignore.Secret1", "SecretValue1"),
|
||||
new TestFile("Secret2", "SecretValue2"));
|
||||
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddKeyPerFile(o => o.FileProvider = testFileProvider)
|
||||
.Build();
|
||||
|
||||
Assert.Null(config["ignore.Secret0"]);
|
||||
Assert.Null(config["ignore.Secret1"]);
|
||||
Assert.Equal("SecretValue2", config["Secret2"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanTurnOffDefaultIgnorePrefixWithCondition()
|
||||
{
|
||||
var testFileProvider = new TestFileProvider(
|
||||
new TestFile("ignore.Secret0", "SecretValue0"),
|
||||
new TestFile("ignore.Secret1", "SecretValue1"),
|
||||
new TestFile("Secret2", "SecretValue2"));
|
||||
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddKeyPerFile(o =>
|
||||
{
|
||||
o.FileProvider = testFileProvider;
|
||||
o.IgnoreCondition = null;
|
||||
})
|
||||
.Build();
|
||||
|
||||
Assert.Equal("SecretValue0", config["ignore.Secret0"]);
|
||||
Assert.Equal("SecretValue1", config["ignore.Secret1"]);
|
||||
Assert.Equal("SecretValue2", config["Secret2"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanIgnoreAllWithCondition()
|
||||
{
|
||||
var testFileProvider = new TestFileProvider(
|
||||
new TestFile("Secret0", "SecretValue0"),
|
||||
new TestFile("Secret1", "SecretValue1"),
|
||||
new TestFile("Secret2", "SecretValue2"));
|
||||
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddKeyPerFile(o =>
|
||||
{
|
||||
o.FileProvider = testFileProvider;
|
||||
o.IgnoreCondition = s => true;
|
||||
})
|
||||
.Build();
|
||||
|
||||
Assert.Empty(config.AsEnumerable());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanIgnoreFilesWithCustomIgnore()
|
||||
{
|
||||
var testFileProvider = new TestFileProvider(
|
||||
new TestFile("meSecret0", "SecretValue0"),
|
||||
new TestFile("meSecret1", "SecretValue1"),
|
||||
new TestFile("Secret2", "SecretValue2"));
|
||||
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddKeyPerFile(o =>
|
||||
{
|
||||
o.FileProvider = testFileProvider;
|
||||
o.IgnorePrefix = "me";
|
||||
})
|
||||
.Build();
|
||||
|
||||
Assert.Null(config["meSecret0"]);
|
||||
Assert.Null(config["meSecret1"]);
|
||||
Assert.Equal("SecretValue2", config["Secret2"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanUnIgnoreDefaultFiles()
|
||||
{
|
||||
var testFileProvider = new TestFileProvider(
|
||||
new TestFile("ignore.Secret0", "SecretValue0"),
|
||||
new TestFile("ignore.Secret1", "SecretValue1"),
|
||||
new TestFile("Secret2", "SecretValue2"));
|
||||
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddKeyPerFile(o =>
|
||||
{
|
||||
o.FileProvider = testFileProvider;
|
||||
o.IgnorePrefix = null;
|
||||
})
|
||||
.Build();
|
||||
|
||||
Assert.Equal("SecretValue0", config["ignore.Secret0"]);
|
||||
Assert.Equal("SecretValue1", config["ignore.Secret1"]);
|
||||
Assert.Equal("SecretValue2", config["Secret2"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BindingDoesNotThrowIfReloadedDuringBinding()
|
||||
{
|
||||
var testFileProvider = new TestFileProvider(
|
||||
new TestFile("Number", "-2"),
|
||||
new TestFile("Text", "Foo"));
|
||||
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddKeyPerFile(o => o.FileProvider = testFileProvider)
|
||||
.Build();
|
||||
|
||||
MyOptions options = null;
|
||||
|
||||
using (var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(250)))
|
||||
{
|
||||
void ReloadLoop()
|
||||
{
|
||||
while (!cts.IsCancellationRequested)
|
||||
{
|
||||
config.Reload();
|
||||
}
|
||||
}
|
||||
|
||||
_ = Task.Run(ReloadLoop);
|
||||
|
||||
while (!cts.IsCancellationRequested)
|
||||
{
|
||||
options = config.Get<MyOptions>();
|
||||
}
|
||||
}
|
||||
|
||||
Assert.Equal(-2, options.Number);
|
||||
Assert.Equal("Foo", options.Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReloadConfigWhenReloadOnChangeIsTrue()
|
||||
{
|
||||
var testFileProvider = new TestFileProvider(
|
||||
new TestFile("Secret1", "SecretValue1"),
|
||||
new TestFile("Secret2", "SecretValue2"));
|
||||
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddKeyPerFile(o =>
|
||||
{
|
||||
o.FileProvider = testFileProvider;
|
||||
o.ReloadOnChange = true;
|
||||
}).Build();
|
||||
|
||||
Assert.Equal("SecretValue1", config["Secret1"]);
|
||||
Assert.Equal("SecretValue2", config["Secret2"]);
|
||||
|
||||
testFileProvider.ChangeFiles(
|
||||
new TestFile("Secret1", "NewSecretValue1"),
|
||||
new TestFile("Secret3", "NewSecretValue3"));
|
||||
|
||||
Assert.Equal("NewSecretValue1", config["Secret1"]);
|
||||
Assert.Null(config["NewSecret2"]);
|
||||
Assert.Equal("NewSecretValue3", config["Secret3"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SameConfigWhenReloadOnChangeIsFalse()
|
||||
{
|
||||
var testFileProvider = new TestFileProvider(
|
||||
new TestFile("Secret1", "SecretValue1"),
|
||||
new TestFile("Secret2", "SecretValue2"));
|
||||
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddKeyPerFile(o =>
|
||||
{
|
||||
o.FileProvider = testFileProvider;
|
||||
o.ReloadOnChange = false;
|
||||
}).Build();
|
||||
|
||||
Assert.Equal("SecretValue1", config["Secret1"]);
|
||||
Assert.Equal("SecretValue2", config["Secret2"]);
|
||||
|
||||
testFileProvider.ChangeFiles(
|
||||
new TestFile("Secret1", "NewSecretValue1"),
|
||||
new TestFile("Secret3", "NewSecretValue3"));
|
||||
|
||||
Assert.Equal("SecretValue1", config["Secret1"]);
|
||||
Assert.Equal("SecretValue2", config["Secret2"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NoFilesReloadWhenAddedFiles()
|
||||
{
|
||||
var testFileProvider = new TestFileProvider();
|
||||
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddKeyPerFile(o =>
|
||||
{
|
||||
o.FileProvider = testFileProvider;
|
||||
o.ReloadOnChange = true;
|
||||
}).Build();
|
||||
|
||||
Assert.Empty(config.AsEnumerable());
|
||||
|
||||
testFileProvider.ChangeFiles(
|
||||
new TestFile("Secret1", "SecretValue1"),
|
||||
new TestFile("Secret2", "SecretValue2"));
|
||||
|
||||
Assert.Equal("SecretValue1", config["Secret1"]);
|
||||
Assert.Equal("SecretValue2", config["Secret2"]);
|
||||
}
|
||||
|
||||
private sealed class MyOptions
|
||||
{
|
||||
public int Number { get; set; }
|
||||
public string Text { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
class TestFileProvider : IFileProvider
|
||||
{
|
||||
IDirectoryContents _contents;
|
||||
MockChangeToken _changeToken;
|
||||
|
||||
public TestFileProvider(params IFileInfo[] files)
|
||||
{
|
||||
_contents = new TestDirectoryContents(files);
|
||||
_changeToken = new MockChangeToken();
|
||||
}
|
||||
|
||||
public IDirectoryContents GetDirectoryContents(string subpath) => _contents;
|
||||
|
||||
public IFileInfo GetFileInfo(string subpath) => new TestFile("TestDirectory");
|
||||
|
||||
public IChangeToken Watch(string filter) => _changeToken;
|
||||
|
||||
internal void ChangeFiles(params IFileInfo[] files)
|
||||
{
|
||||
_contents = new TestDirectoryContents(files);
|
||||
_changeToken.RaiseCallback();
|
||||
}
|
||||
}
|
||||
|
||||
class MockChangeToken : IChangeToken
|
||||
{
|
||||
private Action _callback;
|
||||
|
||||
public bool ActiveChangeCallbacks => true;
|
||||
|
||||
public bool HasChanged => true;
|
||||
|
||||
public IDisposable RegisterChangeCallback(Action<object> callback, object state)
|
||||
{
|
||||
var disposable = new MockDisposable();
|
||||
_callback = () => callback(state);
|
||||
return disposable;
|
||||
}
|
||||
|
||||
internal void RaiseCallback()
|
||||
{
|
||||
_callback?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
class MockDisposable : IDisposable
|
||||
{
|
||||
public bool Disposed { get; set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
class TestDirectoryContents : IDirectoryContents
|
||||
{
|
||||
List<IFileInfo> _list;
|
||||
|
||||
public TestDirectoryContents(params IFileInfo[] files)
|
||||
{
|
||||
_list = new List<IFileInfo>(files);
|
||||
}
|
||||
|
||||
public bool Exists => true;
|
||||
|
||||
public IEnumerator<IFileInfo> GetEnumerator() => _list.GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
|
||||
//TODO: Probably need a directory and file type.
|
||||
class TestFile : IFileInfo
|
||||
{
|
||||
private readonly string _name;
|
||||
private readonly string _contents;
|
||||
|
||||
public bool Exists => true;
|
||||
|
||||
public bool IsDirectory
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public DateTimeOffset LastModified => throw new NotImplementedException();
|
||||
|
||||
public long Length => throw new NotImplementedException();
|
||||
|
||||
public string Name => _name;
|
||||
|
||||
public string PhysicalPath => "Root/" + Name;
|
||||
|
||||
public TestFile(string name)
|
||||
{
|
||||
_name = name;
|
||||
IsDirectory = true;
|
||||
}
|
||||
|
||||
public TestFile(string name, string contents)
|
||||
{
|
||||
_name = name;
|
||||
_contents = contents;
|
||||
}
|
||||
|
||||
public Stream CreateReadStream()
|
||||
{
|
||||
if (IsDirectory)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot create stream from directory");
|
||||
}
|
||||
|
||||
return _contents == null
|
||||
? new MemoryStream()
|
||||
: new MemoryStream(Encoding.UTF8.GetBytes(_contents));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>$(DefaultNetCoreTargetFramework);net472</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Config\test\Microsoft.Extensions.Configuration.Tests.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.Extensions.Configuration.Abstractions" />
|
||||
<Reference Include="Microsoft.Extensions.Configuration.Binder" />
|
||||
<Reference Include="Microsoft.Extensions.Configuration.FileExtensions" />
|
||||
<Reference Include="Microsoft.Extensions.Configuration.KeyPerFile" />
|
||||
<Reference Include="Microsoft.Extensions.Configuration" />
|
||||
<Reference Include="Microsoft.Extensions.FileProviders.Abstractions" />
|
||||
<Reference Include="Microsoft.Extensions.FileProviders.Physical" />
|
||||
<Reference Include="Microsoft.Extensions.Primitives" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<Project>
|
||||
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.props))\Directory.Build.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<GenerateDocumentationFile Condition=" '$(IsUnitTestProject)' != 'true' ">true</GenerateDocumentationFile>
|
||||
<PackageTags>files;filesystem</PackageTags>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<!-- This file is automatically generated. -->
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;$(DefaultNetCoreTargetFramework)</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
|
||||
<Compile Include="Microsoft.Extensions.FileProviders.Embedded.netstandard2.0.cs" />
|
||||
<Reference Include="Microsoft.Extensions.FileProviders.Abstractions" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)' == '$(DefaultNetCoreTargetFramework)'">
|
||||
<Compile Include="Microsoft.Extensions.FileProviders.Embedded.netcoreapp.cs" />
|
||||
<Reference Include="Microsoft.Extensions.FileProviders.Abstractions" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,181 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.FileProviders.Embedded;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace Microsoft.Extensions.FileProviders
|
||||
{
|
||||
/// <summary>
|
||||
/// Looks up files using embedded resources in the specified assembly.
|
||||
/// This file provider is case sensitive.
|
||||
/// </summary>
|
||||
public class EmbeddedFileProvider : IFileProvider
|
||||
{
|
||||
private static readonly char[] _invalidFileNameChars = Path.GetInvalidFileNameChars()
|
||||
.Where(c => c != '/' && c != '\\').ToArray();
|
||||
|
||||
private readonly Assembly _assembly;
|
||||
private readonly string _baseNamespace;
|
||||
private readonly DateTimeOffset _lastModified;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EmbeddedFileProvider" /> class using the specified
|
||||
/// assembly with the base namespace defaulting to the assembly name.
|
||||
/// </summary>
|
||||
/// <param name="assembly">The assembly that contains the embedded resources.</param>
|
||||
public EmbeddedFileProvider(Assembly assembly)
|
||||
: this(assembly, assembly?.GetName()?.Name)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EmbeddedFileProvider" /> class using the specified
|
||||
/// assembly and base namespace.
|
||||
/// </summary>
|
||||
/// <param name="assembly">The assembly that contains the embedded resources.</param>
|
||||
/// <param name="baseNamespace">The base namespace that contains the embedded resources.</param>
|
||||
public EmbeddedFileProvider(Assembly assembly, string baseNamespace)
|
||||
{
|
||||
if (assembly == null)
|
||||
{
|
||||
throw new ArgumentNullException("assembly");
|
||||
}
|
||||
|
||||
_baseNamespace = string.IsNullOrEmpty(baseNamespace) ? string.Empty : baseNamespace + ".";
|
||||
_assembly = assembly;
|
||||
|
||||
_lastModified = DateTimeOffset.UtcNow;
|
||||
|
||||
if (!string.IsNullOrEmpty(_assembly.Location))
|
||||
{
|
||||
try
|
||||
{
|
||||
_lastModified = File.GetLastWriteTimeUtc(_assembly.Location);
|
||||
}
|
||||
catch (PathTooLongException)
|
||||
{
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Locates a file at the given path.
|
||||
/// </summary>
|
||||
/// <param name="subpath">The path that identifies the file. </param>
|
||||
/// <returns>
|
||||
/// The file information. Caller must check Exists property. A <see cref="NotFoundFileInfo" /> if the file could
|
||||
/// not be found.
|
||||
/// </returns>
|
||||
public IFileInfo GetFileInfo(string subpath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(subpath))
|
||||
{
|
||||
return new NotFoundFileInfo(subpath);
|
||||
}
|
||||
|
||||
var builder = new StringBuilder(_baseNamespace.Length + subpath.Length);
|
||||
builder.Append(_baseNamespace);
|
||||
|
||||
// Relative paths starting with a leading slash okay
|
||||
if (subpath.StartsWith("/", StringComparison.Ordinal))
|
||||
{
|
||||
builder.Append(subpath, 1, subpath.Length - 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append(subpath);
|
||||
}
|
||||
|
||||
for (var i = _baseNamespace.Length; i < builder.Length; i++)
|
||||
{
|
||||
if (builder[i] == '/' || builder[i] == '\\')
|
||||
{
|
||||
builder[i] = '.';
|
||||
}
|
||||
}
|
||||
|
||||
var resourcePath = builder.ToString();
|
||||
if (HasInvalidPathChars(resourcePath))
|
||||
{
|
||||
return new NotFoundFileInfo(resourcePath);
|
||||
}
|
||||
|
||||
var name = Path.GetFileName(subpath);
|
||||
if (_assembly.GetManifestResourceInfo(resourcePath) == null)
|
||||
{
|
||||
return new NotFoundFileInfo(name);
|
||||
}
|
||||
|
||||
return new EmbeddedResourceFileInfo(_assembly, resourcePath, name, _lastModified);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerate a directory at the given path, if any.
|
||||
/// This file provider uses a flat directory structure. Everything under the base namespace is considered to be one
|
||||
/// directory.
|
||||
/// </summary>
|
||||
/// <param name="subpath">The path that identifies the directory</param>
|
||||
/// <returns>
|
||||
/// Contents of the directory. Caller must check Exists property. A <see cref="NotFoundDirectoryContents" /> if no
|
||||
/// resources were found that match <paramref name="subpath" />
|
||||
/// </returns>
|
||||
public IDirectoryContents GetDirectoryContents(string subpath)
|
||||
{
|
||||
// The file name is assumed to be the remainder of the resource name.
|
||||
if (subpath == null)
|
||||
{
|
||||
return NotFoundDirectoryContents.Singleton;
|
||||
}
|
||||
|
||||
// EmbeddedFileProvider only supports a flat file structure at the base namespace.
|
||||
if (subpath.Length != 0 && !string.Equals(subpath, "/", StringComparison.Ordinal))
|
||||
{
|
||||
return NotFoundDirectoryContents.Singleton;
|
||||
}
|
||||
|
||||
var entries = new List<IFileInfo>();
|
||||
|
||||
// TODO: The list of resources in an assembly isn't going to change. Consider caching.
|
||||
var resources = _assembly.GetManifestResourceNames();
|
||||
for (var i = 0; i < resources.Length; i++)
|
||||
{
|
||||
var resourceName = resources[i];
|
||||
if (resourceName.StartsWith(_baseNamespace, StringComparison.Ordinal))
|
||||
{
|
||||
entries.Add(new EmbeddedResourceFileInfo(
|
||||
_assembly,
|
||||
resourceName,
|
||||
resourceName.Substring(_baseNamespace.Length),
|
||||
_lastModified));
|
||||
}
|
||||
}
|
||||
|
||||
return new EnumerableDirectoryContents(entries);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Embedded files do not change.
|
||||
/// </summary>
|
||||
/// <param name="pattern">This parameter is ignored</param>
|
||||
/// <returns>A <see cref="NullChangeToken" /></returns>
|
||||
public IChangeToken Watch(string pattern)
|
||||
{
|
||||
return NullChangeToken.Singleton;
|
||||
}
|
||||
|
||||
private static bool HasInvalidPathChars(string path)
|
||||
{
|
||||
return path.IndexOfAny(_invalidFileNameChars) != -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Microsoft.Extensions.FileProviders.Embedded
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a file embedded in an assembly.
|
||||
/// </summary>
|
||||
public class EmbeddedResourceFileInfo : IFileInfo
|
||||
{
|
||||
private readonly Assembly _assembly;
|
||||
private readonly string _resourcePath;
|
||||
|
||||
private long? _length;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="EmbeddedFileProvider"/> for an assembly using <paramref name="resourcePath"/> as the base
|
||||
/// </summary>
|
||||
/// <param name="assembly">The assembly that contains the embedded resource</param>
|
||||
/// <param name="resourcePath">The path to the embedded resource</param>
|
||||
/// <param name="name">An arbitrary name for this instance</param>
|
||||
/// <param name="lastModified">The <see cref="DateTimeOffset" /> to use for <see cref="LastModified" /></param>
|
||||
public EmbeddedResourceFileInfo(
|
||||
Assembly assembly,
|
||||
string resourcePath,
|
||||
string name,
|
||||
DateTimeOffset lastModified)
|
||||
{
|
||||
_assembly = assembly;
|
||||
_resourcePath = resourcePath;
|
||||
Name = name;
|
||||
LastModified = lastModified;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Always true.
|
||||
/// </summary>
|
||||
public bool Exists => true;
|
||||
|
||||
/// <summary>
|
||||
/// The length, in bytes, of the embedded resource
|
||||
/// </summary>
|
||||
public long Length
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!_length.HasValue)
|
||||
{
|
||||
using (var stream = _assembly.GetManifestResourceStream(_resourcePath))
|
||||
{
|
||||
_length = stream.Length;
|
||||
}
|
||||
}
|
||||
return _length.Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Always null.
|
||||
/// </summary>
|
||||
public string PhysicalPath => null;
|
||||
|
||||
/// <summary>
|
||||
/// The name of embedded file
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The time, in UTC, when the <see cref="EmbeddedFileProvider"/> was created
|
||||
/// </summary>
|
||||
public DateTimeOffset LastModified { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Always false.
|
||||
/// </summary>
|
||||
public bool IsDirectory => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Stream CreateReadStream()
|
||||
{
|
||||
var stream = _assembly.GetManifestResourceStream(_resourcePath);
|
||||
if (!_length.HasValue)
|
||||
{
|
||||
_length = stream.Length;
|
||||
}
|
||||
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.Extensions.FileProviders.Embedded
|
||||
{
|
||||
internal class EnumerableDirectoryContents : IDirectoryContents
|
||||
{
|
||||
private readonly IEnumerable<IFileInfo> _entries;
|
||||
|
||||
public EnumerableDirectoryContents(IEnumerable<IFileInfo> entries)
|
||||
{
|
||||
if (entries == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(entries));
|
||||
}
|
||||
|
||||
_entries = entries;
|
||||
}
|
||||
|
||||
public bool Exists
|
||||
{
|
||||
get { return true; }
|
||||
}
|
||||
|
||||
public IEnumerator<IFileInfo> GetEnumerator()
|
||||
{
|
||||
return _entries.GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return _entries.GetEnumerator();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace Microsoft.Extensions.FileProviders.Embedded.Manifest
|
||||
{
|
||||
internal class EmbeddedFilesManifest
|
||||
{
|
||||
private static readonly char[] _invalidFileNameChars = Path.GetInvalidFileNameChars()
|
||||
.Where(c => c != Path.DirectorySeparatorChar && c != Path.AltDirectorySeparatorChar).ToArray();
|
||||
|
||||
private static readonly char[] _separators = new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar };
|
||||
|
||||
private readonly ManifestDirectory _rootDirectory;
|
||||
|
||||
internal EmbeddedFilesManifest(ManifestDirectory rootDirectory)
|
||||
{
|
||||
if (rootDirectory == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(rootDirectory));
|
||||
}
|
||||
|
||||
_rootDirectory = rootDirectory;
|
||||
}
|
||||
|
||||
internal ManifestEntry ResolveEntry(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path) || HasInvalidPathChars(path))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// trimmed is a string without leading nor trailing path separators
|
||||
// so if we find an empty string while iterating over the segments
|
||||
// we know for sure the path is invalid and we treat it as the above
|
||||
// case by returning null.
|
||||
// Examples of invalid paths are: //wwwroot /\wwwroot //wwwroot//jquery.js
|
||||
var trimmed = RemoveLeadingAndTrailingDirectorySeparators(path);
|
||||
// Paths consisting only of a single path separator like / or \ are ok.
|
||||
if (trimmed.Length == 0)
|
||||
{
|
||||
return _rootDirectory;
|
||||
}
|
||||
|
||||
var tokenizer = new StringTokenizer(trimmed, _separators);
|
||||
ManifestEntry currentEntry = _rootDirectory;
|
||||
foreach (var segment in tokenizer)
|
||||
{
|
||||
if (segment.Equals(""))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
currentEntry = currentEntry.Traverse(segment);
|
||||
}
|
||||
|
||||
return currentEntry;
|
||||
}
|
||||
|
||||
private static StringSegment RemoveLeadingAndTrailingDirectorySeparators(string path)
|
||||
{
|
||||
Debug.Assert(path.Length > 0);
|
||||
var start = Array.IndexOf(_separators, path[0]) == -1 ? 0 : 1;
|
||||
if (start == path.Length)
|
||||
{
|
||||
return StringSegment.Empty;
|
||||
}
|
||||
|
||||
var end = Array.IndexOf(_separators, path[path.Length - 1]) == -1 ? path.Length : path.Length - 1;
|
||||
var trimmed = new StringSegment(path, start, end - start);
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
internal EmbeddedFilesManifest Scope(string path)
|
||||
{
|
||||
if (ResolveEntry(path) is ManifestDirectory directory && directory != ManifestEntry.UnknownPath)
|
||||
{
|
||||
return new EmbeddedFilesManifest(directory.ToRootDirectory());
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Invalid path: '{path}'");
|
||||
}
|
||||
|
||||
private static bool HasInvalidPathChars(string path) => path.IndexOfAny(_invalidFileNameChars) != -1;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace Microsoft.Extensions.FileProviders.Embedded.Manifest
|
||||
{
|
||||
internal class ManifestDirectory : ManifestEntry
|
||||
{
|
||||
protected ManifestDirectory(string name, ManifestEntry[] children)
|
||||
: base(name)
|
||||
{
|
||||
if (children == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(children));
|
||||
}
|
||||
|
||||
Children = children;
|
||||
}
|
||||
|
||||
public IReadOnlyList<ManifestEntry> Children { get; protected set; }
|
||||
|
||||
public override ManifestEntry Traverse(StringSegment segment)
|
||||
{
|
||||
if (segment.Equals(".", StringComparison.Ordinal))
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
if (segment.Equals("..", StringComparison.Ordinal))
|
||||
{
|
||||
return Parent;
|
||||
}
|
||||
|
||||
foreach (var child in Children)
|
||||
{
|
||||
if (segment.Equals(child.Name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
return UnknownPath;
|
||||
}
|
||||
|
||||
public virtual ManifestDirectory ToRootDirectory() => CreateRootDirectory(CopyChildren());
|
||||
|
||||
public static ManifestDirectory CreateDirectory(string name, ManifestEntry[] children)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
throw new ArgumentException($"'{nameof(name)}' must not be null, empty or whitespace.", nameof(name));
|
||||
}
|
||||
|
||||
if (children == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(children));
|
||||
}
|
||||
|
||||
var result = new ManifestDirectory(name, children);
|
||||
ValidateChildrenAndSetParent(children, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static ManifestRootDirectory CreateRootDirectory(ManifestEntry[] children)
|
||||
{
|
||||
if (children == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(children));
|
||||
}
|
||||
|
||||
var result = new ManifestRootDirectory(children);
|
||||
ValidateChildrenAndSetParent(children, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
internal static void ValidateChildrenAndSetParent(ManifestEntry[] children, ManifestDirectory parent)
|
||||
{
|
||||
foreach (var child in children)
|
||||
{
|
||||
if (child == UnknownPath)
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid entry type '{nameof(ManifestSinkDirectory)}'");
|
||||
}
|
||||
|
||||
if (child is ManifestRootDirectory)
|
||||
{
|
||||
throw new InvalidOperationException($"Can't add a root folder as a child");
|
||||
}
|
||||
|
||||
child.SetParent(parent);
|
||||
}
|
||||
}
|
||||
|
||||
private ManifestEntry[] CopyChildren()
|
||||
{
|
||||
var list = new List<ManifestEntry>();
|
||||
for (int i = 0; i < Children.Count; i++)
|
||||
{
|
||||
var child = Children[i];
|
||||
switch (child)
|
||||
{
|
||||
case ManifestSinkDirectory s:
|
||||
case ManifestRootDirectory r:
|
||||
throw new InvalidOperationException("Unexpected manifest node.");
|
||||
case ManifestDirectory d:
|
||||
var grandChildren = d.CopyChildren();
|
||||
var newDirectory = CreateDirectory(d.Name, grandChildren);
|
||||
list.Add(newDirectory);
|
||||
break;
|
||||
case ManifestFile f:
|
||||
var file = new ManifestFile(f.Name, f.ResourcePath);
|
||||
list.Add(file);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException("Unexpected manifest node.");
|
||||
}
|
||||
}
|
||||
|
||||
return list.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
// 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.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Microsoft.Extensions.FileProviders.Embedded.Manifest
|
||||
{
|
||||
internal class ManifestDirectoryContents : IDirectoryContents
|
||||
{
|
||||
private readonly DateTimeOffset _lastModified;
|
||||
private IFileInfo[] _entries;
|
||||
|
||||
public ManifestDirectoryContents(Assembly assembly, ManifestDirectory directory, DateTimeOffset lastModified)
|
||||
{
|
||||
if (assembly == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(assembly));
|
||||
}
|
||||
|
||||
if (directory == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(directory));
|
||||
}
|
||||
|
||||
Assembly = assembly;
|
||||
Directory = directory;
|
||||
_lastModified = lastModified;
|
||||
}
|
||||
|
||||
public bool Exists => true;
|
||||
|
||||
public Assembly Assembly { get; }
|
||||
|
||||
public ManifestDirectory Directory { get; }
|
||||
|
||||
public IEnumerator<IFileInfo> GetEnumerator()
|
||||
{
|
||||
return EnsureEntries().GetEnumerator();
|
||||
|
||||
IReadOnlyList<IFileInfo> EnsureEntries() => _entries = _entries ?? ResolveEntries().ToArray();
|
||||
|
||||
IEnumerable<IFileInfo> ResolveEntries()
|
||||
{
|
||||
if (Directory == ManifestEntry.UnknownPath)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (var entry in Directory.Children)
|
||||
{
|
||||
switch (entry)
|
||||
{
|
||||
case ManifestFile f:
|
||||
yield return new ManifestFileInfo(Assembly, f, _lastModified);
|
||||
break;
|
||||
case ManifestDirectory d:
|
||||
yield return new ManifestDirectoryInfo(d, _lastModified);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException("Unknown entry type");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Microsoft.Extensions.FileProviders.Embedded.Manifest
|
||||
{
|
||||
internal class ManifestDirectoryInfo : IFileInfo
|
||||
{
|
||||
public ManifestDirectoryInfo(ManifestDirectory directory, DateTimeOffset lastModified)
|
||||
{
|
||||
if (directory == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(directory));
|
||||
}
|
||||
|
||||
Directory = directory;
|
||||
LastModified = lastModified;
|
||||
}
|
||||
|
||||
public bool Exists => true;
|
||||
|
||||
public long Length => -1;
|
||||
|
||||
public string PhysicalPath => null;
|
||||
|
||||
public string Name => Directory.Name;
|
||||
|
||||
public DateTimeOffset LastModified { get; }
|
||||
|
||||
public bool IsDirectory => true;
|
||||
|
||||
public ManifestDirectory Directory { get; }
|
||||
|
||||
public Stream CreateReadStream() =>
|
||||
throw new InvalidOperationException("Cannot create a stream for a directory.");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace Microsoft.Extensions.FileProviders.Embedded.Manifest
|
||||
{
|
||||
internal abstract class ManifestEntry
|
||||
{
|
||||
public ManifestEntry(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public ManifestEntry Parent { get; private set; }
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public static ManifestEntry UnknownPath { get; } = ManifestSinkDirectory.Instance;
|
||||
|
||||
protected internal virtual void SetParent(ManifestDirectory directory)
|
||||
{
|
||||
if (Parent != null)
|
||||
{
|
||||
throw new InvalidOperationException("Directory already has a parent.");
|
||||
}
|
||||
|
||||
Parent = directory;
|
||||
}
|
||||
|
||||
public abstract ManifestEntry Traverse(StringSegment segment);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
// 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 Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace Microsoft.Extensions.FileProviders.Embedded.Manifest
|
||||
{
|
||||
internal class ManifestFile : ManifestEntry
|
||||
{
|
||||
public ManifestFile(string name, string resourcePath)
|
||||
: base(name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
throw new ArgumentException($"'{nameof(name)}' must not be null, empty or whitespace.", nameof(name));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(resourcePath))
|
||||
{
|
||||
throw new ArgumentException($"'{nameof(resourcePath)}' must not be null, empty or whitespace.", nameof(resourcePath));
|
||||
}
|
||||
|
||||
ResourcePath = resourcePath;
|
||||
}
|
||||
|
||||
public string ResourcePath { get; }
|
||||
|
||||
public override ManifestEntry Traverse(StringSegment segment) => UnknownPath;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Microsoft.Extensions.FileProviders.Embedded.Manifest
|
||||
{
|
||||
internal class ManifestFileInfo : IFileInfo
|
||||
{
|
||||
private long? _length;
|
||||
|
||||
public ManifestFileInfo(Assembly assembly, ManifestFile file, DateTimeOffset lastModified)
|
||||
{
|
||||
if (assembly == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(assembly));
|
||||
}
|
||||
|
||||
if (file == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(file));
|
||||
}
|
||||
|
||||
Assembly = assembly;
|
||||
ManifestFile = file;
|
||||
LastModified = lastModified;
|
||||
}
|
||||
|
||||
public Assembly Assembly { get; }
|
||||
|
||||
public ManifestFile ManifestFile { get; }
|
||||
|
||||
public bool Exists => true;
|
||||
|
||||
public long Length => EnsureLength();
|
||||
|
||||
public string PhysicalPath => null;
|
||||
|
||||
public string Name => ManifestFile.Name;
|
||||
|
||||
public DateTimeOffset LastModified { get; }
|
||||
|
||||
public bool IsDirectory => false;
|
||||
|
||||
private long EnsureLength()
|
||||
{
|
||||
if (_length == null)
|
||||
{
|
||||
using (var stream = Assembly.GetManifestResourceStream(ManifestFile.ResourcePath))
|
||||
{
|
||||
_length = stream.Length;
|
||||
}
|
||||
}
|
||||
|
||||
return _length.Value;
|
||||
}
|
||||
|
||||
public Stream CreateReadStream()
|
||||
{
|
||||
var stream = Assembly.GetManifestResourceStream(ManifestFile.ResourcePath);
|
||||
if (!_length.HasValue)
|
||||
{
|
||||
_length = stream.Length;
|
||||
}
|
||||
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Microsoft.Extensions.FileProviders.Embedded.Manifest
|
||||
{
|
||||
internal static class ManifestParser
|
||||
{
|
||||
private static readonly string DefaultManifestName = "Microsoft.Extensions.FileProviders.Embedded.Manifest.xml";
|
||||
|
||||
public static EmbeddedFilesManifest Parse(Assembly assembly)
|
||||
{
|
||||
return Parse(assembly, DefaultManifestName);
|
||||
}
|
||||
|
||||
public static EmbeddedFilesManifest Parse(Assembly assembly, string name)
|
||||
{
|
||||
if (assembly == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(assembly));
|
||||
}
|
||||
|
||||
if (name == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
var stream = assembly.GetManifestResourceStream(name);
|
||||
if (stream == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Could not load the embedded file manifest " +
|
||||
$"'{name}' for assembly '{assembly.GetName().Name}'.");
|
||||
}
|
||||
|
||||
var document = XDocument.Load(stream);
|
||||
|
||||
var manifest = EnsureElement(document, "Manifest");
|
||||
var manifestVersion = EnsureElement(manifest, "ManifestVersion");
|
||||
var version = EnsureText(manifestVersion);
|
||||
if (!string.Equals("1.0", version, StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException($"The embedded file manifest '{name}' for " +
|
||||
$"assembly '{assembly.GetName().Name}' specifies an unsupported file format" +
|
||||
$" version: '{version}'.");
|
||||
}
|
||||
var fileSystem = EnsureElement(manifest, "FileSystem");
|
||||
|
||||
var entries = fileSystem.Elements();
|
||||
var entriesList = new List<ManifestEntry>();
|
||||
foreach (var element in entries)
|
||||
{
|
||||
var entry = BuildEntry(element);
|
||||
entriesList.Add(entry);
|
||||
}
|
||||
|
||||
ValidateEntries(entriesList);
|
||||
|
||||
var rootDirectory = ManifestDirectory.CreateRootDirectory(entriesList.ToArray());
|
||||
|
||||
return new EmbeddedFilesManifest(rootDirectory);
|
||||
|
||||
}
|
||||
|
||||
private static void ValidateEntries(List<ManifestEntry> entriesList)
|
||||
{
|
||||
for (int i = 0; i < entriesList.Count - 1; i++)
|
||||
{
|
||||
for (int j = i + 1; j < entriesList.Count; j++)
|
||||
{
|
||||
if (string.Equals(entriesList[i].Name, entriesList[j].Name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Found two entries with the same name but different casing:" +
|
||||
$" '{entriesList[i].Name}' and '{entriesList[j]}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ManifestEntry BuildEntry(XElement element)
|
||||
{
|
||||
RuntimeHelpers.EnsureSufficientExecutionStack();
|
||||
if (element.NodeType != XmlNodeType.Element)
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid manifest format. Expected a 'File' or a 'Directory' node:" +
|
||||
$" '{element.ToString()}'");
|
||||
}
|
||||
|
||||
if (string.Equals(element.Name.LocalName, "File", StringComparison.Ordinal))
|
||||
{
|
||||
var entryName = EnsureName(element);
|
||||
var path = EnsureElement(element, "ResourcePath");
|
||||
var pathValue = EnsureText(path);
|
||||
return new ManifestFile(entryName, pathValue);
|
||||
}
|
||||
|
||||
if (string.Equals(element.Name.LocalName, "Directory", StringComparison.Ordinal))
|
||||
{
|
||||
var directoryName = EnsureName(element);
|
||||
var children = new List<ManifestEntry>();
|
||||
foreach (var child in element.Elements())
|
||||
{
|
||||
children.Add(BuildEntry(child));
|
||||
}
|
||||
|
||||
ValidateEntries(children);
|
||||
|
||||
return ManifestDirectory.CreateDirectory(directoryName, children.ToArray());
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Invalid manifest format.Expected a 'File' or a 'Directory' node. " +
|
||||
$"Got '{element.Name.LocalName}' instead.");
|
||||
}
|
||||
|
||||
private static XElement EnsureElement(XContainer container, string elementName)
|
||||
{
|
||||
var element = container.Element(elementName);
|
||||
if (element == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid manifest format. Missing '{elementName}' element name");
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
private static string EnsureName(XElement element)
|
||||
{
|
||||
var value = element.Attribute("Name")?.Value;
|
||||
if (value == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid manifest format. '{element.Name}' must contain a 'Name' attribute.");
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private static string EnsureText(XElement element)
|
||||
{
|
||||
if (element.Elements().Count() == 0 &&
|
||||
!element.IsEmpty &&
|
||||
element.Nodes().Count() == 1 &&
|
||||
element.FirstNode.NodeType == XmlNodeType.Text)
|
||||
{
|
||||
return element.Value;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"Invalid manifest format. '{element.Name.LocalName}' must contain " +
|
||||
$"a text value. '{element.Value}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 Microsoft.Extensions.FileProviders.Embedded.Manifest
|
||||
{
|
||||
internal class ManifestRootDirectory : ManifestDirectory
|
||||
{
|
||||
public ManifestRootDirectory(ManifestEntry[] children)
|
||||
: base(name: null, children: children)
|
||||
{
|
||||
SetParent(ManifestSinkDirectory.Instance);
|
||||
}
|
||||
|
||||
public override ManifestDirectory ToRootDirectory() => this;
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
||||
using System;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace Microsoft.Extensions.FileProviders.Embedded.Manifest
|
||||
{
|
||||
internal class ManifestSinkDirectory : ManifestDirectory
|
||||
{
|
||||
private ManifestSinkDirectory()
|
||||
: base(name: null, children: Array.Empty<ManifestEntry>())
|
||||
{
|
||||
SetParent(this);
|
||||
Children = new[] { this };
|
||||
}
|
||||
|
||||
public static ManifestDirectory Instance { get; } = new ManifestSinkDirectory();
|
||||
|
||||
public override ManifestEntry Traverse(StringSegment segment) => this;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using Microsoft.Extensions.FileProviders.Embedded.Manifest;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace Microsoft.Extensions.FileProviders
|
||||
{
|
||||
/// <summary>
|
||||
/// An embedded file provider that uses a manifest compiled in the assembly to
|
||||
/// reconstruct the original paths of the embedded files when they were embedded
|
||||
/// into the assembly.
|
||||
/// </summary>
|
||||
public class ManifestEmbeddedFileProvider : IFileProvider
|
||||
{
|
||||
private readonly DateTimeOffset _lastModified;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="ManifestEmbeddedFileProvider"/>.
|
||||
/// </summary>
|
||||
/// <param name="assembly">The assembly containing the embedded files.</param>
|
||||
public ManifestEmbeddedFileProvider(Assembly assembly)
|
||||
: this(assembly, ManifestParser.Parse(assembly), ResolveLastModified(assembly)) { }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="ManifestEmbeddedFileProvider"/>.
|
||||
/// </summary>
|
||||
/// <param name="assembly">The assembly containing the embedded files.</param>
|
||||
/// <param name="root">The relative path from the root of the manifest to use as root for the provider.</param>
|
||||
public ManifestEmbeddedFileProvider(Assembly assembly, string root)
|
||||
: this(assembly, root, ResolveLastModified(assembly))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="ManifestEmbeddedFileProvider"/>.
|
||||
/// </summary>
|
||||
/// <param name="assembly">The assembly containing the embedded files.</param>
|
||||
/// <param name="root">The relative path from the root of the manifest to use as root for the provider.</param>
|
||||
/// <param name="lastModified">The LastModified date to use on the <see cref="IFileInfo"/> instances
|
||||
/// returned by this <see cref="IFileProvider"/>.</param>
|
||||
public ManifestEmbeddedFileProvider(Assembly assembly, string root, DateTimeOffset lastModified)
|
||||
: this(assembly, ManifestParser.Parse(assembly).Scope(root), lastModified)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="ManifestEmbeddedFileProvider"/>.
|
||||
/// </summary>
|
||||
/// <param name="assembly">The assembly containing the embedded files.</param>
|
||||
/// <param name="root">The relative path from the root of the manifest to use as root for the provider.</param>
|
||||
/// <param name="manifestName">The name of the embedded resource containing the manifest.</param>
|
||||
/// <param name="lastModified">The LastModified date to use on the <see cref="IFileInfo"/> instances
|
||||
/// returned by this <see cref="IFileProvider"/>.</param>
|
||||
public ManifestEmbeddedFileProvider(Assembly assembly, string root, string manifestName, DateTimeOffset lastModified)
|
||||
: this(assembly, ManifestParser.Parse(assembly, manifestName).Scope(root), lastModified)
|
||||
{
|
||||
}
|
||||
|
||||
internal ManifestEmbeddedFileProvider(Assembly assembly, EmbeddedFilesManifest manifest, DateTimeOffset lastModified)
|
||||
{
|
||||
if (assembly == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(assembly));
|
||||
}
|
||||
|
||||
if (manifest == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(manifest));
|
||||
}
|
||||
|
||||
Assembly = assembly;
|
||||
Manifest = manifest;
|
||||
_lastModified = lastModified;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="Assembly"/> for this provider.
|
||||
/// </summary>
|
||||
public Assembly Assembly { get; }
|
||||
|
||||
internal EmbeddedFilesManifest Manifest { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IDirectoryContents GetDirectoryContents(string subpath)
|
||||
{
|
||||
var entry = Manifest.ResolveEntry(subpath);
|
||||
if (entry == null || entry == ManifestEntry.UnknownPath)
|
||||
{
|
||||
return NotFoundDirectoryContents.Singleton;
|
||||
}
|
||||
|
||||
if (!(entry is ManifestDirectory directory))
|
||||
{
|
||||
return NotFoundDirectoryContents.Singleton;
|
||||
}
|
||||
|
||||
return new ManifestDirectoryContents(Assembly, directory, _lastModified);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IFileInfo GetFileInfo(string subpath)
|
||||
{
|
||||
var entry = Manifest.ResolveEntry(subpath);
|
||||
switch (entry)
|
||||
{
|
||||
case null:
|
||||
return new NotFoundFileInfo(subpath);
|
||||
case ManifestFile f:
|
||||
return new ManifestFileInfo(Assembly, f, _lastModified);
|
||||
case ManifestDirectory d when d != ManifestEntry.UnknownPath:
|
||||
return new NotFoundFileInfo(d.Name);
|
||||
}
|
||||
|
||||
return new NotFoundFileInfo(subpath);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IChangeToken Watch(string filter)
|
||||
{
|
||||
if (filter == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(filter));
|
||||
}
|
||||
|
||||
return NullChangeToken.Singleton;
|
||||
}
|
||||
|
||||
private static DateTimeOffset ResolveLastModified(Assembly assembly)
|
||||
{
|
||||
var result = DateTimeOffset.UtcNow;
|
||||
|
||||
if (!string.IsNullOrEmpty(assembly.Location))
|
||||
{
|
||||
try
|
||||
{
|
||||
result = File.GetLastWriteTimeUtc(assembly.Location);
|
||||
}
|
||||
catch (PathTooLongException)
|
||||
{
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<RootNamespace>Microsoft.Extensions.FileProviders</RootNamespace>
|
||||
<Description>File provider for files in embedded resources for Microsoft.Extensions.FileProviders.</Description>
|
||||
<TargetFrameworks>netstandard2.0;$(DefaultNetCoreTargetFramework)</TargetFrameworks>
|
||||
<NuspecFile>$(MSBuildProjectName).multitarget.nuspec</NuspecFile>
|
||||
<TargetFrameworks Condition="'$(DotNetBuildFromSource)' == 'true'">$(DefaultNetCoreTargetFramework)</TargetFrameworks>
|
||||
<NuspecFile Condition="'$(DotNetBuildFromSource)' == 'true'">$(MSBuildProjectName).netcoreapp.nuspec</NuspecFile>
|
||||
<IsPackable>true</IsPackable>
|
||||
<IsShipping>true</IsShipping>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="Microsoft.Extensions.FileProviders.Embedded.Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.Extensions.FileProviders.Abstractions" />
|
||||
<ProjectReference Include="..\..\Manifest.MSBuildTask\src\Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.csproj" PrivateAssets="All" ReferenceOutputAssembly="false" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<SignedPackageFile Include="$(TargetPath)" Certificate="$(AssemblySigningCertName)" />
|
||||
<SignedPackageFile Include="Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.dll" Certificate="$(AssemblySigningCertName)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<NuspecProperty Include="AssemblyName=$(AssemblyName)" />
|
||||
<NuspecProperty Include="OutputPath=$(OutputPath)" />
|
||||
<NuspecProperty Include="TaskAssemblyNetStandard=$(ArtifactsDir)bin\$(AssemblyName).Manifest.Task\$(Configuration)\netstandard2.0\$(AssemblyName).Manifest.Task.dll"/>
|
||||
<NuspecProperty Include="TaskSymbolNetStandard=$(ArtifactsDir)bin\$(AssemblyName).Manifest.Task\$(Configuration)\netstandard2.0\$(AssemblyName).Manifest.Task.pdb" Condition="'$(DebugType)'!='embedded'"/>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd">
|
||||
<metadata>
|
||||
$CommonMetadataElements$
|
||||
<dependencies>
|
||||
<group targetFramework=".NETCoreApp3.1">
|
||||
<dependency id="Microsoft.Extensions.FileProviders.Abstractions" version="$version$" exclude="Build,Analyzers" />
|
||||
</group>
|
||||
<group targetFramework=".NETStandard2.0">
|
||||
<dependency id="Microsoft.Extensions.FileProviders.Abstractions" version="$version$" exclude="Build,Analyzers" />
|
||||
</group>
|
||||
</dependencies>
|
||||
</metadata>
|
||||
|
||||
<files>
|
||||
$CommonFileElements$
|
||||
<file src="$OutputPath$**\$AssemblyName$.dll" target="lib\" />
|
||||
<file src="$OutputPath$**\$AssemblyName$.pdb" target="lib\" />
|
||||
<file src="$OutputPath$**\$AssemblyName$.xml" target="lib\" />
|
||||
<file src="build\**\*" target="build\" />
|
||||
<file src="buildMultiTargeting\**\*" target="buildMultiTargeting\" />
|
||||
<file src="$TaskAssemblyNetStandard$" target="tasks\netstandard2.0\$AssemblyName$.Manifest.Task.dll" />
|
||||
<file src="$TaskSymbolNetStandard$" target="tasks\netstandard2.0\$AssemblyName$.Manifest.Task.pdb" />
|
||||
</files>
|
||||
</package>
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd">
|
||||
<metadata>
|
||||
$CommonMetadataElements$
|
||||
<dependencies>
|
||||
<group targetFramework=".NETCoreApp3.1">
|
||||
<dependency id="Microsoft.Extensions.FileProviders.Abstractions" version="$version$" exclude="Build,Analyzers" />
|
||||
</group>
|
||||
</dependencies>
|
||||
</metadata>
|
||||
|
||||
<files>
|
||||
$CommonFileElements$
|
||||
<file src="$OutputPath$**\$AssemblyName$.dll" target="lib\" />
|
||||
<file src="$OutputPath$**\$AssemblyName$.pdb" target="lib\" />
|
||||
<file src="$OutputPath$**\$AssemblyName$.xml" target="lib\" />
|
||||
<file src="build\**\*" target="build\" />
|
||||
<file src="buildMultiTargeting\**\*" target="buildMultiTargeting\" />
|
||||
<file src="$TaskAssemblyNetStandard$" target="tasks\netstandard2.0\$AssemblyName$.Manifest.Task.dll" />
|
||||
<file src="$TaskSymbolNetStandard$" target="tasks\netstandard2.0\$AssemblyName$.Manifest.Task.pdb" />
|
||||
</files>
|
||||
</package>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<Project TreatAsLocalProperty="_FileProviderTaskFolder;_FileProviderTaskAssembly">
|
||||
<PropertyGroup>
|
||||
<GenerateEmbeddedFilesManifest Condition="'$(GenerateEmbeddedFilesManifest)' == ''">false</GenerateEmbeddedFilesManifest>
|
||||
<EmbeddedFilesManifestFileName Condition="'$(EmbeddedFilesManifestFileName)' == ''">Microsoft.Extensions.FileProviders.Embedded.Manifest.xml</EmbeddedFilesManifestFileName>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<_FileProviderTaskAssembly>$(MSBuildThisFileDirectory)..\..\tasks\netstandard2.0\Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.dll</_FileProviderTaskAssembly>
|
||||
</PropertyGroup>
|
||||
|
||||
<UsingTask
|
||||
AssemblyFile="$(_FileProviderTaskAssembly)"
|
||||
TaskName="Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.GenerateEmbeddedResourcesManifest" />
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
<Project>
|
||||
<PropertyGroup>
|
||||
<PrepareResourceNamesDependsOn>_CalculateEmbeddedFilesManifestInputs;$(PrepareResourceNamesDependsOn)</PrepareResourceNamesDependsOn>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target
|
||||
Name="_CalculateEmbeddedFilesManifestInputs"
|
||||
Condition="'$(GenerateEmbeddedFilesManifest)' == 'true'">
|
||||
|
||||
<PropertyGroup>
|
||||
<_GeneratedManifestFile>$(IntermediateOutputPath)$(EmbeddedFilesManifestFileName)</_GeneratedManifestFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<_FilesForManifest Include="@(EmbeddedResource)" />
|
||||
<_FilesForManifest Remove="@(EmbeddedResource->WithMetadataValue('ExcludeFromManifest','true'))" />
|
||||
</ItemGroup>
|
||||
|
||||
<Warning
|
||||
Text="GenerateEmbeddedFilesManifest was set, but no EmbeddedResource items were found that could be added to the manifest."
|
||||
Condition="@(_FilesForManifest->Count()) == 0" />
|
||||
|
||||
<ItemGroup Condition="@(_FilesForManifest->Count()) != 0">
|
||||
<EmbeddedResource
|
||||
Include="$(_GeneratedManifestFile)"
|
||||
LogicalName="$(EmbeddedFilesManifestFileName)" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
<Target Name="_CreateGeneratedManifestInfoInputsCacheFile" DependsOnTargets="_CalculateEmbeddedFilesManifestInputs">
|
||||
<PropertyGroup>
|
||||
<_GeneratedManifestInfoInputsCacheFile>$(IntermediateOutputPath)$(MSBuildProjectName).EmbeddedFilesManifest.cache</_GeneratedManifestInfoInputsCacheFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<Hash ItemsToHash="@(_FilesForManifest)">
|
||||
<Output TaskParameter="HashResult" PropertyName="_EmbeddedGeneratedManifestHash" />
|
||||
</Hash>
|
||||
|
||||
<WriteLinesToFile
|
||||
Lines="$(_EmbeddedGeneratedManifestHash)"
|
||||
File="$(_GeneratedManifestInfoInputsCacheFile)"
|
||||
Overwrite="True"
|
||||
WriteOnlyWhenDifferent="True" />
|
||||
|
||||
<ItemGroup>
|
||||
<FileWrites Include="$(_GeneratedManifestInfoInputsCacheFile)" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
<Target
|
||||
Name="_GenerateEmbeddedFilesManifest"
|
||||
DependsOnTargets="_CreateGeneratedManifestInfoInputsCacheFile"
|
||||
AfterTargets="PrepareResourceNames"
|
||||
Condition="'$(GenerateEmbeddedFilesManifest)' == 'true' AND @(_FilesForManifest->Count()) != 0"
|
||||
Inputs="$(_GeneratedManifestInfoInputsCacheFile)"
|
||||
Outputs="$(_GeneratedManifestFile)">
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Rebuild _FilesForManifest since PrepareResourceNames would have updated EmbeddedResource. -->
|
||||
<_FilesForManifest Remove="@(_FilesForManifest)" />
|
||||
<_FilesForManifest Include="@(EmbeddedResource)" />
|
||||
<_FilesForManifest Remove="@(EmbeddedResource->WithMetadataValue('ExcludeFromManifest','true'))" />
|
||||
</ItemGroup>
|
||||
|
||||
<GenerateEmbeddedResourcesManifest
|
||||
EmbeddedFiles="@(_FilesForManifest)"
|
||||
ManifestFile="$(_GeneratedManifestFile)" />
|
||||
</Target>
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<Project>
|
||||
<Import Project="..\build\netstandard2.0\Microsoft.Extensions.FileProviders.Embedded.props" />
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<Project>
|
||||
<Import Project="..\build\netstandard2.0\Microsoft.Extensions.FileProviders.Embedded.targets" />
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,231 @@
|
|||
// 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 Microsoft.AspNetCore.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Extensions.FileProviders.Embedded.Tests
|
||||
{
|
||||
public class EmbeddedFileProviderTests
|
||||
{
|
||||
private static readonly string Namespace = typeof(EmbeddedFileProviderTests).Namespace;
|
||||
|
||||
[Fact]
|
||||
public void ConstructorWithNullAssemblyThrowsArgumentException()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => new EmbeddedFileProvider(null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetFileInfo_ReturnsNotFoundFileInfo_IfFileDoesNotExist()
|
||||
{
|
||||
// Arrange
|
||||
var provider = new EmbeddedFileProvider(GetType().GetTypeInfo().Assembly);
|
||||
|
||||
// Act
|
||||
var fileInfo = provider.GetFileInfo("DoesNotExist.Txt");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(fileInfo);
|
||||
Assert.False(fileInfo.Exists);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("File.txt")]
|
||||
[InlineData("/File.txt")]
|
||||
public void GetFileInfo_ReturnsFilesAtRoot(string filePath)
|
||||
{
|
||||
// Arrange
|
||||
var provider = new EmbeddedFileProvider(GetType().GetTypeInfo().Assembly);
|
||||
var expectedFileLength = 8;
|
||||
|
||||
// Act
|
||||
var fileInfo = provider.GetFileInfo(filePath);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(fileInfo);
|
||||
Assert.True(fileInfo.Exists);
|
||||
Assert.NotEqual(default(DateTimeOffset), fileInfo.LastModified);
|
||||
Assert.Equal(expectedFileLength, fileInfo.Length);
|
||||
Assert.False(fileInfo.IsDirectory);
|
||||
Assert.Null(fileInfo.PhysicalPath);
|
||||
Assert.Equal("File.txt", fileInfo.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetFileInfo_ReturnsNotFoundFileInfo_IfFileDoesNotExistUnderSpecifiedNamespace()
|
||||
{
|
||||
// Arrange
|
||||
var provider = new EmbeddedFileProvider(GetType().GetTypeInfo().Assembly, Namespace + ".SubNamespace");
|
||||
|
||||
// Act
|
||||
var fileInfo = provider.GetFileInfo("File.txt");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(fileInfo);
|
||||
Assert.False(fileInfo.Exists);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetFileInfo_ReturnsNotFoundIfPathStartsWithBackSlash()
|
||||
{
|
||||
// Arrange
|
||||
var provider = new EmbeddedFileProvider(GetType().GetTypeInfo().Assembly);
|
||||
|
||||
// Act
|
||||
var fileInfo = provider.GetFileInfo("\\File.txt");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(fileInfo);
|
||||
Assert.False(fileInfo.Exists);
|
||||
}
|
||||
|
||||
public static TheoryData GetFileInfo_LocatesFilesUnderSpecifiedNamespaceData
|
||||
{
|
||||
get
|
||||
{
|
||||
var theoryData = new TheoryData<string>
|
||||
{
|
||||
"ResourcesInSubdirectory/File3.txt"
|
||||
};
|
||||
|
||||
if (TestPlatformHelper.IsWindows)
|
||||
{
|
||||
theoryData.Add("ResourcesInSubdirectory\\File3.txt");
|
||||
}
|
||||
|
||||
return theoryData;
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(GetFileInfo_LocatesFilesUnderSpecifiedNamespaceData))]
|
||||
public void GetFileInfo_LocatesFilesUnderSpecifiedNamespace(string path)
|
||||
{
|
||||
// Arrange
|
||||
var provider = new EmbeddedFileProvider(GetType().GetTypeInfo().Assembly, Namespace + ".Resources");
|
||||
|
||||
// Act
|
||||
var fileInfo = provider.GetFileInfo(path);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(fileInfo);
|
||||
Assert.True(fileInfo.Exists);
|
||||
Assert.NotEqual(default(DateTimeOffset), fileInfo.LastModified);
|
||||
Assert.True(fileInfo.Length > 0);
|
||||
Assert.False(fileInfo.IsDirectory);
|
||||
Assert.Null(fileInfo.PhysicalPath);
|
||||
Assert.Equal("File3.txt", fileInfo.Name);
|
||||
}
|
||||
|
||||
public static TheoryData GetFileInfo_LocatesFilesUnderSubDirectoriesData
|
||||
{
|
||||
get
|
||||
{
|
||||
var theoryData = new TheoryData<string>
|
||||
{
|
||||
"Resources/File.txt"
|
||||
};
|
||||
|
||||
if (TestPlatformHelper.IsWindows)
|
||||
{
|
||||
theoryData.Add("Resources\\File.txt");
|
||||
}
|
||||
|
||||
return theoryData;
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(GetFileInfo_LocatesFilesUnderSubDirectoriesData))]
|
||||
public void GetFileInfo_LocatesFilesUnderSubDirectories(string path)
|
||||
{
|
||||
// Arrange
|
||||
var provider = new EmbeddedFileProvider(GetType().GetTypeInfo().Assembly);
|
||||
|
||||
// Act
|
||||
var fileInfo = provider.GetFileInfo(path);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(fileInfo);
|
||||
Assert.True(fileInfo.Exists);
|
||||
Assert.NotEqual(default(DateTimeOffset), fileInfo.LastModified);
|
||||
Assert.True(fileInfo.Length > 0);
|
||||
Assert.False(fileInfo.IsDirectory);
|
||||
Assert.Null(fileInfo.PhysicalPath);
|
||||
Assert.Equal("File.txt", fileInfo.Name);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData("/")]
|
||||
public void GetDirectoryContents_ReturnsAllFilesInFileSystem(string path)
|
||||
{
|
||||
// Arrange
|
||||
var provider = new EmbeddedFileProvider(GetType().GetTypeInfo().Assembly, Namespace + ".Resources");
|
||||
|
||||
// Act
|
||||
var files = provider.GetDirectoryContents(path);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(files.OrderBy(f => f.Name, StringComparer.Ordinal),
|
||||
file => Assert.Equal("File.txt", file.Name),
|
||||
file => Assert.Equal("ResourcesInSubdirectory.File3.txt", file.Name));
|
||||
|
||||
Assert.False(provider.GetDirectoryContents("file").Exists);
|
||||
Assert.False(provider.GetDirectoryContents("file/").Exists);
|
||||
Assert.False(provider.GetDirectoryContents("file.txt").Exists);
|
||||
Assert.False(provider.GetDirectoryContents("file/txt").Exists);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDirectoryContents_ReturnsEmptySequence_IfResourcesDoNotExistUnderNamespace()
|
||||
{
|
||||
// Arrange
|
||||
var provider = new EmbeddedFileProvider(GetType().GetTypeInfo().Assembly, "Unknown.Namespace");
|
||||
|
||||
// Act
|
||||
var files = provider.GetDirectoryContents(string.Empty);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(files);
|
||||
Assert.True(files.Exists);
|
||||
Assert.Empty(files);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Resources")]
|
||||
[InlineData("/Resources")]
|
||||
public void GetDirectoryContents_ReturnsNotFoundDirectoryContents_IfHierarchicalPathIsSpecified(string path)
|
||||
{
|
||||
// Arrange
|
||||
var provider = new EmbeddedFileProvider(GetType().GetTypeInfo().Assembly);
|
||||
|
||||
// Act
|
||||
var files = provider.GetDirectoryContents(path);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(files);
|
||||
Assert.False(files.Exists);
|
||||
Assert.Empty(files);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Watch_ReturnsNoOpTrigger()
|
||||
{
|
||||
// Arange
|
||||
var provider = new EmbeddedFileProvider(GetType().GetTypeInfo().Assembly);
|
||||
|
||||
// Act
|
||||
var token = provider.Watch("Resources/File.txt");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(token);
|
||||
Assert.False(token.ActiveChangeCallbacks);
|
||||
Assert.False(token.HasChanged);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
Hello
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.Extensions.FileProviders
|
||||
{
|
||||
internal class FileInfoComparer : IEqualityComparer<IFileInfo>
|
||||
{
|
||||
public static FileInfoComparer Instance { get; set; } = new FileInfoComparer();
|
||||
|
||||
public bool Equals(IFileInfo x, IFileInfo y)
|
||||
{
|
||||
if (x == null && y == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((x == null && y != null) || (x != null && y == null))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return x.Exists == y.Exists &&
|
||||
x.IsDirectory == y.IsDirectory &&
|
||||
x.Length == y.Length &&
|
||||
string.Equals(x.Name, y.Name, StringComparison.Ordinal) &&
|
||||
string.Equals(x.PhysicalPath, y.PhysicalPath, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
public int GetHashCode(IFileInfo obj) => 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
// 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 Xunit;
|
||||
|
||||
namespace Microsoft.Extensions.FileProviders.Embedded.Manifest
|
||||
{
|
||||
public class EmbeddedFilesManifestTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("/wwwroot//jquery.validate.js")]
|
||||
[InlineData("//wwwroot/jquery.validate.js")]
|
||||
public void ResolveEntry_IgnoresInvalidPaths(string path)
|
||||
{
|
||||
// Arrange
|
||||
var manifest = new EmbeddedFilesManifest(
|
||||
ManifestDirectory.CreateRootDirectory(
|
||||
new[]
|
||||
{
|
||||
ManifestDirectory.CreateDirectory("wwwroot",
|
||||
new[]
|
||||
{
|
||||
new ManifestFile("jquery.validate.js","wwwroot.jquery.validate.js")
|
||||
})
|
||||
}));
|
||||
// Act
|
||||
var entry = manifest.ResolveEntry(path);
|
||||
|
||||
// Assert
|
||||
Assert.Null(entry);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("/")]
|
||||
[InlineData("./")]
|
||||
[InlineData("/wwwroot/jquery.validate.js")]
|
||||
[InlineData("/wwwroot/")]
|
||||
public void ResolveEntry_AllowsSingleDirectorySeparator(string path)
|
||||
{
|
||||
// Arrange
|
||||
var manifest = new EmbeddedFilesManifest(
|
||||
ManifestDirectory.CreateRootDirectory(
|
||||
new[]
|
||||
{
|
||||
ManifestDirectory.CreateDirectory("wwwroot",
|
||||
new[]
|
||||
{
|
||||
new ManifestFile("jquery.validate.js","wwwroot.jquery.validate.js")
|
||||
})
|
||||
}));
|
||||
// Act
|
||||
var entry = manifest.ResolveEntry(path);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Extensions.FileProviders.Embedded.Manifest
|
||||
{
|
||||
public class ManifestEntryTests
|
||||
{
|
||||
[Fact]
|
||||
public void TraversingAFile_ReturnsUnknownPath()
|
||||
{
|
||||
// Arrange
|
||||
var file = new ManifestFile("a", "a.b.c");
|
||||
|
||||
// Act
|
||||
var result = file.Traverse(".");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(ManifestEntry.UnknownPath, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TraversingANonExistingFile_ReturnsUnknownPath()
|
||||
{
|
||||
// Arrange
|
||||
var directory = ManifestDirectory.CreateDirectory("a", Array.Empty<ManifestEntry>());
|
||||
|
||||
// Act
|
||||
var result = directory.Traverse("missing.txt");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(ManifestEntry.UnknownPath, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TraversingWithDot_ReturnsSelf()
|
||||
{
|
||||
// Arrange
|
||||
var directory = ManifestDirectory.CreateDirectory("a", Array.Empty<ManifestEntry>());
|
||||
|
||||
// Act
|
||||
var result = directory.Traverse(".");
|
||||
|
||||
// Assert
|
||||
Assert.Same(directory, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TraversingWithDotDot_ReturnsParent()
|
||||
{
|
||||
// Arrange
|
||||
var childDirectory = ManifestDirectory.CreateDirectory("b", Array.Empty<ManifestEntry>());
|
||||
var directory = ManifestDirectory.CreateDirectory("a", new[] { childDirectory });
|
||||
|
||||
// Act
|
||||
var result = childDirectory.Traverse("..");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(directory, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TraversingRootDirectoryWithDotDot_ReturnsSinkDirectory()
|
||||
{
|
||||
// Arrange
|
||||
var directory = ManifestDirectory.CreateRootDirectory(Array.Empty<ManifestEntry>());
|
||||
|
||||
// Act
|
||||
var result = directory.Traverse("..");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(ManifestEntry.UnknownPath, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScopingAFolderAndTryingToGetAScopedFile_ReturnsSinkDirectory()
|
||||
{
|
||||
// Arrange
|
||||
var directory = ManifestDirectory.CreateRootDirectory(new[] {
|
||||
ManifestDirectory.CreateDirectory("a",
|
||||
new[] { new ManifestFile("test1.txt", "text.txt") }),
|
||||
ManifestDirectory.CreateDirectory("b",
|
||||
new[] { new ManifestFile("test2.txt", "test2.txt") }) });
|
||||
|
||||
var newRoot = ((ManifestDirectory)directory.Traverse("a")).ToRootDirectory();
|
||||
|
||||
// Act
|
||||
var result = newRoot.Traverse("../b/test.txt");
|
||||
|
||||
// Assert
|
||||
Assert.Same(ManifestEntry.UnknownPath, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("..")]
|
||||
[InlineData(".")]
|
||||
[InlineData("file.txt")]
|
||||
[InlineData("folder")]
|
||||
public void TraversingUnknownPath_ReturnsSinkDirectory(string path)
|
||||
{
|
||||
// Arrange
|
||||
var directory = ManifestEntry.UnknownPath;
|
||||
|
||||
// Act
|
||||
var result = directory.Traverse(path);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(ManifestEntry.UnknownPath, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 Xunit;
|
||||
|
||||
namespace Microsoft.Extensions.FileProviders.Embedded.Manifest
|
||||
{
|
||||
public class ManifestParserTests
|
||||
{
|
||||
[Fact]
|
||||
public void Parse_UsesDefaultManifestNameForManifest()
|
||||
{
|
||||
// Arrange
|
||||
var assembly = new TestAssembly(
|
||||
TestEntry.Directory("unused",
|
||||
TestEntry.File("sample.txt")));
|
||||
|
||||
// Act
|
||||
var manifest = ManifestParser.Parse(assembly);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(manifest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_FindsManifestWithCustomName()
|
||||
{
|
||||
// Arrange
|
||||
var assembly = new TestAssembly(
|
||||
TestEntry.Directory("unused",
|
||||
TestEntry.File("sample.txt")),
|
||||
manifestName: "Manifest.xml");
|
||||
|
||||
// Act
|
||||
var manifest = ManifestParser.Parse(assembly, "Manifest.xml");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(manifest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_ThrowsForEntriesWithDifferentCasing()
|
||||
{
|
||||
// Arrange
|
||||
var assembly = new TestAssembly(
|
||||
TestEntry.Directory("unused",
|
||||
TestEntry.File("sample.txt"),
|
||||
TestEntry.File("SAMPLE.TXT")));
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<InvalidOperationException>(() => ManifestParser.Parse(assembly));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(MalformedManifests))]
|
||||
public void Parse_ThrowsForInvalidManifests(string invalidManifest)
|
||||
{
|
||||
// Arrange
|
||||
var assembly = new TestAssembly(invalidManifest);
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<InvalidOperationException>(() => ManifestParser.Parse(assembly));
|
||||
}
|
||||
|
||||
public static TheoryData<string> MalformedManifests =>
|
||||
new TheoryData<string>
|
||||
{
|
||||
"<Manifest></Manifest>",
|
||||
"<Manifest><ManifestVersion></ManifestVersion></Manifest>",
|
||||
"<Manifest><ManifestVersion /></Manifest>",
|
||||
"<Manifest><ManifestVersion><Version>2.0</Version></ManifestVersion></Manifest>",
|
||||
"<Manifest><ManifestVersion>2.0</ManifestVersion></Manifest>",
|
||||
@"<Manifest><ManifestVersion>1.0</ManifestVersion>
|
||||
<FileSystem><File><ResourcePath>path</ResourcePath></File></FileSystem></Manifest>",
|
||||
|
||||
@"<Manifest><ManifestVersion>1.0</ManifestVersion>
|
||||
<FileSystem><File Name=""sample.txt""><ResourcePath></ResourcePath></File></FileSystem></Manifest>",
|
||||
|
||||
@"<Manifest><ManifestVersion>1.0</ManifestVersion>
|
||||
<FileSystem><File Name=""sample.txt"">sample.txt</File></FileSystem></Manifest>",
|
||||
|
||||
@"<Manifest><ManifestVersion>1.0</ManifestVersion>
|
||||
<FileSystem><Directory></Directory></FileSystem></Manifest>",
|
||||
|
||||
@"<Manifest><ManifestVersion>1.0</ManifestVersion>
|
||||
<FileSystem><Directory Name=""wwwroot""><Unknown /></Directory></FileSystem></Manifest>"
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ManifestsWithAdditionalData))]
|
||||
public void Parse_IgnoresAdditionalDataOnFileAndDirectoryNodes(string manifest)
|
||||
{
|
||||
// Arrange
|
||||
var assembly = new TestAssembly(manifest);
|
||||
|
||||
// Act
|
||||
var result = ManifestParser.Parse(assembly);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
|
||||
public static TheoryData<string> ManifestsWithAdditionalData =>
|
||||
new TheoryData<string>
|
||||
{
|
||||
@"<Manifest><ManifestVersion>1.0</ManifestVersion>
|
||||
<FileSystem><Directory Name=""wwwroot"" AdditionalAttribute=""value""></Directory></FileSystem></Manifest>",
|
||||
|
||||
@"<Manifest><ManifestVersion>1.0</ManifestVersion>
|
||||
<FileSystem><Directory Name=""wwwroot"" AdditionalAttribute=""value"">
|
||||
<File Name=""sample.txt"" AdditionalValue=""value""><ResourcePath something=""abc"">path</ResourcePath><hash>1234</hash></File>
|
||||
</Directory></FileSystem></Manifest>"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Microsoft.Extensions.FileProviders.Embedded.Manifest
|
||||
{
|
||||
class TestEntry
|
||||
{
|
||||
public bool IsFile => ResourcePath != null;
|
||||
public string Name { get; set; }
|
||||
public TestEntry[] Children { get; set; }
|
||||
public string ResourcePath { get; set; }
|
||||
|
||||
public static TestEntry Directory(string name, params TestEntry[] entries) =>
|
||||
new TestEntry() { Name = name, Children = entries };
|
||||
|
||||
public static TestEntry File(string name, string path = null) =>
|
||||
new TestEntry() { Name = name, ResourcePath = path ?? name };
|
||||
|
||||
public XElement ToXElement() => IsFile ?
|
||||
new XElement("File", new XAttribute("Name", Name), new XElement("ResourcePath", ResourcePath)) :
|
||||
new XElement("Directory", new XAttribute("Name", Name), Children.Select(c => c.ToXElement()));
|
||||
|
||||
public IEnumerable<TestEntry> GetFiles()
|
||||
{
|
||||
if (IsFile)
|
||||
{
|
||||
return Enumerable.Empty<TestEntry>();
|
||||
}
|
||||
|
||||
var files = Children.Where(c => c.IsFile).ToArray();
|
||||
var otherFiles = Children.Where(c => !c.IsFile).SelectMany(d => d.GetFiles()).ToArray();
|
||||
|
||||
return files.Concat(otherFiles).ToArray();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,428 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.FileProviders.Embedded.Manifest;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Extensions.FileProviders
|
||||
{
|
||||
public class ManifestEmbeddedFileProviderTests
|
||||
{
|
||||
[Fact]
|
||||
public void GetFileInfo_CanResolveSimpleFiles()
|
||||
{
|
||||
// Arrange
|
||||
var assembly = new TestAssembly(
|
||||
TestEntry.Directory("unused",
|
||||
TestEntry.File("jquery.validate.js"),
|
||||
TestEntry.File("jquery.min.js"),
|
||||
TestEntry.File("site.css")));
|
||||
|
||||
// Act
|
||||
var provider = new ManifestEmbeddedFileProvider(assembly);
|
||||
|
||||
// Assert
|
||||
var jqueryValidate = provider.GetFileInfo("jquery.validate.js");
|
||||
Assert.True(jqueryValidate.Exists);
|
||||
Assert.False(jqueryValidate.IsDirectory);
|
||||
Assert.Equal("jquery.validate.js", jqueryValidate.Name);
|
||||
Assert.Null(jqueryValidate.PhysicalPath);
|
||||
Assert.Equal(0, jqueryValidate.Length);
|
||||
|
||||
var jqueryMin = provider.GetFileInfo("jquery.min.js");
|
||||
Assert.True(jqueryMin.Exists);
|
||||
Assert.False(jqueryMin.IsDirectory);
|
||||
Assert.Equal("jquery.min.js", jqueryMin.Name);
|
||||
Assert.Null(jqueryMin.PhysicalPath);
|
||||
Assert.Equal(0, jqueryMin.Length);
|
||||
|
||||
var siteCss = provider.GetFileInfo("site.css");
|
||||
Assert.True(siteCss.Exists);
|
||||
Assert.False(siteCss.IsDirectory);
|
||||
Assert.Equal("site.css", siteCss.Name);
|
||||
Assert.Null(siteCss.PhysicalPath);
|
||||
Assert.Equal(0, siteCss.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetFileInfo_CanResolveFilesInsideAFolder()
|
||||
{
|
||||
// Arrange
|
||||
var assembly = new TestAssembly(
|
||||
TestEntry.Directory("unused",
|
||||
TestEntry.Directory("wwwroot",
|
||||
TestEntry.File("jquery.validate.js"),
|
||||
TestEntry.File("jquery.min.js"),
|
||||
TestEntry.File("site.css"))));
|
||||
|
||||
// Act
|
||||
var provider = new ManifestEmbeddedFileProvider(assembly);
|
||||
|
||||
// Assert
|
||||
var jqueryValidate = provider.GetFileInfo(Path.Combine("wwwroot", "jquery.validate.js"));
|
||||
Assert.True(jqueryValidate.Exists);
|
||||
Assert.False(jqueryValidate.IsDirectory);
|
||||
Assert.Equal("jquery.validate.js", jqueryValidate.Name);
|
||||
Assert.Null(jqueryValidate.PhysicalPath);
|
||||
Assert.Equal(0, jqueryValidate.Length);
|
||||
|
||||
var jqueryMin = provider.GetFileInfo(Path.Combine("wwwroot", "jquery.min.js"));
|
||||
Assert.True(jqueryMin.Exists);
|
||||
Assert.False(jqueryMin.IsDirectory);
|
||||
Assert.Equal("jquery.min.js", jqueryMin.Name);
|
||||
Assert.Null(jqueryMin.PhysicalPath);
|
||||
Assert.Equal(0, jqueryMin.Length);
|
||||
|
||||
var siteCss = provider.GetFileInfo(Path.Combine("wwwroot", "site.css"));
|
||||
Assert.True(siteCss.Exists);
|
||||
Assert.False(siteCss.IsDirectory);
|
||||
Assert.Equal("site.css", siteCss.Name);
|
||||
Assert.Null(siteCss.PhysicalPath);
|
||||
Assert.Equal(0, siteCss.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetFileInfo_ResolveNonExistingFile_ReturnsNotFoundFileInfo()
|
||||
{
|
||||
// Arrange
|
||||
var assembly = new TestAssembly(
|
||||
TestEntry.Directory("unused",
|
||||
TestEntry.Directory("wwwroot",
|
||||
TestEntry.File("jquery.validate.js"),
|
||||
TestEntry.File("jquery.min.js"),
|
||||
TestEntry.File("site.css"))));
|
||||
|
||||
var provider = new ManifestEmbeddedFileProvider(assembly);
|
||||
|
||||
// Act
|
||||
var file = provider.GetFileInfo("some/non/existing/file.txt");
|
||||
|
||||
// Assert
|
||||
Assert.IsType<NotFoundFileInfo>(file);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetFileInfo_ResolveNonExistingDirectory_ReturnsNotFoundFileInfo()
|
||||
{
|
||||
// Arrange
|
||||
var assembly = new TestAssembly(
|
||||
TestEntry.Directory("unused",
|
||||
TestEntry.Directory("wwwroot",
|
||||
TestEntry.File("jquery.validate.js"),
|
||||
TestEntry.File("jquery.min.js"),
|
||||
TestEntry.File("site.css"))));
|
||||
|
||||
var provider = new ManifestEmbeddedFileProvider(assembly);
|
||||
|
||||
// Act
|
||||
var file = provider.GetFileInfo("some");
|
||||
|
||||
// Assert
|
||||
Assert.IsType<NotFoundFileInfo>(file);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetFileInfo_ResolveExistingDirectory_ReturnsNotFoundFileInfo()
|
||||
{
|
||||
// Arrange
|
||||
var assembly = new TestAssembly(
|
||||
TestEntry.Directory("unused",
|
||||
TestEntry.Directory("wwwroot",
|
||||
TestEntry.File("jquery.validate.js"),
|
||||
TestEntry.File("jquery.min.js"),
|
||||
TestEntry.File("site.css"))));
|
||||
|
||||
var provider = new ManifestEmbeddedFileProvider(assembly);
|
||||
|
||||
// Act
|
||||
var file = provider.GetFileInfo("wwwroot");
|
||||
|
||||
// Assert
|
||||
Assert.IsType<NotFoundFileInfo>(file);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("WWWROOT", "JQUERY.VALIDATE.JS")]
|
||||
[InlineData("WwWRoOT", "JQuERY.VALiDATE.js")]
|
||||
public void GetFileInfo_ResolvesFiles_WithDifferentCasing(string folder, string file)
|
||||
{
|
||||
// Arrange
|
||||
var assembly = new TestAssembly(
|
||||
TestEntry.Directory("unused",
|
||||
TestEntry.Directory("wwwroot",
|
||||
TestEntry.File("jquery.validate.js"),
|
||||
TestEntry.File("jquery.min.js"),
|
||||
TestEntry.File("site.css"))));
|
||||
|
||||
// Act
|
||||
var provider = new ManifestEmbeddedFileProvider(assembly);
|
||||
|
||||
// Assert
|
||||
var jqueryValidate = provider.GetFileInfo(Path.Combine(folder, file));
|
||||
Assert.True(jqueryValidate.Exists);
|
||||
Assert.False(jqueryValidate.IsDirectory);
|
||||
Assert.Equal("jquery.validate.js", jqueryValidate.Name);
|
||||
Assert.Null(jqueryValidate.PhysicalPath);
|
||||
Assert.Equal(0, jqueryValidate.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetFileInfo_AllowsLeadingDots_OnThePath()
|
||||
{
|
||||
// Arrange
|
||||
var assembly = new TestAssembly(
|
||||
TestEntry.Directory("unused",
|
||||
TestEntry.Directory("wwwroot",
|
||||
TestEntry.File("jquery.validate.js"),
|
||||
TestEntry.File("jquery.min.js"),
|
||||
TestEntry.File("site.css"))));
|
||||
|
||||
// Act
|
||||
var provider = new ManifestEmbeddedFileProvider(assembly);
|
||||
|
||||
// Assert
|
||||
var jqueryValidate = provider.GetFileInfo(Path.Combine(".", "wwwroot", "jquery.validate.js"));
|
||||
Assert.True(jqueryValidate.Exists);
|
||||
Assert.False(jqueryValidate.IsDirectory);
|
||||
Assert.Equal("jquery.validate.js", jqueryValidate.Name);
|
||||
Assert.Null(jqueryValidate.PhysicalPath);
|
||||
Assert.Equal(0, jqueryValidate.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetFileInfo_EscapingFromTheRootFolder_ReturnsNotFound()
|
||||
{
|
||||
// Arrange
|
||||
var assembly = new TestAssembly(
|
||||
TestEntry.Directory("unused",
|
||||
TestEntry.Directory("wwwroot",
|
||||
TestEntry.File("jquery.validate.js"),
|
||||
TestEntry.File("jquery.min.js"),
|
||||
TestEntry.File("site.css"))));
|
||||
|
||||
// Act
|
||||
var provider = new ManifestEmbeddedFileProvider(assembly);
|
||||
|
||||
// Assert
|
||||
var jqueryValidate = provider.GetFileInfo(Path.Combine("..", "wwwroot", "jquery.validate.js"));
|
||||
Assert.IsType<NotFoundFileInfo>(jqueryValidate);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("wwwroot/jquery?validate.js")]
|
||||
[InlineData("wwwroot/jquery*validate.js")]
|
||||
[InlineData("wwwroot/jquery:validate.js")]
|
||||
[InlineData("wwwroot/jquery<validate.js")]
|
||||
[InlineData("wwwroot/jquery>validate.js")]
|
||||
[InlineData("wwwroot/jquery\0validate.js")]
|
||||
public void GetFileInfo_ReturnsNotFoundfileInfo_ForPathsWithInvalidCharacters(string path)
|
||||
{
|
||||
// Arrange
|
||||
var assembly = new TestAssembly(
|
||||
TestEntry.Directory("unused",
|
||||
TestEntry.Directory("wwwroot",
|
||||
TestEntry.File("jquery.validate.js"),
|
||||
TestEntry.File("jquery.min.js"),
|
||||
TestEntry.File("site.css"))));
|
||||
|
||||
// Act
|
||||
var provider = new ManifestEmbeddedFileProvider(assembly);
|
||||
|
||||
// Assert
|
||||
var file = provider.GetFileInfo(path);
|
||||
Assert.IsType<NotFoundFileInfo>(file);
|
||||
Assert.Equal(path, file.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDirectoryContents_CanEnumerateExistingFolders()
|
||||
{
|
||||
// Arrange
|
||||
var assembly = new TestAssembly(
|
||||
TestEntry.Directory("unused",
|
||||
TestEntry.Directory("wwwroot",
|
||||
TestEntry.File("jquery.validate.js"),
|
||||
TestEntry.File("jquery.min.js"),
|
||||
TestEntry.File("site.css"))));
|
||||
|
||||
var provider = new ManifestEmbeddedFileProvider(assembly);
|
||||
|
||||
var expectedContents = new[]
|
||||
{
|
||||
CreateTestFileInfo("jquery.validate.js"),
|
||||
CreateTestFileInfo("jquery.min.js"),
|
||||
CreateTestFileInfo("site.css")
|
||||
};
|
||||
|
||||
// Act
|
||||
var contents = provider.GetDirectoryContents("wwwroot").ToArray();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedContents, contents, FileInfoComparer.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDirectoryContents_EnumeratesOnlyAGivenLevel()
|
||||
{
|
||||
// Arrange
|
||||
var assembly = new TestAssembly(
|
||||
TestEntry.Directory("unused",
|
||||
TestEntry.Directory("wwwroot",
|
||||
TestEntry.File("jquery.validate.js"),
|
||||
TestEntry.File("jquery.min.js"),
|
||||
TestEntry.File("site.css"))));
|
||||
|
||||
var provider = new ManifestEmbeddedFileProvider(assembly);
|
||||
|
||||
var expectedContents = new[]
|
||||
{
|
||||
CreateTestFileInfo("wwwroot", isDirectory: true)
|
||||
};
|
||||
|
||||
// Act
|
||||
var contents = provider.GetDirectoryContents(".").ToArray();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedContents, contents, FileInfoComparer.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDirectoryContents_EnumeratesFilesAndDirectoriesOnAGivenPath()
|
||||
{
|
||||
// Arrange
|
||||
var assembly = new TestAssembly(
|
||||
TestEntry.Directory("unused",
|
||||
TestEntry.Directory("wwwroot"),
|
||||
TestEntry.File("site.css")));
|
||||
|
||||
var provider = new ManifestEmbeddedFileProvider(assembly);
|
||||
|
||||
var expectedContents = new[]
|
||||
{
|
||||
CreateTestFileInfo("wwwroot", isDirectory: true),
|
||||
CreateTestFileInfo("site.css")
|
||||
};
|
||||
|
||||
// Act
|
||||
var contents = provider.GetDirectoryContents(".").ToArray();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedContents, contents, FileInfoComparer.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDirectoryContents_ReturnsNoEntries_ForNonExistingDirectories()
|
||||
{
|
||||
// Arrange
|
||||
var assembly = new TestAssembly(
|
||||
TestEntry.Directory("unused",
|
||||
TestEntry.Directory("wwwroot"),
|
||||
TestEntry.File("site.css")));
|
||||
|
||||
var provider = new ManifestEmbeddedFileProvider(assembly);
|
||||
|
||||
// Act
|
||||
var contents = provider.GetDirectoryContents("non-existing");
|
||||
|
||||
// Assert
|
||||
Assert.IsType<NotFoundDirectoryContents>(contents);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDirectoryContents_ReturnsNoEntries_ForFilePaths()
|
||||
{
|
||||
// Arrange
|
||||
var assembly = new TestAssembly(
|
||||
TestEntry.Directory("unused",
|
||||
TestEntry.Directory("wwwroot"),
|
||||
TestEntry.File("site.css")));
|
||||
|
||||
var provider = new ManifestEmbeddedFileProvider(assembly);
|
||||
|
||||
// Act
|
||||
var contents = provider.GetDirectoryContents("site.css");
|
||||
|
||||
// Assert
|
||||
Assert.IsType<NotFoundDirectoryContents>(contents);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("wwwro*t")]
|
||||
[InlineData("wwwro?t")]
|
||||
[InlineData("wwwro:t")]
|
||||
[InlineData("wwwro<t")]
|
||||
[InlineData("wwwro>t")]
|
||||
[InlineData("wwwro\0t")]
|
||||
public void GetDirectoryContents_ReturnsNotFoundDirectoryContents_ForPathsWithInvalidCharacters(string path)
|
||||
{
|
||||
// Arrange
|
||||
var assembly = new TestAssembly(
|
||||
TestEntry.Directory("unused",
|
||||
TestEntry.Directory("wwwroot",
|
||||
TestEntry.File("jquery.validate.js"),
|
||||
TestEntry.File("jquery.min.js"),
|
||||
TestEntry.File("site.css"))));
|
||||
|
||||
// Act
|
||||
var provider = new ManifestEmbeddedFileProvider(assembly);
|
||||
|
||||
// Assert
|
||||
var directory = provider.GetDirectoryContents(path);
|
||||
Assert.IsType<NotFoundDirectoryContents>(directory);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Contructor_CanScopeManifestToAFolder()
|
||||
{
|
||||
// Arrange
|
||||
var assembly = new TestAssembly(
|
||||
TestEntry.Directory("unused",
|
||||
TestEntry.Directory("wwwroot",
|
||||
TestEntry.File("jquery.validate.js")),
|
||||
TestEntry.File("site.css")));
|
||||
|
||||
var provider = new ManifestEmbeddedFileProvider(assembly);
|
||||
var scopedProvider = new ManifestEmbeddedFileProvider(assembly, provider.Manifest.Scope("wwwroot"), DateTimeOffset.UtcNow);
|
||||
|
||||
// Act
|
||||
var jqueryValidate = scopedProvider.GetFileInfo("jquery.validate.js");
|
||||
|
||||
// Assert
|
||||
Assert.True(jqueryValidate.Exists);
|
||||
Assert.False(jqueryValidate.IsDirectory);
|
||||
Assert.Equal("jquery.validate.js", jqueryValidate.Name);
|
||||
Assert.Null(jqueryValidate.PhysicalPath);
|
||||
Assert.Equal(0, jqueryValidate.Length);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("wwwroot/jquery.validate.js")]
|
||||
[InlineData("../wwwroot/jquery.validate.js")]
|
||||
[InlineData("site.css")]
|
||||
[InlineData("../site.css")]
|
||||
public void ScopedFileProvider_DoesNotReturnFilesOutOfScope(string path)
|
||||
{
|
||||
// Arrange
|
||||
var assembly = new TestAssembly(
|
||||
TestEntry.Directory("unused",
|
||||
TestEntry.Directory("wwwroot",
|
||||
TestEntry.File("jquery.validate.js")),
|
||||
TestEntry.File("site.css")));
|
||||
|
||||
var provider = new ManifestEmbeddedFileProvider(assembly);
|
||||
var scopedProvider = new ManifestEmbeddedFileProvider(assembly, provider.Manifest.Scope("wwwroot"), DateTimeOffset.UtcNow);
|
||||
|
||||
// Act
|
||||
var jqueryValidate = scopedProvider.GetFileInfo(path);
|
||||
|
||||
// Assert
|
||||
Assert.IsType<NotFoundFileInfo>(jqueryValidate);
|
||||
}
|
||||
|
||||
private IFileInfo CreateTestFileInfo(string name, bool isDirectory = false) =>
|
||||
new TestFileInfo(name, isDirectory);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>$(DefaultNetCoreTargetFramework);net472</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="File.txt;sub\**\*;Resources\**\*" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.Extensions.FileProviders.Abstractions" />
|
||||
<Reference Include="Microsoft.Extensions.FileProviders.Embedded" />
|
||||
<Reference Include="Microsoft.Extensions.Primitives" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1 @@
|
|||
Resources-Hello
|
||||
|
|
@ -0,0 +1 @@
|
|||
Hello3
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
// 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.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.Extensions.FileProviders.Embedded.Manifest;
|
||||
|
||||
namespace Microsoft.Extensions.FileProviders
|
||||
{
|
||||
internal class TestAssembly : Assembly
|
||||
{
|
||||
public TestAssembly(string manifest, string manifestName = "Microsoft.Extensions.FileProviders.Embedded.Manifest.xml")
|
||||
{
|
||||
ManifestStream = new MemoryStream();
|
||||
using (var writer = new StreamWriter(ManifestStream, Encoding.UTF8, 1024, leaveOpen: true))
|
||||
{
|
||||
writer.Write(manifest);
|
||||
}
|
||||
|
||||
ManifestStream.Seek(0, SeekOrigin.Begin);
|
||||
ManifestName = manifestName;
|
||||
}
|
||||
|
||||
public TestAssembly(TestEntry entry, string manifestName = "Microsoft.Extensions.FileProviders.Embedded.Manifest.xml")
|
||||
{
|
||||
ManifestName = manifestName;
|
||||
|
||||
var manifest = new XDocument(
|
||||
new XDeclaration("1.0", "utf-8", "yes"),
|
||||
new XElement("Manifest",
|
||||
new XElement("ManifestVersion", "1.0"),
|
||||
new XElement("FileSystem", entry.Children.Select(c => c.ToXElement()))));
|
||||
|
||||
ManifestStream = new MemoryStream();
|
||||
using (var writer = XmlWriter.Create(ManifestStream, new XmlWriterSettings { CloseOutput = false }))
|
||||
{
|
||||
manifest.WriteTo(writer);
|
||||
}
|
||||
|
||||
ManifestStream.Seek(0, SeekOrigin.Begin);
|
||||
Files = entry.GetFiles().Select(f => f.ResourcePath).ToArray();
|
||||
}
|
||||
|
||||
public string ManifestName { get; }
|
||||
public MemoryStream ManifestStream { get; private set; }
|
||||
public string[] Files { get; private set; }
|
||||
|
||||
public override Stream GetManifestResourceStream(string name)
|
||||
{
|
||||
if (string.Equals(ManifestName, name))
|
||||
{
|
||||
return ManifestStream;
|
||||
}
|
||||
|
||||
return Files.Contains(name) ? Stream.Null : null;
|
||||
}
|
||||
|
||||
public override string Location => null;
|
||||
|
||||
public override AssemblyName GetName()
|
||||
{
|
||||
return new AssemblyName("TestAssembly");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Microsoft.Extensions.FileProviders
|
||||
{
|
||||
internal class TestFileInfo : IFileInfo
|
||||
{
|
||||
private readonly string _name;
|
||||
private readonly bool _isDirectory;
|
||||
|
||||
public TestFileInfo(string name, bool isDirectory)
|
||||
{
|
||||
_name = name;
|
||||
_isDirectory = isDirectory;
|
||||
}
|
||||
|
||||
public bool Exists => true;
|
||||
|
||||
public long Length => _isDirectory ? -1 : 0;
|
||||
|
||||
public string PhysicalPath => null;
|
||||
|
||||
public string Name => _name;
|
||||
|
||||
public DateTimeOffset LastModified => throw new NotImplementedException();
|
||||
|
||||
public bool IsDirectory => _isDirectory;
|
||||
|
||||
public Stream CreateReadStream() => Stream.Null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
Hello2
|
||||
Binary file not shown.
|
|
@ -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.Extensions.FileProviders.Embedded.Manifest.Task
|
||||
{
|
||||
public class EmbeddedItem : IEquatable<EmbeddedItem>
|
||||
{
|
||||
public string ManifestFilePath { get; set; }
|
||||
|
||||
public string AssemblyResourceName { get; set; }
|
||||
|
||||
public bool Equals(EmbeddedItem other) =>
|
||||
string.Equals(ManifestFilePath, other?.ManifestFilePath, StringComparison.Ordinal) &&
|
||||
string.Equals(AssemblyResourceName, other?.AssemblyResourceName, StringComparison.Ordinal);
|
||||
|
||||
public override bool Equals(object obj) => Equals(obj as EmbeddedItem);
|
||||
public override int GetHashCode() => ManifestFilePath.GetHashCode() ^ AssemblyResourceName.GetHashCode();
|
||||
}
|
||||
}
|
||||
|
|
@ -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.Generic;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.Internal
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{Name,nq}")]
|
||||
public class Entry : IEquatable<Entry>
|
||||
{
|
||||
public bool IsFile { get; private set; }
|
||||
|
||||
public string Name { get; private set; }
|
||||
|
||||
public string AssemblyResourceName { get; private set; }
|
||||
|
||||
public ISet<Entry> Children { get; } = new SortedSet<Entry>(NameComparer.Instance);
|
||||
|
||||
public static Entry Directory(string name) =>
|
||||
new Entry { Name = name };
|
||||
|
||||
public static Entry File(string name, string assemblyResourceName) =>
|
||||
new Entry { Name = name, AssemblyResourceName = assemblyResourceName, IsFile = true };
|
||||
|
||||
internal void AddChild(Entry child)
|
||||
{
|
||||
if (IsFile)
|
||||
{
|
||||
throw new InvalidOperationException("Tried to add children to a file.");
|
||||
}
|
||||
|
||||
if (Children.Contains(child))
|
||||
{
|
||||
throw new InvalidOperationException($"An item with the name '{child.Name}' already exists.");
|
||||
}
|
||||
|
||||
Children.Add(child);
|
||||
}
|
||||
|
||||
internal Entry GetDirectory(string currentSegment)
|
||||
{
|
||||
if (IsFile)
|
||||
{
|
||||
throw new InvalidOperationException("Tried to get a directory from a file.");
|
||||
}
|
||||
|
||||
foreach (var child in Children)
|
||||
{
|
||||
if (child.HasName(currentSegment))
|
||||
{
|
||||
if (child.IsFile)
|
||||
{
|
||||
throw new InvalidOperationException("Tried to find a directory but found a file instead");
|
||||
}
|
||||
else
|
||||
{
|
||||
return child;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public bool Equals(Entry other)
|
||||
{
|
||||
if (other == null || !other.HasName(Name) || other.IsFile != IsFile)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (IsFile)
|
||||
{
|
||||
return string.Equals(other.AssemblyResourceName, AssemblyResourceName, StringComparison.Ordinal);
|
||||
}
|
||||
else
|
||||
{
|
||||
return SameChildren(Children, other.Children);
|
||||
}
|
||||
}
|
||||
|
||||
private bool HasName(string currentSegment)
|
||||
{
|
||||
return string.Equals(Name, currentSegment, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private bool SameChildren(ISet<Entry> left, ISet<Entry> right)
|
||||
{
|
||||
if (left.Count != right.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var le = left.GetEnumerator();
|
||||
var re = right.GetEnumerator();
|
||||
while (le.MoveNext() && re.MoveNext())
|
||||
{
|
||||
if (!le.Current.Equals(re.Current))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private class NameComparer : IComparer<Entry>
|
||||
{
|
||||
public static NameComparer Instance { get; } = new NameComparer();
|
||||
|
||||
public int Compare(Entry x, Entry y) =>
|
||||
string.Compare(x?.Name, y?.Name, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
using Microsoft.Build.Framework;
|
||||
|
||||
namespace Microsoft.Extensions.FileProviders.Embedded.Manifest.Task
|
||||
{
|
||||
/// <summary>
|
||||
/// Task for generating a manifest file out of the embedded resources in an
|
||||
/// assembly.
|
||||
/// </summary>
|
||||
public class GenerateEmbeddedResourcesManifest : Microsoft.Build.Utilities.Task
|
||||
{
|
||||
private const string LogicalName = "LogicalName";
|
||||
private const string ManifestResourceName = "ManifestResourceName";
|
||||
private const string TargetPath = "TargetPath";
|
||||
|
||||
[Required]
|
||||
public ITaskItem[] EmbeddedFiles { get; set; }
|
||||
|
||||
[Required]
|
||||
public string ManifestFile { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Execute()
|
||||
{
|
||||
var processedItems = CreateEmbeddedItems(EmbeddedFiles);
|
||||
|
||||
var manifest = BuildManifest(processedItems);
|
||||
|
||||
var document = manifest.ToXmlDocument();
|
||||
|
||||
var settings = new XmlWriterSettings()
|
||||
{
|
||||
Encoding = Encoding.UTF8,
|
||||
CloseOutput = true
|
||||
};
|
||||
|
||||
using (var xmlWriter = GetXmlWriter(settings))
|
||||
{
|
||||
document.WriteTo(xmlWriter);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected virtual XmlWriter GetXmlWriter(XmlWriterSettings settings)
|
||||
{
|
||||
if (settings == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(settings));
|
||||
}
|
||||
|
||||
var fileStream = new FileStream(ManifestFile, FileMode.Create);
|
||||
return XmlWriter.Create(fileStream, settings);
|
||||
}
|
||||
|
||||
public EmbeddedItem[] CreateEmbeddedItems(params ITaskItem[] items)
|
||||
{
|
||||
if (items == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(items));
|
||||
}
|
||||
|
||||
return items.Select(er => new EmbeddedItem
|
||||
{
|
||||
ManifestFilePath = GetManifestPath(er),
|
||||
AssemblyResourceName = GetAssemblyResourceName(er)
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
public Manifest BuildManifest(EmbeddedItem[] processedItems)
|
||||
{
|
||||
if (processedItems == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(processedItems));
|
||||
}
|
||||
|
||||
var manifest = new Manifest();
|
||||
foreach (var item in processedItems)
|
||||
{
|
||||
manifest.AddElement(item.ManifestFilePath, item.AssemblyResourceName);
|
||||
}
|
||||
|
||||
return manifest;
|
||||
}
|
||||
|
||||
private string GetManifestPath(ITaskItem taskItem) => string.Equals(taskItem.GetMetadata(LogicalName), taskItem.GetMetadata(ManifestResourceName)) ?
|
||||
taskItem.GetMetadata(TargetPath) :
|
||||
NormalizePath(taskItem.GetMetadata(LogicalName));
|
||||
|
||||
private string GetAssemblyResourceName(ITaskItem taskItem) => string.Equals(taskItem.GetMetadata(LogicalName), taskItem.GetMetadata(ManifestResourceName)) ?
|
||||
taskItem.GetMetadata(ManifestResourceName) :
|
||||
taskItem.GetMetadata(LogicalName);
|
||||
|
||||
private string NormalizePath(string path) => Path.DirectorySeparatorChar == '\\' ?
|
||||
path.Replace("/", "\\") : path.Replace("\\", "/");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.Internal;
|
||||
|
||||
namespace Microsoft.Extensions.FileProviders.Embedded.Manifest.Task
|
||||
{
|
||||
public class Manifest
|
||||
{
|
||||
public Entry Root { get; set; } = Entry.Directory("");
|
||||
|
||||
public void AddElement(string originalPath, string assemblyResourceName)
|
||||
{
|
||||
if (originalPath == null)
|
||||
{
|
||||
throw new System.ArgumentNullException(nameof(originalPath));
|
||||
}
|
||||
|
||||
if (assemblyResourceName == null)
|
||||
{
|
||||
throw new System.ArgumentNullException(nameof(assemblyResourceName));
|
||||
}
|
||||
|
||||
var paths = originalPath.Split(Path.DirectorySeparatorChar);
|
||||
var current = Root;
|
||||
for (int i = 0; i < paths.Length - 1; i++)
|
||||
{
|
||||
var currentSegment = paths[i];
|
||||
var next = current.GetDirectory(currentSegment);
|
||||
if (next == null)
|
||||
{
|
||||
next = Entry.Directory(currentSegment);
|
||||
current.AddChild(next);
|
||||
}
|
||||
current = next;
|
||||
}
|
||||
|
||||
current.AddChild(Entry.File(paths[paths.Length - 1], assemblyResourceName));
|
||||
}
|
||||
|
||||
public XDocument ToXmlDocument()
|
||||
{
|
||||
var document = new XDocument(new XDeclaration("1.0", "utf-8", "yes"));
|
||||
var root = new XElement(ElementNames.Root,
|
||||
new XElement(ElementNames.ManifestVersion, "1.0"),
|
||||
new XElement(ElementNames.FileSystem,
|
||||
Root.Children.Select(e => BuildNode(e))));
|
||||
|
||||
document.Add(root);
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
private XElement BuildNode(Entry entry)
|
||||
{
|
||||
if (entry.IsFile)
|
||||
{
|
||||
return new XElement(ElementNames.File,
|
||||
new XAttribute(ElementNames.Name, entry.Name),
|
||||
new XElement(ElementNames.ResourcePath, entry.AssemblyResourceName));
|
||||
}
|
||||
else
|
||||
{
|
||||
var directory = new XElement(ElementNames.Directory, new XAttribute(ElementNames.Name, entry.Name));
|
||||
directory.Add(entry.Children.Select(c => BuildNode(c)));
|
||||
return directory;
|
||||
}
|
||||
}
|
||||
|
||||
private class ElementNames
|
||||
{
|
||||
public static readonly string Directory = "Directory";
|
||||
public static readonly string Name = "Name";
|
||||
public static readonly string FileSystem = "FileSystem";
|
||||
public static readonly string Root = "Manifest";
|
||||
public static readonly string File = "File";
|
||||
public static readonly string ResourcePath = "ResourcePath";
|
||||
public static readonly string ManifestVersion = "ManifestVersion";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>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.</Description>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||
<IsShippingAssembly>true</IsShippingAssembly>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsImplementationProject>false</IsImplementationProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.Build.Framework" />
|
||||
<Reference Include="Microsoft.Build.Utilities.Core" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,388 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.Build.Framework;
|
||||
using Microsoft.Build.Utilities;
|
||||
using Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.Internal;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Extensions.FileProviders.Embedded.Manifest.Task
|
||||
{
|
||||
public class GenerateEmbeddedResourcesManifestTest
|
||||
{
|
||||
[Fact]
|
||||
public void CreateEmbeddedItems_MapsMetadataFromEmbeddedResources_UsesTheTargetPath()
|
||||
{
|
||||
// Arrange
|
||||
var task = new TestGenerateEmbeddedResourcesManifest();
|
||||
var embeddedFiles = CreateEmbeddedResource(
|
||||
CreateMetadata(@"lib\js\jquery.validate.js"));
|
||||
|
||||
var expectedItems = new[]
|
||||
{
|
||||
CreateEmbeddedItem(@"lib\js\jquery.validate.js","lib.js.jquery.validate.js")
|
||||
};
|
||||
|
||||
// Act
|
||||
var embeddedItems = task.CreateEmbeddedItems(embeddedFiles);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedItems, embeddedItems);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateEmbeddedItems_MapsMetadataFromEmbeddedResources_WithLogicalName()
|
||||
{
|
||||
// Arrange
|
||||
var task = new TestGenerateEmbeddedResourcesManifest();
|
||||
var DirectorySeparator = (Path.DirectorySeparatorChar == '\\' ? '/' : '\\');
|
||||
var embeddedFiles = CreateEmbeddedResource(
|
||||
CreateMetadata("site.css", null, "site.css"),
|
||||
CreateMetadata("lib/jquery.validate.js", null, $"dist{DirectorySeparator}jquery.validate.js"));
|
||||
|
||||
var expectedItems = new[]
|
||||
{
|
||||
CreateEmbeddedItem("site.css","site.css"),
|
||||
CreateEmbeddedItem(Path.Combine("dist","jquery.validate.js"),$"dist{DirectorySeparator}jquery.validate.js")
|
||||
};
|
||||
|
||||
// Act
|
||||
var embeddedItems = task.CreateEmbeddedItems(embeddedFiles);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedItems, embeddedItems);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildManifest_CanCreatesManifest_ForTopLevelFiles()
|
||||
{
|
||||
// Arrange
|
||||
var task = new TestGenerateEmbeddedResourcesManifest();
|
||||
var embeddedFiles = CreateEmbeddedResource(
|
||||
CreateMetadata("jquery.validate.js"),
|
||||
CreateMetadata("jquery.min.js"),
|
||||
CreateMetadata("Site.css"));
|
||||
|
||||
var manifestFiles = task.CreateEmbeddedItems(embeddedFiles);
|
||||
|
||||
var expectedManifest = new Manifest()
|
||||
{
|
||||
Root = Entry.Directory("").AddRange(
|
||||
Entry.File("jquery.validate.js", "jquery.validate.js"),
|
||||
Entry.File("jquery.min.js", "jquery.min.js"),
|
||||
Entry.File("Site.css", "Site.css"))
|
||||
};
|
||||
|
||||
// Act
|
||||
var manifest = task.BuildManifest(manifestFiles);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedManifest, manifest, ManifestComparer.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildManifest_CanCreatesManifest_ForFilesWithinAFolder()
|
||||
{
|
||||
// Arrange
|
||||
var task = new TestGenerateEmbeddedResourcesManifest();
|
||||
var embeddedFiles = CreateEmbeddedResource(
|
||||
CreateMetadata(Path.Combine("wwwroot", "js", "jquery.validate.js")),
|
||||
CreateMetadata(Path.Combine("wwwroot", "js", "jquery.min.js")),
|
||||
CreateMetadata(Path.Combine("wwwroot", "css", "Site.css")),
|
||||
CreateMetadata(Path.Combine("Areas", "Identity", "Views", "Account", "Index.cshtml")));
|
||||
|
||||
var manifestFiles = task.CreateEmbeddedItems(embeddedFiles);
|
||||
|
||||
var expectedManifest = new Manifest()
|
||||
{
|
||||
Root = Entry.Directory("").AddRange(
|
||||
Entry.Directory("wwwroot").AddRange(
|
||||
Entry.Directory("js").AddRange(
|
||||
Entry.File("jquery.validate.js", "wwwroot.js.jquery.validate.js"),
|
||||
Entry.File("jquery.min.js", "wwwroot.js.jquery.min.js")),
|
||||
Entry.Directory("css").AddRange(
|
||||
Entry.File("Site.css", "wwwroot.css.Site.css"))),
|
||||
Entry.Directory("Areas").AddRange(
|
||||
Entry.Directory("Identity").AddRange(
|
||||
Entry.Directory("Views").AddRange(
|
||||
Entry.Directory("Account").AddRange(
|
||||
Entry.File("Index.cshtml", "Areas.Identity.Views.Account.Index.cshtml"))))))
|
||||
};
|
||||
|
||||
// Act
|
||||
var manifest = task.BuildManifest(manifestFiles);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedManifest, manifest, ManifestComparer.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildManifest_RespectsEntriesWithLogicalName()
|
||||
{
|
||||
// Arrange
|
||||
var task = new TestGenerateEmbeddedResourcesManifest();
|
||||
var embeddedFiles = CreateEmbeddedResource(
|
||||
CreateMetadata("jquery.validate.js", null, @"wwwroot\lib\js\jquery.validate.js"),
|
||||
CreateMetadata("jquery.min.js", null, @"wwwroot\lib/js\jquery.min.js"),
|
||||
CreateMetadata("Site.css", null, "wwwroot/lib/css/site.css"));
|
||||
var manifestFiles = task.CreateEmbeddedItems(embeddedFiles);
|
||||
|
||||
var expectedManifest = new Manifest()
|
||||
{
|
||||
Root = Entry.Directory("").AddRange(
|
||||
Entry.Directory("wwwroot").AddRange(
|
||||
Entry.Directory("lib").AddRange(
|
||||
Entry.Directory("js").AddRange(
|
||||
Entry.File("jquery.validate.js", @"wwwroot\lib\js\jquery.validate.js"),
|
||||
Entry.File("jquery.min.js", @"wwwroot\lib/js\jquery.min.js")),
|
||||
Entry.Directory("css").AddRange(
|
||||
Entry.File("site.css", "wwwroot/lib/css/site.css")))))
|
||||
};
|
||||
|
||||
// Act
|
||||
var manifest = task.BuildManifest(manifestFiles);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedManifest, manifest, ManifestComparer.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildManifest_SupportsFilesAndFoldersWithDifferentCasing()
|
||||
{
|
||||
// Arrange
|
||||
var task = new TestGenerateEmbeddedResourcesManifest();
|
||||
var embeddedFiles = CreateEmbeddedResource(
|
||||
CreateMetadata(Path.Combine("A", "b", "c.txt")),
|
||||
CreateMetadata(Path.Combine("A", "B", "c.txt")),
|
||||
CreateMetadata(Path.Combine("A", "B", "C.txt")),
|
||||
CreateMetadata(Path.Combine("A", "b", "C.txt")),
|
||||
CreateMetadata(Path.Combine("A", "d")),
|
||||
CreateMetadata(Path.Combine("A", "D", "e.txt")));
|
||||
|
||||
var manifestFiles = task.CreateEmbeddedItems(embeddedFiles);
|
||||
|
||||
var expectedManifest = new Manifest()
|
||||
{
|
||||
Root = Entry.Directory("").AddRange(
|
||||
Entry.Directory("A").AddRange(
|
||||
Entry.Directory("b").AddRange(
|
||||
Entry.File("c.txt", @"A.b.c.txt"),
|
||||
Entry.File("C.txt", @"A.b.C.txt")),
|
||||
Entry.Directory("B").AddRange(
|
||||
Entry.File("c.txt", @"A.B.c.txt"),
|
||||
Entry.File("C.txt", @"A.B.C.txt")),
|
||||
Entry.Directory("D").AddRange(
|
||||
Entry.File("e.txt", "A.D.e.txt")),
|
||||
Entry.File("d", "A.d")))
|
||||
};
|
||||
|
||||
// Act
|
||||
var manifest = task.BuildManifest(manifestFiles);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedManifest, manifest, ManifestComparer.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildManifest_ThrowsInvalidOperationException_WhenTryingToAddAFileWithTheSameNameAsAFolder()
|
||||
{
|
||||
// Arrange
|
||||
var task = new TestGenerateEmbeddedResourcesManifest();
|
||||
var embeddedFiles = CreateEmbeddedResource(
|
||||
CreateMetadata(Path.Combine("A", "b", "c.txt")),
|
||||
CreateMetadata(Path.Combine("A", "b")));
|
||||
|
||||
var manifestFiles = task.CreateEmbeddedItems(embeddedFiles);
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<InvalidOperationException>(() => task.BuildManifest(manifestFiles));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildManifest_ThrowsInvalidOperationException_WhenTryingToAddAFolderWithTheSameNameAsAFile()
|
||||
{
|
||||
// Arrange
|
||||
var task = new TestGenerateEmbeddedResourcesManifest();
|
||||
var embeddedFiles = CreateEmbeddedResource(
|
||||
CreateMetadata(Path.Combine("A", "b")),
|
||||
CreateMetadata(Path.Combine("A", "b", "c.txt")));
|
||||
|
||||
var manifestFiles = task.CreateEmbeddedItems(embeddedFiles);
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<InvalidOperationException>(() => task.BuildManifest(manifestFiles));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToXmlDocument_GeneratesTheCorrectXmlDocument()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = new Manifest()
|
||||
{
|
||||
Root = Entry.Directory("").AddRange(
|
||||
Entry.Directory("A").AddRange(
|
||||
Entry.Directory("b").AddRange(
|
||||
Entry.File("c.txt", @"A.b.c.txt"),
|
||||
Entry.File("C.txt", @"A.b.C.txt")),
|
||||
Entry.Directory("B").AddRange(
|
||||
Entry.File("c.txt", @"A.B.c.txt"),
|
||||
Entry.File("C.txt", @"A.B.C.txt")),
|
||||
Entry.Directory("D").AddRange(
|
||||
Entry.File("e.txt", "A.D.e.txt")),
|
||||
Entry.File("d", "A.d")))
|
||||
};
|
||||
|
||||
var expectedDocument = new XDocument(
|
||||
new XDeclaration("1.0", "utf-8", "yes"),
|
||||
new XElement("Manifest",
|
||||
new XElement("ManifestVersion", "1.0"),
|
||||
new XElement("FileSystem",
|
||||
new XElement("Directory", new XAttribute("Name", "A"),
|
||||
new XElement("Directory", new XAttribute("Name", "B"),
|
||||
new XElement("File", new XAttribute("Name", "C.txt"), new XElement("ResourcePath", "A.B.C.txt")),
|
||||
new XElement("File", new XAttribute("Name", "c.txt"), new XElement("ResourcePath", "A.B.c.txt"))),
|
||||
new XElement("Directory", new XAttribute("Name", "D"),
|
||||
new XElement("File", new XAttribute("Name", "e.txt"), new XElement("ResourcePath", "A.D.e.txt"))),
|
||||
new XElement("Directory", new XAttribute("Name", "b"),
|
||||
new XElement("File", new XAttribute("Name", "C.txt"), new XElement("ResourcePath", "A.b.C.txt")),
|
||||
new XElement("File", new XAttribute("Name", "c.txt"), new XElement("ResourcePath", "A.b.c.txt"))),
|
||||
new XElement("File", new XAttribute("Name", "d"), new XElement("ResourcePath", "A.d"))))));
|
||||
|
||||
// Act
|
||||
var document = manifest.ToXmlDocument();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedDocument.ToString(), document.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Execute_WritesManifest_ToOutputFile()
|
||||
{
|
||||
// Arrange
|
||||
var task = new TestGenerateEmbeddedResourcesManifest();
|
||||
var embeddedFiles = CreateEmbeddedResource(
|
||||
CreateMetadata(Path.Combine("A", "b", "c.txt")),
|
||||
CreateMetadata(Path.Combine("A", "B", "c.txt")),
|
||||
CreateMetadata(Path.Combine("A", "B", "C.txt")),
|
||||
CreateMetadata(Path.Combine("A", "b", "C.txt")),
|
||||
CreateMetadata(Path.Combine("A", "d")),
|
||||
CreateMetadata(Path.Combine("A", "D", "e.txt")));
|
||||
|
||||
task.EmbeddedFiles = embeddedFiles;
|
||||
task.ManifestFile = Path.Combine("obj", "debug", "netstandard2.0");
|
||||
|
||||
var expectedDocument = new XDocument(
|
||||
new XDeclaration("1.0", "utf-8", "yes"),
|
||||
new XElement("Manifest",
|
||||
new XElement("ManifestVersion", "1.0"),
|
||||
new XElement("FileSystem",
|
||||
new XElement("Directory", new XAttribute("Name", "A"),
|
||||
new XElement("Directory", new XAttribute("Name", "B"),
|
||||
new XElement("File", new XAttribute("Name", "C.txt"), new XElement("ResourcePath", "A.B.C.txt")),
|
||||
new XElement("File", new XAttribute("Name", "c.txt"), new XElement("ResourcePath", "A.B.c.txt"))),
|
||||
new XElement("Directory", new XAttribute("Name", "D"),
|
||||
new XElement("File", new XAttribute("Name", "e.txt"), new XElement("ResourcePath", "A.D.e.txt"))),
|
||||
new XElement("Directory", new XAttribute("Name", "b"),
|
||||
new XElement("File", new XAttribute("Name", "C.txt"), new XElement("ResourcePath", "A.b.C.txt")),
|
||||
new XElement("File", new XAttribute("Name", "c.txt"), new XElement("ResourcePath", "A.b.c.txt"))),
|
||||
new XElement("File", new XAttribute("Name", "d"), new XElement("ResourcePath", "A.d"))))));
|
||||
|
||||
var expectedOutput = new MemoryStream();
|
||||
var writer = XmlWriter.Create(expectedOutput, new XmlWriterSettings { Encoding = Encoding.UTF8 });
|
||||
expectedDocument.WriteTo(writer);
|
||||
writer.Flush();
|
||||
expectedOutput.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
// Act
|
||||
task.Execute();
|
||||
|
||||
// Assert
|
||||
task.Output.Seek(0, SeekOrigin.Begin);
|
||||
using (var expectedReader = new StreamReader(expectedOutput))
|
||||
{
|
||||
using (var reader = new StreamReader(task.Output))
|
||||
{
|
||||
Assert.Equal(expectedReader.ReadToEnd(), reader.ReadToEnd());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private EmbeddedItem CreateEmbeddedItem(string manifestPath, string assemblyName) =>
|
||||
new EmbeddedItem
|
||||
{
|
||||
ManifestFilePath = manifestPath,
|
||||
AssemblyResourceName = assemblyName
|
||||
};
|
||||
|
||||
|
||||
public class TestGenerateEmbeddedResourcesManifest
|
||||
: GenerateEmbeddedResourcesManifest
|
||||
{
|
||||
public TestGenerateEmbeddedResourcesManifest()
|
||||
: this(new MemoryStream())
|
||||
{
|
||||
}
|
||||
|
||||
public TestGenerateEmbeddedResourcesManifest(Stream output)
|
||||
{
|
||||
Output = output;
|
||||
}
|
||||
|
||||
public Stream Output { get; }
|
||||
|
||||
protected override XmlWriter GetXmlWriter(XmlWriterSettings settings)
|
||||
{
|
||||
settings.CloseOutput = false;
|
||||
return XmlWriter.Create(Output, settings);
|
||||
}
|
||||
}
|
||||
|
||||
private ITaskItem[] CreateEmbeddedResource(params IDictionary<string, string>[] files) =>
|
||||
files.Select(f => CreateTaskItem(f)).ToArray();
|
||||
|
||||
private ITaskItem CreateTaskItem(IDictionary<string, string> metadata)
|
||||
{
|
||||
var result = new TaskItem();
|
||||
foreach (var kvp in metadata)
|
||||
{
|
||||
result.SetMetadata(kvp.Key, kvp.Value);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static IDictionary<string, string>
|
||||
CreateMetadata(
|
||||
string targetPath,
|
||||
string manifestResourceName = null,
|
||||
string logicalName = null) =>
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
["TargetPath"] = targetPath,
|
||||
["ManifestResourceName"] = manifestResourceName ?? targetPath.Replace("/", ".").Replace("\\", "."),
|
||||
["LogicalName"] = logicalName ?? targetPath.Replace("/", ".").Replace("\\", "."),
|
||||
};
|
||||
|
||||
private class ManifestComparer : IEqualityComparer<Manifest>
|
||||
{
|
||||
public static IEqualityComparer<Manifest> Instance { get; } = new ManifestComparer();
|
||||
|
||||
public bool Equals(Manifest x, Manifest y)
|
||||
{
|
||||
return x.Root.Equals(y.Root);
|
||||
}
|
||||
|
||||
public int GetHashCode(Manifest obj)
|
||||
{
|
||||
return obj.Root.GetHashCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\src\Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.Build.Framework" />
|
||||
<Reference Include="Microsoft.Build.Utilities.Core" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.Internal;
|
||||
|
||||
namespace Microsoft.Extensions.FileProviders.Embedded.Manifest.Task
|
||||
{
|
||||
internal static class SetExtensions
|
||||
{
|
||||
public static Entry AddRange(this Entry source, params Entry[] elements)
|
||||
{
|
||||
foreach (var element in elements)
|
||||
{
|
||||
source.Children.Add(element);
|
||||
}
|
||||
|
||||
return source;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<!-- This file is automatically generated. -->
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;$(DefaultNetCoreTargetFramework)</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
|
||||
<Compile Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions.netstandard2.0.cs" />
|
||||
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)' == '$(DefaultNetCoreTargetFramework)'">
|
||||
<Compile Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions.netcoreapp.cs" />
|
||||
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
// 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<string> tags) { }
|
||||
public HealthCheckRegistration(string name, Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck instance, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus, System.Collections.Generic.IEnumerable<string> tags, System.TimeSpan? timeout) { }
|
||||
public HealthCheckRegistration(string name, System.Func<System.IServiceProvider, Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck> factory, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus, System.Collections.Generic.IEnumerable<string> tags) { }
|
||||
public HealthCheckRegistration(string name, System.Func<System.IServiceProvider, Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck> factory, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus, System.Collections.Generic.IEnumerable<string> tags, System.TimeSpan? timeout) { }
|
||||
public System.Func<System.IServiceProvider, Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck> 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<string> 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<string, object> data = null) { throw null; }
|
||||
public System.Collections.Generic.IReadOnlyDictionary<string, object> 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<string, object> data = null) { throw null; }
|
||||
public static Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult Healthy(string description = null, System.Collections.Generic.IReadOnlyDictionary<string, object> data = null) { throw null; }
|
||||
public static Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult Unhealthy(string description = null, System.Exception exception = null, System.Collections.Generic.IReadOnlyDictionary<string, object> data = null) { throw null; }
|
||||
}
|
||||
public sealed partial class HealthReport
|
||||
{
|
||||
public HealthReport(System.Collections.Generic.IReadOnlyDictionary<string, Microsoft.Extensions.Diagnostics.HealthChecks.HealthReportEntry> entries, System.TimeSpan totalDuration) { }
|
||||
public System.Collections.Generic.IReadOnlyDictionary<string, Microsoft.Extensions.Diagnostics.HealthChecks.HealthReportEntry> 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<string, object> data) { throw null; }
|
||||
public HealthReportEntry(Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus status, string description, System.TimeSpan duration, System.Exception exception, System.Collections.Generic.IReadOnlyDictionary<string, object> data, System.Collections.Generic.IEnumerable<string> tags = null) { throw null; }
|
||||
public System.Collections.Generic.IReadOnlyDictionary<string, object> 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 System.Collections.Generic.IEnumerable<string> Tags { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
|
||||
}
|
||||
public enum HealthStatus
|
||||
{
|
||||
Unhealthy = 0,
|
||||
Degraded = 1,
|
||||
Healthy = 2,
|
||||
}
|
||||
public partial interface IHealthCheck
|
||||
{
|
||||
System.Threading.Tasks.Task<Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult> 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
// 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<string> tags) { }
|
||||
public HealthCheckRegistration(string name, Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck instance, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus, System.Collections.Generic.IEnumerable<string> tags, System.TimeSpan? timeout) { }
|
||||
public HealthCheckRegistration(string name, System.Func<System.IServiceProvider, Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck> factory, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus, System.Collections.Generic.IEnumerable<string> tags) { }
|
||||
public HealthCheckRegistration(string name, System.Func<System.IServiceProvider, Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck> factory, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus, System.Collections.Generic.IEnumerable<string> tags, System.TimeSpan? timeout) { }
|
||||
public System.Func<System.IServiceProvider, Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck> 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<string> 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<string, object> data = null) { throw null; }
|
||||
public System.Collections.Generic.IReadOnlyDictionary<string, object> 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<string, object> data = null) { throw null; }
|
||||
public static Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult Healthy(string description = null, System.Collections.Generic.IReadOnlyDictionary<string, object> data = null) { throw null; }
|
||||
public static Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult Unhealthy(string description = null, System.Exception exception = null, System.Collections.Generic.IReadOnlyDictionary<string, object> data = null) { throw null; }
|
||||
}
|
||||
public sealed partial class HealthReport
|
||||
{
|
||||
public HealthReport(System.Collections.Generic.IReadOnlyDictionary<string, Microsoft.Extensions.Diagnostics.HealthChecks.HealthReportEntry> entries, System.TimeSpan totalDuration) { }
|
||||
public System.Collections.Generic.IReadOnlyDictionary<string, Microsoft.Extensions.Diagnostics.HealthChecks.HealthReportEntry> 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<string, object> data) { throw null; }
|
||||
public HealthReportEntry(Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus status, string description, System.TimeSpan duration, System.Exception exception, System.Collections.Generic.IReadOnlyDictionary<string, object> data, System.Collections.Generic.IEnumerable<string> tags = null) { throw null; }
|
||||
public System.Collections.Generic.IReadOnlyDictionary<string, object> 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 System.Collections.Generic.IEnumerable<string> Tags { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
|
||||
}
|
||||
public enum HealthStatus
|
||||
{
|
||||
Unhealthy = 0,
|
||||
Degraded = 1,
|
||||
Healthy = 2,
|
||||
}
|
||||
public partial interface IHealthCheck
|
||||
{
|
||||
System.Threading.Tasks.Task<Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult> 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
// 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 class HealthCheckContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="HealthCheckRegistration"/> of the currently executing <see cref="IHealthCheck"/>.
|
||||
/// </summary>
|
||||
public HealthCheckRegistration Registration { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
/// <summary>
|
||||
/// Represent the registration information associated with an <see cref="IHealthCheck"/> implementation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The health check registration is provided as a separate object so that application developers can customize
|
||||
/// how health check implementations are configured.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The registration is provided to an <see cref="IHealthCheck"/> implementation during execution through
|
||||
/// <see cref="HealthCheckContext.Registration"/>. This allows a health check implementation to access named
|
||||
/// options or perform other operations based on the registered name.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class HealthCheckRegistration
|
||||
{
|
||||
private Func<IServiceProvider, IHealthCheck> _factory;
|
||||
private string _name;
|
||||
private TimeSpan _timeout;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="T:Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckRegistration" /> for an existing <see cref="T:Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck" /> instance.
|
||||
/// </summary>
|
||||
/// <param name="name">The health check name.</param>
|
||||
/// <param name="instance">The <see cref="T:Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck" /> instance.</param>
|
||||
/// <param name="failureStatus">
|
||||
/// The <see cref="T:Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus" /> that should be reported upon failure of the health check. If the provided value
|
||||
/// is <c>null</c>, then <see cref="F:Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus.Unhealthy" /> will be reported.
|
||||
/// </param>
|
||||
/// <param name="tags">A list of tags that can be used for filtering health checks.</param>
|
||||
public HealthCheckRegistration(string name, IHealthCheck instance, HealthStatus? failureStatus, IEnumerable<string> tags)
|
||||
: this(name, instance, failureStatus, tags, default)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="HealthCheckRegistration"/> for an existing <see cref="IHealthCheck"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="name">The health check name.</param>
|
||||
/// <param name="instance">The <see cref="IHealthCheck"/> instance.</param>
|
||||
/// <param name="failureStatus">
|
||||
/// The <see cref="HealthStatus"/> that should be reported upon failure of the health check. If the provided value
|
||||
/// is <c>null</c>, then <see cref="HealthStatus.Unhealthy"/> will be reported.
|
||||
/// </param>
|
||||
/// <param name="tags">A list of tags that can be used for filtering health checks.</param>
|
||||
/// <param name="timeout">An optional <see cref="TimeSpan"/> representing the timeout of the check.</param>
|
||||
public HealthCheckRegistration(string name, IHealthCheck instance, HealthStatus? failureStatus, IEnumerable<string> tags, TimeSpan? timeout)
|
||||
{
|
||||
if (name == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
if (instance == null)
|
||||
{
|
||||
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<string>(tags ?? Array.Empty<string>(), StringComparer.OrdinalIgnoreCase);
|
||||
Factory = (_) => instance;
|
||||
Timeout = timeout ?? System.Threading.Timeout.InfiniteTimeSpan;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="HealthCheckRegistration"/> for an existing <see cref="IHealthCheck"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="name">The health check name.</param>
|
||||
/// <param name="factory">A delegate used to create the <see cref="IHealthCheck"/> instance.</param>
|
||||
/// <param name="failureStatus">
|
||||
/// The <see cref="HealthStatus"/> that should be reported when the health check reports a failure. If the provided value
|
||||
/// is <c>null</c>, then <see cref="HealthStatus.Unhealthy"/> will be reported.
|
||||
/// </param>
|
||||
/// <param name="tags">A list of tags that can be used for filtering health checks.</param>
|
||||
public HealthCheckRegistration(
|
||||
string name,
|
||||
Func<IServiceProvider, IHealthCheck> factory,
|
||||
HealthStatus? failureStatus,
|
||||
IEnumerable<string> tags)
|
||||
: this(name, factory, failureStatus, tags, default)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="HealthCheckRegistration"/> for an existing <see cref="IHealthCheck"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="name">The health check name.</param>
|
||||
/// <param name="factory">A delegate used to create the <see cref="IHealthCheck"/> instance.</param>
|
||||
/// <param name="failureStatus">
|
||||
/// The <see cref="HealthStatus"/> that should be reported when the health check reports a failure. If the provided value
|
||||
/// is <c>null</c>, then <see cref="HealthStatus.Unhealthy"/> will be reported.
|
||||
/// </param>
|
||||
/// <param name="tags">A list of tags that can be used for filtering health checks.</param>
|
||||
/// <param name="timeout">An optional <see cref="TimeSpan"/> representing the timeout of the check.</param>
|
||||
public HealthCheckRegistration(
|
||||
string name,
|
||||
Func<IServiceProvider, IHealthCheck> factory,
|
||||
HealthStatus? failureStatus,
|
||||
IEnumerable<string> tags,
|
||||
TimeSpan? timeout)
|
||||
{
|
||||
if (name == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
if (factory == null)
|
||||
{
|
||||
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<string>(tags ?? Array.Empty<string>(), StringComparer.OrdinalIgnoreCase);
|
||||
Factory = factory;
|
||||
Timeout = timeout ?? System.Threading.Timeout.InfiniteTimeSpan;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a delegate used to create the <see cref="IHealthCheck"/> instance.
|
||||
/// </summary>
|
||||
public Func<IServiceProvider, IHealthCheck> Factory
|
||||
{
|
||||
get => _factory;
|
||||
set
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
}
|
||||
|
||||
_factory = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="HealthStatus"/> that should be reported upon failure of the health check.
|
||||
/// </summary>
|
||||
public HealthStatus FailureStatus { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the timeout used for the test.
|
||||
/// </summary>
|
||||
public TimeSpan Timeout
|
||||
{
|
||||
get => _timeout;
|
||||
set
|
||||
{
|
||||
if (value <= TimeSpan.Zero && value != System.Threading.Timeout.InfiniteTimeSpan)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(value));
|
||||
}
|
||||
|
||||
_timeout = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the health check name.
|
||||
/// </summary>
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
set
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
}
|
||||
|
||||
_name = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of tags that can be used for filtering health checks.
|
||||
/// </summary>
|
||||
public ISet<string> Tags { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the result of a health check.
|
||||
/// </summary>
|
||||
public struct HealthCheckResult
|
||||
{
|
||||
private static readonly IReadOnlyDictionary<string, object> _emptyReadOnlyDictionary = new Dictionary<string, object>();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="HealthCheckResult"/> with the specified values for <paramref name="status"/>,
|
||||
/// <paramref name="exception"/>, <paramref name="description"/>, and <paramref name="data"/>.
|
||||
/// </summary>
|
||||
/// <param name="status">A value indicating the status of the component that was checked.</param>
|
||||
/// <param name="description">A human-readable description of the status of the component that was checked.</param>
|
||||
/// <param name="exception">An <see cref="Exception"/> representing the exception that was thrown when checking for status (if any).</param>
|
||||
/// <param name="data">Additional key-value pairs describing the health of the component.</param>
|
||||
public HealthCheckResult(HealthStatus status, string description = null, Exception exception = null, IReadOnlyDictionary<string, object> data = null)
|
||||
{
|
||||
Status = status;
|
||||
Description = description;
|
||||
Exception = exception;
|
||||
Data = data ?? _emptyReadOnlyDictionary;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets additional key-value pairs describing the health of the component.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, object> Data { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a human-readable description of the status of the component that was checked.
|
||||
/// </summary>
|
||||
public string Description { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets an <see cref="Exception"/> representing the exception that was thrown when checking for status (if any).
|
||||
/// </summary>
|
||||
public Exception Exception { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating the status of the component that was checked.
|
||||
/// </summary>
|
||||
public HealthStatus Status { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="HealthCheckResult"/> representing a healthy component.
|
||||
/// </summary>
|
||||
/// <param name="description">A human-readable description of the status of the component that was checked. Optional.</param>
|
||||
/// <param name="data">Additional key-value pairs describing the health of the component. Optional.</param>
|
||||
/// <returns>A <see cref="HealthCheckResult"/> representing a healthy component.</returns>
|
||||
public static HealthCheckResult Healthy(string description = null, IReadOnlyDictionary<string, object> data = null)
|
||||
{
|
||||
return new HealthCheckResult(status: HealthStatus.Healthy, description, exception: null, data);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="HealthCheckResult"/> representing a degraded component.
|
||||
/// </summary>
|
||||
/// <param name="description">A human-readable description of the status of the component that was checked. Optional.</param>
|
||||
/// <param name="exception">An <see cref="Exception"/> representing the exception that was thrown when checking for status. Optional.</param>
|
||||
/// <param name="data">Additional key-value pairs describing the health of the component. Optional.</param>
|
||||
/// <returns>A <see cref="HealthCheckResult"/> representing a degraged component.</returns>
|
||||
public static HealthCheckResult Degraded(string description = null, Exception exception = null, IReadOnlyDictionary<string, object> data = null)
|
||||
{
|
||||
return new HealthCheckResult(status: HealthStatus.Degraded, description, exception: exception, data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="HealthCheckResult"/> representing an unhealthy component.
|
||||
/// </summary>
|
||||
/// <param name="description">A human-readable description of the status of the component that was checked. Optional.</param>
|
||||
/// <param name="exception">An <see cref="Exception"/> representing the exception that was thrown when checking for status. Optional.</param>
|
||||
/// <param name="data">Additional key-value pairs describing the health of the component. Optional.</param>
|
||||
/// <returns>A <see cref="HealthCheckResult"/> representing an unhealthy component.</returns>
|
||||
public static HealthCheckResult Unhealthy(string description = null, Exception exception = null, IReadOnlyDictionary<string, object> data = null)
|
||||
{
|
||||
return new HealthCheckResult(status: HealthStatus.Unhealthy, description, exception, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the result of executing a group of <see cref="IHealthCheck"/> instances.
|
||||
/// </summary>
|
||||
public sealed class HealthReport
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a new <see cref="HealthReport"/> from the specified results.
|
||||
/// </summary>
|
||||
/// <param name="entries">A <see cref="IReadOnlyDictionary{TKey, T}"/> containing the results from each health check.</param>
|
||||
/// <param name="totalDuration">A value indicating the time the health check service took to execute.</param>
|
||||
public HealthReport(IReadOnlyDictionary<string, HealthReportEntry> entries, TimeSpan totalDuration)
|
||||
{
|
||||
Entries = entries;
|
||||
Status = CalculateAggregateStatus(entries.Values);
|
||||
TotalDuration = totalDuration;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="IReadOnlyDictionary{TKey, T}"/> containing the results from each health check.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The keys in this dictionary map the name of each executed health check to a <see cref="HealthReportEntry"/> for the
|
||||
/// result data returned from the corresponding health check.
|
||||
/// </remarks>
|
||||
public IReadOnlyDictionary<string, HealthReportEntry> Entries { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="HealthStatus"/> representing the aggregate status of all the health checks. The value of <see cref="Status"/>
|
||||
/// will be the most severe status reported by a health check. If no checks were executed, the value is always <see cref="HealthStatus.Healthy"/>.
|
||||
/// </summary>
|
||||
public HealthStatus Status { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the time the health check service took to execute.
|
||||
/// </summary>
|
||||
public TimeSpan TotalDuration { get; }
|
||||
|
||||
private HealthStatus CalculateAggregateStatus(IEnumerable<HealthReportEntry> entries)
|
||||
{
|
||||
// This is basically a Min() check, but we know the possible range, so we don't need to walk the whole list
|
||||
var currentValue = HealthStatus.Healthy;
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
if (currentValue > entry.Status)
|
||||
{
|
||||
currentValue = entry.Status;
|
||||
}
|
||||
|
||||
if (currentValue == HealthStatus.Unhealthy)
|
||||
{
|
||||
// Game over, man! Game over!
|
||||
// (We hit the worst possible status, so there's no need to keep iterating)
|
||||
return currentValue;
|
||||
}
|
||||
}
|
||||
|
||||
return currentValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an entry in a <see cref="HealthReport"/>. Corresponds to the result of a single <see cref="IHealthCheck"/>.
|
||||
/// </summary>
|
||||
public struct HealthReportEntry
|
||||
{
|
||||
private static readonly IReadOnlyDictionary<string, object> _emptyReadOnlyDictionary = new Dictionary<string, object>();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="HealthReportEntry"/> with the specified values for <paramref name="status"/>, <paramref name="exception"/>,
|
||||
/// <paramref name="description"/>, and <paramref name="data"/>.
|
||||
/// </summary>
|
||||
/// <param name="status">A value indicating the health status of the component that was checked.</param>
|
||||
/// <param name="description">A human-readable description of the status of the component that was checked.</param>
|
||||
/// <param name="duration">A value indicating the health execution duration.</param>
|
||||
/// <param name="exception">An <see cref="Exception"/> representing the exception that was thrown when checking for status (if any).</param>
|
||||
/// <param name="data">Additional key-value pairs describing the health of the component.</param>
|
||||
public HealthReportEntry(HealthStatus status, string description, TimeSpan duration, Exception exception, IReadOnlyDictionary<string, object> data)
|
||||
: this(status, description, duration, exception, data, null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="HealthReportEntry"/> with the specified values for <paramref name="status"/>, <paramref name="exception"/>,
|
||||
/// <paramref name="description"/>, and <paramref name="data"/>.
|
||||
/// </summary>
|
||||
/// <param name="status">A value indicating the health status of the component that was checked.</param>
|
||||
/// <param name="description">A human-readable description of the status of the component that was checked.</param>
|
||||
/// <param name="duration">A value indicating the health execution duration.</param>
|
||||
/// <param name="exception">An <see cref="Exception"/> representing the exception that was thrown when checking for status (if any).</param>
|
||||
/// <param name="data">Additional key-value pairs describing the health of the component.</param>
|
||||
/// <param name="tags">Tags associated with the health check that generated the report entry.</param>
|
||||
public HealthReportEntry(HealthStatus status, string description, TimeSpan duration, Exception exception, IReadOnlyDictionary<string, object> data, IEnumerable<string> tags = null)
|
||||
{
|
||||
Status = status;
|
||||
Description = description;
|
||||
Duration = duration;
|
||||
Exception = exception;
|
||||
Data = data ?? _emptyReadOnlyDictionary;
|
||||
Tags = tags ?? Enumerable.Empty<string>();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets additional key-value pairs describing the health of the component.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, object> Data { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a human-readable description of the status of the component that was checked.
|
||||
/// </summary>
|
||||
public string Description { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the health check execution duration.
|
||||
/// </summary>
|
||||
public TimeSpan Duration { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets an <see cref="System.Exception"/> representing the exception that was thrown when checking for status (if any).
|
||||
/// </summary>
|
||||
public Exception Exception { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the health status of the component that was checked.
|
||||
/// </summary>
|
||||
public HealthStatus Status { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the tags associated with the health check.
|
||||
/// </summary>
|
||||
public IEnumerable<string> Tags { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the reported status of a health check result.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// A status of <see cref="Unhealthy"/> should be considered the default value for a failing health check. Application
|
||||
/// developers may configure a health check to report a different status as desired.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The values of this enum or ordered from least healthy to most healthy. So <see cref="HealthStatus.Degraded"/> is
|
||||
/// greater than <see cref="HealthStatus.Unhealthy"/> but less than <see cref="HealthStatus.Healthy"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public enum HealthStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates that the health check determined that the component was unhealthy, or an unhandled
|
||||
/// exception was thrown while executing the health check.
|
||||
/// </summary>
|
||||
Unhealthy = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the health check determined that the component was in a degraded state.
|
||||
/// </summary>
|
||||
Degraded = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the health check determined that the component was healthy.
|
||||
/// </summary>
|
||||
Healthy = 2,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
// 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;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a health check, which can be used to check the status of a component in the application, such as a backend service, database or some internal
|
||||
/// state.
|
||||
/// </summary>
|
||||
public interface IHealthCheck
|
||||
{
|
||||
/// <summary>
|
||||
/// Runs the health check, returning the status of the component being checked.
|
||||
/// </summary>
|
||||
/// <param name="context">A context object associated with the current execution.</param>
|
||||
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the health check.</param>
|
||||
/// <returns>A <see cref="Task{HealthCheckResult}"/> that completes when the health check has finished, yielding the status of the component being checked.</returns>
|
||||
Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a publisher of <see cref="HealthReport"/> information.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The default health checks implementation provided an <c>IHostedService</c> implementation that can
|
||||
/// be used to execute health checks at regular intervals and provide the resulting <see cref="HealthReport"/>
|
||||
/// data to all registered <see cref="IHealthCheckPublisher"/> instances.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// To provide an <see cref="IHealthCheckPublisher"/> implementation, register an instance or type as a singleton
|
||||
/// service in the dependency injection container.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <see cref="IHealthCheckPublisher"/> instances are provided with a <see cref="HealthReport"/> after executing
|
||||
/// health checks in a background thread. The use of <see cref="IHealthCheckPublisher"/> depend on hosting in
|
||||
/// an application using <c>IWebHost</c> or generic host (<c>IHost</c>). Execution of <see cref="IHealthCheckPublisher"/>
|
||||
/// instance is not related to execution of health checks via a middleware.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public interface IHealthCheckPublisher
|
||||
{
|
||||
/// <summary>
|
||||
/// Publishes the provided <paramref name="report"/>.
|
||||
/// </summary>
|
||||
/// <param name="report">The <see cref="HealthReport"/>. The result of executing a set of health checks.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
|
||||
/// <returns>A <see cref="Task"/> which will complete when publishing is complete.</returns>
|
||||
Task PublishAsync(HealthReport report, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>Abstractions for defining health checks in .NET applications
|
||||
|
||||
Commonly Used Types
|
||||
Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck
|
||||
</Description>
|
||||
<RootNamespace>Microsoft.Extensions.Diagnostics.HealthChecks</RootNamespace>
|
||||
<TargetFrameworks>netstandard2.0;$(DefaultNetCoreTargetFramework)</TargetFrameworks>
|
||||
<TargetFrameworks Condition="'$(DotNetBuildFromSource)' == 'true'">$(DefaultNetCoreTargetFramework)</TargetFrameworks>
|
||||
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<PackageTags>diagnostics;healthchecks</PackageTags>
|
||||
<IsPackable>true</IsPackable>
|
||||
<IsShipping>true</IsShipping>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<!-- This file is automatically generated. -->
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;$(DefaultNetCoreTargetFramework)</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
|
||||
<Compile Include="Microsoft.Extensions.Diagnostics.HealthChecks.netstandard2.0.cs" />
|
||||
<Reference Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" />
|
||||
<Reference Include="Microsoft.Extensions.Hosting.Abstractions" />
|
||||
<Reference Include="Microsoft.Extensions.Options" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)' == '$(DefaultNetCoreTargetFramework)'">
|
||||
<Compile Include="Microsoft.Extensions.Diagnostics.HealthChecks.netcoreapp.cs" />
|
||||
<Reference Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" />
|
||||
<Reference Include="Microsoft.Extensions.Hosting.Abstractions" />
|
||||
<Reference Include="Microsoft.Extensions.Options" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -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<string> 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<string> tags = null, System.TimeSpan? timeout = default(System.TimeSpan?)) { throw null; }
|
||||
public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddCheck<T>(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus, System.Collections.Generic.IEnumerable<string> tags) where T : class, Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck { throw null; }
|
||||
public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddCheck<T>(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus = default(Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus?), System.Collections.Generic.IEnumerable<string> 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<T>(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus, System.Collections.Generic.IEnumerable<string> tags, params object[] args) where T : class, Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck { throw null; }
|
||||
public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddTypeActivatedCheck<T>(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus, System.Collections.Generic.IEnumerable<string> tags, System.TimeSpan timeout, params object[] args) where T : class, Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck { throw null; }
|
||||
public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddTypeActivatedCheck<T>(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<T>(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<System.Threading.CancellationToken, System.Threading.Tasks.Task<Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult>> check, System.Collections.Generic.IEnumerable<string> tags) { throw null; }
|
||||
public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddAsyncCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, System.Func<System.Threading.CancellationToken, System.Threading.Tasks.Task<Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult>> check, System.Collections.Generic.IEnumerable<string> 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<System.Threading.Tasks.Task<Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult>> check, System.Collections.Generic.IEnumerable<string> tags) { throw null; }
|
||||
public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddAsyncCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, System.Func<System.Threading.Tasks.Task<Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult>> check, System.Collections.Generic.IEnumerable<string> 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<Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult> check, System.Collections.Generic.IEnumerable<string> tags) { throw null; }
|
||||
public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, System.Func<Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult> check, System.Collections.Generic.IEnumerable<string> 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<System.Threading.CancellationToken, Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult> check, System.Collections.Generic.IEnumerable<string> tags) { throw null; }
|
||||
public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, System.Func<System.Threading.CancellationToken, Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult> check, System.Collections.Generic.IEnumerable<string> 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<Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckRegistration, bool> 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<Microsoft.Extensions.Diagnostics.HealthChecks.HealthReport> CheckHealthAsync(System.Func<Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckRegistration, bool> predicate, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));
|
||||
public System.Threading.Tasks.Task<Microsoft.Extensions.Diagnostics.HealthChecks.HealthReport> CheckHealthAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
|
||||
}
|
||||
public sealed partial class HealthCheckServiceOptions
|
||||
{
|
||||
public HealthCheckServiceOptions() { }
|
||||
public System.Collections.Generic.ICollection<Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckRegistration> Registrations { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string> 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<string> tags = null, System.TimeSpan? timeout = default(System.TimeSpan?)) { throw null; }
|
||||
public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddCheck<T>(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus, System.Collections.Generic.IEnumerable<string> tags) where T : class, Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck { throw null; }
|
||||
public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddCheck<T>(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus = default(Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus?), System.Collections.Generic.IEnumerable<string> 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<T>(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus, System.Collections.Generic.IEnumerable<string> tags, params object[] args) where T : class, Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck { throw null; }
|
||||
public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddTypeActivatedCheck<T>(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus, System.Collections.Generic.IEnumerable<string> tags, System.TimeSpan timeout, params object[] args) where T : class, Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck { throw null; }
|
||||
public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddTypeActivatedCheck<T>(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<T>(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<System.Threading.CancellationToken, System.Threading.Tasks.Task<Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult>> check, System.Collections.Generic.IEnumerable<string> tags) { throw null; }
|
||||
public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddAsyncCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, System.Func<System.Threading.CancellationToken, System.Threading.Tasks.Task<Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult>> check, System.Collections.Generic.IEnumerable<string> 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<System.Threading.Tasks.Task<Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult>> check, System.Collections.Generic.IEnumerable<string> tags) { throw null; }
|
||||
public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddAsyncCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, System.Func<System.Threading.Tasks.Task<Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult>> check, System.Collections.Generic.IEnumerable<string> 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<Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult> check, System.Collections.Generic.IEnumerable<string> tags) { throw null; }
|
||||
public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, System.Func<Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult> check, System.Collections.Generic.IEnumerable<string> 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<System.Threading.CancellationToken, Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult> check, System.Collections.Generic.IEnumerable<string> tags) { throw null; }
|
||||
public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, System.Func<System.Threading.CancellationToken, Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult> check, System.Collections.Generic.IEnumerable<string> 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<Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckRegistration, bool> 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<Microsoft.Extensions.Diagnostics.HealthChecks.HealthReport> CheckHealthAsync(System.Func<Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckRegistration, bool> predicate, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));
|
||||
public System.Threading.Tasks.Task<Microsoft.Extensions.Diagnostics.HealthChecks.HealthReport> CheckHealthAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
|
||||
}
|
||||
public sealed partial class HealthCheckServiceOptions
|
||||
{
|
||||
public HealthCheckServiceOptions() { }
|
||||
public System.Collections.Generic.ICollection<Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckRegistration> Registrations { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,346 @@
|
|||
// 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.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Internal;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
internal class DefaultHealthCheckService : HealthCheckService
|
||||
{
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly IOptions<HealthCheckServiceOptions> _options;
|
||||
private readonly ILogger<DefaultHealthCheckService> _logger;
|
||||
|
||||
public DefaultHealthCheckService(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IOptions<HealthCheckServiceOptions> options,
|
||||
ILogger<DefaultHealthCheckService> logger)
|
||||
{
|
||||
_scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
||||
// We're specifically going out of our way to do this at startup time. We want to make sure you
|
||||
// get any kind of health-check related error as early as possible. Waiting until someone
|
||||
// actually tries to **run** health checks would be real baaaaad.
|
||||
ValidateRegistrations(_options.Value.Registrations);
|
||||
}
|
||||
public override async Task<HealthReport> CheckHealthAsync(
|
||||
Func<HealthCheckRegistration, bool> predicate,
|
||||
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<HealthReportEntry>[registrations.Count];
|
||||
var index = 0;
|
||||
using (var scope = _scopeFactory.CreateScope())
|
||||
{
|
||||
foreach (var registration in registrations)
|
||||
{
|
||||
tasks[index++] = Task.Run(() => RunCheckAsync(scope, registration, cancellationToken), cancellationToken);
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
index = 0;
|
||||
var entries = new Dictionary<string, HealthReportEntry>(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<HealthReportEntry> 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,
|
||||
tags: registration.Tags);
|
||||
|
||||
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 occurred 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;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateRegistrations(IEnumerable<HealthCheckRegistration> registrations)
|
||||
{
|
||||
// Scan the list for duplicate names to provide a better error if there are duplicates.
|
||||
var duplicateNames = registrations
|
||||
.GroupBy(c => c.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.Where(g => g.Count() > 1)
|
||||
.Select(g => g.Key)
|
||||
.ToList();
|
||||
|
||||
if (duplicateNames.Count > 0)
|
||||
{
|
||||
throw new ArgumentException($"Duplicate health checks were registered with the name(s): {string.Join(", ", duplicateNames)}", nameof(registrations));
|
||||
}
|
||||
}
|
||||
|
||||
internal static class EventIds
|
||||
{
|
||||
public static readonly EventId HealthCheckProcessingBegin = new EventId(100, "HealthCheckProcessingBegin");
|
||||
public static readonly EventId HealthCheckProcessingEnd = new EventId(101, "HealthCheckProcessingEnd");
|
||||
|
||||
public static readonly EventId HealthCheckBegin = new EventId(102, "HealthCheckBegin");
|
||||
public static readonly EventId HealthCheckEnd = new EventId(103, "HealthCheckEnd");
|
||||
public static readonly EventId HealthCheckError = new EventId(104, "HealthCheckError");
|
||||
public static readonly EventId HealthCheckData = new EventId(105, "HealthCheckData");
|
||||
}
|
||||
|
||||
private static class Log
|
||||
{
|
||||
private static readonly Action<ILogger, Exception> _healthCheckProcessingBegin = LoggerMessage.Define(
|
||||
LogLevel.Debug,
|
||||
EventIds.HealthCheckProcessingBegin,
|
||||
"Running health checks");
|
||||
|
||||
private static readonly Action<ILogger, double, HealthStatus, Exception> _healthCheckProcessingEnd = LoggerMessage.Define<double, HealthStatus>(
|
||||
LogLevel.Debug,
|
||||
EventIds.HealthCheckProcessingEnd,
|
||||
"Health check processing completed after {ElapsedMilliseconds}ms with combined status {HealthStatus}");
|
||||
|
||||
private static readonly Action<ILogger, string, Exception> _healthCheckBegin = LoggerMessage.Define<string>(
|
||||
LogLevel.Debug,
|
||||
EventIds.HealthCheckBegin,
|
||||
"Running health check {HealthCheckName}");
|
||||
|
||||
// These are separate so they can have different log levels
|
||||
private static readonly string HealthCheckEndText = "Health check {HealthCheckName} completed after {ElapsedMilliseconds}ms with status {HealthStatus} and '{HealthCheckDescription}'";
|
||||
|
||||
private static readonly Action<ILogger, string, double, HealthStatus, string, Exception> _healthCheckEndHealthy = LoggerMessage.Define<string, double, HealthStatus, string>(
|
||||
LogLevel.Debug,
|
||||
EventIds.HealthCheckEnd,
|
||||
HealthCheckEndText);
|
||||
|
||||
private static readonly Action<ILogger, string, double, HealthStatus, string, Exception> _healthCheckEndDegraded = LoggerMessage.Define<string, double, HealthStatus, string>(
|
||||
LogLevel.Warning,
|
||||
EventIds.HealthCheckEnd,
|
||||
HealthCheckEndText);
|
||||
|
||||
private static readonly Action<ILogger, string, double, HealthStatus, string, Exception> _healthCheckEndUnhealthy = LoggerMessage.Define<string, double, HealthStatus, string>(
|
||||
LogLevel.Error,
|
||||
EventIds.HealthCheckEnd,
|
||||
HealthCheckEndText);
|
||||
|
||||
private static readonly Action<ILogger, string, double, HealthStatus, string, Exception> _healthCheckEndFailed = LoggerMessage.Define<string, double, HealthStatus, string>(
|
||||
LogLevel.Error,
|
||||
EventIds.HealthCheckEnd,
|
||||
HealthCheckEndText);
|
||||
|
||||
private static readonly Action<ILogger, string, double, Exception> _healthCheckError = LoggerMessage.Define<string, double>(
|
||||
LogLevel.Error,
|
||||
EventIds.HealthCheckError,
|
||||
"Health check {HealthCheckName} threw an unhandled exception after {ElapsedMilliseconds}ms");
|
||||
|
||||
public static void HealthCheckProcessingBegin(ILogger logger)
|
||||
{
|
||||
_healthCheckProcessingBegin(logger, null);
|
||||
}
|
||||
|
||||
public static void HealthCheckProcessingEnd(ILogger logger, HealthStatus status, TimeSpan duration)
|
||||
{
|
||||
_healthCheckProcessingEnd(logger, duration.TotalMilliseconds, status, null);
|
||||
}
|
||||
|
||||
public static void HealthCheckBegin(ILogger logger, HealthCheckRegistration registration)
|
||||
{
|
||||
_healthCheckBegin(logger, registration.Name, null);
|
||||
}
|
||||
|
||||
public static void HealthCheckEnd(ILogger logger, HealthCheckRegistration registration, HealthReportEntry entry, TimeSpan duration)
|
||||
{
|
||||
switch (entry.Status)
|
||||
{
|
||||
case HealthStatus.Healthy:
|
||||
_healthCheckEndHealthy(logger, registration.Name, duration.TotalMilliseconds, entry.Status, entry.Description, null);
|
||||
break;
|
||||
|
||||
case HealthStatus.Degraded:
|
||||
_healthCheckEndDegraded(logger, registration.Name, duration.TotalMilliseconds, entry.Status, entry.Description, null);
|
||||
break;
|
||||
|
||||
case HealthStatus.Unhealthy:
|
||||
_healthCheckEndUnhealthy(logger, registration.Name, duration.TotalMilliseconds, entry.Status, entry.Description, null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static void HealthCheckError(ILogger logger, HealthCheckRegistration registration, Exception exception, TimeSpan duration)
|
||||
{
|
||||
_healthCheckError(logger, registration.Name, duration.TotalMilliseconds, exception);
|
||||
}
|
||||
|
||||
public static void HealthCheckData(ILogger logger, HealthCheckRegistration registration, HealthReportEntry entry)
|
||||
{
|
||||
if (entry.Data.Count > 0 && logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
logger.Log(
|
||||
LogLevel.Debug,
|
||||
EventIds.HealthCheckData,
|
||||
new HealthCheckDataLogValue(registration.Name, entry.Data),
|
||||
null,
|
||||
(state, ex) => state.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class HealthCheckDataLogValue : IReadOnlyList<KeyValuePair<string, object>>
|
||||
{
|
||||
private readonly string _name;
|
||||
private readonly List<KeyValuePair<string, object>> _values;
|
||||
|
||||
private string _formatted;
|
||||
|
||||
public HealthCheckDataLogValue(string name, IReadOnlyDictionary<string, object> values)
|
||||
{
|
||||
_name = name;
|
||||
_values = values.ToList();
|
||||
|
||||
// We add the name as a kvp so that you can filter by health check name in the logs.
|
||||
// This is the same parameter name used in the other logs.
|
||||
_values.Add(new KeyValuePair<string, object>("HealthCheckName", name));
|
||||
}
|
||||
|
||||
public KeyValuePair<string, object> this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (index < 0 || index >= Count)
|
||||
{
|
||||
throw new IndexOutOfRangeException(nameof(index));
|
||||
}
|
||||
|
||||
return _values[index];
|
||||
}
|
||||
}
|
||||
|
||||
public int Count => _values.Count;
|
||||
|
||||
public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
|
||||
{
|
||||
return _values.GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return _values.GetEnumerator();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (_formatted == null)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine($"Health check data for {_name}:");
|
||||
|
||||
var values = _values;
|
||||
for (var i = 0; i < values.Count; i++)
|
||||
{
|
||||
var kvp = values[i];
|
||||
builder.Append(" ");
|
||||
builder.Append(kvp.Key);
|
||||
builder.Append(": ");
|
||||
|
||||
builder.AppendLine(kvp.Value?.ToString());
|
||||
}
|
||||
|
||||
_formatted = builder.ToString();
|
||||
}
|
||||
|
||||
return _formatted;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
// 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;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
/// <summary>
|
||||
/// A simple implementation of <see cref="IHealthCheck"/> which uses a provided delegate to
|
||||
/// implement the check.
|
||||
/// </summary>
|
||||
internal sealed class DelegateHealthCheck : IHealthCheck
|
||||
{
|
||||
private readonly Func<CancellationToken, Task<HealthCheckResult>> _check;
|
||||
|
||||
/// <summary>
|
||||
/// Create an instance of <see cref="DelegateHealthCheck"/> from the specified delegate.
|
||||
/// </summary>
|
||||
/// <param name="check">A delegate which provides the code to execute when the health check is run.</param>
|
||||
public DelegateHealthCheck(Func<CancellationToken, Task<HealthCheckResult>> check)
|
||||
{
|
||||
_check = check ?? throw new ArgumentNullException(nameof(check));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the health check, returning the status of the component being checked.
|
||||
/// </summary>
|
||||
/// <param name="context">A context object associated with the current execution.</param>
|
||||
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the health check.</param>
|
||||
/// <returns>A <see cref="Task{HealthCheckResult}"/> that completes when the health check has finished, yielding the status of the component being checked.</returns>
|
||||
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) => _check(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides extension methods for registering <see cref="HealthCheckService"/> in an <see cref="IServiceCollection"/>.
|
||||
/// </summary>
|
||||
public static class HealthCheckServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the <see cref="HealthCheckService"/> to the container, using the provided delegate to register
|
||||
/// health checks.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This operation is idempotent - multiple invocations will still only result in a single
|
||||
/// <see cref="HealthCheckService"/> instance in the <see cref="IServiceCollection"/>. It can be invoked
|
||||
/// multiple times in order to get access to the <see cref="IHealthChecksBuilder"/> in multiple places.
|
||||
/// </remarks>
|
||||
/// <param name="services">The <see cref="IServiceCollection"/> to add the <see cref="HealthCheckService"/> to.</param>
|
||||
/// <returns>An instance of <see cref="IHealthChecksBuilder"/> from which health checks can be registered.</returns>
|
||||
public static IHealthChecksBuilder AddHealthChecks(this IServiceCollection services)
|
||||
{
|
||||
services.TryAddSingleton<HealthCheckService, DefaultHealthCheckService>();
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IHostedService, HealthCheckPublisherHostedService>());
|
||||
return new HealthChecksBuilder(services);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
{
|
||||
internal class HealthChecksBuilder : IHealthChecksBuilder
|
||||
{
|
||||
public HealthChecksBuilder(IServiceCollection services)
|
||||
{
|
||||
Services = services;
|
||||
}
|
||||
|
||||
public IServiceCollection Services { get; }
|
||||
|
||||
public IHealthChecksBuilder Add(HealthCheckRegistration registration)
|
||||
{
|
||||
if (registration == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(registration));
|
||||
}
|
||||
|
||||
Services.Configure<HealthCheckServiceOptions>(options =>
|
||||
{
|
||||
options.Registrations.Add(registration);
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,285 @@
|
|||
// 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 Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides basic extension methods for registering <see cref="IHealthCheck"/> instances in an <see cref="IHealthChecksBuilder"/>.
|
||||
/// </summary>
|
||||
public static class HealthChecksBuilderAddCheckExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a new health check with the specified name and implementation.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
|
||||
/// <param name="name">The name of the health check.</param>
|
||||
/// <param name="instance">An <see cref="IHealthCheck"/> instance.</param>
|
||||
/// <param name="failureStatus">
|
||||
/// The <see cref="HealthStatus"/> that should be reported when the health check reports a failure. If the provided value
|
||||
/// is <c>null</c>, then <see cref="HealthStatus.Unhealthy"/> will be reported.
|
||||
/// </param>
|
||||
/// <param name="tags">A list of tags that can be used to filter health checks.</param>
|
||||
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
|
||||
// 2.0 BACKCOMPAT OVERLOAD -- DO NOT TOUCH
|
||||
public static IHealthChecksBuilder AddCheck(
|
||||
this IHealthChecksBuilder builder,
|
||||
string name,
|
||||
IHealthCheck instance,
|
||||
HealthStatus? failureStatus,
|
||||
IEnumerable<string> tags)
|
||||
{
|
||||
return AddCheck(builder, name, instance, failureStatus, tags, default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new health check with the specified name and implementation.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
|
||||
/// <param name="name">The name of the health check.</param>
|
||||
/// <param name="instance">An <see cref="IHealthCheck"/> instance.</param>
|
||||
/// <param name="failureStatus">
|
||||
/// The <see cref="HealthStatus"/> that should be reported when the health check reports a failure. If the provided value
|
||||
/// is <c>null</c>, then <see cref="HealthStatus.Unhealthy"/> will be reported.
|
||||
/// </param>
|
||||
/// <param name="tags">A list of tags that can be used to filter health checks.</param>
|
||||
/// <param name="timeout">An optional <see cref="TimeSpan"/> representing the timeout of the check.</param>
|
||||
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
|
||||
public static IHealthChecksBuilder AddCheck(
|
||||
this IHealthChecksBuilder builder,
|
||||
string name,
|
||||
IHealthCheck instance,
|
||||
HealthStatus? failureStatus = null,
|
||||
IEnumerable<string> tags = null,
|
||||
TimeSpan? timeout = null)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
if (name == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
if (instance == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(instance));
|
||||
}
|
||||
|
||||
return builder.Add(new HealthCheckRegistration(name, instance, failureStatus, tags, timeout));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new health check with the specified name and implementation.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The health check implementation type.</typeparam>
|
||||
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
|
||||
/// <param name="name">The name of the health check.</param>
|
||||
/// <param name="failureStatus">
|
||||
/// The <see cref="HealthStatus"/> that should be reported when the health check reports a failure. If the provided value
|
||||
/// is <c>null</c>, then <see cref="HealthStatus.Unhealthy"/> will be reported.
|
||||
/// </param>
|
||||
/// <param name="tags">A list of tags that can be used to filter health checks.</param>
|
||||
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
|
||||
/// <remarks>
|
||||
/// This method will use <see cref="ActivatorUtilities.GetServiceOrCreateInstance{T}(IServiceProvider)"/> to create the health check
|
||||
/// instance when needed. If a service of type <typeparamref name="T"/> is registered in the dependency injection container
|
||||
/// with any lifetime it will be used. Otherwise an instance of type <typeparamref name="T"/> will be constructed with
|
||||
/// access to services from the dependency injection container.
|
||||
/// </remarks>
|
||||
// 2.0 BACKCOMPAT OVERLOAD -- DO NOT TOUCH
|
||||
public static IHealthChecksBuilder AddCheck<T>(
|
||||
this IHealthChecksBuilder builder,
|
||||
string name,
|
||||
HealthStatus? failureStatus,
|
||||
IEnumerable<string> tags) where T : class, IHealthCheck
|
||||
{
|
||||
return AddCheck<T>(builder, name, failureStatus, tags, default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new health check with the specified name and implementation.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The health check implementation type.</typeparam>
|
||||
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
|
||||
/// <param name="name">The name of the health check.</param>
|
||||
/// <param name="failureStatus">
|
||||
/// The <see cref="HealthStatus"/> that should be reported when the health check reports a failure. If the provided value
|
||||
/// is <c>null</c>, then <see cref="HealthStatus.Unhealthy"/> will be reported.
|
||||
/// </param>
|
||||
/// <param name="tags">A list of tags that can be used to filter health checks.</param>
|
||||
/// <param name="timeout">An optional <see cref="TimeSpan"/> representing the timeout of the check.</param>
|
||||
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
|
||||
/// <remarks>
|
||||
/// This method will use <see cref="ActivatorUtilities.GetServiceOrCreateInstance{T}(IServiceProvider)"/> to create the health check
|
||||
/// instance when needed. If a service of type <typeparamref name="T"/> is registered in the dependency injection container
|
||||
/// with any lifetime it will be used. Otherwise an instance of type <typeparamref name="T"/> will be constructed with
|
||||
/// access to services from the dependency injection container.
|
||||
/// </remarks>
|
||||
public static IHealthChecksBuilder AddCheck<T>(
|
||||
this IHealthChecksBuilder builder,
|
||||
string name,
|
||||
HealthStatus? failureStatus = null,
|
||||
IEnumerable<string> tags = null,
|
||||
TimeSpan? timeout = null) 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.GetServiceOrCreateInstance<T>(s), failureStatus, tags, timeout));
|
||||
}
|
||||
|
||||
// NOTE: AddTypeActivatedCheck has overloads rather than default parameters values, because default parameter values don't
|
||||
// play super well with params.
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new type activated health check with the specified name and implementation.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The health check implementation type.</typeparam>
|
||||
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
|
||||
/// <param name="name">The name of the health check.</param>
|
||||
/// <param name="args">Additional arguments to provide to the constructor.</param>
|
||||
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
|
||||
/// <remarks>
|
||||
/// This method will use <see cref="ActivatorUtilities.CreateInstance{T}(IServiceProvider, object[])"/> to create the health check
|
||||
/// instance when needed. Additional arguments can be provided to the constructor via <paramref name="args"/>.
|
||||
/// </remarks>
|
||||
public static IHealthChecksBuilder AddTypeActivatedCheck<T>(this IHealthChecksBuilder builder, string name, params object[] args) where T : class, IHealthCheck
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
if (name == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
return AddTypeActivatedCheck<T>(builder, name, failureStatus: null, tags: null, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new type activated health check with the specified name and implementation.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The health check implementation type.</typeparam>
|
||||
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
|
||||
/// <param name="name">The name of the health check.</param>
|
||||
/// <param name="failureStatus">
|
||||
/// The <see cref="HealthStatus"/> that should be reported when the health check reports a failure. If the provided value
|
||||
/// is <c>null</c>, then <see cref="HealthStatus.Unhealthy"/> will be reported.
|
||||
/// </param>
|
||||
/// <param name="args">Additional arguments to provide to the constructor.</param>
|
||||
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
|
||||
/// <remarks>
|
||||
/// This method will use <see cref="ActivatorUtilities.CreateInstance{T}(IServiceProvider, object[])"/> to create the health check
|
||||
/// instance when needed. Additional arguments can be provided to the constructor via <paramref name="args"/>.
|
||||
/// </remarks>
|
||||
public static IHealthChecksBuilder AddTypeActivatedCheck<T>(
|
||||
this IHealthChecksBuilder builder,
|
||||
string name,
|
||||
HealthStatus? failureStatus,
|
||||
params object[] args) where T : class, IHealthCheck
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
if (name == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
return AddTypeActivatedCheck<T>(builder, name, failureStatus, tags: null, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new type activated health check with the specified name and implementation.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The health check implementation type.</typeparam>
|
||||
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
|
||||
/// <param name="name">The name of the health check.</param>
|
||||
/// <param name="failureStatus">
|
||||
/// The <see cref="HealthStatus"/> that should be reported when the health check reports a failure. If the provided value
|
||||
/// is <c>null</c>, then <see cref="HealthStatus.Unhealthy"/> will be reported.
|
||||
/// </param>
|
||||
/// <param name="tags">A list of tags that can be used to filter health checks.</param>
|
||||
/// <param name="args">Additional arguments to provide to the constructor.</param>
|
||||
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
|
||||
/// <remarks>
|
||||
/// This method will use <see cref="ActivatorUtilities.CreateInstance{T}(IServiceProvider, object[])"/> to create the health check
|
||||
/// instance when needed. Additional arguments can be provided to the constructor via <paramref name="args"/>.
|
||||
/// </remarks>
|
||||
public static IHealthChecksBuilder AddTypeActivatedCheck<T>(
|
||||
this IHealthChecksBuilder builder,
|
||||
string name,
|
||||
HealthStatus? failureStatus,
|
||||
IEnumerable<string> tags,
|
||||
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<T>(s, args), failureStatus, tags));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new type activated health check with the specified name and implementation.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The health check implementation type.</typeparam>
|
||||
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
|
||||
/// <param name="name">The name of the health check.</param>
|
||||
/// <param name="failureStatus">
|
||||
/// The <see cref="HealthStatus"/> that should be reported when the health check reports a failure. If the provided value
|
||||
/// is <c>null</c>, then <see cref="HealthStatus.Unhealthy"/> will be reported.
|
||||
/// </param>
|
||||
/// <param name="tags">A list of tags that can be used to filter health checks.</param>
|
||||
/// <param name="args">Additional arguments to provide to the constructor.</param>
|
||||
/// <param name="timeout">A <see cref="TimeSpan"/> representing the timeout of the check.</param>
|
||||
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
|
||||
/// <remarks>
|
||||
/// This method will use <see cref="ActivatorUtilities.CreateInstance{T}(IServiceProvider, object[])"/> to create the health check
|
||||
/// instance when needed. Additional arguments can be provided to the constructor via <paramref name="args"/>.
|
||||
/// </remarks>
|
||||
public static IHealthChecksBuilder AddTypeActivatedCheck<T>(
|
||||
this IHealthChecksBuilder builder,
|
||||
string name,
|
||||
HealthStatus? failureStatus,
|
||||
IEnumerable<string> 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<T>(s, args), failureStatus, tags, timeout));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,229 @@
|
|||
// 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;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides extension methods for registering delegates with the <see cref="IHealthChecksBuilder"/>.
|
||||
/// </summary>
|
||||
public static class HealthChecksBuilderDelegateExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a new health check with the specified name and implementation.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
|
||||
/// <param name="name">The name of the health check.</param>
|
||||
/// <param name="tags">A list of tags that can be used to filter health checks.</param>
|
||||
/// <param name="check">A delegate that provides the health check implementation.</param>
|
||||
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
|
||||
// 2.0 BACKCOMPAT OVERLOAD -- DO NOT TOUCH
|
||||
public static IHealthChecksBuilder AddCheck(
|
||||
this IHealthChecksBuilder builder,
|
||||
string name,
|
||||
Func<HealthCheckResult> check,
|
||||
IEnumerable<string> tags)
|
||||
{
|
||||
return AddCheck(builder, name, check, tags, default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new health check with the specified name and implementation.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
|
||||
/// <param name="name">The name of the health check.</param>
|
||||
/// <param name="tags">A list of tags that can be used to filter health checks.</param>
|
||||
/// <param name="check">A delegate that provides the health check implementation.</param>
|
||||
/// <param name="timeout">An optional <see cref="TimeSpan"/> representing the timeout of the check.</param>
|
||||
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
|
||||
public static IHealthChecksBuilder AddCheck(
|
||||
this IHealthChecksBuilder builder,
|
||||
string name,
|
||||
Func<HealthCheckResult> check,
|
||||
IEnumerable<string> tags = null,
|
||||
TimeSpan? timeout = default)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
if (name == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
if (check == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(check));
|
||||
}
|
||||
|
||||
var instance = new DelegateHealthCheck((ct) => Task.FromResult(check()));
|
||||
return builder.Add(new HealthCheckRegistration(name, instance, failureStatus: null, tags, timeout));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new health check with the specified name and implementation.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
|
||||
/// <param name="name">The name of the health check.</param>
|
||||
/// <param name="tags">A list of tags that can be used to filter health checks.</param>
|
||||
/// <param name="check">A delegate that provides the health check implementation.</param>
|
||||
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
|
||||
// 2.0 BACKCOMPAT OVERLOAD -- DO NOT TOUCH
|
||||
public static IHealthChecksBuilder AddCheck(
|
||||
this IHealthChecksBuilder builder,
|
||||
string name,
|
||||
Func<CancellationToken, HealthCheckResult> check,
|
||||
IEnumerable<string> tags)
|
||||
{
|
||||
return AddCheck(builder, name, check, tags, default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new health check with the specified name and implementation.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
|
||||
/// <param name="name">The name of the health check.</param>
|
||||
/// <param name="tags">A list of tags that can be used to filter health checks.</param>
|
||||
/// <param name="check">A delegate that provides the health check implementation.</param>
|
||||
/// <param name="timeout">An optional <see cref="TimeSpan"/> representing the timeout of the check.</param>
|
||||
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
|
||||
public static IHealthChecksBuilder AddCheck(
|
||||
this IHealthChecksBuilder builder,
|
||||
string name,
|
||||
Func<CancellationToken, HealthCheckResult> check,
|
||||
IEnumerable<string> tags = null,
|
||||
TimeSpan? timeout = default)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
if (name == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
if (check == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(check));
|
||||
}
|
||||
|
||||
var instance = new DelegateHealthCheck((ct) => Task.FromResult(check(ct)));
|
||||
return builder.Add(new HealthCheckRegistration(name, instance, failureStatus: null, tags, timeout));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new health check with the specified name and implementation.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
|
||||
/// <param name="name">The name of the health check.</param>
|
||||
/// <param name="tags">A list of tags that can be used to filter health checks.</param>
|
||||
/// <param name="check">A delegate that provides the health check implementation.</param>
|
||||
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
|
||||
// 2.0 BACKCOMPAT OVERLOAD -- DO NOT TOUCH
|
||||
public static IHealthChecksBuilder AddAsyncCheck(
|
||||
this IHealthChecksBuilder builder,
|
||||
string name,
|
||||
Func<Task<HealthCheckResult>> check,
|
||||
IEnumerable<string> tags)
|
||||
{
|
||||
return AddAsyncCheck(builder, name, check, tags, default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new health check with the specified name and implementation.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
|
||||
/// <param name="name">The name of the health check.</param>
|
||||
/// <param name="tags">A list of tags that can be used to filter health checks.</param>
|
||||
/// <param name="check">A delegate that provides the health check implementation.</param>
|
||||
/// <param name="timeout">An optional <see cref="TimeSpan"/> representing the timeout of the check.</param>
|
||||
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
|
||||
public static IHealthChecksBuilder AddAsyncCheck(
|
||||
this IHealthChecksBuilder builder,
|
||||
string name,
|
||||
Func<Task<HealthCheckResult>> check,
|
||||
IEnumerable<string> tags = null,
|
||||
TimeSpan? timeout = default)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
if (name == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
if (check == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(check));
|
||||
}
|
||||
|
||||
var instance = new DelegateHealthCheck((ct) => check());
|
||||
return builder.Add(new HealthCheckRegistration(name, instance, failureStatus: null, tags, timeout));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new health check with the specified name and implementation.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
|
||||
/// <param name="name">The name of the health check.</param>
|
||||
/// <param name="tags">A list of tags that can be used to filter health checks.</param>
|
||||
/// <param name="check">A delegate that provides the health check implementation.</param>
|
||||
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
|
||||
// 2.0 BACKCOMPAT OVERLOAD -- DO NOT TOUCH
|
||||
public static IHealthChecksBuilder AddAsyncCheck(
|
||||
this IHealthChecksBuilder builder,
|
||||
string name,
|
||||
Func<CancellationToken, Task<HealthCheckResult>> check,
|
||||
IEnumerable<string> tags)
|
||||
{
|
||||
return AddAsyncCheck(builder, name, check, tags, default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new health check with the specified name and implementation.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
|
||||
/// <param name="name">The name of the health check.</param>
|
||||
/// <param name="tags">A list of tags that can be used to filter health checks.</param>
|
||||
/// <param name="check">A delegate that provides the health check implementation.</param>
|
||||
/// <param name="timeout">An optional <see cref="TimeSpan"/> representing the timeout of the check.</param>
|
||||
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
|
||||
public static IHealthChecksBuilder AddAsyncCheck(
|
||||
this IHealthChecksBuilder builder,
|
||||
string name,
|
||||
Func<CancellationToken, Task<HealthCheckResult>> check,
|
||||
IEnumerable<string> tags = null,
|
||||
TimeSpan? timeout = default)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
if (name == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
if (check == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(check));
|
||||
}
|
||||
|
||||
var instance = new DelegateHealthCheck((ct) => check(ct));
|
||||
return builder.Add(new HealthCheckRegistration(name, instance, failureStatus: null, tags, timeout));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
// 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.Extensions.Diagnostics.HealthChecks;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
{
|
||||
/// <summary>
|
||||
/// A builder used to register health checks.
|
||||
/// </summary>
|
||||
public interface IHealthChecksBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a <see cref="HealthCheckRegistration"/> for a health check.
|
||||
/// </summary>
|
||||
/// <param name="registration">The <see cref="HealthCheckRegistration"/>.</param>
|
||||
IHealthChecksBuilder Add(HealthCheckRegistration registration);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="IServiceCollection"/> into which <see cref="IHealthCheck"/> instances should be registered.
|
||||
/// </summary>
|
||||
IServiceCollection Services { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
internal class HealthCheckLogScope : IReadOnlyList<KeyValuePair<string, object>>
|
||||
{
|
||||
public string HealthCheckName { get; }
|
||||
|
||||
int IReadOnlyCollection<KeyValuePair<string, object>>.Count { get; } = 1;
|
||||
|
||||
KeyValuePair<string, object> IReadOnlyList<KeyValuePair<string, object>>.this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (index == 0)
|
||||
{
|
||||
return new KeyValuePair<string, object>(nameof(HealthCheckName), HealthCheckName);
|
||||
}
|
||||
|
||||
throw new ArgumentOutOfRangeException(nameof(index));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="HealthCheckLogScope"/> with the provided name.
|
||||
/// </summary>
|
||||
/// <param name="healthCheckName">The name of the health check being executed.</param>
|
||||
public HealthCheckLogScope(string healthCheckName)
|
||||
{
|
||||
HealthCheckName = healthCheckName;
|
||||
}
|
||||
|
||||
IEnumerator<KeyValuePair<string, object>> IEnumerable<KeyValuePair<string, object>>.GetEnumerator()
|
||||
{
|
||||
yield return new KeyValuePair<string, object>(nameof(HealthCheckName), HealthCheckName);
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return ((IEnumerable<KeyValuePair<string, object>>)this).GetEnumerator();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,262 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Internal;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
internal sealed class HealthCheckPublisherHostedService : IHostedService
|
||||
{
|
||||
private readonly HealthCheckService _healthCheckService;
|
||||
private readonly IOptions<HealthCheckPublisherOptions> _options;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IHealthCheckPublisher[] _publishers;
|
||||
|
||||
private CancellationTokenSource _stopping;
|
||||
private Timer _timer;
|
||||
|
||||
public HealthCheckPublisherHostedService(
|
||||
HealthCheckService healthCheckService,
|
||||
IOptions<HealthCheckPublisherOptions> options,
|
||||
ILogger<HealthCheckPublisherHostedService> logger,
|
||||
IEnumerable<IHealthCheckPublisher> publishers)
|
||||
{
|
||||
if (healthCheckService == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(healthCheckService));
|
||||
}
|
||||
|
||||
if (options == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
if (logger == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
if (publishers == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(publishers));
|
||||
}
|
||||
|
||||
_healthCheckService = healthCheckService;
|
||||
_options = options;
|
||||
_logger = logger;
|
||||
_publishers = publishers.ToArray();
|
||||
|
||||
_stopping = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
internal bool IsStopping => _stopping.IsCancellationRequested;
|
||||
|
||||
internal bool IsTimerRunning => _timer != null;
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_publishers.Length == 0)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// IMPORTANT - make sure this is the last thing that happens in this method. The timer can
|
||||
// fire before other code runs.
|
||||
_timer = NonCapturingTimer.Create(Timer_Tick, null, dueTime: _options.Value.Delay, period: _options.Value.Period);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
_stopping.Cancel();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore exceptions thrown as a result of a cancellation.
|
||||
}
|
||||
|
||||
if (_publishers.Length == 0)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
_timer?.Dispose();
|
||||
_timer = null;
|
||||
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// Yes, async void. We need to be async. We need to be void. We handle the exceptions in RunAsync
|
||||
private async void Timer_Tick(object state)
|
||||
{
|
||||
await RunAsync();
|
||||
}
|
||||
|
||||
// Internal for testing
|
||||
internal async Task RunAsync()
|
||||
{
|
||||
var duration = ValueStopwatch.StartNew();
|
||||
Logger.HealthCheckPublisherProcessingBegin(_logger);
|
||||
|
||||
CancellationTokenSource cancellation = null;
|
||||
try
|
||||
{
|
||||
var timeout = _options.Value.Timeout;
|
||||
|
||||
cancellation = CancellationTokenSource.CreateLinkedTokenSource(_stopping.Token);
|
||||
cancellation.CancelAfter(timeout);
|
||||
|
||||
await RunAsyncCore(cancellation.Token);
|
||||
|
||||
Logger.HealthCheckPublisherProcessingEnd(_logger, duration.GetElapsedTime());
|
||||
}
|
||||
catch (OperationCanceledException) when (IsStopping)
|
||||
{
|
||||
// This is a cancellation - if the app is shutting down we want to ignore it. Otherwise, it's
|
||||
// a timeout and we want to log it.
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// This is an error, publishing failed.
|
||||
Logger.HealthCheckPublisherProcessingEnd(_logger, duration.GetElapsedTime(), ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
cancellation.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunAsyncCore(CancellationToken cancellationToken)
|
||||
{
|
||||
// Forcibly yield - we want to unblock the timer thread.
|
||||
await Task.Yield();
|
||||
|
||||
// The health checks service does it's own logging, and doesn't throw exceptions.
|
||||
var report = await _healthCheckService.CheckHealthAsync(_options.Value.Predicate, cancellationToken);
|
||||
|
||||
var publishers = _publishers;
|
||||
var tasks = new Task[publishers.Length];
|
||||
for (var i = 0; i < publishers.Length; i++)
|
||||
{
|
||||
tasks[i] = RunPublisherAsync(publishers[i], report, cancellationToken);
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
private async Task RunPublisherAsync(IHealthCheckPublisher publisher, HealthReport report, CancellationToken cancellationToken)
|
||||
{
|
||||
var duration = ValueStopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
Logger.HealthCheckPublisherBegin(_logger, publisher);
|
||||
|
||||
await publisher.PublishAsync(report, cancellationToken);
|
||||
Logger.HealthCheckPublisherEnd(_logger, publisher, duration.GetElapsedTime());
|
||||
}
|
||||
catch (OperationCanceledException) when (IsStopping)
|
||||
{
|
||||
// This is a cancellation - if the app is shutting down we want to ignore it. Otherwise, it's
|
||||
// a timeout and we want to log it.
|
||||
}
|
||||
catch (OperationCanceledException ocex)
|
||||
{
|
||||
Logger.HealthCheckPublisherTimeout(_logger, publisher, duration.GetElapsedTime());
|
||||
throw ocex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.HealthCheckPublisherError(_logger, publisher, duration.GetElapsedTime(), ex);
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
internal static class EventIds
|
||||
{
|
||||
public static readonly EventId HealthCheckPublisherProcessingBegin = new EventId(100, "HealthCheckPublisherProcessingBegin");
|
||||
public static readonly EventId HealthCheckPublisherProcessingEnd = new EventId(101, "HealthCheckPublisherProcessingEnd");
|
||||
public static readonly EventId HealthCheckPublisherProcessingError = new EventId(101, "HealthCheckPublisherProcessingError");
|
||||
|
||||
public static readonly EventId HealthCheckPublisherBegin = new EventId(102, "HealthCheckPublisherBegin");
|
||||
public static readonly EventId HealthCheckPublisherEnd = new EventId(103, "HealthCheckPublisherEnd");
|
||||
public static readonly EventId HealthCheckPublisherError = new EventId(104, "HealthCheckPublisherError");
|
||||
public static readonly EventId HealthCheckPublisherTimeout = new EventId(104, "HealthCheckPublisherTimeout");
|
||||
}
|
||||
|
||||
private static class Logger
|
||||
{
|
||||
private static readonly Action<ILogger, Exception> _healthCheckPublisherProcessingBegin = LoggerMessage.Define(
|
||||
LogLevel.Debug,
|
||||
EventIds.HealthCheckPublisherProcessingBegin,
|
||||
"Running health check publishers");
|
||||
|
||||
private static readonly Action<ILogger, double, Exception> _healthCheckPublisherProcessingEnd = LoggerMessage.Define<double>(
|
||||
LogLevel.Debug,
|
||||
EventIds.HealthCheckPublisherProcessingEnd,
|
||||
"Health check publisher processing completed after {ElapsedMilliseconds}ms");
|
||||
|
||||
private static readonly Action<ILogger, IHealthCheckPublisher, Exception> _healthCheckPublisherBegin = LoggerMessage.Define<IHealthCheckPublisher>(
|
||||
LogLevel.Debug,
|
||||
EventIds.HealthCheckPublisherBegin,
|
||||
"Running health check publisher '{HealthCheckPublisher}'");
|
||||
|
||||
private static readonly Action<ILogger, IHealthCheckPublisher, double, Exception> _healthCheckPublisherEnd = LoggerMessage.Define<IHealthCheckPublisher, double>(
|
||||
LogLevel.Debug,
|
||||
EventIds.HealthCheckPublisherEnd,
|
||||
"Health check '{HealthCheckPublisher}' completed after {ElapsedMilliseconds}ms");
|
||||
|
||||
private static readonly Action<ILogger, IHealthCheckPublisher, double, Exception> _healthCheckPublisherError = LoggerMessage.Define<IHealthCheckPublisher, double>(
|
||||
LogLevel.Error,
|
||||
EventIds.HealthCheckPublisherError,
|
||||
"Health check {HealthCheckPublisher} threw an unhandled exception after {ElapsedMilliseconds}ms");
|
||||
|
||||
private static readonly Action<ILogger, IHealthCheckPublisher, double, Exception> _healthCheckPublisherTimeout = LoggerMessage.Define<IHealthCheckPublisher, double>(
|
||||
LogLevel.Error,
|
||||
EventIds.HealthCheckPublisherTimeout,
|
||||
"Health check {HealthCheckPublisher} was canceled after {ElapsedMilliseconds}ms");
|
||||
|
||||
public static void HealthCheckPublisherProcessingBegin(ILogger logger)
|
||||
{
|
||||
_healthCheckPublisherProcessingBegin(logger, null);
|
||||
}
|
||||
|
||||
public static void HealthCheckPublisherProcessingEnd(ILogger logger, TimeSpan duration, Exception exception = null)
|
||||
{
|
||||
_healthCheckPublisherProcessingEnd(logger, duration.TotalMilliseconds, exception);
|
||||
}
|
||||
|
||||
public static void HealthCheckPublisherBegin(ILogger logger, IHealthCheckPublisher publisher)
|
||||
{
|
||||
_healthCheckPublisherBegin(logger, publisher, null);
|
||||
}
|
||||
|
||||
public static void HealthCheckPublisherEnd(ILogger logger, IHealthCheckPublisher publisher, TimeSpan duration)
|
||||
{
|
||||
_healthCheckPublisherEnd(logger, publisher, duration.TotalMilliseconds, null);
|
||||
}
|
||||
|
||||
public static void HealthCheckPublisherError(ILogger logger, IHealthCheckPublisher publisher, TimeSpan duration, Exception exception)
|
||||
{
|
||||
_healthCheckPublisherError(logger, publisher, duration.TotalMilliseconds, exception);
|
||||
}
|
||||
|
||||
public static void HealthCheckPublisherTimeout(ILogger logger, IHealthCheckPublisher publisher, TimeSpan duration)
|
||||
{
|
||||
_healthCheckPublisherTimeout(logger, publisher, duration.TotalMilliseconds, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for the default service that executes <see cref="IHealthCheckPublisher"/> instances.
|
||||
/// </summary>
|
||||
public sealed class HealthCheckPublisherOptions
|
||||
{
|
||||
private TimeSpan _delay;
|
||||
private TimeSpan _period;
|
||||
|
||||
public HealthCheckPublisherOptions()
|
||||
{
|
||||
_delay = TimeSpan.FromSeconds(5);
|
||||
_period = TimeSpan.FromSeconds(30);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the initial delay applied after the application starts before executing
|
||||
/// <see cref="IHealthCheckPublisher"/> instances. The delay is applied once at startup, and does
|
||||
/// not apply to subsequent iterations. The default value is 5 seconds.
|
||||
/// </summary>
|
||||
public TimeSpan Delay
|
||||
{
|
||||
get => _delay;
|
||||
set
|
||||
{
|
||||
if (value == System.Threading.Timeout.InfiniteTimeSpan)
|
||||
{
|
||||
throw new ArgumentException($"The {nameof(Delay)} must not be infinite.", nameof(value));
|
||||
}
|
||||
|
||||
_delay = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the period of <see cref="IHealthCheckPublisher"/> execution. The default value is
|
||||
/// 30 seconds.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The <see cref="Period"/> cannot be set to a value lower than 1 second.
|
||||
/// </remarks>
|
||||
public TimeSpan Period
|
||||
{
|
||||
get => _period;
|
||||
set
|
||||
{
|
||||
if (value < TimeSpan.FromSeconds(1))
|
||||
{
|
||||
throw new ArgumentException($"The {nameof(Period)} must be greater than or equal to one second.", nameof(value));
|
||||
}
|
||||
|
||||
if (value == System.Threading.Timeout.InfiniteTimeSpan)
|
||||
{
|
||||
throw new ArgumentException($"The {nameof(Period)} must not be infinite.", nameof(value));
|
||||
}
|
||||
|
||||
_period = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a predicate that is used to filter the set of health checks executed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If <see cref="Predicate"/> is <c>null</c>, the health check publisher service will run all
|
||||
/// registered health checks - this is the default behavior. To run a subset of health checks,
|
||||
/// provide a function that filters the set of checks. The predicate will be evaluated each period.
|
||||
/// </remarks>
|
||||
public Func<HealthCheckRegistration, bool> Predicate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the timeout for executing the health checks an all <see cref="IHealthCheckPublisher"/>
|
||||
/// instances. Use <see cref="System.Threading.Timeout.InfiniteTimeSpan"/> to execute with no timeout.
|
||||
/// The default value is 30 seconds.
|
||||
/// </summary>
|
||||
public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(30);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
// 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;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
/// <summary>
|
||||
/// A service which can be used to check the status of <see cref="IHealthCheck"/> instances
|
||||
/// registered in the application.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The default implementation of <see cref="HealthCheckService"/> is registered in the dependency
|
||||
/// injection container as a singleton service by calling
|
||||
/// <see cref="HealthCheckServiceCollectionExtensions.AddHealthChecks(IServiceCollection)"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The <see cref="IHealthChecksBuilder"/> returned by
|
||||
/// <see cref="HealthCheckServiceCollectionExtensions.AddHealthChecks(IServiceCollection)"/>
|
||||
/// provides a convenience API for registering health checks.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <see cref="IHealthCheck"/> implementations can be registered through extension methods provided by
|
||||
/// <see cref="IHealthChecksBuilder"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public abstract class HealthCheckService
|
||||
{
|
||||
/// <summary>
|
||||
/// Runs all the health checks in the application and returns the aggregated status.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A <see cref="CancellationToken"/> which can be used to cancel the health checks.</param>
|
||||
/// <returns>
|
||||
/// A <see cref="Task{T}"/> which will complete when all the health checks have been run,
|
||||
/// yielding a <see cref="HealthReport"/> containing the results.
|
||||
/// </returns>
|
||||
public Task<HealthReport> CheckHealthAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return CheckHealthAsync(predicate: null, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the provided health checks and returns the aggregated status
|
||||
/// </summary>
|
||||
/// <param name="predicate">
|
||||
/// A predicate that can be used to include health checks based on user-defined criteria.
|
||||
/// </param>
|
||||
/// <param name="cancellationToken">A <see cref="CancellationToken"/> which can be used to cancel the health checks.</param>
|
||||
/// <returns>
|
||||
/// A <see cref="Task{T}"/> which will complete when all the health checks have been run,
|
||||
/// yielding a <see cref="HealthReport"/> containing the results.
|
||||
/// </returns>
|
||||
public abstract Task<HealthReport> CheckHealthAsync(
|
||||
Func<HealthCheckRegistration, bool> predicate,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for the default implementation of <see cref="HealthCheckService"/>
|
||||
/// </summary>
|
||||
public sealed class HealthCheckServiceOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the health check registrations.
|
||||
/// </summary>
|
||||
public ICollection<HealthCheckRegistration> Registrations { get; } = new List<HealthCheckRegistration>();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<Description>Components for performing health checks in .NET applications
|
||||
|
||||
Commonly Used Types:
|
||||
Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckService
|
||||
Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder
|
||||
</Description>
|
||||
<TargetFrameworks>netstandard2.0;$(DefaultNetCoreTargetFramework)</TargetFrameworks>
|
||||
<TargetFrameworks Condition="'$(DotNetBuildFromSource)' == 'true'">$(DefaultNetCoreTargetFramework)</TargetFrameworks>
|
||||
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<PackageTags>diagnostics;healthchecks</PackageTags>
|
||||
<IsPackable>true</IsPackable>
|
||||
<IsShipping>true</IsShipping>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="Microsoft.Extensions.Diagnostics.HealthChecks.Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="$(SharedSourceRoot)NonCapturingTimer\**\*.cs" />
|
||||
<Compile Include="$(SharedSourceRoot)ValueStopwatch\**\*.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" />
|
||||
<Reference Include="Microsoft.Extensions.Hosting.Abstractions" />
|
||||
<Reference Include="Microsoft.Extensions.Options" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,534 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.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;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
public class DefaultHealthCheckServiceTest
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_ThrowsUsefulExceptionForDuplicateNames()
|
||||
{
|
||||
// Arrange
|
||||
//
|
||||
// Doing this the old fashioned way so we can verify that the exception comes
|
||||
// from the constructor.
|
||||
var serviceCollection = new ServiceCollection();
|
||||
serviceCollection.AddLogging();
|
||||
serviceCollection.AddOptions();
|
||||
serviceCollection.AddHealthChecks()
|
||||
.AddCheck("Foo", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy())))
|
||||
.AddCheck("Foo", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy())))
|
||||
.AddCheck("Bar", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy())))
|
||||
.AddCheck("Baz", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy())))
|
||||
.AddCheck("Baz", new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy())));
|
||||
|
||||
var services = serviceCollection.BuildServiceProvider();
|
||||
|
||||
var scopeFactory = services.GetRequiredService<IServiceScopeFactory>();
|
||||
var options = services.GetRequiredService<IOptions<HealthCheckServiceOptions>>();
|
||||
var logger = services.GetRequiredService<ILogger<DefaultHealthCheckService>>();
|
||||
|
||||
// Act
|
||||
var exception = Assert.Throws<ArgumentException>(() => new DefaultHealthCheckService(scopeFactory, options, logger));
|
||||
|
||||
// Assert
|
||||
Assert.StartsWith($"Duplicate health checks were registered with the name(s): Foo, Baz", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckAsync_RunsAllChecksAndAggregatesResultsAsync()
|
||||
{
|
||||
const string DataKey = "Foo";
|
||||
const string DataValue = "Bar";
|
||||
const string DegradedMessage = "I'm not feeling so good";
|
||||
const string UnhealthyMessage = "Halp!";
|
||||
const string HealthyMessage = "Everything is A-OK";
|
||||
var exception = new Exception("Things are pretty bad!");
|
||||
var healthyCheckTags = new List<string> { "healthy-check-tag" };
|
||||
var degradedCheckTags = new List<string> { "degraded-check-tag" };
|
||||
var unhealthyCheckTags = new List<string> { "unhealthy-check-tag" };
|
||||
|
||||
// Arrange
|
||||
var data = new Dictionary<string, object>()
|
||||
{
|
||||
{ DataKey, DataValue }
|
||||
};
|
||||
|
||||
var service = CreateHealthChecksService(b =>
|
||||
{
|
||||
b.AddAsyncCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage, data)), healthyCheckTags);
|
||||
b.AddAsyncCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Degraded(DegradedMessage)), degradedCheckTags);
|
||||
b.AddAsyncCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Unhealthy(UnhealthyMessage, exception)), unhealthyCheckTags);
|
||||
});
|
||||
|
||||
// Act
|
||||
var results = await service.CheckHealthAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
results.Entries.OrderBy(kvp => kvp.Key),
|
||||
actual =>
|
||||
{
|
||||
Assert.Equal("DegradedCheck", actual.Key);
|
||||
Assert.Equal(DegradedMessage, actual.Value.Description);
|
||||
Assert.Equal(HealthStatus.Degraded, actual.Value.Status);
|
||||
Assert.Null(actual.Value.Exception);
|
||||
Assert.Empty(actual.Value.Data);
|
||||
Assert.Equal(actual.Value.Tags, degradedCheckTags);
|
||||
},
|
||||
actual =>
|
||||
{
|
||||
Assert.Equal("HealthyCheck", actual.Key);
|
||||
Assert.Equal(HealthyMessage, actual.Value.Description);
|
||||
Assert.Equal(HealthStatus.Healthy, actual.Value.Status);
|
||||
Assert.Null(actual.Value.Exception);
|
||||
Assert.Collection(actual.Value.Data, item =>
|
||||
{
|
||||
Assert.Equal(DataKey, item.Key);
|
||||
Assert.Equal(DataValue, item.Value);
|
||||
});
|
||||
Assert.Equal(actual.Value.Tags, healthyCheckTags);
|
||||
},
|
||||
actual =>
|
||||
{
|
||||
Assert.Equal("UnhealthyCheck", actual.Key);
|
||||
Assert.Equal(UnhealthyMessage, actual.Value.Description);
|
||||
Assert.Equal(HealthStatus.Unhealthy, actual.Value.Status);
|
||||
Assert.Same(exception, actual.Value.Exception);
|
||||
Assert.Empty(actual.Value.Data);
|
||||
Assert.Equal(actual.Value.Tags, unhealthyCheckTags);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckAsync_RunsFilteredChecksAndAggregatesResultsAsync()
|
||||
{
|
||||
const string DataKey = "Foo";
|
||||
const string DataValue = "Bar";
|
||||
const string DegradedMessage = "I'm not feeling so good";
|
||||
const string UnhealthyMessage = "Halp!";
|
||||
const string HealthyMessage = "Everything is A-OK";
|
||||
var exception = new Exception("Things are pretty bad!");
|
||||
|
||||
// Arrange
|
||||
var data = new Dictionary<string, object>
|
||||
{
|
||||
{ DataKey, DataValue }
|
||||
};
|
||||
|
||||
var service = CreateHealthChecksService(b =>
|
||||
{
|
||||
b.AddAsyncCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage, data)));
|
||||
b.AddAsyncCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Degraded(DegradedMessage)));
|
||||
b.AddAsyncCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Unhealthy(UnhealthyMessage, exception)));
|
||||
});
|
||||
|
||||
// Act
|
||||
var results = await service.CheckHealthAsync(c => c.Name == "HealthyCheck");
|
||||
|
||||
// Assert
|
||||
Assert.Collection(results.Entries,
|
||||
actual =>
|
||||
{
|
||||
Assert.Equal("HealthyCheck", actual.Key);
|
||||
Assert.Equal(HealthyMessage, actual.Value.Description);
|
||||
Assert.Equal(HealthStatus.Healthy, actual.Value.Status);
|
||||
Assert.Null(actual.Value.Exception);
|
||||
Assert.Collection(actual.Value.Data, item =>
|
||||
{
|
||||
Assert.Equal(DataKey, item.Key);
|
||||
Assert.Equal(DataValue, item.Value);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckHealthAsync_SetsRegistrationForEachCheck()
|
||||
{
|
||||
// Arrange
|
||||
var thrownException = new InvalidOperationException("Whoops!");
|
||||
var faultedException = new InvalidOperationException("Ohnoes!");
|
||||
|
||||
var service = CreateHealthChecksService(b =>
|
||||
{
|
||||
b.AddCheck<NameCapturingCheck>("A");
|
||||
b.AddCheck<NameCapturingCheck>("B");
|
||||
b.AddCheck<NameCapturingCheck>("C");
|
||||
});
|
||||
|
||||
// Act
|
||||
var results = await service.CheckHealthAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
results.Entries,
|
||||
actual =>
|
||||
{
|
||||
Assert.Equal("A", actual.Key);
|
||||
Assert.Collection(
|
||||
actual.Value.Data,
|
||||
kvp => Assert.Equal(kvp, new KeyValuePair<string, object>("name", "A")));
|
||||
},
|
||||
actual =>
|
||||
{
|
||||
Assert.Equal("B", actual.Key);
|
||||
Assert.Collection(
|
||||
actual.Value.Data,
|
||||
kvp => Assert.Equal(kvp, new KeyValuePair<string, object>("name", "B")));
|
||||
},
|
||||
actual =>
|
||||
{
|
||||
Assert.Equal("C", actual.Key);
|
||||
Assert.Collection(
|
||||
actual.Value.Data,
|
||||
kvp => Assert.Equal(kvp, new KeyValuePair<string, object>("name", "C")));
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckHealthAsync_Cancellation_CanPropagate()
|
||||
{
|
||||
// Arrange
|
||||
var insideCheck = new TaskCompletionSource<object>();
|
||||
|
||||
var service = CreateHealthChecksService(b =>
|
||||
{
|
||||
b.AddAsyncCheck("cancels", async ct =>
|
||||
{
|
||||
insideCheck.SetResult(null);
|
||||
|
||||
await Task.Delay(10000, ct);
|
||||
return HealthCheckResult.Unhealthy();
|
||||
});
|
||||
});
|
||||
|
||||
var cancel = new CancellationTokenSource();
|
||||
var task = service.CheckHealthAsync(cancel.Token);
|
||||
|
||||
// After this returns we know the check has started
|
||||
await insideCheck.Task;
|
||||
|
||||
cancel.Cancel();
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<TaskCanceledException>(async () => await task);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckHealthAsync_ConvertsExceptionInHealthCheckToUnhealthyResultAsync()
|
||||
{
|
||||
// Arrange
|
||||
var thrownException = new InvalidOperationException("Whoops!");
|
||||
var faultedException = new InvalidOperationException("Ohnoes!");
|
||||
|
||||
var service = CreateHealthChecksService(b =>
|
||||
{
|
||||
b.AddAsyncCheck("Throws", ct => throw thrownException);
|
||||
b.AddAsyncCheck("Faults", ct => Task.FromException<HealthCheckResult>(faultedException));
|
||||
b.AddAsyncCheck("Succeeds", ct => Task.FromResult(HealthCheckResult.Healthy()));
|
||||
});
|
||||
|
||||
// Act
|
||||
var results = await service.CheckHealthAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
results.Entries,
|
||||
actual =>
|
||||
{
|
||||
Assert.Equal("Throws", actual.Key);
|
||||
Assert.Equal(thrownException.Message, actual.Value.Description);
|
||||
Assert.Equal(HealthStatus.Unhealthy, actual.Value.Status);
|
||||
Assert.Same(thrownException, actual.Value.Exception);
|
||||
},
|
||||
actual =>
|
||||
{
|
||||
Assert.Equal("Faults", actual.Key);
|
||||
Assert.Equal(faultedException.Message, actual.Value.Description);
|
||||
Assert.Equal(HealthStatus.Unhealthy, actual.Value.Status);
|
||||
Assert.Same(faultedException, actual.Value.Exception);
|
||||
},
|
||||
actual =>
|
||||
{
|
||||
Assert.Equal("Succeeds", actual.Key);
|
||||
Assert.Null(actual.Value.Description);
|
||||
Assert.Equal(HealthStatus.Healthy, actual.Value.Status);
|
||||
Assert.Null(actual.Value.Exception);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckHealthAsync_SetsUpALoggerScopeForEachCheck()
|
||||
{
|
||||
// Arrange
|
||||
var sink = new TestSink();
|
||||
var check = new DelegateHealthCheck(cancellationToken =>
|
||||
{
|
||||
Assert.Collection(sink.Scopes,
|
||||
actual =>
|
||||
{
|
||||
Assert.Equal(actual.LoggerName, typeof(DefaultHealthCheckService).FullName);
|
||||
Assert.Collection((IEnumerable<KeyValuePair<string, object>>)actual.Scope,
|
||||
item =>
|
||||
{
|
||||
Assert.Equal("HealthCheckName", item.Key);
|
||||
Assert.Equal("TestScope", item.Value);
|
||||
});
|
||||
});
|
||||
return Task.FromResult(HealthCheckResult.Healthy());
|
||||
});
|
||||
|
||||
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
|
||||
var service = CreateHealthChecksService(b =>
|
||||
{
|
||||
// Override the logger factory for testing
|
||||
b.Services.AddSingleton<ILoggerFactory>(loggerFactory);
|
||||
|
||||
b.AddCheck("TestScope", check);
|
||||
});
|
||||
|
||||
// Act
|
||||
var results = await service.CheckHealthAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Collection(results.Entries, actual =>
|
||||
{
|
||||
Assert.Equal("TestScope", actual.Key);
|
||||
Assert.Equal(HealthStatus.Healthy, actual.Value.Status);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckHealthAsync_CheckCanDependOnTransientService()
|
||||
{
|
||||
// Arrange
|
||||
var service = CreateHealthChecksService(b =>
|
||||
{
|
||||
b.Services.AddTransient<AnotherService>();
|
||||
|
||||
b.AddCheck<CheckWithServiceDependency>("Test");
|
||||
});
|
||||
|
||||
// Act
|
||||
var results = await service.CheckHealthAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
results.Entries,
|
||||
actual =>
|
||||
{
|
||||
Assert.Equal("Test", actual.Key);
|
||||
Assert.Equal(HealthStatus.Healthy, actual.Value.Status);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckHealthAsync_CheckCanDependOnScopedService()
|
||||
{
|
||||
// Arrange
|
||||
var service = CreateHealthChecksService(b =>
|
||||
{
|
||||
b.Services.AddScoped<AnotherService>();
|
||||
|
||||
b.AddCheck<CheckWithServiceDependency>("Test");
|
||||
});
|
||||
|
||||
// Act
|
||||
var results = await service.CheckHealthAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
results.Entries,
|
||||
actual =>
|
||||
{
|
||||
Assert.Equal("Test", actual.Key);
|
||||
Assert.Equal(HealthStatus.Healthy, actual.Value.Status);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckHealthAsync_CheckCanDependOnSingletonService()
|
||||
{
|
||||
// Arrange
|
||||
var service = CreateHealthChecksService(b =>
|
||||
{
|
||||
b.Services.AddSingleton<AnotherService>();
|
||||
|
||||
b.AddCheck<CheckWithServiceDependency>("Test");
|
||||
});
|
||||
|
||||
// Act
|
||||
var results = await service.CheckHealthAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
results.Entries,
|
||||
actual =>
|
||||
{
|
||||
Assert.Equal("Test", actual.Key);
|
||||
Assert.Equal(HealthStatus.Healthy, actual.Value.Status);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckHealthAsync_ChecksAreRunInParallel()
|
||||
{
|
||||
// Arrange
|
||||
var input1 = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var input2 = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var output1 = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var output2 = new TaskCompletionSource<object>(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<IHealthChecksBuilder> configure)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging();
|
||||
services.AddOptions();
|
||||
|
||||
var builder = services.AddHealthChecks();
|
||||
if (configure != null)
|
||||
{
|
||||
configure(builder);
|
||||
}
|
||||
|
||||
return (DefaultHealthCheckService)services.BuildServiceProvider(validateScopes: true).GetRequiredService<HealthCheckService>();
|
||||
}
|
||||
|
||||
private class AnotherService { }
|
||||
|
||||
private class CheckWithServiceDependency : IHealthCheck
|
||||
{
|
||||
public CheckWithServiceDependency(AnotherService _)
|
||||
{
|
||||
}
|
||||
|
||||
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(HealthCheckResult.Healthy());
|
||||
}
|
||||
}
|
||||
|
||||
private class NameCapturingCheck : IHealthCheck
|
||||
{
|
||||
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var data = new Dictionary<string, object>()
|
||||
{
|
||||
{ "name", context.Registration.Name },
|
||||
};
|
||||
return Task.FromResult(HealthCheckResult.Healthy(data: data));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,257 @@
|
|||
// 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;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
{
|
||||
// Integration tests for extension methods on IHealthCheckBuilder
|
||||
//
|
||||
// We test the longest overload of each 'family' of Add...Check methods, since they chain to each other.
|
||||
public class HealthChecksBuilderTest
|
||||
{
|
||||
[Fact]
|
||||
public void AddCheck_Instance()
|
||||
{
|
||||
// Arrange
|
||||
var instance = new DelegateHealthCheck((_) =>
|
||||
{
|
||||
return Task.FromResult(HealthCheckResult.Healthy());
|
||||
});
|
||||
|
||||
var services = CreateServices();
|
||||
services.AddHealthChecks().AddCheck("test", failureStatus: HealthStatus.Degraded,tags: new[] { "tag", }, instance: instance);
|
||||
|
||||
var serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
// Act
|
||||
var options = serviceProvider.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value;
|
||||
|
||||
// Assert
|
||||
var registration = Assert.Single(options.Registrations);
|
||||
Assert.Equal("test", registration.Name);
|
||||
Assert.Equal(HealthStatus.Degraded, registration.FailureStatus);
|
||||
Assert.Equal<string>(new[] { "tag", }, registration.Tags);
|
||||
Assert.Same(instance, registration.Factory(serviceProvider));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddCheck_T_TypeActivated()
|
||||
{
|
||||
// Arrange
|
||||
var services = CreateServices();
|
||||
services.AddHealthChecks().AddCheck<TestHealthCheck>("test", failureStatus: HealthStatus.Degraded, tags: new[] { "tag", });
|
||||
|
||||
var serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
// Act
|
||||
var options = serviceProvider.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value;
|
||||
|
||||
// Assert
|
||||
var registration = Assert.Single(options.Registrations);
|
||||
Assert.Equal("test", registration.Name);
|
||||
Assert.Equal(HealthStatus.Degraded, registration.FailureStatus);
|
||||
Assert.Equal<string>(new[] { "tag", }, registration.Tags);
|
||||
Assert.IsType<TestHealthCheck>(registration.Factory(serviceProvider));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddCheck_T_Service()
|
||||
{
|
||||
// Arrange
|
||||
var instance = new TestHealthCheck();
|
||||
|
||||
var services = CreateServices();
|
||||
services.AddSingleton(instance);
|
||||
services.AddHealthChecks().AddCheck<TestHealthCheck>("test", failureStatus: HealthStatus.Degraded, tags: new[] { "tag", });
|
||||
|
||||
var serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
// Act
|
||||
var options = serviceProvider.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value;
|
||||
|
||||
// Assert
|
||||
var registration = Assert.Single(options.Registrations);
|
||||
Assert.Equal("test", registration.Name);
|
||||
Assert.Equal(HealthStatus.Degraded, registration.FailureStatus);
|
||||
Assert.Equal<string>(new[] { "tag", }, registration.Tags);
|
||||
Assert.Same(instance, registration.Factory(serviceProvider));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddTypeActivatedCheck()
|
||||
{
|
||||
// Arrange
|
||||
var services = CreateServices();
|
||||
services
|
||||
.AddHealthChecks()
|
||||
.AddTypeActivatedCheck<TestHealthCheckWithArgs>("test", failureStatus: HealthStatus.Degraded, tags: new[] { "tag", }, args: new object[] { 5, "hi", });
|
||||
|
||||
var serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
// Act
|
||||
var options = serviceProvider.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value;
|
||||
|
||||
// Assert
|
||||
var registration = Assert.Single(options.Registrations);
|
||||
Assert.Equal("test", registration.Name);
|
||||
Assert.Equal(HealthStatus.Degraded, registration.FailureStatus);
|
||||
Assert.Equal<string>(new[] { "tag", }, registration.Tags);
|
||||
|
||||
var check = Assert.IsType<TestHealthCheckWithArgs>(registration.Factory(serviceProvider));
|
||||
Assert.Equal(5, check.I);
|
||||
Assert.Equal("hi", check.S);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddDelegateCheck_NoArg()
|
||||
{
|
||||
// Arrange
|
||||
var services = CreateServices();
|
||||
services.AddHealthChecks().AddCheck("test", tags: new[] { "tag", }, check: () =>
|
||||
{
|
||||
return HealthCheckResult.Healthy();
|
||||
});
|
||||
|
||||
var serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
// Act
|
||||
var options = serviceProvider.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value;
|
||||
|
||||
// Assert
|
||||
var registration = Assert.Single(options.Registrations);
|
||||
Assert.Equal("test", registration.Name);
|
||||
Assert.Equal(HealthStatus.Unhealthy, registration.FailureStatus);
|
||||
Assert.Equal<string>(new[] { "tag", }, registration.Tags);
|
||||
Assert.IsType<DelegateHealthCheck>(registration.Factory(serviceProvider));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddDelegateCheck_CancellationToken()
|
||||
{
|
||||
// Arrange
|
||||
var services = CreateServices();
|
||||
services.AddHealthChecks().AddCheck("test", (_) =>
|
||||
{
|
||||
return HealthCheckResult.Degraded();
|
||||
}, tags: new[] { "tag", });
|
||||
|
||||
var serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
// Act
|
||||
var options = serviceProvider.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value;
|
||||
|
||||
// Assert
|
||||
var registration = Assert.Single(options.Registrations);
|
||||
Assert.Equal("test", registration.Name);
|
||||
Assert.Equal(HealthStatus.Unhealthy, registration.FailureStatus);
|
||||
Assert.Equal<string>(new[] { "tag", }, registration.Tags);
|
||||
Assert.IsType<DelegateHealthCheck>(registration.Factory(serviceProvider));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddAsyncDelegateCheck_NoArg()
|
||||
{
|
||||
// Arrange
|
||||
var services = CreateServices();
|
||||
services.AddHealthChecks().AddAsyncCheck("test", () =>
|
||||
{
|
||||
return Task.FromResult(HealthCheckResult.Healthy());
|
||||
}, tags: new[] { "tag", });
|
||||
|
||||
var serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
// Act
|
||||
var options = serviceProvider.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value;
|
||||
|
||||
// Assert
|
||||
var registration = Assert.Single(options.Registrations);
|
||||
Assert.Equal("test", registration.Name);
|
||||
Assert.Equal(HealthStatus.Unhealthy, registration.FailureStatus);
|
||||
Assert.Equal<string>(new[] { "tag", }, registration.Tags);
|
||||
Assert.IsType<DelegateHealthCheck>(registration.Factory(serviceProvider));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddAsyncDelegateCheck_CancellationToken()
|
||||
{
|
||||
// Arrange
|
||||
var services = CreateServices();
|
||||
services.AddHealthChecks().AddAsyncCheck("test", (_) =>
|
||||
{
|
||||
return Task.FromResult(HealthCheckResult.Unhealthy());
|
||||
}, tags: new[] { "tag", });
|
||||
|
||||
var serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
// Act
|
||||
var options = serviceProvider.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value;
|
||||
|
||||
// Assert
|
||||
var registration = Assert.Single(options.Registrations);
|
||||
Assert.Equal("test", registration.Name);
|
||||
Assert.Equal(HealthStatus.Unhealthy, registration.FailureStatus);
|
||||
Assert.Equal<string>(new[] { "tag", }, registration.Tags);
|
||||
Assert.IsType<DelegateHealthCheck>(registration.Factory(serviceProvider));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChecksCanBeRegisteredInMultipleCallsToAddHealthChecks()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services
|
||||
.AddHealthChecks()
|
||||
.AddAsyncCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy()));
|
||||
services
|
||||
.AddHealthChecks()
|
||||
.AddAsyncCheck("Bar", () => Task.FromResult(HealthCheckResult.Healthy()));
|
||||
|
||||
// Act
|
||||
var options = services.BuildServiceProvider().GetRequiredService<IOptions<HealthCheckServiceOptions>>();
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
options.Value.Registrations,
|
||||
actual => Assert.Equal("Foo", actual.Name),
|
||||
actual => Assert.Equal("Bar", actual.Name));
|
||||
}
|
||||
|
||||
private IServiceCollection CreateServices()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging();
|
||||
services.AddOptions();
|
||||
return services;
|
||||
}
|
||||
|
||||
private class TestHealthCheck : IHealthCheck
|
||||
{
|
||||
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
private class TestHealthCheckWithArgs : IHealthCheck
|
||||
{
|
||||
public TestHealthCheckWithArgs(int i, string s)
|
||||
{
|
||||
I = i;
|
||||
S = s;
|
||||
}
|
||||
|
||||
public int I { get; set; }
|
||||
|
||||
public string S { get; set; }
|
||||
|
||||
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
{
|
||||
public class ServiceCollectionExtensionsTest
|
||||
{
|
||||
[Fact]
|
||||
public void AddHealthChecks_RegistersSingletonHealthCheckServiceIdempotently()
|
||||
{
|
||||
// Arrange
|
||||
var services = new ServiceCollection();
|
||||
|
||||
// Act
|
||||
services.AddHealthChecks();
|
||||
services.AddHealthChecks();
|
||||
|
||||
// Assert
|
||||
Assert.Collection(services.OrderBy(s => s.ServiceType.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(HealthCheckPublisherHostedService), actual.ImplementationType);
|
||||
Assert.Null(actual.ImplementationInstance);
|
||||
Assert.Null(actual.ImplementationFactory);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact] // see: https://github.com/dotnet/extensions/issues/639
|
||||
public void AddHealthChecks_RegistersPublisherService_WhenOtherHostedServicesRegistered()
|
||||
{
|
||||
// Arrange
|
||||
var services = new ServiceCollection();
|
||||
|
||||
// Act
|
||||
services.AddSingleton<IHostedService, DummyHostedService>();
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,528 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Testing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
public class HealthCheckPublisherHostedServiceTest
|
||||
{
|
||||
[Fact]
|
||||
public async Task StartAsync_WithoutPublishers_DoesNotStartTimer()
|
||||
{
|
||||
// Arrange
|
||||
var publishers = new IHealthCheckPublisher[]
|
||||
{
|
||||
};
|
||||
|
||||
var service = CreateService(publishers);
|
||||
|
||||
try
|
||||
{
|
||||
// Act
|
||||
await service.StartAsync();
|
||||
|
||||
// Assert
|
||||
Assert.False(service.IsTimerRunning);
|
||||
Assert.False(service.IsStopping);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await service.StopAsync();
|
||||
Assert.False(service.IsTimerRunning);
|
||||
Assert.True(service.IsStopping);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartAsync_WithPublishers_StartsTimer()
|
||||
{
|
||||
// Arrange
|
||||
var publishers = new IHealthCheckPublisher[]
|
||||
{
|
||||
new TestPublisher(),
|
||||
};
|
||||
|
||||
var service = CreateService(publishers);
|
||||
|
||||
try
|
||||
{
|
||||
// Act
|
||||
await service.StartAsync();
|
||||
|
||||
// Assert
|
||||
Assert.True(service.IsTimerRunning);
|
||||
Assert.False(service.IsStopping);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await service.StopAsync();
|
||||
Assert.False(service.IsTimerRunning);
|
||||
Assert.True(service.IsStopping);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartAsync_WithPublishers_StartsTimer_RunsPublishers()
|
||||
{
|
||||
// Arrange
|
||||
var unblock0 = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var unblock1 = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var unblock2 = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
var publishers = new TestPublisher[]
|
||||
{
|
||||
new TestPublisher() { Wait = unblock0.Task, },
|
||||
new TestPublisher() { Wait = unblock1.Task, },
|
||||
new TestPublisher() { Wait = unblock2.Task, },
|
||||
};
|
||||
|
||||
var service = CreateService(publishers, configure: (options) =>
|
||||
{
|
||||
options.Delay = TimeSpan.FromMilliseconds(0);
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
// Act
|
||||
await service.StartAsync();
|
||||
|
||||
await publishers[0].Started.TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||
await publishers[1].Started.TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||
await publishers[2].Started.TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||
|
||||
unblock0.SetResult(null);
|
||||
unblock1.SetResult(null);
|
||||
unblock2.SetResult(null);
|
||||
|
||||
// Assert
|
||||
Assert.True(service.IsTimerRunning);
|
||||
Assert.False(service.IsStopping);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await service.StopAsync();
|
||||
Assert.False(service.IsTimerRunning);
|
||||
Assert.True(service.IsStopping);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StopAsync_CancelsExecution()
|
||||
{
|
||||
// Arrange
|
||||
var unblock = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
var publishers = new TestPublisher[]
|
||||
{
|
||||
new TestPublisher() { Wait = unblock.Task, }
|
||||
};
|
||||
|
||||
var service = CreateService(publishers);
|
||||
|
||||
try
|
||||
{
|
||||
await service.StartAsync();
|
||||
|
||||
// Start execution
|
||||
var running = service.RunAsync();
|
||||
|
||||
// Wait for the publisher to see the cancellation token
|
||||
await publishers[0].Started.TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||
Assert.Single(publishers[0].Entries);
|
||||
|
||||
// Act
|
||||
await service.StopAsync(); // Trigger cancellation
|
||||
|
||||
// Assert
|
||||
await AssertCancelledAsync(publishers[0].Entries[0].cancellationToken);
|
||||
Assert.False(service.IsTimerRunning);
|
||||
Assert.True(service.IsStopping);
|
||||
|
||||
unblock.SetResult(null);
|
||||
|
||||
await running.TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||
}
|
||||
finally
|
||||
{
|
||||
await service.StopAsync();
|
||||
Assert.False(service.IsTimerRunning);
|
||||
Assert.True(service.IsStopping);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_WaitsForCompletion_Single()
|
||||
{
|
||||
// Arrange
|
||||
var sink = new TestSink();
|
||||
|
||||
var unblock = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
var publishers = new TestPublisher[]
|
||||
{
|
||||
new TestPublisher() { Wait = unblock.Task, },
|
||||
};
|
||||
|
||||
var service = CreateService(publishers, sink: sink);
|
||||
|
||||
try
|
||||
{
|
||||
await service.StartAsync();
|
||||
|
||||
// Act
|
||||
var running = service.RunAsync();
|
||||
|
||||
await publishers[0].Started.TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||
|
||||
unblock.SetResult(null);
|
||||
|
||||
await running.TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||
|
||||
// Assert
|
||||
Assert.True(service.IsTimerRunning);
|
||||
Assert.False(service.IsStopping);
|
||||
|
||||
for (var i = 0; i < publishers.Length; i++)
|
||||
{
|
||||
var report = Assert.Single(publishers[i].Entries).report;
|
||||
Assert.Equal(new[] { "one", "two", }, report.Entries.Keys.OrderBy(k => k));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
await service.StopAsync();
|
||||
Assert.False(service.IsTimerRunning);
|
||||
Assert.True(service.IsStopping);
|
||||
}
|
||||
|
||||
Assert.Collection(
|
||||
sink.Writes,
|
||||
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.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); },
|
||||
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherEnd, entry.EventId); },
|
||||
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherProcessingEnd, entry.EventId); });
|
||||
}
|
||||
|
||||
// Not testing logs here to avoid differences in logging order
|
||||
[Fact]
|
||||
public async Task RunAsync_WaitsForCompletion_Multiple()
|
||||
{
|
||||
// Arrange
|
||||
var unblock0 = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var unblock1 = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var unblock2 = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
var publishers = new TestPublisher[]
|
||||
{
|
||||
new TestPublisher() { Wait = unblock0.Task, },
|
||||
new TestPublisher() { Wait = unblock1.Task, },
|
||||
new TestPublisher() { Wait = unblock2.Task, },
|
||||
};
|
||||
|
||||
var service = CreateService(publishers);
|
||||
|
||||
try
|
||||
{
|
||||
await service.StartAsync();
|
||||
|
||||
// Act
|
||||
var running = service.RunAsync();
|
||||
|
||||
await publishers[0].Started.TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||
await publishers[1].Started.TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||
await publishers[2].Started.TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||
|
||||
unblock0.SetResult(null);
|
||||
unblock1.SetResult(null);
|
||||
unblock2.SetResult(null);
|
||||
|
||||
await running.TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||
|
||||
// Assert
|
||||
Assert.True(service.IsTimerRunning);
|
||||
Assert.False(service.IsStopping);
|
||||
|
||||
for (var i = 0; i < publishers.Length; i++)
|
||||
{
|
||||
var report = Assert.Single(publishers[i].Entries).report;
|
||||
Assert.Equal(new[] { "one", "two", }, report.Entries.Keys.OrderBy(k => k));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
await service.StopAsync();
|
||||
Assert.False(service.IsTimerRunning);
|
||||
Assert.True(service.IsStopping);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_PublishersCanTimeout()
|
||||
{
|
||||
// Arrange
|
||||
var sink = new TestSink();
|
||||
var unblock = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
var publishers = new TestPublisher[]
|
||||
{
|
||||
new TestPublisher() { Wait = unblock.Task, },
|
||||
};
|
||||
|
||||
var service = CreateService(publishers, sink: sink, configure: (options) =>
|
||||
{
|
||||
options.Timeout = TimeSpan.FromMilliseconds(50);
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
await service.StartAsync();
|
||||
|
||||
// Act
|
||||
var running = service.RunAsync();
|
||||
|
||||
await publishers[0].Started.TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||
|
||||
await AssertCancelledAsync(publishers[0].Entries[0].cancellationToken);
|
||||
|
||||
unblock.SetResult(null);
|
||||
|
||||
await running.TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||
|
||||
// Assert
|
||||
Assert.True(service.IsTimerRunning);
|
||||
Assert.False(service.IsStopping);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await service.StopAsync();
|
||||
Assert.False(service.IsTimerRunning);
|
||||
Assert.True(service.IsStopping);
|
||||
}
|
||||
|
||||
Assert.Collection(
|
||||
sink.Writes,
|
||||
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.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); },
|
||||
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherTimeout, entry.EventId); },
|
||||
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherProcessingEnd, entry.EventId); });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_CanFilterHealthChecks()
|
||||
{
|
||||
// Arrange
|
||||
var publishers = new TestPublisher[]
|
||||
{
|
||||
new TestPublisher(),
|
||||
new TestPublisher(),
|
||||
};
|
||||
|
||||
var service = CreateService(publishers, configure: (options) =>
|
||||
{
|
||||
options.Predicate = (r) => r.Name == "one";
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
await service.StartAsync();
|
||||
|
||||
// Act
|
||||
await service.RunAsync().TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||
|
||||
// Assert
|
||||
for (var i = 0; i < publishers.Length; i++)
|
||||
{
|
||||
var report = Assert.Single(publishers[i].Entries).report;
|
||||
Assert.Equal(new[] { "one", }, report.Entries.Keys.OrderBy(k => k));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
await service.StopAsync();
|
||||
Assert.False(service.IsTimerRunning);
|
||||
Assert.True(service.IsStopping);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_HandlesExceptions()
|
||||
{
|
||||
// Arrange
|
||||
var sink = new TestSink();
|
||||
var publishers = new TestPublisher[]
|
||||
{
|
||||
new TestPublisher() { Exception = new InvalidTimeZoneException(), },
|
||||
};
|
||||
|
||||
var service = CreateService(publishers, sink: sink);
|
||||
|
||||
try
|
||||
{
|
||||
await service.StartAsync();
|
||||
|
||||
// Act
|
||||
await service.RunAsync().TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||
|
||||
}
|
||||
finally
|
||||
{
|
||||
await service.StopAsync();
|
||||
Assert.False(service.IsTimerRunning);
|
||||
Assert.True(service.IsStopping);
|
||||
}
|
||||
|
||||
Assert.Collection(
|
||||
sink.Writes,
|
||||
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.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); },
|
||||
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherError, entry.EventId); },
|
||||
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherProcessingEnd, entry.EventId); });
|
||||
}
|
||||
|
||||
// Not testing logging here to avoid flaky ordering issues
|
||||
[Fact]
|
||||
public async Task RunAsync_HandlesExceptions_Multiple()
|
||||
{
|
||||
// Arrange
|
||||
var sink = new TestSink();
|
||||
var publishers = new TestPublisher[]
|
||||
{
|
||||
new TestPublisher() { Exception = new InvalidTimeZoneException(), },
|
||||
new TestPublisher(),
|
||||
new TestPublisher() { Exception = new InvalidTimeZoneException(), },
|
||||
};
|
||||
|
||||
var service = CreateService(publishers, sink: sink);
|
||||
|
||||
try
|
||||
{
|
||||
await service.StartAsync();
|
||||
|
||||
// Act
|
||||
await service.RunAsync().TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||
|
||||
}
|
||||
finally
|
||||
{
|
||||
await service.StopAsync();
|
||||
Assert.False(service.IsTimerRunning);
|
||||
Assert.True(service.IsStopping);
|
||||
}
|
||||
}
|
||||
|
||||
private HealthCheckPublisherHostedService CreateService(
|
||||
IHealthCheckPublisher[] publishers,
|
||||
Action<HealthCheckPublisherOptions> configure = null,
|
||||
TestSink sink = null)
|
||||
{
|
||||
var serviceCollection = new ServiceCollection();
|
||||
serviceCollection.AddOptions();
|
||||
serviceCollection.AddLogging();
|
||||
serviceCollection.AddHealthChecks()
|
||||
.AddCheck("one", () => { return HealthCheckResult.Healthy(); })
|
||||
.AddCheck("two", () => { return HealthCheckResult.Healthy(); });
|
||||
|
||||
// Choosing big values for tests to make sure that we're not dependent on the defaults.
|
||||
// All of the tests that rely on the timer will set their own values for speed.
|
||||
serviceCollection.Configure<HealthCheckPublisherOptions>(options =>
|
||||
{
|
||||
options.Delay = TimeSpan.FromMinutes(5);
|
||||
options.Period = TimeSpan.FromMinutes(5);
|
||||
options.Timeout = TimeSpan.FromMinutes(5);
|
||||
});
|
||||
|
||||
if (publishers != null)
|
||||
{
|
||||
for (var i = 0; i < publishers.Length; i++)
|
||||
{
|
||||
serviceCollection.AddSingleton<IHealthCheckPublisher>(publishers[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (configure != null)
|
||||
{
|
||||
serviceCollection.Configure(configure);
|
||||
}
|
||||
|
||||
if (sink != null)
|
||||
{
|
||||
serviceCollection.AddSingleton<ILoggerFactory>(new TestLoggerFactory(sink, enabled: true));
|
||||
}
|
||||
|
||||
var services = serviceCollection.BuildServiceProvider();
|
||||
return services.GetServices<IHostedService>().OfType< HealthCheckPublisherHostedService>().Single();
|
||||
}
|
||||
|
||||
private static async Task AssertCancelledAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await Assert.ThrowsAsync<TaskCanceledException>(() => Task.Delay(TimeSpan.FromSeconds(10), cancellationToken));
|
||||
}
|
||||
|
||||
private class TestPublisher : IHealthCheckPublisher
|
||||
{
|
||||
private TaskCompletionSource<object> _started;
|
||||
|
||||
public TestPublisher()
|
||||
{
|
||||
_started = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
}
|
||||
|
||||
public List<(HealthReport report, CancellationToken cancellationToken)> Entries { get; } = new List<(HealthReport report, CancellationToken cancellationToken)>();
|
||||
|
||||
public Exception Exception { get; set; }
|
||||
|
||||
public Task Started => _started.Task;
|
||||
|
||||
public Task Wait { get; set; }
|
||||
|
||||
public async Task PublishAsync(HealthReport report, CancellationToken cancellationToken)
|
||||
{
|
||||
Entries.Add((report, cancellationToken));
|
||||
|
||||
// Signal that we've started
|
||||
_started.SetResult(null);
|
||||
|
||||
if (Wait != null)
|
||||
{
|
||||
await Wait;
|
||||
}
|
||||
|
||||
if (Exception != null)
|
||||
{
|
||||
throw Exception;
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.Generic;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||
{
|
||||
public class HealthReportTest
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(HealthStatus.Healthy)]
|
||||
[InlineData(HealthStatus.Degraded)]
|
||||
[InlineData(HealthStatus.Unhealthy)]
|
||||
public void Status_MatchesWorstStatusInResults(HealthStatus status)
|
||||
{
|
||||
var result = new HealthReport(new Dictionary<string, HealthReportEntry>()
|
||||
{
|
||||
{"Foo", new HealthReportEntry(HealthStatus.Healthy, null,TimeSpan.MinValue, null, null) },
|
||||
{"Bar", new HealthReportEntry(HealthStatus.Healthy, null, TimeSpan.MinValue,null, null) },
|
||||
{"Baz", new HealthReportEntry(status, exception: null, description: null,duration:TimeSpan.MinValue, data: null) },
|
||||
{"Quick", new HealthReportEntry(HealthStatus.Healthy, null, TimeSpan.MinValue, null, null) },
|
||||
{"Quack", new HealthReportEntry(HealthStatus.Healthy, null, TimeSpan.MinValue, null, null) },
|
||||
{"Quock", new HealthReportEntry(HealthStatus.Healthy, null, TimeSpan.MinValue, null, null) },
|
||||
}, totalDuration: TimeSpan.MinValue);
|
||||
|
||||
Assert.Equal(status, result.Status);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(200)]
|
||||
[InlineData(300)]
|
||||
[InlineData(400)]
|
||||
public void TotalDuration_MatchesTotalDurationParameter(int milliseconds)
|
||||
{
|
||||
var result = new HealthReport(new Dictionary<string, HealthReportEntry>()
|
||||
{
|
||||
{"Foo", new HealthReportEntry(HealthStatus.Healthy, null,TimeSpan.MinValue, null, null) }
|
||||
}, totalDuration: TimeSpan.FromMilliseconds(milliseconds));
|
||||
|
||||
Assert.Equal(TimeSpan.FromMilliseconds(milliseconds), result.TotalDuration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<Import Project="$(RepoRoot)src\Logging\Logging.Testing\src\build\Microsoft.Extensions.Logging.Testing.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>$(DefaultNetCoreTargetFramework);net472</TargetFrameworks>
|
||||
<RootNamespace>Microsoft.Extensions.Diagnostics.HealthChecks</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
||||
<Reference Include="Microsoft.Extensions.DependencyInjection" />
|
||||
<Reference Include="Microsoft.Extensions.Diagnostics.HealthChecks" />
|
||||
<Reference Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" />
|
||||
<Reference Include="Microsoft.Extensions.Hosting.Abstractions" />
|
||||
<Reference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<Reference Include="Microsoft.Extensions.Logging.Testing" />
|
||||
<Reference Include="Microsoft.Extensions.Logging" />
|
||||
<Reference Include="Microsoft.Extensions.Options" />
|
||||
<Reference Include="Microsoft.Extensions.Primitives" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\..\..\Shared\test\SingleThreadedSynchronizationContext.cs" Link="Shared\SingleThreadedSynchronizationContext.cs"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"solution": {
|
||||
"path": "..\\..\\Extensions.sln",
|
||||
"projects": [
|
||||
"src\\JSInterop\\Microsoft.JSInterop\\src\\Microsoft.JSInterop.csproj",
|
||||
"src\\JSInterop\\Microsoft.JSInterop\\test\\Microsoft.JSInterop.Tests.csproj"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<Project>
|
||||
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), Directory.Build.props))\Directory.Build.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<PackageId>@microsoft/dotnet-js-interop</PackageId>
|
||||
<IsPackable>true</IsPackable>
|
||||
<IsTestProject>false</IsTestProject>
|
||||
<IsShipping>true</IsShipping>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), Directory.Build.targets))\Directory.Build.targets" />
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,348 @@
|
|||
{
|
||||
"name": "@microsoft/dotnet-js-interop",
|
||||
"version": "5.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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "@microsoft/dotnet-js-interop",
|
||||
"version": "5.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/dotnet/extensions.git"
|
||||
},
|
||||
"author": "Microsoft",
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/dotnet/extensions/issues"
|
||||
},
|
||||
"homepage": "https://github.com/dotnet/extensions/tree/master/src/JSInterop#readme",
|
||||
"files": [
|
||||
"dist/**"
|
||||
],
|
||||
"devDependencies": {
|
||||
"rimraf": "^2.5.4",
|
||||
"tslint": "^5.9.1",
|
||||
"typescript": "^2.7.1"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,298 @@
|
|||
// 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<any> } = {};
|
||||
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<T>(assemblyName: string, methodIdentifier: string, ...args: any[]): T {
|
||||
return invokePossibleInstanceMethod<T>(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<T>(assemblyName: string, methodIdentifier: string, ...args: any[]): Promise<T> {
|
||||
return invokePossibleInstanceMethodAsync(assemblyName, methodIdentifier, null, args);
|
||||
}
|
||||
|
||||
function invokePossibleInstanceMethod<T>(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, args: any[] | null): 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<T>(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, args: any[] | null): Promise<T> {
|
||||
if (assemblyName && dotNetObjectId) {
|
||||
throw new Error(`For instance method calls, assemblyName should be null. Received '${assemblyName}'.`) ;
|
||||
}
|
||||
|
||||
const asyncCallId = nextAsyncCallId++;
|
||||
const resultPromise = new Promise<T>((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<T> {
|
||||
resolve: (value?: T | PromiseLike<T>) => 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;
|
||||
|
||||
/**
|
||||
* Invoked by the runtime to complete an asynchronous JavaScript function call started from .NET
|
||||
*
|
||||
* @param callId A value identifying the asynchronous operation.
|
||||
* @param succeded Whether the operation succeeded or not.
|
||||
* @param resultOrError The serialized result or the serialized error from the async operation.
|
||||
*/
|
||||
endInvokeJSFromDotNet(callId: number, succeeded: boolean, resultOrError: any): 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, // Note that this is used by the JS interop code inside Mono WebAssembly itself
|
||||
|
||||
/**
|
||||
* 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<any>(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().endInvokeJSFromDotNet(asyncHandle, true, JSON.stringify([asyncHandle, true, result], argReplacer)),
|
||||
error => getRequiredDispatcher().endInvokeJSFromDotNet(asyncHandle, false, 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';
|
||||
let lastSegmentValue: any;
|
||||
identifier.split('.').forEach(segment => {
|
||||
if (segment in result) {
|
||||
lastSegmentValue = result;
|
||||
result = result[segment];
|
||||
resultIdentifier += '.' + segment;
|
||||
} else {
|
||||
throw new Error(`Could not find '${segment}' in '${resultIdentifier}'.`);
|
||||
}
|
||||
});
|
||||
|
||||
if (result instanceof Function) {
|
||||
result = result.bind(lastSegmentValue);
|
||||
cachedJSFunctions[identifier] = result;
|
||||
return result;
|
||||
} else {
|
||||
throw new Error(`The value '${resultIdentifier}' is not a function.`);
|
||||
}
|
||||
}
|
||||
|
||||
class DotNetObject {
|
||||
constructor(private _id: number) {
|
||||
}
|
||||
|
||||
public invokeMethod<T>(methodIdentifier: string, ...args: any[]): T {
|
||||
return invokePossibleInstanceMethod<T>(null, methodIdentifier, this._id, args);
|
||||
}
|
||||
|
||||
public invokeMethodAsync<T>(methodIdentifier: string, ...args: any[]): Promise<T> {
|
||||
return invokePossibleInstanceMethodAsync<T>(null, methodIdentifier, this._id, args);
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
const promise = invokePossibleInstanceMethodAsync<any>(null, '__Dispose', this._id, null);
|
||||
promise.catch(error => console.error(error));
|
||||
}
|
||||
|
||||
public serializeAsArg() {
|
||||
return { __dotNetObject: this._id };
|
||||
}
|
||||
}
|
||||
|
||||
const dotNetObjectRefKey = '__dotNetObject';
|
||||
attachReviver(function reviveDotNetObject(key: any, value: any) {
|
||||
if (value && typeof value === 'object' && value.hasOwnProperty(dotNetObjectRefKey)) {
|
||||
return new DotNetObject(value.__dotNetObject);
|
||||
}
|
||||
|
||||
// Unrecognized - let another reviver handle it
|
||||
return value;
|
||||
});
|
||||
|
||||
function argReplacer(key: string, value: any) {
|
||||
return value instanceof DotNetObject ? value.serializeAsArg() : value;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue