EF Core: Circular entity reference - c#

I would say that it takes a lot of time to get to know ASP.NET Core to understand how to achieve things then previous versions with webforms, but I understand that ASP.NET Core is bigger and you are able to build more complex solutions.
I'm quite new to ASP.NET Core and I'm trying to understand EF Core and related data. I'm using https://learn.microsoft.com/en-us/aspnet/core/data/ef-mvc/intro to learn the basics and create my first ASP.NET Core application.
I have a Entity "Standard" that can have multiple Forms (Form entity). The entities share a couple of same properties so I've made them both inherit from a master class called MasterDocument. Previously called Document.
Standard:
namespace Skjemabasen.Models.Document
{
public class Standard : MasterDocument
{
[Display(Name = "Kategori")]
public virtual Category Category { get; set; }
[Display(Name = "Dokumenter")]
public ICollection<Form> Forms { get; set; }
}
}
Form:
public class Form : MasterDocument
{
public Format Format { get; set; }
public virtual Member Assignee { get; set; }
public String Status { get; set; }
[ForeignKey("Standard")]
public int StandardId { get; set; }
public Standard Standard { get; set; }
public ICollection<Customer.Subscription> Subscribers { get; set; }
}
MasterDocument:
namespace Skjemabasen.Models.Document
{
public class MasterDocument : IDocument
{
public int ID { get; set; }
[Required]
[Display(Name = "EStandard")]
[StringLength(50)]
public string EStandard { get; set; }
[Required]
[Column("Betegnelse")]
[Display(Name = "Betegnelse")]
[StringLength(60)]
public string Betegnelse { get; set; }
[Display(Name = "Kommentar")]
public string Comment { get; set; }
}
}
I understand that this can cause circular request or circular deletion so I inserted a DeleteBehavior.Restrict on Standard:
modelBuilder.Entity<Standard>()
.HasOne(d => d.Forms)
.WithMany()
.OnDelete(DeleteBehavior.Restrict);
My complete context class:
namespace Skjemabasen.Data
{
public class SkjemabasenContext : DbContext
{
public SkjemabasenContext(DbContextOptions<SkjemabasenContext> options) :base(options)
{
}
public DbSet<Member> Members { get; set; }
public DbSet<Category> Categories { get; set; }
public DbSet<Standard> Standards { get; set; }
public DbSet<Form> Forms { get; set; }
public DbSet<Customer> Customers { get; set; }
public DbSet<Revision> Revisions { get; set; }
public DbSet<Subscription> Subscriptions { get; set; }
public DbSet<MasterDocument> Documents { get; set; }
public IQueryable<Customer> CurrentCustomers
{
get { return Customers.Where(c => c.Inactive == false); }
}
public IQueryable<Customer> InActiveCustomers
{
get { return Customers.Where(c => c.Inactive == true); }
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Member>().ToTable("Member");
modelBuilder.Entity<Category>().ToTable("Category");
modelBuilder.Entity<Standard>().ToTable("Standard");
modelBuilder.Entity<Form>().ToTable("Form");
modelBuilder.Entity<Customer>().ToTable("Customer");
modelBuilder.Entity<Revision>().ToTable("Revision");
modelBuilder.Entity<Subscription>().ToTable("Subscription");
modelBuilder.Entity<MasterDocument>().ToTable("Document");
modelBuilder.Entity<Standard>()
.HasOne(d => d.Forms)
.WithMany()
.OnDelete(DeleteBehavior.Restrict);
}
}
}
When I try to run the application I get the error:
System.ArgumentException: 'The entity type
'System.Collections.Generic.ICollection`1[Skjemabasen.Models.Document.Form]'
provided for the argument 'clrType' must be a reference type.' Because
all Forms must have a parent Standard and both 'Standard' and 'Form'
inherits from MasterDocument, I understand that ASP.NET Core warns
about circular deletion, but I'm not sure how to achieve this. The
error says something about ICollection of 'Forms' not being a
reference type. Is something missing in 'Standard' related to the
relation between and 'Form'.
Based on https://learn.microsoft.com/en-us/aspnet/core/data/ef-mvc/intro I can't figure out what I'm missing here.

