Resolved EF VS in memory list and the initialization of navigation properties

Matthieu

Member
Joined
Aug 15, 2020
Messages
19
Programming Experience
Beginner
I have a question regarding navigation properties in EF and it's initialization. I created a small case to clarify the issue I'm dealing with. Sorry for the extreme long post but I like to be clear :).

Under my InMemoryRepository class I created 2 lists, being ‘Tickets’ and ‘TicketResponses’. The static method ‘Seed’ fills these 2 lists. You will notice that I didn’t initialize the ‘Responses’ collection of the object ‘t1’. Further you can find a find method ‘ReadTicket’ that returns a ticket based on it’s ID and a method ‘ReadTicketRepsonse’ that returns a ticket response also based on it’s ID.

InMemoryRepository:
public class InMemoryRepository : IRepository
    {
        private static readonly List<Ticket> Tickets = new List<Ticket>();
        private static readonly List<TicketResponse> TicketResponses = new List<TicketResponse>();

        static InMemoryRepository()
        {
            Seed();
        }
        
        public Ticket ReadTicket(int tickedId)
        {
            return Tickets.Find(t => t.TicketId == tickedId);
        }

        public TicketResponse ReadTicketResponse(int ticketResponseId)
        {
            return TicketResponses.Find(tr => tr.TicketResponseId == ticketResponseId);
        }

        private static void Seed()
        {
            Ticket t1 = new Ticket()
            {
                TicketId = 1,
                Name = "Ticket 1"
            };
            Tickets.Add(t1);

            TicketResponse tr1 = new TicketResponse()
            {
                TicketResponseId = 1,
                Name = "Ticket response 1",
                Ticket = t1
            };
            TicketResponses.Add(tr1);
        }
    }

Under my TicketManager class, I have an instance of my InMemoryRepository class and I also defined two methods, being ‘GetTicket’ and ‘GetTicketResponse’. Both methods are using the instance of the InMemoryRepository to return a ticket or ticket response based on an ID.

TicketManager:
 public class TicketManager : ITicketManager
    {
        private readonly IRepository _repository = new InMemoryRepository();
        
        public Ticket GetTicket(int ticketId)
        {
            return _repository.ReadTicket(ticketId);
        }
        
        public TicketResponse GetTicketResponse(int ticketResponseId)
        {
            return _repository.ReadTicketResponse(ticketResponseId);
        }
    }

Under my Program class, I created an instance of my TicketManager class. I’m using this instance to get a ticket with ID = 1. From this ticket I’m asking the name and I also do a count on the ‘Responses’ collection from this ticket. The latter will give an System.NullReferenceException because I didn’t initialize the ‘Responses’ collection during the creation of this ticket under my ‘Seed’ method.

Program:
class Program
    {
        private static readonly ITicketManager TicketManager = new TicketManager();
        
        static void Main(string[] args)
        {
            Ticket ticket = TicketManager.GetTicket(1);

            Console.WriteLine(ticket.Name);
            Console.WriteLine(ticket.Responses.Count);
        }
    }

Now I will do the same but instead of using in memory lists, I will make use of Entity Framework.

I created a DbContext (EfTestPartFourDbContext) where I have 2 DbSets (Tickets and TicketResponses). Further I have a EfTestPartFoutDbInitializer class that contains a ‘Seed’ method and the EfTestPartFourDbContext class uses to fill it’s DbSets (just for testing).

EfTestPartFourDbContext:
internal class EfTestPartFourDbContext : DbContext
    {
        public DbSet<Ticket> Tickets { get; set; }
        public DbSet<TicketResponse> TicketResponses { get; set; }

        public EfTestPartFourDbContext()
        {
            EfTestPartFourDbInitializer.Initialize(this, true);
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder
                .UseSqlite("Data Source = EfTestPartFour.db")
                .UseLoggerFactory(LoggerFactory.Create(b => b.AddDebug()));
        }
    }

EfTestPartFourDbInitializer:
internal static class EfTestPartFourDbInitializer
    {
        public static void Initialize(EfTestPartFourDbContext context, bool dropCreateDatabase)
        {
            if (dropCreateDatabase)
            {
                context.Database.EnsureDeleted();
            }

            if (context.Database.EnsureCreated())
            {
                Seed(context);
            }
        }

        private static void Seed(EfTestPartFourDbContext context)
        {
            Ticket t1 = new Ticket()
            {
                TicketId = 1,
                Name = "Ticket 1"
            };
            context.Tickets.Add(t1);

            TicketResponse tr1 = new TicketResponse()
            {
                TicketResponseId = 1,
                Name = "Ticket response 1",
                Ticket = t1
            };
            context.TicketResponses.Add(tr1);
            
            context.SaveChanges();

            foreach (var entry in context.ChangeTracker.Entries())
            {
                entry.State = EntityState.Detached;
            }
        }
    }

