Add a one-to-many connection to Many-to-Many - c#

I have a Many To Many relationship with some additional fields. But as there are Photos added to the many to many relationship which might apply to other relations I wanted to seperate it so I can change it by just altering the One to many relation. This is the model
public class Segment
{
public int SegmentId { get; set; }
public int ConnectionPointIdEnd { get; set; }
public string ConnectionName { get; set; }
public string ConnectionInformation { get; set; }
public string Image { get; set; }
public string Direction { get; set; }
public ICollection<ConnectionPointRoute> ConnectionPointRoutes { get; set; }
}
public class ConnectionPointRoute
{
public int ConnectionPointId { get; set; }
public int RouteId { get; set; }
public int SegmentId { get; set; }
public int Position { get; set; }
public ConnectionPoint ConnectionPoint { get; set; }
public Route Route { get; set; }
public Segment Segment { get; set; }
}
And the modelbuilder looks like this :
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<ConnectionPointRoute>()
.HasKey(c => new { c.ConnectionPointId, c.RouteId, c.SegmentId });
modelBuilder.Entity<ConnectionPoint>()
.HasMany(c => c.ConnectionPointRoutes)
.WithRequired(x => x.ConnectionPoint)
.HasForeignKey(c => c.ConnectionPointId);
modelBuilder.Entity<Route>()
.HasMany(c => c.ConnectionPointRoutes)
.WithRequired(x => x.Route)
.HasForeignKey(c => c.RouteId);
modelBuilder.Entity<Segment>()
.HasMany(c => c.ConnectionPointRoutes)
.WithRequired(x => x.Segment)
.HasForeignKey(c => c.SegmentId);
}
And this all works well for getting the items, but for some reason it doesn't allow me to post a new Route for instance, it gets me the error:
"Multiplicity constraint violated. The role
'Segment_ConnectionPointRoutes_Source' of the relationship
'InBuildingNavigator.Data.Models.Segment_ConnectionPointRoutes' has
multiplicity 1 or 0..1."
Any thoughts?

Fixed this! I had an error in my Post code, I added the full child objects which doesn't make a whole lot of sense in my case.
Ask me if you want a more detailed fix!

