From f91fccc510cc8789a9e6dac05d806be6f3fe517d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Wed, 7 Mar 2018 14:10:29 +0300 Subject: [PATCH] Handle multitenancy in microservices. --- .../MicroserviceDemo.AuthServer/Startup.cs | 2 +- .../MicroserviceDemo.TenancyService.csproj | 1 + .../MicroservicesDemoTenancyServiceModule.cs | 9 ++- .../Startup.cs | 2 +- .../MicroserviceDemo.Web/Startup.cs | 2 +- .../AbpAspNetCoreMultiTenancyModule.cs | 9 ++- .../ClaimsHttpTenantResolveContributer.cs | 24 ------- .../MultiTenancyOptionsExtensions.cs | 8 ++- .../Collections/Generic/AbpListExtensions.cs | 48 +++++++++++++ .../Abp/IdentityServer/AbpClaimsService.cs | 30 ++++++++ .../AspNetIdentity/AbpClaimsService.cs | 13 ---- .../AspNetIdentity/AbpProfileService.cs | 17 ++--- .../Volo.Abp.MultiTenancy.Abstractions.csproj | 1 + ...ClaimsPrincipalTenantResolveContributer.cs | 21 ++++++ .../Abp/MultiTenancy/TenantResolveOptions.cs | 5 +- .../Principal/AbpClaimsIdentityExtensions.cs | 67 ++++++++++++++++++ .../Generic/AbpListExtensions_Tests.cs | 69 +++++++++++++++++++ 17 files changed, 265 insertions(+), 63 deletions(-) delete mode 100644 src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/ClaimsHttpTenantResolveContributer.cs create mode 100644 src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AbpClaimsService.cs delete mode 100644 src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AspNetIdentity/AbpClaimsService.cs create mode 100644 src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/CurrentClaimsPrincipalTenantResolveContributer.cs create mode 100644 src/Volo.Abp.Security/System/Security/Principal/AbpClaimsIdentityExtensions.cs create mode 100644 test/Volo.Abp.Core.Tests/System/Collections/Generic/AbpListExtensions_Tests.cs diff --git a/src/MicroserviceDemo/MicroserviceDemo.AuthServer/Startup.cs b/src/MicroserviceDemo/MicroserviceDemo.AuthServer/Startup.cs index e976a59d57..bab82c960f 100644 --- a/src/MicroserviceDemo/MicroserviceDemo.AuthServer/Startup.cs +++ b/src/MicroserviceDemo/MicroserviceDemo.AuthServer/Startup.cs @@ -26,7 +26,7 @@ namespace MicroserviceDemo.AuthServer .AddDebug() .AddSerilog(new LoggerConfiguration() .Enrich.FromLogContext() - .WriteTo.RollingFile("Logs/logs.txt") + .WriteTo.File("Logs/logs.txt") .CreateLogger() ); diff --git a/src/MicroserviceDemo/MicroserviceDemo.TenancyService/MicroserviceDemo.TenancyService.csproj b/src/MicroserviceDemo/MicroserviceDemo.TenancyService/MicroserviceDemo.TenancyService.csproj index 4e28bfadb6..10c93f1e64 100644 --- a/src/MicroserviceDemo/MicroserviceDemo.TenancyService/MicroserviceDemo.TenancyService.csproj +++ b/src/MicroserviceDemo/MicroserviceDemo.TenancyService/MicroserviceDemo.TenancyService.csproj @@ -20,6 +20,7 @@ + diff --git a/src/MicroserviceDemo/MicroserviceDemo.TenancyService/MicroservicesDemoTenancyServiceModule.cs b/src/MicroserviceDemo/MicroserviceDemo.TenancyService/MicroservicesDemoTenancyServiceModule.cs index b8f96c327b..d6568e6a28 100644 --- a/src/MicroserviceDemo/MicroserviceDemo.TenancyService/MicroservicesDemoTenancyServiceModule.cs +++ b/src/MicroserviceDemo/MicroserviceDemo.TenancyService/MicroservicesDemoTenancyServiceModule.cs @@ -6,7 +6,7 @@ using Microsoft.Extensions.DependencyInjection; using Swashbuckle.AspNetCore.Swagger; using Volo.Abp; using Volo.Abp.AspNetCore.Modularity; -using Volo.Abp.Authorization.Permissions; +using Volo.Abp.AspNetCore.MultiTenancy; using Volo.Abp.Autofac; using Volo.Abp.Data; using Volo.Abp.EntityFrameworkCore; @@ -15,7 +15,6 @@ using Volo.Abp.MultiTenancy; using Volo.Abp.MultiTenancy.EntityFrameworkCore; using Volo.Abp.Permissions.EntityFrameworkCore; using Volo.Abp.Security.Claims; -using Volo.Abp.Threading; namespace MicroserviceDemo.TenancyService { @@ -24,6 +23,7 @@ namespace MicroserviceDemo.TenancyService [DependsOn(typeof(AbpMultiTenancyHttpApiModule))] [DependsOn(typeof(AbpMultiTenancyApplicationModule))] [DependsOn(typeof(AbpPermissionsEntityFrameworkCoreModule))] + [DependsOn(typeof(AbpAspNetCoreMultiTenancyModule))] public class MicroservicesDemoTenancyServiceModule : AbpModule { public override void ConfigureServices(IServiceCollection services) @@ -83,9 +83,6 @@ namespace MicroserviceDemo.TenancyService { var app = context.GetApplicationBuilder(); - var store = context.ServiceProvider.GetRequiredService(); - var xx = AsyncHelper.RunSync(() => store.IsGrantedAsync("AbpTenantManagement.Tenants", "Role", "admin")); - app.UseStaticFiles(); app.UseSwagger(); @@ -96,6 +93,8 @@ namespace MicroserviceDemo.TenancyService app.UseAuthentication(); + app.UseMultiTenancy(); + app.UseMvcWithDefaultRoute(); } diff --git a/src/MicroserviceDemo/MicroserviceDemo.TenancyService/Startup.cs b/src/MicroserviceDemo/MicroserviceDemo.TenancyService/Startup.cs index de33f56420..795f768579 100644 --- a/src/MicroserviceDemo/MicroserviceDemo.TenancyService/Startup.cs +++ b/src/MicroserviceDemo/MicroserviceDemo.TenancyService/Startup.cs @@ -26,7 +26,7 @@ namespace MicroserviceDemo.TenancyService .AddDebug() .AddSerilog(new LoggerConfiguration() .Enrich.FromLogContext() - .WriteTo.RollingFile("Logs/logs.txt") + .WriteTo.File("Logs/logs.txt") .CreateLogger() ); diff --git a/src/MicroserviceDemo/MicroserviceDemo.Web/Startup.cs b/src/MicroserviceDemo/MicroserviceDemo.Web/Startup.cs index c904f49d1a..5f798df736 100644 --- a/src/MicroserviceDemo/MicroserviceDemo.Web/Startup.cs +++ b/src/MicroserviceDemo/MicroserviceDemo.Web/Startup.cs @@ -26,7 +26,7 @@ namespace MicroserviceDemo.Web .AddDebug() .AddSerilog(new LoggerConfiguration() .Enrich.FromLogContext() - .WriteTo.RollingFile("Logs/logs.txt") + .WriteTo.File("Logs/logs.txt") .CreateLogger() ); diff --git a/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/AbpAspNetCoreMultiTenancyModule.cs b/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/AbpAspNetCoreMultiTenancyModule.cs index 14f196f27f..a5a10f6528 100644 --- a/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/AbpAspNetCoreMultiTenancyModule.cs +++ b/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/AbpAspNetCoreMultiTenancyModule.cs @@ -14,11 +14,10 @@ namespace Volo.Abp.AspNetCore.MultiTenancy { services.Configure(options => { - options.TenantResolvers.Insert(0, new ClaimsHttpTenantResolveContributer()); - options.TenantResolvers.Insert(1, new QueryStringTenantResolveContributer()); - options.TenantResolvers.Insert(2, new RouteTenantResolveContributer()); - options.TenantResolvers.Insert(3, new HeaderTenantResolveContributer()); - options.TenantResolvers.Insert(4, new CookieTenantResolveContributer()); + options.TenantResolvers.Add(new QueryStringTenantResolveContributer()); + options.TenantResolvers.Add(new RouteTenantResolveContributer()); + options.TenantResolvers.Add(new HeaderTenantResolveContributer()); + options.TenantResolvers.Add(new CookieTenantResolveContributer()); }); services.AddAssemblyOf(); diff --git a/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/ClaimsHttpTenantResolveContributer.cs b/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/ClaimsHttpTenantResolveContributer.cs deleted file mode 100644 index a320a1fbee..0000000000 --- a/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/ClaimsHttpTenantResolveContributer.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Linq; -using Microsoft.AspNetCore.Http; -using Volo.Abp.MultiTenancy; - -namespace Volo.Abp.AspNetCore.MultiTenancy -{ - public class ClaimsHttpTenantResolveContributer : HttpTenantResolveContributerBase - { - protected override void ResolveFromHttpContext(ITenantResolveContext context, HttpContext httpContext) - { - if (httpContext.User?.Identity?.IsAuthenticated == true) - { - base.ResolveFromHttpContext(context, httpContext); - context.Handled = true; - } - } - - protected override string GetTenantIdOrNameFromHttpContextOrNull(ITenantResolveContext context, HttpContext httpContext) - { - var tenantKey = context.GetAspNetCoreMultiTenancyOptions().TenantKey; - return httpContext.User.Claims.FirstOrDefault(c => c.Type == tenantKey)?.Value; - } - } -} \ No newline at end of file diff --git a/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/MultiTenancy/MultiTenancyOptionsExtensions.cs b/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/MultiTenancy/MultiTenancyOptionsExtensions.cs index 79623ff56e..8e6d3ae7ea 100644 --- a/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/MultiTenancy/MultiTenancyOptionsExtensions.cs +++ b/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/MultiTenancy/MultiTenancyOptionsExtensions.cs @@ -1,4 +1,5 @@ -using Volo.Abp.AspNetCore.MultiTenancy; +using System.Collections.Generic; +using Volo.Abp.AspNetCore.MultiTenancy; namespace Volo.Abp.MultiTenancy { @@ -6,7 +7,10 @@ namespace Volo.Abp.MultiTenancy { public static void AddDomainTenantResolver(this TenantResolveOptions options, string domainFormat) { - options.TenantResolvers.Insert(0, new DomainTenantResolveContributer(domainFormat)); + options.TenantResolvers.InsertAfter( + r => r is CurrentClaimsPrincipalTenantResolveContributer, + new DomainTenantResolveContributer(domainFormat) + ); } } } \ No newline at end of file diff --git a/src/Volo.Abp.Core/System/Collections/Generic/AbpListExtensions.cs b/src/Volo.Abp.Core/System/Collections/Generic/AbpListExtensions.cs index 864c50acdc..d4711ad7fc 100644 --- a/src/Volo.Abp.Core/System/Collections/Generic/AbpListExtensions.cs +++ b/src/Volo.Abp.Core/System/Collections/Generic/AbpListExtensions.cs @@ -9,6 +9,54 @@ namespace System.Collections.Generic /// public static class AbpListExtensions { + public static int FindIndex(this IList source, Predicate selector) + { + for (var i = 0; i < source.Count; ++i) + { + if (selector(source[i])) + { + return i; + } + } + + return -1; + } + + public static void AddFirst(this IList source, T item) + { + source.Insert(0, item); + } + + public static void AddLast(this IList source, T item) + { + source.Insert(source.Count, item); + } + + public static void InsertAfter(this IList source, Predicate selector, T item) + { + var index = source.FindIndex(selector); + if (index < 0) + { + source.AddFirst(item); + return; + } + + source.Insert(index + 1, item); + } + + + public static void InsertBefore(this IList source, Predicate selector, T item) + { + var index = source.FindIndex(selector); + if (index < 0) + { + source.AddLast(item); + return; + } + + source.Insert(index, item); + } + public static void MoveItem(this List source, Predicate selector, int targetIndex) { if (!targetIndex.IsBetween(0, source.Count - 1)) diff --git a/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AbpClaimsService.cs b/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AbpClaimsService.cs new file mode 100644 index 0000000000..60453bc927 --- /dev/null +++ b/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AbpClaimsService.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using IdentityServer4.Services; +using Microsoft.Extensions.Logging; +using Volo.Abp.Security.Claims; + +namespace Volo.Abp.IdentityServer +{ + public class AbpClaimsService : DefaultClaimsService + { + public AbpClaimsService(IProfileService profile, ILogger logger) + : base(profile, logger) + { + } + + protected override IEnumerable GetOptionalClaims(ClaimsPrincipal subject) + { + var tenantClaim = subject.FindFirst(AbpClaimTypes.TenantId); + if (tenantClaim == null) + { + return base.GetOptionalClaims(subject); + } + else + { + return base.GetOptionalClaims(subject).Union(new[] { tenantClaim }); + } + } + } +} diff --git a/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AspNetIdentity/AbpClaimsService.cs b/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AspNetIdentity/AbpClaimsService.cs deleted file mode 100644 index c516f45e6a..0000000000 --- a/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AspNetIdentity/AbpClaimsService.cs +++ /dev/null @@ -1,13 +0,0 @@ -using IdentityServer4.Services; -using Microsoft.Extensions.Logging; - -namespace Volo.Abp.IdentityServer.AspNetIdentity -{ - public class AbpClaimsService : DefaultClaimsService - { - public AbpClaimsService(IProfileService profile, ILogger logger) - : base(profile, logger) - { - } - } -} diff --git a/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AspNetIdentity/AbpProfileService.cs b/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AspNetIdentity/AbpProfileService.cs index 2c0dcafa47..7b411221df 100644 --- a/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AspNetIdentity/AbpProfileService.cs +++ b/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AspNetIdentity/AbpProfileService.cs @@ -1,44 +1,41 @@ using System.Threading.Tasks; +using System.Security.Principal; using IdentityServer4.AspNetIdentity; using IdentityServer4.Models; using Microsoft.AspNetCore.Identity; using Volo.Abp.Identity; +using Volo.Abp.MultiTenancy; using Volo.Abp.Uow; namespace Volo.Abp.IdentityServer.AspNetIdentity { - //TODO: Implement multi-tenancy as like in old ABP - public class AbpProfileService : ProfileService { - private readonly IUnitOfWorkManager _unitOfWorkManager; - + private readonly ICurrentTenant _currentTenant; public AbpProfileService( IdentityUserManager userManager, IUserClaimsPrincipalFactory claimsFactory, - IUnitOfWorkManager unitOfWorkManager) + ICurrentTenant currentTenant) : base(userManager, claimsFactory) { - _unitOfWorkManager = unitOfWorkManager; + _currentTenant = currentTenant; } [UnitOfWork] public override async Task GetProfileDataAsync(ProfileDataRequestContext context) { - using (var uow = _unitOfWorkManager.Begin()) + using (_currentTenant.Change(context.Subject.FindTenantId())) { await base.GetProfileDataAsync(context); - await uow.CompleteAsync(); } } [UnitOfWork] public override async Task IsActiveAsync(IsActiveContext context) { - using (var uow = _unitOfWorkManager.Begin()) + using (_currentTenant.Change(context.Subject.FindTenantId())) { await base.IsActiveAsync(context); - await uow.CompleteAsync(); } } } diff --git a/src/Volo.Abp.MultiTenancy.Abstractions/Volo.Abp.MultiTenancy.Abstractions.csproj b/src/Volo.Abp.MultiTenancy.Abstractions/Volo.Abp.MultiTenancy.Abstractions.csproj index 07f95cb1ee..3b155ac986 100644 --- a/src/Volo.Abp.MultiTenancy.Abstractions/Volo.Abp.MultiTenancy.Abstractions.csproj +++ b/src/Volo.Abp.MultiTenancy.Abstractions/Volo.Abp.MultiTenancy.Abstractions.csproj @@ -16,6 +16,7 @@ + diff --git a/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/CurrentClaimsPrincipalTenantResolveContributer.cs b/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/CurrentClaimsPrincipalTenantResolveContributer.cs new file mode 100644 index 0000000000..4d9beb36e6 --- /dev/null +++ b/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/CurrentClaimsPrincipalTenantResolveContributer.cs @@ -0,0 +1,21 @@ +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Security.Claims; + +namespace Volo.Abp.MultiTenancy +{ + public class CurrentClaimsPrincipalTenantResolveContributer : ITenantResolveContributer + { + public void Resolve(ITenantResolveContext context) + { + var principal = context.ServiceProvider.GetRequiredService().Principal; + if (principal?.Identity?.IsAuthenticated != true) + { + return; + } + + context.TenantIdOrName = principal.Claims.FirstOrDefault(c => c.Type == AbpClaimTypes.TenantId)?.Value; + context.Handled = true; + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/TenantResolveOptions.cs b/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/TenantResolveOptions.cs index 1ea7c0a0b1..6cee735e84 100644 --- a/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/TenantResolveOptions.cs +++ b/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/TenantResolveOptions.cs @@ -10,7 +10,10 @@ namespace Volo.Abp.MultiTenancy public TenantResolveOptions() { - TenantResolvers = new List(); + TenantResolvers = new List + { + new CurrentClaimsPrincipalTenantResolveContributer() + }; } } } \ No newline at end of file diff --git a/src/Volo.Abp.Security/System/Security/Principal/AbpClaimsIdentityExtensions.cs b/src/Volo.Abp.Security/System/Security/Principal/AbpClaimsIdentityExtensions.cs new file mode 100644 index 0000000000..321aa22914 --- /dev/null +++ b/src/Volo.Abp.Security/System/Security/Principal/AbpClaimsIdentityExtensions.cs @@ -0,0 +1,67 @@ +using System.Linq; +using System.Security.Claims; +using JetBrains.Annotations; +using Volo.Abp; +using Volo.Abp.Security.Claims; + +namespace System.Security.Principal +{ + public static class AbpClaimsIdentityExtensions + { + public static Guid? FindUserId([NotNull] this ClaimsPrincipal principal) + { + Check.NotNull(principal, nameof(principal)); + + var userIdOrNull = principal.Claims?.FirstOrDefault(c => c.Type == AbpClaimTypes.UserId); + if (userIdOrNull == null || userIdOrNull.Value.IsNullOrWhiteSpace()) + { + return null; + } + + return Guid.Parse(userIdOrNull.Value); + } + + public static Guid? FindTenantId([NotNull] this ClaimsPrincipal principal) + { + Check.NotNull(principal, nameof(principal)); + + var tenantIdOrNull = principal.Claims?.FirstOrDefault(c => c.Type == AbpClaimTypes.TenantId); + if (tenantIdOrNull == null || tenantIdOrNull.Value.IsNullOrWhiteSpace()) + { + return null; + } + + return Guid.Parse(tenantIdOrNull.Value); + } + + public static Guid? FindUserId([NotNull] this IIdentity identity) + { + Check.NotNull(identity, nameof(identity)); + + var claimsIdentity = identity as ClaimsIdentity; + + var userIdOrNull = claimsIdentity?.Claims?.FirstOrDefault(c => c.Type == AbpClaimTypes.UserId); + if (userIdOrNull == null || userIdOrNull.Value.IsNullOrWhiteSpace()) + { + return null; + } + + return Guid.Parse(userIdOrNull.Value); + } + + public static Guid? FindTenantId([NotNull] this IIdentity identity) + { + Check.NotNull(identity, nameof(identity)); + + var claimsIdentity = identity as ClaimsIdentity; + + var tenantIdOrNull = claimsIdentity?.Claims?.FirstOrDefault(c => c.Type == AbpClaimTypes.TenantId); + if (tenantIdOrNull == null || tenantIdOrNull.Value.IsNullOrWhiteSpace()) + { + return null; + } + + return Guid.Parse(tenantIdOrNull.Value); + } + } +} diff --git a/test/Volo.Abp.Core.Tests/System/Collections/Generic/AbpListExtensions_Tests.cs b/test/Volo.Abp.Core.Tests/System/Collections/Generic/AbpListExtensions_Tests.cs new file mode 100644 index 0000000000..e8ed194314 --- /dev/null +++ b/test/Volo.Abp.Core.Tests/System/Collections/Generic/AbpListExtensions_Tests.cs @@ -0,0 +1,69 @@ +using System.Linq; +using Shouldly; +using Xunit; + +namespace System.Collections.Generic +{ + public class AbpListExtensions_Tests + { + [Fact] + public void InsertAfter() + { + var list = Enumerable.Range(1, 3).ToList(); + + list.InsertAfter(i => i == 2, 42); + + list.Count.ShouldBe(4); + list[0].ShouldBe(1); + list[1].ShouldBe(2); + list[2].ShouldBe(42); + list[3].ShouldBe(3); + + list.InsertAfter(i => i == 3, 43); + + list.Count.ShouldBe(5); + list[0].ShouldBe(1); + list[1].ShouldBe(2); + list[2].ShouldBe(42); + list[3].ShouldBe(3); + list[4].ShouldBe(43); + } + + [Fact] + public void InsertAfter_Should_Insert_To_First_If_Not_Found() + { + var list = Enumerable.Range(1, 3).ToList(); + + list.InsertAfter(i => i == 999, 42); + + list.Count.ShouldBe(4); + list[0].ShouldBe(42); + list[1].ShouldBe(1); + list[2].ShouldBe(2); + list[3].ShouldBe(3); + } + + [Fact] + public void InsertBefore() + { + var list = Enumerable.Range(1, 3).ToList(); + + list.InsertBefore(i => i == 2, 42); + + list.Count.ShouldBe(4); + list[0].ShouldBe(1); + list[1].ShouldBe(42); + list[2].ShouldBe(2); + list[3].ShouldBe(3); + + list.InsertBefore(i => i == 1, 43); + + list.Count.ShouldBe(5); + list[0].ShouldBe(43); + list[1].ShouldBe(1); + list[2].ShouldBe(42); + list[3].ShouldBe(2); + list[4].ShouldBe(3); + } + } +}