EntityFramework (Шаблон репозитория, Проверка данных, Dto's) - PullRequest
0 голосов
/ 08 мая 2019

У меня было некоторое трудное время, чтобы разобраться, как создать Restful API с EntityFramework.Проблема главным образом в том, что этот API следует использовать в течение длительного времени, и я хочу, чтобы он был максимально понятным и понятным, с хорошей производительностью.Достаточно этого, давайте разберемся с этой проблемой.

Disclamer : Из-за companypolicy и не могу публиковать здесь слишком много, но я постараюсь решить проблему наилучшим образомвозможный.Там также будут только фрагменты кода, и они могут быть недействительными.Я также довольно новичок в C # и, будучи JuniorD, я никогда раньше не касался API. И извините за мой английский, это мой второй язык.

Каждая модель является производнойиз BaseModel class

public class BaseModel
{
    [Required]
    public Guid CompanyId { get; set; }

    public DateTime CreatedDateTime { get; set; }

    [StringLength(100)]
    public string CreatedBy { get; set; }

    public DateTime ChangedDateTime { get; set; }

    [StringLength(100)]
    public string ChangedBy { get; set; }

    public bool IsActive { get; set; } = true;

    public bool IsDeleted { get; set; }
}

public class Carrier : BaseModel
{
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    [Key]
    public Guid CarrierId { get; set; }

    public int CarrierNumber { get; set; }

    [StringLength(100)]
    public string CarrierName { get; set; }

    [StringLength(100)]
    public string AddressLine { get; set; }

    public Guid? PostOfficeId { get; set; }
    public PostOffice PostOffice { get; set; }
    public Guid? CountryId { get; set; }
    public Country Country { get; set; }

    public List<CustomerCarrierLink> CustomerCarrierLinks { get; set; }
}

Каждый репозиторий происходит из репозитория и имеет собственный интерфейс.

public class CarrierRepository : Repository<Carrier>, ICarrierRepository
{
    public CarrierRepository(CompanyMasterDataContext context, UnitOfWork unitOfWork) : base(context, unitOfWork) { }

    #region Helpers
    public override ObjectRequestResult<Carrier> Validate(Carrier carrier, List<string> errorMessages)
    {
        var errorMessages = new List<string>();

        if(carrier != null)
        {
            var carrierIdentifier = (carrier.CarrierName ?? carrier.CarrierNumber.ToString()) ?? carrier.CarrierGLN;

            if (string.IsNullOrWhiteSpace(carrier.CarrierName))
            {
                errorMessages.Add($"Carrier({carrierIdentifier}): Carrier name is null/empty");
            }
        }
        else
        {
            errorMessages.Add("Carrier: Cannot validate null value.");
        }

        return CreateObjectResultFromList(errorMessages, carrier); // nonsense
    }

}

UnitOfWork наследуется от классаUnitOfWorkDiscoverySet, этот класс инициализирует свойства репозитория с помощью отражения, а также содержит метод (OnBeforeChildEntityProcessed) для вызова каждого OnBeforeChildEntityProcessed.

public class UnitOfWork : UnitOfWorkDiscoverySet
{
    public UnitOfWork(CompanyMasterDataContext context) 
        : base(context){}

    public CarrierRepository Carriers { get; internal set; }
    public PostOfficeRepository PostOffices { get; internal set; }
    public CustomerCarrierLinkRepository CustomerCarrierLinks { get; internal set; }
}


public IRepository<Entity> where Entity : BaseModel
{
ObjectRequestResult<Entity> Add(Entity entity);
ObjectRequestResult<Entity> Update(Entity entity);
ObjectRequestResult<Entity> Delete(Entity entity);
ObjectRequestResult<Entity> Validate(Entity entity);
Entity GetById(Guid id);
Guid GetEntityId(Entity entity);
}

public abstract class Repository<Entity> : IRepository<Entity> where Entity : BaseModel
{
    protected CompanyMasterDataContext _context;
    protected UnitOfWork _unitOfWork;

    public Repository(CompanyMasterDataContext context, UnitOfWork unitOfWork)
    {
        _context = context;
        _unitOfWork = unitOfWork;
    }

    public ObjectRequestResult<Entity> Add(Entity entity)
    {
        if (!EntityExist(GetEntityId(entity)))
        {
            try
            {
                var validationResult = Validate(entity);

                if (validationResult.IsSucceeded)
                {
                    _context.Add(entity);
                    _context.UpdateEntitiesByBaseModel(entity);
                    _context.SaveChanges();

                    return new ObjectRequestResult<Entity>()
                    {
                        ResultCode = ResultCode.Succceeded,
                        ResultObject = entity,
                        Message = OBJECT_ADDED
                    };
                }

                return validationResult;
            }
            catch (Exception exception)
            {
                return new ObjectRequestResult<Entity>()
                {
                    ResultCode = ResultCode.Failed,
                    ResultObject = entity,
                    Message = OBJECT_NOT_ADDED,
                    ErrorMessages = new List<string>()
                    {
                        exception?.Message,
                        exception?.InnerException?.Message
                    }
                };
            }
        }

        return Update(entity);
    }

    public virtual ObjectRequestResult Validate(Entity entity)
    {
        if(entity != null)
        {
            if(!CompanyExist(entity.CompanyId))
            {
                return EntitySentNoCompanyIdNotValid(entity); // nonsense
            }
        }

        return EntitySentWasNullBadValidation(entity); // nonsense
    }
}

DbContext class:

public class CompanyMasterDataContext : DbContext {

public DbSet<PostOffice> PostOffices { get; set; }
public DbSet<Carrier> Carriers { get; set; }

public DbSet<Company> Companies { get; set; }
public DbSet<CustomerCarrierLink> CustomerCarrierLinks { get; set; }



public UnitOfWork Unit { get; internal set; }

public CompanyMasterDataContext(DbContextOptions<CompanyMasterDataContext> options)
    : base(options)
{
    Unit = new UnitOfWork(this);
}

public void UpdateEntitiesByBaseModel(BaseModel baseModel)
{
    foreach (var entry in ChangeTracker.Entries())
    {
        switch (entry.State)
        {
            case EntityState.Added:
                entry.CurrentValues["CompanyId"] = baseModel.CompanyId;
                entry.CurrentValues["CreatedDateTime"] = DateTime.Now;
                entry.CurrentValues["CreatedBy"] = baseModel.CreatedBy;
                entry.CurrentValues["IsDeleted"] = false;
                entry.CurrentValues["IsActive"] = true;
                Unit.OnBeforeChildEntityProcessed(entry.Entity, enumEntityProcessState.Add);
                break;

            case EntityState.Deleted:
                entry.State = EntityState.Modified;
                entry.CurrentValues["ChangedDateTime"] = DateTime.Now;
                entry.CurrentValues["ChangedBy"] = baseModel.ChangedBy;
                entry.CurrentValues["IsDeleted"] = true;
                Unit.OnBeforeChildEntityProcessed(entry.Entity, enumEntityProcessState.Delete);
                break;

            case EntityState.Modified:
                if (entry.Entity != null && entry.Entity.GetType() != typeof(Company))
                    entry.CurrentValues["CompanyId"] = baseModel.CompanyId;

                entry.CurrentValues["ChangedDateTime"] = DateTime.Now;
                entry.CurrentValues["ChangedBy"] = baseModel.ChangedBy;

                Unit.OnBeforeChildEntityProcessed(entry.Entity, enumEntityProcessState.Update);
                break;
        }
    }
}

}

DiscoveryClass:

    public abstract class UnitOfWorkDiscoverySet
{
    private Dictionary<Type, object> Repositories { get; set; }
    private CompanyMasterDataContext _context;

    public UnitOfWorkDiscoverySet(CompanyMasterDataContext context)
    {
        _context = context;
        InitializeSets();
    }

    private void InitializeSets()
    {
        var discoverySetType = GetType();
        var discoverySetProperties = discoverySetType.GetProperties();

        Repositories = new Dictionary<Type, object>();

        foreach (var child in discoverySetProperties)
        {
            var childType = child.PropertyType;
            var repositoryType = childType.GetInterfaces()
                .Where( i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IRepository<>))
                .FirstOrDefault();

            if (repositoryType != null)
            {
                var repositoryModel = repositoryType.GenericTypeArguments.FirstOrDefault();

                if (repositoryModel != null)
                {
                    if (repositoryModel.IsSubclassOf(typeof(BaseModel)))
                    {
                        var repository = InitializeProperty(child); //var repository = child.GetValue(this);

                        if (repository != null)
                        {
                            Repositories.Add(repositoryModel, repository);
                        }
                    }
                }
            }
        }
    }

    private object InitializeProperty(PropertyInfo property)
    {
        if(property != null)
        {
            var instance = Activator.CreateInstance(property.PropertyType, new object[] {
                _context, this
            });

            if(instance != null)
            {
                property.SetValue(this, instance);
                return instance;
            }
        }

        return null;
    }

    public void OnBeforeChildEntityProcessed(object childObject, enumEntityProcessState processState)
    {
        if(childObject != null)
        {
            var repository = GetRepositoryByObject(childObject);
            var parameters = new object[] { childObject, processState };

            InvokeRepositoryMethod(repository, "OnBeforeEntityProcessed", parameters);
        }
    }

    public void ValidateChildren<Entity>(Entity entity, List<string> errorMessages) where Entity : BaseModel
    {
        var children = BaseModelUpdater.GetChildModels(entity);

        if(children != null)
        {
            foreach(var child in children)
            {
                if(child != null)
                {
                    if (child.GetType() == typeof(IEnumerable<>))
                    {
                        var list = (IEnumerable<object>) child;

                        if(list != null)
                        {
                            foreach (var childInList in list)
                            {
                                ValidateChild(childInList, errorMessages);
                            }
                        }
                    }

                    ValidateChild(child, errorMessages);
                }
            }
        }
    }

    public void ValidateChild(object childObject, List<string> errorMessages)
    {
        if(childObject != null)
        {
            var repository = GetRepositoryByObject(childObject);
            var parameters = new object[] { childObject, errorMessages };

            InvokeRepositoryMethod(repository, "Validate", parameters);
        }
    }

    public void InvokeRepositoryMethod(object repository, string methodName, object[] parameters)
    {
        if (repository != null)
        {
            var methodToInvoke = repository.GetType().GetMethod(methodName);
            var methods = repository.GetType().GetMethods().Where(x => x.Name == methodName);

            if (methodToInvoke != null)
            {
                methodToInvoke.Invoke(repository, parameters);
            }
        }
    }

    public object GetRepositoryByObject(object objectForRepository)
    {
        return Repositories?[objectForRepository.GetType()];
    }

    public object GetObject<Entity>(Type type, Entity entity) where Entity : BaseModel
    {
        var childObjects = BaseModelUpdater.GetChildModels(entity);

        foreach (var childObject in childObjects)
        {
            if (childObject.GetType().FullName == type.FullName)
            {
                return childObject;
            }
        }

        return null;
    }
}

}

