I have 2 classes:
public class FlightCostInfo : BaseEntity<Guid>
{
public Flight FlightInfo { get; set; }
public virtual ICollection<Cost> Costs { get; set; }
public virtual ICollection<PriceCalculationNotification> Notifications { get; set; }
public Guid CalculationResultId { get; set; }
}
public class Cost : BaseEntity<Guid>
{
public BasePrice Price { get; set; }
public decimal Amount { get; set; }
public Vendor Vendor { get; set; }
public Guid FlightCostInfoId { get; set; }
}
And mapping for them:
internal class FlightCostInfoMapping : EntityTypeConfiguration<FlightCostInfo>
{
public FlightCostInfoMapping()
{
HasKey(i => i.Id);
Property(i => i.CalculationResultId).HasColumnName("CalculationResult_Id");
HasOptional(i => i.FlightInfo);
HasMany(i => i.Costs).WithRequired().HasForeignKey(c => c.FlightCostInfoId);
HasMany(i => i.Notifications).WithRequired().HasForeignKey(n => n.FlightCostInfoId);
}
}
internal class CostMapping : EntityTypeConfiguration<Cost>
{
public CostMapping()
{
HasKey(c => c.Id);
Property(c => c.FlightCostInfoId).HasColumnName("FlightCostInfo_Id");
HasRequired(c => c.Price);
HasRequired(c => c.Vendor);
}
}
When I'm saving List of FlightCostInfo where each contains one or more Cost objects I recieve following error:
Multiplicity constraint violated. The role FlightCostInfo_Costs_Source of the relationship Charges.Infrastructure.DataAccess.FlightCostInfo_Costs has multiplicity 1 or 0..1
I don't have any idea why this happens. Could anyone help?
Update:
Code to save list of FlightCostInfo:
public virtual void Save(IEnumerable<TObject> entities)
{
Context.Configuration.AutoDetectChangesEnabled = false;
entities.ToList().ForEach(entity =>
{
if (Equals(entity.Id, default(TKey)) || !Context.ChangeTracker.Entries<TObject>().ToList().Any(dbEntry => dbEntry.Entity.Id.Equals(entity.Id)))
{
Set.Add(entity);
}
});
Context.ChangeTracker.DetectChanges();
SaveChanges();
Context.Configuration.AutoDetectChangesEnabled = true;
}
protected void SaveChanges()
{
var entriesWithGuidKey = Context.ChangeTracker.Entries<BaseEntity<Guid>>().Where(e => e.Entity.Id == Guid.Empty).ToList();
entriesWithGuidKey.ForEach(e => e.Entity.Id = Guid.NewGuid());
var entriesWithPeriodicValidity = Context.ChangeTracker.Entries<IPeriodicValidityObject>().ToList();
entriesWithPeriodicValidity.ForEach(e =>
{
if (e.State != System.Data.EntityState.Unchanged)
{
e.Entity.ChangedDate = DateTime.UtcNow;
}
});
Context.SaveChanges();
}
The problem appeared to be in Equals overload for BaseEntity. So EF thought that all Cost objects in FlightCostInfo collection are equal.
Closing the question
Related
I'm encountering an issue inserting records with where a join entity connects them. I am using EF Core 6.0 in a database-first situation. I have two tables, Product and Rule and a many-many relationship exists between them and is tracked by the Product_Rule table. When I insert a set of Product records with child Rule records through an API call, the records are inserted to the two primary tables correctly but the Product_Rule table just has negative values for the foreign key values. The problem specifically occurs when inserting multiple Products with child Rules at the same time. A single Product insert works fine. My entities:
Product:
public class Product
{
public long ProductId { get; set; }
public string ProductDescription { get; set; }
public DateTime InsertedOn { get; set; }
public IEnumerable<Rule>? Rules { get; set; }
public IEnumerable<ProductRule>? ProductRules { get; set; }
}
Rule:
public class Rule
{
public long RuleId { get; set; }
public string RuleDescription { get; set; }
public DateTime InsertedOn { get; set; }
public IEnumerable<Product>? Products { get; set; }
public IEnumerable<ProductRule>? ProductRules { get; set; }
}
ProductRule:
public class ProductRule
{
public long ProductRuleId { get; set; }
public long ProductId { get; set; }
public long RuleId { get; set; }
public Product Product { get; set; }
public Rule Rule { get; set; }
}
My Configurations, using the many-many documentation I found here:
Product:
public class ProductConfiguration : IEntityTypeConfiguration<Product>
{
public void Configure(EntityTypeBuilder<Product> entityTypeBuilder)
{
// Tables / Keys
entityTypeBuilder.ToTable("Product");
entityTypeBuilder.HasKey(product => product.ProductId);
// Relationships
entityTypeBuilder
.HasMany(product => product.Rules)
.WithMany(rule => rule.Products)
.UsingEntity<ProductRule>(
join => join
.HasOne(productRule => productRule.Rule)
.WithMany(rule => rule.ProductRules)
.HasForeignKey(productRule => productRule.RuleId),
join => join
.HasOne(productRule => productRule.Product)
.WithMany(product => product.ProductRules)
.HasForeignKey(productRule => productRule.ProductId),
join =>
{
join.ToTable("Product_Rule");
});
// Properties
entityTypeBuilder
.Property(product => product.InsertedOn)
.HasColumnType("datetime")
.HasDefaultValueSql("getdate()");
}
}
Rule:
public class RuleConfiguration : IEntityTypeConfiguration<Rule>
{
public void Configure(EntityTypeBuilder<Rule> entityTypeBuilder)
{
// Tables / Keys
entityTypeBuilder.ToTable("Rule");
entityTypeBuilder.HasKey(rule => rule.RuleId);
// Relationships
entityTypeBuilder
.HasMany(rule => rule.Products)
.WithMany(product => product.Rules)
.UsingEntity<ProductRule>(
join => join
.HasOne(productRule => productRule.Product)
.WithMany(product => product.ProductRules)
.HasForeignKey(productRule => productRule.ProductId),
join => join
.HasOne(productRule => productRule.Rule)
.WithMany(rule => rule.ProductRules)
.HasForeignKey(productRule => productRule.RuleId),
join =>
{
join.ToTable("Product_Rule");
});
// Properties
entityTypeBuilder
.Property(rule => rule.InsertedOn)
.HasColumnType("datetime")
.HasDefaultValueSql("getdate()");
}
}
ProductRule:
public class ProductRuleConfiguration : IEntityTypeConfiguration<ProductRule>
{
public void Configure(EntityTypeBuilder<ProductRule> entityTypeBuilder)
{
// Tables / Keys
entityTypeBuilder.ToTable("Product_Rule");
entityTypeBuilder.HasKey(product => product.ProductRuleId);
// Relationships
entityTypeBuilder
.HasOne(productRule => productRule.Rule)
.WithMany(rule => rule.ProductRules)
.HasForeignKey(productRule => productRule.RuleId);
entityTypeBuilder
.HasOne(productRule => productRule.Product)
.WithMany(rule => rule.ProductRules)
.HasForeignKey(productRule => productRule.ProductId);
// Properties
}
}
My DBContext:
public class MyDbContext : DbContext
{
public DbSet<Product> Products { get; set; }
public DbSet<Rule> Rules { get; set; }
public MyDbContext(DbContextOptions<MyDbContext> options) : base(options)
{
// Calls base constructor only
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(MyDbContext).Assembly);
}
}
My service method:
public async Task<bool> SaveNewProducts(IEnumerable<Product> products)
{
try
{
await _myDbContext.Products.AddRangeAsync(products);
await _myDbContext.SaveChangesAsync();
return true;
}
catch (Exception e)
{
Console.WriteLine(e);
return false;
}
}
I looked at other SO posts including this one, but they either did not seem to match my situation or lacked a minimum reproducible example so they were of no help. I do have this entire solution on GitHub for reference. I'm sure I am just misunderstanding something about the use of many-many relationships when an explicit join entity is defined but I can't find what from the documentation.
Thanks!
Looks like a bug when adding multiple entities in a single SaveChanges (whether batching is on or not). To work around change your product service to insert them one-by-one in a transaction, eg
public async Task SaveNewProducts(IEnumerable<Product> products)
{
try
{
using var tran = await _myDbContext.Database.BeginTransactionAsync();
foreach (var product in products)
{
_myDbContext.Products.Add(product);
_myDbContext.SaveChanges();
}
tran.Commit();
return;
}
catch (Exception e)
{
throw;
}
}
Here's a simplified repro:
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.Extensions.Logging;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Common;
using var db = new Db();
db.Database.EnsureDeleted();
db.Database.EnsureCreated();
var products = new List<Product>();
for (int i = 0; i < 2; i++)
{
var p = new Product() { ProductDescription = $"Test{i}" };
var r = new Rule() { RuleDescription = $"Test{i}" };
p.Rules.Add(r);
products.Add(p);
}
db.AddRange(products);
db.SaveChanges();
Console.WriteLine("Finished");
class Db : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("Server=localhost;Database=EfCore7Test;TrustServerCertificate=true;Integrated Security=true",
o =>
{
o.UseRelationalNulls().MaxBatchSize(1);
})
.LogTo(m => Console.WriteLine(m), LogLevel.Trace);
optionsBuilder.EnableSensitiveDataLogging();
base.OnConfiguring(optionsBuilder);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Product>()
.HasMany(product => product.Rules)
.WithMany(rule => rule.Products)
.UsingEntity<ProductRule>(
join => join
.HasOne(productRule => productRule.Rule)
.WithMany()
.HasForeignKey(productRule => productRule.RuleId),
join => join
.HasOne(productRule => productRule.Product)
.WithMany()
.HasForeignKey(productRule => productRule.ProductId),
join =>
{
join.ToTable("Product_Rule");
join.HasKey(e => e.ProductRuleId);
});
}
}
public class Rule
{
public long RuleId { get; set; }
public string RuleDescription { get; set; }
public DateTime InsertedOn { get; set; }
public ICollection<Product>? Products { get; set; } = new HashSet<Product>();
}
public class Product
{
public long ProductId { get; set; }
public string ProductDescription { get; set; }
public DateTime InsertedOn { get; set; }
public ICollection<Rule>? Rules { get; set; } = new HashSet<Rule>();
}
public class ProductRule
{
public long ProductRuleId { get; set; }
public long ProductId { get; set; }
public long RuleId { get; set; }
public Product Product { get; set; }
public Rule Rule { get; set; }
}
Which fails after attempting to run this
Executed DbCommand (2ms) [Parameters=[#p2='-9223372036854774807', #p3='-9223372036854774807'], CommandType='Text', CommandTimeout='30']
SET IMPLICIT_TRANSACTIONS OFF;
SET NOCOUNT ON;
INSERT INTO [Product_Rule] ([ProductId], [RuleId])
OUTPUT INSERTED.[ProductRuleId]
VALUES (#p2, #p3);
And which should be submitted as an issue here: https://github.com/dotnet/efcore/issues
I am struggling to have the Foreign Keys in my mappings.
My Model looks like this:
public class Accountant: Entity
{
public virtual string Name { get; set; }
public virtual IList<Company> Companies { get; set; }
}
public class Company: Entity
{
public virtual string Name { get; set; }
public virtual Subscriber Subscriber { get; set; }
public virtual Accountant Accountant { get; set; }
public virtual Funder Funder { get; set; }
}
and my Mappings look like this
public class AccountantMap : ClassMap<Accountant>
{
public AccountantMap()
{
Id(x => x.Id);
Map(x => x.Name);
HasMany(x => x.Companies)
.Inverse()
.Cascade.All();
Table("tblAccountant");
}
}
public class CompanyMap : ClassMap<Company>
{
public CompanyMap()
{
Id(x => x.Id);
Map(x => x.Name);
References(x => x.Subscriber).Cascade.SaveUpdate();
References(x => x.Accountant).Cascade.SaveUpdate();
References(x => x.Funder).Cascade.SaveUpdate();
Table("tblCompany");
}
}
And so, what I am trying to do, am trying to save the Accountant Object and it must update the foreign key in the table tblCompany
here's how my Save method looks like
public void Create_Accountant()
{
var repo = new Repository();
var companies = new List<Company>();
companies.Add(repo.GetById<Company>(new Guid("02032BD9-2769-4183-9750-AF1F00A5E191")));
companies.Add(repo.GetById<Company>(new Guid("F86E8B40-73D2-447E-A525-AF1F00A5E191")));
var accountant = new Accountant
{
Name = "Accountant Simba",
Companies= companies
};
repo.Save(accountant);
}
public void Save<T>(T entity) where T: Entity
{
using (var session = _factory.OpenSession())
{
using (var transaction = session.BeginTransaction())
{
try
{
session.SaveOrUpdate(entity);
session.Flush();
transaction.Commit();
//return entity.Id;
}
catch (Exception)
{
transaction.Rollback();
throw;
}
}
}
}
After the code has executed, this is what in my Database
DB Results
You'd notice that the Account_id column is empty and it should not be empty.
Someone, please help me, what am I doing wrong?
The references are not set but Inverse() tells NHibernate to use the references.
Change the code to this then you can't forget to set it anymore.
public class Accountant : Entity
{
public Accountant()
{
Companies = new ParentAwareCollection<Company>(c => c.Accountant = this, c => c.Accountant = null);
}
public virtual string Name { get; set; }
public virtual IList<Company> Companies { get; protected set; }
}
public sealed class ParentAwareCollection<TChild> : Collection<TChild>
{
private readonly Action<TChild> _setParent;
private readonly Action<TChild> _removeParent;
public ParentAwareCollection(Action<TChild> setParent, Action<TChild> removeParent)
{
_setParent = setParent;
_removeParent = removeParent;
}
protected override void InsertItem(int index, TChild item)
{
base.InsertItem(index, item);
_setParent(item);
}
protected override void RemoveItem(int index)
{
TChild removedItem = this[index];
_removeParent(removedItem);
base.RemoveItem(index);
}
protected override void ClearItems()
{
foreach (var item in this)
{
_removeParent(item);
}
base.ClearItems();
}
}
and usage
var accountant = new Accountant
{
Name = "Accountant Simba",
Companies =
{
repo.GetById<Company>(new Guid("02032BD9-2769-4183-9750-AF1F00A5E191")),
repo.GetById<Company>(new Guid("F86E8B40-73D2-447E-A525-AF1F00A5E191"))
}
};
I am attempting to save date in multiple tables with a one-to-many relationship in using EF Core. When I do, I get this error:
InvalidOperationException: The instance of entity type 'OrganizationGroupEntity' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting key values.
Here is my code:
Request model:
public class Organization
{
public Organization()
{ }
public Organization(OrganizationEntity organizationEntity, List<OrganizationGroupEntity> organizationGroupEntities)
{
Id = organizationEntity.Id;
Name = organizationEntity.Name;
Groups = ToList(organizationGroupEntities);
}
public int Id { get; set; }
public string Name { get; set; }}
public List<OrganizationGroup> Groups { get; set; }
private List<OrganizationGroup> ToList(List<OrganizationGroupEntity> organizationGroupEntities)
{
return organizationGroupEntities.Select(
entity => new OrganizationGroup(entity)
).ToList();
}
}
public class OrganizationGroup
{
public OrganizationGroup()
{ }
public OrganizationGroup (OrganizationGroupEntity entity)
{
Id = entity.Id;
Group = entity.Group;
}
public int Id { get; set; }
public string Group { get; set; }
}
Entity models:
public class OrganizationEntity
{
public OrganizationEntity()
{ }
public OrganizationEntity(Organization model)
{
Id = model.Id;
Name = model.Name;
}
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public string Name { get; set; }
}
public class OrganizationGroupEntity
{
public OrganizationGroupEntity()
{ }
public OrganizationGroupEntity(int organizationId, OrganizationGroup model)
{
Id = model.Id;
OrganizationId = organizationId;
Group = model.Group;
}
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public int OrganizationId { get; set; }
public string Group { get; set; }
}
dbContext:
public DbSet<OrganizationEntity> Organizations { get; set; }
public DbSet<OrganizationGroupEntity> OrganizationGroups { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<OrganizationEntity>()
.ToTable("Organizations", "dbo");
modelBuilder.Entity<OrganizationGroupEntity>()
.ToTable("OrganizationGroups", "dbo");
}
repository:
public async Task<Organization> UpdateOrganization(Organization request)
{
// Get the org entity
var organizationEntity = new OrganizationEntity(request);
// get the org groups entities
var groupEntities = request.Groups
.Select(
group => new OrganizationGroupEntity(request.Id, group)
).ToList();
// Get the group entities to remove
var oldEntities = GetOrganizationGroups(request.Id);
var entitiesToRemove = new List<OrganizationGroupEntity>();
foreach (var oldEntity in oldEntities.Result)
{
if (!groupEntities.Any(e => e.Id == oldEntity.Id))
{
entitiesToRemove.Add(oldEntity);
}
}
using (var transaction = _context.Database.BeginTransaction())
{
_context.Organizations.Update(organizationEntity);
_context.OrganizationGroups.UpdateRange(groupEntities); // <-- Fails here
_context.OrganizationGroups.RemoveRange(entitiesToRemove);
await _context.SaveChangesAsync();
transaction.Commit();
}
return request;
}
private async Task<IEnumerable<OrganizationGroupEntity>> GetOrganizationGroups(int organizationId)
{
return await _context.OrganizationGroups
.Where(e => e.OrganizationId == organizationId)
.OrderBy(e => e.Order)
.ToListAsync();
}
It turns out when I was getting the current groupEntities in order to fins out what to remove I was initiating tracking on that table. Adding AsNoTracking() to GetOrganizationGroups solved my issue. Like so:
private async Task<IEnumerable<OrganizationGroupEntity>> GetOrganizationGroups(int organizationId)
{
return await _context.OrganizationGroups
.AsNoTracking()
.Where(e => e.OrganizationId == organizationId)
.OrderBy(e => e.Order)
.ToListAsync();
}
I have Student entity which has Class (Class can have many students)
This is my Student.cs and StudentDto.cs
public partial class Students
{
public int Id { get; set; }
public string FullName { get; set; }
public int? FkClass { get; set; }
}
public class StudentsDTO
{
public int Id { get; set; }
public string FullName { get; set; }
public int FkClass { get; set; }
public int FkClassNavigationId { get; set; }
public string FkClassNavigationTitle { get; set; }
}
When I am creating new student and select class id from the dropdown, instead of using that class, new class is created (with null values and random ID).
I am not sure where is the best way to fix this mistake? Is it the problem in Automapper, context.cs class or my add method is wrong itself?
In my service I do this:
public Students PostStudent(StudentsDTO item)
{
var sub = _mapper.Map<Students>(item);
_repository.PostStudent(sub);
return sub;
}
and In the repository I do the following:
public void PostStudent(Students c)
{
this.museumContext.Add(c);
this.museumContext.SaveChanges();
}
Automapper.cs:
CreateMap<Students, StudentsDTO>()
.ReverseMap();
and context.s
modelBuilder.Entity<Students>(entity =>
{
entity.ToTable("students");
entity.Property(e => e.Id).HasColumnName("id");
entity.Property(e => e.FkClass).HasColumnName("fkClass");
entity.Property(e => e.FullName)
.HasColumnName("fullName")
.HasMaxLength(255);
entity.HasOne(d => d.FkClassNavigation)
.WithMany(p => p.Students)
.HasForeignKey(d => d.FkClass)
.HasConstraintName("Include");
});
Can someone help me understand how to fix this?
The weird thing is if I add var a = _classesService.GetClassById(s.FkClass); to my controller it works fine. It's not that I'm assigning what I retrieve or anything.
[HttpPost]
[AllowAnonymous]
public IActionResult PostStudent([FromBody] StudentsDTO s)
{
if (!this.ModelState.IsValid)
{
return this.BadRequest(ModelState);
}
try
{
var a = _classesService.GetClassById(s.FkClass);
var sub = this._service.PostStudent(s);
return Ok();
}
catch (Exception err)
{
return BadRequest(err);
}
}
How would one turn the enums used in an EF Core database context into lookup tables and add the relevant foreign keys?
Same as EF5 Code First Enums and Lookup Tables but for EF Core instead of EF 6
Related to How can I make EF Core database first use Enums?
You can use an enum in your code and have a lookup table in your db by using a combination of these two EF Core features:
Value Conversions - to convert the enum to int when reading/writing to db
Data Seeding - to add the enum values in the db, in a migration
Here below a data model example:
public class Wine
{
public int WineId { get; set; }
public string Name { get; set; }
public WineVariantId WineVariantId { get; set; }
public WineVariant WineVariant { get; set; }
}
public enum WineVariantId : int
{
Red = 0,
White = 1,
Rose = 2
}
public class WineVariant
{
public WineVariantId WineVariantId { get; set; }
public string Name { get; set; }
public List<Wine> Wines { get; set; }
}
Here the DbContext where you configure value conversions and data seeding:
public class WineContext : DbContext
{
public DbSet<Wine> Wines { get; set; }
public DbSet<WineVariant> WineVariants { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlite("Data Source=wines.db");
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Wine>()
.Property(e => e.WineVariantId)
.HasConversion<int>();
modelBuilder
.Entity<WineVariant>()
.Property(e => e.WineVariantId)
.HasConversion<int>();
modelBuilder
.Entity<WineVariant>().HasData(
Enum.GetValues(typeof(WineVariantId))
.Cast<WineVariantId>()
.Select(e => new WineVariant()
{
WineVariantId = e,
Name = e.ToString()
})
);
}
}
Then you can use the enum values in your code as follow:
db.Wines.Add(new Wine
{
Name = "Gutturnio",
WineVariantId = WineVariantId.Red,
});
db.Wines.Add(new Wine
{
Name = "Ortrugo",
WineVariantId = WineVariantId.White,
});
Here is what your db will contain:
I published the complete example as a gist: https://gist.github.com/paolofulgoni/825bef5cd6cd92c4f9bbf33f603af4ff
Here is another example :
public class Weather {
public int Id { get; init; }
public WeatherType Type { get; init; }
}
public enum WeatherType {
Cloudy = 1,
Sunny = 2,
Rainy = 3,
}
And you can add HasConversion in a seperate class like this :
public class WeatherEntityTypeConfiguration : IEntityTypeConfiguration<Weather>
{
public void Configure(EntityTypeBuilder<Weather> builder)
{
builder.ToTable("Weather").HasKey(k => k.Id);
builder.Property(p => p.Id).IsRequired();
builder.Property(p => p.Type).HasConversion<int>().IsRequired();
// builder.Property(p => p.Type).HasConversion<string>().IsRequired();
}
}
Note : If you use HasConversion<int>() data would be stored in database as an integer but if you use HasConversion<string>() data would be stored as string (in this example : Cloudy, Sunny or Rainy )
In addition to #PaoloFulgoni, here is how you'd do it if you want a many-to-many relationship with enums i.e. you want many user roles or wine variants and work with enums moreover, you can't store it as a flag because you need to know about the roles/privileges without source code(on db side).
TLDR ;) You'd have to create a join table which contains about about who has what privilege(or roles if you want).
There is a Users table which has a list of privileges, a privilege table which has privilege definition i.e. Id, name. And a Join table which will have User and Privilege as it's key. If an entry against this user/privilege combination is present that means this user has this privilege/role.
The code:
//for enum
public enum UserPrivilegeId : int
{
AddProject = 0,
ModifyProject = 1,
DeleteProject = 2,
AddUser = 3,
ModifyUser = 4,
DeleteUser = 5
}
//User class
public record User
{
public User()
{
Privileges = new HashSet<Privilege>();
}
public int Id { get; set; }
public string Username { get; set; }
public string PasswordHash { get; set; }
public virtual ICollection<Privilege> Privileges { get; set; }
public virtual List<UserPrivilege> UserPrivileges { get; set; }
}
//Privilege Class
public record Privilege //note record is IMPORTANT here, because this forces it to compare by value, if you want to *use a class*, then make sure to override GetHashCode and Equals
{
public Privilege()
{
Users = new HashSet<User>();
}
public Privilege(UserPrivilegeId privilegeId, string privilegeName)
{
PrivilegeId = privilegeId;
PrivilegeName = privilegeName;
Users = new HashSet<User>();
}
[Key]
public UserPrivilegeId PrivilegeId { get; set; }
public string PrivilegeName { get; set; }
public virtual ICollection<User> Users { get; set; }
public virtual List<UserPrivilege> UserPrivileges { get; set; }
}
//and finally the UserPrivilege join class
public record UserPrivilege
{
public UserPrivilegeId PrivilageId { get; set; }
public Privilege Privilage { get; set; }
public int UserId { get; set; }
public User User { get; set; }
}
//The set-up in dbContext
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Privilege>()
.HasKey(p => p.PrivilegeId);
modelBuilder.Entity<Privilege>()
.Property(p => p.PrivilegeId)
.HasConversion<int>();
modelBuilder.Entity<User>()
.HasMany(user => user.Privileges)
.WithMany(privilege => privilege.Users)
.UsingEntity<UserPrivilege>(
j => j
.HasOne(up => up.Privilage)
.WithMany(u => u.UserPrivileges)
.HasForeignKey(up => up.PrivilageId),
j => j
.HasOne(up => up.User)
.WithMany(p => p.UserPrivileges)
.HasForeignKey(up => up.UserId),
j =>
{
j.Property(u => u.PrivilageId).HasConversion<int>();
j.HasKey(u => new { u.PrivilageId, u.UserId });
});
//this adds definitions of privileges to the table
modelBuilder.Entity<Privilege>()
.HasData(
Enum.GetValues(typeof(UserPrivilegeId))
.Cast<UserPrivilegeId>()
.Select(p => new Privilege(p, p.ToString())));
base.OnModelCreating(modelBuilder);
}
Use it by creating a wrapper around it with a boolean on IsActive like this:
public class UserPrivelegesDTO
{
public UserPrivelegesDTO(UserPrivilegeId privilege, bool isActive)
{
this.PrivilegeId = privilege;
this.PrivilegeName = privilege.ToString();
this.IsActive = isActive;
}
public UserPrivilegeId PrivilegeId { get; set; }
public string PrivilegeName { get; set; }
public bool IsActive { get; set; }
}
If you want to convert from List<Privileges> to List<UserPrivilegeDTO>, you can
return await _context.Privileges.OrderBy(x => x.PrivilegeId).ToListAsync(cancellationToken);
To Convert back to List<Privileges>, simply
var privileges = _userPrivilegesViewModel.Privileges.Where(x => x.IsActive).Select(x => new Privilege(x.PrivilegeId, x.PrivilegeName));
If you want to check if the user has privilege
var user = _context.Users.Include(x => x.Privileges).FirstAsync(x => x.Id == 1);
if (request.Editor.Privileges.Any(p => p.PrivilegeId == UserPrivilegeId.ModifyUser))
return true;
When you want to update privileges
var PrivilegeChangeUser = await
_context.Users
.Include(user => user.Privileges)
.Include(user => user.UserPrivileges)
.FirstOrDefaultAsync(user => user.Id == request.UserId);
//**NOTE**you *need* to include the join table i.e. UserPrivileges in order replace the privileges, if you do not include it EF will try to add the privileges which already exist :(
//To update the privileges from an IEnumerable<UserPrivilegeIdEnum>
//first get the privileges objects and add that to users
var AllPrivileges =
await _context.Privileges
.Include(x => x.UserPrivileges)
.Include(x => x.Users)
.Where(x =>
request.Privileges
.Contains(x.PrivilegeId)
).ToListAsync(cancellationToken);
PrivilegeChangeUser.Privileges = AllPrivileges;