Browse Source

Merge pull request #5728 from abpframework/auto-validate-antiforgery

Auto Validate AntiForgery Token for HTTP API Requests
pull/5741/head
Halil İbrahim Kalkan 6 years ago
committed by GitHub
parent
commit
c9ce1809c6
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Account.cshtml
  2. 2
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Application.cshtml
  3. 2
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Empty.cshtml
  4. 6
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs
  5. 35
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AntiForgery/AbpAntiForgeryCookieNameProvider.cs
  6. 8
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AntiForgery/AbpAntiForgeryManagerAspNetCoreExtensions.cs
  7. 65
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AntiForgery/AbpAntiForgeryOptions.cs
  8. 37
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AntiForgery/AbpAutoValidateAntiforgeryTokenAttribute.cs
  9. 65
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AntiForgery/AbpAutoValidateAntiforgeryTokenAuthorizationFilter.cs
  10. 37
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AntiForgery/AbpValidateAntiForgeryTokenAttribute.cs
  11. 81
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AntiForgery/AbpValidateAntiforgeryTokenAuthorizationFilter.cs
  12. 8
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AntiForgery/AspNetCoreAbpAntiForgeryManager.cs
  13. 9
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationController.cs
  14. 8
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationScriptController.cs
  15. 1
      framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/RequestLocalization/DefaultAbpRequestLocalizationOptionsProvider.cs
  16. 14
      framework/src/Volo.Abp.Core/System/AbpObjectExtensions.cs

2
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Account.cshtml

@ -13,7 +13,6 @@
@using Volo.Abp.AspNetCore.Mvc.UI.Widgets.Components.WidgetStyles
@using Volo.Abp.MultiTenancy
@using Volo.Abp.Localization
@inject IAbpAntiForgeryManager AbpAntiForgeryManager
@inject IBrandingProvider BrandingProvider
@inject IOptions<AbpMultiTenancyOptions> MultiTenancyOptions
@inject ICurrentTenant CurrentTenant
@ -22,7 +21,6 @@
@{
Layout = null;
AbpAntiForgeryManager.SetCookie();
var containerClass = ViewBag.FluidLayout == true ? "container-fluid" : "container"; //TODO: Better and type-safe options
var rtl = CultureHelper.IsRtl ? "rtl" : string.Empty;
}

2
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Application.cshtml

@ -9,12 +9,10 @@
@using Volo.Abp.AspNetCore.Mvc.UI.Widgets.Components.WidgetScripts
@using Volo.Abp.AspNetCore.Mvc.UI.Widgets.Components.WidgetStyles
@using Volo.Abp.Localization
@inject IAbpAntiForgeryManager AbpAntiForgeryManager
@inject IBrandingProvider BrandingProvider
@inject IPageLayout PageLayout
@{
Layout = null;
AbpAntiForgeryManager.SetCookie();
var containerClass = ViewBag.FluidLayout == true ? "container-fluid" : "container"; //TODO: Better and type-safe options
var pageTitle = ViewBag.Title == null ? BrandingProvider.AppName : ViewBag.Title; //TODO: Discard to get from Title

2
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Empty.cshtml

@ -8,12 +8,10 @@
@using Volo.Abp.AspNetCore.Mvc.UI.Widgets.Components.WidgetScripts
@using Volo.Abp.AspNetCore.Mvc.UI.Widgets.Components.WidgetStyles
@using Volo.Abp.Localization
@inject IAbpAntiForgeryManager AbpAntiForgeryManager
@inject IBrandingProvider BrandingProvider
@inject IPageLayout PageLayout
@{
Layout = null;
AbpAntiForgeryManager.SetCookie();
var containerClass = ViewBag.FluidLayout == true ? "container-fluid" : "container"; //TODO: Better and type-safe options
var pageTitle = ViewBag.Title == null ? BrandingProvider.AppName : ViewBag.Title; //TODO: Discard to get from Title

6
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs

@ -22,6 +22,7 @@ using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Localization;
using Volo.Abp.ApiVersioning;
using Volo.Abp.AspNetCore.Mvc.AntiForgery;
using Volo.Abp.AspNetCore.Mvc.ApiExploring;
using Volo.Abp.AspNetCore.Mvc.Conventions;
using Volo.Abp.AspNetCore.Mvc.DataAnnotations;
@ -94,7 +95,10 @@ namespace Volo.Abp.AspNetCore.Mvc
}
});
var mvcCoreBuilder = context.Services.AddMvcCore();
var mvcCoreBuilder = context.Services.AddMvcCore(options =>
{
options.Filters.Add(new AbpAutoValidateAntiforgeryTokenAttribute());
});
context.Services.ExecutePreConfiguredActions(mvcCoreBuilder);
var abpMvcDataAnnotationsLocalizationOptions = context.Services

