Skip to content

Commit

Permalink
Add ResourceExtensions.HasAnnotation methods (#6357)
Browse files Browse the repository at this point in the history
Also modify the TryGetAnnotation methods to reduce allocations and remove stack-based recursion.

Expand test coverage.

Some XML doc tweaks.
  • Loading branch information
drewnoakes authored Nov 11, 2024
1 parent 2e60ee6 commit cc15725
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 37 deletions.
105 changes: 73 additions & 32 deletions src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public static class ResourceExtensions
/// <typeparam name="T">The type of the annotation to get.</typeparam>
/// <param name="resource">The resource to get the annotation from.</param>
/// <param name="annotation">When this method returns, contains the last annotation of the specified type from the resource, if found; otherwise, the default value for <typeparamref name="T"/>.</param>
/// <returns><c>true</c> if the last annotation of the specified type was found in the resource; otherwise, <c>false</c>.</returns>
/// <returns><see langword="true"/> if the last annotation of the specified type was found in the resource; otherwise, <see langword="false"/>.</returns>
public static bool TryGetLastAnnotation<T>(this IResource resource, [NotNullWhen(true)] out T? annotation) where T : IResourceAnnotation
{
if (resource.Annotations.OfType<T>().LastOrDefault() is { } lastAnnotation)
Expand All @@ -26,7 +26,7 @@ public static bool TryGetLastAnnotation<T>(this IResource resource, [NotNullWhen
}
else
{
annotation = default(T);
annotation = default;
return false;
}
}
Expand All @@ -36,15 +36,15 @@ public static bool TryGetLastAnnotation<T>(this IResource resource, [NotNullWhen
/// </summary>
/// <typeparam name="T">The type of annotation to retrieve.</typeparam>
/// <param name="resource">The resource to retrieve annotations from.</param>
/// <param name="result">When this method returns, contains the annotations of the specified type, if found; otherwise, null.</param>
/// <returns>true if annotations of the specified type were found; otherwise, false.</returns>
/// <param name="result">When this method returns, contains the annotations of the specified type, if found; otherwise, <see langword="null"/>.</param>
/// <returns><see langword="true"/> if annotations of the specified type were found; otherwise, <see langword="false"/>.</returns>
public static bool TryGetAnnotationsOfType<T>(this IResource resource, [NotNullWhen(true)] out IEnumerable<T>? result) where T : IResourceAnnotation
{
var matchingTypeAnnotations = resource.Annotations.OfType<T>();
var matchingTypeAnnotations = resource.Annotations.OfType<T>().ToArray();

if (matchingTypeAnnotations.Any())
if (matchingTypeAnnotations.Length is not 0)
{
result = matchingTypeAnnotations.ToArray();
result = matchingTypeAnnotations;
return true;
}
else
Expand All @@ -54,45 +54,86 @@ public static bool TryGetAnnotationsOfType<T>(this IResource resource, [NotNullW
}
}

/// <summary>
/// Gets whether <paramref name="resource"/> has an annotation of type <typeparamref name="T"/>
/// </summary>
/// <typeparam name="T">The type of annotation to retrieve.</typeparam>
/// <param name="resource">The resource to retrieve annotations from.</param>
/// <returns><see langword="true"/> if an annotation of the specified type was found; otherwise, <see langword="false"/>.</returns>
public static bool HasAnnotationOfType<T>(this IResource resource) where T : IResourceAnnotation
{
return resource.Annotations.Any(a => a is T);
}

/// <summary>
/// Attempts to retrieve all annotations of the specified type from the given resource including from parents.
/// </summary>
/// <typeparam name="T">The type of annotation to retrieve.</typeparam>
/// <param name="resource">The resource to retrieve annotations from.</param>
/// <param name="result">When this method returns, contains the annotations of the specified type, if found; otherwise, null.</param>
/// <returns>true if annotations of the specified type were found; otherwise, false.</returns>
/// <param name="result">When this method returns, contains the annotations of the specified type, if found; otherwise, <see langword="null"/>.</param>
/// <returns><see langword="true"/> if annotations of the specified type were found; otherwise, <see langword="false"/>.</returns>
public static bool TryGetAnnotationsIncludingAncestorsOfType<T>(this IResource resource, [NotNullWhen(true)] out IEnumerable<T>? result) where T : IResourceAnnotation
{
var matchingTypeAnnotations = resource.Annotations.OfType<T>();

if (resource is IResourceWithParent resourceWithParent)
if (resource is IResourceWithParent)
{
if (resourceWithParent.Parent.TryGetAnnotationsIncludingAncestorsOfType<T>(out var ancestorMatchingTypeAnnotations))
{
result = matchingTypeAnnotations.Concat(ancestorMatchingTypeAnnotations);
return true;
}
else if (matchingTypeAnnotations.Any())
{
result = matchingTypeAnnotations;
return true;
}
else
List<T>? annotations = null;

while (true)
{
result = null;
return false;
foreach (var annotation in resource.Annotations.OfType<T>())
{
annotations ??= [];
annotations.Add(annotation);
}

if (resource is IResourceWithParent child)
{
resource = child.Parent;
}
else
{
break;
}
}

result = annotations;
return annotations is not null;
}
else if (matchingTypeAnnotations.Any())
{
result = matchingTypeAnnotations.ToArray();
return true;
}
else

return TryGetAnnotationsOfType(resource, out result);
}

/// <summary>
/// Gets whether <paramref name="resource"/> or its ancestors have an annotation of type <typeparamref name="T"/>
/// </summary>
/// <typeparam name="T">The type of annotation to retrieve.</typeparam>
/// <param name="resource">The resource to retrieve annotations from.</param>
/// <returns><see langword="true"/> if an annotation of the specified type was found; otherwise, <see langword="false"/>.</returns>
public static bool HasAnnotationIncludingAncestorsOfType<T>(this IResource resource) where T : IResourceAnnotation
{
if (resource is IResourceWithParent)
{
result = null;
while (true)
{
if (HasAnnotationOfType<T>(resource))
{
return true;
}

if (resource is IResourceWithParent child)
{
resource = child.Parent;
}
else
{
break;
}
}

return false;
}

return HasAnnotationOfType<T>(resource);
}

/// <summary>
Expand Down
2 changes: 2 additions & 0 deletions src/Aspire.Hosting/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,8 @@ static Aspire.Hosting.ApplicationModel.CommandResults.Success() -> Aspire.Hostin
static Aspire.Hosting.ApplicationModel.ResourceExtensions.GetEnvironmentVariableValuesAsync(this Aspire.Hosting.ApplicationModel.IResourceWithEnvironment! resource, Aspire.Hosting.DistributedApplicationOperation applicationOperation = Aspire.Hosting.DistributedApplicationOperation.Run) -> System.Threading.Tasks.ValueTask<System.Collections.Generic.Dictionary<string!, string!>!>
Aspire.Hosting.ApplicationModel.ResourceNotificationService.WaitForResourceAsync(string! resourceName, string? targetState = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
Aspire.Hosting.ApplicationModel.ResourceNotificationService.WaitForResourceAsync(string! resourceName, System.Collections.Generic.IEnumerable<string!>! targetStates, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<string!>!
static Aspire.Hosting.ApplicationModel.ResourceExtensions.HasAnnotationIncludingAncestorsOfType<T>(this Aspire.Hosting.ApplicationModel.IResource! resource) -> bool
static Aspire.Hosting.ApplicationModel.ResourceExtensions.HasAnnotationOfType<T>(this Aspire.Hosting.ApplicationModel.IResource! resource) -> bool
static Aspire.Hosting.ApplicationModel.ResourceExtensions.TryGetAnnotationsIncludingAncestorsOfType<T>(this Aspire.Hosting.ApplicationModel.IResource! resource, out System.Collections.Generic.IEnumerable<T>? result) -> bool
static Aspire.Hosting.ContainerResourceBuilderExtensions.WithContainerName<T>(this Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>! builder, string! name) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>!
static Aspire.Hosting.ContainerResourceBuilderExtensions.WithImageRegistry<T>(this Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>! builder, string? registry) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>!
Expand Down
89 changes: 84 additions & 5 deletions tests/Aspire.Hosting.Tests/ResourceExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,36 @@ namespace Aspire.Hosting.Tests;
public class ResourceExtensionsTests
{
[Fact]
public void TryGetAnnotationOfTypeReturnsFalseWhenNoAnnotations()
public void TryGetAnnotationsOfTypeReturnsFalseWhenNoAnnotations()
{
using var builder = TestDistributedApplicationBuilder.Create();
var parent = builder.AddResource(new ParentResource("parent"));

Assert.False(parent.Resource.TryGetAnnotationsOfType<DummyAnnotation>(out _));
Assert.False(parent.Resource.HasAnnotationOfType<DummyAnnotation>());
Assert.False(parent.Resource.TryGetAnnotationsOfType<DummyAnnotation>(out var annotations));
Assert.Null(annotations);
}

[Fact]
public void TryGetAnnotationOfTypeReturnsTrueWhenNoAnnotations()
public void TryGetAnnotationsOfTypeReturnsFalseWhenOnlyAnnotationsOfOtherTypes()
{
using var builder = TestDistributedApplicationBuilder.Create();
var parent = builder.AddResource(new ParentResource("parent"))
.WithAnnotation(new AnotherDummyAnnotation());

Assert.False(parent.Resource.HasAnnotationOfType<DummyAnnotation>());
Assert.False(parent.Resource.TryGetAnnotationsOfType<DummyAnnotation>(out var annotations));
Assert.Null(annotations);
}

[Fact]
public void TryGetAnnotationsOfTypeReturnsTrueWhenNoAnnotations()
{
using var builder = TestDistributedApplicationBuilder.Create();
var parent = builder.AddResource(new ParentResource("parent"))
.WithAnnotation(new DummyAnnotation());

Assert.True(parent.Resource.HasAnnotationOfType<DummyAnnotation>());
Assert.True(parent.Resource.TryGetAnnotationsOfType<DummyAnnotation>(out var annotations));
Assert.Single(annotations);
}
Expand All @@ -36,10 +51,49 @@ public void TryGetAnnotationsIncludingAncestorsOfTypeReturnsAnnotationFromParent
var parent = builder.AddResource(new ParentResource("parent"))
.WithAnnotation(new DummyAnnotation());

Assert.True(parent.Resource.HasAnnotationIncludingAncestorsOfType<DummyAnnotation>());
Assert.True(parent.Resource.TryGetAnnotationsIncludingAncestorsOfType<DummyAnnotation>(out var annotations));
Assert.Single(annotations);
}

[Fact]
public void TryGetAnnotationIncludingAncestorsOfTypeReturnsFalseWhenNoAnnotations()
{
using var builder = TestDistributedApplicationBuilder.Create();
var parent = builder.AddResource(new ParentResource("parent"));

Assert.False(parent.Resource.HasAnnotationIncludingAncestorsOfType<DummyAnnotation>());
Assert.False(parent.Resource.TryGetAnnotationsIncludingAncestorsOfType<DummyAnnotation>(out var annotations));
Assert.Null(annotations);
}

[Fact]
public void TryGetAnnotationIncludingAncestorsOfTypeReturnsFalseWhenOnlyAnnotationsOfOtherTypes()
{
using var builder = TestDistributedApplicationBuilder.Create();
var parent = builder.AddResource(new ParentResource("parent"))
.WithAnnotation(new AnotherDummyAnnotation());

Assert.False(parent.Resource.HasAnnotationIncludingAncestorsOfType<DummyAnnotation>());
Assert.False(parent.Resource.TryGetAnnotationsIncludingAncestorsOfType<DummyAnnotation>(out var annotations));
Assert.Null(annotations);
}

[Fact]
public void TryGetAnnotationIncludingAncestorsOfTypeReturnsFalseWhenOnlyAnnotationsOfOtherTypesIncludingParent()
{
using var builder = TestDistributedApplicationBuilder.Create();
var parent = builder.AddResource(new ParentResource("parent"))
.WithAnnotation(new AnotherDummyAnnotation());

var child = builder.AddResource(new ChildResource("child", parent.Resource))
.WithAnnotation(new AnotherDummyAnnotation());

Assert.False(parent.Resource.HasAnnotationIncludingAncestorsOfType<DummyAnnotation>());
Assert.False(child.Resource.TryGetAnnotationsIncludingAncestorsOfType<DummyAnnotation>(out var annotations));
Assert.Null(annotations);
}

[Fact]
public void TryGetAnnotationsIncludingAncestorsOfTypeReturnsAnnotationFromParent()
{
Expand All @@ -49,6 +103,7 @@ public void TryGetAnnotationsIncludingAncestorsOfTypeReturnsAnnotationFromParent

var child = builder.AddResource(new ChildResource("child", parent.Resource));

Assert.True(parent.Resource.HasAnnotationIncludingAncestorsOfType<DummyAnnotation>());
Assert.True(child.Resource.TryGetAnnotationsIncludingAncestorsOfType<DummyAnnotation>(out var annotations));
Assert.Single(annotations);
}
Expand All @@ -63,10 +118,29 @@ public void TryGetAnnotationsIncludingAncestorsOfTypeCombinesAnnotationsFromPare
var child = builder.AddResource(new ChildResource("child", parent.Resource))
.WithAnnotation(new DummyAnnotation());

Assert.True(parent.Resource.HasAnnotationIncludingAncestorsOfType<DummyAnnotation>());
Assert.True(child.Resource.TryGetAnnotationsIncludingAncestorsOfType<DummyAnnotation>(out var annotations));
Assert.Equal(2, annotations.Count());
}

[Fact]
public void TryGetAnnotationsIncludingAncestorsOfTypeCombinesAnnotationsFromParentAndChildAndGrandchild()
{
using var builder = TestDistributedApplicationBuilder.Create();
var parent = builder.AddResource(new ParentResource("parent"))
.WithAnnotation(new DummyAnnotation());

var child = builder.AddResource(new ChildResource("child", parent: parent.Resource))
.WithAnnotation(new DummyAnnotation());

var grandchild = builder.AddResource(new ChildResource("grandchild", parent: child.Resource))
.WithAnnotation(new DummyAnnotation());

Assert.True(parent.Resource.HasAnnotationIncludingAncestorsOfType<DummyAnnotation>());
Assert.True(grandchild.Resource.TryGetAnnotationsIncludingAncestorsOfType<DummyAnnotation>(out var annotations));
Assert.Equal(3, annotations.Count());
}

[Fact]
public void TryGetContainerImageNameReturnsCorrectFormatWhenShaSupplied()
{
Expand Down Expand Up @@ -190,13 +264,18 @@ private sealed class ParentResource(string name) : Resource(name)

}

private sealed class ChildResource(string name, ParentResource parent) : Resource(name), IResourceWithParent<ParentResource>
private sealed class ChildResource(string name, Resource parent) : Resource(name), IResourceWithParent<Resource>
{
public ParentResource Parent => parent;
public Resource Parent => parent;
}

private sealed class DummyAnnotation : IResourceAnnotation
{

}

private sealed class AnotherDummyAnnotation : IResourceAnnotation
{

}
}

0 comments on commit cc15725

Please sign in to comment.