Skip to content

Commit a5a80ca

Browse files
committed
feat(url-shortener): add Lambda-based URL shortener with QR support
* Add ShortenFunction + RedirectFunction (AWS Lambda emulators) * Generate 6-char slugs with SlugGenerator and store in DynamoDB * Optional QR-code generation via Net.Codecrete.QrCodeGenerator ans SkiaSharp; PNGs saved to S3 * Provision Urls table and qr-bucket through CDK stack outputs * Wire POST /shorten and GET /{slug} routes in API Gateway emulator
1 parent 814e903 commit a5a80ca

File tree

13 files changed

+420
-95
lines changed

13 files changed

+420
-95
lines changed

Directory.Packages.props

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@
4343
<PackageVersion Include="OpenTelemetry.Instrumentation.AWSLambda" Version="1.12.0"/>
4444
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0"/>
4545
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0"/>
46+
<!-- third-party packages -->
47+
<PackageVersion Include="Net.Codecrete.QrCodeGenerator" Version="2.0.7" />
48+
<PackageVersion Include="SkiaSharp" Version="3.119.0" />
49+
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.0" />
4650
<!-- test packages -->
4751
<PackageVersion Include="Aspire.Hosting.Testing" Version="9.4.0"/>
4852
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1"/>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
using System.Diagnostics;
2+
3+
namespace LocalStack.Playground.ServiceDefaults.ActivitySources;
4+
5+
public static class UrlShortenerActivitySource
6+
{
7+
public const string ActivitySourceName = "LocalStack.Lambda.UrlShortener";
8+
public static readonly ActivitySource ActivitySource = new(ActivitySourceName);
9+
}

playground/LocalStack.Playground.ServiceDefaults/LocalStackPlaygroundExtensions.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// ReSharper disable CheckNamespace
33

44
using AWS.Messaging.Telemetry.OpenTelemetry;
5+
using LocalStack.Playground.ServiceDefaults.ActivitySources;
56
using Microsoft.AspNetCore.Builder;
67
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
78
using Microsoft.Extensions.DependencyInjection;
@@ -60,7 +61,8 @@ public static TBuilder ConfigureOpenTelemetry<TBuilder>(this TBuilder builder) w
6061
// Add instrumentation for the AWS .NET SDK.
6162
.AddAWSInstrumentation()
6263
.AddAWSLambdaConfigurations(options => options.DisableAwsXRayContextExtraction = true)
63-
.AddAWSMessagingInstrumentation();
64+
.AddAWSMessagingInstrumentation()
65+
.AddSource(UrlShortenerActivitySource.ActivitySourceName);
6466
});
6567

6668
builder.AddOpenTelemetryExporters();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"version": "2.0",
3+
"routeKey": "POST /shorten",
4+
"rawPath": "/shorten",
5+
"rawQueryString": "",
6+
"headers": {
7+
"content-type": "application/json",
8+
"user-agent": "curl/7.81.0"
9+
},
10+
"requestContext": {
11+
"accountId": "123456789012",
12+
"apiId": "local",
13+
"domainName": "localhost",
14+
"domainPrefix": "",
15+
"http": {
16+
"method": "POST",
17+
"path": "/shorten",
18+
"protocol": "HTTP/1.1",
19+
"sourceIp": "127.0.0.1",
20+
"userAgent": "curl/7.81.0"
21+
},
22+
"requestId": "offline-request",
23+
"routeKey": "POST /shorten",
24+
"stage": "$default",
25+
"time": "01/Aug/2025:12:00:00 +0000",
26+
"timeEpoch": 1690891200000
27+
},
28+
"body": "{ \"Url\": \"https://aws.amazon.com\", \"Format\": \"qr\" }",
29+
"isBase64Encoded": false
30+
}

playground/lambda/LocalStack.Lambda.AppHost/CustomStack.cs

Lines changed: 0 additions & 61 deletions
This file was deleted.

playground/lambda/LocalStack.Lambda.AppHost/Program.cs

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Amazon;
44
using Aspire.Hosting.AWS.Lambda;
55
using Aspire.Hosting.LocalStack.Container;
6+
using AWSCDK.AppHost;
67

78
var builder = DistributedApplication.CreateBuilder(args);
89

