I am new at C# entity framework. I am trying to build an API, but stuck in retrieving data from relational table.
I have a pei_crops table in MS SQL database, where c_id is the primary key. I have another table called pei_pests, where p_id is the primary key. Another table is pei_cropspests where I have built relation for which pest attack which crop. Multiple pests can attack one crop and one pest can attack multiple crops. In this pei_cropspests table I have put p_id as primary and foreign key and c_id as primary and foreign key as well.
pei_crops table:
c_id
c_name
c_description
1
Corn
NULL
pei_pests table:
p_id
p_name
p_URL
1
pest1
NULL
2
pest2
NULL
pei_cropspests table:
p_id
c_id
1
1
2
1
Now In my API I want to show something like that
[
{
"cId":1,
"pests":[
{
"pId":1,
"pName": pest1,
"pURL": null
},
{
"pId":2,
"pName": pest2,
"pURL": null
}
]
}
]
My get request looks like this so far in C# web API project:
[Route("Getspecific/{cropId}")]
[HttpGet]
public async Task<IActionResult> GetSpecific(int cropId)
{
var cropDetails = await _db.PeiCrops.Where(c=>c.CId == cropId).Include(i=>i.PeiCropspests).ToListAsync();
return Ok(cropDetails);
}
This code returns me only the pID and URL of the pest that effects cID number 1. But I also want the pest name and URL along with their id.
Could someone please show me how to do it. Maybe there is some way to join two table and show the data? I just do not know how to do it in C#. Any help appreciated. Thank you.
Entities class:
PeiCrop:
using System;
using System.Collections.Generic;
#nullable disable
namespace PEI_API.EF
{
public partial class PeiCrop
{
public PeiCrop()
{
PeiCropimages = new HashSet<PeiCropimage>();
PeiCropsdiseases = new HashSet<PeiCropsdisease>();
PeiCropspests = new HashSet<PeiCropspest>();
}
public int CId { get; set; }
public string CName { get; set; }
public string CPhotoUrl { get; set; }
public string CDescription { get; set; }
public virtual ICollection<PeiCropimage> PeiCropimages { get; set; }
public virtual ICollection<PeiCropsdisease> PeiCropsdiseases { get; set; }
public virtual ICollection<PeiCropspest> PeiCropspests { get; set; }
}
}
PeiPest:
using System;
using System.Collections.Generic;
#nullable disable
namespace PEI_API.EF
{
public partial class PeiPest
{
public PeiPest()
{
PeiCropspests = new HashSet<PeiCropspest>();
PeiPestimages = new HashSet<PeiPestimage>();
}
public int PId { get; set; }
public string PName { get; set; }
public string PPhotoUrl { get; set; }
public string PDescription { get; set; }
public virtual ICollection<PeiCropspest> PeiCropspests { get; set; }
public virtual ICollection<PeiPestimage> PeiPestimages { get; set; }
}
}
PeiCropspest:
using System.Collections.Generic;
#nullable disable
namespace PEI_API.EF
{
public partial class PeiCropspest
{
public int PId { get; set; }
public int CId { get; set; }
public virtual PeiCrop CIdNavigation { get; set; }
public virtual PeiPest PIdNavigation { get; set; }
}
}
You're pretty close, but you're also not entirely using EF like you could, I mean you do not actually have to make the relationship table yourself but could refer directly to a list of the entity pei_pests from the entity pei_crop and let EF create the other.
//Example just getting one property from each,
//but you can new a composite return type up if you wish, using select
var cropDetails = await _db.PeiCrops
.Where(c=>c.CId == cropId)
.Include(i=>i.PeiCropspests)
.ThenInclucde(t => t.Pests)
.Select(s => new { CropId = s.p_id, PestName = s.PeiCropsPests.Pest.p_name })
.ToListAsync();
https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.select?view=net-5.0
First, you need to configure the relationships :
class MyContext : DbContext
{
...
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<PeiCropspest>()
.HasKey(cp => new { cp.PId, cp.CId });
//Configure one PeiPest to many PeiCropspest
modelBuilder.Entity<PeiCropspest>()
// Specify PeiCropspest's navigation property to one PeiPest
.HasOne(cp => cp.PIdNavigation)
// Specify PeiPest's navigaton property to many PeiCropspest
.WithMany(p => p.PeiCropspests)
// Specify PeiCropspest's navigation property
// to use this PeiCropspest's property as foreign key
.HasForeignKey(cp => cp.PId);
//Configure one PeiCrop to many PeiCropspest
modelBuilder.Entity<PeiCropspest>()
// Specify PeiCropspest's navigation shadow property to one PeiCrop
.HasOne<PeiCrop>()
// Specify PeiCrop's navigaton property to many PeiCropspest
.WithMany(c => c.PeiCropspests)
// Specify PeiCropspest's navigation shadow property
// to use this PeiCropspest's property as foreign key
.HasForeignKey(cp => cp.CId);
}
public DbSet<PeiCrop> PeiCrops { get; set; }
}
Then you can do a projection in the LINQ query :
public async Task<IActionResult> GetSpecific(int cropId)
{
var cropDetails = await _db.PeiCrops
.Where(c=>c.CId == cropId)
.Select(c => new {
cId = c.CId,
pests = c.PeiCropspests.Select(p => new {
pId = p.PIdNavigation.PId,
pName = p.PIdNavigation.PName,
pUrl = p.PIdNavigation.PPhotoUrl
})
})
.ToListAsync();
return Ok(cropDetails);
}
Do you know? From EF Core 5, it's possible to do many to many relationship without intermediary entity. This can simplify your entity model. cf. the documentation
Related
I've been struggling with this all evening and still don't fully understand how Entity Framework Core works with many to many relationships.
I have a TransportProvider class and a Tag class. It's a many to many relationship. When adding a new TransportProvider you can assign tags. If the tag already exists in the database I'd like to add that existing tag, otherwise I'd like to insert a new tag. This is what I have for my TransportProvider class:
public class TransportProvider
{
public int ID { get; set; }
[Display(Name = "Company name")]
[Required]
[StringLength(200)]
public string CompanyName { get; set; }
... standard properties
public bool Disabled { get; set; }
[NotMapped]
public string SelectedTags { get; set; }
public ICollection<Tag> Tags { get; set; }
}
My tag class:
public class Tag
{
public int ID { get; set; }
[Required]
[StringLength(100)]
public string Name { get; set; }
public ICollection<TransportProvider> TransportProviders { get; set; }
}
And this is my controller function that creates a new transport provider:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("ID,CompanyName,ContactName,ContactTelephone1,ContactTelephone2,ContactEmail,CompanyWebsite,AddressLine1,AddressLine2,Suburb,Province,PostCode,Country,Lat,Lng,SelectedTags,Notes,Disabled")] TransportProvider transportProvider)
{
if (ModelState.IsValid)
{
var selectedTags = !string.IsNullOrEmpty(transportProvider.SelectedTags) ? transportProvider.SelectedTags.Split(',') : new string[0];
_context.TransportProviders.Add(transportProvider);
foreach (var selectedTag in selectedTags)
{
var tag = _context.Tags.SingleOrDefault(t => t.Name.ToLower() == selectedTag);
if (tag == null)
{
tag = new Tag();
tag.Name = selectedTag;
}
transportProvider.Tags.Add(tag);
}
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View(transportProvider);
}
and finally my context class:
public class AntelopeContext : DbContext
{
public AntelopeContext(DbContextOptions<AntelopeContext> options) : base(options)
{
}
public DbSet<TransportProvider> TransportProviders { get; set; }
public DbSet<Tag> Tags { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<TransportProvider>().ToTable("TransportProvider");
modelBuilder.Entity<Tag>().ToTable("Tag");
}
}
If I try and execute this code I get a NullReferenceException for the line:
transportProvider.Tags.Add(tag);
I don't know why this is so difficult to do. All I want to do is add tags to a transport provider. If the tag is new it needs to insert a new tag record. If not then it just has to link the existing tag.
How do I do this?
Thanks
Many to many relationships require a collection navigation property on both sides. They will be discovered by convention like other types of relationships.
public class TransportProvider
{
public int TransportProviderId { get; set; }
public string CompanyName { get; set; }
public bool Disabled { get; set; }
public ICollection<Tag> Tags { get; set; }
}
public class Tag
{
public int TagId { get; set; }
public string Name { get; set;}
public ICollection<TransportProvider> TransportProviders { get; set; }
}
The way this relationship is implemented in the database is by a join table that contains foreign keys to both TransferProvider and Tag. For example this is what EF will create in a relational database for the above model.
CREATE TABLE [TransportProvider] (
[TransportProviderId] int NOT NULL IDENTITY,
[CompanyName] nvarchar(max) NULL,
[Disable] bit NULL,
CONSTRAINT [PK_TransportProvider] PRIMARY KEY ([TransportProviderId])
);
CREATE TABLE [Tag] (
[TagId] int NOT NULL IDENTITY,
[Name] nvarchar(max) NULL,
CONSTRAINT [PK_Tag] PRIMARY KEY ([TagId])
);
CREATE TABLE [TransportProviderTag] (
[TransportProviderId] int NOT NULL,
[TagId] int NOT NULL,
CONSTRAINT [PK_TransportProviderTag] PRIMARY KEY ([TransportProviderId], [TagId]),
CONSTRAINT [FK_TransportProviderTag_TransportProviders_TransportProviderId] FOREIGN KEY ([TransportProviderId]) REFERENCES [TransferProviders] ([TransferProviderId]) ON DELETE CASCADE,
CONSTRAINT [FK_TransportProviderTag_Tags_TagId] FOREIGN KEY ([TagId]) REFERENCES [Tags] ([TagId]) ON DELETE CASCADE
);
Internally, EF creates an entity type to represent the join table that will be referred to as the join entity type.
This is a code first approach.
You have first to create TransferProvider and Tag, and then add what row with them in TransferProviderTag table
Since you didn't bind the Tags property, it will default be null, you need to initialize the Tags in TransportProvider firstly.
public async Task<IActionResult> Create([Bind("ID,CompanyName,ContactName,ContactTelephone1,ContactTelephone2,ContactEmail,CompanyWebsite,AddressLine1,AddressLine2,Suburb,Province,PostCode,Country,Lat,Lng,SelectedTags,Notes,Disabled")] TransportProvider transportProvider)
{
transportProvider.Tags = new List<Tag>();
//...
}
Finally! I got it working. I'm not sure this is the 'correct' way, but it seems to work.
I was under the impression that EF Core 5 didn't require joining tables in many-to-many relationships. However when I tried to execute without a joining table I was getting an error about a joining table not being present. I therefore added one as suggested.
I then manually created the TransportProvider, manually checked for a Tag and created if it didn't exist, then manually entered the joining table record. I still feel this probably isn't the most efficient way of doing things, but it works. Code in case anyone is interested:
public async Task<IActionResult> Create([Bind("ID,CompanyName,ContactName,ContactTelephone1,ContactTelephone2,ContactEmail,CompanyWebsite,AddressLine1,AddressLine2,Suburb,Province,PostCode,Country,Lat,Lng,SelectedTags,Notes,Disabled")] TransportProvider transportProvider)
{
if (ModelState.IsValid)
{
var selectedTags = !string.IsNullOrEmpty(transportProvider.SelectedTags) ? transportProvider.SelectedTags.Split(',') : new string[0];
transportProvider.TransportProviderTags = new List<TransportProviderTag>();
_context.TransportProviders.Add(transportProvider);
await _context.SaveChangesAsync();
foreach (var selectedTag in selectedTags)
{
var tag = _context.Tags.SingleOrDefault(t => t.Name.ToLower() == selectedTag);
if (tag == null)
{
tag = new Tag();
tag.Name = selectedTag;
_context.Tags.Add(tag);
await _context.SaveChangesAsync();
}
var tpt = new TransportProviderTag();
tpt.TransportProviderID = transportProvider.ID;
tpt.TagID = tag.ID;
transportProvider.TransportProviderTags.Add(tpt);
await _context.SaveChangesAsync();
}
return RedirectToAction(nameof(Index));
}
return View(transportProvider);
}
Updated context class:
public class AntelopeContext : DbContext
{
public AntelopeContext(DbContextOptions<AntelopeContext> options) : base(options)
{
}
public DbSet<TransportProvider> TransportProviders { get; set; }
public DbSet<Tag> Tags { get; set; }
public DbSet<TransportProviderTag> TransportProviderTags { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<TransportProvider>().ToTable("TransportProvider");
modelBuilder.Entity<Tag>().ToTable("Tag");
modelBuilder.Entity<TransportProviderTag>().ToTable("TransportProviderTag");
modelBuilder.Entity<TransportProviderTag>()
.HasKey(tpt => new { tpt.TransportProviderID, tpt.TagID });
modelBuilder.Entity<TransportProviderTag>()
.HasOne(tpt => tpt.TransportProvider)
.WithMany(tp => tp.TransportProviderTags)
.HasForeignKey(tpt => tpt.TransportProviderID);
modelBuilder.Entity<TransportProviderTag>()
.HasOne(tpt => tpt.Tag)
.WithMany(t => t.TransportProviderTags)
.HasForeignKey(tpt => tpt.TagID);
}
}
And thanks #MilutinSpaic and #mj1313 for steering me in the right direction. Hopefully this will help someone else
I have a class
public class Document
{
public string TranId { get; set; }
public Record Record { get; set; }
public List<Error> Errors { get; set; }
}
public class Record
{
public string TranId { get; set; }
public List<DataResult> DataResults { get; set; }
}
public class DataResult
{
public string DataSourceName { get; set; }
public List<DataField> DataFields { get; set; }
public List<CustomField> CustomFields { get; set; }
}
I want to map Record and DataResult classes as Value objects so I tried to map as
public void Configure(EntityTypeBuilder<Document> builder)
{
builder.ToTable("Document");
builder.HasKey(x => x.TranId);
builder.OwnsOne(a => a.Record, a =>
{
a.ToTable("Doc_Record");
a.Property(p => p.TranId).HasMaxLength(100)
.HasColumnName("TranID")
.HasDefaultValue("");
a.OwnsMany(x => x.DataResults, x =>
{
x.ToTable("Doc_Rec_DataResults");
x.Property(p => p.DataSourceName).HasMaxLength(150)
.HasColumnName("DataSourceName")
.HasDefaultValue("");
});
}
}
When I try to add this migration it errors with a message:
The entity type 'DataResult' requires a primary key to be defined.
And why it requires primary key cause I'm trying to map as a value
object?
Someone suggested using this link and I try to add
a.OwnsMany(x => x.DataResults, x =>
{
x.WithOwner().HasForeignKey("RecordId");
x.ToTable("Doc_Rec_DataResults");
x.Property(p => p.DataSourceName).HasMaxLength(150)
.HasColumnName("DataSourceName")
.HasDefaultValue("");
});
but this approach is not working cause WithOwner is available from .net core 3 where I'm using .net core 2 (and do I really need to add RecordId property into Record class (it's a value object).
It would be great if someone can provide an example of how to map collection of value objects with OwnsMany in EF Core 2.
In this ef core 2.2 example, we have a Company that owns a collection of Addresses, here's the implementation. note that i omitted some useful code to stick to the point, refer to the full example for further explanation. Also note that this feature OwnsMany() is not available in pre ef core 2.2
public class CompanyAddress
{
public string City { get; }
public string AddressLine1 { get; }
}
public class Company
{
private List<CompanyAddress> addresses = new List<CompanyAddress>();
public Guid Id { get; }
public string Name { get; }
public IEnumerable<CompanyAddress> Addresses { get => this.addresses; }
public void AssignAddress(CompanyAddress address)
{
var exists = this.addresses.Contains(address);
if (!exists)
{
this.addresses.Add(address);
}
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Company>().OwnsMany<CompanyAddress>("Addresses", a =>
{
a.HasForeignKey("CompanyId");
a.Property(ca => ca.City);
a.Property(ca => ca.AddressLine1);
a.HasKey("CompanyId", "City", "AddressLine1");
});
}
here's a repo of full solution for the article's owner
I'm currently using MVC with EF to have a small server with API querying a SQL database. But in the API reply I'm not able to hide some parameters.
The main object
public class AssetItem
{
[Key]
public Int32 AssetId { get; set; }
public String AssetName { get; set; }
public int OdForeignKey { get; set; }
[ForeignKey("OdForeignKey")]
public OperationalDataItem OperationalDataItem { get; set; }
}
The other one:
public class OperationalDataItem
{
[Key]
public Int32 OperationalDataId { get; set; }
public String Comunity { get; set; }
public List<AssetItem> AssetItems { get; set; }
}
From what I have read, this should be ok, I have also set the context:
public AssetContext(DbContextOptions<AssetContext> options) : base(options)
{}
public DbSet<AssetItem> AssetItems { get; set; }
public DbSet<OperationalDataItem> OperationalDataItems { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<AssetItem>().HasOne(p =>
p.OperationalDataItem).WithMany(b => b.AssetItems).HasForeignKey(p =>
p.OdForeignKey);
}
And the seeding in program.cs
context.AssetItems.Add(
new AssetItem { AssetName = "Test test", OdForeignKey = 1,
OperationalDataItem =
new OperationalDataItem {Comunity = "Comunity1" }});
So calling the API this results in:
{ "assetId":3,
"assetName":"Test test",
"odForeignKey":1,
"operationalDataItem":null }
From what I read this is because of the lazy loading, how can I hide the result operationalDataItem?
In case is not possible i have of course try to query for it and give it back and it give something like:
{ "assetId":3,
"assetName":"Test test",
"odForeignKey":1,
"operationalDataItem":
{ "operationalDataId":1,
"comunity":"Comunity1",
"assetItems":[
But in this case I would like to hide "assetsItems" in the reply to the FE.
How can I hide those parameters?
The API is quite simple, just an example code:
var todoItem = await _context.AssetItems.FindAsync((Int32)id);
var item = _context.OperationalDataItems.Find((Int32)todoItem.OdForeignKey);
todoItem.OperationalDataItem = item;
return todoItem
If you want to fetch data from the database, but you only want to fetch some properties, use Select. Usually this is more efficient than using Find, because you'll only transfer the data that you actually plan to use.
To fetch some properties of the assetItem that has primary key assetItemId:
var result = dbContext.AssetItems
.Where(assetItem => assetItem.AssetItmId = assetItemId)
.Select(assetItem => new
{
// Select only the properties that you plan to use
Id = assetItem.AssertItemId,
Name = assetItem.Name,
OperationalData = new
{
// again, select only the properties that you plan to use
Id = assetItem.OperationalData.OperationalDataId,
Community = assetItem.OperationalData.Community,
},
})
.FirstOrDefault();
Or the other way round:
Fetch several properties of all (or some) OperationalDataItems, each with some properties of all (or some) of its AssetItems:
var result = dbContext.OperqationalDataItems
.Where(operationalDataItem => ...) // only if you don't want all
.Select(operationalDataItem => new
{
Id = operationalDataItem.Id,
Community = operationalDataItem.Community
AssetItems = operationalDataItem.AssetItems
.Where(assetItem => ...) // only if you don't want all its assetItems
.Select(assetItem => new
{
// Select only the properties you plan to use:
Id = assetItem.Id,
...
// not useful: you know the value of the foreign key:
// OperationalDataId = assetItem.OperationalDataId,
})
.ToList();
})
.ToList(); // or: FirstOrDefault if you expect only one element
Entity framework knows your one-to-many relation and is smart enough to know which (group-)join is needed for your query.
Some side remarks
You've declare your many-relation a List<AssetItem>. Are you sure that operationalDataItem.AssetItems[4] has a defined meaning? Wouldn't it be better to stick to the entity framework code first conventions? This would also eliminate the need for most attributes and / or fluent API
public class OperationalDataItem
{
public int Id { get; set; }
public String Comunity { get; set; }
...
// Every OperationalDataItem has zero or more AssetItems (one-to-many)
public virtual ICollection<AssetItem> AssetItems { get; set; }
}
public class AssetItem
{
public int Id { get; set; }
public String Name { get; set; }
...
// every AssetItem belongs to exactly one OperationalDataItem, using foreign key
public int OperationDataItemId { get; set; }
public virtual OperationalDataItem OperationalDataItem { get; set; }
}
In entity framework the columns of a table are represented by the non-virtual properties. The virtual properties represent the relations between the tables (one-to-many, many-to-many)
Because I stuck to the conventions, no attributes nor fluent API is needed. Entity framework is able to detect the one-to-many relation and the primary and foreign keys. Only if I am not satisfied with the names or the types of the columns I would need fluent API.
So I am trying to achieve entity splitting in EF 6.1 with Code First, and I am running into an error.
I have the following tables:
CREATE TABLE [dbo].[Organization]
(
[OrganizationId] INT NOT NULL PRIMARY KEY IDENTITY,
[TenantId] INT NOT NULL,
[Name] NVARCHAR(80) NOT NULL
)
CREATE TABLE [dbo].[OrganizationSettings]
(
[OrganizationSettingsId] INT NOT NULL PRIMARY KEY IDENTITY,
[OrganizationId] INT NOT NULL,
[AllowMultipleTimers] BIT NOT NULL,
CONSTRAINT [FK_OrganizationSettings_Organization] FOREIGN KEY (OrganizationId) REFERENCES Organization(OrganizationId)
)
With the following model objects:
public partial class Organization
{
public int OrganizationId { get; set; }
public int TenantId { get; set; }
public string Name { get; set; }
public OrganizationSettings Settings { get; set; }
}
public class OrganizationSettings
{
public int OrganizationSettingsId { get; set; }
public int OrganizationId { get; set; }
public bool AllowMultipleTimers { get; set; }
}
With the following config code:
var org = modelBuilder.Entity<Organization>();
org.Map(u =>
{
u.Properties(m => new { m.TenantId, m.Name });
})
.ToTable("Organization");
org.Map(u =>
{
u.Property(m => m.Settings.AllowMultipleTimers).HasColumnName("AllowMultipleTimers");
u.ToTable("OrganizationSettings");
});
Then just the following query:
context.Organizations.FirstOrDefault();
Which yields the following error:
The property 'Settings.AllowMultipleTimers' on type 'Organization'
cannot be mapped because it has been explicitly excluded from the
model or it is of a type not supported by the DbModelBuilderVersion
being used.
What am I doing wrong here?
Update: I forgot to mention that I created the database by hand, and am using the CF fluent API to map my models, rather than using "real" Code First.
While I was pretty sure I had this mapping working before, I went ahead and went a little different route.
First I got rid of the surrogate key on `OrganizationSettings (probably not strictly necessary), and then mapped it as an entity with a 1:1 relationship.
My OrganizationSettings is now:
public class OrganizationSettings
{
public int OrganizationId { get; set; }
public bool AllowMultipleTimers { get; set; }
}
OrganizationId is both a primary key and a foreign key.
And the config is:
var org = modelBuilder.Entity<Organization>()
.Map(u =>
{
u.Properties(m => new { m.TenantId, m.Name });
})
.HasRequired(m => m.Settings)
.WithRequiredPrincipal();
modelBuilder.Entity<OrganizationSettings>()
.HasKey(m => m.OrganizationId);
And this seems to work just fine. Since I'm not exposing a DbSet for OrganizationSettings it keeps the conceptual modeling of OrganizationSettings as a value object intact.
Were you trying to set up OrganizationSettings as a complex type while using entity splitting as well? Something like this, perhaps:
public partial class Organization
{
public int OrganizationId { get; set; }
public int TenantId { get; set; }
public string Name { get; set; }
public OrganizationSettings Settings { get; set; }
}
public class OrganizationSettings
{
public bool AllowMultipleTimers { get; set; }
}
// if you don't have a key defined on OrganizationSettings, this might not be needed
modelBuilder.ComplexType<OrganizationSettings>();
modelBuilder.Entity<Organization>()
.Map(u =>
{
u.Properties(m => new { m.OrganizationId, m.TenantId, m.Name });
u.ToTable("Organization");
})
.Map(u =>
{
u.Properties(m => new { m.OrganizationId, m.Settings.AllowMultipleTimers });
u.ToTable("OrganizationSettings");
// If you wanted to set the key column name
u.Property(m => m.OrganizationId).HasColumnName("OrganizationSettingsId");
});
I'm using Entity Framework 4.3.1 Code-First and I need to split an entity between two tables. The tables have a primary key shared, and it is 1-to-1, but the columns are not named the same on each table.
I don't control the data layout, nor can I request any changes.
So for example, the SQL tables could be
And this would be my entity...
public class MyEntity
{
public int Id {get; set;}
public string Name {get;set}
public string FromAnotherTable {get;set;}
}
And here is the mapping I have.
public class MyEntityMapping : EntityTypeConfiguration<MyEntity>
{
public MyEntityMapping()
{
this.Property(e => e.Id).HasColumnName("ThePrimaryKeyId");
this.Property(e => e.Name).HasColumnName("MyDatabaseName");
this.Property(e => e.FromAnothertable).HasColumnName("AnotherTableColumn");
this.Map(m =>
{
m.Properties(e =>
{
e.Id,
e.Name
});
m.ToTable("MainTable");
});
this.Map(m =>
{
m.Properties(e =>
{
e.Id,
e.FromAnotherTable
});
m.ToTable("ExtendedTable");
});
}
Since the key shared between them has a different column name, I'm not sure how to map it. This mapping will compile, but fails at runtime because EF emits SQL looking for the "ThePrimaryKeyId" column on the "ExtendedTable" table, which doesn't exist.
EDIT
To clarify, what I have defined above can (and does) work if the PK on the "ExtendedTable" followed naming conventions. But it doesn't and I can't change the schema.
Basically, what I need EF to emit is a SQL statement like
SELECT
[e1].*, /*yes, wildcards are bad. doing it here for brevity*/
[e2].*
FROM [MainTable] AS [e1]
INNER JOIN [ExtendedTable] AS [e2] /*Could be left join, don't care. */
ON [e1].[ThePrimaryKeyId] = [e2].[NotTheSameName]
But the only thing it seems to want to emit is
SELECT
[e1].*,
[e2].*
FROM [MainTable] AS [e1]
INNER JOIN [ExtendedTable] AS [e2]
ON [e1].[ThePrimaryKeyId] = [e2].[ThePrimaryKeyId] /* this column doesn't exist */
Edit
I tried the 1-to-1 approach again at NSGaga's suggestion. It didn't work, but here are the results.
Entities
public class MyEntity
{
public int Id { get; set; }
public int Name { get; set; }
public virtual ExtEntity ExtendedProperties { get; set; }
}
public class ExtEntity
{
public int Id { get; set; }
public string AnotherTableColumn { get; set; }
public virtual MyEntity MainEntry { get; set; }
}
Here are the mapping classes
public class MyEntityMapping : EntityTypeConfiguration<MyEntity>
{
public MyEntityMapping()
{
this.Property(e => e.Id).HasColumnName("ThePrimaryKeyId");
this.Property(e => e.Name).HasColumnName("MyDatabaseName");
this.ToTable("MainTable");
this.HasKey(e => e.Id);
this.HasRequired(e => e.ExtendedProperties).WithRequiredPrincipal(f => f.MainEntry);
}
}
public class ExtEntityMapping : EntityTypeConfiguration<ExtEntity>
{
public ExtEntityMapping()
{
this.Property(e => e.Id).HasColumnName("NotTheSameName");
this.Property(e => e.AnotherTableColumn).HasColumnName("AnotherTableColumn");
this.ToTable("ExtendedTable");
this.HasKey(e => e.Id);
this.HasRequired(e => e.MainEntry).WithRequiredDependent(f => f.ExtendedProperties);
}
}
This setup gets the message
"Column or attribute 'MyEntity_ThePrimaryKeyId' is not defined in 'ExtendedTable'"
Changing the final map line to
this.HasRequired(e => e.MainEntry).WithRequiredDependent(f => f.ExtendedProperties).Map(m => M.MapKey("NotTheSameName"));
Returns this message
"Each property name in a type must be unique. property name 'NotTheSameName' was already defined."
Changing the mapped key to use the column from the parent table, MapKey("ThePrimaryKeyId"). returns this message
"Column or attribute 'ThePrimaryKeyId' is not defined in 'ExtendedTable'"
Removing the Id property from the ExtEntity class throws an error because then the entity doesn't have a defined key.
I have been working on this very issue for a few days, what I finally did was to set the column name of the Id field within the context of the mapping fragment. This way you can give the Id (or the foreign key dependent on the Id) a different name from the Id of the main table.
this.Map(m =>
{
m.Property(p => p.Id).HasColumnName("NotTheSameName");
m.Properties(e =>
{
e.Id,
e.FromAnotherTable
});
m.ToTable("ExtendedTable");
});
If you run and debug this, you would find that it would give you something like what you want:
[e1].[ThePrimaryKeyId] = [e2].[NotTheSameName]
I can't find anything that specifically states that the name of the column has to be the same in both tables; but neither can I find anything that says it doesn't, or explains how you would map that scenario. Every example I can find has the key with the same name in both tables. It looks to me like this is a hole in the DbContext design.
Move the HasColumnName to within the mapping:
this.Property(e => e.FromAnothertable).HasColumnName("AnotherTableColumn");
this.Map(m =>
{
m.Properties(e => new
{
e.Id,
e.Name
});
m.Property(e => e.Id).HasColumnName("ThePrimaryKeyId");
m.Property(e => e.Name).HasColumnName("MyDatabaseName");
m.Property(e => e.Id).HasColumnName("ThePrimaryKeyId");
m.ToTable("MainTable");
});
this.Map(m =>
{
m.Properties(e => new
{
e.Id,
e.FromAnotherTable
});
m.ToTable("ExtendedTable");
});
}
No Visual Studio here, but try this with the 1-to-1 approach:
this.HasRequired(e => e.ExtendedProperties).HasConstraint((e, m) => e.Id == m.Id);
Update:
Here are some links that might help (could not find a real reference link)
How to declare one to one relationship using Entity Framework 4 Code First (POCO)
Entity Framework 4 CTP 4 Code First: how to work with unconventional primary and foreign key names
And just to provide (as I promised) a 1-to-1 (two entities, two tables) mapping, for what it's worth.
Here is what works for me and should in your case...
public class MainTable
{
public int ThePrimaryKeyId { get; set; }
public string Name { get; set; }
}
public class ExtendedTable
{
public int NotTheSameNameID { get; set; }
public string AnotherTableColumn { get; set; }
public MainTable MainEntry { get; set; }
}
public class MainDbContext : DbContext
{
public DbSet<MainTable> MainEntries { get; set; }
public DbSet<ExtendedTable> ExtendedEntries { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<MainTable>()
.HasKey(x => new { x.ThePrimaryKeyId });
modelBuilder.Entity<ExtendedTable>()
.HasKey(x => new { x.NotTheSameNameID });
// Extended To Main 1 on 1
modelBuilder.Entity<ExtendedTable>()
.HasRequired(i => i.MainEntry)
.WithRequiredDependent();
}
}
...and a test code something like...
using (var db = new UserDbContext())
{
foreach (var userid in Enumerable.Range(1, 100))
{
var main = new MainTable { Name = "Main" + userid };
db.MainEntries.Add(main);
var extended = new ExtendedTable { AnotherTableColumn = "Extended" + userid, MainEntry = main };
db.ExtendedEntries.Add(extended);
}
int recordsAffected = db.SaveChanges();
foreach (var main in db.MainEntries)
Console.WriteLine("{0}, {1}", main.Name, main.ThePrimaryKeyId);
foreach (var extended in db.ExtendedEntries)
Console.WriteLine("{0}, {1}, {2}, {3}", extended.AnotherTableColumn, extended.NotTheSameNameID, extended.MainEntry.Name, extended.MainEntry.ThePrimaryKeyId);
}
That creates the following SQL script, tables...
CREATE TABLE [MainTables] (
[ThePrimaryKeyId] [int] NOT NULL IDENTITY,
[Name] [nvarchar](4000),
CONSTRAINT [PK_MainTables] PRIMARY KEY ([ThePrimaryKeyId])
)
CREATE TABLE [ExtendedTables] (
[NotTheSameNameID] [int] NOT NULL,
[AnotherTableColumn] [nvarchar](4000),
CONSTRAINT [PK_ExtendedTables] PRIMARY KEY ([NotTheSameNameID])
)
CREATE INDEX [IX_NotTheSameNameID] ON [ExtendedTables]([NotTheSameNameID])
ALTER TABLE [ExtendedTables] ADD CONSTRAINT [FK_ExtendedTables_MainTables_NotTheSameNameID] FOREIGN KEY ([NotTheSameNameID]) REFERENCES [MainTables] ([ThePrimaryKeyId])
And a note, as per our discussion above...
This ain't the 'splitting' - but
(a) code first IMO doesn't allow anything like that (I tried that first and also modifying the migrations manually but it's 'internally' all based on the expected column names being the same and there seems to be no way around it, for this version of EF at least.
(b) table structure wise - the tables could be made to look exactly what you need (as I said before I used it to relate the existing aspnet membership tables (which I could not change) into my user-table which has an own user-id pointing to outside/aspnet table and id.
True, you cannot make it using one C# model class - but the C# side is much more flexible and if you can control the C# that should give the same effect, to my opinion at least (like in the test, you can access it always through the extended entity, both extended and the main columns and they're always matched 1 to 1 and stay 'in sync'.
Hope this helps some
NOTE: you don't have to worry about the fk id etc. - just always access and add the Main entry via MainEntry, and id-s will be fine.
EDIT:
You could also do the following, to gain the appearance of having to deal with just one class (i.e. sort of a split)
public class ExtendedTable
{
public int NotTheSameNameID { get; set; }
public string AnotherTableColumn { get; set; }
public string Name { get { return MainEntry.Name; } set { MainEntry.Name = value; } }
// public int MainID { get { return MainEntry.ThePrimaryKeyId; } set { MainEntry.ThePrimaryKeyId = value; } }
internal MainTable MainEntry { get; set; }
public ExtendedTable()
{
this.MainEntry = new MainTable();
}
}
...and use it like this...
var extended = new ExtendedTable { AnotherTableColumn = "Extended" + userid, Name = "Main" + userid };
...also you can revert the direction of the fk by doing the WithRequiredPrincipal instead of dependent.
(also all references have to be w/o 'virtual' if you have required one-to-one)
(and MainTable can be made 'internal' as it's here, so it's not visible from outside - it cannot be nested as that EF doesn't allow - is treated like NotMapped)
...well, that's the best I could do:)
I would like to suggest using some data annotations like this:
MainTable
---------
MainTableId
DatabaseName
ExtendedTable
----------
NotTheSameName
AnotherColumn
public class MainTable
{
[Key]
public int MainTableId { get; set; }
public string DatabaseName { get; set; }
[InverseProperty("MainTable")]
public virtual ExtendedTable ExtendedTable { get; set; }
}
public class ExtendedTable
{
[Key]
public int NotTheSameName { get; set; }
public string AnotherColumn { get; set; }
[ForeignKey("NotTheSameName")]
public virtual MainTable MainTable { get; set; }
}
Looks like it's been fixed in Entity Framework 6. See this issue http://entityframework.codeplex.com/workitem/388
I faced this issue, and solved by add Column attribute to match the both column names.
[Key]
[Column("Id")]
public int GroupId { get; set; }