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
60 changes: 18 additions & 42 deletions lessons/202/cs-app-aot/AmazonS3Uploader.cs
Original file line number Diff line number Diff line change
@@ -1,54 +1,30 @@
using Amazon;
using Amazon.Runtime;
using Amazon.S3;
using Amazon.S3.Model;
using Amazon.Runtime;

internal sealed class AmazonS3Uploader : IDisposable
namespace cs_app_aot;

public sealed class AmazonS3Uploader
{
private readonly AmazonS3Client client;
private readonly AmazonS3Client _client;

public AmazonS3Uploader(string? accessKey, string? secretKey, string? endpoint)
public AmazonS3Uploader(string? accessKey, string? secretKey, string? endpoint, string? region)
{
var credentials = new BasicAWSCredentials(accessKey, secretKey);
var clientConfig = new AmazonS3Config()
var creds = new BasicAWSCredentials(accessKey, secretKey);
var cfg = new AmazonS3Config
{
ServiceURL = endpoint,
ForcePathStyle = true,
ForcePathStyle = true
};

client = new AmazonS3Client(credentials, clientConfig);
}
if (!string.IsNullOrWhiteSpace(endpoint))
cfg.ServiceURL = endpoint;
if (!string.IsNullOrWhiteSpace(region))
cfg.RegionEndpoint = RegionEndpoint.GetBySystemName(region);

public void Dispose()
{
client.Dispose();
_client = new AmazonS3Client(creds, cfg);
}

public async Task Upload(string? bucket, string key, string? path)
{
try
{
PutObjectRequest putRequest = new PutObjectRequest
{
BucketName = bucket,
Key = key,
FilePath = path
};

PutObjectResponse response = await client.PutObjectAsync(putRequest);
}
catch (AmazonS3Exception amazonS3Exception)
{
if (amazonS3Exception.ErrorCode != null &&
(amazonS3Exception.ErrorCode.Equals("InvalidAccessKeyId")
||
amazonS3Exception.ErrorCode.Equals("InvalidSecurity")))
{
throw new Exception("Check the provided AWS Credentials.");
}
else
{
throw new Exception("Error occurred: " + amazonS3Exception.Message);
}
}
}
}
public Task Upload(string? bucket, string key, string? path, CancellationToken ct = default)
=> _client.PutObjectAsync(new PutObjectRequest { BucketName = bucket, Key = key, FilePath = path }, ct);
}
11 changes: 0 additions & 11 deletions lessons/202/cs-app-aot/DbOptions.cs

This file was deleted.

13 changes: 0 additions & 13 deletions lessons/202/cs-app-aot/Device.cs

This file was deleted.

28 changes: 13 additions & 15 deletions lessons/202/cs-app-aot/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-jammy AS build
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:9.0-noble AS build
ARG TARGETARCH
WORKDIR /source

# copy csproj and restore as distinct layers
COPY *.csproj .
WORKDIR /src
COPY *.csproj ./
RUN dotnet restore -a $TARGETARCH

