Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#738 #1990 Route Metadata as custom properties #1843

Merged
merged 32 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
8cf8305
feat(configuration): adding route metadata
vantm Dec 6, 2023
d5eae05
feat(configuration): update docs
vantm Dec 7, 2023
2bd8d75
feat(configuration): replace Dictionary<> by IDictionary<>, code clea…
vantm Dec 11, 2023
b5087d8
feat(configuration): replace Dictionary<> by IDictionary<>
vantm Dec 11, 2023
f132867
feat(configuration): replace Dictionary<> by IDictionary<>
vantm Dec 11, 2023
f84327a
feat(configuration): update the data type of FileDynamicRoute Metadata
vantm Dec 11, 2023
3a94444
formatting
raman-m Dec 12, 2023
2b07078
feat(configuration): fix integration tests
vantm Mar 18, 2024
35ec151
feat !1843 add extension methods for DownstreamRoute to get metadata
vantm Mar 18, 2024
97df824
feat !1843 add extension methods for DownstreamRoute
vantm Mar 18, 2024
775e1ad
feat !1843 update docs
vantm Mar 18, 2024
1c95421
feat !1843 update docs
vantm Mar 18, 2024
d03bdbc
feat !1843 cleanup split string logic
vantm Mar 18, 2024
474eef0
SA1505: An opening brace should not be followed by a blank line
raman-m Apr 12, 2024
48a17a3
IDE1006: Naming rule violation: These words must begin with upper cas…
raman-m Apr 12, 2024
7055457
Fix compile errors after rebasing
raman-m Apr 19, 2024
dfcc351
Fix unit tests + AAA pattern
raman-m Apr 19, 2024
8f21752
First Version, providing a generic extension method GetMetadata<T> wi…
ggnaegi May 2, 2024
768a305
Adding ConvertToNumericType method to be able to use the NumberStyles…
ggnaegi May 2, 2024
7544f60
adding first acceptance tests
ggnaegi May 2, 2024
7f2f575
The tests are now passing again...
ggnaegi May 2, 2024
a3a2725
adding latest test cases. That should be enough (includes global conf…
ggnaegi May 3, 2024
aebf5b2
Update metadata.rst
ggnaegi May 2, 2024
c9724e2
adding the xml docs for IMetadataCreator and MetadataCreator
ggnaegi May 3, 2024
0d37962
renaming MetadataCreator to DefaultMetadataCreator
ggnaegi May 3, 2024
3c624fa
number tests for .net 6 too
ggnaegi May 3, 2024
0279fe3
Moving Metadata specific downstream route extensions to the Metadata …
ggnaegi May 3, 2024
a4db704
cleanup
ggnaegi May 3, 2024
8bd1dbc
applying some of the requested changes
ggnaegi May 17, 2024
987c288
Final code review by @raman-m
raman-m May 21, 2024
40b404f
Add traits
raman-m May 21, 2024
5f2beb6
Fix docs build error
raman-m May 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 60 additions & 1 deletion docs/features/configuration.rst
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We must have a short subsection in configuration.rst doc and redirect to the webpage with complete reference as metadata.rst chapter.

Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ Here is an example Route configuration. You don't need to set all of these thing
"IPAllowedList": [],
"IPBlockedList": [],
"ExcludeAllowedFromBlocked": false
}
},
"Metadata": {}
}