Just two more things to this:
I would recommend you to use an extra object for the many-to-many relationship (if you don't already do this). This will give you more control over the table name and over selections you may want to do.
use the virtual keyword for your properties, which you do not need directly (for your collections) - this will allow ef to implement lazy loading on them.

Related

The entity type 'List<string>' requires a primary key to be defined in my mstest

While making some unit tests with mstest this error showed up when I set up UseInMemoryDatabase. Important is that the error does not show up when I run the app. Only when running a test. It seems to come from here:
public List<string> WordProgress { get; set; } = new List<string>();
The error is gone when I add [NotMapped] above, but this makes the column dissapear.
Context:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<FinalWord>()
.HasMany(c => c.Games)
.WithOne(e => e.FinalWord);
modelBuilder.Entity<Game>()
.HasMany(c => c.GameWords)
.WithOne(e => e.Game);
modelBuilder.Entity<Game>()
.HasOne(c => c.FinalWord)
.WithMany(e => e.Games);
modelBuilder.Entity<Word>()
.HasMany(c => c.GameWords)
.WithOne(e => e.Word);
modelBuilder.Entity<GameWord>()
.HasOne(c => c.Game)
.WithMany(e => e.GameWords);
modelBuilder.Entity<GameWord>()
.HasOne(c => c.Word)
.WithMany(e => e.GameWords);
}
GameWord.cs
public class GameWord
{
[Key]
public int Id { get; set; }
public List<string> WordProgress { get; set; } = new List<string>();
[Required]
public Word Word { get; set; }
[Required]
public Game Game { get; set; }
public bool Finished { get; set; } = false;
}
And my test setup.
public UnitTest1()
{
DbContextOptionsBuilder<LingoContext> dbOptions = new DbContextOptionsBuilder<LingoContext>()
.UseInMemoryDatabase(
Guid.NewGuid().ToString()
);
_context = new LingoContext(dbOptions.Options);
}
[TestMethod]
public void GetAllGames()
{
var repo = new SqlGameRepo(_context);
Game game1 = new Game();
Game game2 = new Game();
_context.Game.Add(game1);
_context.Game.Add(game2);
_context.SaveChanges();
IEnumerable<Game> result = repo.GetAllGames();
Assert.AreEqual(result.Count(), 2);
}
Anyone knows the reason why?
Entity Framework is treating the List<String> as a navigation property, e.g. it expects there to be a table of string which it can join to the GameWord table.
Relational databases do not support a column being a 'list' - you have to create a separate table and use a foreign key and a join, or map the column to a different type e.g. convert the values to a comma-separated string or a json string
As a separate table:
public class GameWord
{
[Key]
public int Id { get; set; }
public List<Attempt> WordProgress { get; set; } = new List<Attempt>();
[Required]
public Word Word { get; set; }
[Required]
public Game Game { get; set; }
public bool Finished { get; set; } = false;
}
public class Attempt
{
[Key]
public int Id { get; set; }
public int GameWordId { get; set; }
public GameWord GameWord { get; set; }
[Required]
public string Value { get; set; }
}
As a comma-separated string:
// The Entity that you have defined doesn't need to be changed
public class GameWord
{
[Key]
public int Id { get; set; }
public List<Attempt> WordProgress { get; set; } = new List<Attempt>();
[Required]
public Word Word { get; set; }
[Required]
public Game Game { get; set; }
public bool Finished { get; set; } = false;
}
// Just specify how to convert the C# property to the SQL value
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<GameWord>()
.Property(c => c.WordProgress)
.HasConversion(
attempts => string.Join(",", attempts),
csv => csv.Split(',')
);
}
As json:
// Specify a different conversion
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<GameWord>()
.Property(c => c.WordProgress)
.HasConversion(
attempts => JsonSerializer.Serialize(attempts, (JsonSerializerOptions)null),
json => JsonSerializer.Deserialize<List<string>>(json, (JsonSerializerOptions)null)
);
}
When you see how easy it is to add a json or csv column to a table with Entity Framework, you might think the first approach of adding a whole new table is overkill. I would still recommend starting with that approach, and only switch to a custom conversion if performance is affected by the join operation (which I think is unlikely here). The reasons being:
Navigation properties with an ORM are common and easy for devs to understand. Custom conversions are not. They are a specific feature of Entity Framework Core, and there are some subtleties in how a conversion affects the change tracking done by EF
If you decide in the future that you want to store more than just a single 'word' value, maybe the 'word' and the time at which the attempt was made, you have to write a funky migration script to unpack the values in the column and convert them to a different format. With a dedicated entity type and child table, you can just add a new column.
Entity Framework usually can't translate SQL filter operations on a converted column. If you want to search for all games where the player has attempted the word 'Fruit', you would have to write the SQL yourself. If you use a child table and navigation properties, this is simple to write:
context.GameWords.Where(
gameWord => gameWord.WordProgress.Any(
attempt => attempt.Value == "Fruit"
)
)

How can you add Index (for lookups) to existing Database in Entity Framework Core 2.0?

I read about the use of Guids for clustering indexes and its efficiency and I was wondering about clearing my whole database and starting from scratch (as it is a small project) but I think it's better if I try to add Indexes now.
Is it possible in my situation, with the following model:
public class Room
{
[Key]
public Guid Id { get; set; }
[Required]
[MaxLength(50)]
public string Name { get; set; }
[MaxLength(500)]
public string Description { get; set; }
[ForeignKey("DeviceId")]
public Device Device { get; set; }
public Guid? DeviceId { get; set; }
}
and the following DBContext:
public LocationContext(DbContextOptions<LocationContext> options) : base(options)
{ }
public DbSet<Room> Rooms { get; set; }
public DbSet<Device> Devices { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Room>()
.HasOne(r => r.Device)
.WithMany()
.HasForeignKey(r => r.DeviceId)
.OnDelete(DeleteBehavior.SetNull);
modelBuilder.Entity<Device>()
.HasOne(r => r.Room)
.WithMany()
.HasForeignKey(r => r.RoomId)
.OnDelete(DeleteBehavior.SetNull);
}
To add Indexes to my entities?
According to some articles I found I have to use Fluent API to add Indexes. So what I want to do is add a INT property to my Room entity(and Device) and use that as my Index to increase performance.
Can I just add a Int to my Properties called Index and configure FluentAPI like this:
modelBuilder.Entity<Room>()
.HasIndex(r => r.Index)
.IsUnique();
Like I said, I want to know if doing this is the correct way and what happens with existing records in my database before committing any changes.

