I have a mental debate with myself every time I start working on a new project and I am designing my POCOs. I have seen many tutorials/code samples that seem to favor foreign key associations:
Foreign key association
public class Order
{
public int ID { get; set; }
public int CustomerID { get; set; } // <-- Customer ID
...
}
As opposed to independent associations:
Independent association
public class Order
{
public int ID { get; set; }
public Customer Customer { get; set; } // <-- Customer object
...
}
I have worked with NHibernate in the past, and used independent associations, which not only feel more OO, but also (with lazy loading) have the advantage of giving me access to the whole Customer object, instead of just its ID. This allows me to, for example, retrieve an Order instance and then do Order.Customer.FirstName without having to do a join explicitly, which is extremely convenient.
So to recap, my questions are:
Are there any significant disadvantages in
using independent associations? and...
If there aren't any, what
would be the reason of using foreign key associations at all?
If you want to take full advantage of ORM you will definitely use Entity reference:
public class Order
{
public int ID { get; set; }
public Customer Customer { get; set; } // <-- Customer object
...
}
Once you generate an entity model from a database with FKs it will always generate entity references. If you don't want to use them you must manually modify the EDMX file and add properties representing FKs. At least this was the case in Entity Framework v1 where only Independent associations were allowed.
Entity framework v4 offers a new type of association called Foreign key association. The most obvious difference between the independent and the foreign key association is in Order class:
public class Order
{
public int ID { get; set; }
public int CustomerId { get; set; } // <-- Customer ID
public Customer Customer { get; set; } // <-- Customer object
...
}
As you can see you have both FK property and entity reference. There are more differences between two types of associations:
Independent association
It is represented as separate object in ObjectStateManager. It has its own EntityState!
When building association you always need entitites from both ends of association
This association is mapped in the same way as entity.
Foreign key association
It is not represented as separate object in ObjectStateManager. Due to that you must follow some special rules.
When building association you don't need both ends of association. It is enough to have child entity and PK of parent entity but PK value must be unique. So when using foreign keys association you must also assign temporary unique IDs to newly generated entities used in relations.
This association is not mapped but instead it defines referential constraints.
If you want to use foreign key association you must tick Include foreign key columns in the model in Entity Data Model Wizard.
Edit:
I found that the difference between these two types of associations is not very well known so I wrote a short article covering this with more details and my own opinion about this.
Use both. And make your entity references virtual to allow for lazy loading. Like this:
public class Order
{
public int ID { get; set; }
public int CustomerID { get; set; }
public virtual Customer Customer { get; set; } // <-- Customer object
...
}
This saves on unnecessary DB lookups, allows lazy loading, and allows you to easily see/set the ID if you know what you want it to be. Note that having both does not change your table structure in any way.
Independent association doesn't work well with AddOrUpdate that is usually used in Seed method. When the reference is an existing item, it will be re-inserted.
// Existing customer.
var customer = new Customer { Id = 1, Name = "edit name" };
db.Set<Customer>().AddOrUpdate(customer);
// New order.
var order = new Order { Id = 1, Customer = customer };
db.Set<Order>().AddOrUpdate(order);
The result is existing customer will be re-inserted and new (re-inserted) customer will be associated with new order.
Unless we use the foreign key association and assign the id.
// Existing customer.
var customer = new Customer { Id = 1, Name = "edit name" };
db.Set<Customer>().AddOrUpdate(customer);
// New order.
var order = new Order { Id = 1, CustomerId = customer.Id };
db.Set<Order>().AddOrUpdate(order);
We have the expected behavior, existing customer will be associated with new order.
I favour the object approach to avoid unnecessary lookups. The property objects can be just as easily populated when you call your factory method to build the whole entity (using simple callback code for nested entities). There are no disadvantages that I can see except for memory usage (but you would cache your objects right?). So, all you are doing is substituting the stack for the heap and making a performance gain from not performing lookups. I hope this makes sense.
Related
I am new to EF Core 6.0.1, using it with Blazor (WebAssembly), .NET 6.0, and Visual Studio 2022. I am creating a database of internal software projects, including their author(s) and maintainer(s).
I am having trouble getting EF Core to take in a List of Authors / List of Maintainers as part of creating a new SoftwareItem from a webform submission.
SoftwareItem in defined (in part) as follows:
public class SoftwareItem
{
[Key]
public int SoftwareId { get; set; }
public string Name { get; set; }
public string CurrentVersion { get; set; }
public string Status { get; set; }
public List<Author> Authors { get; set; }
public List<Maintainer> Maintainers { get; set;}
[other properties omitted]
}
An Author is defined as follows:
public class Author
{
[Key]
public int AuthorId { get; set; }
public int SoftwareItemId { get; set; }
public int ProgrammerId { get; set; }
public Programmer Programmer { get; set; }
}
Maintainer is identical, except for having a MaintainerId instead of an AuthorId.
Programmer is defined as:
public class Programmer
{
[Key]
public int ProgrammerId { get; set; }
public string ProgrammerName { get; set; }
}
EF Core created the tables for me based on a migration, and I have manually populated the Programmer table with the nine people who might be an Author and/or a Maintainer.
I have a webform where the user can create a new SoftwareItem, with pre-populated drop-downs for Authors and Maintainers that, after querying the database, contain the potential ProgrammerNames. The user can assign up to three Authors and up to three Maintainers before submitting the webform (via an Author1 dropdown, an Author2 dropdown etc.) Submitting the webform calls the InsertSoftware method, included below.
Note that I'm not a fan of the repetition between the Author logic and Maintainer logic, and the List should probably be a HashSet (in case the same author is set in Author1 and Author2) but those are issues for another day. The Author1 and similar variables are the int IDs set by the webform. I've previously verified they are being set to the appropriate values via a JavaScript alert. An ID of 0 means the value was never set (e.g. there is no second author).
The SoftwareItem here is instantiated as a new object on OnIntializedAsync and bound as the webform's model.
public async Task InsertSoftware()
{
List<int> authorIdsToAdd = new List<int>();
authorIdsToAdd.Add(Author1);
authorIdsToAdd.Add(Author2);
authorIdsToAdd.Add(Author3);
SoftwareItem.Authors = new List<Author>();
foreach (int author in authorIdsToAdd)
{
if (author != 0)
{
foreach (Programmer programmer in ProgrammerList)
{
if (programmer.ProgrammerId == author)
{
Author addedAuthor = new Author();
addedAuthor.Programmer = new Programmer();
addedAuthor.Programmer.ProgrammerId = author;
SoftwareItem.Authors.Add(addedAuthor);
}
}
}
}
[repeat code for the Maintainers]
await Http.PostAsJsonAsync("api/softwareitem", SoftwareItem);
Navigation.NavigateTo("software/fetchsoftware");
}
The SoftwareItem API is (in part) as follows:
[HttpPost]
public async Task<IActionResult> Create([FromBody] SoftwareItem softwareItem)
{
_context.Software.Add(softwareItem);
await _context.SaveChangesAsync();
return Ok(softwareItem);
}
My understanding from this Stack Overflow question is that if objects have been instantiated for a navigation property when the parent entity is added and saved to the database context, then EF Core will also add the new navigation property values to their appropriate tables. However, that isn't happening, and all I'm getting is a 500 error in the console.
What I'm expecting is that...
A new entry will be inserted into the SoftwareItem table
New entries will be inserted into the Author table, containing an auto-incremented AuthorId, the SoftwareItem's SoftwareItemId, and the ProgrammerId from the webform
New entries will be inserted into the Maintainer table, containing an auto-incremented MaintainerId, the SoftwareItem's SoftwareItemId, and the ProgrammerId from the webform.
Ok, it's a bit difficult to make out what your code is precisely doing but there are a few issues I see.
First, with entities you should always avoid ever reinitializing navigation property lists. During inserts it's "ok", but anywhere else it would lead to bugs/errors so it's better to simply not see it in the code. Pre-initialize your properties in the entity itself:
public class SoftwareItem
{
// ...
public virtual ICollection<Author> Authors { get; set; } = new List<Author>();
public virtual ICollection<Maintainer> Maintainers { get; set;} = new List<Maintainer>();
}
This ensures the collections are ready to go when you need them for a new entity.
Next, it can be helpful to structure your code to avoid things like module level variables. Your InsertSoftware() method references an instance of SoftwareItem and it isn't clear where, or what this reference would be pointing at. If you have a method chain that loaded a particular software item instance to be updated, pass the reference through the chain of methods as a parameter. This helps encapsulate the logic. You should also look to define a scope for whenever you are referencing a DbContext. With Blazor this needs to be done a bit more explicitly to avoid DbContext instances from being too long-lived. Long-lived DbContext instances are a problem because they lead to performance degradation as they track increasing numbers of entities, and can easly become "poisoned" with invalid entities that prevent things like SaveChanges() calls from succeeding. Keep instances alive only as long as absolutely necessary. I would strongly recommend looking at unit of work patterns to help encapsulate the lifetime scope of a DbContext. Ideally entities loaded by a DbContext should not be passed outside of that scope to avoid issues and complexity with detached or orphaned entities.
Next, it is important to know when you are looking to create new entities vs. reference existing data. Code like this is a big red flag:
Author addedAuthor = new Author();
addedAuthor.Programmer = new Programmer();
addedAuthor.Programmer.ProgrammerId = author;
From what I can make out, the Author (and Maintainer) are linking entities so we will want to create one for each "link" between a software item and a programmer. However, Programmer is a reference to what should be an existing row in the database.
If you do something like:
var programmer = new Programmer { ProgrammerId == author };
then associate that programmer as a reference to another entity, you might guess this would tell EF to find and associate an existing programmer.. Except it doesn't. You are telling EF to associate a new programmer with a particular ID. Depending on how EF has been configured for that entity (whether to use an identity column for the PK or not) this will result in one of three things happening if that programmer ID already exists:
A new programmer is created with an entirely new ID (identity gives it a new id and ProgrammerId is ignored)
EF throws an exception when it tries to insert a new programmer with the same ID. (Duplicate PK)
EF throws an exception if you tell it add a new programmer and it happens to already be tracking an instance with the same ID.
So, to fix this, load your references:
List<int> authorIdsToAdd = new List<int>();
// likely need logic to only add authors if they are selected, and unique.
authorIdsToAdd.Add(Author1);
authorIdsToAdd.Add(Author2);
authorIdsToAdd.Add(Author3);
// Define your own suitable scope mechanism for this method or method chain
using (var context = new AppDbContext())
{
var softwareItem = new SoftwareItem { /* populate values from DTO or Map from DTO */ }
// Retrieve references
var authors = await context.Programmers.Where(x => authorIdsToAdd.Contains(x.ProgrammerId)).ToListAsync();
foreach(var author in authors)
{
softwareItem.Authors.Add(new Author { Programmer = author });
}
// Continue for Maintainers...
await context.SaveChangesAsync();
}
I am coding an MVC5 C# Internet application and I have a class called MapCompany and a class classed MapLocation.
Each MapCompany has a list of MapCompany's. My question is, for the context class, should I just have a DbSet<MapCompany>, and add MapLocations to the specific MapCompany, or should I have both a DbSet<MapCompany> and a DbSet<MapLocation>?
EDIT
Here is a bit of information about the application:
Each MapCompany can have many MapLocations ~5-25
I wish to be able to access any MapLocation by its id
Because I wish to be able to access each MapLocation by its id, will there be a lot of database searching if there is only a DbSet<MapCompany>, as I would have to search through each MapCompany to find a MapLocation by its id?
Would it be more economical to have a DbSet of each because of the increase in database searching? Also, will this make the database a lot larger?
Either way, each MapCompany needs to have many MapLocations, and I need to be able to retrieve any object by its own id efficiently.
With this above information, I am interested in whether I should code a DbSet for each object.
If you have foreign key relationship between those entities.
public class MapCompany
{
public int Id { get; set; }
public ICollection<MapLocation> MapLocations { get; set; } // foreign key
}
public class MapLocation
{
public int Id { get; set; }
public int MapCompanyId { get; set; } // foreign key
public MapCompany MapCompany { get; set; } // foreign key
}
It's okay to only have DbSet<MyCompany>, the MapLocation will still be generated on the database because of the foreign key relationship.
If you want to also have DbSet<MapLocation>, it's also okay.
If you don't have DbSet<MapLocation>, but want to access it directly without retrieving MyCompany entity first, you can do this.
using (var context = new MyDbContext())
{
var mapLocation = context.Set<MapLocation>().Find(1);
}
I would recommend you use the Repository Pattern for your data layer.
http://msdn.microsoft.com/en-us/library/ff649690.aspx
I would also make a relationship between your two models IF you are not expecting a large amount of Map Location records for each MapCompany. Otherwise it might be more optimal to return a list of id's instead of models.
Its hard to say without knowing anything about your application.
As suggested, you need to use Repository Pattern... Try to check this example on Repository Pattern:
http://www.asp.net/mvc/tutorials/getting-started-with-ef-5-using-mvc-4/implementing-the-repository-and-unit-of-work-patterns-in-an-asp-net-mvc-application
I am trying to use EF Code First on an existing database. I first tried some of the reverse-engineering tools, but I ran into problems with that, so at the moment I am trying to hand-code some of the classes. I am having some trouble getting some of the foreign key relationships set up. Consider two tables. The first is called LocaleValueLookup:
public class LocaleValueLookup
{
public int Id { get; set; }
public Guid Guid { get; set; }
}
This table provides an Id for multi-language text held in a different table (that other table is not important for the purposes of this question). The second table is called SectionType, and it has an optional FK to LocaleValueLookup:
public class SectionType
{
public int EnumId { get; set; }
public string Name { get; set; }
public int? DefaultSectionTextLocaleValueLookupId { get; set; }
// Navigation property
public LocaleValueLookup DefaultSectionTextLocaleValueLookup { get; set; }
}
I have tried various things, including adding a [ForeignKey] attribute to the SectionType.LocaleValueLookup property, and various incantations in the DbContext.OnModelCreating() override, but when I query the DbContext, I can't get the DefaultSectionTextLocaleValueLookup to be anything but null. I can retrieve other objects from the context just fine, and I have verified that DefaultSectionTextLocaleValueLookupId is not null at least some of the time.
My OnModelBuilding() contains the following:
modelBuilder.Entity<LocaleValueLookup>()
.ToTable("LocaleValueLookup")
.HasKey(lvl => lvl.Id);
modelBuilder.Entity<LocaleValueLookup>().Property(lvl => lvl.Id).IsRequired();
modelBuilder.Entity<SectionType>()
.ToTable("SectionType")
.HasKey(st => st.EnumId);
modelBuilder.Entity<SectionType>().Property(st => st.EnumId).IsRequired();
A couple of other points:
I would prefer not to have a SectionType collection on the LocaleValueLookup object. LocaleValueLookup is a low-level class that a lot of other classes depend on, so to include a collection property on LocaleValueLookup for every other class that references it will make for an unwieldy class with a lot of collections on it that I don't need from a domain perspective.
I would prefer to do the mapping setup in DbContext.OnModelCreating() rather than using attributes on my model objects
Any help would be greatly appreciated!
It looks like your foreign key is nullable so that means an optional -> many relationship.
Could you try something like this:
modelBuilder.Entity<SectionType>()
.HasOptional(opt => opt.DefaultSectionTextLocaleValueLookup)
.WithMany() // no navigation on the other side
.HasForeignKey(fk => fk.DefaultSectionTextLocaleValueLookupId);
If you were to write a query like this you should get a value back:
var query =
from st in db.SectionTypes
where st.EnumId == 12345
select new
{
SectionType = st,
LocaleValue = st.DefaultSectionTextLocaleValueLookup
};
It will only be non-null if the foreign key has a value, obviously.
Say I have the following entity classes:
public class Order
{
public int OrderID { get; set; }
public ICollection<OrderLine> OrderLines { get; set; }
}
public class OrderLine
{
public int OrderLineID { get; set; }
public Order Order { get; set; }
}
I want to enforce a minimum cardinality of 1 for this relationship; ie I want to ensure that an Order cannot be created without at least 1 OrderLine.
I'm using EF code first fluent style configurations and I am able to enforce the fact that OrderLine must have an Order reference (using HasRequired() extension method) but I cant see how I can prevent an Order from being created without at least one OrderLine.
In short: you can't. Your requirement cannot be mapped to a database constraint: orders and order lines are saved separately, so when you create an order and add an order line, either the order or the order line must be saved first. The order line -> order relation is backed by a foreign key, so the order must be saved first. When the order is saved, as far as the database knows, the order has no order lines, they're not added until later.
You can create custom validation functions and call them before saving. If you're using an ObjectContext, you will have to do this yourself. If you have a DbContext, you should be able to override DbContext.ValidateEntity. For obvious reasons, this only works if you make all database modifications through your context. If you modify the database tables directly, custom validation functions don't get used.
It can be done, just not with fluent configuration:
public class Order : IValidatableObject
{
public IEnumerable<ValidationResult> Validate(
ValidationContext validationContext)
{
if (!OrderLines.Any())
yield return new ValidationResult("At least one line needed");
}
}
This will be enforced when you SaveChanges(), just like a Required property, or any other model constraint.
I am designing my database using code first and I need a little help I think.
I am getting this error:
Introducing FOREIGN KEY constraint 'SalesOrder_Invoices' on table 'Invoices' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
Could not create constraint. See previous errors.
I am trying to have the following relationships/keys:
--> = 1 to Many Relationship
Customer --> CustomerLocation
CustomerLocation --> SalesOrder
SalesOrder --> Invoice
SalesRep --> SalesOrder
PaymentTerm --> Customer
PaymentTerm --> SalesOrder
PaymentTerm --> Invoice
I am trying to define them by the standard of:
<ClassName><PrimaryKeyID>
Example: Customer has ID property, so in CustomerLocation i define the foreign key like so:
Public Property CustomerID AS Integer
All I have to do is define the foreign key correct? Do I also have to have navigation properties for each key I define?
And, can I not have multiple foreign keys on the same primary key of an object?
Updated
So to define a relationship, do you use the ClassName.PrimaryKeyProperty? or do you use navigation properties? Or both? Confused!!
Update 2
So to make a relationship work you have to define both sides... I think.
Public Class Customer
Public Property ID AS Integer
Public Overrideable Property Locations AS ICollection(OF CustomerLocation)
End Class
Public Class CustomerLocation
Public Property ID AS Integer
Public Property CustomerID AS Integer
End Class
This is exception caused by SQL server when you have multiple paths of cascade deletes. If you delete your PaymentTerm it will trigger cascade delete on all three relations. This will blow up when creating either SalesOrder or Invoice. EF creates by default all one-to-many relations with ON DELETE CASCADE you can remap your specific relation to not use it by:
modelBuilder.Entity<...>()
.HasRequired(...)
.WithMany(...)
.HasForeignKey(...)
.WillCascadeOnDelete(false);
Or you can turn it off globaly by removing the convention:
modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
You can get around this error on a particular migration by editing the generated Up() method with a line something like this:
AddForeignKey("dbo.Payments", "EventID", "dbo.Events", "EventID", cascadeDelete: true)
and change that cascadeDelete: value to false on the offending relationship(s).
Read this, I am sure this will help you find the answer.
Also, according to ScottGu's blogpost, I think in general it should be that you just create the classes as follows (I didn't read it carefully enough, so you should check it out for further details):
public class Customer
{
public int CustomerID { get; set; }
public int CustomerLocationID { get; set; }
public virtual CustomerLocation Location { get; set; }
}
public class CustomerLocation
{
public int CustomerLocationID { get; set; }
public virtual ICollection<Customer> Customers { get; set; }
}