Ссылки на загрузку EF Core неизвестного объекта

ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ: Так как мы все знакомы с этим, я буду использовать дизайн университета Contoso, чтобы объяснить мой вопрос. Кроме того, я использую ядро ​​EF и ядро ​​.net 2.0 в первом проекте mvc-кода.

университет Контозо

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

[Route("/api/{resource}")]

Ресурс - это объект, с которым клиент хочет работать, например, если кто-то хочет получить все курсы, используя API, которые он должен сделать, запрос GET на http://www.example.com/api/course/ или http://www.example.com/api/course/2 чтобы получить один по идентификатору, и следующий код сделает работу.

[HttpGet("{id:int:min(1)?}")]
public IActionResult Read([FromRoute] string resource, [FromRoute] int? id)
{
    //find resourse in models 
    IEntityType entityType = _context.Model
        .GetEntityTypes()
        .FirstOrDefault(x => x.Name.EndsWith($".{resource}", StringComparison.OrdinalIgnoreCase));
    if (entityType == null) return NotFound(resource);

    Type type = entityType.ClrType; 

    if (id == null)//select all from table
    {
        var entityRows = context.GetType().GetMethod("Set").MakeGenericMethod(type).Invoke(context, null);

        if (entityRows == null)
            return NoContent();

        //TODO: load references (1)

        return Ok(entityRows);
    }
    else //select by id
    {
        var entityRow = _context.Find(type, id);
        if (entityRow == null)
            return NoContent();

        //TODO: load references (2)

        return Ok(entityRows);
    }
}

Этот маленький кусочек кода сделает волшебство за одним небольшим исключением, промежуточные коллекции не будут загружены. В нашем примере извлеченный курс или курсы не будут иметь информации для CourseInstructor (промежуточный набор между Course и Person). Я пытаюсь найти способ загружать свойства навигации, только если это коллекция; или любым другим условием, которое гарантирует, что загружаются только отношения "многие ко многим".

Для //TODO: загрузить ссылку (2) я мог бы использовать

_context.Entry(entityRow).Collection("CourseInsructor").Load();

Во время выполнения, если бы я мог найти все свойства навигации (отфильтрованные по речевым условиям) и для каждого из них я сделал Load(), я должен получить желаемый результат. Моя проблема, когда я получаю все (когда идентификатор равен нулю) entityRows имеет тип "InternalDbSet", который является неизвестной моделью.

Так что для двух TODO мне нужна помощь в выполнении следующих шагов

1: найти свойства навигации только для отношений многие ко многим 2: загрузить их

Какие-либо предложения?

1 ответ

В общем, это кажется мне очень плохой идеей. В то время как материал CRUD будет идентичен для большинства ресурсов, будут различия (как вы сейчас столкнулись). Есть также кое-что, что можно сказать о наличии самодокументируемого API: с отдельными контроллерами вы знаете, к каким ресурсам можно получить доступ по природе наличия контроллера, связанного с этим ресурсом. С тем, как вы это делаете, это полный черный ящик. Это также, конечно, повлияет на любой вид фактически сгенерированной документации API. Например, если вы включите Swagger в свой проект, он не сможет определить, что вы здесь делаете. Наконец, теперь вы должны использовать отражение для всего, что повлияет на вашу производительность.

Вместо этого я бы предложил создать базовый абстрактный контроллер, а затем создать контроллер для каждого уникального ресурса, который наследуется от него, например:

public abstract class BaseController<TEntity> : Controller
    where TEntity : class, new()
{
    protected readonly MyContext _context;

    public BaseController(MyContext context)
    {
        _context = context ?? throw new ArgumentNullException(nameof(context));
    }

    ...

    [HttpGet("create")]
    public IActionResult Create()
    {
       var model = new TEntity();
       return View(model);
    }

    [HttpPost("create")]
    public async Task<IActionResult> Create(TEntity model)
    {
        if (ModelState.IsValid)
        {
            _context.Add(model);
            await _context.SaveChangesAsync();
            return RedirectToAction("Index");
        }

        return View(model);
    }

    ...
}

Я просто хотел привести небольшой пример, но вы бы построили все остальные методы CRUD таким же образом, используя в общем TEntity, Затем для каждого фактического ресурса вы просто делаете:

public class WidgetController : BaseController<Widget>
{
    public WidgetController(MyContext context)
        : base(context)
    {
    }
}

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

Затем, чтобы решить такие проблемы, как у вас здесь, вы можете добавить хуки к вашему базовому контроллеру: по существу, это просто виртуальные методы, которые используются в действиях CRUD вашего базового контроллера и ничего не делают или просто делают вещи по умолчанию. Однако затем вы можете переопределить их в своих производных контроллерах для заглушки в дополнительных функциях. Например, вы можете добавить что-то вроде:

  public virtual IQueryable<TEntity> GetQueryable()
      => _context.Set<TEntity>();

Затем в вашем производном контроллере вы можете сделать что-то вроде:

public class CourseController : BaseController<Course>
{
    ...

    public override IQueryable<Course> GetQueryable()
        => base.GetQueryable().Include(x => x.CourseInstructors).ThenInclude(x => x.Instructor);

Так, например, вы бы сделали свой BaseController.Index действие, возможно, использовать GetQueryable() чтобы получить список объектов для отображения. Просто переопределив это в производном классе, вы можете изменить то, что происходит, в зависимости от контекста определенного типа ресурса.