Skip to content

Commit

Permalink
Custom response middleware (#52)
Browse files Browse the repository at this point in the history
  • Loading branch information
ttu committed May 22, 2019
1 parent 2678905 commit 348363a
Show file tree
Hide file tree
Showing 9 changed files with 281 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* ADDED: Define stored data id-field name
* CHANGED: Rename Common property from appsettings.json to DataStore
* CHANGED: Remove authentication.json and add Authentication settings to appsettings.json
* ADDED: GraphQL endpoint to support operations in query parameter
* ADDED: Configurable reponse transfrom middleware

### [0.9.1] - 2019-02-08
* CHANGED: Target framework to .NET Core 2.2
Expand Down
17 changes: 17 additions & 0 deletions FakeServer.Test/ObjectHelperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,5 +139,22 @@ public void MessageBus_Subscribe_MessageChanged()

Assert.True(success);
}

[Theory]
[InlineData("[{\"id\":1,\"name\":\"Jam\"es\"}]", "[{\"id\":1,\"name\":\"Jam\"es\"}]")]
public void RemoveLiterals(string original, string expected)
{
var result = ObjectHelper.RemoveLiterals(original);
Assert.Equal(expected, result);
}

[Theory]
[InlineData("/api/users/1/2/3", "users")]
[InlineData("/api/users?hello=test", "users")]
public void GetCollectionFromPath(string request, string expected)
{
var result = ObjectHelper.GetCollectionFromPath(request);
Assert.Equal(expected, result);
}
}
}
18 changes: 18 additions & 0 deletions FakeServer/Common/ObjectHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Dynamic;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;

namespace FakeServer.Common
{
Expand Down Expand Up @@ -199,5 +200,22 @@ public static dynamic TryToCastValue(dynamic value)

return value;
}

public static string RemoveLiterals(string input) => Regex.Replace(input, "[\\\\](?=(\"))", "");

public static string GetCollectionFromPath(string path)
{
try
{
var collection = path.Remove(0, Config.ApiRoute.Length + 2);
collection = collection.IndexOf("/") != -1 ? collection.Remove(collection.IndexOf("/")) : collection;
collection = collection.IndexOf("?") != -1 ? collection.Remove(collection.IndexOf("?")) : collection;
return collection;
}
catch
{
return string.Empty;
}
}
}
}
103 changes: 103 additions & 0 deletions FakeServer/CustomResponse/CustomResponseMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using FakeServer.Common;
using Microsoft.AspNetCore.Http;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace FakeServer.CustomResponse
{
public class Globals
{
public HttpContext _Context;
public string _CollectionId;
public string _Body;
public string _Method;
}

public class CustomResponseMiddleware
{
private readonly RequestDelegate _next;
private readonly CustomResponseSettings _settings;
private readonly List<Script<object>> _scripts;

public CustomResponseMiddleware(RequestDelegate next, IOptions<CustomResponseSettings> settings)
{
_next = next;
_settings = settings.Value;
_scripts = _settings.Scripts.Select(s =>
{
var script = CSharpScript.Create<object>(s.Script,
ScriptOptions.Default
.WithReferences(s.References)
.WithImports(s.Usings),
globalsType: typeof(Globals));
script.Compile();
return script;
}).ToList();
}

public async Task Invoke(HttpContext context)
{
var scriptSettings = _settings.Scripts.LastOrDefault(s =>
s.Methods.Contains(context.Request.Method) && s.Paths.Any(context.Request.Path.Value.Contains));

if (scriptSettings == null)
{
await _next(context);
return;
}

var idx = _settings.Scripts.IndexOf(scriptSettings);
var script = _scripts[idx];

var originalStream = context.Response.Body;

using (var ms = new MemoryStream())
{
context.Response.Body = ms;

await _next(context);

var bodyString = Encoding.UTF8.GetString((context.Response.Body as MemoryStream).ToArray());
var globalObject = new Globals
{
_Context = context,
_CollectionId = ObjectHelper.GetCollectionFromPath(context.Request.Path.Value),
_Body = ObjectHelper.RemoveLiterals(bodyString),
_Method = context.Request.Method
};

var scriptResult = await script.RunAsync(globalObject);

// HACK: Remove string quotemarks from around the _Body
// Original body is e.g. in an array: [{\"id\":1,\"name\":\"Jame\s\"}]
// Script will return new { Data = _Body }
// Script will set Data as a string { Data = "[{\"id\":1,\"name\":\"Jame\s\"}]" }

var jsonCleared = ObjectHelper.RemoveLiterals(JsonConvert.SerializeObject(scriptResult.ReturnValue));

var bodyCleanStart = jsonCleared.IndexOf(globalObject._Body);

if (bodyCleanStart != -1)
{
var bodyCleanEnd = bodyCleanStart + globalObject._Body.Length;

jsonCleared = jsonCleared
.Remove(bodyCleanEnd, 1)
.Remove(bodyCleanStart - 1, 1);
}

var byteArray = Encoding.ASCII.GetBytes(jsonCleared);
var stream = new MemoryStream(byteArray);
await stream.CopyToAsync(originalStream);
}
}
}
}
19 changes: 19 additions & 0 deletions FakeServer/CustomResponse/CustomResponseSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.Collections.Generic;

