Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -210,6 +210,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.WithLifetime<T>(this Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>! builder, Aspire.Hosting.ApplicationModel.ContainerLifetime lifetime) -> 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 @@ -9,21 +9,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 @@ -35,10 +50,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 @@ -48,6 +102,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 @@ -62,10 +117,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 @@ -189,13 +263,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
{

}
}