Before I start my question, I will point out that this is for an assignment in a programming course I am doing, and I'm afraid the code needs to be in VB.
Scenario: We are writing an app to help manage a veterinary clinic. There is a legacy MySQL database which cannot be changed. Information relating to pets and their owners are stored in two separate tables ("pets" table and "owners" table, and the tables are linked by the FK of CustomerId. We are able to use our choice of data access technologies and ORMs, and I have chosen to use EF to take advantage of the Change Tracking (I'd prefer to not have to write this code).
What I need to do is create an Entity Framework DbSet that contains information from both the pet and owner tables. I have looked at Table splitting in EF, but the two "entities" of pet and owner do not have the same primary key (which as I understand Table Splitting is required).
I have reviewed the following articles, and they have not helped:
Return data from two tables with Entity Framework
I am using EF6 and the "Code First from existing Database" workflow.
My Pet class looks like (I've removed the auto generated data annotations for brevity):
Partial Public Class Pet
Public Sub New()
bookings = New HashSet(Of Booking)()
stays = New HashSet(Of Stay)()
End Sub
Public Property petID As Integer
Public Property petName As String
Public Property species As String
Public Property breed As String
Public Property DOB As Date?
Public Property gender As String
Public Property weight As Single?
Public Property customerID As Integer?
Public Overridable Property bookings As ICollection(Of Booking)
Public Overridable Property customer As Customer
Public Overridable Property stays As ICollection(Of Stay)
End Class
My Customer class:
Partial Public Class Customer
Public Sub New()
pets = New HashSet(Of Pet)()
End Sub
Public Property customerID As Integer
Public Property title As String
Public Property firstName As String
Public Property lastName As String
Public Property gender As String
Public Property DOB As Date?
Public Property email As String
Public Property phone1 As String
Public Property phone2 As String
Public Property street1 As String
Public Property street2 As String
Public Property suburb As String
Public Property state As String
Public Property postcode As String
Public Overridable Property state1 As State
Public Overridable Property pets As ICollection(Of Pet)
Public ReadOnly Property FullName() As String
Get
Return $"{Me.lastName}, {Me.firstName}"
End Get
End Property
End Class
I also have a PetInfo class that does NOT map to the DB:
Public Class PetInfoModel
Public Property PetID As Integer
Public Property PetName As String
Public Property Species As String
Public Property Breed As String
Public Property DOB As Date
Public Property Gender As String
Public Property Weight As Decimal
Public Property OwnerFirstName As String
Public Property OwnerLastName As String
Public ReadOnly Property OwnerName() As String
Get
Return $"{OwnerLastName}, {OwnerFirstName}"
End Get
End Property
End Class
Now for the hard part: I would like to be able to use the PetInfoModel as a DbSet in my context to take advantage of the EF change tracking.
If it makes any difference (I don't think it should), I am using WPF MVVM and Caliburn.Micro for the UI. The ultimate goal is to get a List bound to a WPF datagrid.
Any assistance or suggestions would be more than welcome. Thanks for your time and efforts.
Regards
Steve Teece
I'm not very familiar with VB, so I'll have to write the answer in C#, I think you get the gist.
So you have DbSet<Pet> Pets
and DbSet<Customer> Customers
and you want to create something that acts as if it was a DbSet<PetInfoModel> PetInfoModels
.
Are you sure you want something that acts like a DbSet? You want to be able to Add / Find / Attach / Remove PetInfoModels? Or do you only want to query data?
It seems to me that you get into troubles, if you want to Add a new PetInfoModel with a zero PetId, and the name of an existing Customer:
Add(new PetInfoModel
{
PetId = 0;
PetName = "Felix"
OwnerFirstName = "John",
OwnerLastName = "Doe",
Species = "Cat",
...
});
Add(new PetInfoModel
{
PetId = 0;
PetName = "Nero"
OwnerFirstName = "John", // NOTE: Same owner name
OwnerLastName = "Doe",
Species = "Dog",
...
});
Do we have one Customer with two Pets: a Cat and a Dog? Or do we have two Customers, with the same name, each with one Pet?
If you want more than just query PetInfoModels
(Add / Update / Remove), you'll need to find a solution for this. I think most problems will be solved if you add a CustomerId
. But then again: your PetInfoModel
would just be a subset of the properties of a "Pet with his Owner", making it a bit useless to create the idea of a PetInfoModel
Anyway: let's assume you've defined a proper PetInfoModel
and you really want to be able to Create / Retrieve / Update / Delete (CRUD) PetInfoModels
as if you have a database table of PetInfoModels
.
You should realize what your DbContext represents. It represents your database. The DbSet<...>
properties of your DbContext represent the tables in your database. Your database does not have a table with PetInfoModels
, hence your DbContext should not have this table.
On the other hand: Quite often you'll see a wrapper class around your DbContext that represents the things that can be stored in your Repository. This class is usually called a Repository.
In fact, a Repository only tells you that your data is stored, not how it is stored: it can be a CSV-file, or a database with a table structure different than the data sequences that can be handled by your repository.
IMHO I think it is wise to let your DbContext represent your database and create a Repository class that represents the stored data in a format that users of your database want.
As a minimum, I think a Repository should be able to Create / Retrieve / Update / Delete (CRUD) Customers and Pets. Later we'll add CRUD functions for PetInfoModels.
A RepositoryItem is something that can be stored / queried / removed from the repository. Every RepositoryItem can be identified by a primary key
interface IRepositoryItem<TRepositoryItem> : IQueryable<TRepositoryItem>
where TRepositoryItem : class
{
TRepositoryItem Add(TRepositoryItem item);
TRepositoryItem Find (params object[] keyValues);
void Remove(TRepositoryItem item);
}
To guarantee this primary key, I created an interface IID and let all my DbSet classes implement this interface. This enhances Find and Remove:
interface IID
{
int Id {get; }
}
class Student : IId
{
public int Id {get; set;}
...
}
interface IRepositoryItem<TRepositoryItem> : IQueryable<TRepositoryItem>
where TRepositoryItem : IID
{
TRepositoryItem Add(TRepositoryItem item);
TRepositoryItem Find (int id);
void Remove(TRepositoryItem item);
// or remove the item with primary key:
void Remove(int id);
}
If we have a DbSet the implementation of an IRespositoryItem is easy:
class RepositoryDbSet<TRepositoryItem> : IRepositoryItem<TRepositoryItem>
where TRepositoryItem : class
{
public IDbSet<TRepositoryItem> DbSet {get; set;}
public TRepositoryItem Add(TRepositoryItem item)
{
return this.DbSet.Add(item);
}
public TRepositoryItem Find (params object[] keyValues)
{
return this.DbSet.Find(keyValues);
}
public void Remove(TRepositoryItem item)
{
return this.DbSet.Remove(item);
}
public void Remove(TRepository
// implementation of IQueryable / IEnumerable is similar: use this.DbSet
}
If you defined interface IID:
public TRrepositoryItem Find(int id)
{
return this.DbSet.Find(id);
}
public void Remove(int id)
{
TRepositoryItem itemToRemove = this.Find(id);
this.DbSet.Remove(itemToRemove);
}
Now that we've defined the class that represents a set in the Repository, we can start creating the Repository itself.
class VetRepository : IDisposable
{
public VetRepository(...)
{
this.dbContext = new DbContext(...);
this.customers = new RepositoryItem<Customer> {DbSet = this.dbContext.Customers};
this.pets = new RepositoryItm<Pet> {DbSet = this.dbContext.Pets};
}
private readonly DbContext dbContext; // the old database
private readonly IRepositoryItem<Customer> customers;
private readonly IRepositoryItem<Pet> pets;
// TODO IDisposable: Dispose the dbcontext
// Customers and Pets:
public IRepositoryItem<Customer> Customers => this.customers;
public IRepositoryItem<Pet> Pets => this.pets;
IRepositoryItem<PetInfoModel> PetInfoModels = // TODO
public void SaveChanges()
{
this.DbContext.SaveChanges();
}
// TODO: SaveChangesAsync
}
We still have to create a repository class that represents the PetInfoModels
. This class should implement IRepositoryItem. This way users of the repository won't notice that the database doesn't have a table with PetInfoModels
class RepositoryPetInfoModel : IRepositoryItem<PetInfoModel>
{
// this class needs both Customers and Pets:
public IDbSet<Customer> Customers {get; set;}
public IDbSet<Pet> Pets {get; set;}
public PetInfoModel Add(PetInfoModel petInfo)
{
// check the input, reject if problems
// decide whether we have a new Pet for new customer
// or a new pet for existing customer
// what to do with missing fields?
// what to do if Customer exists, but his name is incorrect?
Pet petToAdd = ... // extract the fields from the petInfo
Customer customerToAdd = ... // or: customerToUpdate?
// Add the Pet,
// Add or Update the Customer
}
Hm, do you see how much troubles your PetInfoModel
encounters if you really want to CRUD?
Retrieve is easy: just create a Query that joins the Pet and his Owner and select the fields for a PetInfoModel. For example
IQueryable<PetInfoModel> CreateQuery()
{
// Get all Customers with their Pets
return this.Customers.Join(this.Pets
{
customer => customer.Id, // from every Customer take the primary key
pet => pet.CustomerId, // from every Pet take the foreign key
// Result selector: take every Customer with a matching Pet
// to make a new PetInfoModel
(customer, pet) => new PetInfoModel
{
CustomerId = customer.Id,
OwnerFirstName = customer.FirstName,
...
PetId = pet.Id,
PetName = pet.Name,
...
});
}
Update is also fairly easy: PetId and CustomerId should exist. Fetch the Pet and Customer and update the fields with the corresponding fields from PetInfoModel
Delete will lead to problems again: what if the Owner has a 2nd Pet? Delete only the Pet but not the Owner? Or Delete the Owner and all hist Pets, inclusive the Pets you didn't mention?
If you only want to query data, then it won't be a problem to introduce a PetInfoModel
.
To really CRUD PetInfoModels
, you'll encounter several problems, especially with the concept of Owners with two Pets, and Owners having the same name. I would advise not to CRUD for PetInfoModels, only query them.
A proper separation between your database and the concept of "stored data" (Repository) is advisable, because it allows you to have a database that differs from the model that users of your Repository see.