1

I am using Strategy Pattern, I have heaps of rules and I need to check all rows in Azure storage table against each Rule.

interface IRule where TEntity : TableEntity, new()
{
    string TableName { get; } // It could be "ContractAccount", "Bill", "Transaction" etc.
    string Rule { get; }
    string SaveToTable { get; }
    TableQuery<TEntity> TableQuery { get; }
    ReportEntity Handle(TableEntity entity);
}

So instance of rules lives inside the Validator.

 public Validator()
        {
            Rules = new List<IRule>();
            Rules.Add(new AddressRule());
        }

The Table Entity class(ContractAccount.cs Bill.cs etc.) will have the same name as the value IRule.TableName holds.

So this is where the ContractAccount comes from.

Then in the Validator, I have Validate() which looks like:

public async void Validate(CloudStorageAccount storageAccount)
{
    var tableClient = storageAccount.CreateCloudTableClient();
           
        //.....
        var query = new TableQuery<ContractAccount>(); //<-- I want to replace ContractAccount with something generic
        
        //...
        var rows = await tableToBeValidated.ExecuteQuerySegmentedAsync(query, token);
    }
    //...
}

In my AddressRule.cs

public class AddressRule : IRule<ContractAccount>
    {
        public string TableName => "ContractAccount";

        public string Rule => "Email cannot be empty";

        public string SaveToTable => "XXXX";

        public TableQuery<ContractAccount> TableQuery => new TableQuery<ContractAccount>();

        public ReportEntity Handle(TableEntity entity)
        {
            var contract = entity as ContractAccount;
            if(contract == null)
            {
                throw new Exception($"Expecting entity type {TableName}, but passed in invalid entity");
            }

            if (string.IsNullOrWhiteSpace(contract.Address))
            {
                var report = new ReportEntity(this.Rule, contract.UserId, contract.AccountNumber, contract.ContractNumber)
                {
                    PartitionKey = contract.UserId,
                    RowKey = contract.AccountNumber
                };
                
                return report;
            }

            return null;
        }
    }

As you can see

var query = new TableQuery<ContractAccount>();

I need to replace the Hard-coded with something like:

var type = Type.GetType(tableName);
var query = new TableQuery<type>();

but the placeholder(ContractAccount) will change when app is running, it could be Bill, Policy, Transaction etc....

I cannot use the <T> thing.

How can I replace the ContractAccount with a generic thing?

Update 2

After applied Juston.Another.Programmer's suggection, I got this error.

enter image description here

Update 3

Now I updated code to below:

interface IRule<TEntity> where TEntity : TableEntity
{
    string TableName { get; }
    string Rule { get; }
    string SaveToTable { get; }
    ReportEntity Handle(TableEntity entity);
    TableQuery<TEntity> GetTableQuery();
}

Which I specified what type of class the TEntity has to be, it removes the 1st error, but the 2nd error persists:

Error CS0310 'TEntity' must be a non-abstract type with a public parameterless constructor in order to use it as parameter 'TElement' in the generic type or method 'TableQuery'

Update 4

I found how to fix the another error:

interface IRule<TEntity> 
   where TEntity : TableEntity, new()

But then, I have problem to add my AddressRule into Rules in the Validator class.

  public Validator()
    {
        Rules = new List<IRule<TableEntity>>();
        var addressRule = new AddressRule();

        Rules.Add(addressRule);
    }

enter image description here

halfer
  • 19,824
  • 17
  • 99
  • 186
Franva
  • 6,565
  • 23
  • 79
  • 144

3 Answers3

1

Something like this:

var genericType = typeof(TableQuery<>);
Type[] itemTypes = { Type.GetType("MyNamespace.Foo.Entities." + tableName) };
var concretType = genericType.MakeGenericType(itemTypes);
var query = Activator.CreateInstance(concretType);
Christoph
  • 3,322
  • 2
  • 19
  • 28
  • Argument 1: cannot convert from 'object' to 'Microsoft.WindowsAzure.Storage.Table.TableQuery' – Franva Jun 26 '19 at 07:38
  • Here is how the query is used and where the error happens: var rows = await tableToBeValidated.ExecuteQuerySegmentedAsync(query, token); I updated into my post as well. – Franva Jun 26 '19 at 07:39
  • the reason is query is an object as created by the Activator, but the ExecuteQuerySegmentedAsync() is looking for TableQuery<> as argument 1. – Franva Jun 26 '19 at 07:45
  • @Franva to do this with reflection, you'll need to make a generic `MethodInfo` of `ExecuteQuerySegmentedAsync` in addition to the generic `TableQuery` type @Christoph put in his answer. It's normally easier to create a single generic helper class that has all these method calls to avoid needing to constantly make generic types. – just.another.programmer Jun 26 '19 at 07:54
  • hi @just.another.programmer could you please be more specific about MethodInfo? – Franva Jun 26 '19 at 23:54
