diff --git a/.github/workflows/liquid-ci-cd-adapter-dataverse.yml b/.github/workflows/liquid-ci-cd-adapter-dataverse.yml
index 2c2cd2cd..57a36385 100644
--- a/.github/workflows/liquid-ci-cd-adapter-dataverse.yml
+++ b/.github/workflows/liquid-ci-cd-adapter-dataverse.yml
@@ -1,5 +1,5 @@
# CI & CD workflow
-name: CI/CD - Liquid.Cache component for Liquid Application Framework
+name: CI/CD - Liquid.Adapter.Dataverse component for Liquid Application Framework
on:
push:
diff --git a/.github/workflows/liquid-ci-cd-adapter-storage.yml b/.github/workflows/liquid-ci-cd-adapter-storage.yml
new file mode 100644
index 00000000..15ec4232
--- /dev/null
+++ b/.github/workflows/liquid-ci-cd-adapter-storage.yml
@@ -0,0 +1,26 @@
+# CI & CD workflow
+name: CI/CD - Liquid.Adapter.AzureStorage component for Liquid Application Framework
+
+on:
+ push:
+ branches: [ main ]
+ paths:
+ - 'src/Liquid.Adapter.AzureStorage/**'
+
+ pull_request:
+ branches: [ main, releases/** ]
+ types: [opened, synchronize, reopened]
+ paths:
+ - 'src/Liquid.Adapter.AzureStorage/**'
+
+ # Allows you to run this workflow manually from the Actions tab
+ workflow_dispatch:
+
+jobs:
+ call-reusable-build-workflow:
+ uses: Avanade/Liquid-Application-Framework/.github/workflows/base-liquid-ci-and-cd.yml@main
+ with:
+ component_name: Liquid.Adapter.AzureStorage
+ secrets:
+ sonar_token: ${{ secrets.SONAR_TOKEN_STORAGE }}
+ nuget_token: ${{ secrets.PUBLISH_TO_NUGET_ORG }}
diff --git a/Liquid.Adapters.sln b/Liquid.Adapters.sln
index 5b844ab4..791a31e7 100644
--- a/Liquid.Adapters.sln
+++ b/Liquid.Adapters.sln
@@ -5,7 +5,15 @@ VisualStudioVersion = 17.6.34202.202
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Liquid.Adapter.Dataverse", "src\Liquid.Adapter.Dataverse\Liquid.Adapter.Dataverse.csproj", "{02191AB8-D13C-4CCC-8455-08813FB0C9C3}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Liquid.Adapter.Dataverse.Tests", "test\Liquid.Adapter.Dataverse.Tests\Liquid.Adapter.Dataverse.Tests.csproj", "{90D60966-6004-4705-907D-780503EBC141}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Liquid.Adapter.Dataverse.Tests", "test\Liquid.Adapter.Dataverse.Tests\Liquid.Adapter.Dataverse.Tests.csproj", "{90D60966-6004-4705-907D-780503EBC141}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Dataverse", "Dataverse", "{0E973865-5B87-43F2-B513-CD1DA96A2A3A}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AzureStorage", "AzureStorage", "{81CB75D9-FC31-4533-9A2D-C9277DD4A33E}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Liquid.Adapter.AzureStorage", "src\Liquid.Adapter.AzureStorage\Liquid.Adapter.AzureStorage.csproj", "{E21AF05A-738E-4DA2-AEEE-9900D7534F7C}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Liquid.Adapter.AzureStorage.Tests", "test\Liquid.Adapter.AzureStorage.Tests\Liquid.Adapter.AzureStorage.Tests.csproj", "{A2A7E164-98DF-4953-9679-B35E109E8990}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -21,10 +29,24 @@ Global
{90D60966-6004-4705-907D-780503EBC141}.Debug|Any CPU.Build.0 = Debug|Any CPU
{90D60966-6004-4705-907D-780503EBC141}.Release|Any CPU.ActiveCfg = Release|Any CPU
{90D60966-6004-4705-907D-780503EBC141}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E21AF05A-738E-4DA2-AEEE-9900D7534F7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E21AF05A-738E-4DA2-AEEE-9900D7534F7C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E21AF05A-738E-4DA2-AEEE-9900D7534F7C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E21AF05A-738E-4DA2-AEEE-9900D7534F7C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A2A7E164-98DF-4953-9679-B35E109E8990}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A2A7E164-98DF-4953-9679-B35E109E8990}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A2A7E164-98DF-4953-9679-B35E109E8990}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A2A7E164-98DF-4953-9679-B35E109E8990}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {02191AB8-D13C-4CCC-8455-08813FB0C9C3} = {0E973865-5B87-43F2-B513-CD1DA96A2A3A}
+ {90D60966-6004-4705-907D-780503EBC141} = {0E973865-5B87-43F2-B513-CD1DA96A2A3A}
+ {E21AF05A-738E-4DA2-AEEE-9900D7534F7C} = {81CB75D9-FC31-4533-9A2D-C9277DD4A33E}
+ {A2A7E164-98DF-4953-9679-B35E109E8990} = {81CB75D9-FC31-4533-9A2D-C9277DD4A33E}
+ EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {8105640B-39D2-4FFE-8BBD-D8E37F2A070D}
EndGlobalSection
diff --git a/src/Liquid.Adapter.AzureStorage/BlobClientFactory.cs b/src/Liquid.Adapter.AzureStorage/BlobClientFactory.cs
new file mode 100644
index 00000000..51b58eb7
--- /dev/null
+++ b/src/Liquid.Adapter.AzureStorage/BlobClientFactory.cs
@@ -0,0 +1,56 @@
+using Azure.Core;
+using Azure.Storage.Blobs;
+using Microsoft.Extensions.Options;
+
+namespace Liquid.Adapter.AzureStorage
+{
+ ///
+ public class BlobClientFactory : IBlobClientFactory
+ {
+ private readonly StorageSettings _options;
+ private IList _clients = new List();
+
+ ///
+ public IList Clients => _clients;
+
+ ///
+ /// Inicialize a new instance of
+ ///
+ /// Configurations set.
+ ///
+ public BlobClientFactory(IOptions? options)
+ {
+ _options = options?.Value ?? throw new ArgumentNullException(nameof(options));
+ }
+
+ ///
+ public List SetContainerClients()
+ {
+ if(_options.Containers.Count == 0)
+ throw new ArgumentNullException(nameof(_options));
+
+ var clients = new List();
+
+ foreach(var container in _options.Containers)
+ {
+ var client = new BlobContainerClient(container.ConnectionString,container.ContainerName);
+
+ clients.Add(client);
+ }
+
+ return clients;
+ }
+
+ ///
+ public BlobContainerClient GetContainerClient(string containerName)
+ {
+ var client = _clients.FirstOrDefault(x => x.Name == containerName);
+
+ if (client == null) {
+ throw new ArgumentException(nameof(containerName));
+ }
+
+ return client;
+ }
+ }
+}
diff --git a/src/Liquid.Adapter.AzureStorage/BlobStorageAdapter.cs b/src/Liquid.Adapter.AzureStorage/BlobStorageAdapter.cs
new file mode 100644
index 00000000..19531a1a
--- /dev/null
+++ b/src/Liquid.Adapter.AzureStorage/BlobStorageAdapter.cs
@@ -0,0 +1,166 @@
+using Azure.Storage.Blobs.Models;
+using Azure.Storage.Blobs.Specialized;
+using Azure.Storage.Sas;
+using System.Diagnostics.CodeAnalysis;
+using System.Text;
+
+namespace Liquid.Adapter.AzureStorage
+{
+ ///
+ [ExcludeFromCodeCoverage]
+ public class BlobStorageAdapter : ILiquidBlobStorageAdapter
+ {
+ private readonly IBlobClientFactory _factory;
+
+ ///
+ /// Initialize a new instance of
+ ///
+ ///
+ ///
+ public BlobStorageAdapter(IBlobClientFactory factory)
+ {
+ _factory = factory ?? throw new ArgumentNullException(nameof(factory));
+
+ _factory.SetContainerClients();
+ }
+
+ ///
+ public async Task DeleteByTags(IDictionary tags, string containerName)
+ {
+ var client = _factory.GetContainerClient(containerName);
+
+ var stringFilter = string.Empty;
+
+ foreach (var tag in tags)
+ {
+ stringFilter += @$"""{tag.Key}"" = '{tag.Value}' AND ";
+ }
+
+ stringFilter = stringFilter.Substring(0, stringFilter.Length - 4);
+
+ await foreach (TaggedBlobItem blobItem in client.FindBlobsByTagsAsync(stringFilter))
+ {
+ var blockBlob = client.GetBlockBlobClient(blobItem.BlobName);
+
+ await blockBlob.DeleteAsync();
+ };
+ }
+
+ ///
+ public async Task> GetAllBlobs(string containerName)
+ {
+ var client = _factory.GetContainerClient(containerName);
+
+ var results = new List();
+
+ await foreach (var blobItem in client.GetBlobsAsync())
+ {
+ var blockBlob = client.GetBlockBlobClient(blobItem.Name);
+ var blob = await blockBlob.DownloadContentAsync();
+
+ var item = new LiquidBlob
+ {
+ Blob = Encoding.UTF8.GetString(blob.Value.Content.ToArray()),
+ Name = blobItem.Name
+ };
+ results.Add(item);
+ }
+
+ return results;
+ }
+
+ ///
+ public async Task Delete(string id, string containerName)
+ {
+ var client = _factory.GetContainerClient(containerName);
+
+ var blobClient = client.GetBlobClient(id);
+
+ await blobClient.DeleteAsync();
+ }
+
+ ///
+ public async Task> ReadBlobsByTags(IDictionary tags, string containerName)
+ {
+ var client = _factory.GetContainerClient(containerName);
+
+ var stringFilter = string.Empty;
+ foreach (var tag in tags)
+ {
+ stringFilter += @$"""{tag.Key}"" = '{tag.Value}' AND ";
+ }
+ stringFilter = stringFilter.Substring(0, stringFilter.Length - 4);
+
+ var results = new List();
+ await foreach (TaggedBlobItem blobItem in client.FindBlobsByTagsAsync(stringFilter))
+ {
+ var blockBlob = client.GetBlockBlobClient(blobItem.BlobName);
+ var blob = await blockBlob.DownloadContentAsync();
+ var item = new LiquidBlob
+ {
+ Blob = Encoding.UTF8.GetString(blob.Value.Content.ToArray()),
+ Tags = blockBlob.GetTags().Value.Tags,
+ Name = blobItem.BlobName
+ };
+ results.Add(item);
+ }
+ return results;
+ }
+
+ ///
+ public async Task UploadBlob(string data, string name, string containerName, IDictionary? tags = null)
+ {
+ var client = _factory.GetContainerClient(containerName);
+
+ var blockBlob = client.GetBlockBlobClient(name);
+
+ var options = new BlobUploadOptions()
+ {
+ Tags = tags
+ };
+ await blockBlob.UploadAsync(new MemoryStream(Encoding.UTF8.GetBytes(data)), options);
+ }
+
+ ///
+ public async Task ReadBlobsByName(string blobName, string containerName)
+ {
+ var client = _factory.GetContainerClient(containerName);
+
+ var blockBlob = client.GetBlockBlobClient(blobName);
+ var blob = await blockBlob.DownloadContentAsync();
+ var item = new LiquidBlob
+ {
+ Blob = Encoding.UTF8.GetString(blob.Value.Content.ToArray()),
+ Tags = blockBlob.GetTags().Value.Tags,
+ Name = blobName
+ };
+
+ return item;
+ }
+
+ ///
+ public string? GetBlobSasUri(string blobName, string containerName, DateTimeOffset expiresOn, BlobContainerSasPermissions permissions)
+ {
+ var blobClient = _factory.GetContainerClient(containerName);
+
+ if (!blobClient.CanGenerateSasUri)
+ {
+ return null;
+ }
+
+ var sasBuilder = new BlobSasBuilder()
+ {
+ BlobContainerName = blobClient.Name,
+ BlobName = blobName,
+ Resource = "b"
+ };
+
+ sasBuilder.ExpiresOn = expiresOn;
+ sasBuilder.SetPermissions(permissions);
+
+ var sasURI = blobClient.GenerateSasUri(sasBuilder);
+
+ return sasURI.AbsolutePath;
+ }
+ }
+}
diff --git a/src/Liquid.Adapter.AzureStorage/Extensions/IServiceCollectionExtensions.cs b/src/Liquid.Adapter.AzureStorage/Extensions/IServiceCollectionExtensions.cs
new file mode 100644
index 00000000..0ed6723f
--- /dev/null
+++ b/src/Liquid.Adapter.AzureStorage/Extensions/IServiceCollectionExtensions.cs
@@ -0,0 +1,36 @@
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Liquid.Adapter.AzureStorage.Extensions
+{
+ ///
+ /// Extension methods of
+ /// for register Liquid Azure Storage services.
+ ///
+ [ExcludeFromCodeCoverage]
+ public static class IServiceCollectionExtensions
+ {
+ ///
+ /// Registers service, it's dependency
+ /// , and also set configuration
+ /// option .
+ ///
+ /// service collection instance.
+ /// configuration section of storage settings.
+ public static IServiceCollection AddLiquidAzureStorageAdapter(this IServiceCollection services, string configSection)
+ {
+ services.AddOptions()
+ .Configure((settings, configuration) =>
+ {
+ configuration.GetSection(configSection).Bind(settings);
+ });
+
+ services.AddTransient();
+
+ services.AddSingleton();
+
+ return services;
+ }
+ }
+}
diff --git a/src/Liquid.Adapter.AzureStorage/IBlobClientFactory.cs b/src/Liquid.Adapter.AzureStorage/IBlobClientFactory.cs
new file mode 100644
index 00000000..6c977af2
--- /dev/null
+++ b/src/Liquid.Adapter.AzureStorage/IBlobClientFactory.cs
@@ -0,0 +1,30 @@
+using Azure.Storage.Blobs;
+
+namespace Liquid.Adapter.AzureStorage
+{
+ ///
+ /// instances factory.
+ ///
+ public interface IBlobClientFactory
+ {
+ ///
+ /// List of instances of .
+ ///
+ IList Clients { get; }
+
+ ///
+ /// Initialize an instance of
+ /// for each container on the and
+ /// add to .
+ ///
+ List SetContainerClients();
+
+ ///
+ /// Get an instance of
+ /// by name.
+ ///
+ ///
+ ///
+ BlobContainerClient GetContainerClient(string containerName);
+ }
+}
diff --git a/src/Liquid.Adapter.AzureStorage/ILiquidBlobStorageAdapter.cs b/src/Liquid.Adapter.AzureStorage/ILiquidBlobStorageAdapter.cs
new file mode 100644
index 00000000..a6fe25cf
--- /dev/null
+++ b/src/Liquid.Adapter.AzureStorage/ILiquidBlobStorageAdapter.cs
@@ -0,0 +1,71 @@
+using Azure.Storage.Sas;
+
+namespace Liquid.Adapter.AzureStorage
+{
+ ///
+ /// Definition of BlobStorage integration service.
+ ///
+ public interface ILiquidBlobStorageAdapter
+ {
+ ///
+ /// Upload a specific blob.
+ ///
+ /// Blob content.
+ /// Blob path.
+ /// Blob container name.
+ /// Blob list of tags.
+ Task UploadBlob(string data, string name, string containerName, IDictionary? tags = null);
+
+ ///
+ /// Remove blob by id.
+ ///
+ /// blob name.
+ /// Blob container name.
+ Task Delete(string id, string containerName);
+
+ ///
+ /// Filter blob by tags and remove them.
+ ///
+ /// Tags for filter.
+ /// Blob container name.
+ Task DeleteByTags(IDictionary tags, string containerName);
+
+ ///
+ /// Get all blobs from a container.
+ ///
+ /// Blob container name.
+ /// List of .
+ Task> GetAllBlobs(string containerName);
+
+ ///
+ /// Filter blobs by tags.
+ ///
+ /// Tags for filter.
+ /// Blob container name.
+ /// List of .
+ Task> ReadBlobsByTags(IDictionary tags, string containerName);
+
+ ///
+ /// Dowload a specific blob.
+ ///
+ /// Blob Id.
+ /// Blob container name.
+ /// .
+ Task ReadBlobsByName(string blobName, string containerName);
+
+ ///
+ /// generates a Blob Shared Access Signature (SAS) Uri
+ /// based on the parameters passed. The SAS is signed by the shared key
+ /// credential of the client.
+ ///
+ /// The id of the blob.
+ /// Name of the container where the blob is stored.
+ /// The time at which the shared access signature becomes invalid.
+ /// This field must be omitted if it has been specified in an
+ /// associated stored access policy.
+ /// The permissions associated with the shared access signature. The
+ /// user is restricted to operations allowed by the permissions.
+ /// Blob sas uri absolute path.
+ string? GetBlobSasUri(string blobName, string containerName, DateTimeOffset expiresOn, BlobContainerSasPermissions permissions);
+ }
+}
diff --git a/src/Liquid.Adapter.AzureStorage/Liquid.Adapter.AzureStorage.csproj b/src/Liquid.Adapter.AzureStorage/Liquid.Adapter.AzureStorage.csproj
new file mode 100644
index 00000000..c3d8f88b
--- /dev/null
+++ b/src/Liquid.Adapter.AzureStorage/Liquid.Adapter.AzureStorage.csproj
@@ -0,0 +1,36 @@
+
+
+
+ net6.0
+ enable
+ enable
+ Avanade Brazil
+ Avanade Inc.
+ Liquid - Modern Application Framework
+ Avanade 2019
+ 6.0.0-preview-20221201-01
+ true
+ true
+
+ Adapter for Microsoft Azure Storage integrations.
+ This component is part of Liquid Application Framework.
+
+ logo.png
+ https://github.com/Avanade/Liquid-Application-Framework
+
+
+
+
+ True
+ \
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Liquid.Adapter.AzureStorage/LiquidBlob.cs b/src/Liquid.Adapter.AzureStorage/LiquidBlob.cs
new file mode 100644
index 00000000..84e5c0cc
--- /dev/null
+++ b/src/Liquid.Adapter.AzureStorage/LiquidBlob.cs
@@ -0,0 +1,26 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace Liquid.Adapter.AzureStorage
+{
+ ///
+ /// Set de propriedades referentes à um item do BlobStorage.
+ ///
+ [ExcludeFromCodeCoverage]
+ public class LiquidBlob
+ {
+ ///
+ /// Lista de tags referentes ao blob.
+ ///
+ public IDictionary? Tags { get; set; }
+
+ ///
+ /// Conteúdo do blob.
+ ///
+ public string? Blob { get; set; }
+
+ ///
+ /// Nome do arquivo no Storage.
+ ///
+ public string Name { get; set; }
+ }
+}
diff --git a/src/Liquid.Adapter.AzureStorage/StorageSettings.cs b/src/Liquid.Adapter.AzureStorage/StorageSettings.cs
new file mode 100644
index 00000000..73ccd0c3
--- /dev/null
+++ b/src/Liquid.Adapter.AzureStorage/StorageSettings.cs
@@ -0,0 +1,32 @@
+namespace Liquid.Adapter.AzureStorage
+{
+ ///
+ /// Set of Azure Storage containers configs.
+ ///
+ public class StorageSettings
+ {
+ ///
+ /// List of container settings.
+ ///
+ public List Containers { get; set; } = new List();
+ }
+
+ ///
+ /// Set of a container connection configuration.
+ ///
+ public class ContainerSettings
+ {
+ ///
+ /// A connection string includes the authentication information
+ /// required for your application to access data in an Azure Storage
+ /// account at runtime.
+ ///
+ public string ConnectionString { get; set; }
+
+ ///
+ /// The name of the blob container in the storage account to reference.
+ ///
+ public string ContainerName { get; set; }
+
+ }
+}
diff --git a/test/Liquid.Adapter.AzureStorage.Tests/BlobClientFactoryTests.cs b/test/Liquid.Adapter.AzureStorage.Tests/BlobClientFactoryTests.cs
new file mode 100644
index 00000000..8da4b530
--- /dev/null
+++ b/test/Liquid.Adapter.AzureStorage.Tests/BlobClientFactoryTests.cs
@@ -0,0 +1,67 @@
+using Azure.Storage.Blobs;
+using Microsoft.Extensions.Options;
+using NSubstitute;
+
+namespace Liquid.Adapter.AzureStorage.Tests
+{
+ public class BlobClientFactoryTests
+ {
+
+ private readonly IBlobClientFactory _sut;
+ private readonly IOptions _options;
+
+ public BlobClientFactoryTests()
+ {
+ _options = Substitute.For>();
+
+ var settings = new StorageSettings();
+ settings.Containers.Add(new ContainerSettings()
+ {
+ ContainerName = "test",
+ ConnectionString = "testestestes"
+ });
+
+ _options.Value.ReturnsForAnyArgs(settings);
+ _sut = new BlobClientFactory(_options);
+ }
+
+
+ [Fact]
+ public void Ctor_WhenOptionsIsNull_ThenReturnArgumentNullException()
+ {
+ Assert.Throws(() => new BlobClientFactory(null));
+ }
+
+ [Fact]
+ public void Ctor_WhenOptionsExists_ThenBlobClientFactoryInstance()
+ {
+ var result = new BlobClientFactory(_options);
+ Assert.NotNull(result);
+ Assert.IsType(result);
+ }
+ [Fact]
+ public void SetContainerClients_WhenOptionsNotSet_ThenThrowArgumentNullException()
+ {
+ var options = Substitute.For>();
+ options.Value.ReturnsForAnyArgs(new StorageSettings());
+
+ var sut = new BlobClientFactory(options);
+
+ Assert.Throws(() => sut.SetContainerClients()) ;
+
+ }
+
+ [Fact]
+ public void SetContainerClients_WhenContainerNameIsInvalid_ThenThrowFormatException()
+ {
+ Assert.True(_sut.Clients.Count == 0);
+ Assert.Throws(() => _sut.SetContainerClients());
+ }
+
+ [Fact]
+ public void GetContainerClient_WhenClientDoesntExists_ThenThrowArgumentException()
+ {
+ Assert.Throws(() => _sut.GetContainerClient("test"));
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Liquid.Adapter.AzureStorage.Tests/Liquid.Adapter.AzureStorage.Tests.csproj b/test/Liquid.Adapter.AzureStorage.Tests/Liquid.Adapter.AzureStorage.Tests.csproj
new file mode 100644
index 00000000..d51aa7de
--- /dev/null
+++ b/test/Liquid.Adapter.AzureStorage.Tests/Liquid.Adapter.AzureStorage.Tests.csproj
@@ -0,0 +1,29 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+ false
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
diff --git a/test/Liquid.Adapter.AzureStorage.Tests/Usings.cs b/test/Liquid.Adapter.AzureStorage.Tests/Usings.cs
new file mode 100644
index 00000000..8c927eb7
--- /dev/null
+++ b/test/Liquid.Adapter.AzureStorage.Tests/Usings.cs
@@ -0,0 +1 @@
+global using Xunit;
\ No newline at end of file