├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── HelloWorld.sln
├── README.md
├── serverless.yml
├── src
└── HelloWorld
│ ├── HelloWorld.csproj
│ ├── Program.cs
│ ├── Startup.cs
│ ├── ValuesController.cs
│ ├── ValuesService.cs
│ ├── appsettings.json
│ └── aws-lambda-tools-defaults.json
└── test
└── HelloWorld.Tests
├── HelloWorld.Tests.csproj
└── TestValuesController.cs
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI/CD
2 |
3 | on:
4 | pull_request: {}
5 | push:
6 | branches: [master]
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: checkout
13 | uses: actions/checkout@v2.0.0
14 |
15 | - name: setup .net core
16 | uses: actions/setup-dotnet@v1.4.0
17 | with:
18 | dotnet-version: 3.1.100
19 |
20 | - name: install lambda tools
21 | run: dotnet tool install -g Amazon.Lambda.Tools
22 |
23 | - run: dotnet test
24 |
25 | - name: build
26 | run: dotnet lambda package --msbuild-parameters "/p:PublishReadyToRun=true --self-contained false"
27 | working-directory: src/HelloWorld
28 |
29 | - name: aws login
30 | if: github.ref == 'refs/heads/master'
31 | uses: aws-actions/configure-aws-credentials@v1
32 | with:
33 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
34 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
35 | aws-region: us-east-1
36 |
37 | - name: Deploy
38 | if: github.ref == 'refs/heads/master'
39 | uses: docker://glassechidna/stackit
40 | with:
41 | args: stackit up --stack-name hello-world-app --template serverless.yml
42 |
43 | resharper:
44 | runs-on: ubuntu-latest
45 | steps:
46 | - name: checkout
47 | uses: actions/checkout@v2
48 |
49 | - name: resharper
50 | uses: glassechidna/resharper-action@master
51 | with:
52 | solution: HelloWorld.sln
53 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | **/bin
2 | **/obj
3 |
4 |
5 |
--------------------------------------------------------------------------------
/HelloWorld.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.26124.0
5 | MinimumVisualStudioVersion = 15.0.26124.0
6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{9A16544E-F5A7-4566-B239-1CC3DC3DD96E}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HelloWorld", "src\HelloWorld\HelloWorld.csproj", "{5BD04563-46D9-4D5B-BCE5-667789E2BBEA}"
9 | EndProject
10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{114EAE3F-4A6E-4446-B4DE-73660768E258}"
11 | EndProject
12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HelloWorld.Tests", "test\HelloWorld.Tests\HelloWorld.Tests.csproj", "{8251D2E8-2A62-4D65-89AD-F738437B91C0}"
13 | EndProject
14 | Global
15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
16 | Debug|Any CPU = Debug|Any CPU
17 | Debug|x64 = Debug|x64
18 | Debug|x86 = Debug|x86
19 | Release|Any CPU = Release|Any CPU
20 | Release|x64 = Release|x64
21 | Release|x86 = Release|x86
22 | EndGlobalSection
23 | GlobalSection(SolutionProperties) = preSolution
24 | HideSolutionNode = FALSE
25 | EndGlobalSection
26 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
27 | {5BD04563-46D9-4D5B-BCE5-667789E2BBEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
28 | {5BD04563-46D9-4D5B-BCE5-667789E2BBEA}.Debug|Any CPU.Build.0 = Debug|Any CPU
29 | {5BD04563-46D9-4D5B-BCE5-667789E2BBEA}.Debug|x64.ActiveCfg = Debug|Any CPU
30 | {5BD04563-46D9-4D5B-BCE5-667789E2BBEA}.Debug|x64.Build.0 = Debug|Any CPU
31 | {5BD04563-46D9-4D5B-BCE5-667789E2BBEA}.Debug|x86.ActiveCfg = Debug|Any CPU
32 | {5BD04563-46D9-4D5B-BCE5-667789E2BBEA}.Debug|x86.Build.0 = Debug|Any CPU
33 | {5BD04563-46D9-4D5B-BCE5-667789E2BBEA}.Release|Any CPU.ActiveCfg = Release|Any CPU
34 | {5BD04563-46D9-4D5B-BCE5-667789E2BBEA}.Release|Any CPU.Build.0 = Release|Any CPU
35 | {5BD04563-46D9-4D5B-BCE5-667789E2BBEA}.Release|x64.ActiveCfg = Release|Any CPU
36 | {5BD04563-46D9-4D5B-BCE5-667789E2BBEA}.Release|x64.Build.0 = Release|Any CPU
37 | {5BD04563-46D9-4D5B-BCE5-667789E2BBEA}.Release|x86.ActiveCfg = Release|Any CPU
38 | {5BD04563-46D9-4D5B-BCE5-667789E2BBEA}.Release|x86.Build.0 = Release|Any CPU
39 | {8251D2E8-2A62-4D65-89AD-F738437B91C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
40 | {8251D2E8-2A62-4D65-89AD-F738437B91C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
41 | {8251D2E8-2A62-4D65-89AD-F738437B91C0}.Debug|x64.ActiveCfg = Debug|Any CPU
42 | {8251D2E8-2A62-4D65-89AD-F738437B91C0}.Debug|x64.Build.0 = Debug|Any CPU
43 | {8251D2E8-2A62-4D65-89AD-F738437B91C0}.Debug|x86.ActiveCfg = Debug|Any CPU
44 | {8251D2E8-2A62-4D65-89AD-F738437B91C0}.Debug|x86.Build.0 = Debug|Any CPU
45 | {8251D2E8-2A62-4D65-89AD-F738437B91C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
46 | {8251D2E8-2A62-4D65-89AD-F738437B91C0}.Release|Any CPU.Build.0 = Release|Any CPU
47 | {8251D2E8-2A62-4D65-89AD-F738437B91C0}.Release|x64.ActiveCfg = Release|Any CPU
48 | {8251D2E8-2A62-4D65-89AD-F738437B91C0}.Release|x64.Build.0 = Release|Any CPU
49 | {8251D2E8-2A62-4D65-89AD-F738437B91C0}.Release|x86.ActiveCfg = Release|Any CPU
50 | {8251D2E8-2A62-4D65-89AD-F738437B91C0}.Release|x86.Build.0 = Release|Any CPU
51 | EndGlobalSection
52 | GlobalSection(NestedProjects) = preSolution
53 | {5BD04563-46D9-4D5B-BCE5-667789E2BBEA} = {9A16544E-F5A7-4566-B239-1CC3DC3DD96E}
54 | {8251D2E8-2A62-4D65-89AD-F738437B91C0} = {114EAE3F-4A6E-4446-B4DE-73660768E258}
55 | EndGlobalSection
56 | EndGlobal
57 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ASP.Net Core 3.1 on AWS Lambda demo
2 |
3 |
4 |
5 |
6 |
7 | As of the end of March 2020, AWS Lambda [supports ASP.Net Core 3.1][lambda-support].
8 | As of mid-March 2020, API Gateway [HTTP APIs become generally available][http-api-ga].
9 | The combination of these two releases means that the best way (in my opinion!) of
10 | writing, deploying and running serverless web apps in the cloud is now even better.
11 |
12 | My favourite pattern for architecting a serverless .Net website is to put a regular
13 | ASP.Net Core website into a Lambda function wholesale. This means that developers
14 | can do local development, unit tests, integration tests the exact same way they
15 | know and love **and** take advantage of serverless infrastructure.
16 |
17 | This repo contains everything you need to take the standard ASP.Net Core "web API"
18 | template and continuously deploy it to AWS Lambda. Here's what's been added:
19 |
20 | ## Additions to standard template
21 |
22 | * [`.github/workflows/ci.yml`](.github/workflows/ci.yml): This is the [GitHub Actions][actions]
23 | pipeline for building and deploying this project to AWS Lambda. The steps are:
24 |
25 | * Setting up .Net SDK and AWS Lambda CLI
26 | * Run unit and integration tests
27 | * Run [ReSharper checks][resharper-action] and reports on PRs
28 | * Build and package app into a zip file suitable for upload to AWS
29 | * Log into AWS (this requires you to [configure AWS creds in GitHub][aws-action])
30 | * Use CloudFormation to deploy the Lambda function and HTTP API
31 |
32 | * [`src/HelloWorld/Program.cs`](src/HelloWorld/Program.cs): This file has been
33 | refactored to support the slightly different way that an ASP.Net Core app is
34 | started in Lambda. You shouldn't need to touch this file at all, except for
35 | changing logging.
36 |
37 | * [`src/HelloWorld/Startup.cs`](src/HelloWorld/Startup.cs): The only change to
38 | this file is to add a (trivial) dependency-injected `IValuesService` to demonstrate
39 | integration testing in the test project.
40 |
41 | * [`test/HelloWorld.Tests/TestValuesController.cs`](test/HelloWorld.Tests/TestValuesController.cs):
42 | This file demonstrates [ASP.Net Core integration tests][anc-tests] in the style
43 | made possible by `Microsoft.AspNetCore.Mvc.Testing`. A mock `IValuesService`
44 | is injected. This shows that tests don't have to be written any differently
45 | just because the app is hosted in Lambda.
46 |
47 | * [`serverless.yml`](serverless.yml): This file contains the entirety of the
48 | serverless infrastructure needed to host the website. The key to the file's
49 | conciseness is the [`AWS::Serverless::Function`][sam-function] that can magic up
50 | an API.
51 |
52 | ## So what should I do?
53 |
54 | First, you'll want to create your own copy of this template repo by clicking
55 | this button on the top right of this page:
56 |
57 |
58 |
59 | Once your repo has been created, the first run in GitHub Actions will unfortunately
60 | fail because you haven't yet setup secrets. You'll want to follow [this AWS guide][aws-action]
61 | to setup your secrets in GitHub. You'll know it's done correctly when your secrets
62 | look like this:
63 |
64 |
65 |
66 | Finally, once your secrets are configured correctly your pipeline will run
67 | successfully. PRs have will run unit tests and building, but only the `master`
68 | branch will get deployed. To access your website, go to your Action's logs,
69 | click the arrow next to the _Deploy_ step and look for the `ApiUrl` output. It
70 | should look something like this:
71 |
72 |
73 |
74 | You can then navigate to that URL in your browser - and add `/api/values` onto
75 | the end of the URL to see the fruits of your labour!
76 |
77 | [lambda-support]: https://aws.amazon.com/blogs/compute/announcing-aws-lambda-supports-for-net-core-3-1/
78 | [http-api-ga]: https://aws.amazon.com/blogs/compute/building-better-apis-http-apis-now-generally-available/
79 | [actions]: https://github.com/features/actions
80 | [aws-action]: https://github.com/aws-actions/configure-aws-credentials
81 | [anc-tests]: https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-3.1
82 | [sam-function]: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-function.html
83 | [resharper-action]: https://github.com/glassechidna/resharper-action
84 |
--------------------------------------------------------------------------------
/serverless.yml:
--------------------------------------------------------------------------------
1 | Transform: AWS::Serverless-2016-10-31
2 |
3 | Resources:
4 | Function:
5 | Type: AWS::Serverless::Function
6 | Properties:
7 | Handler: HelloWorld::HelloWorld.LambdaHandler::FunctionHandlerAsync
8 | CodeUri: src/HelloWorld/bin/Release/netcoreapp3.1/HelloWorld.zip
9 | Runtime: dotnetcore3.1
10 | MemorySize: 512
11 | Timeout: 20
12 | AutoPublishAlias: live
13 | Events:
14 | Api:
15 | Type: HttpApi
16 | Properties:
17 | TimeoutInMillis: 20000
18 |
19 | Outputs:
20 | ApiUrl:
21 | Value: !Sub https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com
22 | Function:
23 | Value: !Ref Function
24 |
--------------------------------------------------------------------------------
/src/HelloWorld/HelloWorld.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | netcoreapp3.1
4 | true
5 | Lambda
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/HelloWorld/Program.cs:
--------------------------------------------------------------------------------
1 | using Amazon.Lambda.AspNetCoreServer;
2 | using Microsoft.AspNetCore;
3 | using Microsoft.AspNetCore.Hosting;
4 |
5 | namespace HelloWorld
6 | {
7 | public class Program
8 | {
9 | public static void Main(string[] args)
10 | {
11 | CreateWebHostBuilder(args).Build().Run();
12 | }
13 |
14 | // Microsoft.AspNetCore.Mvc.Testing wants the method below to have this exact name
15 | public static IWebHostBuilder CreateWebHostBuilder(string[] args)
16 | {
17 | return WebHost.
18 | CreateDefaultBuilder(args).
19 | // maybe you'll want to add your own logging configuration here (e.g. Serilog, etc)
20 | UseStartup();
21 | }
22 | }
23 |
24 | // On Lambda, Program.Main is **not** executed. Instead, Lambda loads this DLL
25 | // into its own app and uses the following class to translate from the Lambda
26 | // protocol to the standard ASP.Net Core web host and middleware pipeline.
27 | public class LambdaHandler : APIGatewayHttpApiV2ProxyFunction
28 | {
29 | protected override IWebHostBuilder CreateWebHostBuilder()
30 | {
31 | return Program.CreateWebHostBuilder(null);
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/src/HelloWorld/Startup.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Builder;
2 | using Microsoft.AspNetCore.Hosting;
3 | using Microsoft.Extensions.Configuration;
4 | using Microsoft.Extensions.DependencyInjection;
5 | using Microsoft.Extensions.Hosting;
6 |
7 | namespace HelloWorld
8 | {
9 | public class Startup
10 | {
11 | public Startup(IConfiguration configuration)
12 | {
13 | Configuration = configuration;
14 | }
15 |
16 | public static IConfiguration Configuration { get; private set; }
17 |
18 | public void ConfigureServices(IServiceCollection services)
19 | {
20 | services.AddControllers();
21 |
22 | services.AddSingleton();
23 | }
24 |
25 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
26 | {
27 | if (env.IsDevelopment()) app.UseDeveloperExceptionPage();
28 |
29 | app.UseHttpsRedirection();
30 | app.UseRouting();
31 | app.UseAuthorization();
32 |
33 | app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/src/HelloWorld/ValuesController.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using Microsoft.AspNetCore.Mvc;
4 |
5 | namespace HelloWorld
6 | {
7 | [Route("api/[controller]")]
8 | public class ValuesController : ControllerBase
9 | {
10 | private readonly IValuesService _values;
11 |
12 | public ValuesController(IValuesService values)
13 | {
14 | _values = values;
15 | }
16 |
17 | // GET api/values
18 | [HttpGet]
19 | public IEnumerable Get()
20 | {
21 | return _values.GetValues();
22 | }
23 |
24 | // GET api/values/5
25 | [HttpGet("{id}")]
26 | public string Get(int id)
27 | {
28 | return _values.GetValues().FirstOrDefault();
29 | }
30 |
31 | // POST api/values
32 | [HttpPost]
33 | public void Post([FromBody] string value)
34 | {
35 | }
36 |
37 | // PUT api/values/5
38 | [HttpPut("{id}")]
39 | public void Put(int id, [FromBody] string value)
40 | {
41 | }
42 |
43 | // DELETE api/values/5
44 | [HttpDelete("{id}")]
45 | public void Delete(int id)
46 | {
47 | }
48 | }
49 | }
--------------------------------------------------------------------------------
/src/HelloWorld/ValuesService.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace HelloWorld
4 | {
5 | public interface IValuesService
6 | {
7 | IEnumerable GetValues();
8 | }
9 |
10 | public class ValuesService : IValuesService
11 | {
12 | public IEnumerable GetValues()
13 | {
14 | return new List
15 | {
16 | "value1",
17 | "value2"
18 | };
19 | }
20 | }
21 | }
--------------------------------------------------------------------------------
/src/HelloWorld/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information"
5 | }
6 | }
7 | }
--------------------------------------------------------------------------------
/src/HelloWorld/aws-lambda-tools-defaults.json:
--------------------------------------------------------------------------------
1 | {
2 | "configuration": "Release",
3 | "framework": "netcoreapp3.1"
4 | }
--------------------------------------------------------------------------------
/test/HelloWorld.Tests/HelloWorld.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | netcoreapp3.1
4 | False
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/test/HelloWorld.Tests/TestValuesController.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Threading.Tasks;
3 | using Microsoft.AspNetCore.Mvc.Testing;
4 | using Microsoft.AspNetCore.TestHost;
5 | using Microsoft.Extensions.DependencyInjection;
6 | using Xunit;
7 |
8 | namespace HelloWorld.Tests
9 | {
10 | public class TestValuesController : IClassFixture>
11 | {
12 | public TestValuesController(WebApplicationFactory factory)
13 | {
14 | _factory = factory;
15 | }
16 |
17 | private readonly WebApplicationFactory _factory;
18 |
19 | [Fact]
20 | public async Task TestSomeMoo()
21 | {
22 | using var client = _factory.WithWebHostBuilder(builder =>
23 | {
24 | builder.ConfigureTestServices(services =>
25 | {
26 | services.AddSingleton();
27 | });
28 | }).CreateClient();
29 |
30 | var index = await client.GetStringAsync("/api/values");
31 | Assert.Equal("[\"value1\"]", index);
32 | }
33 | }
34 |
35 | public class TestValuesService : IValuesService
36 | {
37 | public IEnumerable GetValues()
38 | {
39 | return new List {"value1"};
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------