< Summary

Information
Class: ArturRios.Util.WebApi.EndpointToggle.EndpointToggleAttribute
Assembly: ArturRios.Util.WebApi
File(s): D:\Repositories\dotnet-webapi-util\src\EndpointToggle\EndpointToggleAttribute.cs
Line coverage
90%
Covered lines: 121
Uncovered lines: 13
Coverable lines: 134
Total lines: 251
Line coverage: 90.2%
Branch coverage
73%
Covered branches: 44
Total branches: 60
Branch coverage: 73.3%
Method coverage
100%
Covered methods: 13
Fully covered methods: 3
Total methods: 13
Method coverage: 100%
Full method coverage: 23%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
.ctor(...)83.33%6689.28%
get_DefaultDisabledMessage()100%11100%
OnActionExecuting(...)80%111078.94%
ReturnObject()100%11100%
ReturnDefault()70%1010100%
GetToggleFromFile()60%121071.42%
GetToggleFromAppSettings(...)100%66100%
GetToggleFromEnvironmentVariables(...)100%44100%
GetDefaultKey()50%66100%
GetControllerName()50%22100%
GetKeyPrefix(...)50%4477.77%
AddKeySuffix(...)100%22100%

File(s)

D:\Repositories\dotnet-webapi-util\src\EndpointToggle\EndpointToggleAttribute.cs