35
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AntiForgery/AbpAntiForgeryCookieNameProvider.cs

@ -0,0 +1,35 @@
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.AspNetCore.Mvc.AntiForgery
{
public class AbpAntiForgeryCookieNameProvider : ITransientDependency
{
private readonly IOptionsSnapshot<CookieAuthenticationOptions> _namedOptionsAccessor;
private readonly AbpAntiForgeryOptions _abpAntiForgeryOptions;
public AbpAntiForgeryCookieNameProvider(
IOptionsSnapshot<CookieAuthenticationOptions> namedOptionsAccessor,
IOptions<AbpAntiForgeryOptions> abpAntiForgeryOptions)
{
_namedOptionsAccessor = namedOptionsAccessor;
_abpAntiForgeryOptions = abpAntiForgeryOptions.Value;
}
public virtual string GetAuthCookieNameOrNull()
{
if (_abpAntiForgeryOptions.AuthCookieSchemaName == null)
{
return null;
}
return _namedOptionsAccessor.Get(_abpAntiForgeryOptions.AuthCookieSchemaName)?.Cookie?.Name;
}
public virtual string GetAntiForgeryCookieNameOrNull()
{
return _abpAntiForgeryOptions.TokenCookie.Name;
}
}
}

8
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AntiForgery/AbpAntiForgeryManagerAspNetCoreExtensions.cs

@ -4,7 +4,11 @@
{
public static void SetCookie(this IAbpAntiForgeryManager manager)
{
manager.HttpContext.Response.Cookies.Append(manager.Options.TokenCookieName, manager.GenerateToken());
manager.HttpContext.Response.Cookies.Append(
manager.Options.TokenCookie.Name,
manager.GenerateToken(),
manager.Options.TokenCookie.Build(manager.HttpContext)
);
}
}
}
}

65
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AntiForgery/AbpAntiForgeryOptions.cs

