深度探索.NET Feature Management功能開關的魔法

来源:https://www.cnblogs.com/ruipeng/p/18098211
-Advertisement-
Play Games

前言 .NET Feature Management 是一個用於管理應用程式功能的庫,它可以幫助開發人員在應用程式中輕鬆地添加、移除和管理功能。使用 Feature Management,開發人員可以根據不同用戶、環境或其他條件來動態地控制應用程式中的功能。這使得開發人員可以更靈活地管理應用程式的功 ...


前言

.NET Feature Management 是一個用於管理應用程式功能的庫,它可以幫助開發人員在應用程式中輕鬆地添加、移除和管理功能。使用 Feature Management,開發人員可以根據不同用戶、環境或其他條件來動態地控制應用程式中的功能。這使得開發人員可以更靈活地管理應用程式的功能,並根據需要快速調整和部署新功能。 Feature Management 還提供了一些方便的工具和 API,幫助開發人員更輕鬆地實現功能管理和控制。

安裝

  • .Net CLI
dotnet add package Microsoft.FeatureManagement.AspNetCore --version 4.0.0-preview2
  • Package Manager
NuGet\Install-Package Microsoft.FeatureManagement.AspNetCore -Version 4.0.0-preview2

或者 Vs Nuget 包管理 管理工具安裝等

依賴註入

.Net 功能管理器是通過框架的本機配置系統配置的,簡單來說只要是.Net 的配置系統支持的數據源都可以用做功能管理(FeatureManagement)的配置源

.NET 中的配置是使用一個或多個配置提供程式執行的。 配置提供程式使用各種配置源從鍵值對讀取配置數據:

  • 設置文件,例如 appsettings.json
  • 環境變數
  • Azure Key Vault
  • Azure 應用配置
  • 命令行參數
  • 已安裝或已創建的自定義提供程式
  • 目錄文件
  • 記憶體中的 .NET 對象
  • 第三方提供程式

.NET 中的配置提供程式

依賴註入:

service.AddFeatureManagement();

預設情況下,功能管理器從 .NET appsettings.json配置數據的 FeatureManagement Section 來獲取數據

  // Define feature flags in config file
  "FeatureManagement": {
    "sayHello": true, // On feature
    "todo": false // Off feature
  }

當然也可以自定義 Section

service.AddFeatureManagement(builder.Configuration.GetSection("CustomFeatureManagement"));
  // Define feature flags in config file
  "CustomFeatureManagement": {
    "sayHello": true, // On feature
    "todo": false // Off feature
  }

功能開關註冊成 Scoped

AddFeatureManagement 方法將特性管理服務作為單例添加到應用程式中,但有些情況下可能需要將特性管理服務添加為Scoped(作用域服務)。例如,我們可能希望使用 Scoped 以獲取上下文信息的功能過濾器。在這種情況下,應該使用 AddScopedFeatureManagement 方法, 這將確保功能管理服務(包括功能過濾器)被添加為 Scoped 服務。

//功能管理註冊 Scoped 作用域
service.AddScopedFeatureManagement();

功能管理的基本形式是檢查功能標誌是否已啟用,然後根據結果執行操作。這通過 IFeatureManagerIsEnabledAsync 方法來實現。

對我們上面的 FeatureManager 的配置來做一個驗證

  • sayhello 功能開關標誌測試
app.MapGet("/sayHello", async Task<IResult> ([FromServices] IFeatureManager manager, string name) =>
{
    if (await manager.IsEnabledAsync("sayHello"))
    {
        return TypedResults.Ok($"hello {name}");
    }
    return TypedResults.NotFound();

}).WithSummary("sayHello");

調用介面查看一下結果,在配置中我們的sayHello設置為true

image

狀態碼為 200,返回信息"hello Ruipeng",符合預期,功能開啟正常。

  • todo 功能開關標誌測試
app.MapGet("/todo", async Task<IResult> ([FromServices] IFeatureManager manager) =>
{
    if (await manager.IsEnabledAsync("todo"))
    {
        return TypedResults.Ok($"todo is enabled !");
    }
    return TypedResults.NotFound();

}).WithSummary("todo");

調用介面查看一下結果,狀態碼 404,返回信息 Not Found,符合預期,功能未開啟。

image