#LineLine coverage
 1using System.Net;
 2using ArturRios.Configuration.Enums;
 3using ArturRios.Output;
 4using Microsoft.AspNetCore.Mvc;
 5using Microsoft.AspNetCore.Mvc.Controllers;
 6using Microsoft.AspNetCore.Mvc.Filters;
 7using Microsoft.Extensions.Configuration;
 8
 9namespace ArturRios.Util.WebApi.EndpointToggle;
 10
 11/// <summary>Action filter attribute that enables or disables a single endpoint, either from a compile-time flag or from
 12/// a runtime configuration value. When the endpoint is disabled the request is short-circuited before the action runs
 13/// and the response is shaped according to <see cref="OutputType"/>: an empty status code, the action's default return
 14/// value, a <see cref="ProcessOutput"/> envelope, or a thrown <see cref="EndpointDisabledException"/>.</summary>
 15/// <remarks>
 16/// Two forms are available. The static form (<see cref="EndpointToggleAttribute(bool, HttpStatusCode, OutputType, strin
 17/// fixes the toggle at compile time. The configuration form
 18/// (<see cref="EndpointToggleAttribute(ConfigurationSourceType, string, string, string, HttpStatusCode, OutputType, str
 19/// resolves the toggle on each request from <c>appsettings.json</c> and/or environment variables, so an endpoint can be
 20/// turned on or off without redeploying.
 21/// </remarks>
 22[AttributeUsage(AttributeTargets.Method)]
 23public class EndpointToggleAttribute : ActionFilterAttribute
 24{
 25    private const string DefaultAppSettingsKeyPrefix = "Endpoints:[Controller]";
 26    private const string DefaultEnvFileKeyPrefix = "Endpoints_[Controller]";
 27    private readonly ConfigurationSourceType _configurationSource;
 28    private readonly string _disabledMessage;
 29    private readonly OutputType _disabledOutputType;
 30    private readonly HttpStatusCode _disabledStatusCode;
 31    private readonly bool _isEnabled;
 1932    private readonly string _key = string.Empty;
 1933    private readonly string _keyPrefix = string.Empty;
 1934    private readonly string _keySeparator = string.Empty;
 1935    private readonly string _keySuffix = string.Empty;
 36    private readonly bool _useConfigurationFile;
 37
 1938    private ActionExecutingContext _context = null!;
 39
 40    /// <summary>Creates a toggle whose enabled state is fixed at compile time.</summary>
 41    /// <param name="isEnabled">Whether the endpoint is enabled. When <c>false</c> the action is short-circuited on ever
 42    /// <param name="disabledStatusCode">The HTTP status code returned when the endpoint is disabled. Defaults to 404 No
 43    /// <param name="disabledOutputType">How the disabled response is shaped. Defaults to <see cref="OutputType.Object"/
 44    /// <param name="disabledMessage">The message included in the disabled response when <paramref name="disabledOutputT
 845    public EndpointToggleAttribute(
 846        bool isEnabled = true,
 847        HttpStatusCode disabledStatusCode = HttpStatusCode.NotFound,
 848        OutputType disabledOutputType = OutputType.Object,
 849        string disabledMessage = "This endpoint is currently disabled"
 850    )
 851    {
 852        _isEnabled = isEnabled;
 853        _disabledStatusCode = disabledStatusCode;
 854        _disabledMessage = disabledMessage;
 855        _disabledOutputType = disabledOutputType;
 856        _useConfigurationFile = false;
 857    }
 58
 59    /// <summary>Creates a toggle whose enabled state is resolved on each request from configuration.</summary>
 60    /// <param name="configurationSource">Where the toggle value is read from: <c>AppSettings</c>, an env file / environ
 61    /// variables, or both. This also selects the key separator (<c>:</c> for app settings, <c>_</c> otherwise).</param>
 62    /// <param name="key">The full configuration key to read. When empty, a key is derived from the key prefix, the
 63    /// controller name, the action name and the optional suffix.</param>
 64    /// <param name="keyPrefix">The prefix for the derived key. When empty a default is used
 65    /// (<c>Endpoints:[Controller]</c> for app settings, <c>Endpoints_[Controller]</c> otherwise), with <c>[Controller]<
 66    /// replaced by the current controller name.</param>
 67    /// <param name="keySuffix">An optional suffix appended to the derived key.</param>
 68    /// <param name="disabledStatusCode">The HTTP status code returned when the endpoint is disabled. Defaults to 404 No
 69    /// <param name="disabledOutputType">How the disabled response is shaped. Defaults to <see cref="OutputType.Object"/
 70    /// <param name="disabledMessage">The message included in the disabled response when <paramref name="disabledOutputT
 1171    public EndpointToggleAttribute(
 1172        ConfigurationSourceType configurationSource,
 1173        string key = "",
 1174        string keyPrefix = "",
 1175        string keySuffix = "",
 1176        HttpStatusCode disabledStatusCode = HttpStatusCode.NotFound,
 1177        OutputType disabledOutputType = OutputType.Object,
 1178        string disabledMessage = "This endpoint is currently disabled"
 1179    )
 1180    {
 1181        _configurationSource = configurationSource;
 1182        _key = key;
 1183        _disabledStatusCode = disabledStatusCode;
 1184        _disabledMessage = disabledMessage;
 1185        _disabledOutputType = disabledOutputType;
 1186        _useConfigurationFile = true;
 1187        _keySuffix = keySuffix;
 1188        _keySeparator = configurationSource == ConfigurationSourceType.AppSettings ? ":" : "_";
 89
 1190        if (string.IsNullOrWhiteSpace(keyPrefix))
 1191        {
 1192            _keyPrefix = configurationSource == ConfigurationSourceType.AppSettings
 1193                ? DefaultAppSettingsKeyPrefix
 1194                : DefaultEnvFileKeyPrefix;
 1195        }
 96        else
 097        {
 098            _keyPrefix = keyPrefix;
 099        }
 11100    }
 101
 102    /// <summary>The default message describing a disabled endpoint: <c>"This endpoint is currently disabled"</c>.</summ
 2103    public static string DefaultDisabledMessage => "This endpoint is currently disabled";
 104
 105    /// <summary>Evaluates the toggle before the action runs and, when the endpoint is disabled, short-circuits the
 106    /// pipeline with a response shaped according to the configured <see cref="OutputType"/>.</summary>
 107    /// <param name="context">The executing-action context for the current request.</param>
 108    /// <exception cref="EndpointDisabledException">Thrown when the endpoint is disabled and its disabled output type is
 109    public override void OnActionExecuting(ActionExecutingContext context)
 19110    {
 19111        _context = context;
 112
 19113        var isEnabled = _useConfigurationFile ? GetToggleFromFile() : _isEnabled;
 114
 19115        if (isEnabled)
 8116        {
 8117            return;
 118        }
 119
 11120        switch (_disabledOutputType)
 121        {
 122            case OutputType.Void:
 1123                _context.Result = new StatusCodeResult((int)_disabledStatusCode);
 1124                break;
 125            case OutputType.Default:
 4126                ReturnDefault();
 4127                break;
 128            case OutputType.Object:
 5129                ReturnObject();
 5130                break;
 131            case OutputType.Exception:
 1132                throw new EndpointDisabledException([DefaultDisabledMessage]);
 133            case OutputType.Primitive:
 0134                ReturnDefault();
 0135                break;
 136            default:
 0137                ReturnObject();
 0138                break;
 139        }
 18140    }
 141
 142    private void ReturnObject()
 5143    {
 5144        var output = ProcessOutput.New.WithMessage(_disabledMessage);
 145
 5146        _context.Result = new ObjectResult(output) { StatusCode = (int)_disabledStatusCode };
 5147    }
 148
 149    private void ReturnDefault()
 4150    {
 4151        var methodInfo = (_context.ActionDescriptor as ControllerActionDescriptor)?.MethodInfo;
 4152        var returnType = methodInfo?.ReturnType;
 153
 4154        if (returnType == null || returnType == typeof(void))
 1155        {
 1156            _context.Result = new StatusCodeResult((int)_disabledStatusCode);
 157
 1158            return;
 159        }
 160
 3161        var defaultObj = returnType.IsValueType ? Activator.CreateInstance(returnType) : null;
 162
 3163        _context.Result = new ObjectResult(defaultObj) { StatusCode = (int)_disabledStatusCode };
 4164    }
 165
 166    private bool GetToggleFromFile()
 11167    {
 11168        var toggleKey = string.IsNullOrWhiteSpace(_key) ? GetDefaultKey() : _key;
 169
 11170        if (string.IsNullOrWhiteSpace(toggleKey))
 0171        {
 0172            return true;
 173        }
 174
 11175        return _configurationSource switch
 11176        {
 7177            ConfigurationSourceType.AppSettings => GetToggleFromAppSettings(toggleKey) ?? true,
 11178            ConfigurationSourceType.EnvFile or ConfigurationSourceType.EnvironmentVariables =>
 4179                GetToggleFromEnvironmentVariables(toggleKey) ?? true,
 0180            _ => GetToggleFromAppSettings(toggleKey) ??
 0181                 GetToggleFromEnvironmentVariables(toggleKey) ?? true
 11182        };
 11183    }
 184
 185    private bool? GetToggleFromAppSettings(string key)
 7186    {
 7187        if (_context.HttpContext.RequestServices.GetService(typeof(IConfiguration)) is not IConfiguration config)
 1188        {
 1189            return null;
 190        }
 191
 6192        var value = config[key];
 193
 6194        if (string.IsNullOrEmpty(value))
 1195        {
 1196            return null;
 197        }
 198
 5199        return bool.TryParse(value, out var parsed) ? parsed : null;
 7200    }
 201
 202    private static bool? GetToggleFromEnvironmentVariables(string key)
 4203    {
 4204        var envValue = Environment.GetEnvironmentVariable(key);
 205
 4206        if (string.IsNullOrEmpty(envValue))
 1207        {
 1208            return null;
 209        }
 210
 3211        if (bool.TryParse(envValue, out var parsed))
 2212        {
 2213            return parsed;
 214        }
 215
 1216        return null;
 4217    }
 218
 219    private string? GetDefaultKey()
 2220    {
 2221        var methodInfo = (_context.ActionDescriptor as ControllerActionDescriptor)?.MethodInfo;
 2222        var methodName = methodInfo?.Name;
 223
 2224        var keyPrefix = GetKeyPrefix(_key);
 225
 2226        return methodInfo is not null ? AddKeySuffix($"{keyPrefix}{_keySeparator}{methodName}") : null;
 2227    }
 228
 229    private string? GetControllerName()
 4230    {
 4231        var controllerActionDescriptor = _context.ActionDescriptor as ControllerActionDescriptor;
 4232        return controllerActionDescriptor?.ControllerName;
 4233    }
 234
 235    private string GetKeyPrefix(string key)
 2236    {
 2237        if (string.IsNullOrWhiteSpace(_keyPrefix))
 0238        {
 0239            return key;
 240        }
 241
 2242        var controllerName = GetControllerName();
 243
 2244        return controllerName is null
 2245            ? _keyPrefix.Replace($"{_keySeparator}[Controller]", string.Empty)
 2246            : _keyPrefix.Replace("[Controller]", GetControllerName());
 2247    }
 248
 249    private string AddKeySuffix(string key) =>
 2250        string.IsNullOrWhiteSpace(_keySuffix) ? key : $"{key}{_keySeparator}{_keySuffix}";
 251}