Entity Framework Many To Many exception with inheritance - entity-framework

I am trying to create two many-to-many relationship maps on a Record object:
Record object that is inherited from
public class Record {
public virtual ICollection<Language> SourceLanguages { get; set; }
public virtual ICollection<Language> TargetLanguages { get; set; }
}
Second Object
public class Language
{
public int Language { get; set; }
public string Locale { get; set; }
public string LanguageName { get; set; }
public virtual ICollection<Record> Records { get; set; }
}
Map for Record
public class RecordMap : EntityTypeConfiguration<Record>
{
this.HasMany(r => r.SourceLanguages)
.WithMany(c => c.Records)
.Map(sl =>
{
sl.ToTable("SourceLanguageRecordMap", "dbo");
sl.MapLeftKey("RecordId");
sl.MapRightKey("LanguageId");
});
this.HasMany(r => r.TargetLanguages)
.WithMany(c => c.Records)
.Map(tl =>
{
tl.ToTable("TargetLanguageRecordMap", "dbo");
tl.MapLeftKey("RecordId");
tl.MapRightKey("LanguageId");
});
}
When I run migration on the object listed above I get the following error:
System.Data.Entity.Core.MetadataException: Schema specified is not
valid. Errors: The relationship
'Toolbox.EntityModel.Contexts.Record_SourceLanguages' was not loaded
because the type 'Toolbox.EntityModel.Contexts.Language' is not
available. ...
Schema specified is not valid. Errors: The relationship
'Toolbox.EntityModel.Contexts.Record_SourceLanguages' was not loaded
because the type 'Toolbox.EntityModel.Contexts.Language' is not
available.
If I comment the following line out, it will work with just one many to many map, however, it will add RecordId_Record to Language Table. Any idea why?
this.HasMany(r => r.TargetLanguages)
.WithMany(c => c.Records)
.Map(tl =>
{
tl.ToTable("TargetLanguageRecordMap", "dbo");
tl.MapLeftKey("RecordId");
tl.MapRightKey("LanguageId");
});
Any idea as to what I am doing wrong?

If you have 2 Many-to-Many relationships to the same table you need to create 2 separate ICollection properties in order for Entity Framework to fully pick up on what you're trying to do. You can't combine them into one, or else you'll get that lovely error that you're seeing there.

Related

Entity Framework Core 5.0 - Many to many select query

I am trying to get a single User, with a list of Items, mapped with a many-to-many entity UserItems. However, I am unable to retrieve the mapped Items due to to an error that I'm unable to solve (error at bottom of question). Here is my code:
public class User
{
public int Id { get; set; }
public ICollection<UserItem> UserItems { get; set; }
}
public class Item
{
public int Id { get; set; }
public ICollection<UserItem> UserItems { get; set; }
}
public class UserItem
{
public int Id { get; set; }
public int UserId { get; set; }
public User User { get; set; }
public int ItemId { get; set; }
public Item Item { get; set; }
public int Quantity { get; set; }
}
The UserItem class configuration has the following relationships defined:
builder.HasOne(x => x.User)
.WithMany(x => x.UserItems)
.HasForeignKey(x => x.UserId)
.OnDelete(DeleteBehavior.ClientCascade);
builder.HasOne(x => x.Item)
.WithMany(x => x.UserItems)
.HasForeignKey(x => x.ItemId)
.OnDelete(DeleteBehavior.ClientCascade);
I have the following generic repo with this method:
public class GenericRepository<T> : where T : class
{
private readonly DbContext _context;
public GenericRepository(DbContext context) => _context = context;
public T Get(Expression<Func<T, bool>> where, params Expression<Func<T, object>>[] navigationProperties)
{
IQueryable<T> query = _context.Set<T>();
query = navigationProperties.Aggregate(query, (current, property) => current.Include(property));
var entity = query.FirstOrDefault(where);
return entity;
}
}
However, when I try to run the code, I get an error on the Select(x => x.Item):
var user = repo.Get(x => x.Id == 1, x => x.UserItems.Select(y => y.Item));
Error:
System.InvalidOperationException: 'The expression 'x.UserItems.AsQueryable().Select(y => y.Item)' is invalid inside an 'Include' operation, since it does not represent a property access: 't => t.MyProperty'. To target navigations declared on derived types, use casting ('t => ((Derived)t).MyProperty') or the 'as' operator ('t => (t as Derived).MyProperty'). Collection navigation access can be filtered by composing Where, OrderBy(Descending), ThenBy(Descending), Skip or Take operations. For more information on including related data, see http://go.microsoft.com/fwlink/?LinkID=746393.'
What am I doing wrong, this seems to work for my other projects?
This error Occurs because you are not passing in a navigation property (x.UserItems would be a navigation property) but rather something you want to do with the navigation property. UserItems.Select(y => y.Item) is not a property of x because Select() is a function and therefore it cannot be included.
What you are trying to do (I assume it is including UserItems and also the corresponding Items) is not going to work with your current implementation of the repository. To include navigation properties of navigation properties .ThenInclude() must be used instead of .Include() which works only for navigation properties directly defined on the Entity the DbSet is created for.
But apart from your question I would suggest not to use such an generic implementation of Repository. The main benefit from using reposiories is to separarte code related to loading and storing of entities from the rest of your code. In your case if the consumer of repository knows that navigation properties must be included and that he has to provide them - then what is the point of having a repository at all? Then the consumer again cares about database specific code which makes having a repository unneccessary. I would recommend just making a conrete "UserRepository" which can only be used to retrieve users and explicitly includes the needed properties.

