From f1f0bd3a979b676dc4e635c9d007b1cc69feb2d9 Mon Sep 17 00:00:00 2001 From: maliming <6908465+maliming@users.noreply.github.com> Date: Tue, 23 Jun 2020 17:44:01 +0800 Subject: [PATCH] Add NamingNormalizerProviders to BlobContainerConfiguration. Resolve #4408 --- ...ureBlobContainerConfigurationExtensions.cs | 1 + ...efaultAzureBlobNamingNormalizerProvider.cs | 52 +++++++++++++++ ...tFileSystemBlobNamingNormalizerProvider.cs | 41 ++++++++++++ ...temBlobContainerConfigurationExtensions.cs | 9 +-- .../Volo/Abp/BlobStoring/BlobContainer.cs | 64 +++++++++++++++---- .../BlobStoring/BlobContainerConfiguration.cs | 10 ++- .../Abp/BlobStoring/BlobContainerFactory.cs | 24 ++++--- .../IBlobNamingNormalizerProvider.cs | 9 +++ .../System/Runtime/IOSPlatformProvider.cs | 9 +++ .../System/Runtime/OSPlatformProvider.cs | 29 +++++++++ ...AzureBlobNamingNormalizerProvider_Tests.cs | 57 +++++++++++++++++ ...ystemBlobNamingNormalizerProvider_Tests.cs | 62 ++++++++++++++++++ 12 files changed, 339 insertions(+), 28 deletions(-) create mode 100644 framework/src/Volo.Abp.BlobStoring.Azure/Volo/Abp/BlobStoring/Azure/DefaultAzureBlobNamingNormalizerProvider.cs create mode 100644 framework/src/Volo.Abp.BlobStoring.FileSystem/Volo/Abp/BlobStoring/FileSystem/DefaultFileSystemBlobNamingNormalizerProvider.cs create mode 100644 framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/IBlobNamingNormalizerProvider.cs create mode 100644 framework/src/Volo.Abp.Core/System/Runtime/IOSPlatformProvider.cs create mode 100644 framework/src/Volo.Abp.Core/System/Runtime/OSPlatformProvider.cs create mode 100644 framework/test/Volo.Abp.BlobStoring.Azure.Tests/Volo/Abp/BlobStoring/Azure/DefaultAzureBlobNamingNormalizerProvider_Tests.cs create mode 100644 framework/test/Volo.Abp.BlobStoring.FileSystem.Tests/Volo/Abp/BlobStoring/FileSystem/DefaultFileSystemBlobNamingNormalizerProvider_Tests.cs diff --git a/framework/src/Volo.Abp.BlobStoring.Azure/Volo/Abp/BlobStoring/Azure/AzureBlobContainerConfigurationExtensions.cs b/framework/src/Volo.Abp.BlobStoring.Azure/Volo/Abp/BlobStoring/Azure/AzureBlobContainerConfigurationExtensions.cs index 945c930175..8b912dfb73 100644 --- a/framework/src/Volo.Abp.BlobStoring.Azure/Volo/Abp/BlobStoring/Azure/AzureBlobContainerConfigurationExtensions.cs +++ b/framework/src/Volo.Abp.BlobStoring.Azure/Volo/Abp/BlobStoring/Azure/AzureBlobContainerConfigurationExtensions.cs @@ -15,6 +15,7 @@ namespace Volo.Abp.BlobStoring.Azure Action azureConfigureAction) { containerConfiguration.ProviderType = typeof(AzureBlobProvider); + containerConfiguration.NamingNormalizerProviders.Add(); azureConfigureAction(new AzureBlobProviderConfiguration(containerConfiguration)); diff --git a/framework/src/Volo.Abp.BlobStoring.Azure/Volo/Abp/BlobStoring/Azure/DefaultAzureBlobNamingNormalizerProvider.cs b/framework/src/Volo.Abp.BlobStoring.Azure/Volo/Abp/BlobStoring/Azure/DefaultAzureBlobNamingNormalizerProvider.cs new file mode 100644 index 0000000000..d787553134 --- /dev/null +++ b/framework/src/Volo.Abp.BlobStoring.Azure/Volo/Abp/BlobStoring/Azure/DefaultAzureBlobNamingNormalizerProvider.cs @@ -0,0 +1,52 @@ +using System.Text.RegularExpressions; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.BlobStoring.Azure +{ + public class DefaultAzureBlobNamingNormalizerProvider : IBlobNamingNormalizerProvider, ITransientDependency + { + /// + ///https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata#container-names + /// + public virtual string NormalizeContainerName(string containerName) + { + // All letters in a container name must be lowercase. + containerName = containerName.ToLower(); + + // Container names can contain only letters, numbers, and the dash (-) character. + containerName = Regex.Replace(containerName, "[^a-z0-9-]", string.Empty); + + // Every dash (-) character must be immediately preceded and followed by a letter or number; + // consecutive dashes are not permitted in container names. + // Container names must start or end with a letter or number + containerName = Regex.Replace(containerName, "-{2,}", "-"); + containerName = Regex.Replace(containerName, "^-", string.Empty); + containerName = Regex.Replace(containerName, "-$", string.Empty); + + // Container names must be from 3 through 63 characters long. + if (containerName.Length < 3) + { + var length = containerName.Length; + for (var i = 0; i < 3 - length; i++) + { + containerName += "0"; + } + } + + if (containerName.Length > 63) + { + containerName = containerName.Substring(0, 63); + } + + return containerName; + } + + /// + ///https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata#blob-names + /// + public virtual string NormalizeBlobName(string blobName) + { + return blobName; + } + } +} diff --git a/framework/src/Volo.Abp.BlobStoring.FileSystem/Volo/Abp/BlobStoring/FileSystem/DefaultFileSystemBlobNamingNormalizerProvider.cs b/framework/src/Volo.Abp.BlobStoring.FileSystem/Volo/Abp/BlobStoring/FileSystem/DefaultFileSystemBlobNamingNormalizerProvider.cs new file mode 100644 index 0000000000..bf42e3589a --- /dev/null +++ b/framework/src/Volo.Abp.BlobStoring.FileSystem/Volo/Abp/BlobStoring/FileSystem/DefaultFileSystemBlobNamingNormalizerProvider.cs @@ -0,0 +1,41 @@ +using System; +using System.Runtime; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.BlobStoring.FileSystem +{ + public class DefaultFileSystemBlobNamingNormalizerProvider : IBlobNamingNormalizerProvider, ITransientDependency + { + private readonly IOSPlatformProvider _iosPlatformProvider; + + public DefaultFileSystemBlobNamingNormalizerProvider(IOSPlatformProvider iosPlatformProvider) + { + _iosPlatformProvider = iosPlatformProvider; + } + + public virtual string NormalizeContainerName(string containerName) + { + return Normalize(containerName); + } + + public virtual string NormalizeBlobName(string blobName) + { + return Normalize(blobName); + } + + protected virtual string Normalize(string fileName) + { + var os = _iosPlatformProvider.GetCurrentOSPlatform(); + if (os == OSPlatform.Windows) + { + // A filename cannot contain any of the following characters: \ / : * ? " < > | + // In order to support the directory included in the blob name, remove / and \ + fileName = Regex.Replace(fileName, "[:\\*\\?\"<>\\|]", string.Empty); + } + + return fileName; + } + } +} diff --git a/framework/src/Volo.Abp.BlobStoring.FileSystem/Volo/Abp/BlobStoring/FileSystem/FileSystemBlobContainerConfigurationExtensions.cs b/framework/src/Volo.Abp.BlobStoring.FileSystem/Volo/Abp/BlobStoring/FileSystem/FileSystemBlobContainerConfigurationExtensions.cs index 24357554eb..92c03580a6 100644 --- a/framework/src/Volo.Abp.BlobStoring.FileSystem/Volo/Abp/BlobStoring/FileSystem/FileSystemBlobContainerConfigurationExtensions.cs +++ b/framework/src/Volo.Abp.BlobStoring.FileSystem/Volo/Abp/BlobStoring/FileSystem/FileSystemBlobContainerConfigurationExtensions.cs @@ -9,16 +9,17 @@ namespace Volo.Abp.BlobStoring.FileSystem { return new FileSystemBlobProviderConfiguration(containerConfiguration); } - + public static BlobContainerConfiguration UseFileSystem( this BlobContainerConfiguration containerConfiguration, Action fileSystemConfigureAction) { containerConfiguration.ProviderType = typeof(FileSystemBlobProvider); - + containerConfiguration.NamingNormalizerProviders.Add(); + fileSystemConfigureAction(new FileSystemBlobProviderConfiguration(containerConfiguration)); - + return containerConfiguration; } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainer.cs b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainer.cs index d165a40316..576e287cb0 100644 --- a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainer.cs +++ b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainer.cs @@ -1,7 +1,9 @@ -using System; +using System; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Volo.Abp.MultiTenancy; using Volo.Abp.Threading; @@ -84,18 +86,44 @@ namespace Volo.Abp.BlobStoring protected ICancellationTokenProvider CancellationTokenProvider { get; } + protected IServiceProvider ServiceProvider { get; } + public BlobContainer( string containerName, BlobContainerConfiguration configuration, IBlobProvider provider, ICurrentTenant currentTenant, - ICancellationTokenProvider cancellationTokenProvider) + ICancellationTokenProvider cancellationTokenProvider, + IServiceProvider serviceProvider) { ContainerName = containerName; Configuration = configuration; Provider = provider; CurrentTenant = currentTenant; CancellationTokenProvider = cancellationTokenProvider; + ServiceProvider = serviceProvider; + } + + private (string, string) NormalizeContainerNameAndBlobName(string containerName, string blobName) + { + if (!Configuration.NamingNormalizerProviders.Any()) + { + return (containerName, blobName); + } + + using (var scope = ServiceProvider.CreateScope()) + { + foreach (var provider in Configuration.NamingNormalizerProviders) + { + var blobNamingNormalizerProvider = scope.ServiceProvider.GetRequiredService(provider) + .As(); + + containerName = blobNamingNormalizerProvider.NormalizeContainerName(containerName); + blobName = blobNamingNormalizerProvider.NormalizeBlobName(blobName); + } + + return (containerName, blobName); + } } public virtual async Task SaveAsync( @@ -106,11 +134,14 @@ namespace Volo.Abp.BlobStoring { using (CurrentTenant.Change(GetTenantIdOrNull())) { + var (normalizedContainerName, normalizedBlobName) = + NormalizeContainerNameAndBlobName(ContainerName, name); + await Provider.SaveAsync( new BlobProviderSaveArgs( - ContainerName, + normalizedContainerName, Configuration, - name, + normalizedBlobName, stream, overrideExisting, CancellationTokenProvider.FallbackToProvider(cancellationToken) @@ -125,11 +156,14 @@ namespace Volo.Abp.BlobStoring { using (CurrentTenant.Change(GetTenantIdOrNull())) { + var (normalizedContainerName, normalizedBlobName) = + NormalizeContainerNameAndBlobName(ContainerName, name); + return await Provider.DeleteAsync( new BlobProviderDeleteArgs( - ContainerName, + normalizedContainerName, Configuration, - name, + normalizedBlobName, CancellationTokenProvider.FallbackToProvider(cancellationToken) ) ); @@ -142,11 +176,14 @@ namespace Volo.Abp.BlobStoring { using (CurrentTenant.Change(GetTenantIdOrNull())) { + var (normalizedContainerName, normalizedBlobName) = + NormalizeContainerNameAndBlobName(ContainerName, name); + return await Provider.ExistsAsync( new BlobProviderExistsArgs( - ContainerName, + normalizedContainerName, Configuration, - name, + normalizedBlobName, CancellationTokenProvider.FallbackToProvider(cancellationToken) ) ); @@ -158,7 +195,7 @@ namespace Volo.Abp.BlobStoring CancellationToken cancellationToken = default) { var stream = await GetOrNullAsync(name, cancellationToken); - + if (stream == null) { //TODO: Consider to throw some type of "not found" exception and handle on the HTTP status side @@ -175,11 +212,14 @@ namespace Volo.Abp.BlobStoring { using (CurrentTenant.Change(GetTenantIdOrNull())) { + var (normalizedContainerName, normalizedBlobName) = + NormalizeContainerNameAndBlobName(ContainerName, name); + return await Provider.GetOrNullAsync( new BlobProviderGetArgs( - ContainerName, + normalizedContainerName, Configuration, - name, + normalizedBlobName, CancellationTokenProvider.FallbackToProvider(cancellationToken) ) ); @@ -196,4 +236,4 @@ namespace Volo.Abp.BlobStoring return CurrentTenant.Id; } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerConfiguration.cs b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerConfiguration.cs index 46f13a1c3d..f5c6a7e50d 100644 --- a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerConfiguration.cs +++ b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerConfiguration.cs @@ -1,6 +1,7 @@ -using System; +using System; using System.Collections.Generic; using JetBrains.Annotations; +using Volo.Abp.Collections; namespace Volo.Abp.BlobStoring { @@ -18,17 +19,20 @@ namespace Volo.Abp.BlobStoring /// then the container is shared by all tenants in the system. /// /// This can be true even if your application is not multi-tenant. - /// + /// /// Default: true. /// public bool IsMultiTenant { get; set; } = true; + public ITypeList NamingNormalizerProviders { get; } + [NotNull] private readonly Dictionary _properties; [CanBeNull] private readonly BlobContainerConfiguration _fallbackConfiguration; public BlobContainerConfiguration(BlobContainerConfiguration fallbackConfiguration = null) { + NamingNormalizerProviders = new TypeList(); _fallbackConfiguration = fallbackConfiguration; _properties = new Dictionary(); } @@ -68,4 +72,4 @@ namespace Volo.Abp.BlobStoring return this; } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerFactory.cs b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerFactory.cs index ab27aab01f..7a4da39ef9 100644 --- a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerFactory.cs +++ b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerFactory.cs @@ -1,4 +1,5 @@ -using Volo.Abp.DependencyInjection; +using System; +using Volo.Abp.DependencyInjection; using Volo.Abp.MultiTenancy; using Volo.Abp.Threading; @@ -7,36 +8,41 @@ namespace Volo.Abp.BlobStoring public class BlobContainerFactory : IBlobContainerFactory, ITransientDependency { protected IBlobProviderSelector ProviderSelector { get; } - + protected IBlobContainerConfigurationProvider ConfigurationProvider { get; } protected ICurrentTenant CurrentTenant { get; } - + protected ICancellationTokenProvider CancellationTokenProvider { get; } + protected IServiceProvider ServiceProvider { get; } + public BlobContainerFactory( IBlobContainerConfigurationProvider configurationProvider, ICurrentTenant currentTenant, - ICancellationTokenProvider cancellationTokenProvider, - IBlobProviderSelector providerSelector) + ICancellationTokenProvider cancellationTokenProvider, + IBlobProviderSelector providerSelector, + IServiceProvider serviceProvider) { ConfigurationProvider = configurationProvider; CurrentTenant = currentTenant; CancellationTokenProvider = cancellationTokenProvider; ProviderSelector = providerSelector; + ServiceProvider = serviceProvider; } - + public virtual IBlobContainer Create(string name) { var configuration = ConfigurationProvider.Get(name); - + return new BlobContainer( name, configuration, ProviderSelector.Get(name), CurrentTenant, - CancellationTokenProvider + CancellationTokenProvider, + ServiceProvider ); } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/IBlobNamingNormalizerProvider.cs b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/IBlobNamingNormalizerProvider.cs new file mode 100644 index 0000000000..8c5bff81c4 --- /dev/null +++ b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/IBlobNamingNormalizerProvider.cs @@ -0,0 +1,9 @@ +namespace Volo.Abp.BlobStoring +{ + public interface IBlobNamingNormalizerProvider + { + string NormalizeContainerName(string containerName); + + string NormalizeBlobName(string blobName); + } +} diff --git a/framework/src/Volo.Abp.Core/System/Runtime/IOSPlatformProvider.cs b/framework/src/Volo.Abp.Core/System/Runtime/IOSPlatformProvider.cs new file mode 100644 index 0000000000..60240159f3 --- /dev/null +++ b/framework/src/Volo.Abp.Core/System/Runtime/IOSPlatformProvider.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace System.Runtime +{ + public interface IOSPlatformProvider + { + OSPlatform GetCurrentOSPlatform(); + } +} diff --git a/framework/src/Volo.Abp.Core/System/Runtime/OSPlatformProvider.cs b/framework/src/Volo.Abp.Core/System/Runtime/OSPlatformProvider.cs new file mode 100644 index 0000000000..d2b05aac60 --- /dev/null +++ b/framework/src/Volo.Abp.Core/System/Runtime/OSPlatformProvider.cs @@ -0,0 +1,29 @@ +using System.Runtime.InteropServices; +using Volo.Abp.DependencyInjection; + +namespace System.Runtime +{ + public class OSPlatformProvider : IOSPlatformProvider, ITransientDependency + { + public OSPlatform GetCurrentOSPlatform() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + //MAC + return OSPlatform.OSX; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return OSPlatform.Linux; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return OSPlatform.Windows; + } + + throw new Exception("Cannot determine operating system!"); + } + } +} diff --git a/framework/test/Volo.Abp.BlobStoring.Azure.Tests/Volo/Abp/BlobStoring/Azure/DefaultAzureBlobNamingNormalizerProvider_Tests.cs b/framework/test/Volo.Abp.BlobStoring.Azure.Tests/Volo/Abp/BlobStoring/Azure/DefaultAzureBlobNamingNormalizerProvider_Tests.cs new file mode 100644 index 0000000000..50e7e0b7ab --- /dev/null +++ b/framework/test/Volo.Abp.BlobStoring.Azure.Tests/Volo/Abp/BlobStoring/Azure/DefaultAzureBlobNamingNormalizerProvider_Tests.cs @@ -0,0 +1,57 @@ +using Shouldly; +using Xunit; + +namespace Volo.Abp.BlobStoring.Azure +{ + public class DefaultAzureBlobNamingNormalizerProvider_Tests : AbpBlobStoringAzureTestCommonBase + { + private readonly IBlobNamingNormalizerProvider _blobNamingNormalizerProvider; + + public DefaultAzureBlobNamingNormalizerProvider_Tests() + { + _blobNamingNormalizerProvider = GetRequiredService(); + } + + [Fact] + public void NormalizeContainerName_Lowercase() + { + var filename = "ThisIsMyContainerName"; + filename = _blobNamingNormalizerProvider.NormalizeContainerName(filename); + filename.ShouldBe("thisismycontainername"); + } + + [Fact] + public void NormalizeContainerName_Only_Letters_Numbers_Dash() + { + var filename = ",./this-i,./s-my-c,./ont,./ai+*/.=!@#$n^&*er-name.+/"; + filename = _blobNamingNormalizerProvider.NormalizeContainerName(filename); + filename.ShouldBe("this-is-my-container-name"); + } + + [Fact] + public void NormalizeContainerName_Dash() + { + var filename = "-this--is----my-container----name-"; + filename = _blobNamingNormalizerProvider.NormalizeContainerName(filename); + filename.ShouldBe("this-is-my-container-name"); + } + + + [Fact] + public void NormalizeContainerName_Min_Length() + { + var filename = "a"; + filename = _blobNamingNormalizerProvider.NormalizeContainerName(filename); + filename.Length.ShouldBeGreaterThanOrEqualTo(3); + } + + + [Fact] + public void NormalizeContainerName_Max_Length() + { + var filename = "abpabpabpabpabpabpabpabpabpabpabpabpabpabpabpabpabpabpabpabpabpabpabp"; + filename = _blobNamingNormalizerProvider.NormalizeContainerName(filename); + filename.Length.ShouldBeLessThanOrEqualTo(63); + } + } +} diff --git a/framework/test/Volo.Abp.BlobStoring.FileSystem.Tests/Volo/Abp/BlobStoring/FileSystem/DefaultFileSystemBlobNamingNormalizerProvider_Tests.cs b/framework/test/Volo.Abp.BlobStoring.FileSystem.Tests/Volo/Abp/BlobStoring/FileSystem/DefaultFileSystemBlobNamingNormalizerProvider_Tests.cs new file mode 100644 index 0000000000..7b4282996c --- /dev/null +++ b/framework/test/Volo.Abp.BlobStoring.FileSystem.Tests/Volo/Abp/BlobStoring/FileSystem/DefaultFileSystemBlobNamingNormalizerProvider_Tests.cs @@ -0,0 +1,62 @@ +using System.Runtime; +using System.Runtime.InteropServices; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; +using Shouldly; +using Xunit; + +namespace Volo.Abp.BlobStoring.FileSystem +{ + public class DefaultFileSystemBlobNamingNormalizerProvider_Tests : AbpBlobStoringFileSystemTestBase + { + private readonly IBlobNamingNormalizerProvider _blobNamingNormalizerProvider; + + public DefaultFileSystemBlobNamingNormalizerProvider_Tests() + { + _blobNamingNormalizerProvider = GetRequiredService(); + } + + protected override void AfterAddApplication(IServiceCollection services) + { + var _iosPlatformProvider = Substitute.For(); + _iosPlatformProvider.GetCurrentOSPlatform().Returns(OSPlatform.Windows); + services.AddSingleton(_iosPlatformProvider); + } + + [Fact] + public void NormalizeContainerName() + { + var filename = "thisismy:*?\"<>|foldername"; + filename = _blobNamingNormalizerProvider.NormalizeContainerName(filename); + filename.ShouldBe("thisismyfoldername"); + } + + [Fact] + public void NormalizeBlobName() + { + var filename = "thisismy:*?\"<>|filename"; + filename = _blobNamingNormalizerProvider.NormalizeContainerName(filename); + filename.ShouldBe("thisismyfilename"); + } + + [Theory] + [InlineData("/")] + [InlineData("\\")] + public void NormalizeContainerName_With_Directory(string delimiter) + { + var filename = $"thisis{delimiter}my:*?\"<>|{delimiter}foldername"; + filename = _blobNamingNormalizerProvider.NormalizeContainerName(filename); + filename.ShouldBe($"thisis{delimiter}my{delimiter}foldername"); + } + + [Theory] + [InlineData("/")] + [InlineData("\\")] + public void NormalizeBlobName_With_Directory(string delimiter) + { + var filename = $"thisis{delimiter}my:*?\"<>|{delimiter}filename"; + filename = _blobNamingNormalizerProvider.NormalizeContainerName(filename); + filename.ShouldBe($"thisis{delimiter}my{delimiter}filename"); + } + } +}