0

You could use reflection like @Christoph suggested, but in this case there's an easier approach. Add a TEntity generic parameter to your IRule class instead of using the TableName string property and add a GetTableQuery method to the class.

interface IRule<TEntity>
{
    string Rule { get; }
    string SaveToTable { get; }
    ReportEntity Handle(TableEntity entity);
    TableQuery<TEntity> GetTableQuery();
}

Then, in your IRule<TEntity> implementations add the correct entity. Eg for AddressRule.

    public class AddressRule : IRule<ContractAcccount>
    {
        public string TableName => "ContractAccount";

        public string Rule => "Email cannot be empty";

        public string SaveToTable => "XXXX";

        public ReportEntity Handle(TableEntity entity)
        {
            var contract = entity as ContractAccount;
            if(contract == null)
            {
                throw new Exception($"Expecting entity type {TableName}, but passed in invalid entity");
            }

            if (string.IsNullOrWhiteSpace(contract.Address))
            {
                var report = new ReportEntity(this.Rule, contract.UserId, contract.AccountNumber, contract.ContractNumber)
                {
                    PartitionKey = contract.UserId,
                    RowKey = contract.AccountNumber
                };

                return report;
            }

            return null;
        }

        public TableQuery<ContractAccount> GetTableQuery()
        {
            return new TableQuery<ContractAccount>();
        }
    }

Now, in your Validate method, you can use the GetTableQuery method from the IRule.

public async void Validate(CloudStorageAccount storageAccount)
{
    var tableClient = storageAccount.CreateCloudTableClient();

        //.....
        var query = rule.GetTableQuery();

        //...
        var rows = await tableToBeValidated.ExecuteQuerySegmentedAsync(query, token);
    }
    //...
}
just.another.programmer
  • 8,579
  • 8
  • 51
  • 90
  • hi JAP, I like this approach, however, it throws me an error. Please see my update 2. – Franva Jun 26 '19 at 23:48
  • hi JAP, I feel I am almost there, but just 1 issue to be solved. I have updated post to include what I have improved so far. Could you please read and help? thanks – Franva Jun 27 '19 at 01:44
0

The longer I think about it the more I get the feeling that what you need is a generic solution and not one with generics. I guess that the table client in line

var tableClient = storageAccount.CreateCloudTableClient();

does always return something like a DataTable or an object with an IEnumerable, independently of whether you ask for a ContractAccount or a Bill. If that's the case, it might be better to have a validator that loads all the rules of all entities from the database (or through factory patterns or hardcoded) and then applies the according ones to the given entity.

Like that, the set of rules can be defined using XML or some other sort of serialization (not part of this example) and only a few rule classes are needed (I call them EntityValidationRule).

The parent of all rules for all entities could look like this:

public abstract class EntityValidationRule {

    //Private Fields
    private Validator validator;

    //Constructors

    public EntityValidationRule(String tableName, IEnumerable<String> affectedFields) {
        TableName = tableName ?? throw new ArgumentNullException(nameof(tableName));
        AffectedFields = affectedFields?.ToArray() ?? Array.Empty<String>();
    }

    //Public Properties

    public String TableName { get; }
    public String[] AffectedFields { get; }
    public virtual String Description { get; protected set; }

    //Public Methods

    public Boolean IsValid(DataRow record, ref IErrorDetails errorDetails) {
        if (record == null) throw new InvalidOperationException("Programming error in Validator.cs");
        if (!Validator.IdentifyerComparer.Equals(record.Table.TableName, TableName)) throw new InvalidOperationException("Programming error in Validator.cs");
        String myError = GetErrorMessageIfInvalid(record);
        if (myError == null) return true;
        errorDetails = CreateErrorDetails(record, myError);
        return false;
    }

    //Protected Properties