I'm assuming you don't actually want to have polymorphic entities by inheriting from MasterDocument. So, from what I see, you want Form and Standard to share the same properties of MasterDocument while MasterDocument being itself an Entity. If that's the case, just abstract away those properties to a base class:
public abstract class MasterBaseDocument
{
public int ID { get; set; }
[Required]
[Display(Name = "EStandard")]
[StringLength(50)]
public string EStandard { get; set; }
[Required]
[Column("Betegnelse")]
[Display(Name = "Betegnelse")]
[StringLength(60)]
public string Betegnelse { get; set; }
[Display(Name = "Kommentar")]
public string Comment { get; set; }
}
public class Form : MasterBaseDocument
{
...
}
public class Standard : MasterBaseDocument
{
...
}
public class MasterDocument : MasterBaseDocument
{
// right now, empty here...
}
That should fix it.
Another approach to your model would be to have a MasterDocument FK on Forms and Standard. That way you don't get the duplicates fields on the tables.
Further improving: Also, keep in mind that you can achieve all those configurations you have using attributes with FluentAPI. This way your classes are keep and decouple from EF stuff. That just adds noise and makes it very hard to read. Should be examples on Fluent API on EF docs as well.

Related

Including multiple many-to-many collection navigation properties

Description
When attempting to eagerly .Include() Many-to-Many ICollection properties on an already nested ICollection, I receive a EntityCommandCompilationException with the innerException: System.NotSupportedException: APPLY joins are not supported.
Are there any (suitable) workarounds, or is this simply not supported by EF6/SQLite? If not supported, would migrating to EntityFramework.Core work?
If some of you can confirm that this is a limitation, I'd create an issue on the SQLite issue tracker
Quick Problem Synopsis
// Bare minimum model definitions - see below for a full implementation
public class Mode
{
[Key] public int Id { get; set; }
}
public class Type
{
[Key] public int Id { get; set; }
}
public class Preset
{
[Key] public string PresetId { get; set; }
/* Note: Both Types and Modes are Many-To-Many Entity Relationship configured
* using Join Table/Fluent API */
public ICollection<Type> Types { get; set; } = new List<Type>();
public ICollection<Mode> Modes { get; set; } = new List<Mode>();
}
public class Plugin
{
[Key] public int Id { get; set; }
public ICollection<Preset> Presets { get; set; } = new List<Preset>();
}
public void test ()
{
context.Plugins
.Include("Presets.Modes")
.Include("Presets.Types")
.Load();
}
// EntityCommandCompilationException An error occurred while preparing the command definition. See the inner exception for details.
// innerException System.NotSupportedException: APPLY joins are not supported
This is the basic code which fails. See below for the complete code and how the models are implemented.
In contrast, including just one of the two ICollection navigation properties works fine:
context.Plugins
.Include("Presets.Modes")
.Load();
Also, including more than one ICollection on the same level works fine:
context.Presets
.Include("Modes")
.Include("Types")
.Load();
Further Information, Environment
The problem only affects EntityFramework6
together with System.Data.SQLite.EF6. As far as I know EntityFramework.SqlServerCompact is not affected.
I am using EF6 Code-First, I have not tried EF6 Model-First or Database-First (and switching to either of those is not an applicable solution)
The problem only occurs with many-to-many collection navigation properties, and only if I attempt to load multiple ones on the same level
I have tried common alternatives, including Select(), which yields the same exception
I have not tried building manual joins or queries, as this would result in extreme code maintenance loss in my project
At this point, I believe the root cause is a limitation of the SQLite EF6 implementation (which doesn't seem to be documented) rather than something I could fix easily with code
I've only found one similar StackOverflow question regarding multiple .Include(), and the (awful to use) SQLite bugtracker as well as the mailing lists don't seem to mention this particular issue
The full source code, including a test database with test data can be found in my GitHub EntityFrameworkResearch repository. Specifically the Model definitions, the DbContext and the repro code
Code Reference
public class Mode
{
[Key] public int Id { get; set; }
public ICollection<Plugin> Plugins { get; set; }
public ICollection<Preset> Presets { get; set; }
public string Name { get; set; }
}
public class Type
{
[Key] public int Id { get; set; }
public ICollection<Plugin> Plugins { get; set; } = new List<Plugin>();
public ICollection<Preset> Presets { get; set; } = new List<Preset>();
public string Name { get; set; }
public string SubTypeName { get; set; }
}
public class Plugin
{
[Key] public int Id { get; set; }
public ICollection<Preset> Presets { get; set; } = new List<Preset>();
public ICollection<Type> DefaultTypes { get; set; } = new List<Type>();
public ICollection<Mode> DefaultModes { get; set; } = new List<Mode>();
}
public class Preset
{
[Key] public string PresetId { get; set; } = Guid.NewGuid().ToString();
[ForeignKey("Plugin")] public int PluginId { get; set; }
public Plugin Plugin { get; set; }
public ICollection<Type> Types { get; set; } = new List<Type>();
public ICollection<Mode> Modes { get; set; } = new List<Mode>();
}
public class ApplicationDatabaseContext : DbContext
{
// Constructor and further database initialization omitted for clarity
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Plugin>()
.HasMany(p => p.DefaultModes)
.WithMany(q => q.Plugins)
.Map(mc => mc.MapLeftKey("PluginId").MapRightKey("ModeId").ToTable("PluginModes"));
modelBuilder.Entity<Plugin>()
.HasMany(p => p.DefaultTypes)
.WithMany(q => q.Plugins)
.Map(mc => mc.MapLeftKey("PluginId").MapRightKey("TypeId").ToTable("PluginTypes"));
modelBuilder.Entity<Preset>()
.HasMany(p => p.Types)
.WithMany(q => q.Presets)
.Map(mc =>mc.MapLeftKey("PresetId").MapRightKey("TypeId").ToTable("PresetTypes"));
modelBuilder.Entity<Preset>()
.HasMany(p => p.Modes)
.WithMany(q => q.Presets)
.Map(mc => mc.MapLeftKey("PresetId").MapRightKey("ModeId").ToTable("PresetModes"));
}
public DbSet<Plugin> Plugins { get; set; }
public DbSet<Preset> Presets { get; set; }
public DbSet<Mode> Modes { get; set; }
public DbSet<Type> Types { get; set; }
}

