diff --git a/docs/features/administration.rst b/docs/features/administration.rst index cadecefdf..61832d1ec 100644 --- a/docs/features/administration.rst +++ b/docs/features/administration.rst @@ -146,4 +146,4 @@ The region is whatever you set against the **Region** field in the `FileCacheOpt """" -.. [#f1] The ``AddOcelot`` method adds default ASP.NET services to DI-container. You could call another more extended ``AddOcelotUsingBuilder`` method while configuring services to build and use custom builder via an ``IMvcCoreBuilder`` interface object. See more instructions in :doc:`../features/dependencyinjection`, "**The AddOcelotUsingBuilder method**" section. +.. [#f1] :ref:`di-the-addocelot-method` adds default ASP.NET services to DI container. You could call another extended :ref:`di-addocelotusingbuilder-method` while configuring services to develop your own :ref:`di-custom-builder`. See more instructions in the ":ref:`di-addocelotusingbuilder-method`" section of :doc:`../features/dependencyinjection` feature. diff --git a/docs/features/configuration.rst b/docs/features/configuration.rst index 936a78a9d..7ae03bb93 100644 --- a/docs/features/configuration.rst +++ b/docs/features/configuration.rst @@ -1,7 +1,7 @@ Configuration ============= -An example configuration can be found here in `ocelot.json `_. +An example configuration can be found here in `ocelot.json`_. There are two sections to the configuration: an array of **Routes** and a **GlobalConfiguration**: * The **Routes** are the objects that tell Ocelot how to treat an upstream request. @@ -75,31 +75,33 @@ More information on how to use these options is below. Multiple Environments --------------------- -Like any other ASP.NET Core project Ocelot supports configuration file names such as **configuration.dev.json**, **configuration.test.json** etc. In order to implement this add the following -to you: +Like any other ASP.NET Core project Ocelot supports configuration file names such as **appsettings.dev.json**, **appsettings.test.json** etc. +In order to implement this add the following to you: .. code-block:: csharp ConfigureAppConfiguration((hostingContext, config) => { - config - .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) + var env = hostingContext.HostingEnvironment; + config.SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", true, true) - .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, true) .AddJsonFile("ocelot.json") - .AddJsonFile($"configuration.{hostingContext.HostingEnvironment.EnvironmentName}.json") + .AddJsonFile($"ocelot.{env.EnvironmentName}.json") .AddEnvironmentVariables(); }) -Ocelot will now use the environment specific configuration and fall back to **ocelot.json** if there isn't one. +Ocelot will now use the environment specific configuration and fall back to `ocelot.json`_ if there isn't one. You also need to set the corresponding environment variable which is ``ASPNETCORE_ENVIRONMENT``. More info on this can be found in the ASP.NET Core docs: `Use multiple environments in ASP.NET Core `_. -Merging Configuration Files ---------------------------- +.. _config-merging-files: -This feature was requested in `issue 296 `_ and allows users to have multiple configuration files to make managing large configurations easier. +Merging Configuration Files [#f1]_ +---------------------------------- + +This feature allows users to have multiple configuration files to make managing large configurations easier [#f1]_. Instead of adding the configuration directly e.g. ``AddJsonFile("ocelot.json")`` you can call ``AddOcelot()`` like below: @@ -107,23 +109,26 @@ Instead of adding the configuration directly e.g. ``AddJsonFile("ocelot.json")`` ConfigureAppConfiguration((hostingContext, config) => { - config - .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) + var env = hostingContext.HostingEnvironment; + config.SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", true, true) - .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) - .AddOcelot(hostingContext.HostingEnvironment) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, true) + .AddOcelot(env) // happy path .AddEnvironmentVariables(); }) -In this scenario Ocelot will look for any files that match the pattern ``(?i)ocelot.([a-zA-Z0-9]*).json`` and then merge these together. +In this scenario Ocelot will look for any files that match the pattern ``^ocelot\.(.*?)\.json$`` and then merge these together. If you want to set the **GlobalConfiguration** property, you must have a file called **ocelot.global.json**. The way Ocelot merges the files is basically load them, loop over them, add any Routes, add any **AggregateRoutes** and if the file is called **ocelot.global.json** add the **GlobalConfiguration** aswell as any Routes or **AggregateRoutes**. -Ocelot will then save the merged configuration to a file called **ocelot.json** and this will be used as the source of truth while Ocelot is running. +Ocelot will then save the merged configuration to a file called `ocelot.json`_ and this will be used as the source of truth while Ocelot is running. At the moment there is no validation at this stage it only happens when Ocelot validates the final merged configuration. This is something to be aware of when you are investigating problems. -We would advise always checking what is in **ocelot.json** file if you have any problems. +We would advise always checking what is in `ocelot.json`_ file if you have any problems. + +Keep files in a folder +^^^^^^^^^^^^^^^^^^^^^^ You can also give Ocelot a specific path to look in for the configuration files like below: @@ -131,15 +136,70 @@ You can also give Ocelot a specific path to look in for the configuration files ConfigureAppConfiguration((hostingContext, config) => { - config - .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) + var env = hostingContext.HostingEnvironment; + config.SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", true, true) - .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) - .AddOcelot("/foo/bar", hostingContext.HostingEnvironment) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, true) + .AddOcelot("/my/folder", env) // happy path .AddEnvironmentVariables(); }) -Ocelot needs the ``HostingEnvironment`` so it knows to exclude anything environment specific from the algorithm. +Ocelot needs the ``HostingEnvironment`` so it knows to exclude anything environment specific from the merging algorithm. + +.. _config-merging-tomemory: + +Merging files to memory [#f2]_ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +By default Ocelot writes the merged configuration back to disk as `ocelot.json`_ (primary configuration file) with adding the file to config. + +If your server don't have write permissions to your configuration folder, you can instruct Ocelot to use the merged configuration directly from memory instead, like this: + +.. code-block:: csharp + + // It implicitly calls ASP.NET AddJsonStream extension method for IConfigurationBuilder + // config.AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json))); + config.AddOcelot(hostingContext.HostingEnvironment, MergeOcelotJson.ToMemory); + +This feature is extremely useful in a cloud environment such as Azure, AWS, GCP where the application may not have enough write permissions to save files. +Additionally, the Docker container environment may lack permissions and would require significant DevOps effort to implement file write operation. +So don't waste your time: use the feature! [#f2]_ + +Reload JSON Config On Change +---------------------------- + +Ocelot supports reloading the JSON configuration file on change. For instance, the following will recreate Ocelot internal configuration when the `ocelot.json`_ file is updated manually: + +.. code-block:: csharp + + config.AddJsonFile("ocelot.json", optional: false, reloadOnChange: true); // ASP.NET framework version + +Please note: as of version `23.2`_, most ``AddOcelot`` methods have optional ``bool?`` arguments aka ``optional`` and ``reloadOnChange``. +So, you can supply these arguments to the internal ``AddJsonFile`` call in the last step of configuring (see `AddOcelotJsonFile `_) e.g. + +.. code-block:: csharp + + config.AddJsonFile(ConfigurationBuilderExtensions.PrimaryConfigFile, optional ?? false, reloadOnChange ?? false); + +As you see, in previous versions less than `23.2`_ ``AddOcelot`` extension methods didn't apply the ``reloadOnChange`` arg because it was ``false``. +Finally, we recommend to control reloading by ``AddOcelot`` extension methods instead of usage of the framework ``AddJsonFile`` one e.g. + +.. code-block:: csharp + + ConfigureAppConfiguration((hostingContext, config) => + { + config.AddJsonFile(ConfigurationBuilderExtensions.PrimaryConfigFile, optional: false, reloadOnChange: true); // old approach + var env = hostingContext.HostingEnvironment; + var mergeTo = MergeOcelotJson.ToFile; // ToMemory + var folder = "/My/folder"; + FileConfiguration configuration = new(); // read from anywhere and initialize + config.AddOcelot(env, mergeTo, optional: false, reloadOnChange: true); // with environment and merging type + config.AddOcelot(folder, env, mergeTo, optional: false, reloadOnChange: true); // with folder, environment and merging type + config.AddOcelot(configuration, optional: false, reloadOnChange: true); // with configuration object created by your own + config.AddOcelot(configuration, env, mergeTo, optional: false, reloadOnChange: true); // with configuration object, environment and merging type + }) + +It would be useful to look into `the ConfigurationBuilderExtensions class `_ code and note something to better understand the signatures of the overloaded methods [#f2]_. Store Configuration in Consul ----------------------------- @@ -151,7 +211,7 @@ The first thing you need to do is install the `NuGet package `_ seconds TTL cache before making a new request to your local Consul agent. - -Reload JSON Config On Change ----------------------------- - -Ocelot supports reloading the JSON configuration file on change. For instance, the following will recreate Ocelot internal configuration when the **ocelot.json** file is updated manually: +This feature has a `3 seconds `_ TTL cache before making a new request to your local Consul agent. -.. code-block:: csharp - - config.AddJsonFile("ocelot.json", optional: false, reloadOnChange: true); +.. _config-consul-key: -Configuration Key ------------------ +Consul Configuration Key [#f4]_ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ If you are using Consul for configuration (or other providers in the future), you might want to key your configurations: so you can have multiple configurations. -This feature was requested in `issue 346 `_. + In order to specify the key you need to set the **ConfigurationKey** property in the **ServiceDiscoveryProvider** options of the configuration JSON file e.g. .. code-block:: json @@ -258,7 +311,7 @@ For ``https`` scheme this fake validator was requested by `issue 309 `_. As a team, we do not consider it as an ideal solution. From one side, the community wants to have an option to work with self-signed certificates. -But from other side, currently source code scanners detect 2 serious security vulnerabilities because of this fake validator in `20.0 release `_. +But from other side, currently source code scanners detect 2 serious security vulnerabilities because of this fake validator in `20.0`_ release. The Ocelot team will rethink this unfortunate situation, and it is highly likely that this feature will at least be redesigned or removed completely. For now, the SSL fake validator makes sense in local development environments when a route has ``https`` or ``wss`` schemes having self-signed certificate for those routes. @@ -279,12 +332,12 @@ As a team, we highly recommend following these instructions when developing your * **Production environments**. **Do not use self-signed certificates at all!** System administrators or DevOps engineers must create real valid certificates being signed by hosting or cloud providers. - **Switch off the feature for all routes!** Remove the **DangerousAcceptAnyServerCertificateValidator** property for all routes in production version of **ocelot.json** file! + **Switch off the feature for all routes!** Remove the **DangerousAcceptAnyServerCertificateValidator** property for all routes in production version of `ocelot.json`_ file! React to Configuration Changes ------------------------------ -Resolve ``IOcelotConfigurationChangeTokenSource`` interface from the DI container if you wish to react to changes to the Ocelot configuration via the :doc:`../features/administration` API or **ocelot.json** being reloaded from the disk. +Resolve ``IOcelotConfigurationChangeTokenSource`` interface from the DI container if you wish to react to changes to the Ocelot configuration via the :doc:`../features/administration` API or `ocelot.json`_ being reloaded from the disk. You may either poll the change token's ``IChangeToken.HasChanged`` property, or register a callback with the ``RegisterChangeCallback`` method. Polling the HasChanged property @@ -342,6 +395,33 @@ DownstreamHttpVersion Ocelot allows you to choose the HTTP version it will use to make the proxy request. It can be set as ``1.0``, ``1.1`` or ``2.0``. +Dependency Injection +-------------------- + +*Dependency Injection* for this **Configuration** feature in Ocelot is designed to extend and/or control **configuring** of Ocelot core before the stage of building ASP.NET MVC pipeline services. +The main method is :ref:`di-configuration-addocelot` of the `ConfigurationBuilderExtensions`_ class which has a bunch of overloaded versions with corresponding signatures. + +Use them in the following ``ConfigureAppConfiguration`` method (**Program.cs** and **Startup.cs**) of your ASP.NET MVC gateway app (minimal web app) to configure Ocelot pipeline and services: + +.. code-block:: csharp + + namespace Microsoft.AspNetCore.Hosting; + + public interface IWebHostBuilder + { + IWebHostBuilder ConfigureAppConfiguration(Action configureDelegate); + } + +More details you could find in the special ":ref:`di-configuration-overview`" section and in subsequent sections of the :doc:`../features/dependencyinjection` feature. + """" -.. [#f1] The ``AddOcelot`` method adds default ASP.NET services to DI-container. You could call another more extended ``AddOcelotUsingBuilder`` method while configuring services to build and use custom builder via an ``IMvcCoreBuilder`` interface object. See more instructions in :doc:`../features/dependencyinjection`, "**The AddOcelotUsingBuilder method**" section. +.. [#f1] ":ref:`config-merging-files`" feature was requested in `issue 296 `_, since then we extended it in `issue 1216 `_ (PR `1227 `_) as ":ref:`config-merging-tomemory`" subfeature which was released as a part of the version `23.2`_. +.. [#f2] ":ref:`config-merging-tomemory`" subfeature is based on the ``MergeOcelotJson`` enumeration type with values: ``ToFile`` and ``ToMemory``. The 1st one is implicit by default, and the second one is exactly what you need when merging to memory. See more details on implementations in the `ConfigurationBuilderExtensions`_ class. +.. [#f3] :ref:`di-the-addocelot-method` adds default ASP.NET services to DI container. You could call another extended :ref:`di-addocelotusingbuilder-method` while configuring services to develop your own :ref:`di-custom-builder`. See more instructions in the ":ref:`di-addocelotusingbuilder-method`" section of :doc:`../features/dependencyinjection` feature. +.. [#f4] ":ref:`config-consul-key`" feature was requested in `issue 346 `_ as a part of version `7.0.0 `_. + +.. _20.0: https://github.com/ThreeMammals/Ocelot/releases/tag/20.0.0 +.. _23.2: https://github.com/ThreeMammals/Ocelot/releases/tag/23.2.0 +.. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/test/Ocelot.ManualTest/ocelot.json +.. _ConfigurationBuilderExtensions: https://github.com/ThreeMammals/Ocelot/blob/develop/src/Ocelot/DependencyInjection/ConfigurationBuilderExtensions.cs diff --git a/docs/features/dependencyinjection.rst b/docs/features/dependencyinjection.rst index 9dbd9b2be..d1333b7b4 100644 --- a/docs/features/dependencyinjection.rst +++ b/docs/features/dependencyinjection.rst @@ -1,3 +1,8 @@ +.. _AddOcelot: #the-addocelot-method +.. _AddOcelotUsingBuilder: #addocelotusingbuilder-method +.. _AddDefaultAspNetServices: #adddefaultaspnetservices-method +.. _OcelotBuilder: #ocelotbuilder-class + Dependency Injection ==================== @@ -7,50 +12,77 @@ Dependency Injection Overview -------- -Dependency Injection feature in Ocelot is designed to extend and/or control building of Ocelot core as ASP.NET MVC pipeline services. -The main methods are `AddOcelot <#the-addocelot-method>`_ and `AddOcelotUsingBuilder <#the-addocelotusingbuilder-method>`_ of the ``ServiceCollectionExtensions`` class. -Use them in **Program.cs** and **Startup.cs** of your ASP.NET MVC gateway app (minimal web app) to enable and build Ocelot pipeline. +| Dependency Injection feature in Ocelot is designed to extend and/or control building of Ocelot core as ASP.NET MVC pipeline services. +| The main methods of the `ServiceCollectionExtensions`_ class are: + +* `AddOcelot`_ adds required Ocelot services to DI and it adds default services using `AddDefaultAspNetServices`_ method. +* `AddOcelotUsingBuilder`_ adds required Ocelot services to DI, and **it adds custom ASP.NET services** with configuration injected implicitly or explicitly. + +Use :ref:`di-service-extensions` in in the following ``ConfigureServices`` method (**Program.cs** and **Startup.cs**) of your ASP.NET MVC gateway app (minimal web app) to add/build Ocelot pipeline services: + +.. code-block:: csharp -And of course, the `OcelotBuilder <#the-ocelotbuilder-class>`_ class is the core of Ocelot. + namespace Microsoft.AspNetCore.Hosting; + public interface IWebHostBuilder + { + IWebHostBuilder ConfigureServices(Action configureServices); + } -IServiceCollection extensions ------------------------------ +The fact is, the `OcelotBuilder`_ class is Ocelot's cornerstone logic. - **Class**: `Ocelot.DependencyInjection.ServiceCollectionExtensions `_ +.. _di-service-extensions: -Based on the current implementations for the ``OcelotBuilder`` class, the ``AddOcelot`` method adds default ASP.NET services to DI container. -You could call another more extended ``AddOcelotUsingBuilder`` method while configuring services to build and use custom builder via an ``IMvcCoreBuilder`` interface object. +``IServiceCollection`` extensions +--------------------------------- -The AddOcelot method -^^^^^^^^^^^^^^^^^^^^ + | **Namespace**: ``Ocelot.DependencyInjection`` + | **Class**: `ServiceCollectionExtensions`_ + +Based on the current implementations for the `OcelotBuilder`_ class, the `AddOcelot`_ method adds required ASP.NET services to DI container. +You could call another more extended `AddOcelotUsingBuilder`_ method while configuring services to build and use custom builder via an ``IMvcCoreBuilder`` object. + +.. _di-the-addocelot-method: + +The ``AddOcelot`` method +^^^^^^^^^^^^^^^^^^^^^^^^ **Signatures**: -* ``IOcelotBuilder AddOcelot(this IServiceCollection services)`` -* ``IOcelotBuilder AddOcelot(this IServiceCollection services, IConfiguration configuration)`` +.. code-block:: csharp + + IOcelotBuilder AddOcelot(this IServiceCollection services); + IOcelotBuilder AddOcelot(this IServiceCollection services, IConfiguration configuration); + +These ``IServiceCollection`` extension methods add default ASP.NET services and Ocelot application services with configuration injected implicitly or explicitly. -This ``IServiceCollection`` extension method adds default ASP.NET services and Ocelot application services with configuration injected implicitly or explicitly. -Note! The method adds **default** ASP.NET services required for Ocelot core in the `AddDefaultAspNetServices <#the-adddefaultaspnetservices-method>`_ method which plays the role of default builder. +**Note!** Both methods add required and **default** ASP.NET services for Ocelot pipeline in the `AddDefaultAspNetServices`_ method which is default builder. -In this scenario, you do nothing except calling the ``AddOcelot`` method which has been mentioned in feature chapters, if additional startup settings are required. -In this case you just reuse default settings to build Ocelot core. The alternative is ``AddOcelotUsingBuilder`` method, see the next section. +In this scenario, you do nothing other than call the ``AddOcelot`` method, which is often mentioned in feature chapters, if additional startup settings are required. +With this method, you simply reuse the default settings to build the Ocelot pipeline. The alternative is ``AddOcelotUsingBuilder`` method, see the next subsection. -The AddOcelotUsingBuilder method +.. _di-addocelotusingbuilder-method: + +``AddOcelotUsingBuilder`` method ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Signatures**: -* ``IOcelotBuilder AddOcelotUsingBuilder(this IServiceCollection services, Func customBuilder)`` -* ``IOcelotBuilder AddOcelotUsingBuilder(this IServiceCollection services, IConfiguration configuration, Func customBuilder)`` +.. code-block:: csharp + + using CustomBuilderFunc = System.Func; + + IOcelotBuilder AddOcelotUsingBuilder(this IServiceCollection services, CustomBuilderFunc customBuilder); + IOcelotBuilder AddOcelotUsingBuilder(this IServiceCollection services, IConfiguration configuration, CustomBuilderFunc customBuilder); -This ``IServiceCollection`` extension method adds Ocelot application services, and it *adds custom ASP.NET services* with configuration injected implicitly or explicitly. -Note! The method adds **custom** ASP.NET services required for Ocelot pipeline using custom builder (``customBuilder`` parameter). -It is highly recommended to read docs of the `AddDefaultAspNetServices <#the-adddefaultaspnetservices-method>`_ method, +These ``IServiceCollection`` extension methods add Ocelot application services, and they add **custom ASP.NET services** with configuration injected implicitly or explicitly. + +**Note!** The method adds **custom** ASP.NET services required for Ocelot pipeline using custom builder (aka ``customBuilder`` parameter). +It is highly recommended to read docs of the `AddDefaultAspNetServices`_ method, or even to review implementation to understand default ASP.NET services which are the minimal part of the gateway pipeline. -In this custom scenario, you control everything during ASP.NET MVC pipeline building, and you provide custom settings to build Ocelot core. +In this custom scenario, you control everything during ASP.NET MVC pipeline building, and you provide custom settings to build Ocelot pipeline. -The OcelotBuilder class +``OcelotBuilder`` class ----------------------- **Source code**: `Ocelot.DependencyInjection.OcelotBuilder `_ @@ -58,7 +90,11 @@ The OcelotBuilder class The ``OcelotBuilder`` class is the core of Ocelot which does the following: - Contructs itself by single public constructor: - ``public OcelotBuilder(IServiceCollection services, IConfiguration configurationRoot, Func customBuilder = null)`` + + .. code-block:: csharp + + public OcelotBuilder(IServiceCollection services, IConfiguration configurationRoot, Func customBuilder = null); + - Initializes and stores public properties: **Services** (``IServiceCollection`` object), **Configuration** (``IConfiguration`` object) and **MvcCoreBuilder** (``IMvcCoreBuilder`` object) - Adds **all application services** during construction phase over the ``Services`` property - Adds ASP.NET services by builder using ``Func`` object in these 2 development scenarios: @@ -73,15 +109,15 @@ The ``OcelotBuilder`` class is the core of Ocelot which does the following: * ``AddDelegatingHandler`` method * ``AddConfigPlaceholders`` method -The AddDefaultAspNetServices method +``AddDefaultAspNetServices`` method ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - **Class**: `Ocelot.DependencyInjection.OcelotBuilder `_ + **Class**: `OcelotBuilder`_ Currently the method is protected and overriding is forbidden. -The role of the method is to inject required services via both ``IServiceCollection`` and ``IMvcCoreBuilder`` interfaces objects for the minimal part of the gateway pipeline. +The role of the method is to inject required services via both ``IServiceCollection`` and ``IMvcCoreBuilder`` interface objects for the minimal part of the gateway pipeline. -Current implementation is the folowing: +Current `implementation `_ is the folowing: .. code-block:: csharp @@ -100,9 +136,9 @@ Current implementation is the folowing: } The method cannot be overridden. It is not virtual, and there is no way to override current behavior by inheritance. -And, the method is default builder of Ocelot pipeline while calling the `AddOcelot <#the-addocelot-method>`_ method. +And, the method is default builder of Ocelot pipeline while calling the `AddOcelot`_ method. As alternative, to "override" this default builder, you can design and reuse custom builder as a ``Func`` delegate object -and pass it as parameter to the `AddOcelotUsingBuilder <#the-addocelotusingbuilder-method>`_ extension method. +and pass it as parameter to the `AddOcelotUsingBuilder`_ extension method. It gives you full control on design and buiding of Ocelot pipeline, but be careful while designing your custom Ocelot pipeline as customizable ASP.NET MVC pipeline. Warning! Most of services from minimal part of the pipeline should be reused, but only a few of services could be removed. @@ -116,16 +152,19 @@ Finally, as a default builder, the method above receives ``IMvcCoreBuilder`` obj The next section shows you an example of designing custom Ocelot pipeline by custom builder. +.. _di-custom-builder: + Custom Builder -------------- + **Goal**: Replace ``Newtonsoft.Json`` services with ``System.Text.Json`` services. -The Problem -^^^^^^^^^^^ +Problem +^^^^^^^ -The default `AddOcelot <#the-addocelot-method>`_ method adds +The main `AddOcelot`_ method adds `Newtonsoft JSON `_ services -by the ``AddNewtonsoftJson`` extension method in default builder (the `AddDefaultAspNetServices <#the-adddefaultaspnetservices-method>`_ method). +by the ``AddNewtonsoftJson`` extension method in default builder (`AddDefaultAspNetServices`_ method). The ``AddNewtonsoftJson`` method calling was introduced in old .NET and Ocelot releases which was necessary when Microsoft did not launch the ``System.Text.Json`` library, but now it affects normal use, so we have an intention to solve the problem. @@ -136,12 +175,14 @@ will help to configure JSON settings by the ``JsonSerializerOptions`` property f Solution ^^^^^^^^ -We have the following methods in ``Ocelot.DependencyInjection.ServiceCollectionExtensions`` class: +We have the following methods in `ServiceCollectionExtensions`_ class: -- ``IOcelotBuilder AddOcelotUsingBuilder(this IServiceCollection services, Func customBuilder)`` -- ``IOcelotBuilder AddOcelotUsingBuilder(this IServiceCollection services, IConfiguration configuration, Func customBuilder)`` +.. code-block:: csharp + + IOcelotBuilder AddOcelotUsingBuilder(this IServiceCollection services, Func customBuilder); + IOcelotBuilder AddOcelotUsingBuilder(this IServiceCollection services, IConfiguration configuration, Func customBuilder); -These method with custom builder allows you to use your any desired JSON library for (de)serialization. +These methods with custom builder allow you to use your any desired JSON library for (de)serialization. But we are going to create custom ``MvcCoreBuilder`` with support of JSON services, such as ``System.Text.Json``. To do that we need to call ``AddJsonOptions`` extension of the ``MvcCoreMvcCoreBuilderExtensions`` class (NuGet package: `Microsoft.AspNetCore.Mvc.Core `_) in **Startup.cs**: @@ -181,3 +222,92 @@ To do that we need to call ``AddJsonOptions`` extension of the ``MvcCoreMvcCoreB The sample code provides settings to render JSON as indented text rather than compressed plain JSON text without spaces. This is just one common use case, and you can add additional services to the builder. + +------------------------------------------------------------------ + +.. _di-configuration-overview: + +Configuration Overview +---------------------- + +*Dependency Injection* for the :doc:`../features/configuration` feature in Ocelot is designed to extend and/or control **configuring** of Ocelot core before the stage of building ASP.NET MVC pipeline services. + +Use :ref:`di-configuration-extensions` in the following ``ConfigureAppConfiguration`` method (**Program.cs** and **Startup.cs**) of your app (minimal web app) to configure Ocelot pipeline and services: + +.. code-block:: csharp + + namespace Microsoft.AspNetCore.Hosting; + public interface IWebHostBuilder + { + IWebHostBuilder ConfigureAppConfiguration(Action configureDelegate); + } + +.. _di-configuration-extensions: + +``IConfigurationBuilder`` extensions +------------------------------------ + + | **Namespace**: ``Ocelot.DependencyInjection`` + | **Class**: `ConfigurationBuilderExtensions`_ + +The main one is the :ref:`di-configuration-addocelot` of the `ConfigurationBuilderExtensions`_ class, which has a list of overloaded versions with corresponding signatures. + +The purpose of the method is to prepare everything before actually configuring with native extensions: +merge partial JSON files, select a merge type to save the merged JSON configuration data ``ToFile`` or ``ToMemory``, +and finally call the following native ``IConfigurationBuilder`` framework extensions: + +* ``AddJsonFile`` finally adds primary configuration file (aka `ocelot.json`_) after the merge stage writing the file back **to the file system** using the ``ToFile`` merge type option, which is implicitly the default. +* ``AddJsonStream`` finally adds the JSON data of the primary configuration file as a UTF-8 stream after the merge stage **into memory** using the ``ToMemory`` merge type option. + +.. _di-configuration-addocelot: + +``AddOcelot`` method +^^^^^^^^^^^^^^^^^^^^ + +**Signatures** of the most common versions: + +.. code-block:: csharp + + IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, IWebHostEnvironment env); + IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, string folder, IWebHostEnvironment env); + +**Note!** These versions use implicit ``ToFile`` merge type to write `ocelot.json`_ back to a disk and finally they call the ``AddJsonFile`` extension. + +**Signatures** of the versions to specify a ``MergeOcelotJson`` option: + +.. code-block:: csharp + + IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, IWebHostEnvironment env, MergeOcelotJson mergeTo, + string primaryConfigFile = null, string globalConfigFile = null, string environmentConfigFile = null, bool? optional = null, bool? reloadOnChange = null); + IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, string folder, IWebHostEnvironment env, MergeOcelotJson mergeTo, + string primaryConfigFile = null, string globalConfigFile = null, string environmentConfigFile = null, bool? optional = null, bool? reloadOnChange = null); + +**Note!** These versions have optional arguments to specify the location of 3 main files that are involved in the merge operation. +In theory, these files can be located anywhere, but in practice it is better to keep the files in one folder. + +**Signatures** of the versions to indicate the ``FileConfiguration`` object of a self-created out-of-the-box configuration: [#f1]_ + +.. code-block:: csharp + + IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, FileConfiguration fileConfiguration, + string primaryConfigFile = null, bool? optional = null, bool? reloadOnChange = null); + IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, FileConfiguration fileConfiguration, IWebHostEnvironment env, MergeOcelotJson mergeTo, + string primaryConfigFile = null, string globalConfigFile = null, string environmentConfigFile = null, bool? optional = null, bool? reloadOnChange = null); + +| **Note 1**. These versions have optional arguments to specify the location of 3 main files that are involved in the merge operation. +| **Note 2**. Your ``FileConfiguration`` object can be serialized/deserialized to/from anywhere: local or remote storages, Consul KV storage and even a database. + Please, read more about this super useful feature in PR `1569`_ [#f1]_. + +"""" + +.. [#f1] Dynamic :doc:`../features/configuration` feature was requested in issues `1228`_, `1235`_, and delivered by PR `1569`_ as a part of the version `20.0`_. Since then we extended it in PR `1227`_ and released it as a part of the version `23.2`_. + +.. _ServiceCollectionExtensions: https://github.com/ThreeMammals/Ocelot/blob/develop/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs#L7 +.. _ConfigurationBuilderExtensions: https://github.com/ThreeMammals/Ocelot/blob/develop/src/Ocelot/DependencyInjection/ConfigurationBuilderExtensions.cs +.. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/test/Ocelot.ManualTest/ocelot.json +.. _1227: https://github.com/ThreeMammals/Ocelot/pull/1227 +.. _1228: https://github.com/ThreeMammals/Ocelot/issues/1228 +.. _1235: https://github.com/ThreeMammals/Ocelot/issues/1235 +.. _1569: https://github.com/ThreeMammals/Ocelot/pull/1569 +.. _20.0: https://github.com/ThreeMammals/Ocelot/releases/tag/20.0.0 +.. _23.2: https://github.com/ThreeMammals/Ocelot/releases/tag/23.2.0 diff --git a/docs/features/qualityofservice.rst b/docs/features/qualityofservice.rst index 5efab9f1d..212ff0d73 100644 --- a/docs/features/qualityofservice.rst +++ b/docs/features/qualityofservice.rst @@ -48,7 +48,7 @@ If someone needs this to be configurable, open an issue. [#f2]_ """" -.. [#f1] The ``AddOcelot`` method adds default ASP.NET services to DI-container. You could call another more extended ``AddOcelotUsingBuilder`` method while configuring services to build and use custom builder via an ``IMvcCoreBuilder`` interface object. See more instructions in :doc:`../features/dependencyinjection`, "**The AddOcelotUsingBuilder method**" section. +.. [#f1] :ref:`di-the-addocelot-method` adds default ASP.NET services to DI container. You could call another extended :ref:`di-addocelotusingbuilder-method` while configuring services to develop your own :ref:`di-custom-builder`. See more instructions in the ":ref:`di-addocelotusingbuilder-method`" section of :doc:`../features/dependencyinjection` feature. .. [#f2] If something doesn't work or you get stuck, please review current `QoS issues `_ filtering by |QoS_label| label. .. |QoS_label| image:: https://img.shields.io/badge/-QoS-D3ADAF.svg diff --git a/docs/features/requestaggregation.rst b/docs/features/requestaggregation.rst index b664a2a74..f1123cf19 100644 --- a/docs/features/requestaggregation.rst +++ b/docs/features/requestaggregation.rst @@ -224,4 +224,4 @@ Aggregation only supports the ``GET`` HTTP verb. """" .. [#f1] This feature was requested as part of `issue 79 `_ and further improvements were made as part of `issue 298 `_. A significant refactoring and revision of the `Multiplexer `_ design was carried out on March 4, 2024 in version `23.1 `_, see PRs `1826 `_ and `1462 `_. -.. [#f2] The ``AddOcelot`` method adds default ASP.NET services to DI-container. You could call another more extended ``AddOcelotUsingBuilder`` method while configuring services to build and use custom builder via an ``IMvcCoreBuilder`` interface object. See more instructions in :doc:`../features/dependencyinjection`, "**The AddOcelotUsingBuilder method**" section. +.. [#f2] :ref:`di-the-addocelot-method` adds default ASP.NET services to DI container. You could call another extended :ref:`di-addocelotusingbuilder-method` while configuring services to develop your own :ref:`di-custom-builder`. See more instructions in the ":ref:`di-addocelotusingbuilder-method`" section of :doc:`../features/dependencyinjection` feature. diff --git a/docs/features/tracing.rst b/docs/features/tracing.rst index 4614917e2..2ae0fc2e0 100644 --- a/docs/features/tracing.rst +++ b/docs/features/tracing.rst @@ -88,4 +88,4 @@ Ocelot will now send tracing information to Butterfly when this Route is called. """" -.. [#f1] The ``AddOcelot`` method adds default ASP.NET services to DI-container. You could call another more extended ``AddOcelotUsingBuilder`` method while configuring services to build and use custom builder via an ``IMvcCoreBuilder`` interface object. See more instructions in :doc:`../features/dependencyinjection`, "**The AddOcelotUsingBuilder method**" section. +.. [#f1] :ref:`di-the-addocelot-method` adds default ASP.NET services to DI container. You could call another extended :ref:`di-addocelotusingbuilder-method` while configuring services to develop your own :ref:`di-custom-builder`. See more instructions in the ":ref:`di-addocelotusingbuilder-method`" section of :doc:`../features/dependencyinjection` feature. diff --git a/docs/introduction/gettingstarted.rst b/docs/introduction/gettingstarted.rst index 333261992..2640b4e4b 100644 --- a/docs/introduction/gettingstarted.rst +++ b/docs/introduction/gettingstarted.rst @@ -71,7 +71,11 @@ Program ^^^^^^^ Then in your **Program.cs** you will want to have the following. -The main things to note are ``AddOcelot()`` [#f1]_ (adds Ocelot default services), ``UseOcelot().Wait()`` (sets up all the Ocelot middleware). + +The main things to note are + +* ``AddOcelot()`` adds Ocelot required and default services [#f1]_ +* ``UseOcelot().Wait()`` sets up all the Ocelot middlewares. .. code-block:: csharp @@ -120,4 +124,4 @@ The main things to note are ``AddOcelot()`` [#f1]_ (adds Ocelot default services """" -.. [#f1] The ``AddOcelot`` method adds default ASP.NET services to DI-container. You could call another more extended ``AddOcelotUsingBuilder`` method while configuring services to build and use custom builder via an ``IMvcCoreBuilder`` interface object. See more instructions in :doc:`../features/dependencyinjection`, "**The AddOcelotUsingBuilder method**" section. +.. [#f1] :ref:`di-the-addocelot-method` adds default ASP.NET services to DI container. You could call another extended :ref:`di-addocelotusingbuilder-method` while configuring services to develop your own :ref:`di-custom-builder`. See more instructions in the ":ref:`di-addocelotusingbuilder-method`" section of :doc:`../features/dependencyinjection` feature. diff --git a/src/Ocelot/Configuration/Repository/DiskFileConfigurationRepository.cs b/src/Ocelot/Configuration/Repository/DiskFileConfigurationRepository.cs index 52660cebd..023fbfafc 100644 --- a/src/Ocelot/Configuration/Repository/DiskFileConfigurationRepository.cs +++ b/src/Ocelot/Configuration/Repository/DiskFileConfigurationRepository.cs @@ -2,24 +2,42 @@ using Newtonsoft.Json; using Ocelot.Configuration.ChangeTracking; using Ocelot.Configuration.File; +using Ocelot.DependencyInjection; using Ocelot.Responses; +using FileSys = System.IO.File; namespace Ocelot.Configuration.Repository { public class DiskFileConfigurationRepository : IFileConfigurationRepository { + private readonly IWebHostEnvironment _hostingEnvironment; private readonly IOcelotConfigurationChangeTokenSource _changeTokenSource; - private readonly string _environmentFilePath; - private readonly string _ocelotFilePath; - private static readonly object _lock = new(); - private const string ConfigurationFileName = "ocelot"; + private FileInfo _ocelotFile; + private FileInfo _environmentFile; + private readonly object _lock = new(); public DiskFileConfigurationRepository(IWebHostEnvironment hostingEnvironment, IOcelotConfigurationChangeTokenSource changeTokenSource) { + _hostingEnvironment = hostingEnvironment; _changeTokenSource = changeTokenSource; - _environmentFilePath = $"{AppContext.BaseDirectory}{ConfigurationFileName}{(string.IsNullOrEmpty(hostingEnvironment.EnvironmentName) ? string.Empty : ".")}{hostingEnvironment.EnvironmentName}.json"; + Initialize(AppContext.BaseDirectory); + } + + public DiskFileConfigurationRepository(IWebHostEnvironment hostingEnvironment, IOcelotConfigurationChangeTokenSource changeTokenSource, string folder) + { + _hostingEnvironment = hostingEnvironment; + _changeTokenSource = changeTokenSource; + Initialize(folder); + } - _ocelotFilePath = $"{AppContext.BaseDirectory}{ConfigurationFileName}.json"; + private void Initialize(string folder) + { + folder ??= AppContext.BaseDirectory; + _ocelotFile = new FileInfo(Path.Combine(folder, ConfigurationBuilderExtensions.PrimaryConfigFile)); + var envFile = !string.IsNullOrEmpty(_hostingEnvironment.EnvironmentName) + ? string.Format(ConfigurationBuilderExtensions.EnvironmentConfigFile, _hostingEnvironment.EnvironmentName) + : ConfigurationBuilderExtensions.PrimaryConfigFile; + _environmentFile = new FileInfo(Path.Combine(folder, envFile)); } public Task> Get() @@ -28,7 +46,7 @@ public Task> Get() lock (_lock) { - jsonConfiguration = System.IO.File.ReadAllText(_environmentFilePath); + jsonConfiguration = FileSys.ReadAllText(_environmentFile.FullName); } var fileConfiguration = JsonConvert.DeserializeObject(jsonConfiguration); @@ -42,19 +60,19 @@ public Task Set(FileConfiguration fileConfiguration) lock (_lock) { - if (System.IO.File.Exists(_environmentFilePath)) + if (_environmentFile.Exists) { - System.IO.File.Delete(_environmentFilePath); + _environmentFile.Delete(); } - System.IO.File.WriteAllText(_environmentFilePath, jsonConfiguration); + FileSys.WriteAllText(_environmentFile.FullName, jsonConfiguration); - if (System.IO.File.Exists(_ocelotFilePath)) + if (_ocelotFile.Exists) { - System.IO.File.Delete(_ocelotFilePath); + _ocelotFile.Delete(); } - System.IO.File.WriteAllText(_ocelotFilePath, jsonConfiguration); + FileSys.WriteAllText(_ocelotFile.FullName, jsonConfiguration); } _changeTokenSource.Activate(); diff --git a/src/Ocelot/DependencyInjection/ConfigurationBuilderExtensions.cs b/src/Ocelot/DependencyInjection/ConfigurationBuilderExtensions.cs index 4649ecd0b..10f4e695b 100644 --- a/src/Ocelot/DependencyInjection/ConfigurationBuilderExtensions.cs +++ b/src/Ocelot/DependencyInjection/ConfigurationBuilderExtensions.cs @@ -13,16 +13,14 @@ public static partial class ConfigurationBuilderExtensions { public const string PrimaryConfigFile = "ocelot.json"; public const string GlobalConfigFile = "ocelot.global.json"; + public const string EnvironmentConfigFile = "ocelot.{0}.json"; #if NET7_0_OR_GREATER [GeneratedRegex(@"^ocelot\.(.*?)\.json$", RegexOptions.IgnoreCase | RegexOptions.Singleline, "en-US")] private static partial Regex SubConfigRegex(); #else private static readonly Regex SubConfigRegexVar = new(@"^ocelot\.(.*?)\.json$", RegexOptions.IgnoreCase | RegexOptions.Singleline, TimeSpan.FromMilliseconds(1000)); - private static Regex SubConfigRegex() - { - return SubConfigRegexVar; - } + private static Regex SubConfigRegex() => SubConfigRegexVar; #endif [Obsolete("Please set BaseUrl in ocelot.json GlobalConfiguration.BaseUrl")] @@ -36,9 +34,7 @@ public static IConfigurationBuilder AddOcelotBaseUrl(this IConfigurationBuilder }, }; - builder.Add(memorySource); - - return builder; + return builder.Add(memorySource); } /// @@ -47,10 +43,8 @@ public static IConfigurationBuilder AddOcelotBaseUrl(this IConfigurationBuilder /// Configuration builder to extend. /// Web hosting environment object. /// An object. - public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, IWebHostEnvironment env) - { - return builder.AddOcelot(".", env); - } + public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, IWebHostEnvironment env) + => builder.AddOcelot(".", env); /// /// Adds Ocelot configuration by environment, reading the required files from the specified folder. @@ -59,31 +53,84 @@ public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder /// Folder to read files from. /// Web hosting environment object. /// An object. - public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, string folder, IWebHostEnvironment env) + public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, string folder, IWebHostEnvironment env) + => builder.AddOcelot(folder, env, MergeOcelotJson.ToFile); + + /// + /// Adds Ocelot configuration by environment and merge option, reading the required files from the current default folder. + /// + /// Use optional arguments for injections and overridings. + /// Configuration builder to extend. + /// Web hosting environment object. + /// Option to merge files to. + /// Primary config file. + /// Global config file. + /// Environment config file. + /// The 2nd argument of the AddJsonFile. + /// The 3rd argument of the AddJsonFile. + /// An object. + public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, IWebHostEnvironment env, MergeOcelotJson mergeTo, + string primaryConfigFile = null, string globalConfigFile = null, string environmentConfigFile = null, bool? optional = null, bool? reloadOnChange = null) // optional injections + => builder.AddOcelot(".", env, mergeTo, primaryConfigFile, globalConfigFile, environmentConfigFile, optional, reloadOnChange); + + /// + /// Adds Ocelot configuration by environment and merge option, reading the required files from the specified folder. + /// + /// Use optional arguments for injections and overridings. + /// Configuration builder to extend. + /// Folder to read files from. + /// Web hosting environment object. + /// Option to merge files to. + /// Primary config file. + /// Global config file. + /// Environment config file. + /// The 2nd argument of the AddJsonFile. + /// The 3rd argument of the AddJsonFile. + /// An object. + public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, string folder, IWebHostEnvironment env, MergeOcelotJson mergeTo, + string primaryConfigFile = null, string globalConfigFile = null, string environmentConfigFile = null, bool? optional = null, bool? reloadOnChange = null) // optional injections { - var excludeConfigName = env?.EnvironmentName != null ? $"ocelot.{env.EnvironmentName}.json" : string.Empty; - - var reg = SubConfigRegex(); + var json = GetMergedOcelotJson(folder, env, null, primaryConfigFile, globalConfigFile, environmentConfigFile); + return ApplyMergeOcelotJsonOption(builder, mergeTo, json, primaryConfigFile, optional, reloadOnChange); + } + + private static IConfigurationBuilder ApplyMergeOcelotJsonOption(IConfigurationBuilder builder, MergeOcelotJson mergeTo, string json, + string primaryConfigFile, bool? optional, bool? reloadOnChange) + { + return mergeTo == MergeOcelotJson.ToMemory ? + builder.AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json))) : + AddOcelotJsonFile(builder, json, primaryConfigFile, optional, reloadOnChange); + } + private static string GetMergedOcelotJson(string folder, IWebHostEnvironment env, + FileConfiguration fileConfiguration = null, string primaryFile = null, string globalFile = null, string environmentFile = null) + { + environmentFile ??= env?.EnvironmentName != null ? string.Format(EnvironmentConfigFile, env.EnvironmentName) : string.Empty; + var reg = SubConfigRegex(); + var environmentFileInfo = new FileInfo(environmentFile); var files = new DirectoryInfo(folder) .EnumerateFiles() - .Where(fi => reg.IsMatch(fi.Name) && fi.Name != excludeConfigName) + .Where(fi => reg.IsMatch(fi.Name) && fi.Name != environmentFileInfo.Name && fi.FullName != environmentFileInfo.FullName) .ToArray(); - - var fileConfiguration = new FileConfiguration(); - + + fileConfiguration ??= new FileConfiguration(); + primaryFile ??= PrimaryConfigFile; + globalFile ??= GlobalConfigFile; + var primaryFileInfo = new FileInfo(primaryFile); + var globalFileInfo = new FileInfo(globalFile); foreach (var file in files) { - if (files.Length > 1 && file.Name.Equals(PrimaryConfigFile, StringComparison.OrdinalIgnoreCase)) + if (files.Length > 1 && + file.Name.Equals(primaryFileInfo.Name, StringComparison.OrdinalIgnoreCase) && + file.FullName.Equals(primaryFileInfo.FullName, StringComparison.OrdinalIgnoreCase)) { continue; } var lines = File.ReadAllText(file.FullName); - var config = JsonConvert.DeserializeObject(lines); - - if (file.Name.Equals(GlobalConfigFile, StringComparison.OrdinalIgnoreCase)) + if (file.Name.Equals(globalFileInfo.Name, StringComparison.OrdinalIgnoreCase) && + file.FullName.Equals(globalFileInfo.FullName, StringComparison.OrdinalIgnoreCase)) { fileConfiguration.GlobalConfiguration = config.GlobalConfiguration; } @@ -92,23 +139,65 @@ public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder fileConfiguration.Routes.AddRange(config.Routes); } - return builder.AddOcelot(fileConfiguration); - } - + return JsonConvert.SerializeObject(fileConfiguration, Formatting.Indented); + } + /// /// Adds Ocelot configuration by ready configuration object and writes JSON to the primary configuration file.
/// Finally, adds JSON file as configuration provider. ///
+ /// Use optional arguments for injections and overridings. + /// Configuration builder to extend. + /// File configuration to add as JSON provider. + /// Primary config file. + /// The 2nd argument of the AddJsonFile. + /// The 3rd argument of the AddJsonFile. + /// An object. + public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, FileConfiguration fileConfiguration, + string primaryConfigFile = null, bool? optional = null, bool? reloadOnChange = null) // optional injections + { + var json = JsonConvert.SerializeObject(fileConfiguration, Formatting.Indented); + return AddOcelotJsonFile(builder, json, primaryConfigFile, optional, reloadOnChange); + } + + /// + /// Adds Ocelot configuration by ready configuration object, environment and merge option, reading the required files from the current default folder. + /// /// Configuration builder to extend. /// File configuration to add as JSON provider. + /// Web hosting environment object. + /// Option to merge files to. + /// Primary config file. + /// Global config file. + /// Environment config file. + /// The 2nd argument of the AddJsonFile. + /// The 3rd argument of the AddJsonFile. /// An object. - public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, FileConfiguration fileConfiguration) + public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, FileConfiguration fileConfiguration, IWebHostEnvironment env, MergeOcelotJson mergeTo, + string primaryConfigFile = null, string globalConfigFile = null, string environmentConfigFile = null, bool? optional = null, bool? reloadOnChange = null) // optional injections { - var json = JsonConvert.SerializeObject(fileConfiguration); - - File.WriteAllText(PrimaryConfigFile, json); - - return builder.AddJsonFile(PrimaryConfigFile, false, false); + var json = GetMergedOcelotJson(".", env, fileConfiguration, primaryConfigFile, globalConfigFile, environmentConfigFile); + return ApplyMergeOcelotJsonOption(builder, mergeTo, json, primaryConfigFile, optional, reloadOnChange); + } + + /// + /// Adds Ocelot primary configuration file (aka ocelot.json).
+ /// Writes JSON to the file.
+ /// Adds the file as a JSON configuration provider via the extension. + ///
+ /// Use optional arguments for injections and overridings. + /// The builder to extend. + /// JSON data of the Ocelot configuration. + /// Primary config file. + /// The 2nd argument of the AddJsonFile. + /// The 3rd argument of the AddJsonFile. + /// An object. + private static IConfigurationBuilder AddOcelotJsonFile(IConfigurationBuilder builder, string json, + string primaryFile = null, bool? optional = null, bool? reloadOnChange = null) // optional injections + { + var primary = primaryFile ?? PrimaryConfigFile; + File.WriteAllText(primary, json); + return builder?.AddJsonFile(primary, optional ?? false, reloadOnChange ?? false); } } } diff --git a/src/Ocelot/DependencyInjection/MergeOcelotJson.cs b/src/Ocelot/DependencyInjection/MergeOcelotJson.cs new file mode 100644 index 000000000..7d7de5e62 --- /dev/null +++ b/src/Ocelot/DependencyInjection/MergeOcelotJson.cs @@ -0,0 +1,14 @@ +namespace Ocelot.DependencyInjection; + +public enum MergeOcelotJson +{ + /// + /// The option to merge all configuration files to one primary config file aka ocelot.json. + /// + ToFile = 0, + + /// + /// The option to merge all configuration files to memory and reuse the config by in-memory configuration provider. + /// + ToMemory = 1, +} diff --git a/test/Ocelot.AcceptanceTests/Authentication/AuthenticationSteps.cs b/test/Ocelot.AcceptanceTests/Authentication/AuthenticationSteps.cs index 29e1cb286..0c2fb65fc 100644 --- a/test/Ocelot.AcceptanceTests/Authentication/AuthenticationSteps.cs +++ b/test/Ocelot.AcceptanceTests/Authentication/AuthenticationSteps.cs @@ -154,13 +154,6 @@ internal Task GivenAuthToken(string url, string apiScope, string cl }, }; - public static FileConfiguration GivenConfiguration(params FileRoute[] routes) - { - var configuration = new FileConfiguration(); - configuration.Routes.AddRange(routes); - return configuration; - } - protected void GivenThereIsAServiceRunningOn(int port, HttpStatusCode statusCode, string responseBody) { var url = DownstreamServiceUrl(port); diff --git a/test/Ocelot.AcceptanceTests/ConfigurationMergeTests.cs b/test/Ocelot.AcceptanceTests/ConfigurationMergeTests.cs new file mode 100644 index 000000000..3836dcbf6 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/ConfigurationMergeTests.cs @@ -0,0 +1,68 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Ocelot.Configuration.File; +using Ocelot.DependencyInjection; +using System.Runtime.CompilerServices; + +namespace Ocelot.AcceptanceTests; + +[Trait("PR", "1227")] +[Trait("Issue", "1216")] +public sealed class ConfigurationMergeTests : Steps +{ + private readonly FileConfiguration _globalConfig; + private readonly string _globalConfigFileName; + + public ConfigurationMergeTests() : base() + { + _globalConfig = new(); + _globalConfigFileName = $"{TestID}-{ConfigurationBuilderExtensions.GlobalConfigFile}"; + } + + protected override void DeleteOcelotConfig(params string[] files) => base.DeleteOcelotConfig(_globalConfigFileName); + + [Fact] + public void Should_run_with_global_config_merged_to_memory() + { + Arrange(); + + // Act + GivenOcelotIsRunningMergedConfig(MergeOcelotJson.ToMemory); + + // Assert + TheOcelotPrimaryConfigFileExists(false); + Assert(); + } + + [Fact] + public void Should_run_with_global_config_merged_to_file() + { + Arrange(); + + // Act + GivenOcelotIsRunningMergedConfig(MergeOcelotJson.ToFile); + + // Assert + TheOcelotPrimaryConfigFileExists(true); + Assert(); + } + + private void GivenOcelotIsRunningMergedConfig(MergeOcelotJson mergeTo) + => StartOcelot((context, config) => config.AddOcelot(_globalConfig, context.HostingEnvironment, mergeTo, _ocelotConfigFileName, _globalConfigFileName, null, false, false)); + + private void TheOcelotPrimaryConfigFileExists(bool expected) + => File.Exists(_ocelotConfigFileName).ShouldBe(expected); + + private void Arrange([CallerMemberName] string testName = null) + { + _globalConfig.GlobalConfiguration.RequestIdKey = testName; + } + + private void Assert([CallerMemberName] string testName = null) + { + var config = _ocelotServer.Services.GetService(); + config.ShouldNotBeNull(); + var actual = config["GlobalConfiguration:RequestIdKey"]; + actual.ShouldNotBeNull().ShouldBe(testName); + } +} diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index cacbd5f43..fe4b2bc7f 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -46,6 +46,7 @@ public class Steps : IDisposable private BearerToken _token; public string RequestIdKey = "OcRequestId"; private readonly Random _random; + protected readonly Guid _testId; protected readonly string _ocelotConfigFileName; protected IWebHostBuilder _webHostBuilder; private WebHostBuilder _ocelotBuilder; @@ -55,9 +56,12 @@ public class Steps : IDisposable public Steps() { _random = new Random(); - _ocelotConfigFileName = $"{Guid.NewGuid():N}-ocelot.json"; + _testId = Guid.NewGuid(); + _ocelotConfigFileName = $"{_testId:N}-{ConfigurationBuilderExtensions.PrimaryConfigFile}"; } + protected string TestID { get => _testId.ToString("N"); } + protected static string DownstreamUrl(int port) => $"{Uri.UriSchemeHttp}://localhost:{port}"; protected static FileConfiguration GivenConfiguration(params FileRoute[] routes) => new() @@ -168,22 +172,26 @@ public void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) File.WriteAllText(_ocelotConfigFileName, jsonConfiguration); } - private void DeleteOcelotConfig() + protected virtual void DeleteOcelotConfig(params string[] files) { - if (!File.Exists(_ocelotConfigFileName)) + var allFiles = files.Append(_ocelotConfigFileName); + foreach (var file in allFiles) { - return; - } + if (!File.Exists(file)) + { + continue; + } - try - { - File.Delete(_ocelotConfigFileName); - } - catch (Exception e) - { - Console.WriteLine(e); + try + { + File.Delete(file); + } + catch (Exception e) + { + Console.WriteLine(e); + } } - } + } public void ThenTheResponseBodyHeaderIs(string key, string value) { @@ -193,24 +201,7 @@ public void ThenTheResponseBodyHeaderIs(string key, string value) public void GivenOcelotIsRunningReloadingConfig(bool shouldReload) { - _webHostBuilder = new WebHostBuilder(); - - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", true, false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); - config.AddJsonFile(_ocelotConfigFileName, false, shouldReload); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => { s.AddOcelot(); }) - .Configure(app => { app.UseOcelot().Wait(); }); - - _ocelotServer = new TestServer(_webHostBuilder); - - _ocelotClient = _ocelotServer.CreateClient(); + StartOcelot((_, config) => config.AddJsonFile(_ocelotConfigFileName, false, shouldReload)); } public void GivenIHaveAChangeToken() @@ -222,24 +213,29 @@ public void GivenIHaveAChangeToken() /// This is annoying cos it should be in the constructor but we need to set up the file before calling startup so its a step. ///
public void GivenOcelotIsRunning() + { + StartOcelot((_, config) => config.AddJsonFile(_ocelotConfigFileName, false, false)); + } + + protected void StartOcelot(Action configureAddOcelot) { _webHostBuilder = new WebHostBuilder(); _webHostBuilder .ConfigureAppConfiguration((hostingContext, config) => { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); var env = hostingContext.HostingEnvironment; + config.SetBasePath(env.ContentRootPath); config.AddJsonFile("appsettings.json", true, false) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); - config.AddJsonFile(_ocelotConfigFileName, false, false); + configureAddOcelot.Invoke(hostingContext, config); // config.AddOcelot(...); config.AddEnvironmentVariables(); }) - .ConfigureServices(s => { s.AddOcelot(); }) - .Configure(app => { app.UseOcelot().Wait(); }); + .ConfigureServices(WithAddOcelot) + .Configure(WithUseOcelot) + .UseEnvironment(nameof(AcceptanceTests)); _ocelotServer = new TestServer(_webHostBuilder); - _ocelotClient = _ocelotServer.CreateClient(); } diff --git a/test/Ocelot.AcceptanceTests/TestConfiguration.cs b/test/Ocelot.AcceptanceTests/TestConfiguration.cs deleted file mode 100644 index 95e5fe9b7..000000000 --- a/test/Ocelot.AcceptanceTests/TestConfiguration.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Ocelot.AcceptanceTests -{ - public static class TestConfiguration - { - public static string ConfigurationPath => Path.Combine(AppContext.BaseDirectory, "ocelot.json"); - } -} diff --git a/test/Ocelot.UnitTests/Configuration/DiskFileConfigurationRepositoryTests.cs b/test/Ocelot.UnitTests/Configuration/DiskFileConfigurationRepositoryTests.cs index 1cdad5b02..30a6354a1 100644 --- a/test/Ocelot.UnitTests/Configuration/DiskFileConfigurationRepositoryTests.cs +++ b/test/Ocelot.UnitTests/Configuration/DiskFileConfigurationRepositoryTests.cs @@ -3,128 +3,129 @@ using Ocelot.Configuration.ChangeTracking; using Ocelot.Configuration.File; using Ocelot.Configuration.Repository; +using Ocelot.DependencyInjection; +using System.Runtime.CompilerServices; namespace Ocelot.UnitTests.Configuration { - public class DiskFileConfigurationRepositoryTests : IDisposable + public sealed class DiskFileConfigurationRepositoryTests : FileUnitTest { private readonly Mock _hostingEnvironment; private readonly Mock _changeTokenSource; private IFileConfigurationRepository _repo; - private string _environmentSpecificPath; - private string _ocelotJsonPath; private FileConfiguration _result; - private FileConfiguration _fileConfiguration; - - // This is a bit dirty and it is dev.dev so that the ConfigurationBuilderExtensionsTests - // cant pick it up if they run in parralel..and the semaphore stops them running at the same time...sigh - // these are not really unit tests but whatever... - private string _environmentName = "DEV.DEV"; - - private static SemaphoreSlim _semaphore; public DiskFileConfigurationRepositoryTests() { - _semaphore = new SemaphoreSlim(1, 1); - _semaphore.Wait(); _hostingEnvironment = new Mock(); - _hostingEnvironment.Setup(he => he.EnvironmentName).Returns(_environmentName); _changeTokenSource = new Mock(MockBehavior.Strict); _changeTokenSource.Setup(m => m.Activate()); - _repo = new DiskFileConfigurationRepository(_hostingEnvironment.Object, _changeTokenSource.Object); + } + + private void Arrange([CallerMemberName] string testName = null) + { + _hostingEnvironment.Setup(he => he.EnvironmentName).Returns(testName); + _repo = new DiskFileConfigurationRepository(_hostingEnvironment.Object, _changeTokenSource.Object, TestID); } [Fact] - public void should_return_file_configuration() - { + public void Should_return_file_configuration() + { + Arrange(); var config = FakeFileConfigurationForGet(); - - this.Given(_ => GivenTheConfigurationIs(config)) - .When(_ => WhenIGetTheRoutes()) - .Then(_ => ThenTheFollowingIsReturned(config)) - .BDDfy(); + GivenTheConfigurationIs(config); + + // Act + WhenIGetTheRoutes(); + + // Assert + ThenTheFollowingIsReturned(config); } [Fact] - public void should_return_file_configuration_if_environment_name_is_unavailable() + public void Should_return_file_configuration_if_environment_name_is_unavailable() { + Arrange(); var config = FakeFileConfigurationForGet(); - - this.Given(_ => GivenTheEnvironmentNameIsUnavailable()) - .And(_ => GivenTheConfigurationIs(config)) - .When(_ => WhenIGetTheRoutes()) - .Then(_ => ThenTheFollowingIsReturned(config)) - .BDDfy(); + GivenTheEnvironmentNameIsUnavailable(); + GivenTheConfigurationIs(config); + + // Act + WhenIGetTheRoutes(); + + // Assert + ThenTheFollowingIsReturned(config); } [Fact] - public void should_set_file_configuration() + public void Should_set_file_configuration() { + Arrange(); var config = FakeFileConfigurationForSet(); - - this.Given(_ => GivenIHaveAConfiguration(config)) - .When(_ => WhenISetTheConfiguration()) - .Then(_ => ThenTheConfigurationIsStoredAs(config)) - .And(_ => ThenTheConfigurationJsonIsIndented(config)) - .And(x => AndTheChangeTokenIsActivated()) - .BDDfy(); + + // Act + WhenISetTheConfiguration(config); + + // Assert + ThenTheConfigurationIsStoredAs(config); + ThenTheConfigurationJsonIsIndented(config); + AndTheChangeTokenIsActivated(); } [Fact] - public void should_set_file_configuration_if_environment_name_is_unavailable() + public void Should_set_file_configuration_if_environment_name_is_unavailable() { + Arrange(); var config = FakeFileConfigurationForSet(); - - this.Given(_ => GivenIHaveAConfiguration(config)) - .And(_ => GivenTheEnvironmentNameIsUnavailable()) - .When(_ => WhenISetTheConfiguration()) - .Then(_ => ThenTheConfigurationIsStoredAs(config)) - .And(_ => ThenTheConfigurationJsonIsIndented(config)) - .BDDfy(); + GivenTheEnvironmentNameIsUnavailable(); + + // Act + WhenISetTheConfiguration(config); + + // Assert + ThenTheConfigurationIsStoredAs(config); + ThenTheConfigurationJsonIsIndented(config); } [Fact] - public void should_set_environment_file_configuration_and_ocelot_file_configuration() + public void Should_set_environment_file_configuration_and_ocelot_file_configuration() { + Arrange(); var config = FakeFileConfigurationForSet(); - - this.Given(_ => GivenIHaveAConfiguration(config)) - .And(_ => GivenTheConfigurationIs(config)) - .And(_ => GivenTheUserAddedOcelotJson()) - .When(_ => WhenISetTheConfiguration()) - .Then(_ => ThenTheConfigurationIsStoredAs(config)) - .And(_ => ThenTheConfigurationJsonIsIndented(config)) - .Then(_ => ThenTheOcelotJsonIsStoredAs(config)) - .BDDfy(); - } - - private void GivenTheUserAddedOcelotJson() - { - _ocelotJsonPath = $"{AppContext.BaseDirectory}/ocelot.json"; - - if (File.Exists(_ocelotJsonPath)) + GivenTheConfigurationIs(config); + var ocelotJson = GivenTheUserAddedOcelotJson(); + + // Act + WhenISetTheConfiguration(config); + + // Assert + ThenTheConfigurationIsStoredAs(config); + ThenTheConfigurationJsonIsIndented(config); + ThenTheOcelotJsonIsStoredAs(ocelotJson, config); + } + + private FileInfo GivenTheUserAddedOcelotJson() + { + var primaryFile = Path.Combine(TestID, ConfigurationBuilderExtensions.PrimaryConfigFile); + var ocelotJson = new FileInfo(primaryFile); + if (ocelotJson.Exists) { - File.Delete(_ocelotJsonPath); + ocelotJson.Delete(); } - File.WriteAllText(_ocelotJsonPath, "Doesnt matter"); + File.WriteAllText(ocelotJson.FullName, "Doesnt matter"); + _files.Add(ocelotJson.FullName); + return ocelotJson; } private void GivenTheEnvironmentNameIsUnavailable() { - _environmentName = null; - _hostingEnvironment.Setup(he => he.EnvironmentName).Returns(_environmentName); - _repo = new DiskFileConfigurationRepository(_hostingEnvironment.Object, _changeTokenSource.Object); - } - - private void GivenIHaveAConfiguration(FileConfiguration fileConfiguration) - { - _fileConfiguration = fileConfiguration; + _hostingEnvironment.Setup(he => he.EnvironmentName).Returns((string)null); } - private void WhenISetTheConfiguration() + private void WhenISetTheConfiguration(FileConfiguration fileConfiguration) { - _repo.Set(_fileConfiguration); + _repo.Set(fileConfiguration); _result = _repo.Get().Result.Data; } @@ -151,34 +152,34 @@ private void ThenTheConfigurationIsStoredAs(FileConfiguration expecteds) } } - private void ThenTheOcelotJsonIsStoredAs(FileConfiguration expecteds) + private void ThenTheOcelotJsonIsStoredAs(FileInfo ocelotJson, FileConfiguration expecteds) { - var resultText = File.ReadAllText(_ocelotJsonPath); + var actual = File.ReadAllText(ocelotJson.FullName); var expectedText = JsonConvert.SerializeObject(expecteds, Formatting.Indented); - resultText.ShouldBe(expectedText); + actual.ShouldBe(expectedText); } - private void GivenTheConfigurationIs(FileConfiguration fileConfiguration) + private void GivenTheConfigurationIs(FileConfiguration fileConfiguration, [CallerMemberName] string environmentName = null) { - _environmentSpecificPath = $"{AppContext.BaseDirectory}/ocelot{(string.IsNullOrEmpty(_environmentName) ? string.Empty : ".")}{_environmentName}.json"; - + var environmentSpecificPath = Path.Combine(TestID, string.Format(ConfigurationBuilderExtensions.EnvironmentConfigFile, environmentName)); var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration, Formatting.Indented); - - if (File.Exists(_environmentSpecificPath)) + var environmentSpecific = new FileInfo(environmentSpecificPath); + if (environmentSpecific.Exists) { - File.Delete(_environmentSpecificPath); + environmentSpecific.Delete(); } - File.WriteAllText(_environmentSpecificPath, jsonConfiguration); + File.WriteAllText(environmentSpecific.FullName, jsonConfiguration); + _files.Add(environmentSpecific.FullName); } - private void ThenTheConfigurationJsonIsIndented(FileConfiguration expecteds) + private void ThenTheConfigurationJsonIsIndented(FileConfiguration expecteds, [CallerMemberName] string environmentName = null) { - var path = !string.IsNullOrEmpty(_environmentSpecificPath) ? _environmentSpecificPath : _environmentSpecificPath = $"{AppContext.BaseDirectory}/ocelot{(string.IsNullOrEmpty(_environmentName) ? string.Empty : ".")}{_environmentName}.json"; - - var resultText = File.ReadAllText(path); + var environmentSpecific = Path.Combine(TestID, string.Format(ConfigurationBuilderExtensions.EnvironmentConfigFile, environmentName)); + var actual = File.ReadAllText(environmentSpecific); var expectedText = JsonConvert.SerializeObject(expecteds, Formatting.Indented); - resultText.ShouldBe(expectedText); + actual.ShouldBe(expectedText); + _files.Add(environmentSpecific); } private void WhenIGetTheRoutes() @@ -216,79 +217,34 @@ private void AndTheChangeTokenIsActivated() private static FileConfiguration FakeFileConfigurationForSet() { - var routes = new List - { - new() - { - DownstreamHostAndPorts = new List - { - new() - { - Host = "123.12.12.12", - Port = 80, - }, - }, - DownstreamScheme = "https", - DownstreamPathTemplate = "/asdfs/test/{test}", - }, - }; - - var globalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "https", - Port = 198, - Host = "blah", - }, - }; - - return new FileConfiguration - { - GlobalConfiguration = globalConfiguration, - Routes = routes, - }; + var route = GivenRoute("123.12.12.12", "/asdfs/test/{test}"); + return GivenConfiguration(route); } private static FileConfiguration FakeFileConfigurationForGet() { - var routes = new List - { - new() - { - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = 80, - }, - }, - DownstreamScheme = "https", - DownstreamPathTemplate = "/test/test/{test}", - }, + var route = GivenRoute("localhost", "/test/test/{test}"); + return GivenConfiguration(route); + } + + private static FileRoute GivenRoute(string host, string downstream) => new() + { + DownstreamHostAndPorts = [new(host, 80)], + DownstreamScheme = Uri.UriSchemeHttps, + DownstreamPathTemplate = downstream, + }; + + private static FileConfiguration GivenConfiguration(params FileRoute[] routes) + { + var config = new FileConfiguration(); + config.Routes.AddRange(routes); + config.GlobalConfiguration.ServiceDiscoveryProvider = new() + { + Scheme = "https", + Port = 198, + Host = "blah", }; - - var globalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "https", - Port = 198, - Host = "blah", - }, - }; - - return new FileConfiguration - { - GlobalConfiguration = globalConfiguration, - Routes = routes, - }; - } - - public void Dispose() - { - _semaphore.Release(); + return config; } } } diff --git a/test/Ocelot.UnitTests/DependencyInjection/ConfigurationBuilderExtensionsTests.cs b/test/Ocelot.UnitTests/DependencyInjection/ConfigurationBuilderExtensionsTests.cs index 80a00bb1a..5a77e5185 100644 --- a/test/Ocelot.UnitTests/DependencyInjection/ConfigurationBuilderExtensionsTests.cs +++ b/test/Ocelot.UnitTests/DependencyInjection/ConfigurationBuilderExtensionsTests.cs @@ -1,15 +1,16 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Newtonsoft.Json; using Ocelot.Configuration.File; using Ocelot.DependencyInjection; - +using System.Runtime.CompilerServices; + namespace Ocelot.UnitTests.DependencyInjection { - public class ConfigurationBuilderExtensionsTests + public sealed class ConfigurationBuilderExtensionsTests : FileUnitTest { private IConfigurationRoot _configuration; - private string _result; private IConfigurationRoot _configRoot; private FileConfiguration _globalConfig; private FileConfiguration _routeA; @@ -17,78 +18,112 @@ public class ConfigurationBuilderExtensionsTests private FileConfiguration _aggregate; private FileConfiguration _envSpecific; private FileConfiguration _combinedFileConfiguration; - private readonly Mock _hostingEnvironment; + private readonly Mock _hostingEnvironment; public ConfigurationBuilderExtensionsTests() { - _hostingEnvironment = new Mock(); - - // Clean up config files before each test - var subConfigFiles = new DirectoryInfo(".").GetFiles("ocelot.*.json"); - - foreach (var config in subConfigFiles) - { - config.Delete(); - } - } - + _hostingEnvironment = new Mock(); + } + + protected override string EnvironmentName() + => _hostingEnvironment?.Object?.EnvironmentName ?? base.EnvironmentName(); + [Fact] public void Should_add_base_url_to_config() - { - this.Given(_ => GivenTheBaseUrl("test")) - .When(_ => WhenIGet("BaseUrl")) - .Then(_ => ThenTheResultIs("test")) - .BDDfy(); + { + // Arrange +#pragma warning disable CS0618 + _configuration = new ConfigurationBuilder() + .AddOcelotBaseUrl("test") + .Build(); +#pragma warning restore CS0618 + + // Act + var actual = _configuration.GetValue("BaseUrl", string.Empty); + + // Assert + actual.ShouldBe("test"); + } - [Fact] - public void Should_merge_files() - { - this.Given(_ => GivenMultipleConfigurationFiles(string.Empty, false)) - .And(_ => GivenTheEnvironmentIs(null)) - .When(_ => WhenIAddOcelotConfiguration()) - .Then(_ => ThenTheConfigsAreMergedAndAddedInApplicationConfiguration(false)) - .BDDfy(); + [Fact] + [Trait("PR", "1227")] + [Trait("Issue", "1216")] + public void Should_merge_files_to_file() + { + // Arrange + GivenTheEnvironmentIs(TestID); + GivenMultipleConfigurationFiles(TestID); + + // Act + WhenIAddOcelotConfiguration(TestID); + + // Assert + ThenTheConfigsAreMergedAndAddedInApplicationConfiguration(false); + TheOcelotPrimaryConfigFileExists(true); } [Fact] public void Should_store_given_configurations_when_provided_file_configuration_object() - { - this.Given(_ => GivenCombinedFileConfigurationObject(string.Empty)) - .And(_ => GivenTheEnvironmentIs(null)) - .When(_ => WhenIAddOcelotConfigurationWithCombinedFileConfiguration()) - .Then(_ => ThenTheConfigsAreMergedAndAddedInApplicationConfiguration(true)) - .BDDfy(); + { + // Arrange + GivenTheEnvironmentIs(TestID); + GivenCombinedFileConfigurationObject(); + + // Act + WhenIAddOcelotConfigurationWithCombinedFileConfiguration(); + + // Assert + ThenTheConfigsAreMergedAndAddedInApplicationConfiguration(true); } [Fact] public void Should_merge_files_except_env() { - this.Given(_ => GivenMultipleConfigurationFiles(string.Empty, true)) - .And(_ => GivenTheEnvironmentIs("Env")) - .When(_ => WhenIAddOcelotConfiguration()) - .Then(_ => ThenTheConfigsAreMergedAndAddedInApplicationConfiguration(false)) - .And(_ => NotContainsEnvSpecificConfig()) - .BDDfy(); + // Arrange + GivenTheEnvironmentIs(TestID); + GivenMultipleConfigurationFiles(TestID, true); + + // Act + WhenIAddOcelotConfiguration(TestID); + + // Assert + ThenTheConfigsAreMergedAndAddedInApplicationConfiguration(false); + NotContainsEnvSpecificConfig(); } [Fact] public void Should_merge_files_in_specific_folder() { - var configFolder = "ConfigFiles"; - this.Given(_ => GivenMultipleConfigurationFiles(configFolder, false)) - .When(_ => WhenIAddOcelotConfigurationWithSpecificFolder(configFolder)) - .Then(_ => ThenTheConfigsAreMergedAndAddedInApplicationConfiguration(false)) - .BDDfy(); + // Arrange + GivenMultipleConfigurationFiles(TestID); + + // Act + WhenIAddOcelotConfiguration(TestID); + + // Assert + ThenTheConfigsAreMergedAndAddedInApplicationConfiguration(false); } - private void GivenCombinedFileConfigurationObject(string folder) - { - if (!string.IsNullOrEmpty(folder)) - { - Directory.CreateDirectory(folder); - } + [Fact] + [Trait("PR", "1227")] + [Trait("Issue", "1216")] + public void Should_merge_files_to_memory() + { + // Arrange + GivenTheEnvironmentIs(TestID); + GivenMultipleConfigurationFiles(TestID); + + // Act + WhenIAddOcelotConfiguration(TestID, MergeOcelotJson.ToMemory); + + // Assert + ThenTheConfigsAreMergedAndAddedInApplicationConfiguration(false); + TheOcelotPrimaryConfigFileExists(false); + } + private void GivenCombinedFileConfigurationObject() + { _combinedFileConfiguration = new FileConfiguration { GlobalConfiguration = GetFileGlobalConfigurationData(), @@ -97,235 +132,107 @@ private void GivenCombinedFileConfigurationObject(string folder) }; } - private void GivenMultipleConfigurationFiles(string folder, bool addEnvSpecificConfig) + private void GivenMultipleConfigurationFiles(string folder, bool withEnvironment = false) { - if (!string.IsNullOrEmpty(folder)) - { - Directory.CreateDirectory(folder); - } - - _globalConfig = new FileConfiguration - { - GlobalConfiguration = GetFileGlobalConfigurationData(), - }; - - _routeA = new FileConfiguration - { - Routes = GetServiceARoutes(), - }; - - _routeB = new FileConfiguration - { - Routes = GetServiceBRoutes(), - }; - - _aggregate = new FileConfiguration - { - Aggregates = GetFileAggregatesRouteData(), - }; - - _envSpecific = new FileConfiguration - { - Routes = GetEnvironmentSpecificRoutes(), + _globalConfig = new() { GlobalConfiguration = GetFileGlobalConfigurationData() }; + _routeA = new() { Routes = GetServiceARoutes() }; + _routeB = new() { Routes = GetServiceBRoutes() }; + _aggregate = new() { Aggregates = GetFileAggregatesRouteData() }; + _envSpecific = new() { Routes = GetEnvironmentSpecificRoutes() }; + + var configParts = new Dictionary + { + { "global", _globalConfig }, + { "routesA", _routeA }, + { "routesB", _routeB }, + { "aggregates", _aggregate }, }; - - var globalFilename = Path.Combine(folder, "ocelot.global.json"); - var routesAFilename = Path.Combine(folder, "ocelot.routesA.json"); - var routesBFilename = Path.Combine(folder, "ocelot.routesB.json"); - var aggregatesFilename = Path.Combine(folder, "ocelot.aggregates.json"); - - File.WriteAllText(globalFilename, JsonConvert.SerializeObject(_globalConfig)); - File.WriteAllText(routesAFilename, JsonConvert.SerializeObject(_routeA)); - File.WriteAllText(routesBFilename, JsonConvert.SerializeObject(_routeB)); - File.WriteAllText(aggregatesFilename, JsonConvert.SerializeObject(_aggregate)); - - if (addEnvSpecificConfig) - { - var envSpecificFilename = Path.Combine(folder, "ocelot.Env.json"); - File.WriteAllText(envSpecificFilename, JsonConvert.SerializeObject(_envSpecific)); + + if (withEnvironment) + { + configParts.Add(EnvironmentName(), _envSpecific); } - } - private static FileGlobalConfiguration GetFileGlobalConfigurationData() - { - return new FileGlobalConfiguration + foreach (var part in configParts) { - BaseUrl = "BaseUrl", - RateLimitOptions = new FileRateLimitOptions - { - HttpStatusCode = 500, - ClientIdHeader = "ClientIdHeader", - DisableRateLimitHeaders = true, - QuotaExceededMessage = "QuotaExceededMessage", - RateLimitCounterPrefix = "RateLimitCounterPrefix", - }, - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "https", - Host = "Host", - Port = 80, - Type = "Type", - }, - RequestIdKey = "RequestIdKey", - }; + var filename = Path.Combine(folder, string.Format(ConfigurationBuilderExtensions.EnvironmentConfigFile, part.Key)); + File.WriteAllText(filename, JsonConvert.SerializeObject(part.Value, Formatting.Indented)); + _files.Add(filename); + } } - private static List GetFileAggregatesRouteData() + private static FileGlobalConfiguration GetFileGlobalConfigurationData() => new() { - return new List + BaseUrl = "BaseUrl", + RateLimitOptions = new() + { + HttpStatusCode = 500, + ClientIdHeader = "ClientIdHeader", + DisableRateLimitHeaders = true, + QuotaExceededMessage = "QuotaExceededMessage", + RateLimitCounterPrefix = "RateLimitCounterPrefix", + }, + ServiceDiscoveryProvider = new() + { + Scheme = "https", + Host = "Host", + Port = 80, + Type = "Type", + }, + RequestIdKey = "RequestIdKey", + }; + + private static List GetFileAggregatesRouteData() => + [ + new() { - new() - { - RouteKeys = new List - { - "KeyB", - "KeyBB", - }, - UpstreamPathTemplate = "UpstreamPathTemplate", - }, - new() - { - RouteKeys = new List - { - "KeyB", - "KeyBB", - }, - UpstreamPathTemplate = "UpstreamPathTemplate", - }, - }; - } - - private static List GetServiceARoutes() + RouteKeys = [ "KeyB", "KeyBB" ], + UpstreamPathTemplate = "UpstreamPathTemplate", + }, + ]; + + private static FileRoute GetRoute(string suffix) => new() { - return new List - { - new() - { - DownstreamScheme = "DownstreamScheme", - DownstreamPathTemplate = "DownstreamPathTemplate", - Key = "Key", - UpstreamHost = "UpstreamHost", - UpstreamHttpMethod = new List - { - "UpstreamHttpMethod", - }, - DownstreamHostAndPorts = new List - { - new() - { - Host = "Host", - Port = 80, - }, - }, - }, - }; - } - - private static List GetServiceBRoutes() + DownstreamScheme = "DownstreamScheme" + suffix, + DownstreamPathTemplate = "DownstreamPathTemplate" + suffix, + Key = "Key" + suffix, + UpstreamHost = "UpstreamHost" + suffix, + UpstreamHttpMethod = ["UpstreamHttpMethod" + suffix], + DownstreamHostAndPorts = + [ + new("Host"+suffix, 80), + ], + }; + + private static List GetServiceARoutes() => [GetRoute("A")]; + private static List GetServiceBRoutes() => [GetRoute("B"), GetRoute("BB")]; + private static List GetEnvironmentSpecificRoutes() => [GetRoute("Spec")]; + + private void GivenTheEnvironmentIs(string folder, [CallerMemberName] string testName = null) { - return new List - { - new() - { - DownstreamScheme = "DownstreamSchemeB", - DownstreamPathTemplate = "DownstreamPathTemplateB", - Key = "KeyB", - UpstreamHost = "UpstreamHostB", - UpstreamHttpMethod = new List - { - "UpstreamHttpMethodB", - }, - DownstreamHostAndPorts = new List - { - new() - { - Host = "HostB", - Port = 80, - }, - }, - }, - new() - { - DownstreamScheme = "DownstreamSchemeBB", - DownstreamPathTemplate = "DownstreamPathTemplateBB", - Key = "KeyBB", - UpstreamHost = "UpstreamHostBB", - UpstreamHttpMethod = new List - { - "UpstreamHttpMethodBB", - }, - DownstreamHostAndPorts = new List - { - new() - { - Host = "HostBB", - Port = 80, - }, - }, - }, - }; - } - - private static List GetEnvironmentSpecificRoutes() - { - return new List - { - new() - { - DownstreamScheme = "DownstreamSchemeSpec", - DownstreamPathTemplate = "DownstreamPathTemplateSpec", - Key = "KeySpec", - UpstreamHost = "UpstreamHostSpec", - UpstreamHttpMethod = new List - { - "UpstreamHttpMethodSpec", - }, - DownstreamHostAndPorts = new List - { - new() - { - Host = "HostSpec", - Port = 80, - }, - }, - }, - }; - } - - private void GivenTheEnvironmentIs(string env) - { - _hostingEnvironment.SetupGet(x => x.EnvironmentName).Returns(env); - } - - private void WhenIAddOcelotConfiguration() - { - IConfigurationBuilder builder = new ConfigurationBuilder(); - - builder.AddOcelot(_hostingEnvironment.Object); - - _configRoot = builder.Build(); + _hostingEnvironment.SetupGet(x => x.EnvironmentName).Returns(testName); + _environmentConfigFileName = Path.Combine(folder, string.Format(ConfigurationBuilderExtensions.EnvironmentConfigFile, testName)); + _files.Add(_environmentConfigFileName); } private void WhenIAddOcelotConfigurationWithCombinedFileConfiguration() { - IConfigurationBuilder builder = new ConfigurationBuilder(); - - builder.AddOcelot(_combinedFileConfiguration); - - _configRoot = builder.Build(); + _configRoot = new ConfigurationBuilder() + .AddOcelot(_combinedFileConfiguration, _primaryConfigFileName, false, false) + .Build(); } - private void WhenIAddOcelotConfigurationWithSpecificFolder(string folder) + private void WhenIAddOcelotConfiguration(string folder, MergeOcelotJson mergeOcelotJson = MergeOcelotJson.ToFile) { - IConfigurationBuilder builder = new ConfigurationBuilder(); - builder.AddOcelot(folder, _hostingEnvironment.Object); - _configRoot = builder.Build(); + _configRoot = new ConfigurationBuilder() + .AddOcelot(folder, _hostingEnvironment.Object, mergeOcelotJson, _primaryConfigFileName, _globalConfigFileName, _environmentConfigFileName, false, false) + .Build(); } private void ThenTheConfigsAreMergedAndAddedInApplicationConfiguration(bool useCombinedConfig) { var fc = (FileConfiguration)_configRoot.Get(typeof(FileConfiguration)); - + fc.GlobalConfiguration.BaseUrl.ShouldBe(useCombinedConfig ? _combinedFileConfiguration.GlobalConfiguration.BaseUrl : _globalConfig.GlobalConfiguration.BaseUrl); fc.GlobalConfiguration.RateLimitOptions.ClientIdHeader.ShouldBe(useCombinedConfig ? _combinedFileConfiguration.GlobalConfiguration.RateLimitOptions.ClientIdHeader : _globalConfig.GlobalConfiguration.RateLimitOptions.ClientIdHeader); fc.GlobalConfiguration.RateLimitOptions.DisableRateLimitHeaders.ShouldBe(useCombinedConfig ? _combinedFileConfiguration.GlobalConfiguration.RateLimitOptions.DisableRateLimitHeaders : _globalConfig.GlobalConfiguration.RateLimitOptions.DisableRateLimitHeaders); @@ -366,24 +273,5 @@ private void NotContainsEnvSpecificConfig() fc.Routes.ShouldNotContain(x => x.DownstreamPathTemplate == _envSpecific.Routes[0].DownstreamPathTemplate); fc.Routes.ShouldNotContain(x => x.Key == _envSpecific.Routes[0].Key); } - - private void GivenTheBaseUrl(string baseUrl) - { -#pragma warning disable CS0618 - var builder = new ConfigurationBuilder() - .AddOcelotBaseUrl(baseUrl); -#pragma warning restore CS0618 - _configuration = builder.Build(); - } - - private void WhenIGet(string key) - { - _result = _configuration.GetValue(key, string.Empty); - } - - private void ThenTheResultIs(string expected) - { - _result.ShouldBe(expected); - } } } diff --git a/test/Ocelot.UnitTests/FileUnitTest.cs b/test/Ocelot.UnitTests/FileUnitTest.cs new file mode 100644 index 000000000..1b81b7e8b --- /dev/null +++ b/test/Ocelot.UnitTests/FileUnitTest.cs @@ -0,0 +1,97 @@ +using Ocelot.DependencyInjection; + +namespace Ocelot.UnitTests; + +public class FileUnitTest : UnitTest, IDisposable +{ + protected string _primaryConfigFileName; + protected string _globalConfigFileName; + protected string _environmentConfigFileName; + protected readonly List _files; + protected readonly List _folders; + + protected FileUnitTest() : this(null) { } + + protected FileUnitTest(string folder) + { + folder ??= TestID; + Directory.CreateDirectory(folder); + _folders = [folder]; + + _primaryConfigFileName = Path.Combine(folder, ConfigurationBuilderExtensions.PrimaryConfigFile); + _globalConfigFileName = Path.Combine(folder, ConfigurationBuilderExtensions.GlobalConfigFile); + _environmentConfigFileName = Path.Combine(folder, string.Format(ConfigurationBuilderExtensions.EnvironmentConfigFile, EnvironmentName())); + _files = [_primaryConfigFileName, _globalConfigFileName, _environmentConfigFileName]; + } + + protected virtual string EnvironmentName() => TestID; + + public virtual void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private bool _disposed; + + /// + /// Protected implementation of Dispose pattern. + /// + /// Flag to trigger actual disposing operation. + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (disposing) + { + DeleteFiles(); + DeleteFolders(); + } + + _disposed = true; + } + + protected void DeleteFiles() + { + foreach (var file in _files) + { + try + { + var f = new FileInfo(file); + if (f.Exists) + { + f.Delete(); + } + } + catch (Exception e) + { + Console.WriteLine(e); + } + } + } + + protected void DeleteFolders() + { + foreach (var folder in _folders) + { + try + { + var f = new DirectoryInfo(folder); + if (f.Exists && f.FullName != AppContext.BaseDirectory) + { + f.Delete(true); + } + } + catch (Exception e) + { + Console.WriteLine(e); + } + } + } + + protected void TheOcelotPrimaryConfigFileExists(bool expected) + => File.Exists(_primaryConfigFileName).ShouldBe(expected); +} diff --git a/test/Ocelot.UnitTests/Multiplexing/MultiplexingMiddlewareTests.cs b/test/Ocelot.UnitTests/Multiplexing/MultiplexingMiddlewareTests.cs index bd513b45a..633fae43b 100644 --- a/test/Ocelot.UnitTests/Multiplexing/MultiplexingMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Multiplexing/MultiplexingMiddlewareTests.cs @@ -119,7 +119,6 @@ Task NextMe(HttpContext context) // Assert ThePipelineIsCalled(1); - } [Fact] diff --git a/test/Ocelot.UnitTests/UnitTest.cs b/test/Ocelot.UnitTests/UnitTest.cs new file mode 100644 index 000000000..aa7b716b3 --- /dev/null +++ b/test/Ocelot.UnitTests/UnitTest.cs @@ -0,0 +1,8 @@ +namespace Ocelot.UnitTests; + +public class UnitTest +{ + protected readonly Guid _testId = Guid.NewGuid(); + + protected string TestID { get => _testId.ToString("N"); } +}