namespace FakeServer.CustomResponse
{
public class CustomResponseSettings
{
public bool Enabled { get; set; }
public List<ScriptSettings> Scripts { get; set; }
}

public class ScriptSettings
{
public string Script { get; set; }
public List<string> Methods { get; set; }
public List<string> Paths { get; set; }
public List<string> Usings { get; set; }
public List<string> References { get; set; }
}
}
1 change: 1 addition & 0 deletions FakeServer/FakeServer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="3.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="2.2.0" />
<PackageReference Include="Serilog.AspNetCore" Version="2.1.1" />
Expand Down
7 changes: 7 additions & 0 deletions FakeServer/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using FakeServer.Authentication.Custom;
using FakeServer.Authentication.Jwt;
using FakeServer.Common;
using FakeServer.CustomResponse;
using FakeServer.GraphQL;
using FakeServer.Jobs;
using FakeServer.Simulate;
Expand Down Expand Up @@ -54,6 +55,7 @@ public void ConfigureServices(IServiceCollection services)
services.Configure<DataStoreSettings>(Configuration.GetSection("DataStore"));
services.Configure<JobsSettings>(Configuration.GetSection("Jobs"));
services.Configure<SimulateSettings>(Configuration.GetSection("Simulate"));
services.Configure<CustomResponseSettings>(Configuration.GetSection("CustomResponse"));

