3
\$\begingroup\$

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...
\$\endgroup\$
  • \$\begingroup\$ This proof-of-concept is also on my GitHub here. \$\endgroup\$ – t3chb0t May 25 '19 at 15:50

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.