├── .github └── FUNDING.yml ├── .gitignore ├── Dockerfile ├── LICENSE.md ├── README.md ├── minimal-apis.png ├── postman.png ├── src ├── Program.cs ├── Properties │ └── launchSettings.json ├── UrlShortener.csproj ├── UrlShortener.sln ├── UrlShortener.sln.DotSettings ├── appsettings.Development.json ├── appsettings.json └── index.html └── url-shortener.gif /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: changhuixu 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | 33 | # Visual Studio 2015/2017 cache/options directory 34 | .vs/ 35 | # Uncomment if you have tasks that create the project's static files in wwwroot 36 | #wwwroot/ 37 | 38 | # Visual Studio 2017 auto generated files 39 | Generated\ Files/ 40 | 41 | # MSTest test Results 42 | [Tt]est[Rr]esult*/ 43 | [Bb]uild[Ll]og.* 44 | 45 | # NUnit 46 | *.VisualState.xml 47 | TestResult.xml 48 | nunit-*.xml 49 | 50 | # Build Results of an ATL Project 51 | [Dd]ebugPS/ 52 | [Rr]eleasePS/ 53 | dlldata.c 54 | 55 | # Benchmark Results 56 | BenchmarkDotNet.Artifacts/ 57 | 58 | # .NET Core 59 | project.lock.json 60 | project.fragment.lock.json 61 | artifacts/ 62 | 63 | # StyleCop 64 | StyleCopReport.xml 65 | 66 | # Files built by Visual Studio 67 | *_i.c 68 | *_p.c 69 | *_h.h 70 | *.ilk 71 | *.meta 72 | *.obj 73 | *.iobj 74 | *.pch 75 | *.pdb 76 | *.ipdb 77 | *.pgc 78 | *.pgd 79 | *.rsp 80 | *.sbr 81 | *.tlb 82 | *.tli 83 | *.tlh 84 | *.tmp 85 | *.tmp_proj 86 | *_wpftmp.csproj 87 | *.log 88 | *.vspscc 89 | *.vssscc 90 | .builds 91 | *.pidb 92 | *.svclog 93 | *.scc 94 | 95 | # Chutzpah Test files 96 | _Chutzpah* 97 | 98 | # Visual C++ cache files 99 | ipch/ 100 | *.aps 101 | *.ncb 102 | *.opendb 103 | *.opensdf 104 | *.sdf 105 | *.cachefile 106 | *.VC.db 107 | *.VC.VC.opendb 108 | 109 | # Visual Studio profiler 110 | *.psess 111 | *.vsp 112 | *.vspx 113 | *.sap 114 | 115 | # Visual Studio Trace Files 116 | *.e2e 117 | 118 | # TFS 2012 Local Workspace 119 | $tf/ 120 | 121 | # Guidance Automation Toolkit 122 | *.gpState 123 | 124 | # ReSharper is a .NET coding add-in 125 | _ReSharper*/ 126 | *.[Rr]e[Ss]harper 127 | *.DotSettings.user 128 | 129 | # JustCode is a .NET coding add-in 130 | .JustCode 131 | 132 | # TeamCity is a build add-in 133 | _TeamCity* 134 | 135 | # DotCover is a Code Coverage Tool 136 | *.dotCover 137 | 138 | # AxoCover is a Code Coverage Tool 139 | .axoCover/* 140 | !.axoCover/settings.json 141 | 142 | # Visual Studio code coverage results 143 | *.coverage 144 | *.coveragexml 145 | 146 | # NCrunch 147 | _NCrunch_* 148 | .*crunch*.local.xml 149 | nCrunchTemp_* 150 | 151 | # MightyMoose 152 | *.mm.* 153 | AutoTest.Net/ 154 | 155 | # Web workbench (sass) 156 | .sass-cache/ 157 | 158 | # Installshield output folder 159 | [Ee]xpress/ 160 | 161 | # DocProject is a documentation generator add-in 162 | DocProject/buildhelp/ 163 | DocProject/Help/*.HxT 164 | DocProject/Help/*.HxC 165 | DocProject/Help/*.hhc 166 | DocProject/Help/*.hhk 167 | DocProject/Help/*.hhp 168 | DocProject/Help/Html2 169 | DocProject/Help/html 170 | 171 | # Click-Once directory 172 | publish/ 173 | 174 | # Publish Web Output 175 | *.[Pp]ublish.xml 176 | *.azurePubxml 177 | # Note: Comment the next line if you want to checkin your web deploy settings, 178 | # but database connection strings (with potential passwords) will be unencrypted 179 | *.pubxml 180 | *.publishproj 181 | 182 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 183 | # checkin your Azure Web App publish settings, but sensitive information contained 184 | # in these scripts will be unencrypted 185 | PublishScripts/ 186 | 187 | # NuGet Packages 188 | *.nupkg 189 | # NuGet Symbol Packages 190 | *.snupkg 191 | # The packages folder can be ignored because of Package Restore 192 | **/[Pp]ackages/* 193 | # except build/, which is used as an MSBuild target. 194 | !**/[Pp]ackages/build/ 195 | # Uncomment if necessary however generally it will be regenerated when needed 196 | #!**/[Pp]ackages/repositories.config 197 | # NuGet v3's project.json files produces more ignorable files 198 | *.nuget.props 199 | *.nuget.targets 200 | 201 | # Microsoft Azure Build Output 202 | csx/ 203 | *.build.csdef 204 | 205 | # Microsoft Azure Emulator 206 | ecf/ 207 | rcf/ 208 | 209 | # Windows Store app package directories and files 210 | AppPackages/ 211 | BundleArtifacts/ 212 | Package.StoreAssociation.xml 213 | _pkginfo.txt 214 | *.appx 215 | *.appxbundle 216 | *.appxupload 217 | 218 | # Visual Studio cache files 219 | # files ending in .cache can be ignored 220 | *.[Cc]ache 221 | # but keep track of directories ending in .cache 222 | !?*.[Cc]ache/ 223 | 224 | # Others 225 | ClientBin/ 226 | ~$* 227 | *~ 228 | *.dbmdl 229 | *.dbproj.schemaview 230 | *.jfm 231 | *.pfx 232 | *.publishsettings 233 | orleans.codegen.cs 234 | 235 | # Including strong name files can present a security risk 236 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 237 | #*.snk 238 | 239 | # Since there are multiple workflows, uncomment next line to ignore bower_components 240 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 241 | #bower_components/ 242 | 243 | # RIA/Silverlight projects 244 | Generated_Code/ 245 | 246 | # Backup & report files from converting an old project file 247 | # to a newer Visual Studio version. Backup files are not needed, 248 | # because we have git ;-) 249 | _UpgradeReport_Files/ 250 | Backup*/ 251 | UpgradeLog*.XML 252 | UpgradeLog*.htm 253 | ServiceFabricBackup/ 254 | *.rptproj.bak 255 | 256 | # SQL Server files 257 | *.mdf 258 | *.ldf 259 | *.ndf 260 | 261 | # Business Intelligence projects 262 | *.rdl.data 263 | *.bim.layout 264 | *.bim_*.settings 265 | *.rptproj.rsuser 266 | *- [Bb]ackup.rdl 267 | *- [Bb]ackup ([0-9]).rdl 268 | *- [Bb]ackup ([0-9][0-9]).rdl 269 | 270 | # Microsoft Fakes 271 | FakesAssemblies/ 272 | 273 | # GhostDoc plugin setting file 274 | *.GhostDoc.xml 275 | 276 | # Node.js Tools for Visual Studio 277 | .ntvs_analysis.dat 278 | node_modules/ 279 | 280 | # Visual Studio 6 build log 281 | *.plg 282 | 283 | # Visual Studio 6 workspace options file 284 | *.opt 285 | 286 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 287 | *.vbw 288 | 289 | # Visual Studio LightSwitch build output 290 | **/*.HTMLClient/GeneratedArtifacts 291 | **/*.DesktopClient/GeneratedArtifacts 292 | **/*.DesktopClient/ModelManifest.xml 293 | **/*.Server/GeneratedArtifacts 294 | **/*.Server/ModelManifest.xml 295 | _Pvt_Extensions 296 | 297 | # Paket dependency manager 298 | .paket/paket.exe 299 | paket-files/ 300 | 301 | # FAKE - F# Make 302 | .fake/ 303 | 304 | # CodeRush personal settings 305 | .cr/personal 306 | 307 | # Python Tools for Visual Studio (PTVS) 308 | __pycache__/ 309 | *.pyc 310 | 311 | # Cake - Uncomment if you are using it 312 | # tools/** 313 | # !tools/packages.config 314 | 315 | # Tabs Studio 316 | *.tss 317 | 318 | # Telerik's JustMock configuration file 319 | *.jmconfig 320 | 321 | # BizTalk build output 322 | *.btp.cs 323 | *.btm.cs 324 | *.odx.cs 325 | *.xsd.cs 326 | 327 | # OpenCover UI analysis results 328 | OpenCover/ 329 | 330 | # Azure Stream Analytics local run output 331 | ASALocalRun/ 332 | 333 | # MSBuild Binary and Structured Log 334 | *.binlog 335 | 336 | # NVidia Nsight GPU debugger configuration file 337 | *.nvuser 338 | 339 | # MFractors (Xamarin productivity tool) working folder 340 | .mfractor/ 341 | 342 | # Local History for Visual Studio 343 | .localhistory/ 344 | 345 | # BeatPulse healthcheck temp database 346 | healthchecksdb 347 | 348 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 349 | MigrationBackup/ 350 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG VERSION=8.0-alpine 2 | 3 | FROM mcr.microsoft.com/dotnet/sdk:$VERSION AS build 4 | WORKDIR /app 5 | 6 | COPY ./src/*.csproj ./ 7 | RUN dotnet restore 8 | 9 | COPY ./src/*.* . 10 | 11 | RUN dotnet publish -c Release -o /out --no-restore 12 | 13 | 14 | FROM mcr.microsoft.com/dotnet/aspnet:$VERSION AS runtime 15 | WORKDIR /app 16 | COPY --from=build /out ./ 17 | ENTRYPOINT ["dotnet", "UrlShortener.dll"] -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2021 Changhui Xu 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Minimal APIs in ASP.NET 8 2 | 3 | ## The solution is updated to .NET 8 4 | 5 | Buy Me a Coffee at ko-fi.com 6 | 7 | ## [Building a URL Shortener Web App using Minimal APIs in .NET 6](https://medium.com/@changhuixu/building-a-url-shortener-web-app-using-minimal-apis-in-net-6-99334ac6e98b) 8 | 9 | As announced in the [.NET 6 Preview 4 blog](https://devblogs.microsoft.com/aspnet/asp-net-core-updates-in-net-6-preview-4/), .NET 6 will release an improvement to ASP.NET Core: minimal APIs for hosting and routing in web applications. With these streamlined APIs, we can build microservices and small HTTP APIs with much less ceremony. 10 | 11 | In this article, we will first briefly describe the minimal APIs feature in .NET 6. To further demonstrate its use case, we then create a URL shortener web app and containerize it using Docker. 12 | 13 | ## Minimal APIs 14 | 15 | To try out a minimal API, we can create an empty web app using the following command. 16 | 17 | ```powershell 18 | dotnet new web 19 | ``` 20 | 21 | This command creates a web project with just a single `Program.cs` file, which has the following content. 22 | 23 | ```csharp 24 | using System; 25 | using Microsoft.AspNetCore.Builder; 26 | using Microsoft.Extensions.Hosting; 27 | 28 | var builder = WebApplication.CreateBuilder(args); 29 | await using var app = builder.Build(); 30 | 31 | if (app.Environment.IsDevelopment()) 32 | { 33 | app.UseDeveloperExceptionPage(); 34 | } 35 | 36 | app.MapGet("/", (Func)(() => "Hello World!")); 37 | 38 | await app.RunAsync(); 39 | ``` 40 | 41 | The `Program.cs` file is written in the style of [top-level program](https://devblogs.microsoft.com/dotnet/welcome-to-c-9-0/#top-level-programs), which gets rid of `class Program` and the `Main()` method thus decreases code clutters. In the code above, the first few lines create a web host, then line 13 defines a route in the routing table using the `MapGet` method. We can continue to build more APIs using `MapGet`, `MapPost`, and similar methods. 42 | 43 | The API composition is quite different from the traditional API Controller or MVC style. The minimal APIs have really pushed the number of lines of code to an extreme. If our APIs are simply doing some non-essential computation or CRUD operations, then the minimal APIs can reduce a lot of overhead. 44 | 45 | If we run the app, we will see the string "`Hello World!`" on the homepage. That's it! With only a few lines of code, we now have a fully functioning HTTP API. 46 | 47 | ## URL Shortener 48 | 49 | For demo purposes, we will create a URL Shortener web app. The app provides a form that allows users to enter a URL to get its shortened version. The app persists the an entry with the original URL and its shortened version. Therefore, the app is able to redirect a shortened URL to its original address. The following screen recording demonstrates the features to be implemented. 50 | 51 | ![url shortener](./url-shortener.gif) 52 | 53 | ### 1. Creating an API to Shorten a URL 54 | 55 | In this app, we are going to store the map entries between shortened and original URLs in a local database. A NoSQL database is suitable for this scenario since this app doesn't have complex data relationships. If we don't consider the scalability, then [LiteDB](https://github.com/mbdavid/LiteDB) is a good database candidate. If you haven't heard about it, LiteDB is a small, fast and lightweight .NET NoSQL embedded database, and we can think LiteDB as a combination of SQLite and MongoDB. 56 | 57 | After install the LiteDB NuGet package, we can register the `LiteDatabase` in the Dependency Injection (DI) container as follows. 58 | 59 | ```csharp 60 | var builder = WebApplication.CreateBuilder(args); 61 | builder.Services.AddSingleton(_ => new LiteDatabase("short-links.db")); 62 | await using var app = builder.Build(); 63 | ``` 64 | 65 | Then we can create a POST API which takes in a request body with a URL and returns a JSON object contains the shortened URL. 66 | 67 | ```csharp 68 | app.MapPost("/url", ShortenerDelegate); 69 | 70 | static async Task ShortenerDelegate(HttpContext httpContext) 71 | { 72 | var request = await httpContext.Request.ReadFromJsonAsync(); 73 | 74 | if (!Uri.TryCreate(request.Url, UriKind.Absolute, out var inputUri)) 75 | { 76 | httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; 77 | await httpContext.Response.WriteAsync("URL is invalid."); 78 | return; 79 | } 80 | 81 | var liteDb = httpContext.RequestServices.GetRequiredService(); 82 | var links = liteDb.GetCollection(BsonAutoId.Int32); 83 | var entry = new ShortUrl(inputUri); 84 | links.Insert(entry); 85 | 86 | var result = $"{httpContext.Request.Scheme}://{httpContext.Request.Host}/{entry.UrlChunk}"; 87 | await httpContext.Response.WriteAsJsonAsync(new { url = result }); 88 | } 89 | 90 | public class UrlDto 91 | { 92 | public string Url { get; set; } 93 | } 94 | 95 | public class ShortUrl 96 | { 97 | public int Id { get; protected set; } 98 | public string Url { get; protected set; } 99 | public string UrlChunk => WebEncoders.Base64UrlEncode(BitConverter.GetBytes(Id)); 100 | 101 | public ShortUrl(Uri url) 102 | { 103 | Url = url.ToString(); 104 | } 105 | } 106 | ``` 107 | 108 | In the code above, line 1 defines the route `/url` for the POST API, and hooks it up with a request delegate `ShortenerDelegate` to handle HTTP requests. Inside the `ShortenerDelegate` method, we first parse the request body to get the URL and validate its format. Then we resolve the `ILiteDatabase` service from the DI container, and insert an entry into the database. In the end, the delegate method returns the shortened URL as a JSON object. 109 | 110 | We model the short URLs as a class `ShortUrl`, where the `Url` property represents the original URL and the `Id` property is auto generated when inserting into the NoSQL database. The `Id` property ensures the uniqueness of each URL in the local database. In order to generate a short URL chunk, we use the `WebEncoders.Base64UrlEncode()` method to convert from an `Id`, an integer, to the `UrlChunk`, a string. Note that you should reference a `using Microsoft.AspNetCore.WebUtilities;` statement to get the method's namespace. 111 | 112 | For example, when `Id = 1`, the `UrlChunk` is `AQAAAA`; when `Id = 2`, the `UrlChunk` is `AgAAAA`; and so on. In the next section, we will convert the `UrlChunk` back to the `Id` to get its original `Url` from the local database. 113 | 114 | With that, we now have an API endpoint which accepts HTTP POST requests with a JSON body containing a URL string, and returns a JSON object containing a shortened URL string. We can test the API endpoint using Postman. 115 | 116 | ![postman](./postman.png) 117 | 118 | ### 2. Creating an API to Redirect URLs 119 | 120 | Now we are going to support another feature for redirecting short URLs to their original URLs. This API has to cover lots of variations, thus the easiest way to catch all URLs is to use the `MapFallback()` method. Also note that we should place this method after all other routes so that those deterministic routes could be matched first. 121 | 122 | ```csharp 123 | app.MapFallback(RedirectDelegate); 124 | 125 | static async Task RedirectDelegate(HttpContext httpContext) 126 | { 127 | var db = httpContext.RequestServices.GetRequiredService(); 128 | var collection = db.GetCollection(); 129 | 130 | var path = httpContext.Request.Path.ToUriComponent().Trim('/'); 131 | var id = BitConverter.ToInt32(WebEncoders.Base64UrlDecode(path)); 132 | var entry = collection.Find(p => p.Id == id).FirstOrDefault(); 133 | 134 | httpContext.Response.Redirect(entry?.Url ?? "/"); 135 | 136 | await Task.CompletedTask; 137 | } 138 | ``` 139 | 140 | The fallback route is hooked to a `RedirectDelegate`. In this delegate method, we first resolve the `ILiteDatabase` from the DI container and search the short URL from the database. If found, then the API redirects the page to its original URL. Otherwise, the API redirects the page to the app's homepage. 141 | 142 | ### 3. Creating an API to Serve a Static HTML Page 143 | 144 | For this app, we only need a simple HTML page to allow end users to send HTTP requests using an input and a button. Thus we are going to create an `index.html` file and write the user interface there. The web page looks like the following screenshot. 145 | 146 | Note that the `index.html` file needs to be included in this project. 147 | 148 | Once the `index.html` file is ready, we can register the route as follows. 149 | 150 | ```csharp 151 | app.MapGet("/", ctx => 152 | { 153 | ctx.Response.ContentType = "text/html"; 154 | return ctx.Response.SendFileAsync("index.html"); 155 | }); 156 | ``` 157 | 158 | Now we have completed this web application. You can test it out if you have the latest .NET 6 preview version installed. Or we can try out the application using Docker. 159 | 160 | ### 4. Containerizing Our App 161 | 162 | Docker allows us to containerize our applications thus easy to deploy, maintain, and scale. I have prepared a Dockerfile for this app. 163 | 164 | ```bash 165 | docker run -it mcr.microsoft.com/dotnet/sdk:6.0-alpine sh 166 | / # dotnet --info 167 | / # dotnet new web 168 | ``` 169 | 170 | ```bash 171 | docker build -t url-shortener-net6 . 172 | docker run -it --rm -p 8080:80 url-shortener-net6 173 | # then visit http://localhost:8080/ 174 | ``` 175 | 176 | ## License 177 | 178 | Feel free to use the code in this repository as it is under MIT license. 179 | 180 | Buy Me a Coffee at ko-fi.com 181 | -------------------------------------------------------------------------------- /minimal-apis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-labs/Minimal-URL-Shortener/78f9cc7458f8aa753ed0d39a7858cc99b6414d50/minimal-apis.png -------------------------------------------------------------------------------- /postman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-labs/Minimal-URL-Shortener/78f9cc7458f8aa753ed0d39a7858cc99b6414d50/postman.png -------------------------------------------------------------------------------- /src/Program.cs: -------------------------------------------------------------------------------- 1 | using LiteDB; 2 | using Microsoft.AspNetCore.WebUtilities; 3 | 4 | var builder = WebApplication.CreateBuilder(args); 5 | builder.Services.AddSingleton(_ => new LiteDatabase("short-links.db")); 6 | await using var app = builder.Build(); 7 | 8 | if (app.Environment.IsDevelopment()) 9 | { 10 | app.UseDeveloperExceptionPage(); 11 | } 12 | 13 | // Home page: A form for submitting a URL 14 | app.MapGet("/", ctx => 15 | { 16 | ctx.Response.ContentType = "text/html"; 17 | return ctx.Response.SendFileAsync("index.html"); 18 | }); 19 | 20 | // API endpoint for shortening a URL and save it to a local database 21 | app.MapPost("/url", ShortenerDelegate); 22 | 23 | // Catch all page: redirecting shortened URL to its original address 24 | app.MapFallback(RedirectDelegate); 25 | 26 | await app.RunAsync(); 27 | return; 28 | 29 | static async Task ShortenerDelegate(HttpContext httpContext) 30 | { 31 | var request = await httpContext.Request.ReadFromJsonAsync() ; 32 | 33 | if (!Uri.TryCreate(request, UriKind.Absolute, out var inputUri)) 34 | { 35 | httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; 36 | await httpContext.Response.WriteAsync("URL is invalid."); 37 | return; 38 | } 39 | 40 | var liteDb = httpContext.RequestServices.GetRequiredService(); 41 | var links = liteDb.GetCollection(BsonAutoId.Int32); 42 | var entry = new ShortUrl(inputUri); 43 | links.Insert(entry); 44 | 45 | var urlChunk = WebEncoders.Base64UrlEncode(BitConverter.GetBytes(entry.Id)); 46 | var result = $"{httpContext.Request.Scheme}://{httpContext.Request.Host}/{urlChunk}"; 47 | await httpContext.Response.WriteAsJsonAsync(new { url = result }); 48 | } 49 | 50 | static Task RedirectDelegate(HttpContext httpContext) 51 | { 52 | var db = httpContext.RequestServices.GetRequiredService(); 53 | var collection = db.GetCollection(); 54 | 55 | var path = httpContext.Request.Path.ToUriComponent().Trim('/'); 56 | var id = BitConverter.ToInt32(WebEncoders.Base64UrlDecode(path)); 57 | var entry = collection.Find(p => p.Id == id).FirstOrDefault(); 58 | 59 | httpContext.Response.Redirect(entry?.Url ?? "/"); 60 | 61 | return Task.CompletedTask; 62 | } 63 | 64 | internal class ShortUrl(Uri url) 65 | { 66 | public int Id { get; protected set; } 67 | public string Url { get; protected set; } = url.ToString(); 68 | } -------------------------------------------------------------------------------- /src/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "UrlShortener": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development" 8 | }, 9 | "applicationUrl": "https://localhost:60624;http://localhost:60625" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /src/UrlShortener.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | PreserveNewest 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/UrlShortener.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.3.32901.215 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UrlShortener", "UrlShortener.csproj", "{7401B08A-ADF7-4062-8243-CC655278D024}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {7401B08A-ADF7-4062-8243-CC655278D024}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {7401B08A-ADF7-4062-8243-CC655278D024}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {7401B08A-ADF7-4062-8243-CC655278D024}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {7401B08A-ADF7-4062-8243-CC655278D024}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {B9621AD7-85F3-4AE7-9B28-06B86090EDAD} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /src/UrlShortener.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | True -------------------------------------------------------------------------------- /src/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Simple URL Shortener in .NET 8 7 | 8 | 9 | 10 | 11 | 12 |
13 |

Simple URL Shortener in .NET 8

14 |
15 |
16 |
17 | 37 |
38 |
39 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /url-shortener.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-labs/Minimal-URL-Shortener/78f9cc7458f8aa753ed0d39a7858cc99b6414d50/url-shortener.gif --------------------------------------------------------------------------------