602 lines
22 KiB
C#
602 lines
22 KiB
C#
// <copyright file="ResourceTest.cs" company="OpenTelemetry Authors">
|
|
// Copyright The OpenTelemetry Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
// </copyright>
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Xunit;
|
|
|
|
namespace OpenTelemetry.Resources.Tests
|
|
{
|
|
public class ResourceTest : IDisposable
|
|
{
|
|
private const string KeyName = "key";
|
|
private const string ValueName = "value";
|
|
|
|
public ResourceTest()
|
|
{
|
|
ClearEnvVars();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
ClearEnvVars();
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
[Fact]
|
|
public void CreateResource_NullAttributeCollection()
|
|
{
|
|
// Act and Assert
|
|
var resource = new Resource(null);
|
|
Assert.Empty(resource.Attributes);
|
|
}
|
|
|
|
[Fact]
|
|
public void CreateResource_NullAttributeValue()
|
|
{
|
|
// Arrange
|
|
var attributes = new Dictionary<string, object> { { "NullValue", null } };
|
|
|
|
// Act and Assert
|
|
Assert.Throws<ArgumentException>(() => new Resource(attributes));
|
|
}
|
|
|
|
[Fact]
|
|
public void CreateResource_EmptyAttributeKey()
|
|
{
|
|
// Arrange
|
|
var attributes = new Dictionary<string, object> { { string.Empty, "value" } };
|
|
|
|
// Act
|
|
var resource = new Resource(attributes);
|
|
|
|
// Assert
|
|
Assert.Single(resource.Attributes);
|
|
|
|
var attribute = resource.Attributes.Single();
|
|
Assert.Empty(attribute.Key);
|
|
Assert.Equal("value", attribute.Value);
|
|
}
|
|
|
|
[Fact]
|
|
public void CreateResource_EmptyAttributeValue()
|
|
{
|
|
// Arrange
|
|
var attributes = new Dictionary<string, object> { { "EmptyValue", string.Empty } };
|
|
|
|
// does not throw
|
|
var resource = new Resource(attributes);
|
|
|
|
// Assert
|
|
Assert.Single(resource.Attributes);
|
|
Assert.Contains(new KeyValuePair<string, object>("EmptyValue", string.Empty), resource.Attributes);
|
|
}
|
|
|
|
[Fact]
|
|
public void CreateResource_EmptyArray()
|
|
{
|
|
// Arrange
|
|
var attributes = new Dictionary<string, object> { { "EmptyArray", Array.Empty<string>() } };
|
|
|
|
// does not throw
|
|
var resource = new Resource(attributes);
|
|
|
|
// Assert
|
|
Assert.Single(resource.Attributes);
|
|
Assert.Equal(Array.Empty<string>(), resource.Attributes.Where(x => x.Key == "EmptyArray").FirstOrDefault().Value);
|
|
}
|
|
|
|
[Fact]
|
|
public void CreateResource_EmptyAttribute()
|
|
{
|
|
// Arrange
|
|
var attributeCount = 0;
|
|
var attributes = CreateAttributes(attributeCount);
|
|
|
|
// Act
|
|
var resource = new Resource(attributes);
|
|
|
|
// Assert
|
|
ValidateResource(resource, attributeCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void CreateResource_SingleAttribute()
|
|
{
|
|
// Arrange
|
|
var attributeCount = 1;
|
|
var attributes = CreateAttributes(attributeCount);
|
|
|
|
// Act
|
|
var resource = new Resource(attributes);
|
|
|
|
// Assert
|
|
ValidateResource(resource, attributeCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void CreateResource_MultipleAttribute()
|
|
{
|
|
// Arrange
|
|
var attributeCount = 5;
|
|
var attributes = CreateAttributes(attributeCount);
|
|
|
|
// Act
|
|
var resource = new Resource(attributes);
|
|
|
|
// Assert
|
|
ValidateResource(resource, attributeCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void CreateResource_SupportedAttributeTypes()
|
|
{
|
|
// Arrange
|
|
var attributes = new Dictionary<string, object>
|
|
{
|
|
{ "string", "stringValue" },
|
|
{ "bool", true },
|
|
{ "double", 0.1d },
|
|
{ "long", 1L },
|
|
|
|
// int and float supported by conversion to long and double
|
|
{ "int", 1 },
|
|
{ "short", (short)1 },
|
|
{ "float", 0.1f },
|
|
};
|
|
|
|
// Act
|
|
var resource = new Resource(attributes);
|
|
|
|
// Assert
|
|
Assert.Equal(7, resource.Attributes.Count());
|
|
Assert.Contains(new KeyValuePair<string, object>("string", "stringValue"), resource.Attributes);
|
|
Assert.Contains(new KeyValuePair<string, object>("bool", true), resource.Attributes);
|
|
Assert.Contains(new KeyValuePair<string, object>("double", 0.1d), resource.Attributes);
|
|
Assert.Contains(new KeyValuePair<string, object>("long", 1L), resource.Attributes);
|
|
|
|
Assert.Contains(new KeyValuePair<string, object>("int", 1L), resource.Attributes);
|
|
Assert.Contains(new KeyValuePair<string, object>("short", 1L), resource.Attributes);
|
|
|
|
double convertedFloat = Convert.ToDouble(0.1f, System.Globalization.CultureInfo.InvariantCulture);
|
|
Assert.Contains(new KeyValuePair<string, object>("float", convertedFloat), resource.Attributes);
|
|
}
|
|
|
|
[Fact]
|
|
public void CreateResource_SupportedAttributeArrayTypes()
|
|
{
|
|
// Arrange
|
|
var attributes = new Dictionary<string, object>
|
|
{
|
|
// natively supported array types
|
|
{ "string arr", new string[] { "stringValue" } },
|
|
{ "bool arr", new bool[] { true } },
|
|
{ "double arr", new double[] { 0.1d } },
|
|
{ "long arr", new long[] { 1L } },
|
|
|
|
// have to convert to other primitive array types
|
|
{ "int arr", new int[] { 1 } },
|
|
{ "short arr", new short[] { (short)1 } },
|
|
{ "float arr", new float[] { 0.1f } },
|
|
};
|
|
|
|
// Act
|
|
var resource = new Resource(attributes);
|
|
|
|
// Assert
|
|
Assert.Equal(7, resource.Attributes.Count());
|
|
Assert.Equal(new string[] { "stringValue" }, resource.Attributes.Where(x => x.Key == "string arr").FirstOrDefault().Value);
|
|
Assert.Equal(new bool[] { true }, resource.Attributes.Where(x => x.Key == "bool arr").FirstOrDefault().Value);
|
|
Assert.Equal(new double[] { 0.1d }, resource.Attributes.Where(x => x.Key == "double arr").FirstOrDefault().Value);
|
|
Assert.Equal(new long[] { 1L }, resource.Attributes.Where(x => x.Key == "long arr").FirstOrDefault().Value);
|
|
|
|
var longArr = new long[] { 1 };
|
|
var doubleArr = new double[] { Convert.ToDouble(0.1f, System.Globalization.CultureInfo.InvariantCulture) };
|
|
Assert.Equal(longArr, resource.Attributes.Where(x => x.Key == "int arr").FirstOrDefault().Value);
|
|
Assert.Equal(longArr, resource.Attributes.Where(x => x.Key == "short arr").FirstOrDefault().Value);
|
|
Assert.Equal(doubleArr, resource.Attributes.Where(x => x.Key == "float arr").FirstOrDefault().Value);
|
|
}
|
|
|
|
[Fact]
|
|
public void CreateResource_NotSupportedAttributeTypes()
|
|
{
|
|
var attributes = new Dictionary<string, object>
|
|
{
|
|
{ "dynamic", new { } },
|
|
{ "array", new int[1] },
|
|
{ "complex", this },
|
|
};
|
|
|
|
Assert.Throws<ArgumentException>(() => new Resource(attributes));
|
|
}
|
|
|
|
[Fact]
|
|
public void MergeResource_EmptyAttributeSource_MultiAttributeTarget()
|
|
{
|
|
// Arrange
|
|
var sourceAttributeCount = 0;
|
|
var sourceAttributes = CreateAttributes(sourceAttributeCount);
|
|
var sourceResource = new Resource(sourceAttributes);
|
|
|
|
var otherAttributeCount = 3;
|
|
var otherAttributes = CreateAttributes(otherAttributeCount);
|
|
var otherResource = new Resource(otherAttributes);
|
|
|
|
// Act
|
|
var newResource = sourceResource.Merge(otherResource);
|
|
|
|
// Assert
|
|
Assert.NotSame(otherResource, newResource);
|
|
Assert.NotSame(sourceResource, newResource);
|
|
|
|
ValidateResource(newResource, sourceAttributeCount + otherAttributeCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void MergeResource_MultiAttributeSource_EmptyAttributeTarget()
|
|
{
|
|
// Arrange
|
|
var sourceAttributeCount = 3;
|
|
var sourceAttributes = CreateAttributes(sourceAttributeCount);
|
|
var sourceResource = new Resource(sourceAttributes);
|
|
|
|
var otherAttributeCount = 0;
|
|
var otherAttributes = CreateAttributes(otherAttributeCount);
|
|
var otherResource = new Resource(otherAttributes);
|
|
|
|
// Act
|
|
var newResource = sourceResource.Merge(otherResource);
|
|
|
|
// Assert
|
|
Assert.NotSame(otherResource, newResource);
|
|
Assert.NotSame(sourceResource, newResource);
|
|
ValidateResource(newResource, sourceAttributeCount + otherAttributeCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void MergeResource_MultiAttributeSource_MultiAttributeTarget_NoOverlap()
|
|
{
|
|
// Arrange
|
|
var sourceAttributeCount = 3;
|
|
var sourceAttributes = CreateAttributes(sourceAttributeCount);
|
|
var sourceResource = new Resource(sourceAttributes);
|
|
|
|
var otherAttributeCount = 3;
|
|
var otherAttributes = CreateAttributes(otherAttributeCount, sourceAttributeCount);
|
|
var otherResource = new Resource(otherAttributes);
|
|
|
|
// Act
|
|
var newResource = sourceResource.Merge(otherResource);
|
|
|
|
// Assert
|
|
Assert.NotSame(otherResource, newResource);
|
|
Assert.NotSame(sourceResource, newResource);
|
|
ValidateResource(newResource, sourceAttributeCount + otherAttributeCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void MergeResource_MultiAttributeSource_MultiAttributeTarget_SingleOverlap()
|
|
{
|
|
// Arrange
|
|
var sourceAttributeCount = 3;
|
|
var sourceAttributes = CreateAttributes(sourceAttributeCount);
|
|
var sourceResource = new Resource(sourceAttributes);
|
|
|
|
var otherAttributeCount = 3;
|
|
var otherAttributes = CreateAttributes(otherAttributeCount, sourceAttributeCount - 1);
|
|
var otherResource = new Resource(otherAttributes);
|
|
|
|
// Act
|
|
var newResource = sourceResource.Merge(otherResource);
|
|
|
|
// Assert
|
|
Assert.NotSame(otherResource, newResource);
|
|
Assert.NotSame(sourceResource, newResource);
|
|
ValidateResource(newResource, sourceAttributeCount + otherAttributeCount - 1);
|
|
|
|
// Also verify target attributes were not overwritten
|
|
foreach (var otherAttribute in otherAttributes)
|
|
{
|
|
Assert.Contains(otherAttribute, otherResource.Attributes);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void MergeResource_MultiAttributeSource_MultiAttributeTarget_FullOverlap()
|
|
{
|
|
// Arrange
|
|
var sourceAttributeCount = 3;
|
|
var sourceAttributes = CreateAttributes(sourceAttributeCount);
|
|
var sourceResource = new Resource(sourceAttributes);
|
|
|
|
var otherAttributeCount = 3;
|
|
var otherAttributes = CreateAttributes(otherAttributeCount);
|
|
var otherResource = new Resource(otherAttributes);
|
|
|
|
// Act
|
|
var newResource = sourceResource.Merge(otherResource);
|
|
|
|
// Assert
|
|
Assert.NotSame(otherResource, newResource);
|
|
Assert.NotSame(sourceResource, newResource);
|
|
ValidateResource(newResource, otherAttributeCount);
|
|
|
|
// Also verify target attributes were not overwritten
|
|
foreach (var otherAttribute in otherAttributes)
|
|
{
|
|
Assert.Contains(otherAttribute, otherResource.Attributes);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void MergeResource_MultiAttributeSource_DuplicatedKeysInPrimary()
|
|
{
|
|
// Arrange
|
|
var sourceAttributes = new List<KeyValuePair<string, object>>
|
|
{
|
|
new KeyValuePair<string, object>("key1", "value1"),
|
|
new KeyValuePair<string, object>("key1", "value1.1"),
|
|
};
|
|
var sourceResource = new Resource(sourceAttributes);
|
|
|
|
var otherAttributes = new List<KeyValuePair<string, object>>
|
|
{
|
|
new KeyValuePair<string, object>("key2", "value2"),
|
|
};
|
|
|
|
var otherResource = new Resource(otherAttributes);
|
|
|
|
// Act
|
|
var newResource = sourceResource.Merge(otherResource);
|
|
|
|
// Assert
|
|
Assert.NotSame(otherResource, newResource);
|
|
Assert.NotSame(sourceResource, newResource);
|
|
|
|
Assert.Equal(2, newResource.Attributes.Count());
|
|
Assert.Contains(new KeyValuePair<string, object>("key1", "value1"), newResource.Attributes);
|
|
Assert.Contains(new KeyValuePair<string, object>("key2", "value2"), newResource.Attributes);
|
|
}
|
|
|
|
[Fact]
|
|
public void MergeResource_UpdatingResourceOverridesCurrentResource()
|
|
{
|
|
// Arrange
|
|
var currentAttributes = new Dictionary<string, object> { { "value", "currentValue" } };
|
|
var updatingAttributes = new Dictionary<string, object> { { "value", "updatedValue" } };
|
|
var currentResource = new Resource(currentAttributes);
|
|
var updatingResource = new Resource(updatingAttributes);
|
|
|
|
var newResource = currentResource.Merge(updatingResource);
|
|
|
|
// Assert
|
|
Assert.Single(newResource.Attributes);
|
|
Assert.Contains(new KeyValuePair<string, object>("value", "updatedValue"), newResource.Attributes);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetResourceWithTelemetrySDKAttributes()
|
|
{
|
|
// Arrange
|
|
var resource = ResourceBuilder.CreateDefault().AddTelemetrySdk().Build();
|
|
|
|
// Assert
|
|
var attributes = resource.Attributes;
|
|
Assert.Equal(4, attributes.Count());
|
|
ValidateTelemetrySdkAttributes(attributes);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetResourceWithDefaultAttributes_EmptyResource()
|
|
{
|
|
// Arrange
|
|
var resource = ResourceBuilder.CreateDefault().Build();
|
|
|
|
// Assert
|
|
var attributes = resource.Attributes;
|
|
Assert.Single(attributes);
|
|
ValidateDefaultAttributes(attributes);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetResourceWithDefaultAttributes_ResourceWithAttrs()
|
|
{
|
|
// Arrange
|
|
var resource = ResourceBuilder.CreateDefault().AddAttributes(CreateAttributes(2)).Build();
|
|
|
|
// Assert
|
|
var attributes = resource.Attributes;
|
|
Assert.Equal(3, attributes.Count());
|
|
ValidateAttributes(attributes, 0, 1);
|
|
ValidateDefaultAttributes(attributes);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetResourceWithDefaultAttributes_WithResourceEnvVar()
|
|
{
|
|
// Arrange
|
|
Environment.SetEnvironmentVariable(OtelEnvResourceDetector.EnvVarKey, "EVKey1=EVVal1,EVKey2=EVVal2");
|
|
var resource = ResourceBuilder.CreateDefault().AddAttributes(CreateAttributes(2)).Build();
|
|
|
|
// Assert
|
|
var attributes = resource.Attributes;
|
|
Assert.Equal(5, attributes.Count());
|
|
ValidateAttributes(attributes, 0, 1);
|
|
ValidateDefaultAttributes(attributes);
|
|
Assert.Contains(new KeyValuePair<string, object>("EVKey1", "EVVal1"), attributes);
|
|
Assert.Contains(new KeyValuePair<string, object>("EVKey2", "EVVal2"), attributes);
|
|
}
|
|
|
|
[Fact]
|
|
public void EnvironmentVariableDetectors_DoNotDuplicateAttributes()
|
|
{
|
|
// Arrange
|
|
Environment.SetEnvironmentVariable(OtelEnvResourceDetector.EnvVarKey, "EVKey1=EVVal1,EVKey2=EVVal2");
|
|
var resource = ResourceBuilder.CreateDefault().AddEnvironmentVariableDetector().AddEnvironmentVariableDetector().Build();
|
|
|
|
// Assert
|
|
var attributes = resource.Attributes;
|
|
Assert.Equal(3, attributes.Count());
|
|
Assert.Contains(new KeyValuePair<string, object>("EVKey1", "EVVal1"), attributes);
|
|
Assert.Contains(new KeyValuePair<string, object>("EVKey2", "EVVal2"), attributes);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetResource_WithServiceEnvVar()
|
|
{
|
|
// Arrange
|
|
Environment.SetEnvironmentVariable(OtelServiceNameEnvVarDetector.EnvVarKey, "some-service");
|
|
var resource = ResourceBuilder.CreateDefault().AddAttributes(CreateAttributes(2)).Build();
|
|
|
|
// Assert
|
|
var attributes = resource.Attributes;
|
|
Assert.Equal(3, attributes.Count());
|
|
ValidateAttributes(attributes, 0, 1);
|
|
Assert.Contains(new KeyValuePair<string, object>("service.name", "some-service"), attributes);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetResource_WithServiceNameSetWithTwoEnvVars()
|
|
{
|
|
// Arrange
|
|
Environment.SetEnvironmentVariable(OtelEnvResourceDetector.EnvVarKey, "service.name=from-resource-attr");
|
|
Environment.SetEnvironmentVariable(OtelServiceNameEnvVarDetector.EnvVarKey, "from-service-name");
|
|
var resource = ResourceBuilder.CreateDefault().AddAttributes(CreateAttributes(2)).Build();
|
|
|
|
// Assert
|
|
var attributes = resource.Attributes;
|
|
Assert.Equal(3, attributes.Count());
|
|
ValidateAttributes(attributes, 0, 1);
|
|
Assert.Contains(new KeyValuePair<string, object>("service.name", "from-service-name"), attributes);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetResource_WithServiceNameSetWithTwoEnvVarsAndCode()
|
|
{
|
|
// Arrange
|
|
Environment.SetEnvironmentVariable(OtelEnvResourceDetector.EnvVarKey, "service.name=from-resource-attr");
|
|
Environment.SetEnvironmentVariable(OtelServiceNameEnvVarDetector.EnvVarKey, "from-service-name");
|
|
var resource = ResourceBuilder.CreateDefault().AddService("from-code").AddAttributes(CreateAttributes(2)).Build();
|
|
|
|
// Assert
|
|
var attributes = resource.Attributes;
|
|
Assert.Equal(4, attributes.Count());
|
|
ValidateAttributes(attributes, 0, 1);
|
|
Assert.Contains(new KeyValuePair<string, object>("service.name", "from-code"), attributes);
|
|
}
|
|
|
|
[Fact]
|
|
public void ResourceBuilder_ServiceProvider_Available()
|
|
{
|
|
var builder = ResourceBuilder.CreateDefault();
|
|
|
|
bool nullTestRun = false;
|
|
|
|
builder.AddDetector(sp =>
|
|
{
|
|
nullTestRun = true;
|
|
Assert.Null(sp);
|
|
return new NoopResourceDetector();
|
|
});
|
|
|
|
builder.Build();
|
|
|
|
Assert.True(nullTestRun);
|
|
|
|
builder = ResourceBuilder.CreateDefault();
|
|
|
|
bool validTestRun = false;
|
|
|
|
var serviceCollection = new ServiceCollection();
|
|
using var serviceProvider = serviceCollection.BuildServiceProvider();
|
|
|
|
builder.ServiceProvider = serviceProvider;
|
|
|
|
builder.AddDetector(sp =>
|
|
{
|
|
validTestRun = true;
|
|
Assert.NotNull(sp);
|
|
return new NoopResourceDetector();
|
|
});
|
|
|
|
builder.Build();
|
|
|
|
Assert.True(validTestRun);
|
|
}
|
|
|
|
private static void ClearEnvVars()
|
|
{
|
|
Environment.SetEnvironmentVariable(OtelEnvResourceDetector.EnvVarKey, null);
|
|
Environment.SetEnvironmentVariable(OtelServiceNameEnvVarDetector.EnvVarKey, null);
|
|
}
|
|
|
|
private static void AddAttributes(Dictionary<string, object> attributes, int attributeCount, int startIndex = 0)
|
|
{
|
|
for (var i = startIndex; i < attributeCount + startIndex; ++i)
|
|
{
|
|
attributes.Add($"{KeyName}{i}", $"{ValueName}{i}");
|
|
}
|
|
}
|
|
|
|
private static void ValidateAttributes(IEnumerable<KeyValuePair<string, object>> attributes, int startIndex = 0, int endIndex = 0)
|
|
{
|
|
var keyValuePairs = attributes as KeyValuePair<string, object>[] ?? attributes.ToArray();
|
|
var endInd = endIndex == 0 ? keyValuePairs.Length - 1 : endIndex;
|
|
for (var i = startIndex; i <= endInd; ++i)
|
|
{
|
|
Assert.Contains(new KeyValuePair<string, object>($"{KeyName}{i}", $"{ValueName}{i}"), keyValuePairs);
|
|
}
|
|
}
|
|
|
|
private static void ValidateResource(Resource resource, int attributeCount)
|
|
{
|
|
Assert.NotNull(resource);
|
|
Assert.NotNull(resource.Attributes);
|
|
Assert.Equal(attributeCount, resource.Attributes.Count());
|
|
ValidateAttributes(resource.Attributes);
|
|
}
|
|
|
|
private static void ValidateTelemetrySdkAttributes(IEnumerable<KeyValuePair<string, object>> attributes)
|
|
{
|
|
Assert.Contains(new KeyValuePair<string, object>("telemetry.sdk.name", "opentelemetry"), attributes);
|
|
Assert.Contains(new KeyValuePair<string, object>("telemetry.sdk.language", "dotnet"), attributes);
|
|
var versionAttribute = attributes.Where(pair => pair.Key.Equals("telemetry.sdk.version"));
|
|
Assert.Single(versionAttribute);
|
|
}
|
|
|
|
private static void ValidateDefaultAttributes(IEnumerable<KeyValuePair<string, object>> attributes)
|
|
{
|
|
var serviceName = attributes.Where(pair => pair.Key.Equals("service.name"));
|
|
Assert.Single(serviceName);
|
|
Assert.Contains("unknown_service", serviceName.FirstOrDefault().Value as string);
|
|
}
|
|
|
|
private static Dictionary<string, object> CreateAttributes(int attributeCount, int startIndex = 0)
|
|
{
|
|
var attributes = new Dictionary<string, object>();
|
|
AddAttributes(attributes, attributeCount, startIndex);
|
|
return attributes;
|
|
}
|
|
|
|
private sealed class NoopResourceDetector : IResourceDetector
|
|
{
|
|
public Resource Detect() => Resource.Empty;
|
|
}
|
|
}
|
|
}
|