Skip to content

Commit

Permalink
Merge pull request #82 from InRule/issue/CAD-327-rest-operation-overr…
Browse files Browse the repository at this point in the history
…ides

CAD-327: Allow overriding the headers and body on a REST operation
  • Loading branch information
inrulejones authored Apr 13, 2022
2 parents 10db1d4 + 12cca57 commit 80eda44
Show file tree
Hide file tree
Showing 16 changed files with 923 additions and 40 deletions.

This file was deleted.

185 changes: 185 additions & 0 deletions Developer Samples/RuntimeOverrides/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
# Runtime Overrides

Runtime overrides may override End Point and Data Element settings that differ from what was originally authored in a Rule Application.
This can be useful when deploying an application to different environments.

Overrides may be specified either using irSDK from the `RuleSession.Overrides` property, or via .config file `<appSettings/>`.

The following End Points may be overridden:
- Database Connection
- Rest Service
- Web Service
- SendMail Service
- XML Schema

The following Data Elements may be overridden:
- Inline Value List
- Inline Table
- Inline XML Document
- SQL Query
- XPath Query
- REST Operation

---
## irSDK Override Mechanism
When creating a RuleSession for executing rules, overrides may be specified prior to execution.

For example:

```
using (RuleSession session = new RuleSession(ruleApp))
{
session.Overrides.OverrideRestOperationUriTemplate("operation1", "api/v2/invoice");
session.CreateEntity("Invoice");
session.ApplyRules();
}
```

---
## Configuration Override Mechanism
Using the `<appSettings/>` in the application's .config file, overrides may be applied declaratively instead of adding irSDK code.

These `<appSettings/>` may be extended using [Configuration Builders](https://github.com/aspnet/MicrosoftConfigurationBuilders) to read configuration from external sources such as Environment Variables, JSON files, or Azure KeyVault.

The `<appSettings/>` follow a key/value pattern. The key is the identifier of the override, and the value is the new value for the Runtime to use.

For example:

```
<configuration>
<appSettings>
<add key="inrule:runtime:overrides:operation1:RestOperation:uritemplate" value="api/v2/test" />
</appSettings>
</configuration>
```

This illustrates the equivalent of the irSDK override example listed above.

### Key Format
The appSetting key follows the following format:

`inrule:runtime:overrides:<name>:<type>:<property>`

- `<name>` is the name of the End Point or Data Element
- `<type>` is the type of End Point or Data Element (see list below)
- `<property>` is the property of the type being overridden (see list below)

**Note: These overrides will apply to End Points and Data Elements with the same name across different Rule Applications.**

The `<name>` component is **case-sensitive**.

The `<type>` and `<property>` components are **not case-sensitive**. The combination of type/property may be selected from the following:

### End Points
| Type | Property |
| ---- | -------- |
| DatabaseConnection | ConnectionString |
| SendMailServer | ServerAddress |
| XmlDocument | XmlPath |
| XmlSchema | XsdPath |
| | EnableXsdValidation |
| WebService | WsdlUrl |
| | ServiceUriOverride |
| | MaxReceivedMessageSize |
| RestService | RootUrl |
| | AllowUntrustedCertificates |
| | AuthenticationType |
| | Username |
| | Password |
| | Domain |
| | X509CertificatePath |
| | X509CertificatePassword |

### Data Elements

| Name | Property |
| ---- | -------- |
| ValueList | ValueListItems* |
| Table | TableSettings* |
| XPathDocument | InlineXml |
| SqlQuery | Query |
| RestOperation | UriTemplate |
| | Body |
| | Headers* |

_*see special handling below_

**Note: All appSetting values must be HTML-safe, so any values containing characters such as '&', '<', '>', '\\"' should first be passed through an HTML encoder.**

For example:

```
const string unencodedValue = @"<InlineXml><Value1>This is a \"test\" & should be encoded</Value1></InlineXml>";
Console.WriteLine("Unencoded Value: " + unencodedValue);
Console.WriteLine("Encoded Value: " + System.Net.WebUtility.HtmlEncode(unencodedValue));
```

The above should output the following:

```
Unencoded Value: <InlineXml><Value1>This is a "test" & should be encoded</Value1></InlineXml>
Encoded Value: &lt;InlineXml&gt;&lt;Value1&gt;This is a &quot;test&quot; &amp; should be encoded&lt;/Value1&gt;&lt;/InlineXml&gt;
```

#### ValueListItems Serialization
The value for ValueListItems uses a custom XML format, which must be HTML encoded for use as the value.

For example:

```
const string valueListItems =
@"<ValueListItems>
<ValueListItem>
<Value>Value1</Value>
<DisplayText>Value One</DisplayText>
</ValueListItem>
<ValueListItem>
<Value>Value2</Value>
</ValueListItem>
<ValueListItem>
<Value>Value3</Value>
</ValueListItem>
</ValueListItems>";
var value = System.Net.WebUtility.HtmlEncode(valueListItems);
```

#### TableSettings Serialization
The value of TableSettings configuration must use irSDK to XML Serialize an instance of `InRule.Repository.TableSettings`. This must then be HTML encoded for use as the value.

For example:

```
var tableSettings = ruleAppDef.DataElements["table1"].TableSettings;
var xs = new XmlSerializer(typeof("InRule.Repository.TableSettings));
var sb = new StringBuilder();
using (var writer = new StringWriter(sb))
{
xs.Serialize(writer, tableSettings);
}
var value = System.Net.WebUtility.HtmlEncode(sb.ToString());
```

#### REST Operation Headers
The Headers are a collection of name/value pairs. Any number of headers may be applied. To simulate a collection, the appSetting key is extended to include the header name.

For example:
```
<configuration>
<appSettings>
<add key="inrule:runtime:overrides:operation1:RestOperation:Headers:Header1" value="value1" />
<add key="inrule:runtime:overrides:operation1:RestOperation:Headers:Header2" value="value2" />
<add key="inrule:runtime:overrides:operation1:RestOperation:Headers:Header3" value="value3" />
<add key="inrule:runtime:overrides:operation1:RestOperation:Headers:Connection" value="keep-alive" />
<add key="inrule:runtime:overrides:operation1:RestOperation:Headers:Accept-Language" value="en-US" />
</appSettings>
</configuration>
```

---
## Samples
The 2 samples illustrate how the REST operation may be overridden by both irSDK and .config `<appSettings/>`:
- [RuntimeOverridesViaSdk](RuntimeOverridesViaSdk/)
- [RuntimeOverridesViaConfig](RuntimeOverridesViaConfig/)
36 changes: 36 additions & 0 deletions Developer Samples/RuntimeOverrides/RuntimeOverrides.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.32126.317
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RuntimeOverridesViaSdk", "RuntimeOverridesViaSdk\RuntimeOverridesViaSdk.csproj", "{5E9273F5-548A-4813-9499-5201B2160060}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RuntimeOverridesViaConfig", "RuntimeOverridesViaConfig\RuntimeOverridesViaConfig.csproj", "{AD7153D5-AD79-4928-AF2E-41CCAD584EE5}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{55B8A9D2-B845-469F-B26D-EAAB2866E33F}"
ProjectSection(SolutionItems) = preProject
README.md = README.md
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{5E9273F5-548A-4813-9499-5201B2160060}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5E9273F5-548A-4813-9499-5201B2160060}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5E9273F5-548A-4813-9499-5201B2160060}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5E9273F5-548A-4813-9499-5201B2160060}.Release|Any CPU.Build.0 = Release|Any CPU
{AD7153D5-AD79-4928-AF2E-41CCAD584EE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AD7153D5-AD79-4928-AF2E-41CCAD584EE5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AD7153D5-AD79-4928-AF2E-41CCAD584EE5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AD7153D5-AD79-4928-AF2E-41CCAD584EE5}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FCBB9BA7-F634-45F4-9797-72E3AA9A6F7A}
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>

