2

I'm loading an IQueryable of books from the Books table and there's another table named BookLoans. I have a DateTime? property in my ViewModel named Availability. I'm trying to figure out the best way to basically go out during my select statement and see if the current book's BookID shows up in a record within the BookLoans table and if a DateTime? variable named ReturnedWhen is also NULL in that record. I'm basically trying to mark the book as available or checked outed when I populate the the IQueryable.

It's been suggested that I try to do this all as one LINQ statement but I'm trying to figure out if it would be easier to just populate books like the following and then run it through a foreach loop? It's also been suggested that a Join would work but really I'm just trying to figure out the best method and then how to go about actually doing it.

 var books =
 _context.Books
 .Select(r => new BookIndexViewModel
 {
     BookID = r.BookID,
     Title = r.Title,
     Author = r.Author,
     ISBN = r.ISBN,
     GenreName = r.Genre.Name
 }).ToList();

Book Entity:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Open_School_Library.Data.Entities
{
    public class Book
    {
        public int BookID { get; set; }
        public string Title { get; set; }
        public string SubTitle { get; set; }
        public string Author { get; set; }
        public int GenreID { get; set; }
        public int DeweyID { get; set; }
        public int ISBN { get; set; }

        public Genre Genre { get; set; }
        public Dewey Dewey { get; set; }
        public virtual BookLoan BookLoan { get; set; }
    }
}

BookLoan Entity:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Open_School_Library.Data.Entities
{
    public class BookLoan
    {
        public int BookLoanID { get; set; }
        public int BookID { get; set; }
        public int StudentID { get; set; }
        public DateTime CheckedOutWhen { get; set; }
        public DateTime DueWhen { get; set; }
        public DateTime? ReturnedWhen { get; set; }

        public Book Book { get; set; }
        public Student Student { get; set; }
    }
}

BookIndexViewModel:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;

namespace Open_School_Library.Models.BookViewModels
{
    public class BookIndexViewModel
    {
        public int BookID { get; set; }
        public string Title { get; set; }
        [Display(Name = "Author")]
        public string Author { get; set; }
        [Display(Name = "Genre")]
        public string GenreName { get; set; }
        public int ISBN { get; set; }
        public string BookLoan { get; set; }
        public DateTime? Availability { get; set; }
    }
}
Christopher Johnson
  • 635
  • 2
  • 7
  • 21

1 Answers1

1

Here are some simplified versions of your classes

public class Book {
    public int BookId { get; set; }
    public string Title { get; set; }
}

public class BookLoan {
    public int BookId { get; set; }
    public DateTime CheckedOutOn { get; set; }
    public DateTime? ReturnedOn { get; set; }
    public DateTime DueOn { get; set; }
}

public class BookViewModel {
    public int BookId { get; set; }
    public string Title { get; set; }
    public bool IsAvailable { get; set; }
    public DateTime? AvailableOn { get; set; }
} 

Then we can do two things to get the view model list you're looking for:

  1. Look at only book loans where ReturnedOn is null
  2. Use DefaultIfEmpty to do a left outer join, see https://msdn.microsoft.com/en-us/library/bb397895.aspx

So you end up with one item per book and only a loanWithDefault if the book is currently checked out.

var viewModel = from book in books
            join loan in loans.Where(x => !x.ReturnedOn.HasValue) on book.BookId equals loan.BookId into result
            from loanWithDefault in result.DefaultIfEmpty()
            select new BookViewModel { 
                BookId = book.BookId,
                Title = book.Title,
                IsAvailable = loanWithDefault == null,
                AvailableOn = loanWithDefault == null ? (DateTime?) null : loanWithDefault.DueOn
            };

Here is a working example: https://dotnetfiddle.net/NZc2Xd

Here is a breakdown of the LINQ query:

  1. from book in books is the first table you are selecting from
  2. loans.Where(x => !x.ReturnedOn.HasValue) limits the loans to a maximum of one item per book (and the loans we care about in this case)
  3. join loan in loans on book.BookId equals loan.BookId into result is joining on the loans table so that we get that as part of our result set. The join condition is the book's BookId equals the loan's BookId
  4. from loanWithDefault in result.DefaultIfEmpty() basically makes the join on loans a "left outer join". This means that even if there are no loans that have yet to be returned for this book you will get the book in your result set. Otherwise, you would only get unavailable books. See this Stack Overflow answer for a good explanation of different joins. Also, see https://msdn.microsoft.com/en-us/library/bb397895.aspx for how to do left outer joins with LINQ.
  5. Finally, you select your view model from the available Book and BookLoan object.

Here is what a similar raw SQL query would look like

select
  Book.BookId,
  Book.BookTitle,
  BookLoan.DueOn
from
  Book
  left outer join BookLoan on
    Book.BookId = BookLoan.BookId
    and BookLoan.ReturnedOn is null
Community
  • 1
  • 1
Robert Harris
  • 482
  • 2
  • 6
  • I got it up and working but I won't lie I don't fully understand what's happening in the LINQ statement and I was wondering if you could break it down for me? – Christopher Johnson Oct 05 '16 at 01:05
  • 1
    @ChristopherJohnson See the breakdown I added to the answer and let me know if that helps at all. LINQ queries can get very complicated and difficult to read. – Robert Harris Oct 05 '16 at 01:29
  • it works wonderfully. I was wondering what I'd have to do to make this work for a single query? I was hoping to reuse or modify this on the Details View which I had been using a `.FirstOrDefault()` on as well as filtering with a `Where whatever.BookId == id`. – Christopher Johnson Oct 06 '16 at 02:29
  • 1
    You just need to use `.Where()` or where in query syntax. You'll still use `.FirstOrDefault()`. Here is a simple example https://dotnetfiddle.net/zYwPRU You could use just `.FirstOrDefault(x => x.BookId == 2)` also. – Robert Harris Oct 06 '16 at 13:58
  • I tried the following but whenever I tried to access Student info from `loanWithDeafult` it would return `null` on books that weren't checked out. https://gist.github.com/Programazing/a9712aa11001658bfdbc123b94666e8b – Christopher Johnson Oct 09 '16 at 02:49
  • loanWithDefault is expected to be null if the book is not checked out currently. You need to check that loanWithDefault is not null before accessing StudentID like is done for AvailableOn. Are you wanting to get the student who previously checked the book out even if it has been returned? – Robert Harris Oct 09 '16 at 14:32
  • Yes I'm just trying to get the information of the student who has the checked out book. I had tried something like this but it still returns null `StudentID = loanWithDefault.StudentID == id ? loanWithDefault.StudentID : 0` – Christopher Johnson Oct 09 '16 at 17:33
  • 1
    I think you want loanWithDefault == null ? null : loanWithDefault.StudentID – Robert Harris Oct 09 '16 at 19:19
  • Thanks, I actually tried that earlier but I didn't even think to make sure that StudentID was nullable in my Entity or my ViewModel. – Christopher Johnson Oct 09 '16 at 19:51