Проблема: Я хочу проверить данные в каждой модели и свойствах / списке дочерних моделей, знаю, что вы можете сказать, что это может бытьсделано с использованием атрибутов, но проверка может быть довольно сложной, и я предпочитаю отделить это в своем собственном пространстве.

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

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

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

Это решение также сильно зависит от метода UpdateEntitiesByBaseModel внутри DbContext.Так что это только изменяет поля, которые должны быть изменены.

Не уверен, что я решил эту проблему так же хорошо, как думал, но я ценю каждый вклад, который приведет меня на правильный путь.Спасибо!

Решение (Изменить): В итоге я использовал только свойства навигации для операций GET и исключил его для операций вставки.Все стало более гибким и быстрым, так что мне не нужно было использовать EF Tracker, который сделал операцию вставки 5000 объектов с 13-минутной операции до 14,3 секунды.

1 Ответ

1 голос
/ 09 мая 2019

Этот вопрос, вероятно, лучше задать в CodeReview, а не в SO, который ориентирован на конкретные вопросы, связанные с кодом. Вы можете задать 10 разных разработчиков и получить 10 разных ответов. :)

Отражение определенно имеет свою стоимость, и я совсем не люблю его использовать.

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

Это довольно распространенная тема, которую я вижу в приложениях и средах, с которыми команды разработчиков, с которыми я работаю, пытаются справиться при работе с ORM. Для меня абстрагирование EF от решения - это все равно что пытаться абстрагироваться от частей .Net. Буквально нет смысла, потому что вы лишены доступа к большей части гибкости и возможностей, которые предлагает Entity Framework. Это приводит к более сложному коду для решения задач, которые EF может делать изначально, оставляя место для ошибок при повторном изобретении колеса или оставляя пробелы, которые впоследствии придется обходить. Вы либо доверяете этому, либо не должны его использовать.

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