上面的示例簡單講解了一下功能開關的使用,接下來深入瞭解功能開關的配置

功能開關的定義

功能開關的標誌由兩部分組成:名稱和用於啟用功能的過濾器列表。

功能過濾器(Feature filters)定義了功能應何時啟用的場景。在評估特性是開啟還是關閉時,會遍歷其功能過濾器列表,直到其中一個過濾器決定啟用該特性。如果一個過濾器都沒有標識改功能應該開啟,那此功能標誌是關閉的狀態。

內置過濾器

  • AlwaysOn: 總是開啟
  • PercentageFilter:根據百分比隨機啟用/禁用功能。這個過濾器允許您基於一個百分比值來決定功能被啟用的概率,提供了一種簡單而靈活的機制來控制特性的曝光範圍。
  • TimeWindowFilter:在預定義的時間視窗內啟用特性。這個過濾器允許您指定特性的開始和結束時間,確保特性只在特定的時間段內可用。這對於限時活動或測試場景非常有用。
  • TargetingFilter:(這個主要是在Azure 用為目標受眾啟用功能的分階段推出針對特定用戶或用戶組啟用特性。這個過濾器允許您根據用戶屬性或標識來啟用特性,例如基於用戶 ID、角色、地區等。此外,對於此過濾器,您還可以設置一個百分比值,以進一步控制特性在目標用戶中的啟用概率。

詳細信息可以參考註冊功能篩選器 Docs

過濾器的配置指南

需要註意的是在功能標誌名稱中禁止使用冒號:,這是為了遵循一定的命名規範,避免與現有的或未來的功能管理系統產生衝突或造成解析錯誤。在定義功能標誌名稱時,請確保使用合法和合適的字元組合,以確保系統的穩定性和可維護性。
功能使用 EnabledFor 屬性來定義它們的功能過濾器


AlwaysOn 過濾器

  // Define feature flags in config file
  "FeatureManagement": {
    //始終啟用該功能
    "featureAlwaysOn": {
      "EnabledFor": [
        {
          "Name": "AlwaysOn"
        }
      ]
    }
  }
app.MapGet("/featureAlwaysOn", async Task<IResult> (IFeatureManager manager) =>
{
    if (await manager.IsEnabledAsync("featureAlwaysOn"))
    {
        return TypedResults.Ok($"featureAlwaysOn is enabled !");
    }
    return TypedResults.NotFound();
}).WithSummary("featureAlwaysOn");

調用介面查看測試結果,返回 200,符合預期

image


TimeWindow 過濾器

  "FeatureManagement": {
    "featureTimeWindow": {
      "EnabledFor": [
        {
          "Name": "TimeWindow",
          "Parameters": {
            "Start": "2024-03-26 13:30:00",
            "End": "2024-03-27 13:30:00"
          }
        }
      ]
    }
  }

指定了一個名為 TimeWindow 的功能過濾器。這是一個可配置的功能過濾,具有 Parameters 屬性,配置了功能活動的開始和結束時間 。

app.MapGet("/featureTimeWindow", async Task<IResult> (IFeatureManager manager) =>
{
    if (await manager.IsEnabledAsync("featureTimeWindow"))
    {
        return TypedResults.Ok($"featureTimeWindow is enabled !");
    }
    return TypedResults.NotFound();
}).WithSummary("TimeWindow 過濾器測試");

調用介面測試:返回 200 符合預期

image


Percentage 過濾器
百分比過濾器(Percentage Filter)它根據指定的百分比值隨機啟用或禁用某個特性。這種過濾器允許您控制特性的曝光率,以便在不同的用戶群體中測試特性的效果,或者在逐步推廣新特性時控制其影響範圍。

  "FeatureManagement": {
    "featurePercentage": {
      "EnabledFor": [
        {
          "Name": "Percentage",
          "Parameters": {
            "Value": "50"
          }
        }
      ]
    }
  },

app.MapGet("/featurePercentage", async Task<IResult> (IFeatureManager manager) =>
{
    if (await manager.IsEnabledAsync("featurePercentage"))
    {
        return TypedResults.Ok($"featurePercentage is enabled !");
    }
    return TypedResults.NotFound();
}).WithSummary("Percentage 過濾器測試");

連續測兩次

第一次測試結果: 返回 200
image

第二次測試結果:返回 404
image

通過測試結果可以看出有百分之五十的幾率成功,符合預期。

RequirementType

功能標誌的 RequirementType 屬性用於確定在評估功能狀態時,過濾器應該使用任何(Any)還是全部(All)邏輯。如果未指定 RequirementType,則預設值為 Any

  • Any 表示只需一個過濾器評估為 true,特性就會被啟用。
  • All 表示每個過濾器都必須評估為 true,特性才會被啟用。
    RequirementTypeAll 會改變遍歷方式。首先,如果沒有過濾器,則功能將被禁用。然後,遍歷特性過濾器,直到其中一個過濾器決定應將功能禁用。如果沒有過濾器指示應禁用功能,則該功能將被視為已啟用。
  "FeatureManagement": {
    "featureRequirementTypeAll": {
      "RequirementType": "All",
      "EnabledFor": [
        {
          "Name": "TimeWindow",
          "Parameters": {
            "Start": "2024-03-27 13:00:00",
            "End": "2024-05-01 13:00:00"
          }
        },
        {
          "Name": "Percentage",
          "Parameters": {
            "Value": "50"
          }
        }
      ]
    }
  },
app.MapGet("/featureRequirementTypeAll", async Task<IResult> (IFeatureManager manager) =>
{
    if (await manager.IsEnabledAsync("featureRequirementTypeAll"))
    {
        return TypedResults.Ok($"featureRequirementTypeAll is enabled !");
    }
    return TypedResults.NotFound();
}).WithSummary("RequirementTypeAll 多過濾器測試");

上面的實例設置為 all 之後此功能標誌的過濾器列表必須全部符合要求才能調用成功。

比如上面我設置的開始日期是2024-03-27 13:00:00當前時間小於這個日期
image

無論調用幾次還是還是 404,結果符合我們的預期。

自定義過濾器

要實現一個功能過濾器,必須要實現的是一個IFeatureFilter介面,介面包含了一個EvaluateAsync的方法。當功能標誌指定啟用該過濾器時,將調用 EvaluateAsync方法,如果方法返回的是true,則表示應該啟用功能。

定義一個中間件介面只對某個用戶組做開放,這個場景在 C 端的產品上比較常見,比如說部分功能的內測。

[FilterAlias("AuthenticatedGroup")]
public class AuthenticatedGroupFilter : IFeatureFilter, IFeatureFilterMetadata, IFilterParametersBinder
{
    public object BindParameters(IConfiguration parameters)
    {
        return parameters.Get<GroupSetting>() ?? new GroupSetting();
    }

    public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext featureFilterContext)
    {
        GroupSetting filterSettings = ((GroupSetting)featureFilterContext.Settings) ?? ((GroupSetting)BindParameters(featureFilterContext.Parameters));
        // 假設您有一個方法來檢查用戶是否已通過身份驗證
        // 例如,這可能是一個從身份驗證服務或中間件中獲得的屬性或方法
        bool isAuthenticated = IsGroupAuthenticated(filterSettings);
        return Task.FromResult(isAuthenticated);
    }


    private bool IsGroupAuthenticated(GroupSetting groupSetting)
    {
        // 在這裡編寫您的身份驗證檢查邏輯
        // 這可能涉及到檢查HTTP請求的上下文、會話狀態、令牌等
        // 具體的實現將取決於您使用的身份驗證機制

        // 示例:返回一個硬編碼的值,表示用戶是否已通過身份驗證
        // 在實際應用中,您應該實現實際的檢查邏輯
        return true; // 或者 false,取決於用戶是否已通過身份驗證
    }
}