@@ -18,27 +19,24 @@
1819
container.LogLevel = LocalStackLogLevel.Debug;
1920
});
2021

21-
var addFunction = builder
22-
.AddAWSLambdaFunction<Projects.LocalStack_Lambda_UrlShortener>(
23-
name: "AddFunction",
24-
lambdaHandler: "LocalStack.Lambda.UrlShortener::LocalStack.Lambda.UrlShortener.Function::FunctionHandler")
22+
var urlShortenerStack = builder
23+
.AddAWSCDKStack("custom", scope => new UrlShortenerStack(scope, "aspire-url-shortener"))
2524
.WithReference(awsConfig);
2625

27-
builder.AddAWSAPIGatewayEmulator("APIGatewayEmulator", APIGatewayType.HttpV2)
28-
// Add the Web API calculator routes
29-
.WithReference(addFunction, Method.Get, "/add/{x}/{y}");
26+
urlShortenerStack.AddOutput("QrBucketName", stack => stack.QrBucket.BucketName);
27+
urlShortenerStack.AddOutput("UrlsTableName", stack => stack.UrlsTable.TableName);
3028

31-
// var customStack = builder
32-
// .AddAWSCDKStack("custom", scope => new CustomStack(scope, "Aspire-custom"))
33-
// .WithReference(awsConfig);
29+
urlShortenerStack.WithTag("aws-repo", "integrations-on-dotnet-aspire-for-aws");
3430

35-
// Add outputs for all the resources to make them available to the frontend
36-
// customStack.AddOutput("BucketName", stack => stack.Bucket.BucketName);
37-
// customStack.AddOutput("ChatTopicArn", stack => stack.ChatTopic.TopicArn);
38-
// customStack.AddOutput("ChatMessagesQueueUrl", stack => stack.ChatMessagesQueue.QueueUrl);
39-
// customStack.AddOutput("ChatMessagesTableName", stack => stack.ChatMessagesTable.TableName);
31+
var urlShortenerLambda = builder
32+
.AddAWSLambdaFunction<Projects.LocalStack_Lambda_UrlShortener>(
33+
name: "UrlShortenerLambda",
34+
lambdaHandler: "LocalStack.Lambda.UrlShortener::LocalStack.Lambda.UrlShortener.Function::FunctionHandler")
35+
.WithReference(urlShortenerStack);
4036

41-
// customStack.WithTag("aws-repo", "integrations-on-dotnet-aspire-for-aws");
37+
builder.AddAWSAPIGatewayEmulator("APIGatewayEmulator", APIGatewayType.HttpV2)
38+
// Add the Web API calculator routes
39+
.WithReference(urlShortenerLambda, Method.Post, "/shorten");
4240

4341
// Autoconfigures the LocalStack for both AWS Cloudformation and CDK resources adds LocalStack reference to all resources that uses AWS references
4442
builder.UseLocalStack(localstack);
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// Originally copied from https://github.com/aws/integrations-on-dotnet-aspire-for-aws
3+
// and adjusted for Aspire.Hosting.LocalStack. All rights reserved.
4+
5+
#pragma warning disable IDE0130
6+
// ReSharper disable CheckNamespace
7+
8+
using Amazon.CDK;
9+
using Amazon.CDK.AWS.DynamoDB;
10+
using Amazon.CDK.AWS.S3;
11+
using Constructs;
12+
using Attribute = Amazon.CDK.AWS.DynamoDB.Attribute;
13+
14+
namespace AWSCDK.AppHost;
15+
16+
internal sealed class UrlShortenerStack : Stack
17+
{
18+
public ITable UrlsTable { get; }
19+
public IBucket QrBucket { get; }
20+
21+
public UrlShortenerStack(Construct scope, string id) : base(scope, id)
22+
{
23+
UrlsTable = new Table(this, "UrlsTable", new TableProps
24+
{
25+
TableName = "Urls",
26+
PartitionKey = new Attribute { Name = "Slug", Type = AttributeType.STRING },
27+
BillingMode = BillingMode.PAY_PER_REQUEST,
28+
});
29+
30+
QrBucket = new Bucket(this, "QrBucket", new BucketProps
31+
{
32+
BucketName = "qr-bucket",
33+
});
34+
}
35+
}

0 commit comments

Comments
 (0)