.NET C# Как сделать заполнитель в Generics универсальным

Обновление 4:

Я нашел, как исправить другую ошибку:

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

Но тогда у меня возникает проблема с добавлением моего AddressRule в правила в классе Validator.

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

        Rules.Add(addressRule);
    }

введите здесь описание изображения

Обновление 3:

Теперь я обновил код ниже:

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

Что я указал, какой тип класса должен быть TEntity, он удаляет 1-ю ошибку, но 2-я ошибка сохраняется:

Ошибка CS0310 «TEntity» должен быть неабстрактным типом с общедоступным конструктором без параметров, чтобы использовать его в качестве параметра «TElement» в универсальном типе или методе «TableQuery».


Обновление 2:

После применения предложения Juston.Another.Programmer я получил эту ошибку.

введите здесь описание изображения


Обновлять:

Я использую шаблон стратегии, у меня есть куча правил, и мне нужно проверить все строки в таблице хранилища Azure по каждому правилу.

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);
}

Итак, экземпляр правил живет внутри валидатора.

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

Класс Table Entity (ContractAccount.cs Bill.cs и т. д.) будет иметь то же имя, что и значение IRule.TableName.

Так вот откуда взялся ContractAccount.


Затем в валидаторе у меня есть Validate(), который выглядит так:

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);
    }
    //...
}

В моем 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;
        }
    }

Как вы видете

var query = new TableQuery<ContractAccount>();

Мне нужно заменить Hard-coded на что-то вроде:

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

но заполнитель (ContractAccount) будет меняться при запуске приложения, это может быть счет, политика, транзакция и т. д....

я не могу использовать

 <T>

предмет.

Как я могу заменить ContractAccount чем-то общим?

Спасибо


person Franva    schedule 26.06.2019    source источник
comment
Добавить общий параметр T в любой метод, в котором находится этот код?   -  person Sweeper    schedule 26.06.2019
comment
привет @Sweeper, я не могу использовать T, см. мой обновленный пост.   -  person Franva    schedule 26.06.2019
comment
привет @John да, пожалуйста, смотрите мой обновленный пост. Спасибо   -  person Franva    schedule 26.06.2019
comment
Можете ли вы просто показать объявление метода?   -  person Sweeper    schedule 26.06.2019
comment
привет @John, как я могу передать TEntityType? Поскольку он всегда меняется в зависимости от того, с каким экземпляром правила он работает.   -  person Franva    schedule 26.06.2019
comment
Похоже, вам нужно использовать Activator для создания экземпляра универсального типа? Несколько похоже на начало stackoverflow.com/questions/9140873/   -  person grek40    schedule 26.06.2019
comment
привет @grek40 не такой тихий как этот. Мне нужно использовать свойства объекта экземпляра, созданного Reflection.   -  person Franva    schedule 26.06.2019
comment
привет @Sweeper обновлен, пожалуйста, помогите   -  person Franva    schedule 26.06.2019
comment
привет @PeterSmith TableQuery — это встроенный класс .NET.   -  person Franva    schedule 26.06.2019


Ответы (3)


Что-то вроде этого:

var genericType = typeof(TableQuery<>);
Type[] itemTypes = { Type.GetType("MyNamespace.Foo.Entities." + tableName) };
var concretType = genericType.MakeGenericType(itemTypes);
var query = Activator.CreateInstance(concretType);
person Christoph    schedule 26.06.2019
comment
Аргумент 1: невозможно преобразовать из «объекта» в «Microsoft.WindowsAzure.Storage.Table.TableQuery». - person Franva; 26.06.2019
comment
Вот как используется запрос и где возникает ошибка: var rows = await tableToBeValidated.ExecuteQuerySegmentedAsync(query, token); Я также обновил свой пост. - person Franva; 26.06.2019
comment
причина в том, что запрос является объектом, созданным активатором, но ExecuteQuerySegmentedAsync() ищет TableQuery‹› в качестве аргумента 1. - person Franva; 26.06.2019
comment
@Franva, чтобы сделать это с отражением, вам нужно сделать общий MethodInfo из ExecuteQuerySegmentedAsync в дополнение к общему типу TableQuery, который @Christoph указал в своем ответе. Обычно проще создать один общий вспомогательный класс, который имеет все эти вызовы методов, чтобы избежать необходимости постоянно создавать общие типы. - person just.another.programmer; 26.06.2019
comment
привет @just.another.programmer, не могли бы вы поподробнее рассказать о MethodInfo? - person Franva; 27.06.2019

Вы можете использовать отражение, как предложил @Christoph, но в этом случае есть более простой подход. Добавьте общий параметр TEntity в класс IRule вместо использования строкового свойства TableName и добавьте в класс метод GetTableQuery.

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

Затем в ваших реализациях IRule<TEntity> добавьте правильный объект. Например, для 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>();
        }
    }

Теперь в вашем методе Validate вы можете использовать метод GetTableQuery из файла IRule.

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

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

        //...
        var rows = await tableToBeValidated.ExecuteQuerySegmentedAsync(query, token);
    }
    //...
}
person just.another.programmer    schedule 26.06.2019
comment
привет JAP, мне нравится этот подход, однако он выдает мне ошибку. Пожалуйста, смотрите мое обновление 2. - person Franva; 27.06.2019
comment
привет JAP, я чувствую, что я почти там, но только 1 вопрос, который нужно решить. Я обновил сообщение, чтобы включить то, что я улучшил до сих пор. Не могли бы вы прочитать и помочь? Спасибо - person Franva; 27.06.2019

Чем дольше я думаю об этом, тем больше у меня появляется ощущение, что вам нужно универсальное решение, а не решение с дженериками. Я предполагаю, что клиент таблицы в очереди

var tableClient = storageAccount.CreateCloudTableClient();

всегда возвращает что-то вроде DataTable или объекта с IEnumerable, независимо от того, запрашиваете ли вы ContractAccount или Bill. В этом случае может быть лучше иметь валидатор, который загружает все правила всех сущностей из базы данных (или с помощью заводских шаблонов или жестко закодированных), а затем применяет соответствующие правила к данной сущности.

Таким образом, набор правил может быть определен с использованием XML или другого вида сериализации (не является частью этого примера), и требуется всего несколько классов правил (я называю их EntityValidationRule).

Родитель всех правил для всех сущностей может выглядеть так:

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);

}

и, чтобы остаться с вашим примером, пример реализации для проверки пустого текстового поля может выглядеть так (с промежуточным классом OneFieldRule):

public abstract class OneFieldRule : EntityValidationRule {

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

    protected String FieldName => AffectedFields[0];

}

а вот так:

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;
    }

}

Затем центральный валидатор, который работает как служба для проверки любого объекта, который необходимо проверить, я мог бы реализовать следующим образом:

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>();
    }

}

И детали ошибки как интерфейс:

public interface IErrorDetails {

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

}

... и его реализация:

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; }

}

Я знаю, что это совершенно другой подход, когда вы начали, но я думаю, что дженерики дают вам чертовски много работы с настраиваемыми сущностями, у которых есть настроенные валидаторы для каждой таблицы в вашей базе данных. И как только вы добавляете таблицу, код нужно писать, компилировать и распространять.

person Christoph    schedule 28.06.2019