FilterAlias是定義過濾器的別名,我們在配置文件中指定時需要用別名,IFeatureFilter介面返回的信息決定功能是否啟用,IFeatureFilterMetadata是一個空的標記介面,用於評估功能狀態的特征過濾器的標記介面,IFilterParametersBinder 介面用於參數綁定。

  • json 配置
  "FeatureManagement": {
    "featureAuthencatedGroup": {
      "EnabledFor": [
        {
          "Name": "AuthenticatedGroup",
          "Parameters": {
            "Groups": [ "AdminGroup", "GroupOne" ]
          }
        }
      ]
    }
  }
  • 依賴註入
services.AddFeatureManagement()
    .AddFeatureFilter<AuthenticatedGroupFilter>();

調用 AddFeatureFilter 方法可把自定義的過濾器註冊到功能管理器中。

app.MapGet("/featureAuthencatedGroup", async Task<IResult> (IFeatureManager manager) =>
{
    if (await manager.IsEnabledAsync("featureAuthencatedGroup"))
    {
        return TypedResults.Ok($"featureAuthencatedGroup is enabled !");
    }
    return TypedResults.NotFound();
}).WithSummary("AuthencatedGroup 自定義過濾器測試");

測試一下,返回 200 ,符合預期
image

一個小 tips;如果多個過濾器有同一個別名是,可以用命名空間加別名的方式來定義唯一一個過濾器,例如,Microsoft.Percentage 是一個完全限定的別名,它明確指出了 Percentage過濾器位於 Microsoft 命名空間下

