Skip to content

Commit aba46fb

Browse files
committed
Refactor PdfService and introduce TemplateService
Refactored PdfService to delegate template rendering and time zone handling to the newly introduced TemplateService, simplifying its logic. Added ITemplateService interface and TemplateGenerationRequest/TemplateResponse models to encapsulate template rendering functionality. Introduced TemplateGenerationRequestValidator for input validation and updated Program.cs to register new services and endpoints, including the /api/template endpoint for rendering templates to HTML. Simplified TimeZoneService logic and removed redundant code. Improved maintainability, modularity, and error handling by separating concerns and enhancing code readability. Closes #30
1 parent c22ecba commit aba46fb

File tree

9 files changed

+132
-53
lines changed

9 files changed

+132
-53
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
using OperationResults;
2+
using PdfSmith.Shared.Models;
3+
4+
namespace PdfSmith.BusinessLayer.Services.Interfaces;
5+
6+
public interface ITemplateService
7+
{
8+
Task<Result<TemplateResponse>> CreateAsync(TemplateGenerationRequest request, CancellationToken cancellationToken);
9+
}

src/PdfSmith.BusinessLayer/Services/PdfService.cs

Lines changed: 6 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,24 @@
1-
using System.Globalization;
2-
using System.Net.Mime;
3-
using Microsoft.Extensions.DependencyInjection;
1+
using System.Net.Mime;
42
using OperationResults;
5-
using PdfSmith.BusinessLayer.Exceptions;
6-
using PdfSmith.BusinessLayer.Extensions;
73
using PdfSmith.BusinessLayer.Generators.Interfaces;
84
using PdfSmith.BusinessLayer.Services.Interfaces;
9-
using PdfSmith.BusinessLayer.Templating.Interfaces;
105
using PdfSmith.Shared.Models;
116
using TinyHelpers.Extensions;
127

138
namespace PdfSmith.BusinessLayer.Services;
149

