Skip to content

Commit

Permalink
Allow loading multiple versions of the same assembly (#332)
Browse files Browse the repository at this point in the history
  • Loading branch information
hlouren authored Feb 16, 2024
1 parent 9fb3ac8 commit 40582fe
Show file tree
Hide file tree
Showing 28 changed files with 202 additions and 27 deletions.
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<PropertyGroup>
<AssemblyVersion>2.0.0.0</AssemblyVersion>
<FileVersion>2.0.0.0</FileVersion>
<Version>2.120.1</Version>
<Version>2.121.1</Version>
<Authors>OutSystems</Authors>
<Product>WebViewControl</Product>
<Copyright>Copyright © OutSystems 2023</Copyright>
Expand Down
67 changes: 64 additions & 3 deletions WebView.sln
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29230.47
# Visual Studio Version 17
VisualStudioVersion = 17.8.34525.116
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebViewControl", "WebViewControl\WebViewControl.csproj", "{A1C2A0C7-DF81-4A8F-AEB5-B5375D5D1B47}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebViewControl.Avalonia", "WebViewControl.Avalonia\WebViewControl.Avalonia.csproj", "{1B789CDF-1B73-450B-BA1D-EC265D498EFA}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests.WebView", "Tests.WebView\Tests.WebView.csproj", "{BE65D793-AFB3-4F5D-A85F-7B7396A5FE59}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests.WebView", "tests\Tests.WebView\Tests.WebView.csproj", "{BE65D793-AFB3-4F5D-A85F-7B7396A5FE59}"
ProjectSection(ProjectDependencies) = postProject
{50037503-26BC-4853-BBF5-7F217E6FA421} = {50037503-26BC-4853-BBF5-7F217E6FA421}
{A4E4B970-35F0-4641-B5B9-5538931727F2} = {A4E4B970-35F0-4641-B5B9-5538931727F2}
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleWebView.Avalonia", "SampleWebView.Avalonia\SampleWebView.Avalonia.csproj", "{68B0F20F-77AE-4819-83CB-92C9C78E8E05}"
EndProject
Expand All @@ -18,6 +22,16 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
Directory.Build.props = Directory.Build.props
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "V1", "V1", "{54FF8FC2-5D9C-45AB-A7E6-6A69BD323FEC}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestResourceAssembly", "tests\TestResourceAssembly.V1.0.0.0\TestResourceAssembly.csproj", "{A4E4B970-35F0-4641-B5B9-5538931727F2}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "V2", "V2", "{BFFEA0F6-3BF7-4A1F-8EC6-73B21BA6766E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestResourceAssembly", "tests\TestResourceAssembly.V2.0.0.0\TestResourceAssembly.csproj", "{50037503-26BC-4853-BBF5-7F217E6FA421}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{B779EFF9-07A0-4BEF-8533-CF3F68830AB4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
Expand Down Expand Up @@ -102,10 +116,57 @@ Global
{68B0F20F-77AE-4819-83CB-92C9C78E8E05}.ReleaseAvaloniaRemoteDebugSupport|ARM64.ActiveCfg = ReleaseAvaloniaRemoteDebugSupport|ARM64
{68B0F20F-77AE-4819-83CB-92C9C78E8E05}.ReleaseAvaloniaRemoteDebugSupport|ARM64.Build.0 = ReleaseAvaloniaRemoteDebugSupport|ARM64
{68B0F20F-77AE-4819-83CB-92C9C78E8E05}.ReleaseWPF|ARM64.ActiveCfg = ReleaseWPF|ARM64
{A4E4B970-35F0-4641-B5B9-5538931727F2}.Debug|ARM64.ActiveCfg = Debug|ARM64
{A4E4B970-35F0-4641-B5B9-5538931727F2}.Debug|ARM64.Build.0 = Debug|ARM64
{A4E4B970-35F0-4641-B5B9-5538931727F2}.Debug|x64.ActiveCfg = Debug|x64
{A4E4B970-35F0-4641-B5B9-5538931727F2}.Debug|x64.Build.0 = Debug|x64
{A4E4B970-35F0-4641-B5B9-5538931727F2}.Release|ARM64.ActiveCfg = Release|ARM64
{A4E4B970-35F0-4641-B5B9-5538931727F2}.Release|ARM64.Build.0 = Release|ARM64
{A4E4B970-35F0-4641-B5B9-5538931727F2}.Release|x64.ActiveCfg = Release|x64
{A4E4B970-35F0-4641-B5B9-5538931727F2}.Release|x64.Build.0 = Release|x64
{A4E4B970-35F0-4641-B5B9-5538931727F2}.ReleaseAvalonia|ARM64.ActiveCfg = Release|ARM64
{A4E4B970-35F0-4641-B5B9-5538931727F2}.ReleaseAvalonia|ARM64.Build.0 = Release|ARM64
{A4E4B970-35F0-4641-B5B9-5538931727F2}.ReleaseAvalonia|x64.ActiveCfg = Release|x64
{A4E4B970-35F0-4641-B5B9-5538931727F2}.ReleaseAvalonia|x64.Build.0 = Release|x64
{A4E4B970-35F0-4641-B5B9-5538931727F2}.ReleaseAvaloniaRemoteDebugSupport|ARM64.ActiveCfg = Release|ARM64
{A4E4B970-35F0-4641-B5B9-5538931727F2}.ReleaseAvaloniaRemoteDebugSupport|ARM64.Build.0 = Release|ARM64
{A4E4B970-35F0-4641-B5B9-5538931727F2}.ReleaseAvaloniaRemoteDebugSupport|x64.ActiveCfg = Release|x64
{A4E4B970-35F0-4641-B5B9-5538931727F2}.ReleaseAvaloniaRemoteDebugSupport|x64.Build.0 = Release|x64
{A4E4B970-35F0-4641-B5B9-5538931727F2}.ReleaseWPF|ARM64.ActiveCfg = Release|ARM64
{A4E4B970-35F0-4641-B5B9-5538931727F2}.ReleaseWPF|ARM64.Build.0 = Release|ARM64
{A4E4B970-35F0-4641-B5B9-5538931727F2}.ReleaseWPF|x64.ActiveCfg = Release|x64
{A4E4B970-35F0-4641-B5B9-5538931727F2}.ReleaseWPF|x64.Build.0 = Release|x64
{50037503-26BC-4853-BBF5-7F217E6FA421}.Debug|ARM64.ActiveCfg = Debug|ARM64
{50037503-26BC-4853-BBF5-7F217E6FA421}.Debug|ARM64.Build.0 = Debug|ARM64
{50037503-26BC-4853-BBF5-7F217E6FA421}.Debug|x64.ActiveCfg = Debug|x64
{50037503-26BC-4853-BBF5-7F217E6FA421}.Debug|x64.Build.0 = Debug|x64
{50037503-26BC-4853-BBF5-7F217E6FA421}.Release|ARM64.ActiveCfg = Release|ARM64
{50037503-26BC-4853-BBF5-7F217E6FA421}.Release|ARM64.Build.0 = Release|ARM64
{50037503-26BC-4853-BBF5-7F217E6FA421}.Release|x64.ActiveCfg = Release|x64
{50037503-26BC-4853-BBF5-7F217E6FA421}.Release|x64.Build.0 = Release|x64
{50037503-26BC-4853-BBF5-7F217E6FA421}.ReleaseAvalonia|ARM64.ActiveCfg = Release|ARM64
{50037503-26BC-4853-BBF5-7F217E6FA421}.ReleaseAvalonia|ARM64.Build.0 = Release|ARM64
{50037503-26BC-4853-BBF5-7F217E6FA421}.ReleaseAvalonia|x64.ActiveCfg = Release|x64
{50037503-26BC-4853-BBF5-7F217E6FA421}.ReleaseAvalonia|x64.Build.0 = Release|x64
{50037503-26BC-4853-BBF5-7F217E6FA421}.ReleaseAvaloniaRemoteDebugSupport|ARM64.ActiveCfg = Release|ARM64
{50037503-26BC-4853-BBF5-7F217E6FA421}.ReleaseAvaloniaRemoteDebugSupport|ARM64.Build.0 = Release|ARM64
{50037503-26BC-4853-BBF5-7F217E6FA421}.ReleaseAvaloniaRemoteDebugSupport|x64.ActiveCfg = Release|x64
{50037503-26BC-4853-BBF5-7F217E6FA421}.ReleaseAvaloniaRemoteDebugSupport|x64.Build.0 = Release|x64
{50037503-26BC-4853-BBF5-7F217E6FA421}.ReleaseWPF|ARM64.ActiveCfg = Release|ARM64
{50037503-26BC-4853-BBF5-7F217E6FA421}.ReleaseWPF|ARM64.Build.0 = Release|ARM64
{50037503-26BC-4853-BBF5-7F217E6FA421}.ReleaseWPF|x64.ActiveCfg = Release|x64
{50037503-26BC-4853-BBF5-7F217E6FA421}.ReleaseWPF|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{BE65D793-AFB3-4F5D-A85F-7B7396A5FE59} = {B779EFF9-07A0-4BEF-8533-CF3F68830AB4}
{54FF8FC2-5D9C-45AB-A7E6-6A69BD323FEC} = {B779EFF9-07A0-4BEF-8533-CF3F68830AB4}
{A4E4B970-35F0-4641-B5B9-5538931727F2} = {54FF8FC2-5D9C-45AB-A7E6-6A69BD323FEC}
{BFFEA0F6-3BF7-4A1F-8EC6-73B21BA6766E} = {B779EFF9-07A0-4BEF-8533-CF3F68830AB4}
{50037503-26BC-4853-BBF5-7F217E6FA421} = {BFFEA0F6-3BF7-4A1F-8EC6-73B21BA6766E}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FD9150A9-9C08-4609-B953-D4B900FD27C0}
EndGlobalSection
Expand Down
51 changes: 39 additions & 12 deletions WebViewControl/AssemblyCache.cs
Original file line number Diff line number Diff line change
@@ -1,28 +1,37 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;

namespace WebViewControl {

internal class AssemblyCache {

private object SyncRoot { get; } = new object();
private Dictionary<string, Assembly> assemblies;

// We now allow to load multiple versions of the same assembly, which means that resource urls can
// (optionally) specify the version. We don't force the version to be specified to maintain backwards
// compatibility, and thus for each assembly name we two entries in the dictionary: with and without a version.
// Note that no guarantee is provided about which version is resolved if there are multiple loaded assemblies
// with the same name and no specific version is provided.
// This, consumer apps are encouraged to include the version in the resource url
private IDictionary<(string AssemblyName, Version AssemblyVersion), Assembly> assemblies;

private bool newAssembliesLoaded = true;

internal Assembly ResolveResourceAssembly(Uri resourceUrl, bool failOnMissingAssembly) {
if (assemblies == null) {
lock (SyncRoot) {
if (assemblies == null) {
assemblies = new Dictionary<string, Assembly>();
assemblies = new Dictionary<(string, Version), Assembly>();
AppDomain.CurrentDomain.AssemblyLoad += delegate { newAssembliesLoaded = true; };
}
}
}

var assemblyName = ResourceUrl.GetEmbeddedResourceAssemblyName(resourceUrl);
var assembly = GetAssemblyByName(assemblyName);
var (assemblyName, assemblyVersion) = ResourceUrl.GetEmbeddedResourceAssemblyNameAndVersion(resourceUrl);
var assembly = GetAssemblyByNameAndVersion(assemblyName, assemblyVersion);

if (assembly == null) {
if (newAssembliesLoaded) {
Expand All @@ -31,26 +40,27 @@ internal Assembly ResolveResourceAssembly(Uri resourceUrl, bool failOnMissingAss
// add loaded assemblies to cache
newAssembliesLoaded = false;
foreach (var domainAssembly in AppDomain.CurrentDomain.GetAssemblies()) {
// replace if duplicated (can happen)
assemblies[domainAssembly.GetName().Name] = domainAssembly;
AddOrReplace(domainAssembly);
}
}
}
}

assembly = GetAssemblyByName(assemblyName);
assembly = GetAssemblyByNameAndVersion(assemblyName, assemblyVersion);
if (assembly == null) {
try {
// try load assembly from its name
var assemblyPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, assemblyName + ".dll");
// try loading the assembly from a file named AssemblyName.dll (or AssemblyName-AssemblyVersion.dll if
// a version was provided)
var fileName = $"{assemblyName}{(assemblyVersion == null ? "" : $"-{assemblyVersion}")}.dll";
var assemblyPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName);
assembly = AssemblyLoader.LoadAssembly(assemblyPath);
} catch (IOException) {
// ignore
}

if (assembly != null) {
lock (SyncRoot) {
assemblies[assembly.GetName().Name] = assembly;
AddOrReplace(assembly);
}
}
}
Expand All @@ -62,9 +72,26 @@ internal Assembly ResolveResourceAssembly(Uri resourceUrl, bool failOnMissingAss
return assembly;
}

private Assembly GetAssemblyByName(string assemblyName) {
private void AddOrReplace(Assembly assembly) {
var identity = assembly.GetName();
var assemblyName = identity.Name;
if (assemblyName == null) {
return;
}

// add two entries, with and without the version.
// for the null-version entry, keep the assembly with the highest version
var version = identity.Version;
if (!assemblies.TryGetValue((assemblyName, null), out var nullVersionAssembly) ||
(nullVersionAssembly.GetName().Version is { } previousVersion && previousVersion < version)) {
assemblies[(assemblyName, null)] = assembly;
}
assemblies[(assemblyName, version)] = assembly;
}

private Assembly GetAssemblyByNameAndVersion(string assemblyName, Version assemblyVersion) {
lock (SyncRoot) {
assemblies.TryGetValue(assemblyName, out var assembly);
assemblies.TryGetValue((assemblyName, assemblyVersion), out var assembly);
return assembly;
}
}
Expand Down
35 changes: 26 additions & 9 deletions WebViewControl/ResourceUrl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public partial class ResourceUrl {
internal const string PathSeparator = "/";

private const string AssemblyPathSeparator = ";";
private const char AssemblyVersionSeparator = '-';
private const string AssemblyPrefix = "assembly:";
private const string DefaultDomain = "webview{0}";

Expand All @@ -23,12 +24,15 @@ public ResourceUrl(params string[] path) {
}

public ResourceUrl(Assembly assembly, params string[] path) : this(path) {
var assemblyName = assembly.GetName().Name;
var identity = assembly.GetName();
var assemblyName = identity.Name;
var assemblyVersion = identity.Version is { } version ? $"{AssemblyVersionSeparator}{version}" : "";

if (Url.StartsWith(PathSeparator)) {
// only prefix with assembly if necessary, to avoid having the same resource loaded from multiple locations
Url = AssemblyPrefix + assemblyName + AssemblyPathSeparator + Url.Substring(1);
Url = AssemblyPrefix + assemblyName + assemblyVersion + AssemblyPathSeparator + Url.Substring(1);
} else {
Url = assemblyName + PathSeparator + Url;
Url = assemblyName + assemblyVersion + PathSeparator + Url;
}
Url = BuildUrl(EmbeddedScheme, Url);
}
Expand Down Expand Up @@ -56,33 +60,46 @@ private static bool ContainsAssemblyLocation(Uri url) {
/// <summary>
/// Supported syntax:
/// embedded://webview/assembly:AssemblyName;Path/To/Resource
/// embedded://webview/assembly:AssemblyName-AssemblyVersion;Path/To/Resource
/// embedded://webview/AssemblyName/Path/To/Resource (AssemblyName is also assumed as default namespace)
/// embedded://webview/AssemblyName-AssemblyVersion/Path/To/Resource
/// </summary>
internal static string[] GetEmbeddedResourcePath(Uri resourceUrl) {
if (ContainsAssemblyLocation(resourceUrl)) {
var indexOfPath = resourceUrl.AbsolutePath.IndexOf(AssemblyPathSeparator);
return resourceUrl.AbsolutePath.Substring(indexOfPath + 1).Split(new [] { PathSeparator }, StringSplitOptions.None);
}
var uriParts = resourceUrl.Segments;
return uriParts.Skip(1).Select(p => p.Replace(PathSeparator, "")).ToArray();
var uriParts = resourceUrl.Segments.Select(p => p.Replace(PathSeparator, "")).ToArray();
var (assemblyName, _) = GetAssemblyNameAndVersion(uriParts[1]);
return uriParts.Skip(2).Prepend(assemblyName).ToArray();
}

/// <summary>
/// Supported syntax:
/// embedded://webview/assembly:AssemblyName;Path/To/Resource
/// embedded://webview/assembly:AssemblyName-AssemblyVersion;Path/To/Resource
/// embedded://webview/AssemblyName/Path/To/Resource (AssemblyName is also assumed as default namespace)
/// embedded://webview/AssemblyName-AssemblyVersion/Path/To/Resource
/// </summary>
public static string GetEmbeddedResourceAssemblyName(Uri resourceUrl) {
public static (string, Version) GetEmbeddedResourceAssemblyNameAndVersion(Uri resourceUrl) {
if (ContainsAssemblyLocation(resourceUrl)) {
var resourcePath = resourceUrl.AbsolutePath.Substring((PathSeparator + AssemblyPrefix).Length);
var indexOfPath = Math.Max(0, resourcePath.IndexOf(AssemblyPathSeparator));
return resourcePath.Substring(0, indexOfPath);
return GetAssemblyNameAndVersion(resourcePath.Substring(0, indexOfPath));
}
if (resourceUrl.Segments.Length > 1) {
var assemblySegment = resourceUrl.Segments[1];
return assemblySegment.EndsWith(PathSeparator) ? assemblySegment.Substring(0, assemblySegment.Length - PathSeparator.Length) : assemblySegment; // default assembly name to the first path
// default assembly name to the first path
return GetAssemblyNameAndVersion(assemblySegment.EndsWith(PathSeparator) ? assemblySegment.Substring(0, assemblySegment.Length - PathSeparator.Length) : assemblySegment);
}
return string.Empty;
return (string.Empty, null);
}

private static (string, Version) GetAssemblyNameAndVersion(string assemblyNameAndVersion) {
var parts = assemblyNameAndVersion.Split(AssemblyVersionSeparator);
return parts.Length == 2 ?
(parts[0], new Version(parts[1])) :
(parts[0], null);
}

internal string WithDomain(string domain) {
Expand Down
1 change: 1 addition & 0 deletions tests/TestResourceAssembly.V1.0.0.0/Resource.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Resource with V1.0.0.0 content
13 changes: 13 additions & 0 deletions tests/TestResourceAssembly.V1.0.0.0/TestResourceAssembly.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(DotnetVersion)</TargetFramework>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<OutputPath>$(MSBuildProjectDirectory)\bin\</OutputPath>
<RootNamespace>TestResourceAssembly</RootNamespace>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
</PropertyGroup>

<ItemGroup>
<EmbeddedResource Include="Resource.txt" />
</ItemGroup>
</Project>
1 change: 1 addition & 0 deletions tests/TestResourceAssembly.V2.0.0.0/Resource.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Resource with V2.0.0.0 content
13 changes: 13 additions & 0 deletions tests/TestResourceAssembly.V2.0.0.0/TestResourceAssembly.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(DotnetVersion)</TargetFramework>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<OutputPath>$(MSBuildProjectDirectory)\bin\</OutputPath>
<RootNamespace>TestResourceAssembly</RootNamespace>
<AssemblyVersion>2.0.0.0</AssemblyVersion>
</PropertyGroup>

<ItemGroup>
<EmbeddedResource Include="Resource.txt" />
</ItemGroup>
</Project>
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading

0 comments on commit 40582fe

Please sign in to comment.