My FeatureSerivce
provides only basic APIs, so it is good in dealing with single features. Like I can configure only one feauture at a time:
public FeatureService Configure(string name, Func<FeatureOptions, FeatureOptions> configure)
This might qiuckly get a lot of work when everything that has something to do with lets say IO
should be disabled.
To simplify this task, I've created another set of extensions that evaluate features' tags and act accordingly.
A tag is an attribute that I use to set them:
[AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class | AttributeTargets.Property)]
public class TagAttribute : Attribute, IEnumerable<string>
{
private readonly string[] _names;
public TagAttribute(params string[] names) => _names = names;
public IEnumerator<string> GetEnumerator() => _names.Cast<string>().GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
Example
namespace Features
{
[TypeMemberKeyFactory]
[RemoveInterfacePrefix]
public interface IDemo : INamespace
{
object Greeting { get; }
[Tag("io")]
object ReadFile { get; }
}
[TypeMemberKeyFactory]
[RemoveInterfacePrefix]
public interface IDatabase : INamespace
{
[Tag("io")]
object Commit { get; }
}
}
Then I use a collection of features that I want to configure together based on some common criteria:
[Fact]
public void Can_configure_features_by_tags()
{
var features = new FeatureService(Logger<FeatureService>.Null);
var names = new[] { typeof(IDemo), typeof(IDatabase) }.GetFeatures("io").ToFeatureNames();
features.Configure(names, o => o ^ Enabled);
var bodyCounter = 0;
var otherCounter = 0;
features.Execute(From<IDemo>.Select(x => x.Greeting), () => bodyCounter++, () => otherCounter++);
features.Execute(From<IDemo>.Select(x => x.ReadFile), () => bodyCounter++, () => otherCounter++);
features.Execute(From<IDatabase>.Select(x => x.Commit), () => bodyCounter++, () => otherCounter++);
Assert.Equal(1, bodyCounter);
Assert.Equal(2, otherCounter);
}
Implementation
This is the set of extensions that drive this process. I separated them as much as it was possible so that each one of them takes care only of one tiny part.
Creating a feature name is a multistep process that works like this
- scan each feature-type and get its tags
- scan each feature-property and get its tags
- merge type's and property's tags together
- check if they match the criteria
- use feature-type and property to create a lambda-expression
- let the key-factory create names for each combination
- use created names to configure each feature
public static class FeatureServiceExtensions
{
[NotNull]
public static FeatureService Configure(this FeatureService features, IEnumerable<string> names, Func<FeatureOptions, FeatureOptions> configure)
{
foreach (var name in names)
{
features.Configure(name, configure);
}
return features;
}
public static IEnumerable<string> ToFeatureNames(this IEnumerable<(Type Feature, PropertyInfo Property)> features, IKeyFactory keyFactory = default)
{
return
from t in features
// x.Member
let l = Expression.Lambda(
Expression.Property(
Expression.Constant(null, t.Feature),
t.Property.Name
)
)
select (keyFactory ?? KeyFactory.Default).CreateKey(l);
}
public static IEnumerable<(Type Feature, PropertyInfo Property)> GetFeatures(this IEnumerable<Type> features, params string[] tags)
{
if (!tags.Any()) throw new ArgumentException("You need to specify at least one tag.");
return features.GetFeatures(tags.AsEnumerable());
}
public static IEnumerable<(Type Feature, PropertyInfo Property)> GetFeatures(this IEnumerable<Type> features, IEnumerable<string> tags)
{
tags = tags.Distinct(SoftString.Comparer);
return
from f in features
let featureTags = f.GetTags()
from p in f.GetProperties()
let propertyTags = p.GetTags().Concat(featureTags).Distinct(SoftString.Comparer)
where propertyTags.Matches(tags)
select (f, p);
}
private static IEnumerable<string> GetTags(this MemberInfo member)
{
return member.GetCustomAttributes<TagAttribute>().SelectMany(t => t);
}
private static bool Matches(this IEnumerable<string> propertyTags, IEnumerable<string> otherTags)
{
return
!otherTags
.Except(propertyTags, SoftString.Comparer)
.Any();
}
}
I slighltly refactored the From<T>
helper from my other question so that I can use the KeyFactory
here and when creating them for multiple features:
public static class From<T> where T : INamespace
{
[NotNull]
public static string Select<TMember>([NotNull] Expression<Func<T, TMember>> selector)
{
return KeyFactory.Default.CreateKey(selector);
}
}
public class KeyFactory : IKeyFactory
{
[NotNull] public static readonly IKeyFactory Default = new KeyFactory();
public string CreateKey(LambdaExpression selector)
{
if (selector == null) throw new ArgumentNullException(nameof(selector));
var member = selector.ToMemberExpression().Member;
return
GetKeyFactory(member)
.FirstOrDefault(Conditional.IsNotNull)
?.CreateKey(selector)
?? throw DynamicException.Create("KeyFactoryNotFound", $"Could not find key-factory on '{selector}'.");
}
[NotNull, ItemCanBeNull]
private static IEnumerable<IKeyFactory> GetKeyFactory(MemberInfo member)
{
// Member's attribute has a higher priority and can override type's default factory.
yield return member.GetCustomAttribute<KeyFactoryAttribute>();
yield return member.DeclaringType?.GetCustomAttribute<KeyFactoryAttribute>();
}
}
Questions
Here's the example usage once again:
var features = new FeatureService(Logger<FeatureService>.Null); var names = new[] { typeof(IDemo), typeof(IDatabase) }.GetFeatures("io").ToFeatureNames(); features.Configure(names, o => o ^ Enabled);
- Would you say that this is easy to use and easily customizable?
- Have I separeted everything properly?
- Do you have an idea how I can better deal with
new[] { typeof(IDemo), typeof(IDatabase) }
as honestly, this part looks like it needs some improvement. Maybe a collection of all features? Mhmmmm...
-
\$\begingroup\$ This proof-of-concept is also on my GitHub here. \$\endgroup\$t3chb0t– t3chb0t2019年05月25日 15:50:50 +00:00Commented May 25, 2019 at 15:50