<appSettings>
<add key="inrule:runtime:overrides:operation1:RestOperation:uritemplate" value="api/vConfig/test" />
<add key="inrule:runtime:overrides:operation1:RestOperation:body" value="{ 'Value': 'configBody' }" />
<add key="inrule:runtime:overrides:operation1:RestOperation:headers:Header1" value="configHeader1" />
<add key="inrule:runtime:overrides:operation1:RestOperation:headers:Header2" value="configHeader2" />
<add key="inrule:runtime:overrides:operation1:RestOperation:headers:Header3" value="configHeader3" />
<add key="inrule:runtime:overrides:operation1:RestOperation:headers:User-Agent" value="Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:50.0) Gecko/20100101 Firefox/50.0" />
</appSettings>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-13.0.0.0" newVersion="13.0.0.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Data.SQLite" publicKeyToken="db937bc2d44ff139" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-1.0.115.5" newVersion="1.0.115.5" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using System;
using System.Net;

namespace RuntimeOverridesViaConfig
{
public sealed class InProcessWebServer : IDisposable
{
private const int HttpPort = 9595;

private readonly Action<HttpListenerContext> _processRequest;
private readonly HttpListener _httpListener;

public InProcessWebServer(Action<HttpListenerContext> processRequest)
{
_processRequest = processRequest;
_httpListener = new HttpListener { AuthenticationSchemes = AuthenticationSchemes.Anonymous };
_httpListener.Prefixes.Add(RootUrl);
_httpListener.Start();
Result = _httpListener.BeginGetContext(WebRequestCallback, _httpListener);
}

public static string RootUrl => $"http://localhost:{HttpPort}/";

public IAsyncResult Result { get; private set; }

public HttpListenerRequest Request { get; private set; }

public void Dispose()
{
if (_httpListener == null) return;

lock (_httpListener)
{
try
{
((IDisposable)_httpListener).Dispose();
}
catch
{
}
}
}

private void WebRequestCallback(IAsyncResult result)
{
// Avoid accessing HttpListener while/after it has been disposed
lock (_httpListener)
{
if (!_httpListener.IsListening) return;

try
{
HttpListenerContext context = _httpListener.EndGetContext(result);

Request = context.Request;

try
{
if (_processRequest == null)
{
context.Response.StatusCode = (int)HttpStatusCode.OK;
context.Response.Headers.Add(HttpResponseHeader.ContentType, "text/plain");
}
else
{
_processRequest(context);
}
}
finally
{
context.Response.OutputStream.Close();
}
}
catch (Exception ex)
{
Console.WriteLine($"[Server] - Error: {ex.Message}");
}
finally
{
// Set up the next context
Result = _httpListener.BeginGetContext(WebRequestCallback, _httpListener);
}
}
}
}
}
Loading

0 comments on commit 80eda44

Please sign in to comment.