diff --git a/ManagedCode.Storage.Azure/AzureBlobStorage.cs b/ManagedCode.Storage.Azure/AzureBlobStorage.cs new file mode 100644 index 00000000..633fa5b1 --- /dev/null +++ b/ManagedCode.Storage.Azure/AzureBlobStorage.cs @@ -0,0 +1,191 @@ +using Azure.Storage.Blobs; +using Azure.Storage.Blobs.Models; +using ManagedCode.Storage.Azure.Options; +using ManagedCode.Storage.Core; +using ManagedCode.Storage.Core.Models; +using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace ManagedCode.Storage.Azure +{ + public class AzureBlobStorage : IBlobStorage + { + private readonly BlobContainerClient _blobContainerClient; + + public AzureBlobStorage(AzureBlobStorageConnectionOptions connectionOptions) + { + _blobContainerClient = new BlobContainerClient( + connectionOptions.ConnectionString, + connectionOptions.Container + ); + + _blobContainerClient.CreateIfNotExists(PublicAccessType.BlobContainer); + } + + public void Dispose() + { + } + + #region Delete + + public async Task DeleteAsync(string blob, CancellationToken cancellationToken = default) + { + var blobClient = _blobContainerClient.GetBlobClient(blob); + await blobClient.DeleteAsync(DeleteSnapshotsOption.None, null, cancellationToken); + } + + public async Task DeleteAsync(Blob blob, CancellationToken cancellationToken = default) + { + var blobClient = _blobContainerClient.GetBlobClient(blob.Name); + await blobClient.DeleteAsync(DeleteSnapshotsOption.None, null, cancellationToken); + } + + public async Task DeleteAsync(IEnumerable blobs, CancellationToken cancellationToken = default) + { + foreach (var blobName in blobs) + { + await DeleteAsync(blobName, cancellationToken); + } + } + + public async Task DeleteAsync(IEnumerable blobs, CancellationToken cancellationToken = default) + { + foreach (var blob in blobs) + { + await DeleteAsync(blob, cancellationToken); + } + } + + #endregion + + #region Download + + public async Task DownloadAsStreamAsync(string blob, CancellationToken cancellationToken = default) + { + var blobClient = _blobContainerClient.GetBlobClient(blob); + var res = await blobClient.DownloadStreamingAsync(); + + return res.Value.Content; + } + + public async Task DownloadAsStreamAsync(Blob blob, CancellationToken cancellationToken = default) + { + return await DownloadAsStreamAsync(blob.Name, cancellationToken); + } + + public async Task DownloadAsync(string blob, CancellationToken cancellationToken = default) + { + var blobClient = _blobContainerClient.GetBlobClient(blob); + var localFile = new LocalFile(); + + await blobClient.DownloadToAsync(localFile.FileStream, cancellationToken); + + return localFile; + } + + public async Task DownloadAsync(Blob blob, CancellationToken cancellationToken = default) + { + return await DownloadAsync(blob.Name, cancellationToken); + } + + #endregion + + #region Exists + + public async Task ExistsAsync(string blob, CancellationToken cancellationToken = default) + { + var blobClient = _blobContainerClient.GetBlobClient(blob); + + return await blobClient.ExistsAsync(cancellationToken); + } + + public async Task ExistsAsync(Blob blob, CancellationToken cancellationToken = default) + { + var blobClient = _blobContainerClient.GetBlobClient(blob.Name); + + return await blobClient.ExistsAsync(cancellationToken); + } + + public async IAsyncEnumerable ExistsAsync(IEnumerable blobs, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + foreach(var blob in blobs) + { + var blobClient = _blobContainerClient.GetBlobClient(blob); + yield return await blobClient.ExistsAsync(cancellationToken); + } + } + + public async IAsyncEnumerable ExistsAsync(IEnumerable blobs, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + foreach (var blob in blobs) + { + var blobClient = _blobContainerClient.GetBlobClient(blob.Name); + yield return await blobClient.ExistsAsync(cancellationToken); + } + } + + #endregion + + #region Get + + public async Task GetBlobAsync(string blob, CancellationToken cancellationToken = default) + { + await Task.Yield(); + + var blobClient = _blobContainerClient.GetBlobClient(blob); + + return new Blob() + { + Name = blobClient.Name, + Uri = blobClient.Uri + }; + } + + public IAsyncEnumerable GetBlobsAsync(IEnumerable blobs, CancellationToken cancellationToken = default) + { + throw new System.NotImplementedException(); + } + + public IAsyncEnumerable GetBlobListAsync(CancellationToken cancellationToken = default) + { + throw new System.NotImplementedException(); + } + + #endregion + + #region Upload + + public async Task UploadAsync(string blob, Stream dataStream, bool append = false, CancellationToken cancellationToken = default) + { + var blobClient = _blobContainerClient.GetBlobClient(blob); + await blobClient.UploadAsync(dataStream, cancellationToken); + } + + public async Task UploadAsync(string blob, string pathToFile, bool append = false, CancellationToken cancellationToken = default) + { + var blobClient = _blobContainerClient.GetBlobClient(blob); + + using (var fs = new FileStream(pathToFile, FileMode.Open, FileAccess.Read)) + { + await blobClient.UploadAsync(fs, cancellationToken); + } + } + + public async Task UploadAsync(Blob blob, Stream dataStream, bool append = false, CancellationToken cancellationToken = default) + { + await UploadAsync(blob.Name, dataStream, append, cancellationToken); + } + + public async Task UploadAsync(Blob blob, string pathToFile, bool append = false, CancellationToken cancellationToken = default) + { + await UploadAsync(blob.Name, pathToFile, append, cancellationToken); + } + + #endregion + } +} diff --git a/ManagedCode.Storage.Azure/Builders/AzureProviderBuilder.cs b/ManagedCode.Storage.Azure/Builders/AzureProviderBuilder.cs new file mode 100644 index 00000000..a4db7567 --- /dev/null +++ b/ManagedCode.Storage.Azure/Builders/AzureProviderBuilder.cs @@ -0,0 +1,12 @@ +using ManagedCode.Storage.Core.Builders; +using Microsoft.Extensions.DependencyInjection; + +namespace ManagedCode.Storage.Azure.Builders +{ + public class AzureProviderBuilder : ProviderBuilder + { + public AzureProviderBuilder(IServiceCollection serviceCollection) : base(serviceCollection) {} + + + } +} diff --git a/ManagedCode.Storage.Azure/EnumeratorCacellationAttribute.cs b/ManagedCode.Storage.Azure/EnumeratorCacellationAttribute.cs new file mode 100644 index 00000000..350ce132 --- /dev/null +++ b/ManagedCode.Storage.Azure/EnumeratorCacellationAttribute.cs @@ -0,0 +1,8 @@ +using System; + +namespace ManagedCode.Storage.Azure +{ + internal class EnumeratorCacellationAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/ManagedCode.Storage.Azure/Extensions/ProviderExtensions.cs b/ManagedCode.Storage.Azure/Extensions/ProviderExtensions.cs new file mode 100644 index 00000000..1e99dd3b --- /dev/null +++ b/ManagedCode.Storage.Azure/Extensions/ProviderExtensions.cs @@ -0,0 +1,29 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using ManagedCode.Storage.Core.Builders; +using ManagedCode.Storage.Core.Helpers; +using ManagedCode.Storage.Core; +using ManagedCode.Storage.Azure.Options; + +namespace ManagedCode.Storage.Azure.Extensions +{ + public static class ProviderExtensions + { + public static ProviderBuilder AddAzureBlobStorage( + this ProviderBuilder providerBuilder, + Action action) + where TAzureStorage : IBlobStorage + { + var connectionOptions = new AzureBlobStorageConnectionOptions(); + action.Invoke(connectionOptions); + + var implementationType = TypeHelpers.GetImplementationType(); + providerBuilder.ServiceCollection.AddScoped(typeof(TAzureStorage), x => Activator.CreateInstance(implementationType, connectionOptions)); + + // Because of AzureBlobStorage does not inherits TAzureStorage, DI complains on unability of casting + // providerBuilder.ServiceCollection.AddScoped(typeof(TAzureStorage), x => new AzureBlobStorage(connectionOptions)); + + return providerBuilder; + } + } +} diff --git a/ManagedCode.Storage.Azure/ManagedCode.Storage.Azure.csproj b/ManagedCode.Storage.Azure/ManagedCode.Storage.Azure.csproj index f47e2998..38f7aa65 100644 --- a/ManagedCode.Storage.Azure/ManagedCode.Storage.Azure.csproj +++ b/ManagedCode.Storage.Azure/ManagedCode.Storage.Azure.csproj @@ -26,6 +26,7 @@ + diff --git a/ManagedCode.Storage.Azure/Options/AzureBlobStorageConnectionOptions.cs b/ManagedCode.Storage.Azure/Options/AzureBlobStorageConnectionOptions.cs new file mode 100644 index 00000000..fe646d7c --- /dev/null +++ b/ManagedCode.Storage.Azure/Options/AzureBlobStorageConnectionOptions.cs @@ -0,0 +1,8 @@ +namespace ManagedCode.Storage.Azure.Options +{ + public class AzureBlobStorageConnectionOptions + { + public string ConnectionString { get; set; } + public string Container { get; set; } + } +} diff --git a/ManagedCode.Storage.Core/Builders/ProviderBuilder.cs b/ManagedCode.Storage.Core/Builders/ProviderBuilder.cs new file mode 100644 index 00000000..54617039 --- /dev/null +++ b/ManagedCode.Storage.Core/Builders/ProviderBuilder.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace ManagedCode.Storage.Core.Builders +{ + public class ProviderBuilder + { + public IServiceCollection ServiceCollection { get; } + + public ProviderBuilder(IServiceCollection serviceCollection) + { + ServiceCollection = serviceCollection; + } + } +} diff --git a/ManagedCode.Storage.Core/Extensions/ServiceCollectionExtensions.cs b/ManagedCode.Storage.Core/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..9968f322 --- /dev/null +++ b/ManagedCode.Storage.Core/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.DependencyInjection; +using ManagedCode.Storage.Core.Builders; + +namespace ManagedCode.Storage.Core.Extensions +{ + public static class ServiceCollectionExtensions + { + public static ProviderBuilder AddManagedCodeStorage(this IServiceCollection serviceCollection) + { + return new ProviderBuilder(serviceCollection); + } + } +} diff --git a/ManagedCode.Storage.Core/Helpers/TypeHelpers.cs b/ManagedCode.Storage.Core/Helpers/TypeHelpers.cs new file mode 100644 index 00000000..f91c8fd6 --- /dev/null +++ b/ManagedCode.Storage.Core/Helpers/TypeHelpers.cs @@ -0,0 +1,49 @@ +using System; +using System.Reflection; +using System.Reflection.Emit; + +namespace ManagedCode.Storage.Core.Helpers +{ + public static class TypeHelpers + { + public static Type GetImplementationType() + where TAbstraction : IBlobStorage + { + var typeSignature = typeof(TAbstraction).Name; + var an = new AssemblyName(typeSignature); + AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(an, AssemblyBuilderAccess.Run); + ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("ManagedCodeCModule"); + TypeBuilder tb = moduleBuilder.DefineType(typeSignature, + TypeAttributes.Public | + TypeAttributes.Class | + TypeAttributes.AutoClass | + TypeAttributes.AnsiClass | + TypeAttributes.BeforeFieldInit | + TypeAttributes.AutoLayout, + null); + + tb.SetParent(typeof(TImplementation)); + tb.AddInterfaceImplementation(typeof(TAbstraction)); + + var newConstructor = tb.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, + new Type[] { typeof(TOptions) }); + + var baseConstructors = typeof(TImplementation).GetConstructors( + BindingFlags.Public | + BindingFlags.NonPublic | + BindingFlags.Instance); + + var emitter = newConstructor.GetILGenerator(); + emitter.Emit(OpCodes.Nop); + + // Load `this` and call base constructor with arguments + emitter.Emit(OpCodes.Ldarg_0); + emitter.Emit(OpCodes.Ldarg, 1); + emitter.Emit(OpCodes.Call, baseConstructors[0]); + + emitter.Emit(OpCodes.Ret); + + return tb.CreateType(); + } + } +} diff --git a/ManagedCode.Storage.Core/IBlobStorage.cs b/ManagedCode.Storage.Core/IBlobStorage.cs index aec839ca..fbe69e8e 100644 --- a/ManagedCode.Storage.Core/IBlobStorage.cs +++ b/ManagedCode.Storage.Core/IBlobStorage.cs @@ -3,17 +3,16 @@ using System.IO; using System.Threading; using System.Threading.Tasks; +using ManagedCode.Storage.Core.Models; namespace ManagedCode.Storage.Core { - public interface IStorage : IDisposable + public interface IBlobStorage : IDisposable { IAsyncEnumerable GetBlobListAsync(CancellationToken cancellationToken = default); - IAsyncEnumerable GetBlob(string blob, CancellationToken cancellationToken = default); - IAsyncEnumerable GetBlob(Blob blob, CancellationToken cancellationToken = default); - IAsyncEnumerable GetBlob(IEnumerable blobs, CancellationToken cancellationToken = default); - IAsyncEnumerable GetBlob(IEnumerable blobs, CancellationToken cancellationToken = default); - + IAsyncEnumerable GetBlobsAsync(IEnumerable blobs, CancellationToken cancellationToken = default); + Task GetBlobAsync(string blob, CancellationToken cancellationToken = default); + Task UploadAsync(string blob, Stream dataStream, bool append = false, CancellationToken cancellationToken = default); Task UploadAsync(string blob, string pathToFile, bool append = false, CancellationToken cancellationToken = default); Task UploadAsync(Blob blob, Stream dataStream, bool append = false, CancellationToken cancellationToken = default); @@ -33,11 +32,5 @@ public interface IStorage : IDisposable Task ExistsAsync(Blob blob, CancellationToken cancellationToken = default); IAsyncEnumerable ExistsAsync(IEnumerable blobs, CancellationToken cancellationToken = default); IAsyncEnumerable ExistsAsync(IEnumerable blobs, CancellationToken cancellationToken = default); - - } - - public class Blob - { - public string Path { get; set; } } } \ No newline at end of file diff --git a/ManagedCode.Storage.Core/ManagedCode.Storage.Core.csproj b/ManagedCode.Storage.Core/ManagedCode.Storage.Core.csproj index 3b2037fc..90f17e82 100644 --- a/ManagedCode.Storage.Core/ManagedCode.Storage.Core.csproj +++ b/ManagedCode.Storage.Core/ManagedCode.Storage.Core.csproj @@ -22,6 +22,7 @@ + diff --git a/ManagedCode.Storage.Core/Models/Blob.cs b/ManagedCode.Storage.Core/Models/Blob.cs new file mode 100644 index 00000000..f4798036 --- /dev/null +++ b/ManagedCode.Storage.Core/Models/Blob.cs @@ -0,0 +1,10 @@ +using System; + +namespace ManagedCode.Storage.Core.Models +{ + public class Blob + { + public string Name { get; set; } + public Uri Uri { get; set; } + } +} diff --git a/ManagedCode.Storage.Tests/Azure/AzureStorageTests.cs b/ManagedCode.Storage.Tests/Azure/AzureStorageTests.cs new file mode 100644 index 00000000..1cf8947d --- /dev/null +++ b/ManagedCode.Storage.Tests/Azure/AzureStorageTests.cs @@ -0,0 +1,91 @@ +using ManagedCode.Storage.Core.Extensions; +using ManagedCode.Storage.Azure.Extensions; +using Microsoft.Extensions.DependencyInjection; +using System.Threading.Tasks; +using Xunit; +using FluentAssertions; +using System.IO; +using System.Text; + +namespace ManagedCode.Storage.Tests.Azure +{ + public class AzureStorageTests + { + private IPhotoStorage _photoStorage; + private IDocumentStorage _documentStorage; + + public AzureStorageTests() + { + var services = new ServiceCollection(); + + services.AddManagedCodeStorage() + .AddAzureBlobStorage(opt => { + opt.ConnectionString = "DefaultEndpointsProtocol=https;AccountName=storagestudying;AccountKey=4Y4IBrITEoWYMGe0gNju9wvUQrWi//1VvPIDN2dYWccWKy9uuKWnMBXxQlmcy3Q9UIU70ZJiy8ULD9QITxyeTQ==;EndpointSuffix=core.windows.net"; + opt.Container = "photos"; + }) + .AddAzureBlobStorage(opt => { + opt.ConnectionString = "DefaultEndpointsProtocol=https;AccountName=storagestudying;AccountKey=4Y4IBrITEoWYMGe0gNju9wvUQrWi//1VvPIDN2dYWccWKy9uuKWnMBXxQlmcy3Q9UIU70ZJiy8ULD9QITxyeTQ==;EndpointSuffix=core.windows.net"; + opt.Container = "documents"; + }); + + var provider = services.BuildServiceProvider(); + + _photoStorage = provider.GetService(); + _documentStorage = provider.GetService(); + } + + [Fact] + public void WhenDIInitialized() + { + _photoStorage.Should().NotBeNull(); + _documentStorage.Should().NotBeNull(); + } + + [Fact] + public async Task WhenSingleBlobExistsIsCalled() + { + var result = await _photoStorage.ExistsAsync("34.png"); + + result.Should().BeTrue(); + } + + [Fact] + public async Task WhenDownloadAsyncIsCalled() + { + var stream = await _documentStorage.DownloadAsStreamAsync("a.txt"); + using var sr = new StreamReader(stream, Encoding.UTF8); + + string content = sr.ReadToEnd(); + + content.Should().NotBeNull(); + } + + [Fact] + public async Task WhenDownloadAsyncToFileIsCalled() + { + var tempFile = await _documentStorage.DownloadAsync("a.txt"); + using var sr = new StreamReader(tempFile.FileStream, Encoding.UTF8); + + string content = sr.ReadToEnd(); + + content.Should().NotBeNull(); + } + + [Fact] + public async Task WhenUploadAsyncIsCalled() + { + var lineToUpload = "some crazy text"; + + var byteArray = Encoding.ASCII.GetBytes(lineToUpload); + var stream = new MemoryStream(byteArray); + + await _documentStorage.UploadAsync("b.txt", stream); + } + + [Fact] + public async Task WhenDeleteAsyncIsCalled() + { + await _documentStorage.DeleteAsync("a.txt"); + } + } +} diff --git a/ManagedCode.Storage.Tests/Azure/IDocumentStorage.cs b/ManagedCode.Storage.Tests/Azure/IDocumentStorage.cs new file mode 100644 index 00000000..ae2952ca --- /dev/null +++ b/ManagedCode.Storage.Tests/Azure/IDocumentStorage.cs @@ -0,0 +1,9 @@ +using ManagedCode.Storage.Azure; +using ManagedCode.Storage.Core; + +namespace ManagedCode.Storage.Tests.Azure +{ + public interface IDocumentStorage : IBlobStorage + { + } +} diff --git a/ManagedCode.Storage.Tests/Azure/IPhotoStorage.cs b/ManagedCode.Storage.Tests/Azure/IPhotoStorage.cs new file mode 100644 index 00000000..49e35665 --- /dev/null +++ b/ManagedCode.Storage.Tests/Azure/IPhotoStorage.cs @@ -0,0 +1,9 @@ +using ManagedCode.Storage.Azure; +using ManagedCode.Storage.Core; + +namespace ManagedCode.Storage.Tests.Azure +{ + public interface IPhotoStorage : IBlobStorage + { + } +} diff --git a/ManagedCode.Storage.Tests/ManagedCode.Storage.Tests.csproj b/ManagedCode.Storage.Tests/ManagedCode.Storage.Tests.csproj index 89712318..35846b37 100644 --- a/ManagedCode.Storage.Tests/ManagedCode.Storage.Tests.csproj +++ b/ManagedCode.Storage.Tests/ManagedCode.Storage.Tests.csproj @@ -9,6 +9,7 @@ +