自定義開啟中間件

  "FeatureManagement": {
    "featureMiddleWare": {
      "EnabledFor": [
        {
          "Name": "Percentage",
          "Parameters": {
            "Value": "50"
          }
        }
      ]
    }
  }

自定義中間件

public class FeatureMiddleWare(RequestDelegate next)
{
    public async Task Invoke(HttpContext context)
    {
        Console.WriteLine("FeatureMiddleWare管道執行之前~");
        await next(context);
        Console.WriteLine("FeatureMiddleWare管道執行之後~");
    }
}

添加擴展方法

//測試中間件的功能開啟
app.UseMiddlewareForFeature<FeatureMiddleWare>("featureMiddleWare");

隨便調用一個介面測試一下,可以看到管道根據百分比觸發成功
image

通過上述調用,應用程式添加了一個中間件組件,只有在特性“featureMiddleWare”被啟用時才會出現在請求管道中。如果在運行時啟用/禁用特性,中間件管道可以動態更改。

這是建立在基於特性對整個應用程式進行分支的更通用能力之上。

app.UseForFeature(featureName, appBuilder =>
{
appBuilder.UseMiddleware<T>();
});

MinimalApis 集成

在我們的 MVC 或者 Razor Pages 中有如下方案來啟用功能的開關,不過多介紹大家可以官方瀏覽學習。

FeatureManagement-Dotnet

services.AddMvc(o =>
{
    o.Filters.AddForFeature<SomeMvcFilter>("FeatureX");
});
[FeatureGate("FeatureX")]
public class IndexModel : PageModel
{
    public void OnGet()
    {
    }
}

MinimalAps 中可以利用 endpoint filter來簡化公功能的開關,

  • 第一步創建最小 Api 的基類,所有的 MinimalApis 過濾器都要繼承它
public abstract class FeatureFlagEndpointFilter(IFeatureManager featureManager) : IEndpointFilter
{
    protected abstract string FeatureFlag { get; }

    private readonly IFeatureManager _featureManager = featureManager;

