From 2161476c2d782bdc5d70a2502f53c36665b8b951 Mon Sep 17 00:00:00 2001 From: Mohammed Atef <122639481+Mohammed-Atef2004@users.noreply.github.com> Date: Fri, 27 Mar 2026 17:33:53 +0200 Subject: [PATCH] feat: add minimal api sample for primitive obsession prevention --- README.md | 14 +++-- global.json | 1 + .../Primitive-Obsession.sln | 25 ++++++++ .../Primitive-Obsession.csproj | 10 ++++ .../Primitive-Obsession/Program.cs | 60 +++++++++++++++++++ .../appsettings.Development.json | 8 +++ .../Primitive-Obsession/appsettings.json | 9 +++ 7 files changed, 122 insertions(+), 5 deletions(-) create mode 100644 projects/minimal-api/Primitive-Obsession/Primitive-Obsession.sln create mode 100644 projects/minimal-api/Primitive-Obsession/Primitive-Obsession/Primitive-Obsession.csproj create mode 100644 projects/minimal-api/Primitive-Obsession/Primitive-Obsession/Program.cs create mode 100644 projects/minimal-api/Primitive-Obsession/Primitive-Obsession/appsettings.Development.json create mode 100644 projects/minimal-api/Primitive-Obsession/Primitive-Obsession/appsettings.json diff --git a/README.md b/README.md index 0076c74dd..ed2333aec 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ You can find samples on new features available in ASP.NET Core 9(3) [here](/proj | [Localization and Globalization](/projects/localization) | 6 | | | [Middleware](/projects/middleware) | 14 | | | [Mini Apps](/projects/mini) | 2 | | -| [Minimal API](/projects/minimal-api) | 36 | Routing, Parameter Bindings, etc | +| [Minimal API](/projects/minimal-api) | 37 | Routing, Parameter Bindings, etc | | [Minimal Hosting](/projects/minimal-hosting) | 23 | | | [MVC](/projects/mvc) | 47 | Localization, Routing, Razor Class Library, Tag Helpers, View Component, etc | | [Output Cache Middleware](/projects/output-cache-middleware) | | | @@ -72,7 +72,7 @@ You can find samples on new features available in ASP.NET Core 9(3) [here](/proj | [Syndications](/projects/syndications) | 3 | | | [Testing](/projects/testing) | 1 | | | [Unpoly](/projects/unpoly) | 5 | | -| [URL Redirect/Rewrite](/projects/rewrite) | 6 | | +| https://weblogs.asp.net/owscott/rewrite-vs-redirect-what-s-the-difference(/projects/rewrite) | 6 | | | [Uri Helper](/projects/uri-helper) | 5 | | | [Windows Service](/projects/windows-service) | 1 | | | [Web Sockets](/projects/web-sockets) | 6 | | @@ -85,6 +85,12 @@ You can find samples on new features available in ASP.NET Core 9(3) [here](/proj To run these samples, simply open your command line console, go to each folder and execute `dotnet watch run`. +### Minimal API (37) + +- [Primitive Obsession Prevention](./projects/minimal-api/primitive-obsession) + + This sample demonstrates how to prevent **Primitive Obsession** by using **Value Objects** and the `static TryParse` pattern for automatic parameter binding in Minimal APIs. + ### Misc (6) - [Application Environment](/projects/application-environment) @@ -166,6 +172,4 @@ All these samples require `SixLabors.ImageSharp.Web` middleware package. This mi ## Misc - [Contributor Guidelines](https://github.com/dodyg/practical-aspnetcore/blob/master/CONTRIBUTING.md) -- [Code of Conduct](https://github.com/dodyg/practical-aspnetcore/blob/master/CODE_OF_CONDUCT.md) - - +- [Code of Conduct](https://github.com/dodyg/practical-aspnetcore/blob/master/CODE_OF_CONDUCT.md) \ No newline at end of file diff --git a/global.json b/global.json index 2f528e75a..0607c4649 100644 --- a/global.json +++ b/global.json @@ -4,4 +4,5 @@ "rollForward": "major", "allowPrerelease": true } + } diff --git a/projects/minimal-api/Primitive-Obsession/Primitive-Obsession.sln b/projects/minimal-api/Primitive-Obsession/Primitive-Obsession.sln new file mode 100644 index 000000000..78a52d8c7 --- /dev/null +++ b/projects/minimal-api/Primitive-Obsession/Primitive-Obsession.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36414.22 d17.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Primitive-Obsession", "Primitive-Obsession\Primitive-Obsession.csproj", "{3722E737-D7F9-444B-BB09-C4B80452ACB4}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3722E737-D7F9-444B-BB09-C4B80452ACB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3722E737-D7F9-444B-BB09-C4B80452ACB4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3722E737-D7F9-444B-BB09-C4B80452ACB4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3722E737-D7F9-444B-BB09-C4B80452ACB4}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {A9FE2ACE-83FF-4EDA-A57F-2237F9A761E7} + EndGlobalSection +EndGlobal diff --git a/projects/minimal-api/Primitive-Obsession/Primitive-Obsession/Primitive-Obsession.csproj b/projects/minimal-api/Primitive-Obsession/Primitive-Obsession/Primitive-Obsession.csproj new file mode 100644 index 000000000..1b3339e24 --- /dev/null +++ b/projects/minimal-api/Primitive-Obsession/Primitive-Obsession/Primitive-Obsession.csproj @@ -0,0 +1,10 @@ + + + + net10.0 + enable + enable + Primitive_Obsession + + + diff --git a/projects/minimal-api/Primitive-Obsession/Primitive-Obsession/Program.cs b/projects/minimal-api/Primitive-Obsession/Primitive-Obsession/Program.cs new file mode 100644 index 000000000..f452494b8 --- /dev/null +++ b/projects/minimal-api/Primitive-Obsession/Primitive-Obsession/Program.cs @@ -0,0 +1,60 @@ +var builder = WebApplication.CreateBuilder(args); +var app = builder.Build(); + +// ------------------------------- +// CASE 1: Primitive Obsession +// ------------------------------- +// We rely on primitive types (string) to represent a domain concept. +// This forces validation logic to be repeated everywhere. + +app.MapPost("/orders-bad", (string email) => +{ +if (string.IsNullOrWhiteSpace(email) || !email.Contains("@")) +return Results.BadRequest("Invalid email address"); + +return Results.Ok($"Order placed for {email}"); +}); + + +// ------------------------------- +// CASE 2: Value Object +// ------------------------------- +// We represent the concept using a dedicated type. +// ASP.NET Core will use TryParse during model binding. + +app.MapPost("/orders-good", (EmailAddress email) => +{ +// If execution reaches here, the email is guaranteed to be valid +return Results.Ok($"Order placed for {email.Value}"); +}); + + +app.Run(); + + +// ------------------------------- +// Value Object +// ------------------------------- +public sealed record EmailAddress +{ + public string Value { get; } + + private EmailAddress(string value) + { + Value = value; + } + + // ASP.NET Core Minimal APIs automatically use TryParse + // during parameter binding. + public static bool TryParse(string? value, out EmailAddress? result) + { + if (!string.IsNullOrWhiteSpace(value) && value.Contains("@")) + { + result = new EmailAddress(value); + return true; + } + + result = null; + return false; + } +} \ No newline at end of file diff --git a/projects/minimal-api/Primitive-Obsession/Primitive-Obsession/appsettings.Development.json b/projects/minimal-api/Primitive-Obsession/Primitive-Obsession/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/projects/minimal-api/Primitive-Obsession/Primitive-Obsession/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/projects/minimal-api/Primitive-Obsession/Primitive-Obsession/appsettings.json b/projects/minimal-api/Primitive-Obsession/Primitive-Obsession/appsettings.json new file mode 100644 index 000000000..10f68b8c8 --- /dev/null +++ b/projects/minimal-api/Primitive-Obsession/Primitive-Obsession/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +}