EF 6 calls INSERT instead of UPDATE - c#

This could be a duplicate question but a lot of searching for the words in the title only got me a lot of unrelated results.
I have an entity that's roughly set up like this:
public abstract class A
{
public GUID AId { get; set; }
public string SomeProperty { get; set; }
}
public class B : A
{
public string SomeOtherProperty { get; set; }
}
The context has public DbSet<B> BInstances { get; set; } for B objects. In OnModelCreating, the mapping has A set to ignored and B is mapped to a table called TableB.
The AId field is not auto-generated (not an identity field) but it's set to be primary key, both in the database and in the mapping. In the database, the field is defined as a non-null uniqueidentifier with no default.
At runtime, I'm loading an instance of B using its key (_token is just a CancellationToken):
var b = await (dbCtx.BInstances.FirstOrDefaultAsync(e => e.AId), _token));
Then, a property of b is set and I try to save it back to database:
b.SomeOtherProperty = "some new text";
await (dbCtx.SaveChangesAsync(_token));
At this point, I'm getting a Violation of PRIMARY KEY constraint error from the database, stating that the value of AId cannot be inserted because it'd be a duplicate. Of course, the ID is already in the database, I loaded the entity from there, using the ID. For some reason, EF generates an INSERT statement, not an UPDATE and I don't understand why.
When I check dbCtx.Entry(b).State, it's already set to EntityState.Modified. I'm at a loss - can someone point out what I'm doing wrong? I never had issues with updating entities before but I haven't used EF with GUID primary keys (usually I use long primary keys).
I'm using EF 6 and .NET Framework 4.7.1.

Thank you all for the suggestions - this turned out to be a mapping problem that I caused.
In my OnModelCreating() call, I called MapInheritedProperties() on a type that didn't inherit from a base class (other than object, of course) - this seems to have triggered a problem. Other entities that do share a base class worked fine with the mapping call.
I also called ToTable() directly against the entity class - this broke my table mapping for reasons I do not understand. Once I moved that call inside Map(), it started working as expected.
So I went from this:
entity.ToTable("tablename");
to this:
entity.Map(m => m.ToTable("tablename"));
to solve the problem.
Hopefully this will be useful for future readers.

try this
b.SomeOtherProperty = "some new text";
dbCtx.BInstances.AddOrUpdate(b);
await (dbCtx.SaveChangesAsync(_token));
AddorUpdate will update your b instance if it is already added.

Related

EF: manual (non autogenerated) keys require ad hoc handling of new entities

In a MVVM application with EF Core as ORM I decided to model a table with a manually inserted, textual primary key.
This is because in this specific application I'd rather use meaningful keys instead of meaningless integer ids, at least for simple key-value tables like the table of the countries of the world.
I have something like:
Id | Description
-----|--------------------------
USA | United States of America
ITA | Italy
etc. etc.
So the entity is:
public class Country
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public string Id { get; set; }
public string Description { get; set; }
}
Here is my viewmodel. It's little more than a container for the ObservableCollection of Countries. Actually it gets loaded from a repository. It's trivial and I inlcuded the entire code at the end. It's not really relevant and I could do with just the DbContext as well. But I wanted to show all the layers to see where the solution belongs to. Oh yes, then it contains the synchronizing code that actually offends EF Core.
public class CountriesViewModel
{
//CountryRepository normally would be injected
public CountryRepository CountryRepository { get; set; } = new CountryRepository(new AppDbContext());
public ObservableCollection<Country> Countries {get; set;}
public CountriesViewModel()
{
Countries = new ObservableCollection<Country>();
Countries.CollectionChanged += Countries_CollectionChanged;
}
private void Countries_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
foreach (Country c in e.NewItems)
{
CountryRepository.Add(c);
}
}
}
In my MainWindow I just have:
<Window.DataContext>
<local:CountriesViewModel/>
</Window.DataContext>
<DockPanel>
<DataGrid ItemsSource="{Binding Countries}"/>
</DockPanel>
Problem and question
Now this doesn't work. When we try to insert a new record, in this case I do it using the automatic feature of DataGrid I get a:
System.InvalidOperationException: 'Unable to track an entity of type 'Country'
because primary key property 'Id' is null.'
Each time i add a new record to the ObservableCollection I also try to add it back to the repository, that in turn adds it on the EF DbContext that doesn't accept entities with null key.
So what are my options here?
One is postponing the addition of the new record till the Id has been inserted. This is not trivial as the collection handling that I've shown, but this is not the problem. The worst is that this way I would have some record that are tracked by EF (the updated and the deleted and the new with pk assigned) and some that are tracked by the view model (the new ones with the key not yet assigned).
Another is using alternate keys; I would have an integer, autogenerated primary key and the ITA,USA etc code would be an alternate key that would be used also in relations. It's not so bad from as simplicity, but I'd like a application-only solution.
What I'm looking for
I'm looking for a neat solution here, a pattern to be used whenever this problem arises and that plays well in the context of a MVVM/EF application.
Of course I could also look in the direction of the view events, that is force the user to insert the key before of a certain event that triggers the insertion. I would consider it a second-class solution because it is sort of view dependent.
Remaining code
Just for completeness, in case that you want to run the code, here is the remaining code.
DbContext
(Configured for postgres)
public class AppDbContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseNpgsql("Host=localhost;Database=WpfApp1;Username=postgres;Password=postgres");
}
public DbSet<Country> Countries { get;set; }
}
Repository
The reason why I implemented the repository for such a simple example is because I think that a possible solution may be to include the new-without-key records managment in the Repository instead of in the viewmodel. I still hope that someone comes out with a simpler solution.
public class CountryRepository
{
private AppDbContext AppDbContext { get; set; }
public CountryRepository(AppDbContext appDbContext) => AppDbContext = appDbContext;
public IEnumerable<Country> All() => AppDbContext.Countries.ToList();
public void Add(Country country) => AppDbContext.Add(country);
//ususally we don't have a save here, it's in a Unit of Work;
//for the example purpose it's ok
public int Save() => AppDbContext.SaveChanges();
}
Probably the cleanest way to address the aforementioned issue in EF Core is to utilize temporary value generation on add. In order to do that, you would need a custom ValueGenerator like this:
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.ValueGeneration;
public class TemporaryStringValueGenerator : ValueGenerator<string>
{
public override bool GeneratesTemporaryValues => true; // <-- essential
public override string Next(EntityEntry entry) => Guid.NewGuid().ToString();
}
and fluent configuration similar to this:
modelBuilder.Entity<Country>().Property(e => e.Id)
.HasValueGenerator<TemporaryStringValueGenerator>()
.ValueGeneratedOnAdd();
The potential drawbacks are:
In pre EF Core 3.0 the generated temporary value is set onto entity instance, thus would be visible in the UI. This has been fixed in EF Core 3.0, so now Temporary key values are no longer set onto entity instances
Even though the property looks empty (null) and is required (default for primary/alternate keys), if you don't provide explicit value, EF Core will try to issue INSERT command and read the "actual" value back from database similar to identity and other database generated values, which in this case will lead to non user friendly database generated runtime exception. But EF Core in general does not do validations, so this won't be so different - you have to add and validate property required rule in the corresponding layer.

.NET Core Entity Framework InvalidOperationException

I have a simple model
[Table("InterfaceType")]
public class InterfaceType
{
[Key]
public int InterfaceTypeId { get; set; }
public string Description { get; set; }
}
and in my DbContext
public DbSet<InterfaceType> InterfaceTypes { get; set; }
and in my controller
List<InterfaceType> types = _context.InterfaceTypes.FromSql(
"SELECT * FROM [Interfaces].[Control].[InterfaceType]").ToList();
Which is returning the error:
InvalidOperationException: The required column 'InterfaceID' was not present in the results of a 'FromSql' operation.
I am using FromSql in other methods similar to this with no issue although those models do contain an InterfaceId. Why does this operation expect an InterfaceId when it is not in the model. I have also tried the below with the same result.
List<InterfaceType> types = _context.InterfaceTypes.FromSql(
"SELECT InterfaceTypeId, Description FROM [Interfaces].[Control].[InterfaceType]").ToList();
I have also tried:
interfacesOverview.SelectedInterface.InterfaceTypes = _context.InterfaceTypes.ToList();
After declaring via the fluent api:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<InterfaceType>().ToTable("InterfaceType", "Control");
}
with the same result.
For clarity here is the table in MSSQL:
CREATE TABLE [Control].[InterfaceType](
[InterfaceTypeId] [tinyint] NOT NULL,
[Description] [varchar](25) NULL,
CONSTRAINT [PK_InterfaceType] PRIMARY KEY CLUSTERED
(
[InterfaceTypeId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
UPDATE
I've looked at the SQL that EF is generating:
SELECT [i].[InterfaceTypeId], [i].[Description], [i].[InterfaceID] FROM [Control].[InterfaceType] AS [i]
Where is it getting InterfaceID from?
Where is it getting InterfaceID from?
First, it should be clear that it's not coming from the shown "simple" (but apparently incomplete) model.
The EF generated SQL clearly indicates that you didn't rename the PK property generated column, also there is no Discriminator column, so it cannot be coming from inheritance. And the chance that you have explicitly defined a shadow property called InterfaceID and not noticing it is small.
All this, along with the fact that the name InterfaceID matches one of the EF Core conventional names for FK property/column name for me is a clear indication of a conventional FK introduced by a relationship. For instance having a second model like this:
public class Interface
{
public int ID { get; set; }
// or
// public int InterfaceID { get; set; }
public ICollection<InterfaceType> InterfaceTypes { get; set; }
}
As explained in the Relationships - Single Navigation Property EF Core documentation topic:
Including just one navigation property (no inverse navigation, and no foreign key property) is enough to have a relationship defined by convention.
and the accompanying example shows Blog / Post model with only public List<Post> Posts { get; set; } property in Blog highlighted.
All EF Core runtime behaviors are based on model metadata. It doesn't matter what is the structure of your database, the more important is what EF Core thinks it is base on your model classes, data annotations and fluent configuration, and if that matches the database schema. The easier way to check that is to generate migration and check if it matches the database schema or not.
So if the relationship is intentional, then you have to update your database to match your model. Otherwise you need to update your model to match the database - by removing or ignoring the collection navigation property (or correcting the invalid data annotation / fluent configuration causing the discrepancy).
My understanding of this problem, is that EF created a Shadow Property
inside your model class, possibly by partially discovered relationship in your Interface model.
Also I feel there is a mismatch between your ModelSnapshot used by EFCore and real state of tables in Database (possibly by pending migration). Double check, how your InterfaceType in <YourDbContext>ModelSnapshot.cs, and check if there's a property you are missing.
My guess is that you also have an "Interface" table registered in the context that holds a reference to the InterfaceType. Interface would have an InterfaceTypeId field declared, however with EF, if you are using HasOne with a ForeignKey, check that you haven't accidentally assigned something like:
.HasOne(x => x.InterfaceType).WithOne().HasForeignKey<InterfaceType>("InterfaceId");
In the case of an Interface having an InterfaceType it would be mapped more like:
.HasOne(x => x.InterfaceType).WithMany();
This might have crept into one of your other associated entities. Often these are typos where the autocomplete picked the wrong type without you noticing. If that mapping exists on any of your classes, EF will be expecting to find an InterfaceId column on InterfaceType. Do a search on HasForeignKey<InterfaceType> and see if that turns up anything out of the ordinary.
First why not use
List<InterfaceType> types = _context.InterfaceTypes.ToList();
Secondly did you apply any changes to the model and forget to persist this to the database, as it could be that the column is correct in your class but not in your database. This is often something i forget to do when using a Code-FirstModel.
Here is some additional info on FromSQL :- https://learn.microsoft.com/en-us/ef/core/querying/raw-sql
More detail on migration here:- https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/
I hope this helps.
Maybe try to add DatabaseGeneratedAttribute
[Table("InterfaceType")]
public class InterfaceType
{
[DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity),Key()]
public int InterfaceTypeId { get; set; }
...
Just to check if there is a way I can reproduce, I created a sample .NET Core Console application to check this and in my case, I am able to retrieve the data from DB without any exception.
I understand you have other models where the same code is working,
and if you move the problematic code outside your original solution, you might be
able to figure out if there is something obvious you are missing.
I tried to follow your code as closely as possible in attempt to reproduce this issue, where some of the things I had to change.
I don't know which .NET Core and EF Core versions you have used. In my sample, I used:
.NET Core 2.2
Microsoft.EntityFrameworkCore 2.2
Microsoft.EntityFrameworkCore.Design 2.2
Microsoft.EntityFrameworkCore.SqlServer 2.2
Microsoft.EntityFrameworkCore.Tools 2.2
I created:
Model Class as per your sample
Context Class with OnModelCreating per your sample
Executed following dotnet core commands in the same order as listed below:
dotnet restore
dotnet build
dotnet ef migrations add InitMgr
dotnet ef database update
Added few test records in the table
Copied your records retrieval code, removed "[Interfaces]" from the query and debugged the code below.
var _context = new InterfaceTypeContext ();
List<InterfaceType> types = _context.InterfaceTypes.FromSql ("SELECT * FROM [Control].[InterfaceType]").ToList ();
I was able to retrieve the data from DB.
It would also help if you share Minimal, Complete, and Verifiable
example for someone to debug and help you find a solution for
this.
The following has worked for me:
Insert some data:
insert into [Control].[InterfaceType] values (1, 'Desc1'), (2, 'Desc2');
C#:
class SOContext : DbContext
{
public DbSet<InterfaceType> InterfaceTypes { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var conn_string = #"Server=(localdb)\mssqllocaldb;Database=Interfaces;Trusted_Connection=Yes;";
optionsBuilder.UseSqlServer(conn_string);
}
}
[Table("InterfaceType", Schema = "Control")]
public class InterfaceType
{
[DatabaseGenerated(DatabaseGeneratedOption.None), Key]
public byte InterfaceTypeId { get; set; }
public string Description { get; set; }
public override string ToString() =>
$"Id: {InterfaceTypeId} | Description: {Description}";
}
// Output:
// Id: 1 | Description: Desc1
// Id: 2 | Description: Desc2

Failed to save the context - foreign key issue

I have some problems during the SaveChangesAsync on my Context. I'm getting this error:
The INSERT statement conflicted with the FOREIGN KEY constraint
I think the reason of this error is self-explanatory, I have some violations in my entities regarding the external key. But...
Let me explain my environment:
I have 2 entities in a 1:N relationship (let's say 'Item1' can be associated with N 'Item2'). My entities look like:
public class Item1
{
[Key]
public int Id { get; set; }
[...]
//navigation property
public ICollection<Item2> Item2list{ get; set; }
}
public class Item2
{
[Key]
public int Id { get; set; }
[..]
//navigation property
public int Item1Id{ get; set; }
public Item1 Item1{ get; set; }
}
So in my code, after I have built 2 instances of these items (oItem1 and oItem2), I have:
oItem1.Item2list.Add(oItem2);
myRepository.Add(oItem1);
And all seems fine but when I call the
Context.SaveChangesAsync()
I get (randomly, it doesn't appear every time) the error I have mentioned before. If I inspect the local cache of my context I see that some 'Item2' have an 'Item1Id = 0' that doesn't match the Id of the entity it should refer to.
And, it it appears, this error occurs just in 2-3 cases (over like 6000 entities and, every time, on different items). Of course I can't save them in my DB, but I don't get why this situation appear and how to fix it.
Note: I'm running in a multi-thread environment and the nature of my error suggests is something related it. But I would also point out that every threads build their own 'Item1' and 'Item2'. The repository (context) is the same for everyone, since I'm injecting it in the constructor of the main class.
I'm not expecting you to solve my problem (but if you do it would be great :P), but I just would like some hints/tips on what to check.
Which parts would you look in?
Have you experienced this kind of issue in the past?
Could it be related to the fact I'm running with multi-thread?
I'm really stuck.
Item2 is referencing Item1Id of Item1 as a foreign key which makes it essential that Item1 should exist before its Id can exist in the Item2 table.
You need to do the following in the given order:
Save Item1 to its corresponding table.
Save Item2 to its corresponding table. Set Item1Id to the Id of Item1 from the above step.
You can get list of Item2 while fetching Item1 enteries by simply including them. (See code below)
Code for writing to Database:
oItem1.Id = 2 //some Id
// setting other oItem1 properties
myRepository.Add(oItem1);
// setting oItem2 properties
oItem2.Item1Id = oItem1.Id;
myRepository.Add(oItem1);
Context.SaveChangesAsync();
Code for reading from Database :
var items = dbContext.Item1.Include( i => i.Item2list).FirstOrDefault(); // I am fetching the first or default record from `Item1` along with the list of `Item2`s associated with it
Or use the equivalent method of your Repository

Entity Framework hard cascade delete

I have a SQLite DB mapped with Entity Framework.
There are 2 tables : Collections (1:n) Albums.
When I delete a collection, all related albums have to be deleted as well.
I use CollectionRepo.Delete(collection); to achieve that. It uses the following code :
public int Delete(Collection entity)
{
Context.Entry(entity).State = EntityState.Deleted;
return Context.SaveChanges();
}
The problem is: when I execute this code, Context.SaveChanges(); give me an exception:
The operation failed: The relationship could not be changed because one or more of the foreign-key properties is non-nullable. When a change is made to a relationship, the related foreign-key property is set to a null value. If the foreign-key does not support null values, a new relationship must be defined, the foreign-key property must be assigned another non-null value, or the unrelated object must be deleted.
It seems that Entity Framework wants to null on the foreign keys instead of deleting the entries. But this is absolutely not what I want because an album makes no sense without a parent (in my use case at least).
I could obviously manualy delete the albums first and then delete the empty collection but it seems to me a bit tricky. First, it seems to me that EF should be smart enough to do it on it's own to simplify the code and second, what if I have dozens of relations to collections and albums, I would end up with quite a big, hard to maintain, code base.
Collection Class
public class Collection
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
public virtual List<Album> Albums { get; set; } = new List<Album>();
}
Album class
public class Album
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
[Required]
[ForeignKey("Collection")]
public long CollectionId { get; set; }
public virtual Collection Collection { get; set; }
}
DbContext child class
public class DataEntities : DbContext
{
public virtual DbSet<Collection> Collections { get; set; }
public virtual DbSet<Album> Albums { get; set; }
public DataEntities() : base("name=Connection")
{
Configuration.ProxyCreationEnabled = false;
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Album>()
.HasRequired(a => a.Collection)
.WithMany(c => c.Albums)
.HasForeignKey(a => a.CollectionId)
.WillCascadeOnDelete(true);
modelBuilder.Entity<Collection>()
.HasMany(c => c.Albums)
.WithRequired(a => a.Collection)
.WillCascadeOnDelete(true);
}
}
Applying detached object graph modifications has always been unclear in EF. This is one of the cases where it fails without a good reason.
Assuming the Collection entity passed to the Delete method has Albums collection populated (at least this is how I was able to reproduce the exception). The line
Context.Entry(entity).State = EntityState.Deleted;
does two things: attaches entity and all Album objects from the entity.Albums to the context, marks entity as Deleted, and (note!) the Album objects as Modified. This leads to incorrect behavior when you call SaveChanges, and at the end generates the exception in question.
There are two ways (workarounds) to fix this incorrect behavior.
The first one is to replace the above line with
Context.Collections.Attach(entity);
Context.Collections.Remove(entity);
The effect is similar to the described above, with the importand difference that now the related Album objects arte marked as Deleted, which allows successfully executing the SaveChanges.
The drawback is that now the SaveChanges issues a DELETE command for each Album before the command for deleting the Collection, which is inefficient and doesn't make much sense since the cascade delete would handle that perfectly inside the database.
The second option is to keep the code as is, but clear the related collection before attaching the entity:
entity.Albums = null;
Context.Entry(entity).State = EntityState.Deleted;
This allows successfully executing SaveChanges and it generates a single DELETE command only for the entity.
The drawback is that you need to write additional code and not forget any child collection which supports cascade delete, and not doing that for collections that need cascade update (i.e. with optional relation which requires updating the FK field with NULL).
The choice is yours.
Per your comments, you're mapping to a pre-existing database (EF did not generate it). CascadeOnDelete only affects the generation of the database. If the database doesn't have CascadeOnDelete configured on the table, then EF will be confused when it attempts to delete and Sqlite doesn't comply.
Also you have the mapping for the foreign key as non-nullable and required (redundant by the way) but in the database the foreign key is nullable. EF assumes that it's not valid because of what you told it.
If you fix your mapping (remove the required annotation from the CollectionID property and change its type to int? instead of just int you should fix your problem. Actually change the mapping in the DbContext class from HasRequired to HasOptional...from there it should work.
Either that or change the table definitions on your database itself.

Entity Framework associations with multiple (separate) keys on view

I'm having problems setting up an Entity Framework 4 model.
A Contact object is exposed in the database as an updateable view. Also due to the history of the database, this Contact view has two different keys, one from a legacy system. So some other tables reference a contact with a 'ContactID' while other older tables reference it with a 'LegacyContactID'.
Since this is a view, there are no foreign keys in the database, and I'm trying to manually add associations in the designer. But the fluent associations don't seem to provide a way of specifying which field is referenced.
How do I build this model?
public class vwContact
{
public int KeyField { get; set; }
public string LegacyKeyField { get; set; }
}
public class SomeObject
{
public virtual vwContact Contact { get; set; }
public int ContactId { get; set; } //references vwContact.KeyField
}
public class LegacyObject
{
public virtual vwContact Contact { get; set; }
public string ContactId { get; set; } //references vwContact.LegacyKeyField
}
ModelCreatingFunction(modelBuilder)
{
// can't set both of these, right?
modelBuilder.Entity<vwContact>().HasKey(x => x.KeyField);
modelBuilder.Entity<vwContact>().HasKey(x => x.LegacyKeyField);
modelBuilder.Entity<LegacyObject>().HasRequired(x => x.Contact).???
//is there some way to say which key field this reference is referencing?
}
EDIT 2: "New things have come to light, man" - His Dudeness
After a but more experimentation and news, I found using a base class and child classes with different keys will not work by itself. With code first especially, base entities must define a key if they are not explicitly mapped to tables.
I left the suggested code below because I still recommend using the base class for your C# manageability, but I below the code I have updated my answer and provided other workaround options.
Unfortunately, the truth revealed is that you cannot accomplish what you seek without altering SQL due to limitations on EF 4.1+ code first.
Base Contact Class
public abstract class BaseContact
{
// Include all properties here except for the keys
// public string Name { get; set; }
}
Entity Classes
Set this up via the fluent API if you like, but for easy illustration I've used the data annotations
public class Contact : BaseContact
{
[Key]
public int KeyField { get; set; }
public string LegacyKeyField { get; set; }
}
public class LegacyContact : BaseContact
{
public int KeyField { get; set; }
[Key]
public string LegacyKeyField { get; set; }
}
Using the Entities
Classes that reference or manipulate the contact objects should reference the base class much like an interface:
public class SomeCustomObject
{
public BaseContact Contact { get; set; }
}
If later you need to programmatically determine what type you are working with use typeof() and manipulate the entity accordingly.
var co = new SomeCustomObject(); // assume its loaded with data
if(co.Contact == typeof(LegacyContact)
// manipulate accordingly.
New Options & Workarounds
As I suggested in comment before, you won't be able to map them to a single view/table anyway so you have a couple options:
a. map your objects to their underlying tables and alter your "get/read" methods on repositories and service classes pull from the joined view -or-
b. create a second view and map each object to their appropriate view.
c. map one entity to its underlying table and one to the view.
Summary
Try (B) first, creating a separate view because it requires the least amount of change to both code and DB schema (you aren't fiddling with underlying tables, or affecting stored procedures). It also ensures your EF C# POCOs will function equivalently (one to a view and one to table may cause quirks). Miguel's answer below seems to be roughly the same suggestion so I would start here if it's possible.
Option (C) seems worst because your POCO entities may behave have unforseen quirks when mapped to different SQL pieces (tables vs. views) causing coding issues down the road.
Option (A), while it fits EF's intention best (entities mapped to tables), it means to get your joined view you must alter your C# services/repositories to work with the EF entities for Add, Update, Delete operations, but tell the Pull/Read-like methods to grab data from the joint views. This is probably your best choice, but involves more work than (B) and may also affect Schema in the long run. More complexity equals more risk.
Edit I'm not sure this is actually possible, and this is why:
The assumption is that a foreign key references a primary key. What you've got is two fields which are both acting as primary keys of vwContact, but depending on which object you ask it's a different field that's the primary key. You can only have one primary key at once, and although you can have a compound primary key you can't do primary key things with only half of it - you have to have a compound foreign key with which to reference it.
This is why Entity Framework doesn't have a way to specify the mapping column on the target side, because it has to use the primary key.
Now, you can layer some more objects on top of the EF entities to do some manual lookup and simulate the navigation properties, but I don't think you can actually get EF to do what you want because SQL itself won't do what you want - the rule is one primary key per table, and it's not negotiable.
From what you said about your database structure, it may be possible for you to write a migration script which can give the contact entities a consistent primary key and update everything else to refer to them with that single primary key rather than the two systems resulting from the legacy data, as you can of course do joins on any fields you like. I don't think you're going to get a seamlessly functional EF model without changing your database though.
Original Answer That Won't Work
So, vwContact contains a key KeyField which is referenced by many SomeObjects and another key LegacyKeyField which is referenced by many LegacyObjects.
I think this is how you have to approach this:
Give vwContact navigation properties for SomeObject and LegacyObject collections:
public virtual ICollection<SomeObject> SomeObjects { get; set; }
public virtual ICollection<LegacyObject> LegacyObjects { get; set; }
Give those navigation properties foreign keys to use:
modelBuilder.Entity<vwContact>()
.HasMany(c => c.SomeObjects)
.WithRequired(s => s.Contact)
.HasForeignKey(c => c.KeyField);
modelBuilder.Entity<vwContact>()
.HasMany(c => c.LegacyObjects)
.WithRequired(l => l.Contact)
.HasForeignKey(c => c.LegacyKeyField);
The trouble is I would guess you've already tried this and it didn't work, in which case I can't offer you much else as I've not done a huge amount of this kind of thing (our database is much closer to the kinds of thing EF expects so we've had to do relatively minimal mapping overrides, usually with many-to-many relationships).
As for your two calls to HasKey on vwContact, they can't both be the definitive key for the object, so it's either a compound key which features both of them, or pick one, or there's another field you haven't mentioned which is the real primary key. From here it's not really possible to say what the right option there is.
You should be able to do this with two different objects to represent the Contact view.
public class vwContact
{
public int KeyField { get; set; }
public string LegacyKeyField { get; set; }
}
public class vwLegacyContact
{
public int KeyField { get; set; }
public string LegacyKeyField { get; set; }
}
public class SomeObject
{
public virtual vwContact Contact { get; set; }
public int ContactId { get; set; } //references vwContact.KeyField
}
public class LegacyObject
{
public virtual vwLegacyContact Contact { get; set; }
public string ContactId { get; set; } //references vwLegacyContact.LegacyKeyField
}
ModelCreatingFunction(modelBuilder)
{
// can't set both of these, right?
modelBuilder.Entity<vwContact>().HasKey(x => x.KeyField);
modelBuilder.Entity<vwLegacyContact>().HasKey(x => x.LegacyKeyField);
// The rest of your configuration
}
I have tried everything that you can imagine, and found that most solutions won't work in this version of EF... maybe in future versions it supports referencing another entity by using an unique field, but this is not the case now. I also found two solutions that work, but they are more of a workaround than solutions.
I tried all of the following things, that didn't work:
Mapping two entities to the same table: this is not allowed in EF4.
Inheriting from a base that has no key definitions: all root classes must have keys, so that inherited classes share this common key... that is how inheritance works in EF4.
Inheriting from base class that defines all fields, including keys, and then use modelBuilder to tell wich base-properties are keys of the derived types: this doesn't work, because the methos HasKey, Property and others that take members as parameters, must reference members of the class itself... referencing properties of a base class is not allowed. This cannot be done: modelBuilder.HasKey<MyClass>(x => x.BaseKeyField)
The two things that I did that worked:
Without DB changes: Map to the table that is source of the view in question... that is, if vwContact is a view to Contacts table, then you can map a class to Contacts, and use it by setting the key to the KeyField, and another class mapping to the vwContacts view, with the key being LegacyKeyField. In the class Contacts, the LegacyKeyField must exist, and you will have to manage this manually, when using the Contacts class. Also, when using the class vwContacts you will have to manually manage the KeyField, unless it is an autoincrement field in the DB, in this case, you must remove the property from vwContacts class.
Changing DB: Create another view, just like the vwContacts, say vwContactsLegacy, and map it to a class in wich the key is the LegacyKeyField, and map vwContacts to the original view, using KeyField as the key. All limitations from the first case also applies: the vwContacts must have the LegacyKeyField, managed manually. And the vwContactsLegacy, must have the KetField if it is not autoincrement idenitity, otherwise it must not be defined.
There are some limitations:
As I said, these solutions are work-arounds... not real solutions, there are some serious implications, that may even make them undesirable:
EF does not know that you are mapping two classes to the same thing. So when you update one thing, the other one could be changed or not, it depends if the objects is cached or not. Also, you could have two objects at the same time, that represents the same thing on the backing storage, so say you load a vwContact and also a vwContactLegacy, changes both, and then try to save both... you will have to care about this yourself.
You will have to manage one of the keys manually. If you are using vwContacts class, the KeyFieldLegacy is there, and you must fill it. If you want to create a vwContacts, and associate is with a LegacyObject, then you need to create the reference manually, because LegacyObject takes a vwContactsLegacy, not a vwContacts... you will have to create the reference by setting the ContactId field.
I hope that this is more of a help than a disillusion, EF is a powerfull toy, but it is far from perfect... though I think it's going to get much better in the next versions.
I think this may be possible using extension methods, although not directly through EF as #Matthew Walton mentioned in his edit above.
However, with extension methods, you can specify what to do behind the scenes, and have a simple call to it.
public class LegacyObject
{
public virtual vwContact Contact { get; set; }
public string ContactId { get; set; } //references vwContact.LegacyKeyField
}
public class LegacyObjectExtensions
{
public static vwContact Contacts(this LegacyObject legacyObject)
{
var dbContext = new LegacyDbContext();
var contacts = from o in legacyObject
join c in dbContext.vwContact
on o.ContactId == c.LegacyKeyField
select c;
return contacts;
}
}
and
public class SomeObject
{
public virtual vwContact Contact { get; set; }
public int ContactId { get; set; } //references vwContact.KeyField
}
public class SomeObjectExtensions
{
public static vwContact Contacts(this SomeObject someObject)
{
var dbContext = new LegacyDbContext();
var contacts = from o in someObject
join c in dbContext.vwContact
on o.ContactId == c.KeyField
select c;
return contacts;
}
}
Then to use you can simply do like this:
var legacyContacts = legacyObject.Contacts();
var someContacts = someObject.Contacts();
Sometimes it makes more sense to map it from the other end of the relationship, in your case:
modelBuilder.Entity<LegacyObject>().HasRequired(x => x.Contact).WithMany().HasForeignKey(u => u.LegacyKeyField);
however this will require that u.LegacyKeyField is marked as a primary key.
And then I'll give my two cents:
if the Legacy db is using LegacyKeyField, then perhaps the legacy db will be read only. In this case we can create two separate contexts Legacy and Non-legacy and map them accordingly. This can potentially become a bit messy as you'd have to remember which object comes from which context. But then again, nothing stops you from adding the same EF code first object into 2 different contexts
Another solution is to use views with ContactId added for all other legacy tables and map them into one context. This will tax performance for the sake of having cleaner context objects, but this can be counteracted on sql side: indexed views, materialized views, stored procs, etc. So than LEGACY_OBJECT becomes VW_LEGACY OBJECT with CONTACT.ContactId brought over, then:
modelBuilder.Entity<LegacyObject>().ToTable("VW_LEGACY_OBJECT");
modelBuilder.Entity<LegacyObject>().HasRequired(x => x.Contact).WithMany().HasForeignKey(u => u.ContactId);
I personally would go with creating "mapper views" with CustomerId on legacy tables, as it's cleaner from c# layer perspective and you can make those views look like real tables. It is also difficult to suggest a solution without knowing what exactly is the scenario that you have a problem with: querying, loading, saving, etc.

Categories

Resources