Ядро EF Обрабатывает Сущности Одинаково, Но Одна Из Них Равна Нулю, а Другая-Нет

#c# #asp.net #entity-framework #razor

Вопрос:

Хорошо, вы все. Я смотрю на это уже несколько часов, и мне кажется, что я схожу с ума. Я знаю, что это немного, и мне жаль, что это так долго, я пытался сделать это как можно проще, но … … позвольте мне просто поставить tl;dr впереди.

tl;dr: Обработка двух объектов («сущностей» в ядре EF) одинакова, но по какой-то причине они обрабатываются по-разному и дают мне очень досадную ошибку. Что это дает?

Хорошо, сначала я использую Entity Framework 6.4.4, EntityFrameworkCore.Sqlite и EntityFrameworkCore.Инструменты 3.1.4, Visual Studio 16.10.0 и .NET Core 3.1, но по причинам, которые вы увидите позже, я действительно не думаю, что это уместно здесь. Я просто включаю это, чтобы убрать это с дороги.

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

MyNamespace/Модели/Project.cs

 namespace MyNamespace.Models {  public class Project  {  public int ID { get; set; }  // Bunch of other stuff, doesn't matter   public ICollectionlt;Commentgt; Comments { get; set; }  public ICollectionlt;MyNamespace.Models.Filegt; Files { get; set; }  // Yes I'm intentionally using the full path here because this is where the confusion is  } }  

MyNamespace/Модели/Comment.cs

 namespace MyNamespace.Models {  public class Comment  {  public int ID { get; set; }  public int ProjectID { get; set; }  // Other attributes   public Project Project { get; set; }  } }  

MyNamespace/Модели/Файл.cs

 namespace MyNamespace.Models {  public class File  {  public int ID { get; set; }  public int ProjectID { get; set; }  // Other attributes   public MyNamespace.Models.Project Project { get; set; }  // Again being very explicit here because I'm at a loss  } }  

You see what I got here, right? A Project entity can contain zero or many comments and zero or many files, and each comment or file can be associated with one project. It’s a one-to-many relationship we’re looking at. Not super complicated, I have other entities with similar relationships, everything works like I expect it to, except the File entity. I’m including the Comment entity here to illustrate that, so far as I can tell, I’m treating the File entity properly, same as the Comment entity, but I don’t know why it’s not working. Just so you know, though, there are other entities that I’m also treating the same and are also working fine.

So those are the models in question. Here’s my DbContext

MyNamespace/Data/ProjectDBContext.cs

 namespace MyNamespace.Data {  public class ProjectDBContext : DbContext  {  public DbSetlt;Projectgt; Projects { get; set; }  // Other stuff   public DbSetlt;Commentgt; Comments { get; set; }  public DbSetlt;MyNamespace.Models.Filegt; Files { get; set; }  } }  

Again, I have plenty of stuff in there, everything was working as expected until I added this new File model.

So, now that we’ve set everything up, let’s get into the code.

My organization is a little weird ,I know, (but I think it’s kind of proper?), but I’m going to include all of it that I believe is relevant. I’m using View Components and Razor Pages, so, first, we have a regular Razor page (I’ll include the MyNamespace/Pages/Projects/View.cshtml.cs if you all really want it, but I really don’t think the problem is here, so I’m going to skip it for now.)

MyNamepsace/Pages/Projects/View.cshtml

 @model MyNamespace.Pages.Projects.View  lt;!-- Some stuff --gt;  lt;divgt;  @await Component.InvokeAsync("CommentsBox", Model.Project) lt;/divgt;  lt;divgt;  @await Component.InvokeAsync("FilesBox", Model.Project) lt;/divgt;  lt;!-- Other stuff --gt;  

Now, I want to be clear, the Files stuff doesn’t work. The Comments stuff does. I’m including both to illustrate that it works with one and not the other, and I don’t know why. Sorry, so, the first part of our view components.

MyNamespace/ViewComponents/CommentsBox.cs

 namespace MyNamespace.ViewComponents {  public class CommentsBox : ViewComponent  {  public IViewComponentResult Invoke(Project project)  {  return View("Default", project)  }  } }  

MyNamespace/ViewComponents/FilesBox.cs

 namespace MyNamespace.ViewComponents {  public class FilesBox : ViewComponent  {  public IViewComponentResult Invoke(Project project)  {  // We'll come back to this later, but I have a breakpoint set at this line.  return View("Default", project)  }  } }  

Now, the views (I actually don’t know if I’m using the correct parlance when I call these «the views» but that’s not the point here)

MyNamespace/Pages/Shared/CommentsBox/Default.cshtml

 @model MyNamespace.Models.Project  lt;!-- Some stuff --gt;  @foreach (var comment in Model.Comments) {  // Just display info, this all works fine }  lt;!-- Some stuff --gt;  

MyNamespace/Pages/Shared/FilesBox/Default.cshtml

 @model MyNamespace.Models.Project  lt;!-- Some stuff --gt;  @foreach (var file in Model.Files) // ERROR {  // Supposed to display the info }  lt;!-- Some stuff --gt;  lt;!-- Do note, there is some code here to upload a file. If I remove the code above that's throwing the error, It actually does upload the file. --gt;  

So the error I’m getting is complaining about the for loop in MyNamespace/Pages/Shared/FilesBox/Default.cshtml , it’s an ArgumentNullException . It’s complaining that Model.Files is null, so I can’t iterate over it. But this doesn’t make sense, (well you’ll see it does a little bit, but I don’t know why it’s null ). Because, for a project where I have no comments and no files, the loop in MyNamespace/Pages/Shared/CommentsBox/Default.cshtml works just fine. Why does it work for Comments and not Files ?

Further, if I remove the for loop from MyNamespace/Pages/Shared/FilesBox/Default.cshtml and use the code that uploads a file to upload a file, it works just fine. No problem at all. Now, we know we have a file in the database associated with this project. So, just for funsies, let’s change MyNamespace/Pages/Shared/FilesBox/Default.cshtml

MyNamespace/Pages/Shared/FilesBox/Default.cshtml

 @model MyNamespace.Models.Project  lt;!-- Some stuff --gt;  @try {  @foreach (var file in Model.Files)  {  // We should display info here  } } catch (Exception ArgumentNullException) {  lt;textgt;Woops we've caught our exceptionlt;/textgt; }  lt;!-- Some stuff --gt;  lt;!-- Do note, there is some code here to upload a file. It actually does upload the file, if I remove the code above that's throwing the error --gt;  

Okay, so we know we have a file in the database, I can see it in the database (not on the webpage ) and I can see that its ProjectID is exactly what I expect it to be. Running it again, we catch our error condition. Why? We know we have a file in the database with a ProjectID pointing to our project. Remember, we don’t have this problem with comments. Even if there are no comments in the database with this ProjectID , we don’t get this error. Yet, even when we DO have a file in the database, we’re still getting the error and we can even confirm by asking for the error condition.

Okay, so, we know we have a file in the database pointing to our project, yet we’re still entering the error condition. I change MyNamespace/Pages/Shared/FilesBox/Default.cshtml back to the original, without the error checking. Database retains its information, no worries there. But we’re back to the same error. It’s just not associating the File entity with its proper Project entity.

Remember that breakpoint? This is where things get curious. If I inspect the Project object that’s being passed from MyNamepsace/Pages/Projects/View.cshtml to MyNamespace/ViewComponents/FilesBox.cs to MyNamespace/Pages/Shared/FilesBox/Default.cshtml at MyNamespace/ViewComponents/FilesBox.cs right before it goes to MyNamespace/Pages/Shared/FilesBox/Default.cshtml , I see something interesting. We can inspect the object during that moment in runtime and see that indeed, the Files attribute of our Project object is null , but for Comments ? It’s not null , it’s Count = 0 … so… why? I feel like I did the exact same thing for both of them. I set them up the same way, treated them the same in the DbContext , treated them the same in their models, and their association with Project . Why is one being treated differently?

Further, if we inspect the Project object at that breakpoint, we can see that for its Comments attribute, where, again, the value is Count = 0 , the type is System.Collections.Generic.ICollectionlt;MyNamespace.Models.Commentgt; {System.Collections.Generic.HashSetlt;MyNamespace.Models.Commentgt;} . For the Files attribute of our Project object, however, where the value is null (even though we know there’s files in the database with a ProjectID pointing to this project), the type is just System.Collection.Generic.ICollectionlt;MyNamespace.Models.Filegt; . I even tried changing the model MyNamespace/Models/Project.cs to

MyNamespace/Models/Project.cs

 namespace MyNamespace.Models {  public class Project  {  public int ID { get; set; }  // Bunch of other stuff, doesn't matter   public ICollectionlt;Commentgt; Comments { get; set; }  public HashSetlt;MyNamespace.Models.Filegt; Files { get; set; }  // See? I'm specifying the HashSet type here  } }  

but, to no avail. Same error, same behavior.

I’m sure whatever I’m missing is small, but I’ve been looking at this for, by the time I typed this whole thing up, more than a day now, and I’m just at a loss. Is it a Visual Studio thing? Am I missing something? I just don’t know. I’m hoping you fine folks might be either able to see what I don’t, or might be able to point me in the right direction.

Ответ №1:

Вау. Помните, как я сказал: «Я включу MyNamespace/Pages/Projects/View.cshtml.cs это, если вы все действительно этого хотите, но я действительно не думаю, что проблема здесь, поэтому я пока пропущу это»?

Ну, я должен был включить это. Горе тому дураку, который не выследит свою цель. Вот соответствующий код из MyNamespace/Pages/Projects/View.cshtml.cs

MyNamespace/Страницы/Проекты/Просмотр.cshtml.cs

 namespace MyNamespace.Pages.Projects {  public class View : PageModel  {  private readonly IReposity _reposity;   public Project Project { get; set; }   public View (IRepository repository)  {  _repository = repository;  }   public async Tasklt;IActionResultgt; OnGetAsync(int id)  {  // Some stuff   Project = _repository.GetOneProjectByIDAsNoTrackingAsync(id);   // Some other stuff  }  } }  

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

MyNamespace/Репозитории/Репозиторий.cs

 namespace MyNamespace.Repositories {  private ProjectDBContext _context;   public Repository(ProjectDBContext context)  {  _context = context;  }   public async Tasklt;Projectgt; GetOneProjectByIDAsNoTrackingAsync(int id)  {  return await _context.Projects  .Include(p =gt; p.Comments)  .Inlcude(p =gt; p.SomethingElse)  // Include a bunch of other stuff  .ThenInclude(s =gt; s.SomethingElseChild)  .AsNotracking()  .FirstOrDefaultAsync(m =gt; m.ID == id);  } }  

Видите ли, я никогда не включал File сюда свою сущность. Блин, да? Это должно быть

MyNamespace/Репозитории/Репозиторий.cs

 namespace MyNamespace.Repositories {  private ProjectDBContext _context;   public Repository(ProjectDBContext context)  {  _context = context;  }   public async Tasklt;Projectgt; GetOneProjectByIDAsNoTrackingAsync(int id)  {  return await _context.Projects  .Include(p =gt; p.Comments)  .Inlcude(p =gt; p.SomethingElse)  .Include(p =gt; p.Files) // lt;== THIS THIS THIS  // Include a bunch of other stuff  .ThenInclude(s =gt; s.SomethingElseChild)  .AsNotracking()  .FirstOrDefaultAsync(m =gt; m.ID == id);  } }  

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