# copy everything else and build app
COPY . .
RUN dotnet publish -a $TARGETARCH --no-restore -o /app /p:AOT=true /p:UseAppHost=false
RUN rm /app/*.dbg /app/*.Development.json

# ensure assembly name (adjust if your project file name differs)
RUN dotnet publish -c Release -a $TARGETARCH -r linux-x64 -o /app \
/p:PublishAot=true /p:StripSymbols=true /p:IlcGenerateStackTraceData=false \
/p:AssemblyName=cs-app-aot

# final stage/image
FROM mcr.microsoft.com/dotnet/aspnet:8.0-jammy-chiseled
EXPOSE 8080
FROM mcr.microsoft.com/dotnet/runtime-deps:9.0-noble-chiseled
ENV ASPNETCORE_URLS=http://+:8080 \
DOTNET_EnableDiagnostics=0
WORKDIR /app
COPY --from=build /app .
ENTRYPOINT ["./cs-app"]
COPY --from=build /app ./
EXPOSE 8080
ENTRYPOINT ["./cs-app-aot"]
24 changes: 0 additions & 24 deletions lessons/202/cs-app-aot/Dockerfile.alpine

This file was deleted.

17 changes: 0 additions & 17 deletions lessons/202/cs-app-aot/Image.cs

This file was deleted.

188 changes: 105 additions & 83 deletions lessons/202/cs-app-aot/Program.cs
Original file line number Diff line number Diff line change
@@ -1,109 +1,131 @@
using Prometheus;
using System.Diagnostics;
using Npgsql;
using cs_app;
using System.Text.Json.Serialization;
using cs_app_aot;
using Npgsql;
using NpgsqlTypes;
using Prometheus;
using Metrics = Prometheus.Metrics;

// Initialize the Web App
var builder = WebApplication.CreateSlimBuilder(args);

// Configure JSON source generatino for AOT support
builder.Services.ConfigureHttpJsonOptions(options =>
// hard-off logging for benchmarks (no sinks)
builder.Logging.ClearProviders();
builder.Logging.SetMinimumLevel(LogLevel.None);

// JSON source-gen for AOT; harmless on JIT
builder.Services.ConfigureHttpJsonOptions(o =>
{
options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
o.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
});

// Load configuration
var dbOptions = new DbOptions();
builder.Configuration.GetSection(DbOptions.PATH).Bind(dbOptions);

var s3Options = new S3Options();
builder.Configuration.GetSection(S3Options.PATH).Bind(s3Options);

// Establish S3 session.
using var amazonS3 = new AmazonS3Uploader(s3Options.User, s3Options.Secret, s3Options.Endpoint);

// Create Postgre connection string
var connString = $"Host={dbOptions.Host};Username={dbOptions.User};Password={dbOptions.Password};Database={dbOptions.Database}";
// config
var cfg = new AppConfig(builder.Configuration);
builder.Services.AddSingleton(cfg);

Console.WriteLine(connString);
// AWS S3
builder.Services.AddSingleton(new AmazonS3Uploader(cfg.User, cfg.Secret, cfg.S3Endpoint, cfg.Region));

// Establish Postgres connection
await using var dataSource = new NpgsqlSlimDataSourceBuilder(connString).Build();

// Counter variable is used to increment image id
var counter = 0;
// Npgsql (AOT/trimming friendly)
var csb = new NpgsqlConnectionStringBuilder
{
Host = cfg.DbHost,
Username = cfg.DbUser,
Password = cfg.DbPassword,
Database = cfg.DbDatabase,
Pooling = true,
MaxPoolSize = 256,
MinPoolSize = 16,
NoResetOnClose = true,
AutoPrepareMinUsages = 2,
MaxAutoPrepare = 32,
Multiplexing = true
};
builder.Services.AddSingleton(_ => new NpgsqlSlimDataSourceBuilder(csb.ConnectionString).Build());

var app = builder.Build();

// Create Summary Prometheus metric to measure latency of the requests.
var summary = Metrics.CreateSummary("myapp_request_duration_seconds", "Duration of the request.", new SummaryConfiguration
{
LabelNames = ["op"],
Objectives = [new QuantileEpsilonPair(0.9, 0.01), new QuantileEpsilonPair(0.99, 0.001)]
});
// Prometheus Summary (prebind labels to skip tiny allocs)
var summary = Metrics.CreateSummary("myapp_request_duration_seconds", "Duration of the request.",
new SummaryConfiguration
{
LabelNames = ["op"],
Objectives = [new QuantileEpsilonPair(0.9, 0.01), new QuantileEpsilonPair(0.99, 0.001)]
});
var s3Dur = summary.WithLabels("s3");
var dbDur = summary.WithLabels("db");

// Enable the /metrics page to export Prometheus metrics.
// endpoints
app.MapMetrics();
app.MapGet("/healthz", () => Results.Ok());
app.MapGet("/api/devices", () => Results.Ok(StaticData.Devices));

// Create endpoint that returns the status of the application.
// Placeholder for the health check
app.MapGet("/healthz", () => Results.Ok("OK"));
app.MapGet("/api/images",
async (HttpContext http,
AmazonS3Uploader s3,
NpgsqlDataSource dataSource) =>
{
var id = Interlocked.Increment(ref StaticData.Counter) - 1;
var image = new Image($"cs-thumbnail-{id}.png");

// S3
var t0 = Stopwatch.GetTimestamp();
await s3.Upload(cfg.S3Bucket, image.ObjKey, cfg.S3ImgPath, http.RequestAborted);
s3Dur.Observe(Stopwatch.GetElapsedTime(t0).TotalSeconds);

// DB
var t1 = Stopwatch.GetTimestamp();
await using (var cmd = dataSource.CreateCommand(StaticData.ImageInsertSql))
{
cmd.Parameters.Add(new NpgsqlParameter<Guid> { NpgsqlDbType = NpgsqlDbType.Uuid, Value = image.ImageUuid });
cmd.Parameters.Add(new NpgsqlParameter<string> { NpgsqlDbType = NpgsqlDbType.Text, Value = image.ObjKey });
cmd.Parameters.Add(new NpgsqlParameter<DateTime>
{ NpgsqlDbType = NpgsqlDbType.TimestampTz, Value = image.CreatedAt });
await cmd.ExecuteNonQueryAsync(http.RequestAborted);
}

dbDur.Observe(Stopwatch.GetElapsedTime(t1).TotalSeconds);
return Results.Ok();
});

// Create endpoint that returns a list of connected devices.
app.MapGet("/api/devices", () =>
{
Device[] devices = [
new("b0e42fe7-31a5-4894-a441-007e5256afea", "5F-33-CC-1F-43-82", "2.1.6"),
new("0c3242f5-ae1f-4e0c-a31b-5ec93825b3e7", "EF-2B-C4-F5-D6-34", "2.1.5"),
new("b16d0b53-14f1-4c11-8e29-b9fcef167c26", "62-46-13-B7-B3-A1", "3.0.0"),
new("51bb1937-e005-4327-a3bd-9f32dcf00db8", "96-A8-DE-5B-77-14", "1.0.1"),
new("e0a1d085-dce5-48db-a794-35640113fa67", "7E-3B-62-A6-09-12", "3.5.6")
];

return Results.Ok(devices);
});
app.Run();

// Create endpoint that uoloades image to S3 and writes metadate to Postgres
app.MapGet("/api/images", async () =>
{
// Generate a new image.
var image = new Image($"cs-thumbnail-{counter}.png");
// ---- app types ----
[JsonSerializable(typeof(Device[]))]
internal partial class AppJsonSerializerContext : JsonSerializerContext;

// Get the current time to record the duration of the S3 request.
var s3StartTime = Stopwatch.GetTimestamp();
public sealed class AppConfig(IConfiguration config)
{
public string? DbDatabase = config.GetValue<string>("Db:database");
public string? DbHost = config.GetValue<string>("Db:host");
public string? DbPassword = config.GetValue<string>("Db:password");
public string? DbUser = config.GetValue<string>("Db:user");

// Upload the image to S3.
await amazonS3.Upload(s3Options.Bucket, image.ObjKey, s3Options.ImgPath);
public string? Region = config.GetValue<string>("S3:region");
public string? S3Bucket = config.GetValue<string>("S3:bucket");
public string? S3Endpoint = config.GetValue<string>("S3:endpoint");
public string? S3ImgPath = config.GetValue<string>("S3:imgPath");
public string? Secret = config.GetValue<string>("S3:secret");
public string? User = config.GetValue<string>("S3:user");
}

// Record the duration of the request to S3.
summary.WithLabels(["s3"]).Observe(Stopwatch.GetElapsedTime(s3StartTime).TotalSeconds);

// Get the current time to record the duration of the Database request.
var dbStartTime = Stopwatch.GetTimestamp();
public sealed class Device
{
public required string Uuid { get; init; }
public required string Mac { get; init; }
public required string Firmware { get; init; }
}

// Prepare the database query to insert a record.
const string sqlQuery = "INSERT INTO cs_image VALUES ($1, $2, $3)";
public readonly struct Image
{
public string ObjKey { get; }
public Guid ImageUuid { get; }
public DateTime CreatedAt { get; }

// Execute the query to create a new image record.
await using (var cmd = dataSource.CreateCommand(sqlQuery))
public Image(string key)
{
cmd.Parameters.AddWithValue(image.ImageUuid);
cmd.Parameters.AddWithValue(image.ObjKey);
cmd.Parameters.AddWithValue(image.CreatedAt);
await cmd.ExecuteNonQueryAsync();
ObjKey = key;
ImageUuid = Guid.NewGuid();
CreatedAt = DateTime.UtcNow;
}

// Record the duration of the insert query.
summary.WithLabels(["db"]).Observe(Stopwatch.GetElapsedTime(dbStartTime).TotalSeconds);

// Increment the counter.
counter++;

return Results.Ok("Saved!");
});

app.Run();

[JsonSerializable(typeof(Device[]))]
internal partial class AppJsonSerializerContext : JsonSerializerContext;
}
Loading