From c071e0f20d8865ffeb989db5ed57bd3f67cc5e73 Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Fri, 12 Dec 2025 09:20:37 -0500 Subject: [PATCH 1/3] Add validation and sync docs with runtime guards --- LayeredCraft.Cdk.Constructs.sln | 92 -------------- LayeredCraft.Cdk.Constructs.slnx | 43 +++++++ README.md | 3 +- docs/constructs/dynamodb-table.md | 119 +++++++++++------- docs/constructs/lambda-function.md | 11 +- docs/constructs/static-site.md | 71 +++++------ docs/examples/index.md | 48 ++++--- docs/index.md | 2 +- docs/testing/index.md | 10 +- .../DynamoDbTableConstruct.cs | 10 +- .../LambdaFunctionConstruct.cs | 16 ++- .../LayeredCraft.Cdk.Constructs.csproj | 2 +- .../LambdaFunctionConstructTests.cs | 11 +- 13 files changed, 215 insertions(+), 223 deletions(-) delete mode 100644 LayeredCraft.Cdk.Constructs.sln create mode 100644 LayeredCraft.Cdk.Constructs.slnx diff --git a/LayeredCraft.Cdk.Constructs.sln b/LayeredCraft.Cdk.Constructs.sln deleted file mode 100644 index de0330a..0000000 --- a/LayeredCraft.Cdk.Constructs.sln +++ /dev/null @@ -1,92 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{5C4245CB-F794-42A5-8226-AF6AA00D0E5E}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{F802A3E0-E887-4EAA-87FA-AB090DCCB788}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{34177AB8-B46F-4340-8D3A-478B24804E73}" - ProjectSection(SolutionItems) = preProject - CLAUDE.md = CLAUDE.md - Directory.Build.props = Directory.Build.props - README.md = README.md - LICENSE = LICENSE - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "git", "git", "{32F31203-71F9-4824-A5E0-D326AF212DD4}" - ProjectSection(SolutionItems) = preProject - .gitignore = .gitignore - .github\dependabot.yml = .github\dependabot.yml - .github\workflows\build.yaml = .github\workflows\build.yaml - .github\workflows\pr-build.yaml = .github\workflows\pr-build.yaml - .github\workflows\docs.yml = .github\workflows\docs.yml - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{B4C8E2F1-3A2D-4B8F-9C7E-1F5A8D9E2B3C}" - ProjectSection(SolutionItems) = preProject - docs\index.md = docs\index.md - mkdocs.yml = mkdocs.yml - requirements.txt = requirements.txt - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "constructs", "constructs", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" - ProjectSection(SolutionItems) = preProject - docs\constructs\lambda-function.md = docs\constructs\lambda-function.md - docs\constructs\static-site.md = docs\constructs\static-site.md - docs\constructs\dynamodb-table.md = docs\constructs\dynamodb-table.md - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "testing", "testing", "{F9E8D7C6-B5A4-9382-7160-5E4D3C2B1A09}" - ProjectSection(SolutionItems) = preProject - docs\testing\index.md = docs\testing\index.md - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{9F8E7D6C-5B4A-3928-1607-E5D4C3B2A190}" - ProjectSection(SolutionItems) = preProject - docs\examples\index.md = docs\examples\index.md - EndProjectSection -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LayeredCraft.Cdk.Constructs", "src\LayeredCraft.Cdk.Constructs\LayeredCraft.Cdk.Constructs.csproj", "{63FCBE95-6714-49D5-A9CD-0BE725BAE259}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LayeredCraft.Cdk.Constructs.Tests", "test\LayeredCraft.Cdk.Constructs.Tests\LayeredCraft.Cdk.Constructs.Tests.csproj", "{7F2D2EA4-D201-4B0E-AE44-6D03B1B7AEBC}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "assets", "assets", "{F7371C5C-5282-4C14-8465-35FABAE8293C}" - ProjectSection(SolutionItems) = preProject - docs\assets\icon.png = docs\assets\icon.png - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "css", "css", "{D7118124-AA5A-45AB-9075-E0F56BAB4F62}" - ProjectSection(SolutionItems) = preProject - docs\assets\css\style.scss = docs\assets\css\style.scss - EndProjectSection -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {63FCBE95-6714-49D5-A9CD-0BE725BAE259} = {5C4245CB-F794-42A5-8226-AF6AA00D0E5E} - {7F2D2EA4-D201-4B0E-AE44-6D03B1B7AEBC} = {F802A3E0-E887-4EAA-87FA-AB090DCCB788} - {A1B2C3D4-E5F6-7890-ABCD-EF1234567890} = {B4C8E2F1-3A2D-4B8F-9C7E-1F5A8D9E2B3C} - {F9E8D7C6-B5A4-9382-7160-5E4D3C2B1A09} = {B4C8E2F1-3A2D-4B8F-9C7E-1F5A8D9E2B3C} - {9F8E7D6C-5B4A-3928-1607-E5D4C3B2A190} = {B4C8E2F1-3A2D-4B8F-9C7E-1F5A8D9E2B3C} - {F7371C5C-5282-4C14-8465-35FABAE8293C} = {B4C8E2F1-3A2D-4B8F-9C7E-1F5A8D9E2B3C} - {D7118124-AA5A-45AB-9075-E0F56BAB4F62} = {F7371C5C-5282-4C14-8465-35FABAE8293C} - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {63FCBE95-6714-49D5-A9CD-0BE725BAE259}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {63FCBE95-6714-49D5-A9CD-0BE725BAE259}.Debug|Any CPU.Build.0 = Debug|Any CPU - {63FCBE95-6714-49D5-A9CD-0BE725BAE259}.Release|Any CPU.ActiveCfg = Release|Any CPU - {63FCBE95-6714-49D5-A9CD-0BE725BAE259}.Release|Any CPU.Build.0 = Release|Any CPU - {7F2D2EA4-D201-4B0E-AE44-6D03B1B7AEBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7F2D2EA4-D201-4B0E-AE44-6D03B1B7AEBC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7F2D2EA4-D201-4B0E-AE44-6D03B1B7AEBC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7F2D2EA4-D201-4B0E-AE44-6D03B1B7AEBC}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal diff --git a/LayeredCraft.Cdk.Constructs.slnx b/LayeredCraft.Cdk.Constructs.slnx new file mode 100644 index 0000000..f0369b4 --- /dev/null +++ b/LayeredCraft.Cdk.Constructs.slnx @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index 9d0616a..3b9bf7a 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,6 @@ public class MyStack : Stack ```csharp var website = new StaticSiteConstruct(this, "Website", new StaticSiteConstructProps { - SiteBucketName = "my-website-bucket", DomainName = "example.com", SiteSubDomain = "www", AssetPath = "./website-build" @@ -171,4 +170,4 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d -This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! \ No newline at end of file +This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! diff --git a/docs/constructs/dynamodb-table.md b/docs/constructs/dynamodb-table.md index b00fc6b..f60c7b3 100644 --- a/docs/constructs/dynamodb-table.md +++ b/docs/constructs/dynamodb-table.md @@ -16,6 +16,7 @@ The `DynamoDbTableConstruct` provides a comprehensive, production-ready DynamoDB ```csharp using Amazon.CDK; +using Amazon.CDK.AWS.DynamoDB; using LayeredCraft.Cdk.Constructs; using LayeredCraft.Cdk.Constructs.Models; @@ -23,12 +24,14 @@ public class MyStack : Stack { public MyStack(Construct scope, string id, IStackProps props = null) : base(scope, id, props) { - var table = new DynamoDbTableConstruct(this, "MyTable", new DynamoDbTableConstructProps - { - TableName = "users", - PartitionKey = new AttributeDefinition { AttributeName = "userId", AttributeType = AttributeType.STRING } - }); - } +var table = new DynamoDbTableConstruct(this, "MyTable", new DynamoDbTableConstructProps +{ + TableName = "users", + PartitionKey = new Attribute { Name = "userId", Type = AttributeType.STRING }, + RemovalPolicy = RemovalPolicy.DESTROY, + BillingMode = BillingMode.PAY_PER_REQUEST +}); +} } ``` @@ -39,17 +42,20 @@ public class MyStack : Stack | Property | Type | Description | |----------|------|-------------| | `TableName` | `string` | Name of the DynamoDB table | -| `PartitionKey` | `AttributeDefinition` | Primary partition key definition | +| `PartitionKey` | `IAttribute` | Primary partition key definition | +| `RemovalPolicy` | `RemovalPolicy` | Behavior when the stack is deleted (e.g., `RemovalPolicy.DESTROY`) | +| `BillingMode` | `BillingMode` | Billing mode for the table (`PAY_PER_REQUEST` or `PROVISIONED`) | ### Optional Properties | Property | Type | Default | Description | |----------|------|---------|-------------| -| `SortKey` | `AttributeDefinition?` | `null` | Primary sort key definition | -| `GlobalSecondaryIndexes` | `GlobalSecondaryIndex[]` | `[]` | GSI definitions | -| `StreamSpecification` | `StreamViewType?` | `null` | DynamoDB stream configuration | +| `SortKey` | `IAttribute?` | `null` | Primary sort key definition | +| `GlobalSecondaryIndexes` | `GlobalSecondaryIndexProps[]` | `[]` | GSI definitions | +| `Stream` | `StreamViewType?` | `null` | DynamoDB stream configuration | | `TimeToLiveAttribute` | `string?` | `null` | TTL attribute name | -| `BillingMode` | `BillingMode` | `PAY_PER_REQUEST` | Table billing mode | + +> PartitionKey is required. The construct validates this at creation time and will throw if it is missing to avoid synth/deploy failures. ## Advanced Examples @@ -59,26 +65,30 @@ public class MyStack : Stack var table = new DynamoDbTableConstruct(this, "MyTable", new DynamoDbTableConstructProps { TableName = "user-sessions", - PartitionKey = new AttributeDefinition { AttributeName = "userId", AttributeType = AttributeType.STRING }, - SortKey = new AttributeDefinition { AttributeName = "sessionId", AttributeType = AttributeType.STRING } + PartitionKey = new Attribute { Name = "userId", Type = AttributeType.STRING }, + SortKey = new Attribute { Name = "sessionId", Type = AttributeType.STRING }, + RemovalPolicy = RemovalPolicy.DESTROY, + BillingMode = BillingMode.PAY_PER_REQUEST }); ``` ### Table with Global Secondary Index ```csharp -var gsi = new GlobalSecondaryIndex +var gsi = new GlobalSecondaryIndexProps { IndexName = "email-index", - PartitionKey = new AttributeDefinition { AttributeName = "email", AttributeType = AttributeType.STRING }, + PartitionKey = new Attribute { Name = "email", Type = AttributeType.STRING }, ProjectionType = ProjectionType.ALL }; var table = new DynamoDbTableConstruct(this, "MyTable", new DynamoDbTableConstructProps { TableName = "users", - PartitionKey = new AttributeDefinition { AttributeName = "userId", AttributeType = AttributeType.STRING }, - GlobalSecondaryIndexes = [gsi] + PartitionKey = new Attribute { Name = "userId", Type = AttributeType.STRING }, + GlobalSecondaryIndexes = [gsi], + RemovalPolicy = RemovalPolicy.DESTROY, + BillingMode = BillingMode.PAY_PER_REQUEST }); ``` @@ -88,8 +98,10 @@ var table = new DynamoDbTableConstruct(this, "MyTable", new DynamoDbTableConstru var table = new DynamoDbTableConstruct(this, "MyTable", new DynamoDbTableConstructProps { TableName = "events", - PartitionKey = new AttributeDefinition { AttributeName = "eventId", AttributeType = AttributeType.STRING }, - StreamSpecification = StreamViewType.NEW_AND_OLD_IMAGES + PartitionKey = new Attribute { Name = "eventId", Type = AttributeType.STRING }, + Stream = StreamViewType.NEW_AND_OLD_IMAGES, + RemovalPolicy = RemovalPolicy.DESTROY, + BillingMode = BillingMode.PAY_PER_REQUEST }); ``` @@ -99,37 +111,41 @@ var table = new DynamoDbTableConstruct(this, "MyTable", new DynamoDbTableConstru var table = new DynamoDbTableConstruct(this, "MyTable", new DynamoDbTableConstructProps { TableName = "sessions", - PartitionKey = new AttributeDefinition { AttributeName = "sessionId", AttributeType = AttributeType.STRING }, - TimeToLiveAttribute = "expiresAt" // Unix timestamp field + PartitionKey = new Attribute { Name = "sessionId", Type = AttributeType.STRING }, + TimeToLiveAttribute = "expiresAt", // Unix timestamp field + RemovalPolicy = RemovalPolicy.DESTROY, + BillingMode = BillingMode.PAY_PER_REQUEST }); ``` ### Complete Configuration ```csharp -var emailGsi = new GlobalSecondaryIndex +var emailGsi = new GlobalSecondaryIndexProps { IndexName = "email-index", - PartitionKey = new AttributeDefinition { AttributeName = "email", AttributeType = AttributeType.STRING }, + PartitionKey = new Attribute { Name = "email", Type = AttributeType.STRING }, ProjectionType = ProjectionType.ALL }; -var statusGsi = new GlobalSecondaryIndex +var statusGsi = new GlobalSecondaryIndexProps { IndexName = "status-index", - PartitionKey = new AttributeDefinition { AttributeName = "status", AttributeType = AttributeType.STRING }, - SortKey = new AttributeDefinition { AttributeName = "createdAt", AttributeType = AttributeType.NUMBER }, + PartitionKey = new Attribute { Name = "status", Type = AttributeType.STRING }, + SortKey = new Attribute { Name = "createdAt", Type = AttributeType.NUMBER }, ProjectionType = ProjectionType.KEYS_ONLY }; var table = new DynamoDbTableConstruct(this, "MyTable", new DynamoDbTableConstructProps { TableName = "users", - PartitionKey = new AttributeDefinition { AttributeName = "userId", AttributeType = AttributeType.STRING }, - SortKey = new AttributeDefinition { AttributeName = "createdAt", AttributeType = AttributeType.NUMBER }, + PartitionKey = new Attribute { Name = "userId", Type = AttributeType.STRING }, + SortKey = new Attribute { Name = "createdAt", Type = AttributeType.NUMBER }, GlobalSecondaryIndexes = [emailGsi, statusGsi], - StreamSpecification = StreamViewType.NEW_AND_OLD_IMAGES, - TimeToLiveAttribute = "expiresAt" + Stream = StreamViewType.NEW_AND_OLD_IMAGES, + TimeToLiveAttribute = "expiresAt", + RemovalPolicy = RemovalPolicy.DESTROY, + BillingMode = BillingMode.PAY_PER_REQUEST }); ``` @@ -141,8 +157,10 @@ The construct provides a convenient method to attach Lambda functions to DynamoD var table = new DynamoDbTableConstruct(this, "MyTable", new DynamoDbTableConstructProps { TableName = "events", - PartitionKey = new AttributeDefinition { AttributeName = "eventId", AttributeType = AttributeType.STRING }, - StreamSpecification = StreamViewType.NEW_AND_OLD_IMAGES + PartitionKey = new Attribute { Name = "eventId", Type = AttributeType.STRING }, + Stream = StreamViewType.NEW_AND_OLD_IMAGES, + RemovalPolicy = RemovalPolicy.DESTROY, + BillingMode = BillingMode.PAY_PER_REQUEST }); // Create a Lambda function to process stream events @@ -167,9 +185,8 @@ The construct automatically creates CloudFormation outputs: var table = new DynamoDbTableConstruct(this, "MyTable", props); // Outputs created: -// - {StackName}-MyTable-table-arn-output -// - {StackName}-MyTable-table-name-output -// - {StackName}-MyTable-table-stream-arn-output (if streams enabled) +// - Export names follow: {stack-name}-{construct-id}-{qualifier} (all lowercase) +// - Qualifiers: arn, name, stream-arn (stream only when enabled) ``` ## Stream View Types @@ -216,11 +233,11 @@ var table = new DynamoDbTableConstruct(this, "MyTable", new DynamoDbTableConstru ### GSI Example with Projection ```csharp -var gsi = new GlobalSecondaryIndex +var gsi = new GlobalSecondaryIndexProps { IndexName = "status-index", - PartitionKey = new AttributeDefinition { AttributeName = "status", AttributeType = AttributeType.STRING }, - SortKey = new AttributeDefinition { AttributeName = "updatedAt", AttributeType = AttributeType.NUMBER }, + PartitionKey = new Attribute { Name = "status", Type = AttributeType.STRING }, + SortKey = new Attribute { Name = "updatedAt", Type = AttributeType.NUMBER }, ProjectionType = ProjectionType.INCLUDE, NonKeyAttributes = ["name", "email"] // Only with INCLUDE projection }; @@ -233,15 +250,17 @@ var gsi = new GlobalSecondaryIndex var userTable = new DynamoDbTableConstruct(this, "UserTable", new DynamoDbTableConstructProps { TableName = "users", - PartitionKey = new AttributeDefinition { AttributeName = "userId", AttributeType = AttributeType.STRING }, + PartitionKey = new Attribute { Name = "userId", Type = AttributeType.STRING }, GlobalSecondaryIndexes = [ - new GlobalSecondaryIndex + new GlobalSecondaryIndexProps { IndexName = "email-index", - PartitionKey = new AttributeDefinition { AttributeName = "email", AttributeType = AttributeType.STRING }, + PartitionKey = new Attribute { Name = "email", Type = AttributeType.STRING }, ProjectionType = ProjectionType.ALL } - ] + ], + RemovalPolicy = RemovalPolicy.DESTROY, + BillingMode = BillingMode.PAY_PER_REQUEST }); ``` @@ -250,8 +269,10 @@ var userTable = new DynamoDbTableConstruct(this, "UserTable", new DynamoDbTableC var sessionTable = new DynamoDbTableConstruct(this, "SessionTable", new DynamoDbTableConstructProps { TableName = "sessions", - PartitionKey = new AttributeDefinition { AttributeName = "sessionId", AttributeType = AttributeType.STRING }, - TimeToLiveAttribute = "expiresAt" + PartitionKey = new Attribute { Name = "sessionId", Type = AttributeType.STRING }, + TimeToLiveAttribute = "expiresAt", + RemovalPolicy = RemovalPolicy.DESTROY, + BillingMode = BillingMode.PAY_PER_REQUEST }); ``` @@ -260,9 +281,11 @@ var sessionTable = new DynamoDbTableConstruct(this, "SessionTable", new DynamoDb var eventTable = new DynamoDbTableConstruct(this, "EventTable", new DynamoDbTableConstructProps { TableName = "events", - PartitionKey = new AttributeDefinition { AttributeName = "aggregateId", AttributeType = AttributeType.STRING }, - SortKey = new AttributeDefinition { AttributeName = "timestamp", AttributeType = AttributeType.NUMBER }, - StreamSpecification = StreamViewType.NEW_AND_OLD_IMAGES + PartitionKey = new Attribute { Name = "aggregateId", Type = AttributeType.STRING }, + SortKey = new Attribute { Name = "timestamp", Type = AttributeType.NUMBER }, + Stream = StreamViewType.NEW_AND_OLD_IMAGES, + RemovalPolicy = RemovalPolicy.DESTROY, + BillingMode = BillingMode.PAY_PER_REQUEST }); ``` @@ -272,4 +295,4 @@ See the [Testing Guide](../testing/index.md) for comprehensive testing utilities ## Examples -For more real-world examples, see the [Examples](../examples/index.md) section. \ No newline at end of file +For more real-world examples, see the [Examples](../examples/index.md) section. diff --git a/docs/constructs/lambda-function.md b/docs/constructs/lambda-function.md index 1a45031..8479258 100644 --- a/docs/constructs/lambda-function.md +++ b/docs/constructs/lambda-function.md @@ -58,9 +58,9 @@ public class MyStack : Stack | `EnvironmentVariables` | `IDictionary` | `{}` | Environment variables | | `IncludeOtelLayer` | `bool` | `false` | Enable OpenTelemetry layer | | `OtelLayerVersion` | `string` | `"0-117-0"` | OpenTelemetry layer version | -| `Architecture` | `string` | `"amd64"` | Lambda architecture (amd64/arm64) | +| `Architecture` | `string` | `"amd64"` | Lambda architecture applied to both the function and OTEL layer (amd64/arm64) | | `Permissions` | `List` | `[]` | Lambda invocation permissions | -| `EnableSnapStart` | `bool` | `false` | Enable SnapStart for improved cold starts | +| `EnableSnapStart` | `bool` | `false` | Enable SnapStart (Java runtimes only; the default `PROVIDED_AL2023` runtime is not eligible) | | `GenerateUrl` | `bool` | `false` | Generate Function URL for HTTP access | ## Advanced Examples @@ -141,7 +141,7 @@ var lambda = new LambdaFunctionConstruct(this, "MyLambda", new LambdaFunctionCon AssetPath = "./lambda-deployment.zip", RoleName = "my-api-role", PolicyName = "my-api-policy", - EnableSnapStart = true // Improves cold start performance + EnableSnapStart = true // SnapStart is only supported for Java runtimes }); ``` @@ -197,9 +197,10 @@ The Lambda functions use the following runtime configuration: !!! info "Runtime Details" - **Runtime**: `PROVIDED_AL2023` (Amazon Linux 2023) - **Handler**: `bootstrap` (for custom runtimes) - - **Architecture**: Configurable (amd64/arm64, default: amd64) + - **Architecture**: Configurable (amd64/arm64, default: amd64) and applied to both function and OTEL layer - **Log Retention**: 2 weeks - **OpenTelemetry Layer**: Configurable AWS managed layer (disabled by default in v2.0+) + - **SnapStart**: Supported only for Java runtimes; enabling it with the default runtime is not allowed ## IAM Permissions @@ -227,4 +228,4 @@ See the [Testing Guide](../testing/index.md) for comprehensive testing utilities ## Examples -For more real-world examples, see the [Examples](../examples/index.md) section. \ No newline at end of file +For more real-world examples, see the [Examples](../examples/index.md) section. diff --git a/docs/constructs/static-site.md b/docs/constructs/static-site.md index 5be705e..e45242e 100644 --- a/docs/constructs/static-site.md +++ b/docs/constructs/static-site.md @@ -4,13 +4,12 @@ The `StaticSiteConstruct` provides complete static website hosting with S3, Clou ## :globe_with_meridians: Features -- **:file_cabinet: S3 Website Hosting**: Optimized S3 bucket configuration for static websites -- **:zap: CloudFront CDN**: Global content delivery with custom error pages -- **:lock: SSL Certificates**: Automatic SSL certificate provisioning and management +- **:file_cabinet: S3 Website Hosting**: Public S3 website bucket optimized for static sites (auto-delete on stack removal) +- **:zap: CloudFront CDN**: Global content delivery with SPA-friendly 403 handling +- **:lock: SSL Certificates**: DNS-validated certificate for the primary site domain and alternates - **:globe_with_meridians: Route53 DNS**: DNS record management for primary and alternate domains -- **:arrows_counterclockwise: API Proxy Support**: Optional CloudFront behavior for `/api/*` paths +- **:arrows_counterclockwise: API Proxy Support**: Optional CloudFront behavior for `/api/*` paths (API domain served directly from origin) - **:package: Asset Deployment**: Automatic deployment with cache invalidation -- **:warning: Custom Error Pages**: 404 and 403 error page handling ## Basic Usage @@ -23,13 +22,13 @@ public class MyStack : Stack { public MyStack(Construct scope, string id, IStackProps props = null) : base(scope, id, props) { - var site = new StaticSiteConstruct(this, "MySite", new StaticSiteConstructProps - { - SiteBucketName = "my-website-bucket", - DomainName = "example.com", - AssetPath = "./website-build" - }); - } +var site = new StaticSiteConstruct(this, "MySite", new StaticSiteConstructProps +{ + DomainName = "example.com", + SiteSubDomain = "www", + AssetPath = "./website-build" +}); +} } ``` @@ -39,17 +38,16 @@ public class MyStack : Stack | Property | Type | Description | |----------|------|-------------| -| `SiteBucketName` | `string` | Name of the S3 bucket for website hosting | -| `DomainName` | `string` | Primary domain name (e.g., "example.com") | -| `AssetPath` | `string` | Path to static website assets | +| `DomainName` | `string` | Root domain name (e.g., "example.com") used for Route53 hosted zone lookup | +| `SiteSubDomain` | `string` | Subdomain to prefix (e.g., `www`). Final site domain is `{SiteSubDomain}.{DomainName}` | +| `AssetPath` | `string` | Path to static website assets that will be uploaded to S3 | ### Optional Properties | Property | Type | Default | Description | |----------|------|---------|-------------| -| `SiteSubDomain` | `string?` | `null` | Subdomain for the site (e.g., "www") | -| `ApiDomain` | `string?` | `null` | API domain for proxy behavior | -| `AlternateDomains` | `string[]` | `[]` | Additional domains to include in certificate | +| `ApiDomain` | `string?` | `null` | Optional API origin for `/api/*` requests (served directly from the origin) | +| `AlternateDomains` | `string[]` | `[]` | Additional domains included in the certificate and DNS records | ## Construct Properties @@ -81,7 +79,6 @@ var domain = site.SiteDomain; // "www.example.com" ```csharp var site = new StaticSiteConstruct(this, "MySite", new StaticSiteConstructProps { - SiteBucketName = "my-website-bucket", DomainName = "example.com", SiteSubDomain = "www", // Creates www.example.com AssetPath = "./website-build" @@ -93,7 +90,6 @@ var site = new StaticSiteConstruct(this, "MySite", new StaticSiteConstructProps ```csharp var site = new StaticSiteConstruct(this, "MySite", new StaticSiteConstructProps { - SiteBucketName = "my-website-bucket", DomainName = "example.com", ApiDomain = "api.example.com", // Proxies /api/* to api.example.com AssetPath = "./website-build" @@ -105,7 +101,6 @@ var site = new StaticSiteConstruct(this, "MySite", new StaticSiteConstructProps ```csharp var site = new StaticSiteConstruct(this, "MySite", new StaticSiteConstructProps { - SiteBucketName = "my-website-bucket", DomainName = "example.com", AlternateDomains = ["www.example.com", "example.org", "www.example.org"], AssetPath = "./website-build" @@ -117,7 +112,6 @@ var site = new StaticSiteConstruct(this, "MySite", new StaticSiteConstructProps ```csharp var site = new StaticSiteConstruct(this, "MySite", new StaticSiteConstructProps { - SiteBucketName = "my-website-bucket", DomainName = "example.com", SiteSubDomain = "www", ApiDomain = "api.example.com", @@ -131,25 +125,24 @@ var site = new StaticSiteConstruct(this, "MySite", new StaticSiteConstructProps The construct automatically resolves domains based on configuration: ### Primary Domain -- If `SiteSubDomain` is provided: `{SiteSubDomain}.{DomainName}` -- If no subdomain: `{DomainName}` +- Site domain is always `{SiteSubDomain}.{DomainName}`. Root/apex domains are not currently supported. ### Certificate Domains The SSL certificate includes: -1. Primary domain (resolved as above) -2. All domains from `AlternateDomains` array -3. API domain (if `ApiDomain` is provided) +1. Primary site domain (resolved as above) +2. All domains from `AlternateDomains` + - `ApiDomain` is **not** added to the certificate; the API origin must serve its own valid certificate. ### Route53 Records A records are created for: -- Primary domain +- Primary site domain - All alternate domains ## CloudFront Configuration ### Default Behavior -- **Origin**: S3 website endpoint -- **Viewer Protocol**: Redirect HTTP to HTTPS +- **Origin**: S3 website endpoint (bucket is public; no OAC/OAI) +- **Viewer Protocol**: Allows HTTP and HTTPS (no automatic redirect) - **Caching**: Optimized for static assets - **Index Document**: `index.html` @@ -160,17 +153,15 @@ A records are created for: - **Caching**: Disabled for API requests ### Error Pages -- **404 Errors**: Redirected to `/index.html` (for SPA routing) - **403 Errors**: Redirected to `/index.html` ## S3 Bucket Configuration The S3 bucket is configured with: -- **Public read access** for website hosting +- **Public read access** for website hosting (no OAC/OAI) - **Website hosting** enabled - **Index document**: `index.html` - **Error document**: `index.html` (for SPA routing) -- **CORS configuration** for cross-origin requests ## Asset Deployment @@ -181,10 +172,9 @@ The construct automatically: ## Security Considerations -- **HTTPS Enforcement**: All traffic is redirected to HTTPS -- **Public Access**: S3 bucket allows public read access (required for website hosting) -- **CORS**: Configured to allow necessary cross-origin requests -- **Certificate Management**: SSL certificates are automatically provisioned and renewed +- **HTTPS Enforcement**: CloudFront allows HTTP; add a `ViewerProtocolPolicy.REDIRECT_TO_HTTPS` behavior if strict HTTPS is required +- **Public Access**: S3 bucket is publicly readable because the origin is the website endpoint +- **Certificate Management**: SSL certificates are automatically provisioned for the site domain(s); API domains must present their own valid certificate ## Regional Considerations @@ -198,8 +188,8 @@ The construct automatically: ```csharp var spa = new StaticSiteConstruct(this, "SPA", new StaticSiteConstructProps { - SiteBucketName = "my-spa-bucket", DomainName = "myapp.com", + SiteSubDomain = "www", AssetPath = "./build" // React/Vue/Angular build output }); ``` @@ -208,7 +198,6 @@ var spa = new StaticSiteConstruct(this, "SPA", new StaticSiteConstructProps ```csharp var blog = new StaticSiteConstruct(this, "Blog", new StaticSiteConstructProps { - SiteBucketName = "my-blog-bucket", DomainName = "myblog.com", SiteSubDomain = "www", ApiDomain = "api.myblog.com", // For comments, search, etc. @@ -220,8 +209,8 @@ var blog = new StaticSiteConstruct(this, "Blog", new StaticSiteConstructProps ```csharp var docs = new StaticSiteConstruct(this, "Docs", new StaticSiteConstructProps { - SiteBucketName = "my-docs-bucket", DomainName = "docs.mycompany.com", + SiteSubDomain = "www", AssetPath = "./docs-build" // Documentation generator output }); ``` @@ -232,4 +221,4 @@ See the [Testing Guide](../testing/index.md) for comprehensive testing utilities ## Examples -For more real-world examples, see the [Examples](../examples/index.md) section. \ No newline at end of file +For more real-world examples, see the [Examples](../examples/index.md) section. diff --git a/docs/examples/index.md b/docs/examples/index.md index ffd9012..4ed3633 100644 --- a/docs/examples/index.md +++ b/docs/examples/index.md @@ -16,6 +16,7 @@ A complete serverless API with Lambda, DynamoDB, and API Gateway. ```csharp using Amazon.CDK; +using Amazon.CDK.AWS.DynamoDB; using Amazon.CDK.AWS.IAM; using LayeredCraft.Cdk.Constructs; using LayeredCraft.Cdk.Constructs.Models; @@ -28,15 +29,17 @@ public class ServerlessApiStack : Stack var userTable = new DynamoDbTableConstruct(this, "UserTable", new DynamoDbTableConstructProps { TableName = "users", - PartitionKey = new AttributeDefinition { AttributeName = "userId", AttributeType = AttributeType.STRING }, + PartitionKey = new Attribute { Name = "userId", Type = AttributeType.STRING }, GlobalSecondaryIndexes = [ - new GlobalSecondaryIndex + new GlobalSecondaryIndexProps { IndexName = "email-index", - PartitionKey = new AttributeDefinition { AttributeName = "email", AttributeType = AttributeType.STRING }, + PartitionKey = new Attribute { Name = "email", Type = AttributeType.STRING }, ProjectionType = ProjectionType.ALL } - ] + ], + RemovalPolicy = RemovalPolicy.DESTROY, + BillingMode = BillingMode.PAY_PER_REQUEST }); // Create Lambda function for API @@ -92,7 +95,6 @@ public class WebsiteStack : Stack // Create static website with API proxy var website = new StaticSiteConstruct(this, "Website", new StaticSiteConstructProps { - SiteBucketName = "my-website-bucket", DomainName = "mywebsite.com", SiteSubDomain = "www", ApiDomain = apiLambda.LiveAliasFunctionUrlDomain!, // Proxy /api/* to Lambda @@ -115,16 +117,20 @@ public class EventDrivenStack : Stack var eventTable = new DynamoDbTableConstruct(this, "EventTable", new DynamoDbTableConstructProps { TableName = "events", - PartitionKey = new AttributeDefinition { AttributeName = "aggregateId", AttributeType = AttributeType.STRING }, - SortKey = new AttributeDefinition { AttributeName = "timestamp", AttributeType = AttributeType.NUMBER }, - StreamSpecification = StreamViewType.NEW_AND_OLD_IMAGES + PartitionKey = new Attribute { Name = "aggregateId", Type = AttributeType.STRING }, + SortKey = new Attribute { Name = "timestamp", Type = AttributeType.NUMBER }, + Stream = StreamViewType.NEW_AND_OLD_IMAGES, + RemovalPolicy = RemovalPolicy.DESTROY, + BillingMode = BillingMode.PAY_PER_REQUEST }); // Read model table var readModelTable = new DynamoDbTableConstruct(this, "ReadModelTable", new DynamoDbTableConstructProps { TableName = "user-projections", - PartitionKey = new AttributeDefinition { AttributeName = "userId", AttributeType = AttributeType.STRING } + PartitionKey = new Attribute { Name = "userId", Type = AttributeType.STRING }, + RemovalPolicy = RemovalPolicy.DESTROY, + BillingMode = BillingMode.PAY_PER_REQUEST }); // Event processor Lambda @@ -253,28 +259,30 @@ var website = new StaticSiteConstruct(this, "Website", new StaticSiteConstructPr var table = new DynamoDbTableConstruct(this, "ComplexTable", new DynamoDbTableConstructProps { TableName = "user-activities", - PartitionKey = new AttributeDefinition { AttributeName = "userId", AttributeType = AttributeType.STRING }, - SortKey = new AttributeDefinition { AttributeName = "timestamp", AttributeType = AttributeType.NUMBER }, + PartitionKey = new Attribute { Name = "userId", Type = AttributeType.STRING }, + SortKey = new Attribute { Name = "timestamp", Type = AttributeType.NUMBER }, GlobalSecondaryIndexes = [ // Query by activity type - new GlobalSecondaryIndex + new GlobalSecondaryIndexProps { IndexName = "activity-type-index", - PartitionKey = new AttributeDefinition { AttributeName = "activityType", AttributeType = AttributeType.STRING }, - SortKey = new AttributeDefinition { AttributeName = "timestamp", AttributeType = AttributeType.NUMBER }, + PartitionKey = new Attribute { Name = "activityType", Type = AttributeType.STRING }, + SortKey = new Attribute { Name = "timestamp", Type = AttributeType.NUMBER }, ProjectionType = ProjectionType.ALL }, // Query by status - new GlobalSecondaryIndex + new GlobalSecondaryIndexProps { IndexName = "status-index", - PartitionKey = new AttributeDefinition { AttributeName = "status", AttributeType = AttributeType.STRING }, - SortKey = new AttributeDefinition { AttributeName = "timestamp", AttributeType = AttributeType.NUMBER }, + PartitionKey = new Attribute { Name = "status", Type = AttributeType.STRING }, + SortKey = new Attribute { Name = "timestamp", Type = AttributeType.NUMBER }, ProjectionType = ProjectionType.KEYS_ONLY } ], - StreamSpecification = StreamViewType.NEW_AND_OLD_IMAGES, - TimeToLiveAttribute = "expiresAt" + Stream = StreamViewType.NEW_AND_OLD_IMAGES, + TimeToLiveAttribute = "expiresAt", + RemovalPolicy = RemovalPolicy.DESTROY, + BillingMode = BillingMode.PAY_PER_REQUEST }); ``` @@ -582,4 +590,4 @@ public class Program } ``` -For more examples, see the test files in the [repository](https://github.com/LayeredCraft/cdk-constructs/tree/main/test) which demonstrate comprehensive usage patterns. \ No newline at end of file +For more examples, see the test files in the [repository](https://github.com/LayeredCraft/cdk-constructs/tree/main/test) which demonstrate comprehensive usage patterns. diff --git a/docs/index.md b/docs/index.md index c146755..c89699d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -50,8 +50,8 @@ public class MyStack : Stack // Create a static website var site = new StaticSiteConstruct(this, "MySite", new StaticSiteConstructProps { - SiteBucketName = "my-website-bucket", DomainName = "example.com", + SiteSubDomain = "www", AssetPath = "./website-build" }); } diff --git a/docs/testing/index.md b/docs/testing/index.md index 6558929..f2663fe 100644 --- a/docs/testing/index.md +++ b/docs/testing/index.md @@ -111,9 +111,10 @@ var props = CdkTestHelper.CreatePropsBuilder(AssetPathExtensions.GetTestLambdaZi .WithOtelEnabled(true) .WithOtelLayerVersion("0-117-0") .WithArchitecture("arm64") - .WithSnapStart(true) .WithGenerateUrl(true) .Build(); + +// SnapStart is only supported for Java runtimes; avoid enabling it with the default runtime. ``` ### OpenTelemetry Configuration (v2.0+) @@ -143,7 +144,6 @@ template.ShouldHaveTimeout(30); // Advanced features template.ShouldHaveOtelLayer(); -template.ShouldHaveSnapStart(); template.ShouldHaveFunctionUrl(); template.ShouldHaveFunctionUrlOutput("test-stack", "test-construct"); @@ -160,6 +160,8 @@ template.ShouldHaveVersionAndAlias("live"); template.ShouldHaveLogGroup("my-function-prod", 14); ``` +SnapStart-specific assertions (`template.ShouldHaveSnapStart()`) apply only when you are using a Java runtime; the default `PROVIDED_AL2023` runtime does not support SnapStart. + ## Static Site Testing ### Props Builder @@ -343,7 +345,7 @@ template.ShouldNotHaveOtelLayer(); // When disabled ### 5. Use Meaningful Test Names ```csharp [Fact] -public void Should_Enable_SnapStart_When_Configured() +public void Should_Enable_Tracing_When_Configured() { // Clear what the test is verifying } @@ -368,4 +370,4 @@ Test stacks are created with us-east-1 environment by default. Override if neede For complete testing examples, see the test files in the repository: - `LambdaFunctionConstructTests.cs` - `StaticSiteConstructTests.cs` -- `DynamoDbTableConstructTests.cs` \ No newline at end of file +- `DynamoDbTableConstructTests.cs` diff --git a/src/LayeredCraft.Cdk.Constructs/DynamoDbTableConstruct.cs b/src/LayeredCraft.Cdk.Constructs/DynamoDbTableConstruct.cs index 3a78027..8d64dbc 100644 --- a/src/LayeredCraft.Cdk.Constructs/DynamoDbTableConstruct.cs +++ b/src/LayeredCraft.Cdk.Constructs/DynamoDbTableConstruct.cs @@ -41,6 +41,9 @@ public class DynamoDbTableConstruct : Construct /// The configuration properties for the DynamoDB table public DynamoDbTableConstruct(Construct scope, string id, IDynamoDbTableConstructProps props) : base(scope, id) { + if (props.PartitionKey is null) + throw new ArgumentException("PartitionKey is required to create a DynamoDB table", nameof(props.PartitionKey)); + var tableProps = new TableProps { TableName = props.TableName, @@ -61,9 +64,10 @@ public DynamoDbTableConstruct(Construct scope, string id, IDynamoDbTableConstruc Table = new Table(this, id, tableProps); var stack = Stack.Of(this); - for (var i = 0; i < props.GlobalSecondaryIndexes.Length; i++) + var globalSecondaryIndexes = props.GlobalSecondaryIndexes ?? []; + for (var i = 0; i < globalSecondaryIndexes.Length; i++) { - var index = props.GlobalSecondaryIndexes[i]; + var index = globalSecondaryIndexes[i]; Table.AddGlobalSecondaryIndex(index); _ = new CfnOutput(this, $"{id}-gsi-{i}", new CfnOutputProps { @@ -115,4 +119,4 @@ public void AttachStreamLambda(Function lambda) BatchSize = 1 }); } -} \ No newline at end of file +} diff --git a/src/LayeredCraft.Cdk.Constructs/LambdaFunctionConstruct.cs b/src/LayeredCraft.Cdk.Constructs/LambdaFunctionConstruct.cs index b30e906..74c2432 100644 --- a/src/LayeredCraft.Cdk.Constructs/LambdaFunctionConstruct.cs +++ b/src/LayeredCraft.Cdk.Constructs/LambdaFunctionConstruct.cs @@ -1,3 +1,4 @@ +using System; using Amazon.CDK; using Amazon.CDK.AWS.IAM; using Amazon.CDK.AWS.Lambda; @@ -81,6 +82,7 @@ public LambdaFunctionConstruct(Construct scope, string id, ILambdaFunctionConstr Environment = props.EnvironmentVariables, LogGroup = logGroup, Tracing = props.IncludeOtelLayer ? Tracing.ACTIVE : Tracing.DISABLED, + Architecture = ResolveArchitecture(props.Architecture), CurrentVersionOptions = new VersionOptions { RemovalPolicy = RemovalPolicy.RETAIN @@ -97,6 +99,11 @@ public LambdaFunctionConstruct(Construct scope, string id, ILambdaFunctionConstr if (props.EnableSnapStart) { + if (LambdaFunction.Runtime.Family != RuntimeFamily.JAVA) + { + throw new NotSupportedException("SnapStart is only supported for Java runtimes. Set EnableSnapStart = false or use a Java runtime."); + } + var cfnFunction = (CfnFunction)LambdaFunction.Node.DefaultChild!; cfnFunction.AddPropertyOverride("SnapStart", new Dictionary { @@ -156,4 +163,11 @@ private void AddPermissionsToAllTargets(string baseId, IFunction function, IVers alias?.AddPermission($"{baseId}-alias-{index}", permission); } } -} \ No newline at end of file + + private static Architecture ResolveArchitecture(string architecture) + { + return string.Equals(architecture, "arm64", StringComparison.OrdinalIgnoreCase) + ? Architecture.ARM_64 + : Architecture.X86_64; + } +} diff --git a/src/LayeredCraft.Cdk.Constructs/LayeredCraft.Cdk.Constructs.csproj b/src/LayeredCraft.Cdk.Constructs/LayeredCraft.Cdk.Constructs.csproj index dba8328..16ca068 100644 --- a/src/LayeredCraft.Cdk.Constructs/LayeredCraft.Cdk.Constructs.csproj +++ b/src/LayeredCraft.Cdk.Constructs/LayeredCraft.Cdk.Constructs.csproj @@ -15,7 +15,7 @@ - + diff --git a/test/LayeredCraft.Cdk.Constructs.Tests/LambdaFunctionConstructTests.cs b/test/LayeredCraft.Cdk.Constructs.Tests/LambdaFunctionConstructTests.cs index e5e5be2..6f01636 100644 --- a/test/LayeredCraft.Cdk.Constructs.Tests/LambdaFunctionConstructTests.cs +++ b/test/LayeredCraft.Cdk.Constructs.Tests/LambdaFunctionConstructTests.cs @@ -1,3 +1,4 @@ +using System; using Amazon.CDK; using Amazon.CDK.Assertions; using AwesomeAssertions; @@ -304,11 +305,11 @@ public void Construct_ShouldEnableSnapStartWhenConfigured(LambdaFunctionConstruc Env = new Amazon.CDK.Environment { Account = "123456789012", Region = "us-east-1" } }); - _ = new LambdaFunctionConstruct(stack, "test-construct", props); - var template = Template.FromStack(stack); + Action act = () => _ = new LambdaFunctionConstruct(stack, "test-construct", props); - // Verify that SnapStart is enabled for published versions - template.ShouldHaveSnapStart(); + // SnapStart is only supported on Java runtimes; default runtime should reject it + act.Should().Throw() + .WithMessage("*SnapStart is only supported for Java runtimes*"); } [Fact] @@ -516,4 +517,4 @@ public void PropsBuilder_ShouldHaveCorrectDefaults() props.TimeoutInSeconds.Should().Be(6); props.GenerateUrl.Should().BeFalse(); } -} \ No newline at end of file +} From 07ebfafdd1196a094f6f450da10966bea3b19e32 Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Fri, 12 Dec 2025 09:33:31 -0500 Subject: [PATCH 2/3] Fix CI workflow configs --- .github/workflows/build.yaml | 5 ++++- .github/workflows/pr-build.yaml | 7 +++++-- .../LayeredCraft.Cdk.Constructs.csproj | 2 +- .../LayeredCraft.Cdk.Constructs.Tests.csproj | 2 +- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 24e97bd..462c896 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -16,8 +16,11 @@ on: permissions: write-all jobs: build: - uses: LayeredCraft/devops-templates/.github/workflows/package-build.yaml@v5.0 + uses: LayeredCraft/devops-templates/.github/workflows/package-build.yaml@v6.2 with: + hasTests: true + useMtpRunner: true + testDirectory: "test" dotnet-version: | 8.0.x 9.0.x diff --git a/.github/workflows/pr-build.yaml b/.github/workflows/pr-build.yaml index 683c92d..450f861 100644 --- a/.github/workflows/pr-build.yaml +++ b/.github/workflows/pr-build.yaml @@ -7,12 +7,15 @@ on: permissions: write-all jobs: build: - uses: LayeredCraft/devops-templates/.github/workflows/pr-build.yaml@v5.0 + uses: LayeredCraft/devops-templates/.github/workflows/pr-build.yaml@v6.2 with: - solution: LayeredCraft.Cdk.Constructs.sln + solution: LayeredCraft.Cdk.Constructs.slnx hasTests: true + useMtpRunner: true + testDirectory: "test" dotnetVersion: | 8.0.x 9.0.x + 10.0.x runCdk: false secrets: inherit \ No newline at end of file diff --git a/src/LayeredCraft.Cdk.Constructs/LayeredCraft.Cdk.Constructs.csproj b/src/LayeredCraft.Cdk.Constructs/LayeredCraft.Cdk.Constructs.csproj index 16ca068..a7692fc 100644 --- a/src/LayeredCraft.Cdk.Constructs/LayeredCraft.Cdk.Constructs.csproj +++ b/src/LayeredCraft.Cdk.Constructs/LayeredCraft.Cdk.Constructs.csproj @@ -3,7 +3,7 @@ enable enable - net8.0;net9.0 + net8.0;net9.0;net10.0 default LayeredCraft.Cdk.Constructs LayeredCraft.Cdk.Constructs diff --git a/test/LayeredCraft.Cdk.Constructs.Tests/LayeredCraft.Cdk.Constructs.Tests.csproj b/test/LayeredCraft.Cdk.Constructs.Tests/LayeredCraft.Cdk.Constructs.Tests.csproj index 798a716..b9e6733 100644 --- a/test/LayeredCraft.Cdk.Constructs.Tests/LayeredCraft.Cdk.Constructs.Tests.csproj +++ b/test/LayeredCraft.Cdk.Constructs.Tests/LayeredCraft.Cdk.Constructs.Tests.csproj @@ -5,7 +5,7 @@ enable Exe LayeredCraft.Cdk.Constructs.Tests - net8.0;net9.0 + net8.0;net9.0;net10.0 default false MIT