.Net Core API with EF Core Code First and IdentityUser

I have an existing application that I am re-writing as .NET Core API with a ReactJS front-end. I am still in the API end, and I've run into a problem.
CODE
I have a BbUser.cs entity class with the following code:
public class BbUser : IdentityUser
{
public int Points { get; set; } = 0;
public string DisplayUsername { get; set; }
}
And I also have an Artist.cs entity class:
public class Artist
{
public int Id { get; set; }
[Required]
[MaxLength(50)]
public string FirstName { get; set; }
[MaxLength(50)]
public string LastName { get; set; }
[MaxLength(100)]
public string UrlFriendly { get; set; }
public string ImageUrl { get; set; }
public bool IsVerified { get; set; }
public DateTime CreatedOn { get; set; }
public DateTime ModifiedOn { get; set; }
public ICollection<Lyric> Lyrics { get; set; } = new List<Lyric>();
public string UserId { get; set; }
public BbUser User { get; set; }
}
I need a one-to-many relationship between BbUser and Artist. One user can submit many artists and lyrics ...etc. Simple stuff really.
PROBLEM
The application builds fine, but when I attempt to run it by hitting a controller that requires access to the Database, I get the following error:
The entity type 'IdentityUserLogin' requires a primary key to be defined.
I had this issues with regular EF Code First (not Core) and the fix for that, does not work here.
This model worked for me(compiled, and no exceptions at runtime) if I used next code in the DbContext class:
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<BbUser>(b => b.ToTable("AspNetUsers"));
base.OnModelCreating(builder);
}
Without calling base.OnModelCreating(builder) I get the same error, because in this case context isn't applying the Identity related schema.
UPDATE:
Everything works fine for me as you can see from the screenshot below:
I have one more idea why you can have such an error. Did your BbContext inherit from DbContext class or IdentityDbContext<IdentityUser>? Because I got the same error that was on your screenshot if I used usual DbContext class.
In order to Idenity tables work fine you should use IdentityDbContext<IdentityUser>. Below the whole code for my working DbContext class
public class BbContext :IdentityDbContext<IdentityUser>
{
public BbContext(DbContextOptions options):base(options)
{
Database.EnsureCreated();
}
public DbSet<Artist> Artists { get; set; }
public DbSet<Lyric> Lyrics { get; set; }
public DbSet<Heart> Hearts { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<BbUser>(b => b.ToTable("AspNetUsers"));
builder.Entity<Heart>().HasKey(h => new {h.UserId, h.LyricId});
base.OnModelCreating(builder);
}
}
Please do try declaring a Guid property named Id, with both Get and Set on the IdentityUserLogin entity.
Another option is to declare a property and decorate it with [Key] attribute

