Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,8 @@ desktop.ini
.env.local
appsettings.*.local.json
secrets.json

# Scratch files used by the local build/test loop (never committed)
_*.log
_*.txt
*.flag
75 changes: 75 additions & 0 deletions NeoReports.sln
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NeoReports.Destinations.Loc
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NeoReports.Sources.Sql.IntegrationTests", "tests\NeoReports.Sources.Sql.IntegrationTests\NeoReports.Sources.Sql.IntegrationTests.csproj", "{EBCE7C5A-C4D5-45B2-962B-0DE16BE1C8EE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NeoReports.Formats.Xlsx", "src\Formats\NeoReports.Formats.Xlsx\NeoReports.Formats.Xlsx.csproj", "{F5E41535-F67C-444D-98CC-C1A1D9DD0ABB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NeoReports.Destinations.S3", "src\Destinations\NeoReports.Destinations.S3\NeoReports.Destinations.S3.csproj", "{66FF3152-38BF-4830-9534-3F77642E651B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NeoReports.Formats.Xlsx.UnitTests", "tests\NeoReports.Formats.Xlsx.UnitTests\NeoReports.Formats.Xlsx.UnitTests.csproj", "{A96127D0-8777-4B00-AB8A-B59BAC0B3342}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NeoReports.Destinations.S3.UnitTests", "tests\NeoReports.Destinations.S3.UnitTests\NeoReports.Destinations.S3.UnitTests.csproj", "{5A33C69B-40A0-49AF-87F4-40B75D0D53A5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "02-sql-to-xlsx-s3", "samples\02-sql-to-xlsx-s3\02-sql-to-xlsx-s3.csproj", "{34079216-845C-417E-8511-FDD3F075CDCD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -161,6 +171,66 @@ Global
{EBCE7C5A-C4D5-45B2-962B-0DE16BE1C8EE}.Release|x64.Build.0 = Release|Any CPU
{EBCE7C5A-C4D5-45B2-962B-0DE16BE1C8EE}.Release|x86.ActiveCfg = Release|Any CPU
{EBCE7C5A-C4D5-45B2-962B-0DE16BE1C8EE}.Release|x86.Build.0 = Release|Any CPU
{F5E41535-F67C-444D-98CC-C1A1D9DD0ABB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F5E41535-F67C-444D-98CC-C1A1D9DD0ABB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F5E41535-F67C-444D-98CC-C1A1D9DD0ABB}.Debug|x64.ActiveCfg = Debug|Any CPU
{F5E41535-F67C-444D-98CC-C1A1D9DD0ABB}.Debug|x64.Build.0 = Debug|Any CPU
{F5E41535-F67C-444D-98CC-C1A1D9DD0ABB}.Debug|x86.ActiveCfg = Debug|Any CPU
{F5E41535-F67C-444D-98CC-C1A1D9DD0ABB}.Debug|x86.Build.0 = Debug|Any CPU
{F5E41535-F67C-444D-98CC-C1A1D9DD0ABB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F5E41535-F67C-444D-98CC-C1A1D9DD0ABB}.Release|Any CPU.Build.0 = Release|Any CPU
{F5E41535-F67C-444D-98CC-C1A1D9DD0ABB}.Release|x64.ActiveCfg = Release|Any CPU
{F5E41535-F67C-444D-98CC-C1A1D9DD0ABB}.Release|x64.Build.0 = Release|Any CPU
{F5E41535-F67C-444D-98CC-C1A1D9DD0ABB}.Release|x86.ActiveCfg = Release|Any CPU
{F5E41535-F67C-444D-98CC-C1A1D9DD0ABB}.Release|x86.Build.0 = Release|Any CPU
{66FF3152-38BF-4830-9534-3F77642E651B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{66FF3152-38BF-4830-9534-3F77642E651B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{66FF3152-38BF-4830-9534-3F77642E651B}.Debug|x64.ActiveCfg = Debug|Any CPU
{66FF3152-38BF-4830-9534-3F77642E651B}.Debug|x64.Build.0 = Debug|Any CPU
{66FF3152-38BF-4830-9534-3F77642E651B}.Debug|x86.ActiveCfg = Debug|Any CPU
{66FF3152-38BF-4830-9534-3F77642E651B}.Debug|x86.Build.0 = Debug|Any CPU
{66FF3152-38BF-4830-9534-3F77642E651B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{66FF3152-38BF-4830-9534-3F77642E651B}.Release|Any CPU.Build.0 = Release|Any CPU
{66FF3152-38BF-4830-9534-3F77642E651B}.Release|x64.ActiveCfg = Release|Any CPU
{66FF3152-38BF-4830-9534-3F77642E651B}.Release|x64.Build.0 = Release|Any CPU
{66FF3152-38BF-4830-9534-3F77642E651B}.Release|x86.ActiveCfg = Release|Any CPU
{66FF3152-38BF-4830-9534-3F77642E651B}.Release|x86.Build.0 = Release|Any CPU
{A96127D0-8777-4B00-AB8A-B59BAC0B3342}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A96127D0-8777-4B00-AB8A-B59BAC0B3342}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A96127D0-8777-4B00-AB8A-B59BAC0B3342}.Debug|x64.ActiveCfg = Debug|Any CPU
{A96127D0-8777-4B00-AB8A-B59BAC0B3342}.Debug|x64.Build.0 = Debug|Any CPU
{A96127D0-8777-4B00-AB8A-B59BAC0B3342}.Debug|x86.ActiveCfg = Debug|Any CPU
{A96127D0-8777-4B00-AB8A-B59BAC0B3342}.Debug|x86.Build.0 = Debug|Any CPU
{A96127D0-8777-4B00-AB8A-B59BAC0B3342}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A96127D0-8777-4B00-AB8A-B59BAC0B3342}.Release|Any CPU.Build.0 = Release|Any CPU
{A96127D0-8777-4B00-AB8A-B59BAC0B3342}.Release|x64.ActiveCfg = Release|Any CPU
{A96127D0-8777-4B00-AB8A-B59BAC0B3342}.Release|x64.Build.0 = Release|Any CPU
{A96127D0-8777-4B00-AB8A-B59BAC0B3342}.Release|x86.ActiveCfg = Release|Any CPU
{A96127D0-8777-4B00-AB8A-B59BAC0B3342}.Release|x86.Build.0 = Release|Any CPU
{5A33C69B-40A0-49AF-87F4-40B75D0D53A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5A33C69B-40A0-49AF-87F4-40B75D0D53A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5A33C69B-40A0-49AF-87F4-40B75D0D53A5}.Debug|x64.ActiveCfg = Debug|Any CPU
{5A33C69B-40A0-49AF-87F4-40B75D0D53A5}.Debug|x64.Build.0 = Debug|Any CPU
{5A33C69B-40A0-49AF-87F4-40B75D0D53A5}.Debug|x86.ActiveCfg = Debug|Any CPU
{5A33C69B-40A0-49AF-87F4-40B75D0D53A5}.Debug|x86.Build.0 = Debug|Any CPU
{5A33C69B-40A0-49AF-87F4-40B75D0D53A5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5A33C69B-40A0-49AF-87F4-40B75D0D53A5}.Release|Any CPU.Build.0 = Release|Any CPU
{5A33C69B-40A0-49AF-87F4-40B75D0D53A5}.Release|x64.ActiveCfg = Release|Any CPU
{5A33C69B-40A0-49AF-87F4-40B75D0D53A5}.Release|x64.Build.0 = Release|Any CPU
{5A33C69B-40A0-49AF-87F4-40B75D0D53A5}.Release|x86.ActiveCfg = Release|Any CPU
{5A33C69B-40A0-49AF-87F4-40B75D0D53A5}.Release|x86.Build.0 = Release|Any CPU
{34079216-845C-417E-8511-FDD3F075CDCD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{34079216-845C-417E-8511-FDD3F075CDCD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{34079216-845C-417E-8511-FDD3F075CDCD}.Debug|x64.ActiveCfg = Debug|Any CPU
{34079216-845C-417E-8511-FDD3F075CDCD}.Debug|x64.Build.0 = Debug|Any CPU
{34079216-845C-417E-8511-FDD3F075CDCD}.Debug|x86.ActiveCfg = Debug|Any CPU
{34079216-845C-417E-8511-FDD3F075CDCD}.Debug|x86.Build.0 = Debug|Any CPU
{34079216-845C-417E-8511-FDD3F075CDCD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{34079216-845C-417E-8511-FDD3F075CDCD}.Release|Any CPU.Build.0 = Release|Any CPU
{34079216-845C-417E-8511-FDD3F075CDCD}.Release|x64.ActiveCfg = Release|Any CPU
{34079216-845C-417E-8511-FDD3F075CDCD}.Release|x64.Build.0 = Release|Any CPU
{34079216-845C-417E-8511-FDD3F075CDCD}.Release|x86.ActiveCfg = Release|Any CPU
{34079216-845C-417E-8511-FDD3F075CDCD}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -176,5 +246,10 @@ Global
{2D69478A-68E9-4FBB-AEDE-40866829609F} = {22222222-2222-2222-2222-222222222222}
{D50FA946-1BA6-4793-8224-2FB629EF0AB5} = {22222222-2222-2222-2222-222222222222}
{EBCE7C5A-C4D5-45B2-962B-0DE16BE1C8EE} = {22222222-2222-2222-2222-222222222222}
{F5E41535-F67C-444D-98CC-C1A1D9DD0ABB} = {11111111-1111-1111-1111-111111111111}
{66FF3152-38BF-4830-9534-3F77642E651B} = {11111111-1111-1111-1111-111111111111}
{A96127D0-8777-4B00-AB8A-B59BAC0B3342} = {22222222-2222-2222-2222-222222222222}
{5A33C69B-40A0-49AF-87F4-40B75D0D53A5} = {22222222-2222-2222-2222-222222222222}
{34079216-845C-417E-8511-FDD3F075CDCD} = {44444444-4444-4444-4444-444444444444}
EndGlobalSection
EndGlobal
10 changes: 5 additions & 5 deletions plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ PRs pequenos e independentes, em ordem. Cada um fecha com testes verdes e fecha
- **Depende de:** PR 2.

## PR 4 — Formats.Xlsx + Destinations.S3
- [ ] `Format.Xlsx(...)` com ClosedXML (aba, auto-filtro, tipos nativos).
- [ ] Multi-output numa passada (CSV + XLSX lendo a source uma vez).
- [ ] `Destination.S3(bucket, keyTemplate)` — upload tudo-ou-nada (sem objeto parcial em falha).
- [ ] Sample `02-sql-to-xlsx-s3`.
- **Aceite:** CA-5, CA-6, CA-8.
- [x] `Format.Xlsx(...)` com ClosedXML (aba, auto-filtro, tipos nativos; formato/data por coluna). Memória cresce com as linhas — ver D14.
- [x] Multi-output numa passada (CSV + XLSX lendo a source uma vez) — provado no E2E `Csv_and_xlsx_are_generated_reading_the_source_once`.
- [x] `Destination.S3(bucket, keyTemplate)` — upload tudo-ou-nada via `PutObject` (sem objeto parcial em falha) — ver D15.
- [x] Sample `02-sql-to-xlsx-s3`.
- **Aceite:** CA-5, CA-6, CA-8. ✅ Total acumulado de testes verdes: 34 (13 Core + 4 CSV + 6 Local + 4 Xlsx + 3 S3 + 4 SQL/E2E).
- **Depende de:** PR 3.

## PR 5 — Memória constante (validação)
Expand Down
26 changes: 26 additions & 0 deletions samples/02-sql-to-xlsx-s3/02-sql-to-xlsx-s3.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\NeoReports.Core\NeoReports.Core.csproj" />
<ProjectReference Include="..\..\src\Sources\NeoReports.Sources.Sql\NeoReports.Sources.Sql.csproj" />
<ProjectReference Include="..\..\src\Formats\NeoReports.Formats.Csv\NeoReports.Formats.Csv.csproj" />
<ProjectReference Include="..\..\src\Formats\NeoReports.Formats.Xlsx\NeoReports.Formats.Xlsx.csproj" />
<ProjectReference Include="..\..\src\Destinations\NeoReports.Destinations.S3\NeoReports.Destinations.S3.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
</ItemGroup>

</Project>
56 changes: 56 additions & 0 deletions samples/02-sql-to-xlsx-s3/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System.Text;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NeoReports.Core.DependencyInjection;
using NeoReports.Core.Pipeline;
using NeoReports.Destinations.S3;
using NeoReports.Sources.Sql;
using static NeoReports.Core.Building.ReportColumns;
// Import the format entry methods directly so Csv(...) and Xlsx(...) read cleanly and avoid the
// Format class-name collision between the two format packages (ADR D16).
using static NeoReports.Formats.Csv.Format;
using static NeoReports.Formats.Xlsx.Format;

// Sample 02 — SQL Server -> CSV + XLSX (single pass) -> Amazon S3.
//
// Reads the source once and writes BOTH formats, then uploads each to S3.
// Requires AWS credentials/region in the environment and an existing bucket:
// dotnet run --project samples/02-sql-to-xlsx-s3 -- "<connection-string>" "<bucket>"

var connectionString = args.Length > 0
? args[0]
: "Server=localhost;Database=Sales;Trusted_Connection=True;TrustServerCertificate=True";
var bucket = args.Length > 1 ? args[1] : "my-reports-bucket";

var services = new ServiceCollection();
services.AddLogging(b => b.AddConsole().SetMinimumLevel(LogLevel.Information));

services.AddReport<Venda>("vendas-mensal", b => b
.From(Source.Sql(
connectionString,
"SELECT Id, Cliente, Valor, Data FROM Vendas " +
"WHERE (@cursor IS NULL OR Id > @cursor) ORDER BY Id")
.Keyset<Venda, long>(v => v.Id, pageSize: 1000))
.Filter(v => v.Valor > 0)
.Columns(
Col<Venda, long>(v => v.Id, "ID Venda"),
Col<Venda, string>(v => v.Cliente, "Cliente"),
Col<Venda, decimal>(v => v.Valor, "Valor", format: "C2", culture: "pt-BR"),
Col<Venda, DateTime>(v => v.Data, "Data Venda", format: "yyyy-MM-dd"))
.To(Csv(o => o.Delimiter(';').Encoding(Encoding.UTF8)))
.To(Xlsx(o => o.SheetName("Vendas").AutoFilter()))
.UploadTo(Destination.S3(bucket, "reports/{name}/{date:yyyy-MM-dd}.{ext}")));

var provider = services.BuildServiceProvider();
var runner = provider.GetRequiredService<IReportRunner>();

var result = await runner.RunAsync("vendas-mensal");

Console.WriteLine($"Status: {result.Status}");
Console.WriteLine($"Records read/written: {result.Stats.RecordsRead}/{result.Stats.RecordsWritten}");
foreach (var upload in result.Uploads)
Console.WriteLine($"Uploaded: {upload.Url} (success={upload.Success})");

return result.Status == ReportRunStatus.Failed ? 1 : 0;

internal sealed record Venda(long Id, string Cliente, decimal Valor, DateTime Data);
14 changes: 14 additions & 0 deletions src/Destinations/NeoReports.Destinations.S3/Destination.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using NeoReports.Core.Building;

namespace NeoReports.Destinations.S3;

/// <summary>Fluent entry point for the Amazon S3 destination.</summary>
public static class Destination
{
/// <summary>Configures an Amazon S3 destination.</summary>
/// <param name="bucket">Target bucket name.</param>
/// <param name="keyTemplate">Key template (e.g. <c>reports/{name}/{date:yyyy-MM-dd}.{ext}</c>).</param>
/// <returns>A <see cref="DestinationSpec"/> to pass to <c>ReportBuilder.UploadTo(...)</c>.</returns>
public static DestinationSpec S3(string bucket, string keyTemplate) =>
new(new S3DestinationFactory(bucket, keyTemplate));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<Description>Amazon S3 destination for NeoReports.</Description>
<PackageTags>reports;reporting;destination;s3;aws</PackageTags>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\NeoReports.Abstractions\NeoReports.Abstractions.csproj" />
<ProjectReference Include="..\..\NeoReports.Core\NeoReports.Core.csproj" />
<ProjectReference Include="..\..\Destinations\NeoReports.Destinations.Local\NeoReports.Destinations.Local.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="AWSSDK.S3" />
</ItemGroup>

</Project>
101 changes: 101 additions & 0 deletions src/Destinations/NeoReports.Destinations.S3/S3Destination.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
using Amazon.S3;
using Amazon.S3.Model;
using NeoReports.Abstractions;
using NeoReports.Destinations.Local;

namespace NeoReports.Destinations.S3;

/// <summary>
/// Uploads the finished report file to an Amazon S3 bucket at a key resolved from a template.
/// The upload is all-or-nothing: <c>PutObject</c> creates the object only on a fully successful
/// transfer, so a failure never leaves a partial object (S3 PUT is atomic per object).
/// </summary>
public sealed class S3Destination : IReportDestination
{
private readonly IAmazonS3 _client;
private readonly bool _ownsClient;
private readonly string _bucket;
private readonly string _keyTemplate;

/// <summary>Creates the destination with an explicit S3 client (the caller owns its lifetime).</summary>
/// <param name="client">The S3 client.</param>
/// <param name="bucket">Target bucket name.</param>
/// <param name="keyTemplate">Key template (e.g. <c>reports/{name}/{date:yyyy-MM-dd}.{ext}</c>).</param>
public S3Destination(IAmazonS3 client, string bucket, string keyTemplate)
: this(client, ownsClient: false, bucket, keyTemplate)
{
}

private S3Destination(IAmazonS3 client, bool ownsClient, string bucket, string keyTemplate)
{
_client = client ?? throw new ArgumentNullException(nameof(client));
_ownsClient = ownsClient;
if (string.IsNullOrWhiteSpace(bucket))
throw new ArgumentException("Bucket must be provided.", nameof(bucket));
if (string.IsNullOrWhiteSpace(keyTemplate))
throw new ArgumentException("Key template must be provided.", nameof(keyTemplate));
_bucket = bucket;
_keyTemplate = keyTemplate;
}

/// <summary>
/// Creates a destination that builds its own <see cref="AmazonS3Client"/> from ambient AWS
/// credentials/region (resolved by the SDK). The client is disposed with the destination.
/// </summary>
/// <param name="bucket">Target bucket name.</param>
/// <param name="keyTemplate">Key template.</param>
public static S3Destination WithDefaultClient(string bucket, string keyTemplate) =>
new(new AmazonS3Client(), ownsClient: true, bucket, keyTemplate);

/// <inheritdoc />
public string Type => "s3";

/// <inheritdoc />
public async Task<UploadResult> UploadAsync(ReportFile file, DestinationContext context, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(file);
ArgumentNullException.ThrowIfNull(context);

var extension = Path.GetExtension(file.FileName).TrimStart('.');
var key = PathTemplate.Expand(
_keyTemplate,
context.Execution.ReportName,
extension,
context.Execution.StartedAt,
context.Execution.Parameters);

try
{
await using var content = file.OpenRead();
var request = new PutObjectRequest
{
BucketName = _bucket,
Key = key,
InputStream = content,
ContentType = file.MimeType,
AutoCloseStream = false,
// Let the SDK compute the length from the seekable stream; disable chunked
// signing so a transient failure aborts the whole PUT (no partial object).
UseChunkEncoding = false,
};

var response = await _client.PutObjectAsync(request, cancellationToken).ConfigureAwait(false);
if ((int)response.HttpStatusCode is < 200 or >= 300)
return UploadResult.Fail($"S3 PutObject returned HTTP {(int)response.HttpStatusCode} for s3://{_bucket}/{key}.");

var url = $"s3://{_bucket}/{key}";
return UploadResult.Ok(url, key);
}
catch (Exception ex)
{
return UploadResult.Fail($"S3 upload to s3://{_bucket}/{key} failed: {ex.Message}");
}
}

/// <summary>Disposes the S3 client if this destination created it.</summary>
public void Dispose()
{
if (_ownsClient)
_client.Dispose();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Amazon.S3;
using NeoReports.Abstractions;

namespace NeoReports.Destinations.S3;

/// <summary>
/// Creates <see cref="S3Destination"/> instances. Resolves an <see cref="IAmazonS3"/> from the
/// service provider when available; otherwise builds a default client from ambient AWS config.
/// </summary>
public sealed class S3DestinationFactory : IDestinationFactory
{
private readonly string _bucket;
private readonly string _keyTemplate;

/// <summary>Creates the factory.</summary>
/// <param name="bucket">Target bucket name.</param>
/// <param name="keyTemplate">Key template applied to every created destination.</param>
public S3DestinationFactory(string bucket, string keyTemplate)
{
_bucket = bucket;
_keyTemplate = keyTemplate;
}

/// <inheritdoc />
public string Type => "s3";

/// <inheritdoc />
public IReportDestination Create(IReadOnlyDictionary<string, object?> options, IServiceProvider services)
{
var client = services?.GetService(typeof(IAmazonS3)) as IAmazonS3;
return client is not null
? new S3Destination(client, _bucket, _keyTemplate)
: S3Destination.WithDefaultClient(_bucket, _keyTemplate);
}
}
Loading
Loading