The actual Route schema for properties can be found in the C# `FileRoute <https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/Configuration/File/FileRoute.cs>`_ class.
Expand Down Expand Up @@ -485,6 +486,64 @@ You can utilize these methods in the ``ConfigureAppConfiguration`` method (locat

You can find additional details in the dedicated :ref:`di-configuration-overview` section and in subsequent sections related to the :doc:`../features/dependencyinjection` chapter.

Route Metadata
--------------

Ocelot provides various features such as routing, authentication, caching, load balancing, and more. However, some users may encounter situations where Ocelot does not meet their specific needs or they want to customize its behavior. In such cases, Ocelot allows users to add metadata to the route configuration. This property can store any arbitrary data that users can access in middlewares or delegating handlers. By using the metadata, users can implement their own logic and extend the functionality of Ocelot.

Here is an example:

.. code-block:: json

{
"Routes": [
{
"UpstreamHttpMethod": [ "GET" ],
"UpstreamPathTemplate": "/posts/{postId}",
"DownstreamPathTemplate": "/api/posts/{postId}",
"DownstreamHostAndPorts": [
{ "Host": "localhost", "Port": 80 }
],
"Metadata": {
"api-id": "FindPost",
"my-extension/param1": "overwritten-value",
"other-extension/param1": "value1",
"other-extension/param2": "value2",
"tags": "tag1, tag2, area1, area2, func1",
"json": "[1, 2, 3, 4, 5]"
}
}
],
"GlobalConfiguration": {
"Metadata": {
"instance_name": "dc-1-54abcz",
"my-extension/param1": "default-value"
}
}
}

Now, the route metadata can be accessed through the `DownstreamRoute` object:

.. code-block:: csharp

public static class OcelotMiddlewares
{
public static Task PreAuthenticationMiddleware(HttpContext context, Func<Task> next)
{
var downstreamRoute = context.Items.DownstreamRoute();

if(downstreamRoute?.Metadata is {} metadata)
{
var param1 = metadata.GetValueOrDefault("my-extension/param1") ?? throw new MyExtensionException("Param 1 is null");
var param2 = metadata.GetValueOrDefault("my-extension/param2", "custom-value");

// working with metadata
}

return next();
}
}

""""

.. [#f1] ":ref:`config-merging-files`" feature was requested in `issue 296 <https://github.com/ThreeMammals/Ocelot/issues/296>`_, since then we extended it in `issue 1216 <https://github.com/ThreeMammals/Ocelot/issues/1216>`_ (PR `1227 <https://github.com/ThreeMammals/Ocelot/pull/1227>`_) as ":ref:`config-merging-tomemory`" subfeature which was released as a part of version `23.2`_.
Expand Down
100 changes: 100 additions & 0 deletions docs/features/metadata.rst
ggnaegi marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
Metadata
========

Configuration
-------------

Ocelot provides various features such as routing, authentication, caching, load
balancing, and more.
However, some users may encounter situations where Ocelot does not meet their
specific needs or they want to customize its behavior.
In such cases, Ocelot allows users to add metadata to the route configuration.
This property can store any arbitrary data that users can access in middlewares
or delegating handlers.

By using the metadata, users can implement their own logic and extend the
functionality of Ocelot e.g.

.. code-block:: json

{
"Routes": [
{
"UpstreamHttpMethod": [ "GET" ],
"UpstreamPathTemplate": "/posts/{postId}",
"DownstreamPathTemplate": "/api/posts/{postId}",
"DownstreamHostAndPorts": [
{ "Host": "localhost", "Port": 80 }
],
"Metadata": {
"id": "FindPost",
"tags": "tag1, tag2, area1, area2, func1",
"plugin1.enabled": "true",
"plugin1.values": "[1, 2, 3, 4, 5]",
"plugin1.param": "value2",
"plugin1.param2": "123",
"plugin2/param1": "overwritten-value",
"plugin2/param2": "{\"name\":\"John Doe\",\"age\":30,\"city\":\"New York\",\"is_student\":false,\"hobbies\":[\"reading\",\"hiking\",\"cooking\"]}"
ggnaegi marked this conversation as resolved.
Show resolved Hide resolved
}
}
],
"GlobalConfiguration": {
"Metadata": {
"instance_name": "machine-1",
"plugin2/param1": "default-value"
}
}
}

Now, the route metadata can be accessed through the ``DownstreamRoute`` object:

.. code-block:: csharp

public class MyMiddleware
ggnaegi marked this conversation as resolved.
Show resolved Hide resolved
{
public Task Invoke(HttpContext context, Func<Task> next)
{
var route = context.Items.DownstreamRoute();
var enabled = route.GetMetadata<bool>("plugin1.enabled");
var values = route.GetMetadata<string[]>("plugin1.values");
var param1 = route.GetMetadata<string>("plugin1.param", "system-default-value");
var param2 = route.GetMetadata<int>("plugin1.param2");

// working on the plugin1's function

return next?.Invoke();
}
}

Extension Methods
-----------------

Ocelot provides one DowstreamRoute extension method to help you retrieve your metadata values effortlessly.
With the exception of the types string, bool, bool?, string[] and numeric, all strings passed as parameters are treated as json strings and an attempt is made to convert them into objects of generic type T.
If the value is null, then, if not explicitely specified, the default for the chosen target type is returned.

.. list-table::
:widths: 20 40 40
raman-m marked this conversation as resolved.
Show resolved Hide resolved

* - Method
- Description
- Notes
* - ``GetMetadata<string>``
- The metadata value is returned as string without further parsing
* - ``GetMetadata<string[]>``
- The metadata value is splitted by a given separator (default ``,``) and
returned as a string array.
- Several parameters can be set in the global configuration, such as Separators (default = ``[","]``), StringSplitOptions (default ``None``) and TrimChars, the characters that should be trimmed (default = ``[' ']``).
* - ``GetMetadata<Any known numeric type>``
- The metadata value is parsed to a number.
- Some parameters can be set in the global configuration, such as NumberStyle (default ``Any``) and CurrentCulture (default ``CultureInfo.CurrentCulture``)
* - ``GetMetadata<T>``
- The metadata value is converted to the given generic type. The value is treated as a json string and the json serializer tries to deserialize the string to the target type.
- A JsonSerializerOptions object can be passed as method parameter, Web is used as default.
* - ``GetMetadata<bool>``
- Check if the metadata value is a truthy value, otherwise return false.
- The truthy values are: ``true``, ``yes``, ``ok``, ``on``, ``enable``, ``enabled``
* - ``GetMetadata<bool?>``
- Check if the metadata value is a truthy value (return true), or falsy value (return false), otherwise return null.
- The known truthy values are: ``true``, ``yes``, ``ok``, ``on``, ``enable``, ``enabled``, ``1``, the known falsy values are: ``false``, ``no``, ``off``, ``disable``, ``disabled``, ``0``

1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ Updated Documentation
features/kubernetes
features/loadbalancer
features/logging
features/metadata
features/methodtransformation
features/middlewareinjection
features/qualityofservice
Expand Down
10 changes: 9 additions & 1 deletion src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public class DownstreamRouteBuilder
private Version _downstreamHttpVersion;
private HttpVersionPolicy _downstreamHttpVersionPolicy;
private Dictionary<string, UpstreamHeaderTemplate> _upstreamHeaders;
private MetadataOptions _metadataOptions;

public DownstreamRouteBuilder()
{
Expand Down Expand Up @@ -275,6 +276,12 @@ public DownstreamRouteBuilder WithDownstreamHttpVersionPolicy(HttpVersionPolicy
return this;
}

public DownstreamRouteBuilder WithMetadata(MetadataOptions metadataOptions)
{
_metadataOptions = metadataOptions;
return this;
}

public DownstreamRoute Build()
{
return new DownstreamRoute(
Expand Down Expand Up @@ -313,6 +320,7 @@ public DownstreamRoute Build()
_downstreamHttpMethod,
_downstreamHttpVersion,
_downstreamHttpVersionPolicy,
_upstreamHeaders);
_upstreamHeaders,
_metadataOptions);
}
}
54 changes: 54 additions & 0 deletions src/Ocelot/Configuration/Builder/MetadataOptionsBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System.Globalization;

namespace Ocelot.Configuration.Builder;

public class MetadataOptionsBuilder
ggnaegi marked this conversation as resolved.
Show resolved Hide resolved
{
private string[] _separators;
private char[] _trimChars;
private StringSplitOptions _stringSplitOption;
private NumberStyles _numberStyle;
private CultureInfo _currentCulture;
private IDictionary<string, string> _metadata;

public MetadataOptionsBuilder WithSeparators(string[] separators)
{
_separators = separators;
return this;
}

public MetadataOptionsBuilder WithTrimChars(char[] trimChars)
{
_trimChars = trimChars;
return this;
}

public MetadataOptionsBuilder WithStringSplitOption(string stringSplitOption)
{
_stringSplitOption = Enum.Parse<StringSplitOptions>(stringSplitOption);
return this;
}

public MetadataOptionsBuilder WithNumberStyle(string numberStyle)
{
_numberStyle = Enum.Parse<NumberStyles>(numberStyle);
return this;
}

public MetadataOptionsBuilder WithCurrentCulture(string currentCulture)
{
_currentCulture = CultureInfo.GetCultureInfo(currentCulture);
return this;
}

public MetadataOptionsBuilder WithMetadata(IDictionary<string, string> metadata)
{
_metadata = metadata;
return this;
}

public MetadataOptions Build()
{
return new MetadataOptions(_separators, _trimChars, _stringSplitOption, _numberStyle, _currentCulture, _metadata);
}
}
34 changes: 34 additions & 0 deletions src/Ocelot/Configuration/Creator/DefaultMetadataCreator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using Ocelot.Configuration.Builder;
using Ocelot.Configuration.File;

namespace Ocelot.Configuration.Creator;
ggnaegi marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// This class implements the <see cref="IMetadataCreator"/> interface.
/// </summary>
public class DefaultMetadataCreator : IMetadataCreator
{
public MetadataOptions Create(IDictionary<string, string> metadata, FileGlobalConfiguration globalConfiguration)
{
// metadata from the route could be null when no metadata is defined
metadata ??= new Dictionary<string, string>();

// metadata from the global configuration is never null
var options = globalConfiguration.MetadataOptions;
var mergedMetadata = new Dictionary<string, string>(options.Metadata);

foreach (var (key, value) in metadata)
{
mergedMetadata[key] = value;
}

return new MetadataOptionsBuilder()
.WithMetadata(mergedMetadata)
.WithSeparators(options.Separators)
.WithTrimChars(options.TrimChars)
.WithStringSplitOption(options.StringSplitOption)
.WithNumberStyle(options.NumberStyle)
.WithCurrentCulture(options.CurrentCulture)
.Build();
}
}
12 changes: 10 additions & 2 deletions src/Ocelot/Configuration/Creator/DynamicsCreator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,18 @@ public class DynamicsCreator : IDynamicsCreator
private readonly IRateLimitOptionsCreator _rateLimitOptionsCreator;
private readonly IVersionCreator _versionCreator;
private readonly IVersionPolicyCreator _versionPolicyCreator;
private readonly IMetadataCreator _metadataCreator;

public DynamicsCreator(IRateLimitOptionsCreator rateLimitOptionsCreator, IVersionCreator versionCreator, IVersionPolicyCreator versionPolicyCreator)
public DynamicsCreator(
IRateLimitOptionsCreator rateLimitOptionsCreator,
IVersionCreator versionCreator,
IVersionPolicyCreator versionPolicyCreator,
IMetadataCreator metadataCreator)
{
_rateLimitOptionsCreator = rateLimitOptionsCreator;
_versionCreator = versionCreator;
_versionPolicyCreator = versionPolicyCreator;
_metadataCreator = metadataCreator;
}

public List<Route> Create(FileConfiguration fileConfiguration)
Expand All @@ -29,14 +35,16 @@ private Route SetUpDynamicRoute(FileDynamicRoute fileDynamicRoute, FileGlobalCon
.Create(fileDynamicRoute.RateLimitRule, globalConfiguration);

var version = _versionCreator.Create(fileDynamicRoute.DownstreamHttpVersion);
var versionPolicy = _versionPolicyCreator.Create(fileDynamicRoute.DownstreamHttpVersionPolicy);
var versionPolicy = _versionPolicyCreator.Create(fileDynamicRoute.DownstreamHttpVersionPolicy);
var metadata = _metadataCreator.Create(fileDynamicRoute.Metadata, globalConfiguration);

var downstreamRoute = new DownstreamRouteBuilder()
.WithEnableRateLimiting(rateLimitOption.EnableRateLimiting)
.WithRateLimitOptions(rateLimitOption)
.WithServiceName(fileDynamicRoute.ServiceName)
.WithDownstreamHttpVersion(version)
.WithDownstreamHttpVersionPolicy(versionPolicy)
.WithMetadata(metadata)
.Build();

var route = new RouteBuilder()
Expand Down
11 changes: 11 additions & 0 deletions src/Ocelot/Configuration/Creator/IMetadataCreator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Ocelot.Configuration.File;

namespace Ocelot.Configuration.Creator;
ggnaegi marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// This interface describes the creation of metadata options.
/// </summary>
public interface IMetadataCreator
ggnaegi marked this conversation as resolved.
Show resolved Hide resolved
{
MetadataOptions Create(IDictionary<string, string> metadata, FileGlobalConfiguration globalConfiguration);
}
8 changes: 7 additions & 1 deletion src/Ocelot/Configuration/Creator/RoutesCreator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public class RoutesCreator : IRoutesCreator
private readonly ISecurityOptionsCreator _securityOptionsCreator;
private readonly IVersionCreator _versionCreator;
private readonly IVersionPolicyCreator _versionPolicyCreator;
private readonly IMetadataCreator _metadataCreator;

public RoutesCreator(
IClaimsToThingCreator claimsToThingCreator,
Expand All @@ -41,7 +42,8 @@ public RoutesCreator(
ISecurityOptionsCreator securityOptionsCreator,
IVersionCreator versionCreator,
IVersionPolicyCreator versionPolicyCreator,
IUpstreamHeaderTemplatePatternCreator upstreamHeaderTemplatePatternCreator)
IUpstreamHeaderTemplatePatternCreator upstreamHeaderTemplatePatternCreator,
IMetadataCreator metadataCreator)
{
_routeKeyCreator = routeKeyCreator;
_loadBalancerOptionsCreator = loadBalancerOptionsCreator;
Expand All @@ -61,6 +63,7 @@ public RoutesCreator(
_versionCreator = versionCreator;
_versionPolicyCreator = versionPolicyCreator;
_upstreamHeaderTemplatePatternCreator = upstreamHeaderTemplatePatternCreator;
_metadataCreator = metadataCreator;
}

public List<Route> Create(FileConfiguration fileConfiguration)
Expand Down Expand Up @@ -114,6 +117,8 @@ private DownstreamRoute SetUpDownstreamRoute(FileRoute fileRoute, FileGlobalConf

var downstreamHttpVersionPolicy = _versionPolicyCreator.Create(fileRoute.DownstreamHttpVersionPolicy);

var metadata = _metadataCreator.Create(fileRoute.Metadata, globalConfiguration);

var route = new DownstreamRouteBuilder()
.WithKey(fileRoute.Key)
.WithDownstreamPathTemplate(fileRoute.DownstreamPathTemplate)
Expand Down Expand Up @@ -151,6 +156,7 @@ private DownstreamRoute SetUpDownstreamRoute(FileRoute fileRoute, FileGlobalConf
.WithDownstreamHttpVersion(downstreamHttpVersion)
.WithDownstreamHttpVersionPolicy(downstreamHttpVersionPolicy)
.WithDownStreamHttpMethod(fileRoute.DownstreamHttpMethod)
.WithMetadata(metadata)
.Build();

return route;
Expand Down
Loading