@ -1,23 +1,68 @@
namespace Volo.Abp.AspNetCore.Mvc.AntiForgery
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Http;
namespace Volo.Abp.AspNetCore.Mvc.AntiForgery
{
public class AbpAntiForgeryOptions
{
/// <summary>
/// Get/sets cookie name to transfer Anti Forgery token between server and client.
/// Default value: "XSRF-TOKEN".
/// Use to set the cookie options to transfer Anti Forgery token between server and client.
/// Default name of the cookie: "XSRF-TOKEN".
/// </summary>
public string TokenCookieName { get; set; }
public CookieBuilder TokenCookie { get; }
/// <summary>
/// Get/sets header name to transfer Anti Forgery token from client to the server.
/// Default value: "X-XSRF-TOKEN".
/// Used to find auth cookie when validating Anti Forgery token.
/// Default value: "Identity.Application".
/// </summary>
public string TokenHeaderName { get; set; }
public string AuthCookieSchemaName { get; set; }
/// <summary>
/// Default value: true.
/// </summary>
public bool AutoValidate { get; set; } = true;
/// <summary>
/// A predicate to filter types to auto-validate.
/// Return true to select the type to validate.
/// Default: returns true for all given types.
/// </summary>
[NotNull]
public Predicate<Type> AutoValidateFilter
{
get => _autoValidateFilter;
set => _autoValidateFilter = Check.NotNull(value, nameof(value));
}
private Predicate<Type> _autoValidateFilter;
/// <summary>
/// Default methods: "GET", "HEAD", "TRACE", "OPTIONS".
/// </summary>
[NotNull]
public HashSet<string> AutoValidateIgnoredHttpMethods
{
get => _autoValidateIgnoredHttpMethods;
set => _autoValidateIgnoredHttpMethods = Check.NotNull(value, nameof(value));
}
private HashSet<string> _autoValidateIgnoredHttpMethods;
public AbpAntiForgeryOptions()
{
TokenCookieName = "XSRF-TOKEN";
TokenHeaderName = "X-XSRF-TOKEN";
AutoValidateFilter = type => true;
TokenCookie = new CookieBuilder
{
Name = "XSRF-TOKEN",
HttpOnly = false,
IsEssential = true,
Expiration = TimeSpan.FromDays(3650) //10 years!
};
AuthCookieSchemaName = "Identity.Application";
AutoValidateIgnoredHttpMethods = new HashSet<string> {"GET", "HEAD", "TRACE", "OPTIONS"};
}
}
}
}

37
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AntiForgery/AbpAutoValidateAntiforgeryTokenAttribute.cs

@ -0,0 +1,37 @@
using System;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
namespace Volo.Abp.AspNetCore.Mvc.AntiForgery
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class AbpAutoValidateAntiforgeryTokenAttribute : Attribute, IFilterFactory, IOrderedFilter
{
/// <summary>
/// Gets the order value for determining the order of execution of filters. Filters execute in
/// ascending numeric value of the <see cref="Order"/> property.
/// </summary>
/// <remarks>
/// <para>
/// Filters are executed in a sequence determined by an ascending sort of the <see cref="Order"/> property.
/// </para>
/// <para>
/// The default Order for this attribute is 1000 because it must run after any filter which does authentication
/// or login in order to allow them to behave as expected (ie Unauthenticated or Redirect instead of 400).
/// </para>
/// <para>
/// Look at <see cref="IOrderedFilter.Order"/> for more detailed info.
/// </para>
/// </remarks>
public int Order { get; set; } = 1000;
/// <inheritdoc />
public bool IsReusable => true;
/// <inheritdoc />
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
return serviceProvider.GetRequiredService<AbpAutoValidateAntiforgeryTokenAuthorizationFilter>();
}
}
}

65
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AntiForgery/AbpAutoValidateAntiforgeryTokenAuthorizationFilter.cs

@ -0,0 +1,65 @@
using System;
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.AspNetCore.Mvc.AntiForgery
{
public class AbpAutoValidateAntiforgeryTokenAuthorizationFilter : AbpValidateAntiforgeryTokenAuthorizationFilter, ITransientDependency
{
private readonly AbpAntiForgeryOptions _options;
public AbpAutoValidateAntiforgeryTokenAuthorizationFilter(
IAntiforgery antiforgery,
AbpAntiForgeryCookieNameProvider antiForgeryCookieNameProvider,
IOptions<AbpAntiForgeryOptions> options,
ILogger<AbpValidateAntiforgeryTokenAuthorizationFilter> logger)
: base(
antiforgery,
antiForgeryCookieNameProvider,
logger)
{
_options = options.Value;
}
protected override bool ShouldValidate(AuthorizationFilterContext context)
{
if (!_options.AutoValidate)
{
return false;
}
if(context.ActionDescriptor.IsControllerAction())
{
var controllerType = context.ActionDescriptor
.AsControllerActionDescriptor()
.ControllerTypeInfo
.AsType();
if (!_options.AutoValidateFilter(controllerType))
{
return false;
}
}
if (IsIgnoredHttpMethod(context))
{
return false;
}
return base.ShouldValidate(context);
}
protected virtual bool IsIgnoredHttpMethod(AuthorizationFilterContext context)
{
return context.HttpContext
.Request
.Method
.ToUpperInvariant()
.IsIn(_options.AutoValidateIgnoredHttpMethods);
}
}
}

