{"id":15002439,"url":"https://github.com/mxsgrs/starter","last_synced_at":"2025-07-27T00:10:43.103Z","repository":{"id":293116717,"uuid":"878644132","full_name":"mxsgrs/starter","owner":"mxsgrs","description":"Starter pack for .NET web API","archived":false,"fork":false,"pushed_at":"2025-06-18T13:16:14.000Z","size":100,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-06-18T13:28:35.065Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mxsgrs.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2024-10-25T19:19:51.000Z","updated_at":"2025-06-18T13:16:18.000Z","dependencies_parsed_at":"2025-05-13T19:40:12.971Z","dependency_job_id":null,"html_url":"https://github.com/mxsgrs/starter","commit_stats":null,"previous_names":["mxsgrs/starter"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/mxsgrs/starter","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mxsgrs%2Fstarter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mxsgrs%2Fstarter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mxsgrs%2Fstarter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mxsgrs%2Fstarter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mxsgrs","download_url":"https://codeload.github.com/mxsgrs/starter/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mxsgrs%2Fstarter/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":267273433,"owners_count":24062588,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-07-26T02:00:08.937Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["asp-net-core","code-first","csharp","docker","dotnet-8","http","sql-server","web-api"],"created_at":"2024-09-24T18:50:14.783Z","updated_at":"2025-07-27T00:10:42.895Z","avatar_url":"https://github.com/mxsgrs.png","language":"C#","readme":"# .NET 9 web API starter pack\n\n## Introduction\n\nThis project implements an ASP.NET Core 9 web API with the most common features. It is paired with a SQL Server database using a code first approach.\nWhile this project use .NET Aspire for running the API and its database, the main goal is not to cover DevOps technologies. This content focus\nprimarily on building a simple ASP.NET web API with the latest .NET version.\n\n### Prerequisites\n\nBefore anything please install if they are not already the following elements\n- Download and install **.NET 9** [here](https://dotnet.microsoft.com/en-us/download/dotnet/9.0)\n- Download and install **Docker Desktop** [here](https://docs.docker.com/desktop/install/windows-install/)\n\n### Run\n\nIn order to run this application, set the app host as start up project and click on run. According to its configuration, .NET Aspire will create a\n**container for the API** based on the corresponding project and an other **container for a SQL Server database** based on a Microsoft official image. \nEntityFramework migrations will be applied at runtime to the database.\n\nInside the web API project there is a folder which contains **predefined HTTP requests**. First request should create a new user with **UserCredentials.http**.\nOnce it's done JWT can be generated with **Authentication.http**, then authorized endpoints can be accessed and so on.\n\n### Tests\n\nOf course this solution includes unit tests and integration tests for this application. \n\nNote that **integration tests use a containerized database** by leveraging the **Testcontainers** nuget package. This is a relevant solution as integration \ntests aim to test the application in an environment mirroring production, so we can validate its behavior in conditions closely matching actual usage.\n\n## Features\n\n### Services and dependency injection pattern\n\nIf you inspect old projects, you might find **most of the code inside controllers**, after all this it what **MVC** means. As practices have evolved,\nit is now recommended to implement business logic in what we call **services**. \n\nOnce this is done, services can be injected in controllers but also in other services. Where previously the business logic contained in a\ncontroller was not accessible from another controller, we can now **share it everywhere** with a service.\n\nIn this project services are declared in **DependencyInjectionSetup.cs**\n\n```csharp\npublic static class DependencyInjection\n{\n    public static void AddStarterServices(this IServiceCollection services)\n    {\n        services.AddScoped\u003cIAppContextAccessor, AppContextAccessor\u003e();\n        services.AddScoped\u003cIUserRepository, UserRepository\u003e();\n        services.AddScoped\u003cIUserService, UserService\u003e();\n        services.AddScoped\u003cIJwtService, JwtService\u003e();\n    }\n}\n```\n\nThis method is called in **Program.cs** like this `builder.Services.AddStarterServices()`. Once this is done, services are \n**available in all controllers and services constructor** like in the following examples.\n\n```csharp\npublic class AuthenticationController(IJwtService jwtService) : StarterControllerBase\n\npublic class JwtService(ILogger\u003cJwtService\u003e logger, IConfiguration configuration,\n    IUserRepository userService) : IJwtService\n```\n\n### JWT authentication\n\nAs JWT authentication is the standard **approach** to secure a web API, it's quite a good place to start. This project implements the whole process.\n- **Send your credentials with a post requests** to an endpoint of **AuthenticationController** and get a token. \n- Now **secured endpoints** can be accessed by adding this token to the **authorization header** of HTTP requests.\n\nJWT authentication is declared in **Program.cs** as follows.\n\n```csharp\nbuilder.Services.AddAuthentication()\n    .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =\u003e\n    {\n        JsonWebTokenParameters jwt = builder.Configuration\n            .GetRequiredSection(\"JsonWebTokenParameters\")\n            .Get\u003cJsonWebTokenParameters\u003e()\n                ?? throw new Exception(\"JWT settings are not configured\");\n\n        byte[] encodedKey = Encoding.ASCII.GetBytes(jwt.Key);\n        SymmetricSecurityKey symmetricSecurityKey = new(encodedKey);\n\n        options.TokenValidationParameters = new TokenValidationParameters\n        {\n            ValidIssuer = jwt.Issuer,\n            ValidAudience = jwt.Audience,\n            IssuerSigningKey = symmetricSecurityKey\n        };\n    });\n```\n\n### Mulitple configurations\n\nMaybe you need an application that has more than the usual Debug and Release configuration. Let's say you have a **custom configuration**\nfor one of your clients. We will call this configuration Custom. That being said you might have different settings for this configuration and\ncreate an **appsettings.Custom.json file**. Now you expect the application to use these settings when you **publish** it with the Custom configuration.\n\nBut this is not how .NET works. No matter what configuration you select during publishing, the application will look for **appsettings.environmentName.json**\nduring its execution. For example **appsettings.Development.json** or **appsettings.Production.json** for respectively **Debug** and **Release** configuration.\n\nThis project has code in Program.cs to **handle this situation**. Hence if you publish your application with Custom configuration, it will look for\n**appsettings.Custom.json** and so on.\n\n```csharp\nstring configurationName = Assembly.GetExecutingAssembly()\n    .GetCustomAttribute\u003cAssemblyConfigurationAttribute\u003e()?.Configuration\n        ?? throw new Exception(\"Can not read configuration name\");\n\nbuilder.Configuration.AddJsonFile($\"appsettings.{configurationName}.json\");\n```\n\n### Endpoints URL convention\n\nGoogle specified some guidelines for URL [here](https://developers.google.com/search/docs/crawling-indexing/url-structure?hl=fr). They should be written \nusing the **kebab case** but as this is the not default behavior in an ASP.NET Core web API, some modifications are needed. First we need to define a \n**IOutboundParameterTransformer** which will convert any value from pascal case to kebab case. In the **Utilities** folder you will find \n**ToKebabParameterTransformer.cs** file with the following content.\n\n```csharp\npublic partial class ToKebabParameterTransformer : IOutboundParameterTransformer\n{\n    public string TransformOutbound(object? value)\n    {\n        return MatchLowercaseThenUppercase()\n            .Replace(value?.ToString() ?? \"\", \"$1-$2\")\n            .ToLower();\n    }\n\n    [GeneratedRegex(\"([a-z])([A-Z])\")]\n    private static partial Regex MatchLowercaseThenUppercase();\n}\n```\n\nNow we can add a new convention inside every controllers by modifying the **Program.cs** file like this.\n\n```csharp\nbuilder.Services.AddControllers(options =\u003e\n    {\n        ToKebabParameterTransformer toKebab = new();\n        options.Conventions.Add(new RouteTokenTransformerConvention(toKebab));\n    });\n```\n\nEvery endpoint URL will now use the kebab case by default.\n\n```\n/api/authentication/token\n```\n\n### HTTP files\n\nWhile **Postman** is really great, having a HTTP client with all your predefined requests **inside your project** is such a handy tool. It allows to \nbind your code to those requests **in the version control**. Hence when members of the team pull your code, they instantly have the possibility \nto test it with your HTTP requests, saving time and making collaboration easier.\n\nHTTP requests are defined in **.http files**. Examples for this project can be found in the **Https** folder. Each file corresponds to a controller. There \na still some limitations, it is not possible to add **pre-request or post-response scripts** like in Postman. That being said, this tool is quite new\nand it is reasonable to think that this kind of features will be added in the future.\n\n```http\nPOST {{HostAddress}}/api/authentication/token\nContent-Type: application/json\n\n{\n  \"emailAddress\": \"john.doe@example.com\",\n  \"hashedPassword\": \"TWF0cml4UmVsb2FkZWQh\"\n}\n```\n\n**Variables**, which are between double curly braces, can be defined in the **http-client.env.json file**. Multiple environments can be configured, making \npossible to attribute **a different value** to a variable for each environment. Then it is easy to **switch** between environment with the same request, \nmaking the workflow even **faster**.\n\nNote that everytime this file is modified, **closing and reopening** Visual Studio is needed so changes are **taken into account**. I hope Microsoft will \nfix this in the future.\n\n```json\n{\n  \"dev\": {\n    \"HostAddress\": \"https://localhost:7137\",\n    \"Jwt\": \"xxx.yyy.zzz\"\n  },\n  \"prod\": {\n    \"HostAddress\": \"https://starterwebapi.com\",\n    \"Jwt\": \"xxx.yyy.zzz\"\n  }\n}\n```\n\nSee official Microsoft documentation for more information [here](https://learn.microsoft.com/en-us/aspnet/core/test/http-files?view=aspnetcore-8.0).\n\n### Code first approach\n\nFirst create DbContext and entity classes inside the infrastructure layer. Then **register DbContext as a service** inside Program.cs.\n\n```csharp\nstring connectionString = builder.Configuration.GetConnectionString(\"SqlServer\")\n    ?? throw new Exception(\"Connection string for SQL Server is missing\");\n\nbuilder.Services.AddDbContext\u003cStarterDbContext\u003e(options =\u003e\n    options.UseSqlServer(connectionString));\n```\n\nOnce all above is done, it is possible **apply this structure** to the running database. As the project containing DbContext and the web API project are \ntwo different things we define an implementation of **DesignTimeDbContextFactory** interface. \n\nThis is needed as otherwise we would have to install EntityFramework packages in the web API project in order to create migration files. This is **strongly \nnot recommanded** as in Clean Architecture, the only layer responsible for persistance is the infrastructure layer, not the presentation layer.\n\nIn this example we are using a SQL Server 2022 database.\n\n```csharp\npublic class StarterDbContextFactory : IDesignTimeDbContextFactory\u003cStarterDbContext\u003e\n{\n    public StarterDbContext CreateDbContext(string[] args)\n    {\n        DbContextOptionsBuilder\u003cStarterDbContext\u003e optionBuilder = new();\n\n        // docker run -e \"ACCEPT_EULA=Y\" -e \"MSSQL_SA_PASSWORD=B1q22MPXUgosXiqZ\" -p 1433:1433 -d mcr.microsoft.com/mssql/server:2022-latest\n        string connectionString = \"Data Source=localhost,1433;Initial Catalog=Starter;User=sa;Password=B1q22MPXUgosXiqZ;TrustServerCertificate=yes\";\n        optionBuilder.UseSqlServer(connectionString);\n\n        // Context used during the creation of migrations\n        return new StarterDbContext(optionBuilder.Options);\n    }\n}\n```\n\nThen create a new migration with this .NET CLI command inside the infrastructure project. New .cs files describing every table will be generated in **Migrations folder**.\n\n```bash\ndotnet ef migrations add InitialCreate --output-dir Persistance/Migrations\n```\n\nWhen it's done, **migration** can be applied to the database with this command. EntityFramework will use the connection string\nin order to connect to the running database and **apply changes** contained in the previously generated migration files.\n\n```bash\ndotnet ef database update\n```\n\nAs this project use a containerized database, some unsual scenarios can happen. For example, the API is already running and a database container is \ncreated. Another scenario could be with an already running database container and the API that starts running.\n\nIn each of these scenarios we need to make sure the **migrations are applied** to the database and in case not, the application should do it. Migration \ncould be **applied at application startup** but that wouldn't cover the scenario where **database is dropped and recreated while applicaton is still running**. \nThis is why I make sure **all migrations are applied at every interaction with the database** with the following piece of code inside my **DbContext**.\n\n```csharp\npublic partial class StarterDbContext : DbContext\n{\n    public StarterDbContext(DbContextOptions\u003cStarterDbContext\u003e options)\n        : base(options)\n    {\n        string? aspNetCoreEnvironment = Environment.GetEnvironmentVariable(\"ASPNETCORE_ENVIRONMENT\");\n\n        if (aspNetCoreEnvironment == \"Development\" || aspNetCoreEnvironment == \"Integration\")\n        {\n            Database.Migrate();\n        }\n    }\n```\n\nAs this application uses an in-memory database for unit tests, **migration should not be applied during those tests**. This is why I am checking the environment \nname before applying or not the migrations.\n\n### Log automatically invalid model state\n\nA common error when developing a web API is to post an **invalid object** and get a **bad request** response in return. When this happens the developer \nneeds to investigate the **ModelState**, but it can be a long and painful process. Fortunately, it is now possible to automatically log ModelState errors and\nsee the **relevant details**, particularly which **object property** is causing the invalid state.\n\n```csharp\nbuilder.Services.AddControllers()\n    .ConfigureApiBehaviorOptions(options =\u003e\n    {\n        var builtInFactory = options.InvalidModelStateResponseFactory;\n        options.InvalidModelStateResponseFactory = context =\u003e\n        {\n            ILogger\u003cProgram\u003e logger = context.HttpContext.RequestServices\n                .GetRequiredService\u003cILogger\u003cProgram\u003e\u003e();\n\n            IEnumerable\u003cModelError\u003e errors = context.ModelState.Values\n                .SelectMany(item =\u003e item.Errors);\n\n            foreach (ModelError error in errors)\n            {\n                logger.LogError(\"{ErrorMessage}\", error.ErrorMessage);\n            }\n\n            return builtInFactory(context);\n        };\n    });\n```\n\nAs this is logging, it will appear in every configured sink. One of them being the console which is always open, it allows developers to see this \ntype of content immediately. Here is an example.\n\n```\n2024-08-08 18:10:01 fail: Program[0]\n2024-08-08 18:10:01       The EmailAddress field is required.\n```\n\n### Global usings\n\nWith the new feature `global using`, namespaces can be included for the whole project instead having to specify it in every file. This feature improve \nmaintainability and save time on repetitive tasks. Implementation can be found in **GlobalUsing.cs** file, inside each project root folder.\n\n```csharp\nglobal using AutoMapper;\nglobal using Microsoft.AspNetCore.Authorization;\nglobal using Microsoft.AspNetCore.Mvc;\nglobal using Microsoft.EntityFrameworkCore;\nglobal using Starter.WebApi;\nglobal using Starter.WebApi.Controllers.Abstracts;\nglobal using Starter.WebApi.Utilities;\n```","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmxsgrs%2Fstarter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmxsgrs%2Fstarter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmxsgrs%2Fstarter/lists"}