services.AddCors(options =>
{
Expand Down Expand Up @@ -138,6 +140,11 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplica
app.UseMiddleware<ErrorMiddleware>();
}

if (Configuration.GetValue<bool>("CustomResponse:Enabled"))
{
app.UseMiddleware<CustomResponseMiddleware>();
}

app.UseWebSockets();

app.UseMiddleware<NotifyWebSocketMiddlerware>();
Expand Down
12 changes: 12 additions & 0 deletions FakeServer/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,17 @@
"Methods": [ "POST", "PUT", "PATCH", "DELETE" ],
"Probability": 50
}
},
"CustomResponse": {
"Enabled": false,
"Scripts": [
{
"Script": "return new { Data = _Body, Success = _Context.Response.StatusCode == 200 };",
"Methods": [ "GET" ],
"Paths": [ "api" ],
"Usings": [ "System", "Microsoft.AspNetCore.Http" ],
"References": [ "Microsoft.AspNetCore" ]
}
]
}
}
102 changes: 102 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Fake JSON Server is a Fake REST API that can be used as a Back End for prototypi
* Swagger [#](#swagger)
* CORS [#](#cors)
* Caching and avoiding mid-air collisions with ETag [#](#caching-and-avoiding-mid-air-collisions-with-etag)
* Configurable custom response transformation [#](#custom-response-transformation)
* _Experimental_ GraphQL query and mutation support [#](#graphql)

##### Developed with
Expand Down Expand Up @@ -109,6 +110,7 @@ Fake JSON Server is a Fake REST API that can be used as a Back End for prototypi
- [Replace item](#replace-item-1)
- [Delete item](#delete-item-1)
* [Simulate Delay and Random Errors](#simulate-delay-and-random-errors)
* [Configurable Custom Response Transformation](#custom-response-transformation)
- [Logging](#logging)
- [Guidelines](#guidelines)
- [Other Links](#other-links)
Expand Down Expand Up @@ -1368,6 +1370,106 @@ Random errors can be simulated by setting `Simulate.Error.Enabled` to _true_. Er

Error simulation is always skipped for Swagger, WebSocket (ws) and for any html file.

### Custom Response Transformation

Fake Server has a custom response middleware to transform reponse body with C#-scripts.

Multiple scripts can be configured and if path matches multiple scipts, last match will be used.

```json
"CustomResponse": {
"Enabled": true,
"Scripts": [
{
"Script": "return new { Data = _Body, Success = _Context.Response.StatusCode == 200 };",
"Methods": [ "GET" ],
"Paths": [ "api" ],
"Usings": [ "System", "Microsoft.AspNetCore.Http" ],
"References": [ "Microsoft.AspNetCore" ]
},
{
"Script": "return new { Data = \"illegal operation\" };",
"Methods": [ "GET" ],
"Paths": [ "api/users" ],
"Usings": [ "System", "Microsoft.AspNetCore.Http" ],
"References": [ "Microsoft.AspNetCore" ]
}
]
}
```

C# code is executed as a csscript and it has some special reacy processed objects.

```csharp
// HttpContext
public HttpContext _Context;
// Collection id parsed from the Request path
public string _CollectionId;
// Original Response Body encoded to string
public string _Body;
// Request Http Method
public string _Method;
```

Example script creates new anonymous object
```csahrp
return new { Data = _Body, Success = _Context.Response.StatusCode == 200 };
```

Previous script will have a response body:

```json
{
"Data": [
{ "id": 1, "name": "James" ...},
{ "id": 2, "name": "Phil", ... },
...
],
"Success": true
}
```

If response data requires so dynamically named properties, e.g. `users` in the example, then response requires more complex processing.

```json
{
"Data": {
"users": [
{ "id": 1, "name": "James" ...},
{ "id": 2, "name": "Phil", ... },
...
]
},
"Success": true
}
```

C#-code for the processing would be following:

```csharp
var data = new ExpandoObject();
var dataItems = data as IDictionary<string, object>;
dataItems.Add(_CollectionId, _Body);

var body = new ExpandoObject();
var items = body as IDictionary<string, object>;
items.Add("Data", data);
items.Add("Success", _Context.Response.StatusCode == 200);
return body;
```

Script also would need `System.Collections.Generic` and `System.Dynamic` as imports.

```json
{
"Script": "var data = new ExpandoObject();var dataItems = data as IDictionary<string, object>;dataItems.Add(_CollectionId, _Body);var body = new ExpandoObject();var items = body as IDictionary<string, object>;items.Add(\"Data\", data);items.Add(\"Success\", _Context.Response.StatusCode == 200);return body;",
"Methods": [ "GET" ],
"Paths": [ "api" ],
"Usings": [ "System", "System.Dynamic", "System.Collections.Generic", "Microsoft.AspNetCore.Http" ],
"References": [ "Microsoft.AspNetCore" ]
}
```

## Logging

Fake JSON Server writes a log file to the application base path (execution folder).
Expand Down

0 comments on commit 348363a

Please sign in to comment.