    public Validator Validator {
        get {
            return validator;
        }
        internal set {
            if ((validator != null) && (!Object.ReferenceEquals(validator, value))) {
                throw new InvalidOperationException("An entity validation rule can only be added to a single validator!");
            }
            validator = value;
        }
    }

    //Protected Methods

    protected virtual IErrorDetails CreateErrorDetails(DataRow record, String errorMessage) {
        return new ErrorDetails(record, this, errorMessage);
    }

    protected abstract String GetErrorMessageIfInvalid(DataRow record);

}

and to stay with your example, the sample implementation for an empty text field check could look like this (having an intermediate class OneFieldRule):

public abstract class OneFieldRule : EntityValidationRule {

    public OneFieldRule(String tableName, String fieldName) : base(tableName, new String[] { fieldName }) {
    }

    protected String FieldName => AffectedFields[0];

}

and like this:

public class TextFieldMustHaveValue : OneFieldRule {

    public TextFieldMustHaveValue(String tableName, String fieldName) : base(tableName, fieldName) {
        Description = $"Field {FieldName} cannot be empty!";
    }

    protected override String GetErrorMessageIfInvalid(DataRow record) {
        if (String.IsNullOrWhiteSpace(record.Field<String>(FieldName))) {
            return Description;
        }
        return null;
    }

}

Then the central validator that works like a service to validate whatever entity needs to be validated I might implement like this:

public sealed class Validator {

    //Private Fields
    private Dictionary<String, List<EntityValidationRule>> ruleDict;

    //Constructors

    //The list of all rules we just have somehow...
    public Validator(IEnumerable<EntityValidationRule> rules, StringComparer identifyerComparer) {
        if (rules == null) throw new ArgumentNullException(nameof(rules));
        if (identifyerComparer == null) identifyerComparer = StringComparer.OrdinalIgnoreCase;
        IdentifyerComparer = identifyerComparer;
        ruleDict = new Dictionary<String, List<EntityValidationRule>>(IdentifyerComparer);
        foreach (EntityValidationRule myRule in rules) {
            myRule.Validator = this;
            List<EntityValidationRule> myRules = null;
            if (ruleDict.TryGetValue(myRule.TableName, out myRules)) {
                myRules.Add(myRule);
            } else {
                myRules = new List<EntityValidationRule> { myRule };
                ruleDict.Add(myRule.TableName, myRules);
            }
        }
    }

    //Public Properties

    public StringComparer IdentifyerComparer { get; }

    //Public Methods

    public Boolean IsValid(DataRow record, ref IErrorDetails[] errors) {
        //Check whether the record is null
        if (record == null) {
            errors = new IErrorDetails[] { new ErrorDetails(record, null, "The given record is null!") };
            return false;
        }
        //Loop through every check and invoke them
        List<IErrorDetails> myErrors = null;
        IErrorDetails myError = null;
        foreach (EntityValidationRule myRule in GetRules(record.Table.TableName)) {
            if (myRule.IsValid(record, ref myError)) {
                if (myErrors == null) myErrors = new List<IErrorDetails>();
                myErrors.Add(myError);
            }
        }
        //Return true if there are no errors
        if (myErrors == null) return true;
        //Otherwise assign them as result and return false
        errors = myErrors.ToArray();
        return false;
    }

    //Private Methods

    private IEnumerable<EntityValidationRule> GetRules(String tableName) {
        if (ruleDict.TryGetValue(tableName, out List<EntityValidationRule> myRules)) return myRules;
        return Array.Empty<EntityValidationRule>();
    }

}

And the error details as an interface:

public interface IErrorDetails {

    DataRow Entity { get; }
    EntityValidationRule Rule { get; }
    String ErrorMessage { get; }

}

...and an implementation of it:

public class ErrorDetails : IErrorDetails {

    public ErrorDetails(DataRow entity, EntityValidationRule rule, String errorMessage) {
        Entity = entity;
        Rule = rule;
        ErrorMessage = errorMessage;
    }

    public DataRow Entity { get; }

    public EntityValidationRule Rule { get; }

    public String ErrorMessage { get; }

}

I know this is a totally different approach as you started off, but I think the generics give you a hell of a lot of work with customized entities that have customized validators for each and every table in your database. And as soon as you add a table, code needs to be written, compiled and redistributed.

Christoph
  • 3,322
  • 2
  • 19
  • 28