One to one EF, where one of the objects has a composite key

I have
public class Expense
{
public int Id { get; private set; }
[ForeignKey("AccountId")]
public virtual Account Account { get; private set; }
[Required]
public int AccountId { get; private set; }
public virtual ExpenseCategory ExpenseCategory { get; private set; }
public Expense(... params ...)
{
this.ExpenseCategory = new ExpenseCategory();
}
protected Expense()
{
}
}
public class Account
{
[Key]
public int Id { get; private set; }
public virtual List<Expense> Expenses { get; private set; }
...
}
public class ExpenseCategory
{
[ForeignKey("CategoryId")]
public virtual BaseCategory Category { get; private set; }
public Guid? CategoryId { get; private set; }
public virtual Expense Expense { get; private set; }
[Key, ForeignKey("Expense")]
public int ExpenseId { get; private set; }
// EF is a 'friend' assembly, don't worry about the internal
internal ExpenseCategory()
{
}
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// This deletes the Expense entity when it is removed from the Account
modelBuilder.Entity<Expense>()
.HasKey(t => new { t.Id, t.AccountId })
.Property(t => t.Id)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
// PROBLEM STARTS HERE MAYBE
modelBuilder.Entity<ExpenseCategory>()
.HasKey(e => e.ExpenseId);
modelBuilder.Entity<Expense>()
.HasRequired(s => s.ExpenseCategory)
.WithRequiredPrincipal(tc => tc.Expense);
}
My relation between Expense and Account is fine - it works exactly as I need it. I am now trying to introduce a 1:1 (I know MSSQL does not support it natively, but EF works around this) relation between the Expense and a Category. I want ExpenseCategory to be my mapping between an Expense and a Category. There must be only one ExpenseCategory per Expense and I want the Key of that mapping to be the ID of the Expense.
I am having trouble, with anything I try. With the current setup I am getting:
the number of properties in the dependent and principal roles in a
relationship constraint must be identical.
I think the issue might be coming from the composite key on the Expense.
Any help?
Figured it out. Added the following on my ExpenseCategory
public int AccountId { get; private set; }
And the following Fluent API:
modelBuilder.Entity<ExpenseCategory>()
.HasKey(e => new { e.ExpenseId, e.AccountId });
modelBuilder.Entity<Expense>()
.HasRequired(s => s.ExpenseCategory)
.WithRequiredPrincipal(tc => tc.Expense)
.WillCascadeOnDelete(true);

Entity Framework Core code first - 0..1 to many relationship and cascade delete

