Use generated internal interfaces for DI services

This commit is contained in:
Adam Hathcock
2026-10-21 09:12:42 +01:00
parent b578325982
commit 4cc8d6b257
13 changed files with 179 additions and 28 deletions

View File

@@ -144,6 +144,10 @@ Pure.DI is a private build-time dependency; consumers
do not need a container or a new client API. The default graph reuses workflow services, not
opened archives, readers, writers, or streams, which retain their existing ownership rules.
Services are exposed directly as typed composition roots, each a singleton within its composition.
Roots and constructor dependencies use internal interfaces generated by Pure.DI's
`[GenerateInterface(asInternal: true)]`. `Services/ServiceContracts.cs` declares their identities
for composition binding and supplements the sync signatures produced by Zomp, since source
generators cannot inspect one another's generated members.
Public extension classes remain beside their public abstractions. Simple overloads supply defaults
locally; extraction and filesystem workflows delegate to `ArchiveExtractor` and `ArchiveFileWriter`,
including reader extraction, writable-archive entry creation, and saving archives to files.

View File

@@ -13,7 +13,7 @@ namespace SharpCompress.Archives;
public static partial class ArchiveFactory
{
private static ArchiveService Service => LibraryDefaults.Composition.Archives;
private static IArchiveService Service => LibraryDefaults.Composition.Archives;
public static IArchive OpenArchive(Stream stream, ReaderOptions? readerOptions = null) =>
Service.OpenArchive(stream, readerOptions);

View File

@@ -10,7 +10,7 @@ namespace SharpCompress.Archives;
public static partial class IArchiveEntryExtensions
{
// Give the sync generator a known receiver type before Pure.DI generates the root property.
private static ArchiveExtractor Extractor => LibraryDefaults.Composition.Extractor;
private static IArchiveExtractor Extractor => LibraryDefaults.Composition.Extractor;
/// <param name="archiveEntry">The archive entry to extract.</param>
extension(IArchiveEntry archiveEntry)

View File

@@ -2,6 +2,7 @@ using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Pure.DI;
using SharpCompress.Archives;
using SharpCompress.Common;
using SharpCompress.IO;
@@ -9,7 +10,8 @@ using SharpCompress.IO;
namespace SharpCompress.Services;
// Destination streams are borrowed, not owned by the reusable extraction service.
internal sealed partial class ArchiveExtractor
[GenerateInterface(asInternal: true)]
internal sealed partial class ArchiveExtractor : IArchiveExtractor
{
[Zomp.SyncMethodGenerator.CreateSyncVersion(PreserveProgress = true)]
public async ValueTask WriteToAsync(

View File

@@ -3,11 +3,13 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Pure.DI;
using SharpCompress.Writers;
namespace SharpCompress.Services;
internal sealed partial class ArchiveFileWriter
[GenerateInterface(asInternal: true)]
internal sealed partial class ArchiveFileWriter : IArchiveFileWriter
{
public void Write(IWriter writer, string entryPath, FileInfo source)
{

View File

@@ -88,7 +88,7 @@ internal sealed partial class ArchiveService
.ConfigureAwait(false);
}
internal ValueTask<T> FindFactoryAsync<T>(
public ValueTask<T> FindFactoryAsync<T>(
string filePath,
CancellationToken cancellationToken = default
)
@@ -98,7 +98,7 @@ internal sealed partial class ArchiveService
return FindFactoryAsync<T>(new FileInfo(filePath), cancellationToken);
}
internal async ValueTask<T> FindFactoryAsync<T>(
public async ValueTask<T> FindFactoryAsync<T>(
FileInfo fileInfo,
CancellationToken cancellationToken = default
)
@@ -123,7 +123,7 @@ internal sealed partial class ArchiveService
.ConfigureAwait(false);
}
internal async ValueTask<T> FindFactoryAsync<T>(
public async ValueTask<T> FindFactoryAsync<T>(
Stream stream,
CancellationToken cancellationToken = default
)
@@ -181,7 +181,8 @@ internal sealed partial class ArchiveService
try
{
foreach (var factory in catalog.Factories)
// The explicit type lets Zomp resolve probe overloads before Pure.DI emits catalog members.
foreach (IFactory factory in catalog.Factories)
{
stream.Seek(startPosition, SeekOrigin.Begin);
var isArchive = await factory
@@ -279,7 +280,7 @@ internal sealed partial class ArchiveService
try
{
foreach (var factory in catalog.Factories)
foreach (IFactory factory in catalog.Factories)
{
stream.Seek(startPosition, SeekOrigin.Begin);
if (
@@ -334,7 +335,7 @@ internal sealed partial class ArchiveService
CancellationToken cancellationToken
)
{
foreach (var wrapper in catalog.Wrappers)
foreach (TarWrapper wrapper in catalog.Wrappers)
{
#if !SYNC_ONLY
cancellationToken.ThrowIfCancellationRequested();

View File

@@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Pure.DI;
using SharpCompress.Archives;
using SharpCompress.Common;
using SharpCompress.Common.Options;
@@ -12,11 +13,12 @@ using SharpCompress.Readers;
namespace SharpCompress.Services;
[GenerateInterface(asInternal: true)]
internal sealed partial class ArchiveService(
FactoryCatalog catalog,
ReaderService readers,
ArchiveExtractor extractor
)
IFactoryCatalog catalog,
IReaderService readers,
IArchiveExtractor extractor
) : IArchiveService
{
public IArchive OpenArchive(Stream stream, ReaderOptions? readerOptions = null)
{

View File

@@ -1,14 +1,16 @@
using System;
using System.Collections.Generic;
using Pure.DI;
using SharpCompress.Factories;
namespace SharpCompress.Services;
// Read through on each operation: legacy registrations made after composition must remain visible.
[GenerateInterface(asInternal: true)]
internal sealed class FactoryCatalog(
Func<IEnumerable<IFactory>> factories,
Func<IEnumerable<TarWrapper>> wrappers
)
) : IFactoryCatalog
{
public IEnumerable<IFactory> Factories => factories();
public IEnumerable<TarWrapper> Wrappers => wrappers();

View File

@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Linq;
using Pure.DI;
using SharpCompress.Common;
using SharpCompress.Factories;
using SharpCompress.IO;
@@ -8,7 +9,8 @@ using SharpCompress.Readers;
namespace SharpCompress.Services;
internal sealed partial class ReaderService(FactoryCatalog catalog)
[GenerateInterface(asInternal: true)]
internal sealed partial class ReaderService(IFactoryCatalog catalog) : IReaderService
{
public IReader OpenReader(string filePath, ReaderOptions? options = null)
{

View File

@@ -9,27 +9,27 @@ internal partial class ServiceComposition
private static void Setup() =>
DI.Setup()
.Hint(Hint.Resolve, "Off")
.Arg<FactoryCatalog>("catalog")
.Bind()
.Arg<IFactoryCatalog>("catalog")
.Bind<IReaderService>()
.As(Lifetime.Singleton)
.To<ReaderService>()
.Bind()
.Bind<IArchiveService>()
.As(Lifetime.Singleton)
.To<ArchiveService>()
.Bind()
.Bind<IWriterService>()
.As(Lifetime.Singleton)
.To<WriterService>()
.Bind()
.Bind<IArchiveExtractor>()
.As(Lifetime.Singleton)
.To<ArchiveExtractor>()
.Bind()
.Bind<IArchiveFileWriter>()
.As(Lifetime.Singleton)
.To<ArchiveFileWriter>()
.Root<ArchiveService>("Archives")
.Root<ReaderService>("Readers")
.Root<WriterService>("Writers")
.Root<ArchiveExtractor>("Extractor")
.Root<ArchiveFileWriter>("FileWriter");
.Root<IArchiveService>("Archives")
.Root<IReaderService>("Readers")
.Root<IWriterService>("Writers")
.Root<IArchiveExtractor>("Extractor")
.Root<IArchiveFileWriter>("FileWriter");
}
internal static class LibraryDefaults

View File

@@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.IO;
using SharpCompress.Archives;
using SharpCompress.Common;
using SharpCompress.Detection;
using SharpCompress.Readers;
namespace SharpCompress.Services;
// Declare the contract identities up front so the composition can bind them in the same
// generator pass that fills in their members.
internal partial interface IFactoryCatalog;
internal partial interface IReaderService;
internal partial interface IWriterService;
internal partial interface IArchiveFileWriter;
// Pure.DI cannot see methods emitted by another source generator. Only the sync signatures
// generated by Zomp are declared here; Pure.DI generates the rest from the implementations.
internal partial interface IArchiveService
{
ArchiveDetection? DetectArchive(string filePath);
ArchiveDetection? DetectArchive(string filePath, ReaderOptions? readerOptions);
ArchiveDetection? DetectArchive(Stream stream);
ArchiveDetection? DetectArchive(Stream stream, ReaderOptions? readerOptions);
ArchiveInformation? InspectArchive(string filePath);
ArchiveInformation? InspectArchive(string filePath, ReaderOptions? readerOptions);
ArchiveInformation? InspectArchive(Stream stream);
ArchiveInformation? InspectArchive(Stream stream, ReaderOptions? readerOptions);
ArchiveInformation? InspectArchive(
IReadOnlyList<FileInfo> fileInfos,
ReaderOptions? readerOptions = null
);
ArchiveInformation? InspectArchive(
IReadOnlyList<Stream> streams,
ReaderOptions? readerOptions = null
);
}
internal partial interface IArchiveExtractor
{
void WriteTo(
IArchiveEntry archiveEntry,
Stream streamToWriteTo,
int? bufferSize,
ExtractionOptions? options = null,
IProgress<ProgressReport>? progress = null
);
void WriteToDirectory(
IArchiveEntry entry,
string destinationDirectory,
ExtractionOptions? options = null
);
void WriteToFile(
IArchiveEntry entry,
string destinationFileName,
ExtractionOptions? options = null
);
}

View File

@@ -3,6 +3,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Pure.DI;
using SharpCompress.Common;
using SharpCompress.Common.Options;
using SharpCompress.Factories;
@@ -10,7 +11,8 @@ using SharpCompress.Writers;
namespace SharpCompress.Services;
internal sealed class WriterService(FactoryCatalog catalog)
[GenerateInterface(asInternal: true)]
internal sealed class WriterService(IFactoryCatalog catalog) : IWriterService
{
public IWriter OpenWriter(
string filePath,

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -36,6 +37,12 @@ public class ServiceCompositionTests : TestBase
typeof(WriterService),
typeof(ArchiveExtractor),
typeof(ArchiveFileWriter),
typeof(IFactoryCatalog),
typeof(IArchiveService),
typeof(IReaderService),
typeof(IWriterService),
typeof(IArchiveExtractor),
typeof(IArchiveFileWriter),
];
Assert.All(
serviceTypes,
@@ -52,6 +59,71 @@ public class ServiceCompositionTests : TestBase
Assert.IsType<TarWrapper[]>(TarWrapper.Wrappers);
}
[Fact]
public void ServiceContracts_IncludeGeneratedSyncMethods()
{
(Type Contract, Type Implementation)[] services =
[
(typeof(IFactoryCatalog), typeof(FactoryCatalog)),
(typeof(IArchiveService), typeof(ArchiveService)),
(typeof(IReaderService), typeof(ReaderService)),
(typeof(IWriterService), typeof(WriterService)),
(typeof(IArchiveExtractor), typeof(ArchiveExtractor)),
(typeof(IArchiveFileWriter), typeof(ArchiveFileWriter)),
];
foreach (var (contract, implementation) in services)
{
Assert.True(contract.IsAssignableFrom(implementation));
foreach (
var method in implementation.GetMethods(
BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly
)
)
{
Assert.Contains(
contract.GetMethods(),
candidate => candidate.ToString() == method.ToString()
);
}
}
Assert.Equal(
typeof(IArchiveService),
typeof(ServiceComposition).GetProperty("Archives")!.PropertyType
);
Assert.Equal(
typeof(IReaderService),
typeof(ServiceComposition).GetProperty("Readers")!.PropertyType
);
Assert.Equal(
typeof(IWriterService),
typeof(ServiceComposition).GetProperty("Writers")!.PropertyType
);
Assert.Equal(
typeof(IArchiveExtractor),
typeof(ServiceComposition).GetProperty("Extractor")!.PropertyType
);
Assert.Equal(
typeof(IArchiveFileWriter),
typeof(ServiceComposition).GetProperty("FileWriter")!.PropertyType
);
}
[Fact]
public void Composition_AcceptsInterfaceOnlyCatalog()
{
IFactoryCatalog catalog = new TestFactoryCatalog();
var composition = new ServiceComposition(catalog);
using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.zip"));
Assert.Equal(ArchiveType.Zip, composition.Archives.DetectArchive(stream)?.ContainerType);
Assert.Equal(0, stream.Position);
}
private sealed class TestFactoryCatalog : IFactoryCatalog
{
public IEnumerable<IFactory> Factories { get; } = [new ZipFactory()];
public IEnumerable<TarWrapper> Wrappers => TarWrapper.Wrappers;
}
[Fact]
public async Task Composition_ReusesServiceRootsDuringConcurrentAccess()
{