Это на самом деле шаблон, за который я пришел отстаивать проекты. Многие люди против шаблона Repository, но это отличный шаблон, чтобы служить границей для домена в целях тестирования. (Нет необходимости устанавливать базы данных в памяти или пытаться смоделировать DbContext / DbSets). Однако IMO шаблон Generic Repository является анти-шаблоном. Он разделяет проблемы сущностей друг от друга, однако во многих случаях мы имеем дело с «графами» сущностей, а не с отдельными типами сущностей. Вместо того, чтобы определять репозитории для каждой сущности, я выбираю что-то, что фактически является репозиторием для каждого контроллера. (С репозиториями для действительно общих сущностей, таких как поиск, например.) Это объясняется двумя причинами:

  • Меньше ссылок на зависимости для передачи / mock
  • Лучше обслуживает SRP
  • Избегать операций с базой данных голубя

Самая большая проблема, с которой я сталкиваюсь с общими репозиториями или репозиториями для каждой сущности, заключается в том, что, хотя они, кажется, соответствуют SRP (ответственному за операции для одной сущности), я чувствую, что они нарушают его, потому что SRP имеет только одну причину для изменения. Если у меня есть сущность Order и репозиторий Order, у меня может быть несколько областей приложения, которые загружают и взаимодействуют с заказами. Методы взаимодействия с сущностями Порядка теперь вызываются в нескольких разных местах, что создает много потенциальных причин для корректировки методов. В итоге вы получаете сложный условный код или несколько очень похожих методов для обслуживания конкретных сценариев. (Заказы для перечисления заказов, заказов по клиентам, заказов по магазину и т. Д.) Когда дело касается проверки сущностей, это обычно делается в контексте всего графика, поэтому имеет смысл централизовать это в коде, связанном с графиком, а не с отдельным юридические лица. Это относится к базовым базовым операциям, таким как Add / Update / Delete. В 80% случаев это работает и экономит усилия, но эти оставшиеся 20% либо приходится вкладывать в шаблон, что приводит к неэффективному и / или подверженному ошибкам коду, либо обходным решениям. ПОЦЕЛУЙ. всегда должен превосходить D.N.R.Y когда дело доходит до разработки программного обеспечения. Консолидация в базовые классы и тому подобное - это оптимизация, которая должна выполняться по мере развития кода, когда идентифицируется «идентичная» функциональность. Когда это делается заранее как архитектурное решение, я рассматриваю эту преждевременную оптимизацию, которая создает препятствия для разработки, проблемы с производительностью и ошибки, когда «похожее», но не «идентичное» поведение группируется вместе, что приводит к ползучему условному коду для крайних случаев.