37
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AntiForgery/AbpValidateAntiForgeryTokenAttribute.cs

@ -0,0 +1,37 @@
using System;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
namespace Volo.Abp.AspNetCore.Mvc.AntiForgery
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class AbpValidateAntiForgeryTokenAttribute : Attribute, IFilterFactory, IOrderedFilter
{
/// <summary>
/// Gets the order value for determining the order of execution of filters. Filters execute in
/// ascending numeric value of the <see cref="Order"/> property.
/// </summary>
/// <remarks>
/// <para>
/// Filters are executed in an ordering determined by an ascending sort of the <see cref="Order"/> property.
/// </para>
/// <para>
/// The default Order for this attribute is 1000 because it must run after any filter which does authentication
/// or login in order to allow them to behave as expected (ie Unauthenticated or Redirect instead of 400).
/// </para>
/// <para>
/// Look at <see cref="IOrderedFilter.Order"/> for more detailed info.
/// </para>
/// </remarks>
public int Order { get; set; } = 1000;
/// <inheritdoc />
public bool IsReusable => true;
/// <inheritdoc />
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
return serviceProvider.GetRequiredService<AbpValidateAntiforgeryTokenAuthorizationFilter>();
}
}
}

81
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AntiForgery/AbpValidateAntiforgeryTokenAuthorizationFilter.cs

@ -0,0 +1,81 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.Extensions.Logging;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.AspNetCore.Mvc.AntiForgery
{
public class AbpValidateAntiforgeryTokenAuthorizationFilter : IAsyncAuthorizationFilter, IAntiforgeryPolicy, ITransientDependency
{
private IAntiforgery _antiforgery;
private readonly AbpAntiForgeryCookieNameProvider _antiForgeryCookieNameProvider;
private readonly ILogger<AbpValidateAntiforgeryTokenAuthorizationFilter> _logger;
public AbpValidateAntiforgeryTokenAuthorizationFilter(
IAntiforgery antiforgery,
AbpAntiForgeryCookieNameProvider antiForgeryCookieNameProvider,
ILogger<AbpValidateAntiforgeryTokenAuthorizationFilter> logger)
{
_antiforgery = antiforgery;
_logger = logger;
_antiForgeryCookieNameProvider = antiForgeryCookieNameProvider;
}
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (!context.IsEffectivePolicy<IAntiforgeryPolicy>(this))
{
_logger.LogInformation("Skipping the execution of current filter as its not the most effective filter implementing the policy " + typeof(IAntiforgeryPolicy));
return;
}
if (ShouldValidate(context))
{
try
{
await _antiforgery.ValidateRequestAsync(context.HttpContext);
}
catch (AntiforgeryValidationException exception)
{
_logger.LogError(exception.Message, exception);
context.Result = new AntiforgeryValidationFailedResult();
}
}
}
protected virtual bool ShouldValidate(AuthorizationFilterContext context)
{
var authCookieName = _antiForgeryCookieNameProvider.GetAuthCookieNameOrNull();
//Always perform antiforgery validation when request contains authentication cookie
if (authCookieName != null &&
context.HttpContext.Request.Cookies.ContainsKey(authCookieName))
{
return true;
}
var antiForgeryCookieName = _antiForgeryCookieNameProvider.GetAntiForgeryCookieNameOrNull();
//No need to validate if antiforgery cookie is not sent.
//That means the request is sent from a non-browser client.
//See https://github.com/aspnet/Antiforgery/issues/115
if (antiForgeryCookieName != null &&
!context.HttpContext.Request.Cookies.ContainsKey(antiForgeryCookieName))
{
return false;
}
// Anything else requires a token.
return true;
}
}
}

8
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AntiForgery/AspNetCoreAbpAntiForgeryManager.cs

