├── .gitignore ├── HelloMiddleware.csproj ├── MockMiddleware.cs ├── Program.cs ├── README.md ├── ToJson.cs ├── appsettings.Development.json ├── appsettings.json ├── article.md └── mock.json /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj 3 | article 4 | Properties 5 | .env -------------------------------------------------------------------------------- /HelloMiddleware.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /MockMiddleware.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Nodes; 3 | 4 | namespace Mock; 5 | 6 | public static class RouteMiddlewareExtensions 7 | { 8 | public static WebApplication UseExtraRoutes(this WebApplication app) 9 | { 10 | var writableDoc = JsonNode.Parse(File.ReadAllText("mock.json")); 11 | 12 | // print API 13 | foreach (var elem in writableDoc?.Root.AsObject().AsEnumerable()){ 14 | Console.WriteLine(string.Format("GET /{0}", elem.Key.ToLower())); 15 | Console.WriteLine(string.Format("GET /{0}", elem.Key.ToLower()) + "/id"); 16 | Console.WriteLine(string.Format("POST /{0}", elem.Key.ToLower())); 17 | Console.WriteLine(string.Format("DELETE /{0}", elem.Key.ToLower()) + "/id"); 18 | Console.WriteLine(" "); 19 | } 20 | 21 | // setup routes 22 | foreach(var elem in writableDoc?.Root.AsObject().AsEnumerable()) { 23 | var arr = elem.Value.AsArray(); 24 | app.MapGet(string.Format("/{0}", elem.Key), () => elem.Value.ToString()); 25 | app.MapGet(string.Format("/{0}", elem.Key) + "/{id}", (int id) => 26 | { 27 | var matchedItem = arr.SingleOrDefault(row => row 28 | .AsObject() 29 | .Any(o => o.Key.ToLower() == "id" && int.Parse(o.Value.ToString()) == id) 30 | ); 31 | return matchedItem; 32 | }); 33 | app.MapPost(string.Format("/{0}", elem.Key), async (HttpRequest request) => { 34 | string content = string.Empty; 35 | using(StreamReader reader = new StreamReader(request.Body)) 36 | { 37 | content = await reader.ReadToEndAsync(); 38 | } 39 | var newNode = JsonNode.Parse(content); 40 | var array = elem.Value.AsArray(); 41 | newNode.AsObject().Add("Id", array.Count() + 1); 42 | array.Add(newNode); 43 | 44 | File.WriteAllText("mock.json", writableDoc.ToString()); 45 | return content; 46 | }); 47 | app.MapPut(string.Format("/{0}", elem.Key), () => { 48 | return "TODO"; 49 | }); 50 | app.MapDelete(string.Format("/{0}", elem.Key) + "/{id}", (int id) => { 51 | 52 | var matchedItem = arr 53 | .Select((value, index) => new{ value, index}) 54 | .SingleOrDefault(row => row.value 55 | .AsObject() 56 | .Any(o => o.Key.ToLower() == "id" && int.Parse(o.Value.ToString()) == id) 57 | ); 58 | if (matchedItem != null) { 59 | arr.RemoveAt(matchedItem.index); 60 | File.WriteAllText("mock.json", writableDoc.ToString()); 61 | } 62 | 63 | return "OK"; 64 | }); 65 | 66 | }; 67 | 68 | return app; 69 | } 70 | } -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | using Mock; 2 | var builder = WebApplication.CreateBuilder(args); 3 | var app = builder.Build(); 4 | 5 | app.MapGet("/", () => "Hello World!"); 6 | app.UseExtraRoutes(); 7 | 8 | app.Run(); 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mock-sharp 2 | 3 | Mock API for minimal API, ASP .NET, written in C# 4 | 5 | ## Run 6 | 7 | Type `dotnet run` 8 | 9 | ```bash 10 | dotnet run 11 | ``` 12 | 13 | This should show the supported routes in the terminal 14 | 15 | ## NuGet package 16 | 17 | Coming... 18 | 19 | ## Features 20 | 21 | Given a mock file, (for now it's hardcoded as *mock.json*), with the following JSON content: 22 | 23 | ```json 24 | { 25 | "Products": [ 26 | { 27 | "Id": 1, 28 | "Name": "Mock" 29 | }, 30 | { 31 | "Id": 2, 32 | "Name": "Second Mock" 33 | } 34 | ] 35 | } 36 | ``` 37 | 38 | it will create the below routes: 39 | 40 | |Verb |Route | Description | 41 | |---------|---------|---------| 42 | |GET | /products | fetches a list of products | 43 | |GET | /products/{id} | fetches one product, given unique identifier `id` | 44 | |POST | /products | creates a new product, assumes a JSON represenation is sent via the BODY | 45 | |DELETE | /products/{id} | deletes one product, given unique identifier `id` | 46 | |PUT | | Coming.. | 47 | 48 | ### Query parameters 49 | 50 | Coming 51 | 52 | ## Mock data 53 | 54 | mock data is in *mock.json*. Here's an example of what it could look like: 55 | 56 | ```json 57 | { 58 | "Products": [ 59 | { 60 | "Id": 1, 61 | "Name": "Mock" 62 | }, 63 | { 64 | "Id": 2, 65 | "Name": "Second Mock" 66 | } 67 | ], 68 | "Orders": [ 69 | { 70 | "Id": 1, 71 | "Name": "Order1" 72 | }, 73 | { 74 | "Id": 2, 75 | "Name": "Second Order" 76 | } 77 | ] 78 | } 79 | ``` 80 | -------------------------------------------------------------------------------- /ToJson.cs: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /article.md: -------------------------------------------------------------------------------- 1 | > TLDR; this article describes how to create a Mock API from a JSON file for minimal API in ASP .NET 2 | 3 | ## What and why Mock APIs 4 | 5 | To mock something means you respond with some kind of fake data, that data can be in-memory, from a file or soome kind of tool generating a bunch of endpoints. There are some reasons why mocking an API could be a good idea: 6 | 7 | - **Different teams work at different speeds**. Lets say your app is built by two different teams, or developers and one is faster than the other. Typically that's the frontend team being the faster one (not always) cause there's a lot to wire up in the backend. 8 | - **You start with the frontend first**. Your team/developer have maybe decided to build a full vertical and starts with the frontend and slowly work their way towards the backend and the data source. 9 | 10 | Ok, so we established there might be a need to mock your API. So how do we do it? You want to be able to specify the data you want to mock and there are some formats out there that makes sense to have such mocked data in like JSON, XML or CSV perhaps. For the sake of this article, we will go with JSON 11 | 12 | ## Planning our project, what we need to do 13 | 14 | Ok, so high-level, we need to do the following: 15 | - **Create a file in JSON**, containing our routes and the response. We imagine the JSON file looking something like so: 16 | 17 | ```json 18 | { 19 | "Products": [ 20 | { 21 | "Id": 1, 22 | "Name": "Mock" 23 | }, 24 | { 25 | "Id": 2, 26 | "Name": "Second Mock" 27 | } 28 | ] 29 | } 30 | ``` 31 | 32 | - **What routes do we want?** A good API should implement GET, POST, PUT and DELETE to support a RESTful approach. 33 | - **Responding to changes.** So what should happen if the user actually calls POST, PUT or DELETE? Reasonably, the mocked file should change. 34 | 35 | Ok, so we know high-level what we need, and how things should behave, lets see if we can choose our technical approach next. 36 | 37 | ## Approach - let's create the solution 38 | 39 | The normal way to setup routes, in Minimal API, is to call code like so: 40 | 41 | ```csharp 42 | app.MapGet("/", () => "Hello World!"); 43 | ``` 44 | 45 | By calling `MapGet()` we create a route to "/" that when called responds with "Hello World". For the sake of our API, we will have to call `MapGet()`, `MapPost()`, `MapPut()` and `MapDelete()`. 46 | 47 | > Here be dragons. Many of you, I'm sure, are used to working with JSON in a typed manor, meaning you are likely to create types for your classes and rely on methods like `Deserialize()` and `Serialize()`. That's a great approach, however, for a mocked API that doesn't even exist yet, this code doesn't rely on any of that :) 48 | 49 | ### Defining the routes, making it loosely coupled 50 | 51 | It would be neat if these routes were loosely coupled code that we could just bring in, when developing, and removed when we are live with our app. 52 | 53 | When `app.MapGet()` was called, it invoked an instance of the class `WebApplication`. By creating an extension method on said class, we have a way an approach to add code in a way that it's nicely separated. We also need a static class to put said extension method in. That means our code starting out should look something like so: 54 | 55 | ```csharp 56 | public static class RouteMiddlewareExtensions 57 | { 58 | public static WebApplication UseExtraRoutes(this WebApplication app) 59 | { 60 | } 61 | } 62 | ``` 63 | 64 | ## Exercise - Read from a mock file, and add support for `GET` 65 | 66 | Ok, we know how we are starting, a static class and an extension method, so lets make that happen: 67 | 68 | 1. Run `command`, to generate a new minimal API project 69 | 70 | ```bash 71 | TODO 72 | cd 73 | ``` 74 | 75 | 1. Create a file *MockMiddleware.cs* and give it the following code: 76 | 77 | ```csharp 78 | using System.Text.Json; 79 | using System.Text.Json.Nodes; 80 | 81 | public static class RouteMiddlewareExtensions 82 | { 83 | public static WebApplication UseExtraRoutes(this WebApplication app) 84 | { 85 | } 86 | } 87 | ``` 88 | 89 | 1. Add code to read a JSON file into a JSON representation: 90 | 91 | ```csharp 92 | var writableDoc = JsonNode.Parse(File.ReadAllText("mock.json")); 93 | ``` 94 | 95 | Note the choice of JsonNode, this is so we can make the JSON doc writable, which we will need for POST, PUT and DELETE laterr on. 96 | 97 | 1. Create the file *mock.json* and give it the following content: 98 | 99 | ```json 100 | { 101 | "Products": [ 102 | { 103 | "Id": 1, 104 | "Name": "Mock" 105 | }, 106 | { 107 | "Id": 2, 108 | "Name": "Second Mock" 109 | } 110 | ], 111 | "Orders": [ 112 | { 113 | "Id": 1, 114 | "Name": "Order1" 115 | }, 116 | { 117 | "Id": 2, 118 | "Name": "Second Order" 119 | } 120 | ] 121 | } 122 | ``` 123 | 124 | ### Add `GET` 125 | 126 | Lets support our first HTTP verb, GET. 127 | 128 | 1. Add the following code: 129 | 130 | ```csharp 131 | foreach(var elem in writableDoc?.Root.AsObject().AsEnumerable()) { 132 | var arr = elem.Value.AsArray(); 133 | app.MapGet(string.Format("/{0}", elem.Key), () => elem.Value.ToString()); 134 | } 135 | ``` 136 | 137 | In the above code, we navigate into the root object. Then, we convert it to an object representation and starts iterating over the keys, according to the mock file, that means `Products` and `Orders`. Lastly, we setup the route and the callback, the route is at `elem.Key` and the value we want to return is at `elem.Value`. 138 | 139 | 1. In the file *Program.cs* add the following line: 140 | 141 | ```csharp 142 | app.UseExtraRoutes(); 143 | ``` 144 | 145 | The preceding code will ensure our routes are added to the app. 146 | 147 | 1. Run `dotnet run`, to run the app 148 | 149 | ```bash 150 | dotnet run 151 | ``` 152 | 153 | 1. Navigate to the port indicated in the console outputt and navigate to `/products` and `/orders`, they should both show an output 154 | 155 | ### Add `GET` by id 156 | 157 | Ok, you got the basic GET case to work, what about filtering the data with parameter. Using `/products/1`, should just return one record back. How do we do that? 158 | 159 | 1. Add the following code in the foreach loop in *MockMiddlware.cs*: 160 | 161 | ```csharp 162 | app.MapGet(string.Format("/{0}", elem.Key) + "/{id}", (int id) => 163 | { 164 | var matchedItem = arr.SingleOrDefault(row => row 165 | .AsObject() 166 | .Any(o => o.Key.ToLower() == "id" && int.Parse(o.Value.ToString()) == id) 167 | ); 168 | return matchedItem; 169 | }); 170 | ``` 171 | 172 | The above code is iterating over the rows for a specific route and looks for an `id` property that matches our `{id}` pattern. The found item is returned back. 173 | 174 | 1. Run `dotnet run` to test out this code: 175 | 176 | ```bash 177 | dotnet run 178 | ``` 179 | 180 | 1. Navigate to `/products/1`, you should see the following JSON output: 181 | 182 | ```json 183 | { 184 | 185 | "Id": 1, 186 | "Name": "Mock" 187 | } 188 | ``` 189 | 190 | Great, we got it to work. 191 | 192 | ## Exercise - write data 193 | 194 | Now that we can read data from our mock api, lets tackle writing data. The fact that we were `JsonNode.Parse()` in the beginning makes it possible for use to use operations on the `JsonNode` instance. In short, our approach will be: 195 | 196 | - find the specific place in the `JsonNode`, that represents our mock data, and change it 197 | - save down the whole `JsonNode` instance to our *mock.json*. If the user uses an operation to change the data, that should be reflected in the Mock file. 198 | 199 | ### Add `POST` 200 | 201 | To implement this route, we will use `MapPost()` but we can't just give it a typed object in the callback for the route, because we don't know what it looks like. Instead, we will use the request object, read the body and add that to the `JsonNode`. 202 | 203 | 1. Add following code to support `POST`: 204 | 205 | ```csharp 206 | app.MapPost(string.Format("/{0}", elem.Key), async (HttpRequest request) => { 207 | string content = string.Empty; 208 | using(StreamReader reader = new StreamReader(request.Body)) 209 | { 210 | content = await reader.ReadToEndAsync(); 211 | } 212 | var newNode = JsonNode.Parse(content); 213 | var array = elem.Value.AsArray(); 214 | newNode.AsObject().Add("Id", array.Count() + 1); 215 | array.Add(newNode); 216 | 217 | File.WriteAllText("mock.json", writableDoc.ToString()); 218 | return content; 219 | }); 220 | ``` 221 | 222 | In the above code, we have `request` as input parameter to our route handler function. 223 | 224 | ```csharp 225 | app.MapPost(string.Format("/{0}", elem.Key), async (HttpRequest request) => {}); 226 | ``` 227 | 228 | Then we read the body, using a `StreamReader`. 229 | 230 | ```csharp 231 | using(StreamReader reader = new StreamReader(request.Body)) 232 | { 233 | content = await reader.ReadToEndAsync(); 234 | } 235 | ``` 236 | 237 | Next, we construct a JSON representation from our received BODY: 238 | 239 | ```csharp 240 | var newNode = JsonNode.Parse(content); 241 | ``` 242 | 243 | This is followed by locating the place to insert this new JSON and adding it: 244 | 245 | ```csharp 246 | var array = elem.Value.AsArray(); 247 | newNode.AsObject().Add("Id", array.Count() + 1); 248 | array.Add(newNode); 249 | ``` 250 | 251 | Lastly, we update the mock file and respond something back to the calling client: 252 | 253 | ```csharp 254 | File.WriteAllText("mock.json", writableDoc.ToString()); 255 | return content; 256 | ``` 257 | 258 | ### Add `DELETE` 259 | 260 | To support deletion, we need a very similar approach to how we located an entry by id parameter. We also need to locate where to delete in the `JsonObject`. 261 | 262 | 1. Add the following code to support delete: 263 | 264 | ```csharp 265 | app.MapDelete(string.Format("/{0}", elem.Key) + "/{id}", (int id) => { 266 | var matchedItem = arr 267 | .Select((value, index) => new{ value, index}) 268 | .SingleOrDefault(row => row.value 269 | .AsObject() 270 | .Any(o => o.Key.ToLower() == "id" && int.Parse(o.Value.ToString()) == id) 271 | ); 272 | if (matchedItem != null) { 273 | arr.RemoveAt(matchedItem.index); 274 | File.WriteAllText("mock.json", writableDoc.ToString()); 275 | } 276 | 277 | return "OK"; 278 | }); 279 | ``` 280 | 281 | First, we find the item in question, but we also make sure that we know what the index of the found item is. We will use this index later to remove the item. Hence, we get the following code: 282 | 283 | ```csharp 284 | var matchedItem = arr 285 | .Select((value, index) => new{ value, index}) 286 | .SingleOrDefault(row => row.value 287 | .AsObject() 288 | .Any(o => o.Key.ToLower() == "id" && int.Parse(o.Value.ToString()) == id) 289 | ); 290 | ``` 291 | 292 | Our `matchedItem` now contains either NULL or an object that has an `index` property. Using this `index` property, we will be able to perform deletions: 293 | 294 | ```csharp 295 | if (matchedItem != null) { 296 | arr.RemoveAt(matchedItem.index); 297 | File.WriteAllText("mock.json", writableDoc.ToString()); 298 | } 299 | ``` 300 | 301 | To test writes, use something like Postman or Advanced REST client, it should work. 302 | 303 | ### Add route info 304 | 305 | We're almost done, as courtesy towards the programmer using this code, we want to print out what routes we have and support so it's easy to know what we support. 306 | 307 | 1. Add this code, just at the start of the method `UseExtraRoutes()`: 308 | 309 | ```csharp 310 | // print API 311 | foreach (var elem in writableDoc?.Root.AsObject().AsEnumerable()){ 312 | Console.WriteLine(string.Format("GET /{0}", elem.Key.ToLower())); 313 | Console.WriteLine(string.Format("GET /{0}", elem.Key.ToLower()) + "/id"); 314 | Console.WriteLine(string.Format("POST /{0}", elem.Key.ToLower())); 315 | Console.WriteLine(string.Format("DELETE /{0}", elem.Key.ToLower()) + "/id"); 316 | Console.WriteLine(" "); 317 | } 318 | ``` 319 | 320 | That's it, that's all we intend to implement. Hopefully this is all useful to you and you will be able to use it next you just want an API up and running that you can build a front end app off of. 321 | 322 | ### Full code 323 | 324 | If you got lost at any point, here's the full code: 325 | 326 | *Program.cs* 327 | 328 | ```csharp 329 | using Mock; 330 | var builder = WebApplication.CreateBuilder(args); 331 | var app = builder.Build(); 332 | 333 | app.MapGet("/", () => "Hello World!"); 334 | app.UseExtraRoutes(); // this is where our routes gets added 335 | 336 | app.Run(); 337 | ``` 338 | 339 | *MockMiddleware.cs* 340 | 341 | ```csharp 342 | using System.Text.Json; 343 | using System.Text.Json.Nodes; 344 | 345 | namespace Mock; 346 | 347 | public static class RouteMiddlewareExtensions 348 | { 349 | public static WebApplication UseExtraRoutes(this WebApplication app) 350 | { 351 | var writableDoc = JsonNode.Parse(File.ReadAllText("mock.json")); 352 | 353 | // print API 354 | foreach (var elem in writableDoc?.Root.AsObject().AsEnumerable()){ 355 | Console.WriteLine(string.Format("GET /{0}", elem.Key.ToLower())); 356 | Console.WriteLine(string.Format("GET /{0}", elem.Key.ToLower()) + "/id"); 357 | Console.WriteLine(string.Format("POST /{0}", elem.Key.ToLower())); 358 | Console.WriteLine(string.Format("DELETE /{0}", elem.Key.ToLower()) + "/id"); 359 | Console.WriteLine(" "); 360 | } 361 | 362 | // setup routes 363 | foreach(var elem in writableDoc?.Root.AsObject().AsEnumerable()) { 364 | var arr = elem.Value.AsArray(); 365 | app.MapGet(string.Format("/{0}", elem.Key), () => elem.Value.ToString()); 366 | app.MapGet(string.Format("/{0}", elem.Key) + "/{id}", (int id) => 367 | { 368 | var matchedItem = arr.SingleOrDefault(row => row 369 | .AsObject() 370 | .Any(o => o.Key.ToLower() == "id" && int.Parse(o.Value.ToString()) == id) 371 | ); 372 | return matchedItem; 373 | }); 374 | app.MapPost(string.Format("/{0}", elem.Key), async (HttpRequest request) => { 375 | string content = string.Empty; 376 | using(StreamReader reader = new StreamReader(request.Body)) 377 | { 378 | content = await reader.ReadToEndAsync(); 379 | } 380 | var newNode = JsonNode.Parse(content); 381 | var array = elem.Value.AsArray(); 382 | newNode.AsObject().Add("Id", array.Count() + 1); 383 | array.Add(newNode); 384 | 385 | File.WriteAllText("mock.json", writableDoc.ToString()); 386 | return content; 387 | }); 388 | app.MapPut(string.Format("/{0}", elem.Key), () => { 389 | return "TODO"; 390 | }); 391 | app.MapDelete(string.Format("/{0}", elem.Key) + "/{id}", (int id) => { 392 | 393 | var matchedItem = arr 394 | .Select((value, index) => new{ value, index}) 395 | .SingleOrDefault(row => row.value 396 | .AsObject() 397 | .Any(o => o.Key.ToLower() == "id" && int.Parse(o.Value.ToString()) == id) 398 | ); 399 | if (matchedItem != null) { 400 | arr.RemoveAt(matchedItem.index); 401 | File.WriteAllText("mock.json", writableDoc.ToString()); 402 | } 403 | 404 | return "OK"; 405 | }); 406 | 407 | }; 408 | 409 | return app; 410 | } 411 | } 412 | ``` 413 | 414 | ### Update - homework 415 | 416 | For your homework, see if you can implement PUT. :) 417 | 418 | ## Summary 419 | 420 | I took you through a journey of implementing a Mock API for minimal APIs. Hopefully you found this useful and is able to use it in a future project. 421 | -------------------------------------------------------------------------------- /mock.json: -------------------------------------------------------------------------------- 1 | { 2 | "Products": [ 3 | { 4 | "Id": 1, 5 | "Name": "Mock" 6 | }, 7 | { 8 | "Id": 2, 9 | "Name": "Second Mock" 10 | } 11 | ], 12 | "Orders": [ 13 | { 14 | "Id": 1, 15 | "Name": "Order1" 16 | }, 17 | { 18 | "Id": 2, 19 | "Name": "Second Order" 20 | } 21 | ] 22 | } --------------------------------------------------------------------------------