Таким образом, вместо OrderRepository для обслуживания заказов, если у меня есть что-то вроде ManageOrderController, у меня будет ManageOrderRepository для его обслуживания.

Например, мне нравится использовать методы в стиле DDD для управления объектами, где мои репозитории играют роль в построении, так как они относятся к области данных и могут проверять / извлекать связанные объекты.Таким образом, типичный репозиторий будет иметь:

IQueryable<TEntity> GetTEntities()
IQueryable<TEntity> GetTEntityById(id)
IQueryable<TRelatedEntity> GetTRelatedEntities()
TEntity CreateTEntity({all required properties/references})
void DeleteTEntity(entity)
TChildEntity CreateTChildEntity(TEntity, {all required properties/references})

Методы извлечения, включая «По идентификатору», поскольку это распространенный сценарий, возвращают IQueryable, чтобы вызывающие абоненты могли контролировать, как используются данные.Это исключает необходимость пытаться абстрагировать возможности Linq, которые EF может использовать, чтобы вызывающие абоненты могли применять фильтры, выполнять разбиение на страницы, сортировку, а затем использовать данные так, как им нужно.(Select, Any и т. Д.) В хранилище применяются основные правила, такие как IsActive и проверки аренды / авторизации.Это служит границей для тестов, так как mocks просто должен возвращать List<TEntity>.AsQueryable() или обернутый с асинхронным типом коллекции.( Модульное тестирование .ToListAsync () с использованием оперативной памяти ) Хранилище также служит отправной точкой для поиска любых связанных объектов по любым применимым критериям.Это можно рассматривать как потенциальное дублирование, но изменения в этом хранилище понадобятся только тогда, когда необходимо изменить контроллер / представление / область приложения.Обычные вещи, такие как поиски, будут извлекаться из собственного репозитория.Это сокращает необходимость в множестве отдельных зависимостей репозитория.Каждая область заботится о себе, поэтому изменения / оптимизации здесь не должны учитывать или влиять на другие области приложения.

Методы «Создать» управляют правилами вокруг создания и привязки объектов к контексту, чтобы гарантировать, что объектывсегда создаются в минимально полном и действительном состоянии.Это где проверка вступает в игру.Любое значение, которое не имеет нулевого значения, передается вместе с FK (ключами или ссылками), необходимыми для обеспечения того, чтобы, если SaveChanges() был следующим вызовом после Create, сущность была бы действительной.

«Удалить»здесь также используются методы для управления проверкой состояния данных / авторизации и применения согласованного поведения.(жесткое или мягкое удаление, аудит и т. д.)

Я не использую методы «Обновить».Обновления обрабатываются методами DDD на самом объекте.Контроллеры определяют единицу работы, используют репозиторий для извлечения сущности, вызывают методы сущности, а затем фиксируют единицу работы.Валидация может быть выполнена на уровне сущности или с помощью класса Validator.

В любом случае, это всего лишь краткое изложение одного подхода из 10+, которое вы можете получить, и, надеюсь, выделит некоторые моменты, которые следует учитыватьс любым подходом вы берете.Когда я работаю с EF, я делаю акцент на:

  1. Сохраняйте это простым.(KISS> DNRY)
  2. Используйте то, что EF может предложить, вместо того, чтобы пытаться скрыть это.

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

...