@ -26,7 +26,11 @@ namespace Volo.Abp.AspNetCore.Mvc.AntiForgery
public void SetCookie()
{
HttpContext.Response.Cookies.Append(Options.TokenCookieName, GenerateToken());
HttpContext.Response.Cookies.Append(
Options.TokenCookie.Name,
GenerateToken(),
Options.TokenCookie.Build(HttpContext)
);
}
public string GenerateToken()
@ -34,4 +38,4 @@ namespace Volo.Abp.AspNetCore.Mvc.AntiForgery
return _antiforgery.GetAndStoreTokens(_httpContextAccessor.HttpContext).RequestToken;
}
}
}
}

9
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationController.cs

@ -1,5 +1,6 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc.AntiForgery;
namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations
{
@ -9,17 +10,21 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations
public class AbpApplicationConfigurationController : AbpController, IAbpApplicationConfigurationAppService
{
private readonly IAbpApplicationConfigurationAppService _applicationConfigurationAppService;
private readonly IAbpAntiForgeryManager _antiForgeryManager;
public AbpApplicationConfigurationController(
IAbpApplicationConfigurationAppService applicationConfigurationAppService)
IAbpApplicationConfigurationAppService applicationConfigurationAppService,
IAbpAntiForgeryManager antiForgeryManager)
{
_applicationConfigurationAppService = applicationConfigurationAppService;
_antiForgeryManager = antiForgeryManager;
}
[HttpGet]
public async Task<ApplicationConfigurationDto> GetAsync()
{
_antiForgeryManager.SetCookie();
return await _applicationConfigurationAppService.GetAsync();
}
}
}
}

8
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationScriptController.cs

@ -2,6 +2,7 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Volo.Abp.AspNetCore.Mvc.AntiForgery;
using Volo.Abp.Auditing;
using Volo.Abp.Http;
using Volo.Abp.Json;
@ -20,17 +21,20 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations
private readonly IJsonSerializer _jsonSerializer;
private readonly AbpAspNetCoreMvcOptions _options;
private readonly IJavascriptMinifier _javascriptMinifier;
private readonly IAbpAntiForgeryManager _antiForgeryManager;
public AbpApplicationConfigurationScriptController(
IAbpApplicationConfigurationAppService configurationAppService,
IJsonSerializer jsonSerializer,
IOptions<AbpAspNetCoreMvcOptions> options,
IJavascriptMinifier javascriptMinifier)
IJavascriptMinifier javascriptMinifier,
IAbpAntiForgeryManager antiForgeryManager)
{
_configurationAppService = configurationAppService;
_jsonSerializer = jsonSerializer;
_options = options.Value;
_javascriptMinifier = javascriptMinifier;
_antiForgeryManager = antiForgeryManager;
}
[HttpGet]
@ -39,6 +43,8 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations
{
var script = CreateAbpExtendScript(await _configurationAppService.GetAsync());
_antiForgeryManager.SetCookie();
return Content(
_options.MinifyGeneratedScript == true
? _javascriptMinifier.Minify(script)

1
framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/RequestLocalization/DefaultAbpRequestLocalizationOptionsProvider.cs

@ -5,7 +5,6 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Localization;
using Microsoft.Extensions.DependencyInjection;
using Nito.AsyncEx;

14
framework/src/Volo.Abp.Core/System/AbpObjectExtensions.cs

@ -1,4 +1,5 @@
using System.ComponentModel;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
@ -49,6 +50,17 @@ namespace System
return list.Contains(item);
}
/// <summary>
/// Check if an item is in the given enumerable.
/// </summary>
/// <param name="item">Item to check</param>
/// <param name="items">Items</param>
/// <typeparam name="T">Type of the items</typeparam>
public static bool IsIn<T>(this T item, IEnumerable<T> items)
{
return items.Contains(item);
}
/// <summary>
/// Can be used to conditionally perform a function
/// on an object and return the modified or the original object.

Loading…
Cancel
Save