How can I map one-to-many (Folder/File)-to-(RoleBindings with Discriminator in one table) using .NET 5 EF and Fluent API?
How can I apply OnDelete() method to have one table RoleBindings where the end result would look like:
Id|Role|Discriminator|FolderId|FileId
and preserve my rule that if I delete RoleBindings nothing happens, but if the Folder/File is deleted, it should delete relations-RoleBindings?
Without Fluent API OnDelete() method the project relationship and behavior is generated badly. Only way I made it work is by defining .OnDelete(DeleteBehavior.Cascade) on Folder/File and .OnDelete(DeleteBehavior.NoAction) on RoleBindings separate tables.
Classes:
public abstract class RoleBindings
{
public Guid Id { get; set; } = Guid.NewGuid();
public Role Role { get; set; }
}
public class FolderRoleBindings : RoleBindings
{
public Guid FolderId { get; set; }
public virtual Folder Folder { get; }
}
public class FileRoleBindings : RoleBindings
{
public Guid FileId { get; set; }
public virtual File File { get; set; }
}
public abstract class File : IFile
{
public Guid Id { get; set; }
public virtual ICollection<FileRoleBindings> Roles { get; set; }
}
public abstract class Folder : IFolder
{
public Guid Id { get; set; }
public virtual ICollection<FolderRoleBindings> Roles { get; set; }
}
Working version when I generate to separate tables:
public class MyDbContext : DbContext
{
public DbSet<Folder> Folders { get; set; }
public DbSet<File> Files { get; set; }
// public DbSet<RoleBindings> RoleBindings { get; set; }
public DbSet<FolderRoleBindings> FolderRoleBindings { get; set; }
public DbSet<FileRoleBindings> FileRoleBindings { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Folder>(e =>
{
e.HasMany(prop => prop.Roles)
.WithOne(ar => ar.Folder)
.HasForeignKey(prop => prop.FolderId)
.OnDelete(DeleteBehavior.NoAction);
});
modelBuilder.Entity<File>(e =>
{
e.HasMany(prop => prop.Roles)
.WithOne(ar => ar.File)
.HasForeignKey(prop => prop.FileId)
.OnDelete(DeleteBehavior.NoAction);
});
modelBuilder.Entity<FileRoleBindings>(e =>
{
e.ToTable(name: "FileRoleBindings", schema: "dbo");
e.HasKey(key => key.Id);
e.HasOne(prop => prop.File)
.WithMany(r => r.Roles)
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity<FolderRoleBindings>(e =>
{
e.ToTable(name: "FolderRoleBindings", schema: "dbo");
e.HasKey(key => key.Id);
e.HasOne(prop => prop.Folder)
.WithMany(r => r.Roles)
.OnDelete(DeleteBehavior.Cascade);
});
}
}
}
How does one specify to have this kind of relationshiop in one table? I couldn't figure out how to apply OnDelete() on this creation:
modelBuilder.Entity<RoleBindings>(e =>
{
e.ToTable(name: "RoleBindings", schema: "dbo");
e.HasKey(key => key.Id);
e.HasDiscriminator()
.HasValue<FileRoleBindings>("File")
.HasValue<FolderRoleBindings>("Folder");
});
Edit1:
When I'm trying to store everything in one table (previous code modelBuilder.Entity()) I get error
CREATE TABLE [dbo].[RoleBindings] (
[Id] uniqueidentifier NOT NULL,
[Role] int NOT NULL,
[Discriminator] nvarchar(max) NOT NULL,
[FolderId] uniqueidentifier NULL,
[FileId] uniqueidentifier NULL,
CONSTRAINT [PK_RoleBindings] PRIMARY KEY ([Id]),
CONSTRAINT [FK_RoleBindings_Folders_FolderId] FOREIGN KEY ([FolderId]) REFERENCES [hierarchy].[Folders] ([Id]) ON DELETE CASCADE,
CONSTRAINT [FK_RoleBindings_Files_FileId] FOREIGN KEY ([FileId]) REFERENCES [reports].[Files] ([Id]) ON DELETE CASCADE
);
Microsoft.Data.SqlClient.SqlException (0x80131904): Introducing FOREIGN KEY constraint 'FK_RoleBindings_Files_FileId' on table 'RoleBindings' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
Could not create constraint or index. See previous errors.
When it's table per type I have no issues. Is there anything I can define in modelBuilder.Entity relationship to have this work with table per hierarchy? If I define relationship inside entities File/Folder then the cascade doesn't transfer over and it prevents deletion of File/Folder if RoleBindings exist for the principal entity.
Related
So i wanted to apply a relation of 1 to 1 from one table to another, with navigational properties on each one and a foreign key that is accessable on at least one of the models.
Lets suppose this example
public class User
{
public int Id { get; set; }
public string Username { get; set; }
public int ContactId { get; set; }
public virtual Contact Contact { get; set; }
}
public class Contact
{
public int Id { get; set; }
public string Name { get; set; }
public virtual User User { get; set; }
}
modelBuilder.Entity<User>().HasOptional<Contact>(u=> u.Contact)
.WithRequired(c => c.User).Map(m => m.MapKey("ContactId")).
Similar to the same example used in this stack overflow question:
EF Code First - 1-to-1 Optional Relationship
The problem is that it gives an error saying that the Property name 'ContactId' is already defined.
But i want to have this foreign property defined both at the database and on the model, so that i can use for example linq:
this.dbContextProvider.CurrentContext.User.SingleOrDefault(src => src.ContactId == contactId);
or is this acceptable or very inneficient:
this.dbContextProvider.CurrentContext.User.SingleOrDefault(src => src.Contact.Id == contactId);
This last options will create a join between the two tables while query the database, right?
The downside of the correct model (i.e. without explicit User.ContactId property) is that in reality it's still a 1:n relationship. The database doesn't enforce 1:1. It's just a FK. The only way to make a true, database-enforced 1:1 association in EF6 is one in which the dependent entity (here: User) has a primary key that's also a foreign key to the principal entity (Contact):
public class User
{
public int Id { get; set; }
public string Username { get; set; }
//public int ContactId { get; set; } <= removed
public virtual Contact Contact { get; set; }
}
public class Contact
{
public int Id { get; set; }
public string Name { get; set; }
public virtual User User { get; set; }
}
And:
modelBuilder.Entity<User>()
.HasRequired<Contact>(u => u.Contact)
.WithOptional(c => c.User);
This generates the following database schema:
CREATE TABLE [dbo].[Users] (
[Id] [int] NOT NULL,
[Username] [nvarchar](max),
CONSTRAINT [PK_dbo.Users] PRIMARY KEY ([Id])
)
CREATE TABLE [dbo].[Contacts] (
[ID] [int] NOT NULL IDENTITY,
[Name] [nvarchar](max),
CONSTRAINT [PK_dbo.Contacts] PRIMARY KEY ([ID])
)
CREATE INDEX [IX_Id] ON [dbo].[Users]([Id])
ALTER TABLE [dbo].[Users] ADD CONSTRAINT [FK_dbo.Users_dbo.Contacts_Id]
FOREIGN KEY ([Id]) REFERENCES [dbo].[Contacts] ([ID])
As for querying, in a query like context.Users.Where(u => u.Contact.ID == 4), EF6 will notice that no Contact fields are requested and it will short-circuit the FK to User.Id, i.e. no join. But of course, in this setup, you may as well use context.Users.Where(u => u.Id == 4).
In EF core it would be possible to use your model, with User.ContactId, by this mapping:
modelBuilder.Entity<User>()
.HasOne(u => u.Contact)
.WithOne(c => c.User)
.HasForeignKey<User>(u => u.ContactId);
EF core is smart enough to create a unique index on User.ContactId, so this is a database-enforced 1:1 association with a separate FK.
I have the traditional ApplicationUser (IdentityUser), and that user can send a friend request to another ApplicationUser. I currently have the following general entity classes:
public class ApplicationUser : IdentityUser
{
public virtual List<DeviceToken> DeviceTokens { get; set; } = new List<DeviceToken>();
public string DisplayName { get; set; }
}
public class FriendRequest
{
public int Id { get; set; }
public DateTime DateRequested { get; set; }
public ApplicationUser Requester { get; set; }
public ApplicationUser Receiver { get; set; }
}
I have ran database-update etc and this is working fine. However when I go into my SQLServer to try to delete an ApplicationUser, it tells me that The DELETE statement conflicted with the REFERENCE constraint "FK_FriendRequest_AspNetUsers_RequesterId".
So I have decided to implement a cascade delete flow from the ApplicationUser to the friend requests that they are part of.
I have tried the resource on here by Microsoft on configuring cascade delete but I cannot figure out how to apply it to my case:
builder.Entity<ApplicationUser>()
.HasMany(e => e.FriendRequests)//No such property, no idea how to address
.OnDelete(DeleteBehavior.ClientCascade);
How do I set up this cascade delete scenario?
Also how do I add a property to ApplicationUser that refers to all the FriendRequests they are part of, and make sure EFCore knows I am referring to that existing FriendRequest entity/table?
Update
Following the suggested approach of adding a virtual property to ApplicationUser, would this be way forward:
public class ApplicationUser : IdentityUser
{
public virtual List<DeviceToken> DeviceTokens { get; set; } = new List<DeviceToken>();
public string DisplayName { get; set; }
public ICollection<FriendRequest> FriendRequests { get; }
}
builder.Entity<ApplicationUser>()
.HasMany(u => u.FriendRequests)
.WithOne(u => u.Requester)
.OnDelete(DeleteBehavior.ClientCascade); //not sure about this
builder.Entity<ApplicationUser>()
.HasMany(u => u.FriendRequests)
.WithOne(u => u.Requester)
.OnDelete(DeleteBehavior.ClientCascade); //not sure about this
Your ApplicationUser needs 2 virtual ICollections.
public class ApplicationUser
{
public int Id { get; set; }
public string DisplayName { get; set; }
public virtual ICollection<FriendRequest> FriendRequestsAsRequestor { get; set; }
public virtual ICollection<FriendRequest> FriendRequestsAsReceiver { get; set; }
}
public class FriendRequest
{
public int Id { get; set; }
public DateTime DateRequested { get; set; }
public int RequestorId { get; set; }
public ApplicationUser Requestor { get; set; }
public int ReceiverId { get; set; }
public ApplicationUser Receiver { get; set; }
}
public class ApplicationUserConfig : IEntityTypeConfiguration<ApplicationUser>
{
public void Configure(EntityTypeBuilder<ApplicationUser> builder)
{
builder.HasMany(au => au.FriendRequestsAsRequestor)
.WithOne(fr => fr.Requestor)
.HasForeignKey(fr => fr.RequestorId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(au => au.FriendRequestsAsReceiver)
.WithOne(fr => fr.Receiver)
.HasForeignKey(fr => fr.ReceiverId)
.OnDelete(DeleteBehavior.Cascade);
}
}
Use:
void AddFriendRequest(int requestorId, int receiverId)
{
var ctxt = new DbContext();
FriendRequest fr = new FriendRequest
{
RequestorId = requestorId;
ReceiverId = receiverId;
DateRequested = DateTime.Now;
}
ctxt.FriendRequests.Add(fr);
ctxt.SaveChanges();
}
List<FriendRequest> GetFriendRequests()
{
var ctxt = new DbContext();
return ctxt.FriendRequests
.Include(fr => fr.Requestor)
.Include(fr => fr.Receiver)
.ToList();
}
ApplicationUser GetUserWithFriendRequests(int id)
{
var ctxt = new DbContext();
return ctxt.ApplicationUser
.Include(au => au.FriendRequestsAsRequestor)
.Include(au => au.FriendRequestsAsReceiver)
.SingleOrDefault(au => au.Id == id);
}
I have tried the resource on here by Microsoft on configuring cascade delete but I cannot figure out how to apply it to my case:
builder.Entity<ApplicationUser>()
.HasMany(e => e.FriendRequests)//No such property, no idea how to address
.OnDelete(DeleteBehavior.ClientCascade);
From the doc of DeleteBehavior :
ClientCascade : For entities being tracked by the DbContext, dependent entities will be deleted when the related principal is deleted. If the database has been created from the model using Entity Framework Migrations or the EnsureCreated() method, then the behavior in the database is to generate an error if a foreign key constraint is violated.
In this case, it's the client (the .NET app) and not the DB that ensure the cascade delete. If the client fail to do the cascade delete (related entity not tracked), the db will generate the error you see.
Maybe the DeleteBehavior.Cascade is more appropriate to your code first scenario :
Cascade : For entities being tracked by the DbContext, dependent entities will be deleted when the related principal is deleted. If the database has been created from the model using Entity Framework Migrations or the EnsureCreated() method, then the behavior in the database is the same as is described above for tracked entities. Keep in mind that some databases cannot easily support this behavior, especially if there are cycles in relationships, in which case it may be better to use ClientCascade which will allow EF to perform cascade deletes on loaded entities even if the database does not support this. This is the default for required relationships. That is, for relationships that have non-nullable foreign keys.
If you try this, you go with this SQL script migration (I assume the SGBDR is SQL Server) :
CREATE TABLE [ApplicationUser] (
[Id] int NOT NULL IDENTITY,
[DisplayName] nvarchar(max) NULL,
CONSTRAINT [PK_ApplicationUser] PRIMARY KEY ([Id])
);
GO
CREATE TABLE [FriendRequests] (
[Id] int NOT NULL IDENTITY,
[DateRequested] datetime2 NOT NULL,
[RequesterId] int NULL,
[ReceiverId] int NULL,
CONSTRAINT [PK_FriendRequests] PRIMARY KEY ([Id]),
CONSTRAINT [FK_FriendRequests_ApplicationUser_ReceiverId] FOREIGN KEY ([ReceiverId]) REFERENCES [ApplicationUser] ([Id]) ON DELETE CASCADE,
CONSTRAINT [FK_FriendRequests_ApplicationUser_RequesterId] FOREIGN KEY ([RequesterId]) REFERENCES [ApplicationUser] ([Id]) ON DELETE CASCADE
);
GO
And when it's apply, this produce this error :
Introducing FOREIGN KEY constraint 'FK_FriendRequests_ApplicationUser_RequesterId' on table 'FriendRequests' may cause cycles or multiple cascade paths.
Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
First time I see this error, then I will refer to this question with #onedaywhen's answer :
SQL Server does simple counting of cascade paths and, rather than trying to work out whether any cycles actually exist, it assumes the worst and refuses to create the referential actions (CASCADE)...
A no perfect solution is to use DeleteBehavior.Cascade and ensure all related entities are tracked before the delete :
public class ApplicationUser
{
public int Id { get; set; }
public string DisplayName { get; set; }
public ICollection<FriendRequest> RequestedRequests { get; set; }
public ICollection<FriendRequest> RecevedRequests { get; set; }
}
public class FriendRequest
{
public int Id { get; set; }
public DateTime DateRequested { get; set; }
public ApplicationUser Requester { get; set; }
public ApplicationUser Receiver { get; set; }
}
public class MyDbContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
optionsBuilder.UseSqlServer("***");
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<FriendRequest>()
.HasOne(r => r.Requester)
.WithMany(u => u.RequestedRequests)
.OnDelete(DeleteBehavior.ClientCascade);
modelBuilder.Entity<FriendRequest>()
.HasOne(r => r.Receiver)
.WithMany(u => u.RecevedRequests)
.OnDelete(DeleteBehavior.ClientCascade);
}
public DbSet<ApplicationUser> Users { get; set; }
public DbSet<FriendRequest> FriendRequests { get; set; }
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
{
PrepareUserToDeleting();
return base.SaveChangesAsync();
}
public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
PrepareUserToDeleting();
return base.SaveChanges();
}
private void PrepareUserToDeleting()
{
// For each deleted user entity
foreach(var entry in ChangeTracker.Entries<ApplicationUser>().Where(e => e.State == EntityState.Deleted))
{
var user = entry.Entity;
// If RecevedRequests isn't loaded
if (user.RecevedRequests == null)
{
//Then load RecevedRequests
entry.Collection(u => u.RecevedRequests).Load();
}
// Idem with RequestedRequests
if (user.RequestedRequests == null)
{
entry.Collection(u => u.RequestedRequests).Load();
}
}
}
}
Actually I know how to create and use many-to-many relation in regular EF Core DB context, but in my present project I decided to migrate my Identity DB context tables to my ApplicationDbContext, where I keep rest of my business tables:
public class ApplicationDbContext : IdentityDbContext<AppUser>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }
public DbSet<CompanyModel> Companies { get; set; }
public DbSet<CompanyUser> CompaniesUsers { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
//many-to-many
modelBuilder.Entity<CompanyUser>()
.HasKey(c => new { c.CompanyId, c.UserId });
/* do I realy need this at all? -> https://stackoverflow.com/a/54612387/12603542
modelBuilder.Entity<CompanyUser>()
.HasOne(cu => cu.AppUser)
.WithMany(au => au.CompaniesUsers)
.HasForeignKey(cu => cu.UserId);
modelBuilder.Entity<CompanyUser>()
.HasOne(cu => cu.CompanyModel)
.WithMany()
.HasForeignKey(cu => cu.CompanyId);
*/
}
}
I have a relations, where many users may have many companies, so I created Company <-> AppUser many-to-many bridge:
public class CompanyUser
{
public int CompanyId { get; set; }
public string UserId { get; set; }
public AppUser AppUser { get; set; }
public CompanyModel CompanyModel { get; set; }
}
And my AppUser contains navigation property:
public class AppUser : IdentityUser
{
//(...)
public ICollection<CompanyUser> CompaniesUsers { get; set; } //many-to-many
}
Same about my CompanyModel:
public class CompanyModel
{
[Key]
public int CompanyId { get; set; }
//(...)
public ICollection<CompanyUser> CompaniesUsers { get; set; } //many-to-many
}
Everything seems to be fine, until I am not trying to retrieve an user with related companies drom DB, where my controller calls DAL method:
AppUser userWithLists = _repositoryUser.GetUser(user.Id);
And in my repository I try to do that:
public AppUser GetUser(string id)
{
return _context.Users
.Include(u => u.CompaniesUsers)
.ThenInclude(u => u.CompanyModel)
.Where(u => u.Id == id)
.FirstOrDefault();
}
But unfortunately when executing LINQ query on my DB context I am receiving an exception:
SqlException: Invalid column name 'AppUserId'. Invalid column name
'AppUserId'.
With commented out OnModelCreating code I am getting another exception:
SqlException: Invalid column name 'CompanyModelCompanyId'.
Bridge table T-SQL creation code:
CREATE TABLE [dbo].[CompaniesUsers] (
[CompanyId] INT NOT NULL,
[UserId] NVARCHAR (450) NOT NULL,
CONSTRAINT [PK_CompaniesUsers] PRIMARY KEY CLUSTERED ([CompanyId] ASC, [UserId] ASC),
CONSTRAINT [FK_CompaniesUsers_Companies_CompanyId] FOREIGN KEY ([CompanyId]) REFERENCES [dbo].[Companies] ([CompanyId]) ON DELETE CASCADE,
CONSTRAINT [FK_CompaniesUsers_AppUser_UserId] FOREIGN KEY ([UserId]) REFERENCES [dbo].[AspNetUsers] ([Id]) ON DELETE CASCADE
);
GO
CREATE NONCLUSTERED INDEX [IX_CompaniesUsers_CompanyId]
ON [dbo].[CompaniesUsers]([CompanyId] ASC);
I've got a simple structure in database:
CREATE TABLE user_categories (
user_id INT NOT NULL,
category_id INT NOT NULL,
PRIMARY KEY (user_id, category_id)
);
CREATE TABLE user_defaults (
user_id INT NOT NULL,
category_id INT NOT NULL,
PRIMARY KEY (user_id),
FOREIGN KEY (user_id, category_id) REFERENCES user_categories (user_id, category_id)
);
The intention here is that user has a set of categories assigned and there's a table with defaults table (which could potentially have other columns)
However I struggle to map this to EF Core, I always end up getting extra columns in user_categories table.
My UserCategory class:
using System.ComponentModel.DataAnnotations.Schema;
namespace EMMA.Authorization.Domain.Entities
{
public class UserCategory
{
public int UserId { get; set; }
[ForeignKey(nameof(UserId))]
public User User { get; set; }
public int CategoryId { get; set; }
[ForeignKey(nameof(CategoryId))]
public Category Category { get; set; }
public UserDefaults UserDefaults { get; set; }
}
}
UserDefaults
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace EMMA.Authorization.Domain.Entities
{
public class UserDefaults
{
public int UserId { get; set; }
public int CategoryId { get; set; }
public virtual ICollection<UserCategory> UserCategories { get; set; }
}
}
That's OnModelCreating method that should configure relationships:
modelBuilder.Entity<UserCategory>().HasKey(s => new { s.UserId, s.CategoryId });
modelBuilder.Entity<UserDefaults>().HasKey(s => s.UserId);
modelBuilder.Entity<UserDefaults>()
.HasMany(s => s.UserCategories)
.WithOne(s => s.UserDefaults)
.HasForeignKey(s => new { s.UserId, s.CategoryId })
.IsRequired();
However that's the exception I'm getting when I try to create a migration:
The relationship from 'UserCategory.UserDefaults' to 'UserDefaults.UserCategories' with foreign key properties {'UserId' : int, 'CategoryId' : int} cannot target the primary key {'UserId' : int} because it is not compatible. Configure a principal key or a set of compatible foreign key properties for this relationship.
How could I configure such an relationship in EF Core as shown in SQL?
The user_catagories object should have a composite key. You are already impliying this by having the FK from the other table be a composite key on the first. EF complains because this is actually not true. You can fix it by annotating both members as primary like so:
public class UserCatagory
{
[Key, Column(Order = 0)]
public int UserId { get; set; }
[Key, Column(Order = 1)]
public int CategoryId { get; set; }
// Etc...
}
I actually realized that I did declare a wrong relationship, it's not one to many, but rather one to one. Declaring the following mapping works as expected:
modelBuilder.Entity<UserCategory>()
.HasOne(s => s.UserDefaults)
.WithOne(s => s.UserCategory)
.HasForeignKey<UserDefaults>(s => new { s.UserId, s.CategoryId })
.IsRequired();
I want to have a intermediate table with only two foreign keys (as a ComposedId).
But NHibernate is automatically creating a "id" property.
I have the following classes
public class Lace
{
public virtual int Id { get; set; }
public virtual string Hostname { get; set; }
public virtual IList<LaceHasCard> LaceHasCards { get; set; }
}
public class Card
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual IList<LaceHasCard> LaceHasCards { get; set; }
}
and this manually created intermediate table
public class LaceHasCard
{
public virtual Card Card { get; set; }
public virtual Lace Lace { get; set; }
}
Mappings
public LaceMapping()
{
Id(x => x.Id, map => map.Generator(Generators.Native));
Property(x => x.Hostname);
Bag(x => x.LaceHasCards, col =>
{
col.Key(k => k.Column("LaceId"));
col.Inverse(true);
}, r => r.OneToMany());
}
public CardMapping()
{
Id(x => x.Id, map => map.Generator(Generators.Native));
Property(x => x.Name);
Bag(x => x.LaceHasCards, col =>
{
col.Key(k => k.Column("CardId"));
col.Inverse(true);
}, r => r.OneToMany());
}
intermediate table mapping
public LaceHasCardMapping()
{
//ComposedId(map =>
//{
// map.Property(x => x.Card.Id, a =>
// {
// a.Column("CardId");
// });
// map.Property(x => x.Lace.Id, a =>
// {
// a.Column("LaceId");
// });
//});
ManyToOne(x => x.Card, map =>
{
map.Column("CardId");
});
ManyToOne(x => x.Lace, map =>
{
map.Column("LaceId");
});
}
If I create the schema with the ComposedId commented out, NHibernate will create a "id" property in the table.
CREATE TABLE [dbo].[LaceHasCard] (
[id] INT NOT NULL,
[CardId] INT NULL,
[LaceId] INT NULL,
PRIMARY KEY CLUSTERED ([id] ASC),
CONSTRAINT [FKDC6D54711CD160AE] FOREIGN KEY ([CardId]) REFERENCES [dbo].[Card] ([Id]),
CONSTRAINT [FKDC6D547151F8AF85] FOREIGN KEY ([LaceId]) REFERENCES [dbo].[Lace] ([Id])
);
If I try to create the schema with the ComposedId, I get the following error message:
Unable to instantiate mapping class (see InnerException):
EmpLaceMgmt.Models.Mappings.LaceHasCardMapping
What would be the right way to tell NHibernate to create a composed Id?
Let me give you suggestion, just my point of view - do not use composite id. Use standard primary key in DB and its C# / entity representation as Id { get; set; }
Chapter 24. Best Practices
...
Declare identifier properties on persistent classes.
NHibernate makes identifier properties optional. There are all sorts of reasons why you should use them. We recommend that identifiers be 'synthetic' (generated, with no business meaning) and of a non-primitive type. For maximum flexibility, use Int64 or String.
See also more about synthetic, surrogate keys at wiki.
From my experience, we should not be worry about having pairing object like this:
public class LaceHasCard
{
public virtual int Id { get; set; } // the key
public virtual Card Card { get; set; }
public virtual Lace Lace { get; set; }
}
Because later it would become so easy to access it:
session.Get<LaceHasCard>(id)
And also to use it in Subqueries (for filtering Card with Laces and vice versa)
One column in DB, autogenerated, should not have any extra bad impact. But handling such table is a bit (a lot) easier...
So, summary, my suggestion would be, make all entities first level citizens, with full rights (including synthetic/surrogate key)