Data annotations not create one-to-many object references

I'm missing something when using the data annotations.
This is my first class
[Table("PriceFeed")]
public class PriceFeed : History
{
public PriceFeed()
{
this.Votes = new List<PriceVote>();
this.History = new List<PriceFeed__History>();
}
[Key]
public long Id { get; set; }
[ForeignKey("Store")]
public long Store_Id { get; set; }
[ForeignKey("Item")]
public long Item_Id { get; set; }
[Required]
public decimal Price { get; set; }
public Store Store { get; set; }
public Item Item { get; set; }
public virtual ICollection<PriceFeed__History> History { get; set; }
}
And this is my second class
[Table("PriceFeed__History")]
public class PriceFeed__History : History
{
[Key]
public long Id { get; set; }
[ForeignKey("PriceFeed")]
public long PriceFeed_Id { get; set; }
[Required]
public decimal Price { get; set; }
public virtual PriceFeed PriceFeed { get; set; }
}
When I run the add-migration, it creates the database correctly but when I try to access PriceFeed.History it gives me an error
{"Message":"An error has occurred.","ExceptionMessage":"A specified Include path is not valid. The EntityType 'Verdinhas.Web.Contexts.PriceFeed' does not declare a navigation property with the name 'PriceFeed__History'."
I always worked with API Fluent and typed by myself the code like
.Entity<Student>()
.HasRequired<Standard>(s => s.Standard)
.WithMany(s => s.Students)
.HasForeignKey(s => s.StdId);
But now I'm using the data annotations and when I generate the migration, it does not create the "withmany" like the above.
What am I doing wrong?
The issue has nothing to do with Data Annotations which seems to be correct in your model.
As mentioned in the comments, the exception is caused by a code that tries to use Include method with string "'PriceFeed__History" - you seem to think that you should specify the related entity types, but in fact you need to specify the navigation property names, which in your case is "History".

EF5 Model-First, DBContext code generation and derived class

I'm creating a EF5 entity model with the designer (VS2012), and used the EF5 DbContext generator as code generation item.
My model contains an entity deriving from another (not abstract).
So let's say the base entity is called BaseEntity, and the derived entity is DerivedEntity.
Now I see in the generated context class, that there is no
Public DbSet<DerivedEntity> DerivedEntities { get; set; }
defined.
Only
Public DbSet<BaseEntity> BaseEntities { get; set; }
is defined.
Is this normal ? And if yes, how do I query the derived entities in linq ?
I'm used to query like this:
using(var ctx = new EntityContainer)
{
var q = from e in ctx.DerivedEntities <-- but this is now not possible since it doesn't exist
select e;
return q.ToList();
}
Thanks for replying.
EDIT:
As requested, generated classes posted:
public partial class Scheduling
{
public int Id { get; set; }
public string Subject { get; set; }
public System.DateTime BeginDate { get; set; }
public System.DateTime EndDate { get; set; }
}
public partial class TeamScheduling : Scheduling
{
public int TeamId { get; set; }
public Nullable<int> AssignmentId { get; set; }
public virtual Team Team { get; set; }
public virtual Assignment Assignment { get; set; }
}
public partial class EntityContainer : DbContext
{
public EntityContainer()
: base("name=EntityContainer")
{
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
throw new UnintentionalCodeFirstException();
}
public DbSet<Team> Teams { get; set; }
public DbSet<Location> Locations { get; set; }
public DbSet<Country> Countries { get; set; }
public DbSet<Assignment> Assignments { get; set; }
public DbSet<ProductType> ProductTypes { get; set; }
public DbSet<AssignmentPreference> AssignmentPreferences { get; set; }
public DbSet<Scheduling> Schedulings { get; set; }
}
As you see, the EntityContainer class does not contain
public DbSet<TeamScheduling> TeamSchedulings { get; set; }
This is expected when you use inheritance the way you have. context.Schedulings contains both Scheduling objects and TeamScheduling objects. You can get the TeamScheduling objects only by asking for context.Schedulings.OfType<TeamScheduling>(). Note that you cannot meaningfully use context.Schedulings.OfType<Scheduling>() to get the others: that will also include the TeamScheduling objects.
You could alternatively try context.Set<TeamScheduling>(), but I'm not entirely sure that will work.
If your intention is to have two tables come up, say a parent Scheduling entity as well as a child TeamScheduling entity that has a foreign key back to the Scheduling entity, consider using a Table-per-Type (TPT) mapping as discussed here.
In essence, you should modify your "OnModelCreating" method to have the following code:
modelBuilder.Entity<TeamScheduling>().ToTable("TeamScheduling");
This explicitly tells EF that you want to have the TeamScheduling subclass to be represented as its own table. Querying it via LINQ would be simple as you would be able to do something like the following:
var teamScheds = context.Set<TeamScheduling>().Where(s => s.Id == 1).FirstOrDefault();

Introducing FOREIGN KEY constraint 'FK_dbo.Models_dbo.Makes_MakeId' on table 'Models' may cause cycles or multiple cascade paths

This is my first day I've spent exploring ASP.NET MVC 4. Specifically I'm using the Web API and obviously this issue is actually an MS SQL issue. I'm running EF migrations PM> Update-Database to get this error, but have seen it when first creating the models. My models are:
public class Car
{
public int Id { get; set; }
public string Name { get; set; }
public int MakeId { get; set; }
public virtual Make Make { get; set; }
public int ModelId { get; set; }
public virtual Model Model { get; set; }
}
public class Make
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Model> Models { get; set; }
}
public class Model
{
public int Id { get; set; }
public string Name { get; set; }
public int MakeId { get; set; }
public virtual Make Make { get; set; }
}
The DB context is:
public class CarsContext : DbContext
{
public CarsContext() : base("name=CarsContext") { }
public DbSet<Car> Cars { get; set; }
public DbSet<Make> Makes { get; set; }
public DbSet<Model> Models { get; set; }
}
}
Would appreciate any help. My background is 5/6 solid of PHP and MySQL, so this is a steep learning curve.
Thanks.
Luke McGregor is correct. In addition to the way you fixed this you can override the default mapping that entity framework is giving you so that it doesn't cascade delete. In you CarsContext class you can override the OnModelCreating() method and specify your own mappings using fluent. This overrides what EF is trying to do by default. So you can do something like this:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Car>()
.HasOptional(x => x.Model)
.WithMany(y => y.Cars) //Add this property to model to make mapping work
.WillCascadeOnDelete(false);
}
This will still work with automatic migrations.
Hope that helps.

Categories