    public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context,
        EndpointFilterDelegate next)
    {
        var isEnabled = await _featureManager.IsEnabledAsync(FeatureFlag);
        if (!isEnabled)
        {
            return TypedResults.NotFound();
        }
        return await next(context);
    }
}
  • 定義目標 Json 配置
  "FeatureManagement": {
    "featureUserApi": {
      "EnabledFor": [
        {
          "Name": "Percentage",
          "Parameters": {
            "Value": "50"
          }
        }
      ]
    }
  • 定義最小 Api 過濾器
public class UserApiFeatureFilter(IFeatureManager featureManager) : FeatureFlagEndpointFilter(featureManager)
{
    protected override string FeatureFlag => "featureUserApi";
}

  • 定義 Api 介面測試
//最小Api分組功能添加
{
    var userGroup = app.MapGroup("User").WithTags("User").AddEndpointFilter<UserApiFeatureFilter>(); ;

    userGroup.MapGet("/featureUserApi", IResult (IFeatureManager manager) =>
    {
        return TypedResults.Ok($"featureUserApi is enabled !");

    }).WithSummary("featureUserApi 最小Api過濾器測試");
}

調用測試,可以看出我們配置的百分比過濾器成功。

image

通過對 IEndpointFilter 的封裝藉助最小 ApiMapGroup 可以對一組相關的 Api 進行功能管理,簡化了我們一個個 Api 註冊。

最後

在本文中,我們深入探討了.NET Feature Management 庫的安裝、配置和使用方法,以及如何利用功能開關來動態管理應用程式的功能。以下是關鍵點的總結和提煉:

  • 安裝與依賴註入:通過.NET CLINuGet Package Manager 安裝等方式 Microsoft.FeatureManagement.AspNetCore 庫,併在應用程式中添加功能管理服務的依賴註入。

  • 功能定義與配置:通過.NET 的配置系統,在 appsettings.json 中定義功能標誌,指定功能的啟用和禁用狀態,以及可選的功能過濾器配置。

  • 自定義功能過濾器:實現 IFeatureFilter 介面來定義自定義功能過濾器,根據特定條件決定功能是否啟用,例如基於用戶組、時間視窗或百分比等條件。

  • 功能開關的使用:利用 IFeatureManagerIsEnabledAsync 方法檢查功能是否啟用,根據不同的功能狀態執行相應的邏輯,實現功能的動態控制。

  • RequirementType 設置:可以通過 RequirementType 屬性指定功能過濾器的邏輯要求,是 Any 還是 All,決定多個過濾器的組合邏輯。

  • 自定義中間件的動態切換:通過自定義功能過濾器和中間件,可以根據功能狀態動態調整請求管道,實現功能開關對中間件的控制。

  • 最小 API 集成:在 Minimal APIs 中,利用 IEndpointFilter 介面來簡化功能開關的應用,將功能管理應用到最小 API 的端點上,實現對一組相關 API 的功能管理。

通過以上總結和提煉,您可以更好地瞭解和應用.NET Feature Management 庫,實現靈活的功能管理和動態控制應用程式的功能。

有條件的富哥可以體驗一下在 Azure 應用程式配置中管理功能標誌

更多詳細的內容請瀏覽FeatureManagement-Dotnet

本文測試完整源代碼

本文來自博客園,作者:董瑞鵬,轉載請註明原文鏈接:https://www.cnblogs.com/ruipeng/p/18098211


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本文介紹瞭如何快速搭建一個基於大型語言模型(LLM)的混元聊天應用。強調了開發速度的重要性,並指出了使用Streamlit這一工具的優勢,特別是對於不熟悉前端代碼的開發者來說,Streamlit提供了一種快速構建聊天應用的方法。 ...
  • 前言 aardio中有些經常使用的庫,換個項目總需要複製一下,還不便於修改。雖然可以直接把它放到aardio\lib目錄下,也是不便於共用給其他人使用。 最近偶然翻到編輯器里的工具->開發環境->擴展庫發佈工具,就想著可以像官方一樣,發佈自己的擴展庫,也便於分享給大家使用,最好能像官方擴展庫一樣線上 ...
  • 隨著汽車的普及和使用頻率的增加,車輛的維修保養成為了車主們經常需要面對的問題。為了提供更好的服務,挖數據平臺提供了一個維修保養記錄統計介面,讓用戶可以方便地查詢車輛的保養記錄和維修記錄。本文將對該介面進行詳細解析,並介紹其使用方法和應用場景。 首先,我們來看一下該介面的具體功能。該介面可以查詢車輛的 ...
  • 在使用Django等框架來操作MySQL時,實際上底層還是通過Python來操作的,首先需要安裝一個驅動程式,在Python3中,驅動程式有多種選擇,比如有pymysql以及mysqlclient等。使用pip命令安裝mysqlclient失敗應如何解決? 安裝的python版本說明 機器同時安裝了 ...
  • Csharper中的表達式樹 這節課來瞭解一下表示式樹是什麼? 在C#中,表達式樹是一種數據結構,它可以表示一些代碼塊,如Lambda表達式或查詢表達式。表達式樹使你能夠查看和操作數據,就像你可以查看和操作代碼一樣。它們通常用於創建動態查詢和解析表達式。 一、認識表達式樹 為什麼要這樣說?它和委托有 ...
  • 一、前言 這是一篇搭建許可權管理系統的系列文章。 隨著網路的發展,信息安全對應任何企業來說都越發的重要,而本系列文章將和大家一起一步一步搭建一個全新的許可權管理系統。 說明:由於搭建一個全新的項目過於繁瑣,所有作者將挑選核心代碼和核心思路進行分享。 二、技術選擇 三、開始設計 1、自主搭建vue前端和. ...
  • 在實際使用中,由於涉及到不同編程語言之間互相調用,導致C++ 中的OpenCV與C#中的OpenCvSharp 圖像數據在不同編程語言之間難以有效傳遞。在本文中我們將結合OpenCvSharp源碼實現原理,探究兩種數據之間的通信方式。 ...
  • 在 WPF 應用程式中,拖放操作是實現用戶交互的重要組成部分。通過拖放操作,用戶可以輕鬆地將數據從一個位置移動到另一個位置,或者將控制項從一個容器移動到另一個容器。然而,WPF 中預設的拖放操作可能並不是那麼好用。為瞭解決這個問題,我們可以自定義一個 Panel 來實現更簡單的拖拽操作。 自定義 Pa ...
一周排行
    -Advertisement-
    Play Games
  • 前言 在我們開發過程中基本上不可或缺的用到一些敏感機密數據,比如SQL伺服器的連接串或者是OAuth2的Secret等,這些敏感數據在代碼中是不太安全的,我們不應該在源代碼中存儲密碼和其他的敏感數據,一種推薦的方式是通過Asp.Net Core的機密管理器。 機密管理器 在 ASP.NET Core ...
  • 新改進提供的Taurus Rpc 功能,可以簡化微服務間的調用,同時可以不用再手動輸出模塊名稱,或調用路徑,包括負載均衡,這一切,由框架實現並提供了。新的Taurus Rpc 功能,將使得服務間的調用,更加輕鬆、簡約、高效。 ...
  • 順序棧的介面程式 目錄順序棧的介面程式頭文件創建順序棧入棧出棧利用棧將10進位轉16進位數驗證 頭文件 #include <stdio.h> #include <stdbool.h> #include <stdlib.h> 創建順序棧 // 指的是順序棧中的元素的數據類型,用戶可以根據需要進行修改 ...
  • 前言 整理這個官方翻譯的系列,原因是網上大部分的 tomcat 版本比較舊,此版本為 v11 最新的版本。 開源項目 從零手寫實現 tomcat minicat 別稱【嗅虎】心有猛虎,輕嗅薔薇。 系列文章 web server apache tomcat11-01-官方文檔入門介紹 web serv ...
  • C總結與剖析:關鍵字篇 -- <<C語言深度解剖>> 目錄C總結與剖析:關鍵字篇 -- <<C語言深度解剖>>程式的本質:二進位文件變數1.變數:記憶體上的某個位置開闢的空間2.變數的初始化3.為什麼要有變數4.局部變數與全局變數5.變數的大小由類型決定6.任何一個變數,記憶體賦值都是從低地址開始往高地 ...
  • 如果讓你來做一個有狀態流式應用的故障恢復,你會如何來做呢? 單機和多機會遇到什麼不同的問題? Flink Checkpoint 是做什麼用的?原理是什麼? ...
  • C++ 多級繼承 多級繼承是一種面向對象編程(OOP)特性,允許一個類從多個基類繼承屬性和方法。它使代碼更易於組織和維護,並促進代碼重用。 多級繼承的語法 在 C++ 中,使用 : 符號來指定繼承關係。多級繼承的語法如下: class DerivedClass : public BaseClass1 ...
  • 前言 什麼是SpringCloud? Spring Cloud 是一系列框架的有序集合,它利用 Spring Boot 的開發便利性簡化了分散式系統的開發,比如服務註冊、服務發現、網關、路由、鏈路追蹤等。Spring Cloud 並不是重覆造輪子,而是將市面上開發得比較好的模塊集成進去,進行封裝,從 ...
  • class_template 類模板和函數模板的定義和使用類似,我們已經進行了介紹。有時,有兩個或多個類,其功能是相同的,僅僅是數據類型不同。類模板用於實現類所需數據的類型參數化 template<class NameType, class AgeType> class Person { publi ...
  • 目錄system v IPC簡介共用記憶體需要用到的函數介面shmget函數--獲取對象IDshmat函數--獲得映射空間shmctl函數--釋放資源共用記憶體實現思路註意 system v IPC簡介 消息隊列、共用記憶體和信號量統稱為system v IPC(進程間通信機制),V是羅馬數字5,是UNI ...