15-
public class PdfService(IServiceProvider serviceProvider, IPdfGenerator pdfGenerator, ITimeZoneService timeZoneService) : IPdfService
10+
public class PdfService(ITemplateService templateService, IPdfGenerator pdfGenerator) : IPdfService
1611
{
1712
public async Task<Result<StreamFileContent>> GeneratePdfAsync(PdfGenerationRequest request, CancellationToken cancellationToken)
1813
{
19-
var templateEngine = serviceProvider.GetKeyedService<ITemplateEngine>(request.TemplateEngine!.ToLowerInvariant().Trim());
14+
var templateResponse = await templateService.CreateAsync(request, cancellationToken);
2015

21-
if (templateEngine is null)
16+
if (!templateResponse.Success)
2217
{
23-
return Result.Fail(FailureReasons.ClientError, "Unable to render the template", $"The template engine '{request.TemplateEngine}' has not been registered");
18+
return Result.Fail(templateResponse.FailureReason, templateResponse.ErrorMessage!, templateResponse.ErrorDetail!, templateResponse.ValidationErrors);
2419
}
2520

26-
var timeZoneInfo = timeZoneService.GetTimeZone();
27-
28-
if (timeZoneInfo is null)
29-
{
30-
var timeZoneId = timeZoneService.GetTimeZoneHeaderValue();
31-
if (timeZoneId is not null)
32-
{
33-
// If timeZoneInfo is null, but timeZoneId has a value, it means that the time zone specified in the header is invalid.
34-
return Result.Fail(FailureReasons.ClientError, "Unable to find the time zone", $"The time zone '{timeZoneId}' is invalid or is not available on the system");
35-
}
36-
}
37-
38-
string? content;
39-
try
40-
{
41-
var model = request.Model?.ToExpandoObject(timeZoneInfo);
42-
43-
cancellationToken.ThrowIfCancellationRequested();
44-
45-
content = await templateEngine.RenderAsync(request.Template, model, CultureInfo.CurrentCulture, cancellationToken);
46-
47-
cancellationToken.ThrowIfCancellationRequested();
48-
}
49-
catch (TemplateEngineException ex)
50-
{
51-
return Result.Fail(FailureReasons.ClientError, "Unable to render the template", ex.Message);
52-
}
53-
54-
var output = await pdfGenerator.CreateAsync(content, request.Options, cancellationToken);
21+
var output = await pdfGenerator.CreateAsync(templateResponse.Content.Result, request.Options, cancellationToken);
5522

5623
cancellationToken.ThrowIfCancellationRequested();
5724

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
using System.Globalization;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using OperationResults;
4+
using PdfSmith.BusinessLayer.Exceptions;
5+
using PdfSmith.BusinessLayer.Extensions;
6+
using PdfSmith.BusinessLayer.Services.Interfaces;
7+
using PdfSmith.BusinessLayer.Templating.Interfaces;
8+
using PdfSmith.Shared.Models;
9+
10+
namespace PdfSmith.BusinessLayer.Services;
11+
12+
public class TemplateService(IServiceProvider serviceProvider, ITimeZoneService timeZoneService) : ITemplateService
13+
{
14+
public async Task<Result<TemplateResponse>> CreateAsync(TemplateGenerationRequest request, CancellationToken cancellationToken)
15+
{
16+
var templateEngine = serviceProvider.GetKeyedService<ITemplateEngine>(request.TemplateEngine!.ToLowerInvariant().Trim());
17+
18+
if (templateEngine is null)
19+
{
20+
return Result.Fail(FailureReasons.ClientError, "Unable to render the template", $"The template engine '{request.TemplateEngine}' has not been registered");
21+
}
22+
23+
var timeZoneInfo = timeZoneService.GetTimeZone();
24+
25+
if (timeZoneInfo is null)
26+
{
27+
var timeZoneId = timeZoneService.GetTimeZoneHeaderValue();
28+
if (timeZoneId is not null)
29+
{
30+
// If timeZoneInfo is null, but timeZoneId has a value, it means that the time zone specified in the header is invalid.
31+
return Result.Fail(FailureReasons.ClientError, "Unable to find the time zone", $"The time zone '{timeZoneId}' is invalid or is not available on the system");
32+
}
33+
}
34+
35+
string? content;
36+
try
37+
{
38+
var model = request.Model?.ToExpandoObject(timeZoneInfo);
39+
40+
cancellationToken.ThrowIfCancellationRequested();
41+
42+
content = await templateEngine.RenderAsync(request.Template, model, CultureInfo.CurrentCulture, cancellationToken);
43+
44+
cancellationToken.ThrowIfCancellationRequested();
45+
}
46+
catch (TemplateEngineException ex)
47+
{
48+
return Result.Fail(FailureReasons.ClientError, "Unable to render the template", ex.Message);
49+
}
50+
51+
return new TemplateResponse(content);
52+
}
53+
}

src/PdfSmith.BusinessLayer/Services/TimeZoneService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public class TimeZoneService(IHttpContextAccessor httpContextAccessor) : ITimeZo
2121

2222
public string? GetTimeZoneHeaderValue()
2323
{
24-
if (httpContextAccessor.HttpContext?.Request?.Headers?.TryGetValue(HeaderKey, out var timeZone) ?? false)
24+
if (httpContextAccessor.HttpContext?.Request?.Headers?.TryGetValue(HeaderKey, out var timeZone) == true)
2525
{
2626
return timeZone.ToString();
2727
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using FluentValidation;
2+
using PdfSmith.Shared.Models;
3+
4+
namespace PdfSmith.BusinessLayer.Validations;
5+
6+
public class TemplateGenerationRequestValidator : AbstractValidator<TemplateGenerationRequest>
7+
{
8+
public TemplateGenerationRequestValidator()
9+
{
10+
RuleFor(r => r.Template).NotEmpty();
11+
RuleFor(r => r.TemplateEngine).NotEmpty().When(r => r.Model is not null);
12+
}
13+
}

src/PdfSmith.Shared/Models/PdfGenerationRequest.cs

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,10 @@ namespace PdfSmith.Shared.Models;
55

66
[method: JsonConstructor]
77
public record class PdfGenerationRequest(string Template, JsonDocument? Model, PdfOptions? Options, string? TemplateEngine = null, string? FileName = null)
8+
: TemplateGenerationRequest(Template, Model, TemplateEngine)
89
{
910
public PdfGenerationRequest(string template, object? model, PdfOptions? options = null, string? templateEngine = null, string? fileName = null)
1011
: this(template, ToJsonDocument(model), options, templateEngine, fileName)
1112
{
1213
}
13-
14-
private static JsonDocument? ToJsonDocument(object? model)
15-
{
16-
if (model is null)
17-
{
18-
return null;
19-
}
20-
21-
var jsonString = JsonSerializer.Serialize(model, JsonSerializerOptions.Default);
22-
return JsonDocument.Parse(jsonString);
23-
}
2414
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Serialization;
3+
4+
namespace PdfSmith.Shared.Models;
5+
6+
[method: JsonConstructor]
7+
public record class TemplateGenerationRequest(string Template, JsonDocument? Model, string? TemplateEngine = null)
8+
{
9+
public TemplateGenerationRequest(string template, object? model, string? templateEngine = null)
10+
: this(template, ToJsonDocument(model), templateEngine)
11+
{
12+
}
13+
14+
protected static JsonDocument? ToJsonDocument(object? model)
15+
{
16+
if (model is null)
17+
{
18+
return null;
19+
}
20+
21+
var jsonString = JsonSerializer.Serialize(model, JsonSerializerOptions.Default);
22+
return JsonDocument.Parse(jsonString);
23+
}
24+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
namespace PdfSmith.Shared.Models;
2+
3+
public record class TemplateResponse(string Result);

src/PdfSmith/Program.cs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
builder.Services.AddKeyedSingleton<ITemplateEngine, RazorTemplateEngine>("razor");
5858
builder.Services.AddKeyedSingleton<ITemplateEngine, HandlebarsTemplateEngine>("handlebars");
5959

60+
builder.Services.AddSingleton<ITemplateService, TemplateService>();
6061
builder.Services.AddSingleton<IPdfGenerator, ChromiumPdfGenerator>();
6162
builder.Services.AddSingleton<IPdfService, PdfService>();
6263

@@ -183,6 +184,25 @@
183184
ResponseWriter = HealthChecksResponseWriter()
184185
});
185186

187+
app.MapPost("/api/template", async (TemplateGenerationRequest request, ITemplateService templateService, HttpContext httpContext) =>
188+
{
189+
var result = await templateService.CreateAsync(request, httpContext.RequestAborted);
190+
191+
var response = httpContext.CreateResponse(result);
192+
return response;
193+
})
194+
.WithName("CreateTemplate")
195+
.WithSummary("Renders a template to HTML using the specified template engine")
196+
.WithDescription("Accepts a template (string) and a model (JSON) and returns the rendered HTML as a string. Supports Razor, Scriban, and Handlebars via the 'templateEngine' property. The template is rendered using the request culture and optional time zone header. Useful to preview or validate templates before generating a PDF.")
197+
.WithValidation<TemplateGenerationRequest>()
198+
.Produces<TemplateResponse>(StatusCodes.Status200OK)
199+
.RequireAuthorization()
200+
.WithRequestTimeout(new RequestTimeoutPolicy
201+
{
202+
Timeout = TimeSpan.FromSeconds(5),
203+
TimeoutStatusCode = StatusCodes.Status408RequestTimeout
204+
});
205+
186206
app.MapPost("/api/pdf", async (PdfGenerationRequest request, IPdfService pdfService, HttpContext httpContext) =>
187207
{
188208
var result = await pdfService.GeneratePdfAsync(request, httpContext.RequestAborted);
@@ -191,8 +211,8 @@
191211
return response;
192212
})
193213
.WithName("GeneratePdf")
194-
.WithSummary("Dynamically generates a PDF document using a provided template and model")
195-
.WithDescription("This endpoint accepts a template (as a string) and a model (as a JSON object) to generate a PDF document on the fly. The template can use the Razor, Scriban, or Handlebars engine, specified via the 'templateEngine' property. The model is injected into the template for dynamic content rendering. Additional PDF options and a custom file name can be provided. The result is a PDF file generated according to the submitted template and data.")
214+
.WithSummary("Renders a template and generates a PDF using the specified template engine")
215+
.WithDescription("Accepts a template (string) and a model (JSON) and returns a generated PDF document. Supports Razor, Scriban, and Handlebars via the 'templateEngine' property. The model is injected into the template for dynamic content rendering. Additional PDF options and a custom file name can be provided. The template is rendered using the request culture and optional time zone header.")
196216
.WithValidation<PdfGenerationRequest>()
197217
.Produces(StatusCodes.Status200OK, contentType: MediaTypeNames.Application.Pdf)
198218
.RequireAuthorization()

0 commit comments

Comments
 (0)