Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Trimming] Simpler image source service provider #4

Closed
wants to merge 6 commits into from
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public static class ImageSourceServiceCollectionExtensions
where TImageSource : IImageSource
where TImageSourceService : class, IImageSourceService<TImageSource>
{
services.GetImageSourceTypeMapping().Add<TImageSource>();
#pragma warning disable RS0030 // Do not use banned APIs, the current method is also banned
services.AddSingleton<IImageSourceService<TImageSource>, TImageSourceService>();
#pragma warning restore RS0030 // Do not use banned APIs
Expand All @@ -34,9 +35,19 @@ public static class ImageSourceServiceCollectionExtensions
public static IImageSourceServiceCollection AddService<TImageSource>(this IImageSourceServiceCollection services, Func<IServiceProvider, IImageSourceService<TImageSource>> implementationFactory)
where TImageSource : IImageSource
{
services.GetImageSourceTypeMapping().Add<TImageSource>();
services.AddSingleton(provider => implementationFactory(((IImageSourceServiceProvider)provider).HostServiceProvider));

return services;
}

internal static Type FindImageSourceServiceType(this IImageSourceServiceCollection services, Type imageSourceType)
=> services.GetImageSourceTypeMapping().FindImageSourceServiceType(imageSourceType);

internal static Type FindImageSourceType(this IImageSourceServiceCollection services, Type imageSourceType)
=> services.GetImageSourceTypeMapping().FindImageSourceType(imageSourceType);

private static ImageSourceToImageSourceServiceTypeMapping GetImageSourceTypeMapping(this IImageSourceServiceCollection services)
=> ImageSourceToImageSourceServiceTypeMapping.GetInstance(services);
}
}
36 changes: 4 additions & 32 deletions src/Core/src/Hosting/ImageSources/ImageSourceServiceProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,14 @@ namespace Microsoft.Maui.Hosting
{
sealed class ImageSourceServiceProvider : MauiFactory, IImageSourceServiceProvider
{
static readonly string ImageSourceInterface = typeof(IImageSource).FullName!;
static readonly Type ImageSourceServiceType = typeof(IImageSourceService<>);

readonly ConcurrentDictionary<Type, Type> _imageSourceCache = new ConcurrentDictionary<Type, Type>();
readonly ConcurrentDictionary<Type, Type> _serviceCache = new ConcurrentDictionary<Type, Type>();
readonly IImageSourceServiceCollection _collection;

public ImageSourceServiceProvider(IImageSourceServiceCollection collection, IServiceProvider hostServiceProvider)
: base(collection)
{
_collection = collection;
HostServiceProvider = hostServiceProvider;
}

Expand All @@ -26,36 +25,9 @@ public ImageSourceServiceProvider(IImageSourceServiceCollection collection, ISer
(IImageSourceService?)GetService(GetImageSourceServiceType(imageSource));

public Type GetImageSourceServiceType(Type imageSource) =>
_serviceCache.GetOrAdd(imageSource, type =>
{
var genericConcreteType = ImageSourceServiceType.MakeGenericType(type);

if (genericConcreteType != null && GetServiceDescriptor(genericConcreteType) != null)
return genericConcreteType;

return ImageSourceServiceType.MakeGenericType(GetImageSourceType(type));
});
_serviceCache.GetOrAdd(imageSource, _collection.FindImageSourceServiceType);

public Type GetImageSourceType(Type imageSource) =>
_imageSourceCache.GetOrAdd(imageSource, CreateImageSourceTypeCacheEntry);

Type CreateImageSourceTypeCacheEntry(Type type)
{
if (type.IsInterface)
{
if (type.GetInterface(ImageSourceInterface) != null)
return type;
}
else
{
foreach (var directInterface in type.GetInterfaces())
{
if (directInterface.GetInterface(ImageSourceInterface) != null)
return directInterface;
}
}

throw new InvalidOperationException($"Unable to find the image source type because none of the interfaces on {type.Name} were derived from {nameof(IImageSource)}.");
}
_imageSourceCache.GetOrAdd(imageSource, _collection.FindImageSourceType);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;

using TypePair = (System.Type ImageSource, System.Type ImageSourceService);

namespace Microsoft.Maui.Hosting
{
internal sealed class ImageSourceToImageSourceServiceTypeMapping
{
private static readonly ConcurrentDictionary<IImageSourceServiceCollection, ImageSourceToImageSourceServiceTypeMapping> s_instances = new();

internal static ImageSourceToImageSourceServiceTypeMapping GetInstance(IImageSourceServiceCollection collection) =>
s_instances.GetOrAdd(collection, static _ => new ImageSourceToImageSourceServiceTypeMapping());

private ConcurrentDictionary<Type, Type> _typeMappings { get; } = new();

public void Add<TImageSource>() where TImageSource : IImageSource =>
_typeMappings[typeof(TImageSource)] = typeof(IImageSourceService<TImageSource>);

public Type FindImageSourceType(Type imageSourceType) =>
FindImageSourceToImageSourceServiceTypeMapping(imageSourceType).ImageSource;

public Type FindImageSourceServiceType(Type imageSourceType) =>
FindImageSourceToImageSourceServiceTypeMapping(imageSourceType).ImageSourceService;

private TypePair FindImageSourceToImageSourceServiceTypeMapping(Type type)
{
Debug.Assert(typeof(IImageSource).IsAssignableFrom(type));

// If there's an exact match for the type, just return it.
if (_typeMappings.TryGetValue(type, out var imageSourceService))
{
return (type, imageSourceService);
}

List<TypePair> matches = new();
foreach (var mapping in _typeMappings)
{
var imageSource = mapping.Key;
if (imageSource.IsAssignableFrom(type) || type.IsAssignableFrom(imageSource))
{
matches.Add((imageSource, mapping.Value));
}
}

return SelectBestMatch(matches, type);
}

private static TypePair SelectBestMatch(List<TypePair> matches, Type type)
{
if (matches.Count == 0)
{
throw new InvalidOperationException($"Unable to find any configured {nameof(IImageSource)} corresponding to {type.Name}.");
}

var bestImageSourceMatch = matches[0].ImageSource;
var bestImageSourceServiceMatch = matches[0].ImageSourceService;

for (int i = 1; i < matches.Count; i++)
{
var (imageSource, imageSourceService) = matches[i];

if (!bestImageSourceMatch.IsAssignableFrom(imageSource) && !imageSource.IsAssignableFrom(bestImageSourceMatch))
{
throw new InvalidOperationException($"Ambiguous image source services for {type} ({bestImageSourceMatch} and {imageSource}).");
}

if (bestImageSourceMatch.IsAssignableFrom(imageSource) || (bestImageSourceMatch.IsInterface && imageSource.IsClass))
{
bestImageSourceMatch = imageSource;
bestImageSourceServiceMatch = imageSourceService;
}
}

return (bestImageSourceMatch, bestImageSourceServiceMatch);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public void ThrowsWhenMissingService()

var ex = Assert.Throws<InvalidOperationException>(() => provider.GetRequiredImageSourceService(new FileImageSourceStub()));

Assert.Contains(nameof(IFileImageSource), ex.Message, StringComparison.Ordinal);
Assert.Contains(nameof(FileImageSourceStub), ex.Message, StringComparison.Ordinal);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Maui.Hosting;
using Xunit;

namespace Microsoft.Maui.UnitTests.ImageSource
{
[Category(TestCategory.Core, TestCategory.ImageSource)]
public class ImageSourceToImageSourceServiceTypeMappingTests
{
[Fact]
public void FindsCorrespondingImageSourceType()
{
var mapping = new ImageSourceToImageSourceServiceTypeMapping();
mapping.Add<IStreamImageSource>();
mapping.Add<IUriImageSource>();

var type = mapping.FindImageSourceType(typeof(StreamImageSourceStub));
Assert.Equal(typeof(IStreamImageSource), type);
}

[Fact]
public void FindsCorrespondingImageSourceServiceType()
{
var mapping = new ImageSourceToImageSourceServiceTypeMapping();
mapping.Add<IStreamImageSource>();
mapping.Add<IUriImageSource>();

var type = mapping.FindImageSourceServiceType(typeof(StreamImageSourceStub));
Assert.Equal(typeof(IImageSourceService<IStreamImageSource>), type);
}

[Fact]
public void PrefersConcreteTypesOverInterfaces()
{
var mapping = new ImageSourceToImageSourceServiceTypeMapping();
mapping.Add<ICustomStreamImageSource>();
mapping.Add<StreamImageSourceStub>();

var imageSourceType = mapping.FindImageSourceType(typeof(IStreamImageSource));
var imageSourceServiceType = mapping.FindImageSourceServiceType(typeof(IStreamImageSource));

Assert.Equal(typeof(StreamImageSourceStub), imageSourceType);
Assert.Equal(typeof(IImageSourceService<StreamImageSourceStub>), imageSourceServiceType);
}

[Fact]
public void PrefersExactMatches()
{
var mapping = new ImageSourceToImageSourceServiceTypeMapping();
mapping.Add<IStreamImageSource>();
mapping.Add<StreamImageSourceStub>();

var imageSourceType = mapping.FindImageSourceType(typeof(IStreamImageSource));
var imageSourceServiceType = mapping.FindImageSourceServiceType(typeof(IStreamImageSource));

Assert.Equal(typeof(IStreamImageSource), imageSourceType);
Assert.Equal(typeof(IImageSourceService<IStreamImageSource>), imageSourceServiceType);
}

[Fact]
public void FindsMoreDerivedTypes()
{
var mapping = new ImageSourceToImageSourceServiceTypeMapping();
mapping.Add<StreamImageSourceStub>();
mapping.Add<DerivedStreamImageSourceStub>();

var imageSourceType = mapping.FindImageSourceType(typeof(IStreamImageSource));
var imageSourceServiceType = mapping.FindImageSourceServiceType(typeof(IStreamImageSource));

Assert.Equal(typeof(DerivedStreamImageSourceStub), imageSourceType);
Assert.Equal(typeof(IImageSourceService<DerivedStreamImageSourceStub>), imageSourceServiceType);
}

[Fact]
public void ThrowsInCaseOfAmbiguity()
{
var mapping = new ImageSourceToImageSourceServiceTypeMapping();
mapping.Add<FileImageSourceA>();
mapping.Add<FileImageSourceB>();

Assert.Throws<InvalidOperationException>(() => mapping.FindImageSourceType(typeof(IFileImageSource)));
}

private interface ICustomStreamImageSource : IStreamImageSource
{
}

private class StreamImageSourceStub : ICustomStreamImageSource
{
public Task<Stream> GetStreamAsync(CancellationToken cancellationToken = default) => Task.FromException<Stream>(new NotImplementedException());
public bool IsEmpty => true;
}

private class DerivedStreamImageSourceStub : StreamImageSourceStub
{
}

private class FileImageSourceA : IFileImageSource
{
public string File => throw new NotImplementedException();
public bool IsEmpty => true;
}

private class FileImageSourceB : IFileImageSource
{
public string File => throw new NotImplementedException();
public bool IsEmpty => true;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -398,48 +398,6 @@ public static void AssertWarnings(this List<WarningsPerFile> actualWarnings, Lis
}
},
new WarningsPerFile
{
File = "src/Core/src/Hosting/ImageSources/ImageSourceServiceProvider.cs",
WarningsPerCode = new List<WarningsPerCode>
{
new WarningsPerCode
{
Code = "IL2070",
Messages = new List<string>
{
"Microsoft.Maui.Hosting.ImageSourceServiceProvider.CreateImageSourceTypeCacheEntry(Type): 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.Interfaces' in call to 'System.Type.GetInterface(String)'. The parameter 'type' of method 'Microsoft.Maui.Hosting.ImageSourceServiceProvider.CreateImageSourceTypeCacheEntry(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.",
"Microsoft.Maui.Hosting.ImageSourceServiceProvider.CreateImageSourceTypeCacheEntry(Type): 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.Interfaces' in call to 'System.Type.GetInterfaces()'. The parameter 'type' of method 'Microsoft.Maui.Hosting.ImageSourceServiceProvider.CreateImageSourceTypeCacheEntry(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.",
}
},
new WarningsPerCode
{
Code = "IL2065",
Messages = new List<string>
{
"Microsoft.Maui.Hosting.ImageSourceServiceProvider.CreateImageSourceTypeCacheEntry(Type): Value passed to implicit 'this' parameter of method 'System.Type.GetInterface(String)' can not be statically determined and may not meet 'DynamicallyAccessedMembersAttribute' requirements.",
}
},
new WarningsPerCode
{
Code = "IL2055",
Messages = new List<string>
{
"Microsoft.Maui.Hosting.ImageSourceServiceProvider.<GetImageSourceServiceType>b__9_0(Type): Call to 'System.Type.MakeGenericType(Type[])' can not be statically analyzed. It's not possible to guarantee the availability of requirements of the generic type.",
"Microsoft.Maui.Hosting.ImageSourceServiceProvider.<GetImageSourceServiceType>b__9_0(Type): Call to 'System.Type.MakeGenericType(Type[])' can not be statically analyzed. It's not possible to guarantee the availability of requirements of the generic type.",
}
},
new WarningsPerCode
{
Code = "IL3050",
Messages = new List<string>
{
"Microsoft.Maui.Hosting.ImageSourceServiceProvider.<GetImageSourceServiceType>b__9_0(Type): Using member 'System.Type.MakeGenericType(Type[])' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling. The native code for this instantiation might not be available at runtime.",
"Microsoft.Maui.Hosting.ImageSourceServiceProvider.<GetImageSourceServiceType>b__9_0(Type): Using member 'System.Type.MakeGenericType(Type[])' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling. The native code for this instantiation might not be available at runtime.",
}
},
}
},
new WarningsPerFile
{
File = "src/Core/src/Platform/ReflectionExtensions.cs",
WarningsPerCode = new List<WarningsPerCode>
Expand Down