Skip to content

Commit

Permalink
Very much WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
Boggin committed Jun 26, 2019
1 parent d57a25c commit 22967a4
Show file tree
Hide file tree
Showing 22 changed files with 507 additions and 19 deletions.
Binary file added .ionide/symbolCache.db
Binary file not shown.
4 changes: 2 additions & 2 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 2,12 @@
"version": "2.0.0",
"tasks": [
{
"taskName": "build",
"label": "build",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/examples/FeatureFlagger.Example.Console/FeatureFlagger.Examples.Console.csproj"
"${workspaceFolder}/FeatureFlagger.sln"
],
"problemMatcher": "$msCompile"
}
Expand Down
7 changes: 6 additions & 1 deletion FeatureFlagger.sln
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 10,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".paket", ".paket", "{F103DA
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{80705188-61E3-4217-8242-1CD206CD152D}"
ProjectSection(SolutionItems) = preProject
.semver = .semver
FeatureFlagger.nuspec = FeatureFlagger.nuspec
FeatureFlagger.xsd = FeatureFlagger.xsd
README.md = README.md
Expand All @@ -28,6 27,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FeatureFlagger.Examples.Con
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FeatureFlagger.Tests", "tests\FeatureFlagger.Tests\FeatureFlagger.Tests.csproj", "{E7769F13-6EEF-459D-BEA2-105C3FFEB444}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FeatureFlagger.ConfigurationWriters", "src\FeatureFlagger.ConfigurationWriters\FeatureFlagger.ConfigurationWriters.csproj", "{46131F8A-055F-4CD4-8A47-405DF4103C68}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -58,6 59,10 @@ Global
{E7769F13-6EEF-459D-BEA2-105C3FFEB444}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E7769F13-6EEF-459D-BEA2-105C3FFEB444}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E7769F13-6EEF-459D-BEA2-105C3FFEB444}.Release|Any CPU.Build.0 = Release|Any CPU
{46131F8A-055F-4CD4-8A47-405DF4103C68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{46131F8A-055F-4CD4-8A47-405DF4103C68}.Debug|Any CPU.Build.0 = Debug|Any CPU
{46131F8A-055F-4CD4-8A47-405DF4103C68}.Release|Any CPU.ActiveCfg = Release|Any CPU
{46131F8A-055F-4CD4-8A47-405DF4103C68}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp1.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../../src/FeatureFlagger/FeatureFlagger.csproj">
<Name>FeatureFlagger.csproj</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup />
<Import Project="..\..\.paket\Paket.Restore.targets" />
</Project>
74 changes: 74 additions & 0 deletions src/FeatureFlagger.Behaviours/Bucket.cs
Original file line number Diff line number Diff line change
@@ -0,0 1,74 @@
namespace FeatureFlagger.Behaviours
{
using System;
using System.Collections.Generic;
using System.Linq;

public class Bucket : IBucket
{
// NOTE: this *really* shouldn't be in memory!
// dictionary has key = bucketName, value = userName list.
private readonly Dictionary<string, List<string>> buckets;

public Bucket()
{
buckets = new Dictionary<string, List<string>>();
}

public bool Check(string bucketName, string userName)
{
bucketName = bucketName.ToUpperInvariant();
userName = userName.ToUpperInvariant();
List<string> users;
if (!buckets.ContainsKey(bucketName)
|| !buckets.TryGetValue(bucketName, out users))
{
return false;
}

return users.Contains(userName);
}

public bool CheckVariants(string featureName, string userName)
{
// get the base feature name.
featureName =
featureName.Remove(
featureName.IndexOf(
Constants.VariantSeparator,
StringComparison.OrdinalIgnoreCase));

// find all the buckets for the group of variants.
var featureBuckets =
buckets.Keys.ToList()
.FindAll(
x =>
x.StartsWith(featureName, StringComparison.OrdinalIgnoreCase));

// check all the variant buckets for the given user.
return featureBuckets.Any(b => Check(b, userName));
}

public void Add(string bucketName, string userName)
{
bucketName = bucketName.ToUpperInvariant();
userName = userName.ToUpperInvariant();
if (buckets.ContainsKey(bucketName))
{
List<string> users;
// ReSharper disable once InvertIf
if (buckets.TryGetValue(bucketName, out users))
{
if (!users.Contains(userName))
{
users.Add(userName);
}
}
}
else
{
buckets.Add(bucketName, new List<string> { userName });
}
}
}
}
138 changes: 138 additions & 0 deletions src/FeatureFlagger.Behaviours/DistributionBehaviour.cs
Original file line number Diff line number Diff line change
@@ -0,0 1,138 @@
namespace FeatureFlagger.Behaviours
{
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Security.Cryptography;

[Export(typeof(IBehaviour))]
public class DistributionBehaviour : IBehaviour
{
private static readonly Random Random = new Random();
private static readonly object Synclock = new object();
private readonly IUser user;
private readonly IBucket bucket;

[ImportingConstructor]
public DistributionBehaviour(
[Import(AllowDefault = true)] IUser user,
[Import(AllowDefault = true)] IBucket bucket)
{
this.user = user ?? new User();
this.bucket = bucket ?? new Bucket();
}

// <distribution [percentage="0.125"] />
public Func<Dictionary<string, string>, bool> Behaviour()
{
const string ControlSubstring = Constants.VariantSeparator Constants.Control;

return x =>
{
var featureName = x[Constants.Feature];
var userName = user.UserName;
// Debug.WriteLine($"UserName: {userName}, FeatureName: {featureName}");
// check the bucket.
var isInBucket = bucket.Check(featureName, userName);
if (isInBucket)
{
// this user is already using this variant.
return true;
}
var percentage = GetPercentage(x);
// is it an A/B test or an ABCD test?
// we do this on the naming conventions where
// an ABCD test contains a variant name.
if (featureName.Contains(Constants.VariantSeparator))
{
// ABCD.
if (bucket.CheckVariants(featureName, userName))
{
return false;
}
if (featureName.IndexOf(ControlSubstring, StringComparison.OrdinalIgnoreCase) >= 0)
{
// _Control variant must be checked last in code flow
// which places an onus on the ordering of variants.
bucket.Add(featureName, userName);
// this user is now in the split test's control group.
return true;
}
// ReSharper disable once InvertIf : it's clearer this way.
if (RollDice(percentage))
{
bucket.Add(featureName, userName);
// this user is now using this variant.
return true;
}
// so, not this variant.
return false;
}
// A/B.
var control = featureName ControlSubstring;
isInBucket = bucket.Check(control, userName);
if (isInBucket)
{
// this user is already in the control group.
return false;
}
if (RollDice(percentage))
{
bucket.Add(featureName, userName);
// in an A/B test this user will get the functionality.
return true;
}
// control.
bucket.Add(control, userName);
return false;
};
}

private static decimal GetPercentage(IReadOnlyDictionary<string, string> x)
{
// percentage is optional: we don't use it for the control group.
string percent =
x.TryGetValue(Constants.Percentage, out percent)
? percent
: "0";

var percentage = Convert.ToDecimal(percent);

// want percentages as decimals, i.e. 0.125 is 12.5%.
if (percentage > 1)
{
throw new ArgumentException("Distribution percentage must be expressed as a decimal.");
}

return percentage;
}

private static bool RollDice(decimal percentage)
{
lock (Synclock)
{
var randomNumber = new byte[1];
var crytoService = new RNGCryptoServiceProvider();
crytoService.GetBytes(randomNumber);
var diceRoll = (randomNumber[0] % 100) 1;

return diceRoll <= percentage * 100;
}
}
}
}
12 changes: 12 additions & 0 deletions src/FeatureFlagger.Behaviours/IBucket.cs
Original file line number Diff line number Diff line change
@@ -0,0 1,12 @@
namespace FeatureFlagger.Behaviours
{
public interface IBucket
{
bool Check(string bucketName, string userName);

bool CheckVariants(string featureName, string userName);

void Add(string bucketName, string userName);
}

}
38 changes: 38 additions & 0 deletions src/FeatureFlagger.Behaviours/User.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 8,44 @@
[Export(typeof(IUser))]
public class User : IUser
{
/*
// TODO: get the authorised user from SSO.
public string UserName => UserTemp.UserName;
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities", Justification = "SQL parameters don't come from user input.")]
public bool UserHasFeature(string userName, string featureName)
{
var connectionString = ConfigurationManager.ConnectionStrings["IntermediaryManagement_DB"].ConnectionString;
var queryString =
"SELECT 1 "
"FROM Users "
"JOIN UserFeature ON Users.Id = UserFeature.Users_Id "
"JOIN Features ON UserFeature.Features_Id = Features.Id "
$"WHERE Users.Name = '{userName}' and Features.Name = '{featureName}'";
var hasFeature = 0;
using (var connection = new SqlConnection(connectionString))
using (var command = new SqlCommand(queryString, connection))
{
// ToDo Code changes required to resolve the Veracode issue below...
//Avoid dynamically constructing SQL queries.Instead, use parameterized prepared statements to prevent the
// database from interpreting the contents of bind variables as part of the query.Always validate untrusted input to
// ensure that it conforms to the expected format, using centralized data validation routines when possible.
//connection.Open();
//var reader = command.ExecuteReader();
//while (reader.Read())
//{
// hasFeature = reader.GetInt32(0);
//}
//reader.Close();
}
return Convert.ToBoolean(hasFeature);
*/
public string UserName()
{
throw new NotImplementedException();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 6,8 @@
<ItemGroup>
<ProjectReference Include="..\FeatureFlagger.Domain\FeatureFlagger.Domain.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.Data.SqlClient" Version="4.6.1" />
</ItemGroup>
<Import Project="..\..\.paket\Paket.Restore.targets" />
</Project>
8 changes: 4 additions & 4 deletions src/FeatureFlagger.ConfigurationReaders/FeaturesSection.cs
Original file line number Diff line number Diff line change
@@ -1,13 1,13 @@
namespace RoyalLondon.IntermediaryManagement.Api.FeatureFlagger
namespace IntermediaryManagement.Api.FeatureFlagger
{
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Globalization;
using System.Linq;
using System.Xml;
using global::FeatureFlagger.Domain;

using System.Xml;
using global::FeatureFlagger.Domain;

public class FeaturesSection : IConfigurationSectionHandler
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1059:MembersShouldNotExposeCertainConcreteTypes", MessageId = "System.Xml.XmlNode", Justification = "Standard method")]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 1,4 @@
namespace RoyalLondon.IntermediaryManagement.Api.FeatureFlagger
namespace IntermediaryManagement.Api.FeatureFlagger
{
public interface IConfigurationSectionHandler
{
Expand Down
6 changes: 3 additions & 3 deletions src/FeatureFlagger.ConfigurationReaders/StoreReader.cs
Original file line number Diff line number Diff line change
@@ -1,13 1,13 @@
namespace Unsettling.FeatureFlagger.ConfigurationReaders
namespace FeatureFlagger.ConfigurationReaders
{
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Composition;
using System.Configuration;
using System.Data.SqlClient;
using System.Linq;

using Feature = Unsettling.FeatureFlagger.Feature;
using Feature = Domain.Feature;

[Export(typeof(IConfigurationReader))]
public class StoreReader : IConfigurationReader
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\FeatureFlagger.Domain\FeatureFlagger.Domain.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.Data.SqlClient" Version="4.6.1" />
</ItemGroup>
<Import Project="..\..\.paket\Paket.Restore.targets" />
</Project>
Loading

0 comments on commit 22967a4

Please sign in to comment.