This concerns a c# EF Core 6 CRUD API in a data-first application.
We have a series of tables managed by the API and some separate, related views that give additional user-friendly information concerning the table entries.
An extremely simple example is:
//============================================
public partial class AuditLog : RecordBase
{
public AuditLog()
{
datestampName = "timeOfChange";
isAudited = false;
includeGlobalRecords = false;
}
public long auditLogID { get; set; }
public string tableName { get; set; }
public string primaryKeyValue { get; set; }
public DateTime? timeOfChange { get; set; }
public DateTime timeChangeApplied { get; set; }
public string action { get; set; }
public long? personID { get; set; }
public string ipAddress { get; set; }
public string changesJson { get; set; }
}
public partial class AuditLogList : AuditLog
{
public string personName { get; set; }
}
The original run of "Ef Core Power Tools / Reverse Engineer" created completely separate classes for these, which I then put back to the inherited form I used with nPoco.
The tool also created entries for these in OnModelCreating() like
//=====================
{
...
modelBuilder.Entity<AuditLog>(entity =>
{
entity.HasIndex(e => new { e.timeOfChange, e.tableName, e.primaryKeyValue }, "x1_AuditLog");
entity.HasIndex(e => e.tableName, "x2_AuditLog");
and
modelBuilder.Entity<AuditLogDetailList>(entity =>
{
entity.HasNoKey();
entity.ToView("AuditLogDetailList");
This failed with the message that because the ...List class inherited from the original, it needed to have an active Key. So I removed the HasNoKey().
I then get an error (apparently on another Building pass)
Both 'AuditLogList' and 'AuditLog' are mapped to the table 'AuditLog'.
All the entity types in a hierarchy that don't have a discriminator
must be mapped to different tables. See
https://go.microsoft.com/fwlink/?linkid=2130430 for more information.
Which seems to indicate that EF is forcing the class inheritance onto the database, ignoring the .ToView().
The link describes ways to share fields within tables and links between tables, but not a successful way to dissociate the inheritance from the EF definition.
Is there a way to do this?
Related
I am working on a project in Entity Framework Core which uses POCOs to connect to a database. However, these POCOs cannot convey intent, which is necessary because of a role-based permission system. I need some way to convey roles alongside my POCO.
However, some of the requirements of the project make it difficult to use subclasses with additional fields to do this. Thus, I have been using interfaces and partial classes, as under normal circumstances, I can use an interface to control which parts of the class are being recognized.
For example, in following code I have a guest entity, with the role of who is editing the entity. Only the fields Id, Name, and Confirmed exist as columns within the database. Under most circumstances, I can cast to an IGuest to remove the "MyRole" field.
public partial class Guest : IGuest
{
public int Id { get; set; }
public string Name { get; set; }
public bool Confirmed { get; set; }
}
public partial class Guest : IRole
{
public string MyRole { get; set; }
}
public interface IGuest
{
public int Id { get; set; }
public string Name { get; set; }
bool Confirmed { get; set; }
}
public interface IRole
{
public string MyRole { get; set; }
}
However, if I try to create a DbSet of guests, it attempts to find a matching column for MyRole. I tried changing the following sets of code
public virtual DbSet<Guest> Guest { get; set; } = null!;
and
modelBuilder.Entity<Guest>(entity =>
{
entity.HasKey(e => e.Id);
});
into the following:
public virtual DbSet<IGuest> Guest { get; set; } = null!;
and
modelBuilder.Entity<IGuest>(entity =>
{
entity.HasKey(e => e.Id);
});
This results in an error
ArgumentException: The specified type '...IGuest' must be a non-interface reference type to be used as an entity type.
Is it possible to use an interface as part of a DbSet / entity to hide the MyRole field (and any others not in the IGuest interface) from Entity Framework so that it does not attempt to map it to a column?
I am using the table per hierarchy approach for achieving inheritance in entity types. I have 3 classes defined:
Room - Base class
SubMapRoom - Inherits from Room
OverviewRoom - Inherits from Room
In the DB, I just have 1 table called Room that has both the SubMapRoom and OverviewRoom columns in it. It also contains the Discriminator column for specifying which type it is.
First, I attempted to move all of the SubMapRoom columns in the Room class into the SubMapRoom class. 1 of the columns contains a foreign key to a different table called Status. After doing this, I tried specifying the foreign key relationship for the SubMapRoom entity type in OnModelCreating(). However, I get a compile error when I try to do this. In the EF Core OnModelCreating() method, I have this code (marked the line that contains the error below):
modelBuilder.Entity<SubMapRoom>(entity =>
{
entity.HasOne(d => d.UnassignedDoctorStatus)
.WithMany(p => p.Room) **ERROR HAPPENS HERE**
.HasForeignKey(d => d.UnassignedDoctorStatusId)
.OnDelete(DeleteBehavior.Cascade)
.HasConstraintName("FK_Room_UnassignedStatusID");
});
modelBuilder.Entity<Room>()
.HasDiscriminator<int>("RoomType")
.HasValue<SubMapRoom>(1)
.HasValue<OverviewRoom>(2);
I get this error:
Cannot convert lambda expression to intended delegate type because some of the return types in the block are not implicitly convertible to the delegate return type
I know that I can solve this by changing the other class (Status) to use the inherited type instead of the base type for the navigation property, but that seems like the wrong way to go. I feel like I am missing something here. What would be the correct way to define a foreign key relationship in an inherited entity type?
[EDIT]
Here are the classes for the 4 models I have referenced here:
public abstract class Room
{
public Room()
{
InverseLinkedRoom = new HashSet<Room>();
}
public int Id { get; set; }
public int SubMapId { get; set; }
public string MapLabel { get; set; }
public string RoomLabel { get; set; }
public int LeftCoordinate { get; set; }
public int TopCoordinate { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public int? LinkedRoomId { get; set; }
public int RoomType { get; set; }
public Room LinkedRoom { get; set; }
public SubMap SubMap { get; set; }
public PatientQueue PatientQueue { get; set; }
public ICollection<Room> InverseLinkedRoom { get; set; }
}
public class SubMapRoom : Room
{
public int? UnassignedDoctorStatusId { get; set; }
public Status UnassignedDoctorStatus { get; set; }
}
// Note: Have not yet attempted to move base class members in here
public class OverviewRoom : Room
{
}
public partial class Status
{
public Status()
{
Room = new HashSet<Room>();
}
public int Id { get; set; }
public string EnumId { get; set; }
public bool Active { get; set; }
public bool IsFastBlink { get; set; }
public ICollection<Room> Room { get; set; }
}
Thanks for the help everyone. I reviewed my DB schema and decided to make some changes that make this problem go away. It actually turns out that my new schema is easier to use in the code than I originally thought. In fact, it's a lot easier. I think I was trying to overengineer this. So sometimes, the solution is to review your schema and figure out if it even makes sense in the first place. Basically, what I did was move the inherited classes to a separate table with a separate ID. Because, at the end of the day, they are logically separate types of entities and only related in terms of the data. In the code, they serve much different purposes even though they share some of the same columns.
At the end of it all, the only disadvantage of this approach is that I am violating DRY on another table (there are 5 repeated columns in it). Otherwise, a lot of other operations are easier to code than before. I am willing to live with that instead of dealing with all of this for now. Later, I can try to use Table Per Hierarchy if I am having to add tons of new columns to both tables.
I apologize on the title as I'm not sure how to summarize the issue we are having in one sentence.
We have two solutions, one of our old code base, another with our latest and greatest, with both solutions sharing a project of code first EF models. The old solution also has another project of older EF models and its own database context. The old code worked with this blend of old and new until I added new classes that inherited an EF one. In our new system, CRUD operations work as expected with the sub classes, but on our old system we get this error:
The entity types 'DiscountActivity' and 'DiscountSeries' cannot share table 'Discounts' because they are not in the same type hierarchy or do not have a valid one to one foreign key relationship with matching primary keys between them.
Here are the classes in question:
[Table("Discounts")]
public class Discount
{
[Column("Id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Column("Type")]
public string Type { get; set; }
[Column("Amount")]
public double Amount { get; set; }
}
public class DiscountActivity : Discount
{
[Column("TypeId")]
public int ActivityId { get; set; }
public virtual Activity Activity { get; set; }
public DiscountActivity()
{
Type = "Activity";
}
}
public class DiscountSeries : Discount
{
[Column("TypeId")]
public int SeriesId { get; set; }
public virtual Series Series { get; set; }
public DiscountSeries()
{
Type = "Series";
}
}
[Table("Activities")]
public class Activity
{
[Column("Id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Column("Name")]
public string Name { get; set; }
public virtual ICollection<DiscountActivity> Discounts { get; set; }
}
[Table("Series")]
public class Series
{
[Column("Id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Column("Name")]
public string Name { get; set; }
public virtual ICollection<DiscountSeries> Discounts { get; set; }
}
If I comment out both ICollection lines in Activity and Series, the error no longer occurs. If I leave one of the two, does not matter which, the error still does not occur. Also, it does not seem to matter if the new or old EF database context is used for the LINQ statements when the ICollection objects are both present, the same error is thrown. So to be clear, when running a LINQ statement on the old database context, we get the above error even though models and collections are in a separate database context.
Here is an example in our old API using the old context:
public class ContractsController : ApiController
{
private readonly DatabaseContext _db = new DatabaseContext();
[ResponseType(typeof(Contract))]
public IHttpActionResult Get(int id)
{
var model = _db.Contracts.Find(id);
if (model == null)
{
return NotFound();
}
return Ok(model);
}
}
Any input is appreciated!
I've just started using Entity Framework for my next project and I'm struggling with the following. I have the following ApplicationUser class:
public class ApplicationUser : IdentityUser
{
public string Name { get; set; }
public int CompanyId { get; set; }
public virtual Company Company { get; set; }
}
I have two classes that inherent from this class:
public class TrainerUser : ApplicationUser
{
public virtual ICollection<ClientUser> Clients { get; set; }
}
public class ClientUser : ApplicationUser
{
public string TrainerId { get; set; }
public TrainerUser Trainer { get; set; }
}
The company class looks like this:
public class Company
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<TrainerUser> Trainers { get; set; }
public virtual ICollection<ClientUser> Clients { get; set; }
}
What I can't figure out is how I can use the fluent API to not include 3 different companyId columns in the ApplicationUsers table.
Currently I have the following fluent API configuration:
modelBuilder.Entity<TrainerUser>().HasRequired(c => c.Company).WithMany(t => t.Trainers).HasForeignKey(c => c.CompanyId);
modelBuilder.Entity<ClientUser>().HasRequired(c => c.Company).WithMany(c => c.Clients).HasForeignKey(c => c.CompanyId);
Can anyone point me in the right direction?
Try adding these to your code.
modelBuilder.Entity<TrainerUser>().ToTable("TrainerUser");
modelBuilder.Entity<ClientUser>().ToTable("ClientUser");
If I am getting you right. you are trying to create a structure representing Table Per Hierarchy (TPT). Read more about it at the link.
Basically what happens is when entity framework encounters inheritance in the entities. Its Default attempt to create tables is by creating column of the set of all properties of all the derived entities from a class with a discriminator column.
What you are trying to create is a separate table for every class in the hierarchy.
I'm an EF noob (any version) and my Google-foo has failed me on finding out how to do this. Which makes me think I must be doing this wrong, but here is the situation:
I'm definitely in an environment that is database first and the schema won't be updated by us coders. I'm also not a fan of 'automatic' code generation, so I've stayed away from the designer or the EF powertools (though I did run through them just to see them work).
To learn I imported the Northwind DB into my LocalDB to have something to play with while creating some simple Web API 2 endpoints. This all went well as I created slimmed down models of the Employees, Shippers, & Region tables in Northwind. Region was particularly interesting as it wasn't plural and EF had issues with that. Anyway, I got by that.
My trouble now is; I want to use a view instead of a table as my source and whatever I'm doing just doesn't seem to work. What I tried was setting it up just like I did the tables. But that produces a ModelValidationException error. I tried looking at the auto-generated code from the designer, but got no insight.
My models:
//-- employee, shipper, & region work as expected
public class employee {
public int EmployeeID { get; set; }
public string LastName { get; set; }
public string FirstName { get; set; }
}
public class shipper {
public int ShipperID { get; set; }
public string CompanyName { get; set; }
public string Phone { get; set; }
}
public class region {
public int RegionID { get; set; }
public string RegionDescription { get; set; }
}
//-- invoice is a view (actual viewname is 'Invoices')
//-- so i followed the same rules as i did for employee & shipper
//-- i have tried uppercase 'I' as well as a plural version of the model
public class invoice {
public string CustomerID { get; set; }
public string CustomerName { get; set; }
public string Salesperson { get; set; }
public int OrderID { get; set; }
public int ProductID { get; set; }
public string ProductName { get; set; }
}
My Context looks like this:
public class NorthwindDBContext : DbContext {
public DbSet<Employee> Employees { get; set; }
public DbSet<shipper> Shippers { get; set; }
public DbSet<region> Regions { get; set; }
public DbSet<Invoice> Invoices { get; set; } //-- offending line of code
protected override void OnModelCreating(DbModelBuilder modelBuilder) {
//--- fix for Region being singular instead of plural
modelBuilder.Entity<region>().ToTable("Region");
}
}
If I comment out the public DbSet<Invoice> Invoices { get; set; } line in the context everything works. Just by having the line present (even if i don't reference the Invoices property) I receive the ModelValidationException error when using the context in anyway.
Can anybody tell me what I'm doing wrong here?
Thanks.
Update: I tried this in one of my controllers, but I am too noob'ish to know if this is the right path either, though it worked as far as getting records.
using (var dbContext = new NorthwindDBContext()) {
return dbContext.Database.SqlQuery<Invoice>("select * from invoices").ToList();
}
Code-first conventions will look for an ID or InvoiceID property to use as a key. Your Invoice model has neither, while the others do. This is the specific reason your code is failing.
The less-specific one is that you can't have entities in EF which lack a unique key. If you can, have the view define a key. Otherwise, you may still be able to work around the issue.