CardMaker: Stateless Business Card Generation
The Challenge
A PR agency needed to generate hundreds of business cards for employees, with requirements:
- Import employee data from Excel
- Generate PowerPoint slides with QR codes
- Preserve custom PowerPoint template formatting
- No database - security and privacy concerns
- Fast turnaround (seconds, not minutes)
Solution: Stateless Architecture
[Route("/generate")]
public async Task<IActionResult> GenerateCards(IFormFile excelFile, IFormFile templateFile)
{
// 1. Parse Excel in-memory
var employees = await _importService.ParseExcelAsync(excelFile.OpenReadStream());
// 2. Generate PowerPoint in-memory
var pptBytes = await _cardGenerator.GenerateAsync(
employees,
templateFile.OpenReadStream()
);
// 3. Return file and discard everything
return File(pptBytes, "application/vnd.openxmlformats-officedocument.presentationml.presentation");
}
No session state, no file storage, no database writes. Just pure transformation.
Technical Deep Dive
1. Excel Import with OpenXML
Reading structured data from Excel templates:
public async Task<List<EmployeeData>> ParseExcelAsync(Stream excelStream)
{
using var document = SpreadsheetDocument.Open(excelStream, false);
var workbookPart = document.WorkbookPart;
var sheet = workbookPart.WorkshookPart.Sheets.GetFirstChild<Sheet>();
// Find header row (flexible positioning)
var headerRow = FindHeaderRow(sheetData, expectedHeaders);
// Parse data rows
var employees = new List<EmployeeData>();
foreach (var row in sheetData.Elements<Row>().Skip(headerRow + 1))
{
employees.Add(ParseRow(row, headerMapping));
}
return employees;
}
2. QR Code Generation
vCard format embedded in QR codes:
public byte[] GenerateVCardQR(EmployeeData employee)
{
var vCard = $@"BEGIN:VCARD
VERSION:3.0
FN:{employee.Name}
ORG:{employee.Company}
TITLE:{employee.Position}
TEL:{employee.Phone}
EMAIL:{employee.Email}
END:VCARD";
using var qrGenerator = new QRCodeGenerator();
var qrCodeData = qrGenerator.CreateQrCode(vCard, QRCodeGenerator.ECCLevel.Q);
var qrCode = new PngByteQRCode(qrCodeData);
return qrCode.GetGraphic(20);
}
3. PowerPoint Slide Generation
Merging data into PowerPoint templates while preserving formatting:
public async Task<byte[]> GenerateCardsAsync(List<EmployeeData> employees, Stream templateStream)
{
using var memoryStream = new MemoryStream();
templateStream.CopyTo(memoryStream);
using var presentation = PresentationDocument.Open(memoryStream, true);
var slideMasterPart = presentation.PresentationPart.SlideMasterParts.First();
var slideTemplate = slideMasterPart.Slide;
foreach (var employee in employees)
{
// Clone template slide
var newSlide = CloneSlide(slideTemplate);
// Replace placeholders
ReplacePlaceholder(newSlide, "{name}", employee.Name);
ReplacePlaceholder(newSlide, "{position}", employee.Position);
ReplacePlaceholder(newSlide, "{email}", employee.Email);
// Insert QR code image
var qrBytes = GenerateVCardQR(employee);
InsertImage(newSlide, "{qrcode}", qrBytes);
presentation.PresentationPart.Presentation.AppendChild(newSlide);
}
presentation.Save();
return memoryStream.ToArray();
}
Security & Performance
Security-First Design
// Rate limiting
builder.Services.AddRateLimiter(options =>
{
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
RateLimitPartition.GetFixedWindowLimiter("global", _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 10,
Window = TimeSpan.FromMinutes(1)
}));
});
// Content Security Policy
app.Use(async (context, next) =>
{
context.Response.Headers.Add("Content-Security-Policy",
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'");
await next();
});
Health Checks
builder.Services.AddHealthChecks()
.AddCheck("memory", () =>
{
var allocated = GC.GetTotalMemory(false);
return allocated < 500_000_000
? HealthCheckResult.Healthy()
: HealthCheckResult.Degraded();
});
app.MapHealthChecks("/health");
Deployment
Docker Compose for simple deployment:
version: '3.8'
services:
cardmaker:
image: hyunjojung/cardmaker:latest
ports:
- "5049:5049"
environment:
- ASPNETCORE_URLS=http://+:5049
restart: unless-stopped
Cloudflare Tunnel for HTTPS:
cloudflared tunnel create cardmaker
cloudflared tunnel route dns cardmaker cardmaker.hyunjo.uk
Results
Before:
- Manual PowerPoint editing: 2-3 hours for 50 cards
- Error-prone (typos, formatting inconsistencies)
- No QR codes
After:
- Automated generation: <10 seconds for 50 cards
- Zero errors (validated at import)
- vCard QR codes for all cards
Live Demo
Try it yourself: cardmaker.hyunjo.uk
Source code: GitHub
Key Takeaways
- Stateless architectures eliminate entire classes of security and scaling issues
- OpenXML SDK provides complete control over Office file formats
- Blazor Server excels at form-heavy, server-rendered apps
- Docker + Cloudflare Tunnel = simple, secure deployment
Building something similar? Feel free to reach out on GitHub!