Make reference table data read-only - EF Core

I have a table (Commodity) which has a one-to-one relationship with another table (CommodityMaterial), in my GET endpoint the Commodity returns it's own columns and also the columns (and values) of the referenced table which works perfectly. However, in the POST operation of the endpoint, a user should not be able to POST data of the reference table (CommodityMaterial), how can this be achieved? I used to disable this by using a DataContract, however, because I need the columns for my GET operator, this is not an option.
I already tried, following this post: https://csharp.christiannagel.com/2016/11/07/efcorefields/, removing the SET on the reference table and making a backing field but this does not seem to work (error that the backing field is read-only).
I also tried setting the SET to protected, but this is not working.
So the question is, how to make the reference table read-only (only available for my GET endpoint and not my POST endpoint).
The Commodity POCO class:
[DataContract]
public class Commodity
{
public Commodity()
{
}
public Commodity(CommodityMaterial commodityMaterial)
{
CommodityMaterial = commodityMaterial;
}
[DataMember]
public long CommodityID { get; set; }
[DataMember]
public long CommodityMaterialID { get; set; }
[DataMember]
public decimal? SpecficWeight { get; set; }
[DataMember]
public CommodityMaterial CommodityMaterial { get; }
}
Fluent part:
modelBuilder.Entity<Commodity>(entity =>
{
entity.Property(e => e.CommodityID)
.HasColumnName("CommodityID")
.ValueGeneratedOnAdd();
entity.Property(e => e.CommodityMaterialID)
.HasColumnName("CommodityMaterialID");
entity.Property(e => e.SpecficWeight)
.HasColumnName("SpecficWeight")
.HasColumnType("decimal(18, 2)");
entity.HasOne(a => a.CommodityMaterial)
.WithOne(b => b.Commodity)
.HasForeignKey<Commodity>(b => b.CommodityMaterialID);
});
The parameters your action accepts should represent what your action does/is allowed to do. If a client should not be able to update a related entity, then the class you bind the request body to, should not have that entity available. Use a view model, essentially:
public class CommodityRequest
{
// all properties you want editable
// exclude `CommodityMaterial` obviously
}
Then:
public IActionResult Update(CommodityRequest model)

InvalidOperationException: The entity type 'Enrollments' requires a primary key to be defined