I created a Repository class that’s completely the same as the InMemoryRepository with the exception it has no longer the 2 lists (Tickets and TicketResponses) but a EfTestPartFourDbContext object to use within the ‘ReadTicket’ and ‘ReadTicketResponse’ methods. Also there is no longer a ‘Seed’ method because the seeding is now done by the EfTestPartFourDbInitializer class.

Repository:
public class Repository : IRepository
    {
        private readonly EfTestPartFourDbContext _context = new EfTestPartFourDbContext();
        
        public Ticket ReadTicket(int tickedId)
        {
           return _context.Tickets.Include(t=>t.Responses).FirstOrDefault(t=>t.TicketId == tickedId);
        }
        
        public IEnumerable<TicketResponse> ReadResponsesOfTicket(int tickedId)
        {
            return _context.TicketResponses.Where(tr => tr.Ticket.TicketId == tickedId);
        }

        public TicketResponse ReadTicketResponse(int ticketResponseId)
        {
            return _context.TicketResponses.Find(ticketResponseId);
        }

        public IEnumerable<TicketResponse> ReadTicketResponses()
        {
            return _context.TicketResponses;
        }
    }

The TicketManager class does now no longer uses an instance of the InMemoryRepository, but is using an instance of the Repository class.

TicketManager:
public class TicketManager : ITicketManager
    {
        private readonly IRepository _repository = new Repository();
        
        public Ticket GetTicket(int ticketId)
        {
            return _repository.ReadTicket(ticketId);
        }
        
        public TicketResponse GetTicketResponse(int ticketResponseId)
        {
            return _repository.ReadTicketResponse(ticketResponseId);
        }
    }

Also My Program class is unchanged. When u run it, I no longer get the System.NullReferenceException when I do a count on the ‘Responses’ collection from the ticket.

So now my question: Why does this not throwing the NullReferenceExcption? I just as well didn’t initialize the ‘Responses’ collection of the ticket during seeding. Is it that EF somehow initializes it's navigation properties?

Thank yu very much!
 
Solution
If it's not throwing an exception, what is the count that is returned? Is it zero or one?

If it's one, I suspect that you told EF about a relationship between the Ticket and TicketResponse and so EF is populating the Response collection like a good ORM would do.

It would help us if you showed us how you declared your Ticket and TicketResponse classes and if you told EF about relationships (aka foreign keys) between the two classes.As

You put me on the right track and after looking it in some more detail, it's indeed one of the tasks of the ORM (in this case EF) to initialize the navigation properties when they are not already initialized.
I also learned that even when you don't...

Skydiver

Staff member
Joined
Apr 6, 2019
Messages
2,135
Location
Chesapeake, VA
Programming Experience
10+
If it's not throwing an exception, what is the count that is returned? Is it zero or one?

If it's one, I suspect that you told EF about a relationship between the Ticket and TicketResponse and so EF is populating the Response collection like a good ORM would do.

It would help us if you showed us how you declared your Ticket and TicketResponse classes and if you told EF about relationships (aka foreign keys) between the two classes.
 

Matthieu

Member
Joined
Aug 15, 2020
Messages
19
Programming Experience
Beginner
If it's not throwing an exception, what is the count that is returned? Is it zero or one?

If it's one, I suspect that you told EF about a relationship between the Ticket and TicketResponse and so EF is populating the Response collection like a good ORM would do.

It would help us if you showed us how you declared your Ticket and TicketResponse classes and if you told EF about relationships (aka foreign keys) between the two classes.As

You put me on the right track and after looking it in some more detail, it's indeed one of the tasks of the ORM (in this case EF) to initialize the navigation properties when they are not already initialized.
I also learned that even when you don't make a DbSet for one of your domain models, but it's used as a navigation property in one of the domain models, EF detect this entity and creates a table for this entity.
So in comparison with let's call it 'imaginary tables' you create as lists in the in memory repository, EF is capable to detect relations and initialize these for me, but in the in memory repository it's my job to initialize the relations, ...
That explains why when using the in memory repository I'm getting the null reference exception and when using the EF repository I'm not getting the null reference exception.

Thanks for the help.
 
Solution
Top Bottom