Ссылки на загрузку 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()
чтобы получить список объектов для отображения. Просто переопределив это в производном классе, вы можете изменить то, что происходит, в зависимости от контекста определенного типа ресурса.