I am new to Asp.Net Core (Even to Asp.Net and web). I am using Asp.Net Core 2 with MySQL, using Pomelo.EntityFrameWorkCore.MySql (2.0.1) driver. I just created a custom dbcontext with Courses and Enrollments table, along with the default created ApplicationDbContext. The Primary Key for Enrollments is a composite key, comprising of UserId and CourseId. Below is the code :
public class CustomDbContext : DbContext
{
public virtual DbSet<Courses> Courses { get; set; }
public virtual DbSet<Enrollments> Enrollments { get; set; }
public CustomDbContext(DbContextOptions<CustomDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Courses>(entity =>
{
entity.ToTable("courses");
entity.HasIndex(e => e.Name)
.HasName("Coursescol_UNIQUE")
.IsUnique();
entity.Property(e => e.Id).HasColumnType("int(11)");
entity.Property(e => e.Duration).HasColumnType("time");
entity.Property(e => e.Name).HasMaxLength(45);
});
modelBuilder.Entity<Enrollments>(entity =>
{
entity.HasKey(e => new { e.UserId, e.CourseId });
entity.ToTable("enrollments");
entity.HasIndex(e => e.CourseId)
.HasName("fk_Courses_Enrollments_CourseId_idx");
entity.HasIndex(e => e.UserId)
.HasName("fk_Users_Enrollments_CourseId_idx");
entity.HasIndex(e => new { e.UserId, e.CourseId })
.HasName("UniqueEnrollment")
.IsUnique();
entity.Property(e => e.CourseId).HasColumnType("int(11)");
entity.HasOne(d => d.Course)
.WithMany(p => p.Enrollments)
.HasForeignKey(d => d.CourseId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("fk_Courses_Enrollments_CourseId");
entity.HasOne(d => d.User)
.WithMany(p => p.Enrollments)
.HasForeignKey(d => d.UserId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("fk_Users_Enrollments_UserId");
});
}
}
The Program.cs goes like :
public static void Main(string[] args)
{
var host = BuildWebHost(args);
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
try
{
var context = services.GetRequiredService<CustomDbContext>();
context.Database.EnsureCreated();
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred while seeding the database.");
}
}
host.Run();
}
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();
}
The configure services method in Startup.cs goes like :
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseMySql(Configuration.GetConnectionString("DefaultConnection")));
services.AddDbContext<CustomDbContext>(options =>
options.UseMySql(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// Add application services.
services.AddTransient<IEmailSender, EmailSender>();
services.AddMvc();
}
The Courses Model goes like :
public partial class Courses
{
public Courses()
{
Enrollments = new HashSet<Enrollments>();
}
public int Id { get; set; }
public string Name { get; set; }
public TimeSpan? Duration { get; set; }
public ICollection<Enrollments> Enrollments { get; set; }
}
The Enrollments Model goes like :
public partial class Enrollments
{
public string UserId { get; set; }
public int CourseId { get; set; }
public Courses Course { get; set; }
public ApplicationUser User { get; set; }
}
The applicationUser model goes like :
public ApplicationUser()
{
Enrollments = new HashSet<Enrollments>();
}
public ICollection<Enrollments> Enrollments { get; set; }
Now, here's what I've tried so far :
If i add Course and Enrollment model to the ApplicationDBContext, then everything goes fine.
If in CustomDBContext i have a non-composite primary Key, even then it works fine. (I just tried another example)
Can somebody please throw some light on why is this error ? Is this the intended way to handle such a case ?
Thanks in advance.
It's because the Enrollments entity has been discovered by ApplicationDbContext through ApplicationUser.Enrollments navigation property. This is explained in the Including & Excluding Types - Conventions section of the EF Core documentation:
By convention, types that are exposed in DbSet properties on your context are included in your model. In addition, types that are mentioned in the OnModelCreating method are also included. Finally, any types that are found by recursively exploring the navigation properties of discovered types are also included in the model.
I guess now you see the problem. The Enrollments is discovered and included in the ApplicationDbContext, but there is no fluent configuration for that entity there, so EF uses only the default conventions and data annotations. And of course composite PK requires fluent configuration. And even there wasn't a composite PK, it's still incorrect to ignore the existing fluent configuration. Note that Courses is also included in the ApplicationDbContext by the aforementioned recursive process (through Enrollments.Courses navigation property). Etc. for other referenced classes.
Note that the same applies in the other direction. ApplicationUser and all referenced from it are discovered and included in the CustomDbContext w/o their fluent configuration.
The conclusion - don't use separate contexts containing interrelated entities. In your case, put all the entities in the ApplicationDBContext.

Many to Many between IdentityUser and other table in a separate context

I am having difficulty creating a join table relationship between my Identity Framework IdentityContext(the IdentityUser) and one of my other tables Let's call it Entry. The problem is, Entry is in an entirely separate context doing it's own thing as well.
What is the proper way to associate these two? Where do I define the Join Table in fluent api?
Right now, I am getting the following error.
The key {'ApplicationUserId'} contains properties in shadow state and is referenced by a relationship from 'ApplicationUser.ApplicationUserEntries' to 'ApplicationUserEntry.ApplicationUser'. Configure a non-shadow principal key for this relationship.
These are how my tables are defined.
public class ApplicationUser : IdentityUser
{
...
public virtual List<ApplicationUserEntry> ApplicationUserEntries { get; set; }
}
public class Entry
{
public int Id { get; set; }
...
public virtual List<ApplicationUserEntry> ApplicationUserEntries { get; set; }
}
And the join table as follows.
public class ApplicationUserEntry
{
public int ApplicationUserId { get; set; }
public ApplicationUser ApplicationUser { get; set; }
public int EntryId { get; set; }
public Entry Entry { get; set; }
}
For the IdentityContext I have just some generic setup for other properties
var users = modelBuilder.Entity<ApplicationUser>();
users.Property(u => u.Name).IsRequired().HasMaxLength(65);
users.Property(u => u.FirstName).HasMaxLength(32);
users.Property(u => u.LastName).HasMaxLength(32);
And in my GoalsContext I have some general setup for other unrelated stuff, and the join table defined for ApplicationUserEntry
// Entry Configuration
var entries = modelBuilder.Entity<Entry>();
entries.HasKey(e => e.Id);
entries.HasAlternateKey(e => new { e.MilestoneId, e.CategoryId, e.MetricId });
entries.Property(e => e.Value).IsRequired();
entries.Property(e => e.Locked).IsRequired().HasDefaultValue(false);
entries.ToTable("GoalsEntries");
// ApplicationUserEntry Join Table
modelBuilder.Entity<ApplicationUserEntry>()
.ToTable("GoalsApplicationUserEntry")
.HasKey(se => new { se.ApplicationUserId, se.EntryId });
modelBuilder.Entity<ApplicationUserEntry>()
.HasOne(se => se.ApplicationUser)
.WithMany(s => s.ApplicationUserEntries)
.HasForeignKey(se => se.ApplicationUserId);
modelBuilder.Entity<ApplicationUserEntry>()
.HasOne(se => se.Entry)
.WithMany(e => e.ApplicationUserEntries)
.HasForeignKey(se => se.EntryId);
Now I'm sure I'm obviously missing something but I can't figure out what. I've never attempted to create a many to many relationship between two tables that are defined in two different contexts... and not even sure if that's wise or not to do.
My ultimate goal is to be able to associate owners with Entry records, so they can only be modified by the owners, which I verify with Identity Framework.
Ideally I would just prefer a unidirectional relationship, so I can find the owner from the Entry, but I'm not intending to get a list of Entry by looking at the IdentityUser

I am creating a Table setting Maxlength = 5 for a column and its created with length = 1

I have the following table being created (by using DNX commands in EF7, now EF Core)
[Table("FishGrade")]
public partial class FishGrade
{
public FishGrade()
{
FishPrices = new HashSet<FishPrice>();
}
[HiddenInput]
[Column("FishGradeId")]
public int Id { get; set; }
[Column("GradeCode")]
[MaxLength(5), Required]
public string Code { get; set; }
[Column("GradeName")]
public string Name { get; set; }
public string IsActive { get; set; }
public virtual ICollection<FishPrice> FishPrices { get; set; }
}
But when it the table is created, the Code column (column named=GradeCode), is created as a 1 character long column.
I also have the following in the OnModelCreating method
modelBuilder.Entity<FishGrade>(entity =>
{
entity.Property(e => e.Code)
.HasColumnType("char");
entity.Property(e => e.Name)
.IsRequired()
.HasMaxLength(50);
entity.Property(e => e.IsActive)
.HasMaxLength(1)
.HasColumnType("char");
});
Why is this happening? How can I get the column to be created with length = 5?
You can define your model in three ways:
conventions
attributes
fluent API
Conventions are applied first, then attributes, and finally the fluent API in your model builder. Your model builder is resetting the attribute configuration.
You should try to simplify your model configuration and use always the same method.
NOTE: take into account that, if you use something like MVC client side validation, it only understands the configuration made via attributes. In all other regards it doesn't mind how you configure your EF model.