I'm trying to create a commenting system backed by Entity Framework Core where multiple entities of different type can have comments attached to them.
These are my entities. (In the real application there are about 7 in total with varying relationships but this is how it generally looks)
public class Comment : IEntityBase
{
public int Id { get; set; }
public int? FreezerId{ get; set; }
public Freezer Freezer { get; set; }
public int? BoxId{ get; set; }
public Box Box{ get; set; }
public string Author { get; set; }
public DateTime CreatedAt { get; set; }
public string Content { get; set; }
}
public class Freezer: IEntityBase
{
public int Id { get; set; }
public string Name { get; set; }
public string Location { get; set; }
public ICollection<Box> Boxes{ get; set; }
public ICollection<Comment> Comments { get; set; }
}
public class Box: IEntityBase
{
public int Id { get; set; }
public Freezer Freezer{get; set;}
public int FreezerId{get; set;}
public string Data{ get; set; }
public ICollection<Comment> Comments { get; set; }
}
I want the Comment entity to be attached to one Freezer or one Box, but not both at the same time.
I defined the relationship in the fluent API as the following:
builder.Entity<Box>(boxBuilder=>
{
boxBuilder.HasOne(box=> box.Freezer)
.WithMany(freezer => freezer.boxes)
.HasForeignKey(box => box.FreezerId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
boxBuilder.HasMany(box => box.Comments)
.WithOne(comment => comment.Box)
.HasForeignKey(comment => comment.BoxId)
.OnDelete(DeleteBehavior.Cascade);
});
builder.Entity<Freezer>(freezerBuilder =>
{
freezerBuilder.HasMany(freezer=> freezer.Comments)
.WithOne(comment => comment.Freezer)
.HasForeignKey(comment => comment.FreezerId)
.OnDelete(DeleteBehavior.Cascade);
});
When I try to update the database to this model I get the following error:
System.Data.SqlClient.SqlException: Introducing FOREIGN KEY constraint 'FK_Comment_Boxes_BoxId' on table 'Comment' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
I think the error comes from the Box and the Freezer property in the Comment class not being optional which would make this a 1 to many relationship instead of a 0..1 to many relationship which is what I want.
With Entity Framework 6 I would just use the .HasOptional() method, but this doesn't exist in Entity Framework Core
I think one way to solve this would be to just subclass the Comment class and create a unique comment class for each entity that can be commented on and move the foreign key and reference property to that subclass instead.
But it feels like I shouldn't have to do it this way.
You have to disable the cascade delete(DeleteBehavior.Restrict) then it will works for you:
modelBuilder.Entity<Box>(boxBuilder =>
{
boxBuilder.HasOne(box => box.Freezer)
.WithMany(freezer => freezer.Boxes)
.HasForeignKey(box => box.FreezerId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
boxBuilder.HasMany(box => box.Comments)
.WithOne(comment => comment.Box)
.HasForeignKey(comment => comment.BoxId)
.OnDelete(DeleteBehavior.Restrict);
});
modelBuilder.Entity<Freezer>(freezerBuilder =>
{
freezerBuilder.HasMany(freezer => freezer.Comments)
.WithOne(comment => comment.Freezer)
.HasForeignKey(comment => comment.FreezerId)
.OnDelete(DeleteBehavior.Restrict);
});
base.OnModelCreating(modelBuilder);
}
Usage:
using (var myConext = new MyDbContext())
{
myConext.Database.EnsureCreated();
myConext.Boxes.Add(new Box() {Freezer = new Freezer()});
myConext.SaveChanges();
}

Can't get EF code first one-to-many working

Picking up from post:
Define one-to-one in EF code first with fluent API
where I had trouble getting a one-to-one working, I now have another problem with a one-to-many.
Here are my two classes:
[Table("PSCStatuses")]
public class PSCStatus
{
[Key]
public int PSCStatusID { get; set; }
public string StatusCode { get; set; }
public string StatusTextDesc { get; set; }
public int NumDaysToProjEndDate { get; set; }
public virtual List<Case> Cases { get; set; }
}
public class Case
{
// Key: Tells EF this is the PK for Case.
// ForeignKey("Appointee"): tells EF Appointee is the Principle/Parent in the 1:1
[Required]
[Key, ForeignKey("Appointee")]
public string ProfileID { get; set; }
[Required]
public int? PSCStatusID { get; set; }
public virtual PSCStatus PSCStatus { get; set; }
public virtual Appointee Appointee { get; set; }
}
You can see here what I had to do in my previous post to get Case to have one Appointee. (And Appointee to have a Case where Appointee is the Principle/Parent).
I don't recall ever having to jump through hoops with EF before. But I think I am very rusty here.
Now after solving that I have a new problem.
I can't get Case to fill in PSCStatus.
When I inspect Case.PSCStatus at a break point after adding the case with the PSCstatusID set, I should see the PSCStatus object filled in and populated.
But it remains null.
I would think that the definition above would tell EF everything it needs to know but it is not working.
I also tried fluent API:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Case>()
.HasRequired<PSCStatus>(c => c.PSCStatus)
.WithMany(s => s.Cases)
.HasForeignKey(c => c.PSCStatusID);
}
Use:
ICollection insead of List
[Edit]
If this doesn't work try this model builder:
modelBuilder.Entity<Case>()
.HasRequired(c => c.PSCStatus)
.WithMany(s => s.Cases)
.HasForeignKey(c => c.PSCStatusID);

Categories