Adding an entity without fetching - entity-framework

Having the following entities:
[Table("School")]
public class SchoolEntity
{
public SchoolEntity()
{
Students = new HashSet<StudentEntity>();
}
[Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
public Guid Id { get; set; }
[ForeignKey("SchoolId")]
public virtual ICollection<StudentEntity> Students { get; set; }
}
[Table("Student")]
public class StudentEntity
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
public Guid Id { get; set; }
[Required]
public SchoolEntity School { get; set; }
}
modelBuilder.Entity<SchoolEntity>()
.HasMany(e => e.Students)
.WithOne(e => e.School)
.IsRequired();
How do I add a new student without retrieving the parent, I tried all sort of combinations but no student is being inserted:
public async Task AddAsync(IList<ParameterDataEntity> entities, Guid schoolId, CancellationToken token)
{
var school = new School { Id = schoolId };
DbContext.Schools.Attach(school);
foreach (var entity in entities)
{
entity.School = school;
DbContext.Students.Add(entity);
DbContext.Entry(entity).State = EntityState.Added;
}
await DbContext.SaveChangesAsync(token);
}
public async Task AddAsync(IList<ParameterDataEntity> entities, Guid schoolId, CancellationToken token)
{
var school = new School { Id = schoolId };
DbContext.Schools.Attach(school);
foreach (var entity in entities)
{
entity.School = school;
DbContext.Students.Add(entity);
}
await DbContext.SaveChangesAsync(token);
}
Thanks

Try to add SchoolEntityId property in StudentEntity and assign it when adding students.
[Table("Student")]
public class StudentEntity
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
public Guid Id { get; set; }
public Guid SchoolEntityId {get; set; }
[Required]
public SchoolEntity School { get; set; }
}
And:
public async Task AddAsync(IList<ParameterDataEntity> entities, Guid schoolId, CancellationToken token)
{
foreach (var entity in entities)
{
entity.SchoolEntityId = schoolId;
DbContext.Students.Add(entity);
}
await DbContext.SaveChangesAsync(token);
}

Related

Is there any change when upgrading .Net 6 to .Net 7 in database migrations?

I am using the Npgsql driver and after upgrading all dependencies to version 7.0.0, I cannot fulfill any migration. I am doing some seeding in DbContext.
This is my context:
public class TransportationContext: DbContext
{
public DbSet<Gender> Genders { get; set; } = null!;
public DbSet<Language> Languages { get; set; } = null!;
public DbSet<Region> Regions { get; set; } = null!;
public DbSet<Role> Roles { get; set; } = null!;
public DbSet<Policy> Policies { get; set; } = null!;
public DbSet<User> Users { get; set; } = null!;
public DbSet<Servant> Servants { get; set; } = null!;
public DbSet<Vehicle> Vehicles { get; set; } = null!;
public DbSet<DiscountCode> DiscountCodes { get; set; } = null!;
public DbSet<DiscountCodeUser> DiscountCodeUsers { get; set; } = null!;
public DbSet<Service> Services { get; set; } = null!;
public DbSet<ServiceBaseType> ServiceBaseTypes { get; set; } = null!;
public DbSet<ServiceAreaType> ServiceAreaTypes { get; set; } = null!;
public DbSet<ServiceAreaParams> ServiceAreaParams { get; set; } = null!;
public DbSet<BaseNightPeriods> BaseNightPeriods { get; set; } = null!;
public DbSet<ServantStatus> ServantStatuses { get; set; } = null!;
public DbSet<Tasks> Tasks { get; set; } = null!;
public DbSet<ServantDailyStatistic> ServantDailyStatistics { get; set; } = null!;
public DbSet<CanceledTask> CanceledTasks { get; set; } = null!;
public DbSet<CancelReason> CancelReasons { get; set; } = null!;
public DbSet<CancelReasonTranslation> CancelReasonTranslations { get; set; } = null!;
public DbSet<Request> Requests { get; set; } = null!;
public DbSet<RequestServant> RequestServants { get; set; } = null!;
public DbSet<Group> Groups { get; set; } = null!;
public DbSet<ServantDailyOnlinePeriod> ServantDailyOnlinePeriods { get; set; } = null!;
public DbSet<ServantHourlyStatistic> ServantHourlyStatistics { get; set; } = null!;
public DbSet<TaskHourlyStatistic> TaskHourlyStatistics { get; set; } = null!;
public DbSet<DailyStatistic> DailyStatistics { get; set; } = null!;
public DbSet<ActiveRole> ActiveRoles { get; set; } = null!;
[Obsolete("Obsolete")]
static TransportationContext()
{
NpgsqlConnection.GlobalTypeMapper.MapEnum<DiscountCodeTypeEnum>();
NpgsqlConnection.GlobalTypeMapper.MapEnum<TaskStatusEnum>();
NpgsqlConnection.GlobalTypeMapper.MapEnum<RequestTypeEnum>();
NpgsqlConnection.GlobalTypeMapper.MapEnum<RequestStatusEnum>();
}
public TransportationContext(DbContextOptions<TransportationContext> options) : base(options)
{
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseNpgsql(o => o.UseNetTopologySuite());
// optionsBuilder.ConfigureWarnings(w => w.Throw(RelationalEventId.MultipleCollectionIncludeWarning));
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasPostgresExtension("postgis");
modelBuilder.HasPostgresEnum<DiscountCodeTypeEnum>();
modelBuilder.HasPostgresEnum<TaskStatusEnum>();
modelBuilder.HasPostgresEnum<RequestTypeEnum>();
modelBuilder.HasPostgresEnum<RequestStatusEnum>();
modelBuilder.Entity<Policy>().HasData(GetPolicies());
modelBuilder.Entity<Role>().HasData(GetBaseRoles());
modelBuilder
.Entity<Policy>()
.HasMany(p => p.Roles)
.WithMany(r => r.Policies)
.UsingEntity(j => j.HasData(GetRolePolicies(1)));
modelBuilder.Entity<DiscountPeriods>(builder =>
{
// StartTime is a TimeOnly property and time on database
builder.Property(x => x.StartTime)
.HasConversion<TimeOnlyConverter, TimeOnlyComparer>();
// EndTime is a TimeOnly property and time on database
builder.Property(x => x.EndTime)
.HasConversion<TimeOnlyConverter, TimeOnlyComparer>();
});
modelBuilder.Entity<ServantDailyStatistic>(builder =>
{
// StartTime is a TimeOnly property and time on database
builder.Property(x => x.Date)
.HasConversion<DateOnlyConverter, DateOnlyComparer>();
});
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
//If the actual entity is an auditable type.
if (typeof(Auditable).IsAssignableFrom(entityType.ClrType))
{
//This adds (In a reflection type way), a Global Query Filter
//https://docs.microsoft.com/en-us/ef/core/querying/filters
//That always excludes deleted items. You can opt out by using dbSet.IgnoreQueryFilters()
var parameter = Expression.Parameter(entityType.ClrType, "p");
var deletedCheck =
Expression.Lambda(
Expression.Equal(Expression.Property(parameter, "DeletedAt"), Expression.Constant(null)),
parameter);
modelBuilder.Entity(entityType.ClrType).HasQueryFilter(deletedCheck);
}
}
public async Task<int> SaveChangesAsync()
{
AddTimestamps();
return await base.SaveChangesAsync();
}
}
I want to add this property in migration
public class RequestServant : Auditable
{
public int Id { get; set; }
> public bool IsOnline {get; set; }
public int RequestId { get; set; }
public int ServantId { get; set; }
This gives me an error every time:
System.ArgumentException: An item with the same key has already been added. Key: CreatedAt
at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
at System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value)
at System.Linq.Enumerable.ToDictionary[TSource,TKey](IEnumerable`1 source, Func`2 keySelector, IEqualityComparer`1 comparer)
at Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationsModelDiffer.AddSeedData(IEntityType entityType, Dictionary`2 identityMaps, EntityState initialState)
at Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationsModelDiffer.TrackData(IRelationalModel source, IRelationalModel target, DiffContext diffContext)
at Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationsModelDiffer.GetDataOperations(IRelationalModel source, IRelationalModel target, DiffContext diffContext)+MoveNext()
at System.Linq.Enumerable.ConcatIterator`1.MoveNext()
at Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationsModelDiffer.Sort(IEnumerable`1 operations, DiffContext diffContext)
at Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationsModelDiffer.GetDifferences(IRelationalModel source, IRelationalModel target)
at Microsoft.EntityFrameworkCore.Migrations.Design.MigrationsScaffolder.ScaffoldMigration(String migrationName, String rootNamespace, String subNamespace, String language)
at Microsoft.EntityFrameworkCore.Design.Internal.MigrationsOperations.AddMigration(String name, String outputDir, String contextType, String namespace)
at Microsoft.EntityFrameworkCore.Design.OperationExecutor.AddMigrationImpl(String name, String outputDir, String contextType, String namespace)
at Microsoft.EntityFrameworkCore.Design.OperationExecutor.AddMigration.<>c__DisplayClass0_0.<.ctor>b__0()
at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.<>c__DisplayClass3_0`1.<Execute>b__0()
at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.Execute(Action action)
An item with the same key has already been added. Key: CreatedAt
Here are the other data context methods for data seeding and global query filter:
private static IEnumerable<Policy> GetPolicies()
{
var allPolicies = Enum.GetValues<PolicyEnum>();
return allPolicies.Select(p => new Policy { Id = (int)p, Name = p.ToString() });
}
private static IEnumerable<Policy> GetBaseRoles()
{
var allPolicies = Enum.GetValues<BaseRoles>();
return allPolicies.Select(p => new Policy { Id = (int)p, Name = p.ToString() });
}
private static IEnumerable<object> GetRolePolicies(int roleId)
{
var allPolicies = Enum.GetValues<PolicyEnum>();
return allPolicies.Select(p => new { PoliciesId = (int)p, RolesId = roleId });
}
private void AddTimestamps()
{
var entities = ChangeTracker.Entries()
.Where(x => x.Entity is Auditable && (x.State == EntityState.Added || x.State == EntityState.Modified));
foreach (var entity in entities)
{
var now = DateTime.UtcNow; // current datetime
if (entity.State == EntityState.Added)
{
((Auditable)entity.Entity).CreatedAt = now;
((Auditable)entity.Entity).UpdatedAt = now;
}
((Auditable)entity.Entity).UpdatedAt = now;
}
}
I couldn't find any solution on the internet.
Update: I downgraded Microsoft.EntityFrameWorkCore from 7.0.0 to 6.6 and all related packages, then the migration was built successfully. So, it looks like a bug. I will try to find the issue on GitHub or create one.

How to bring name from another context by using foreign key?

I'm tying to write an EntityFramework query to bring hospital name by hospital ID from Hospitals Context to Departments context.I tried couple of things like join tables etc. but I couldn't complete to write that correct query.Here my models and context below
Models
public class Hospital
{
public int Id { get; set; }
public string Name { get; set; }
public string Location { get; set; }
}
public class Department
{
public int Id { get; set; }
public string Name { get; set; }
public int HospitalId { get; set; }
}
Context
public class DataContext : DbContext
{
public DataContext(DbContextOptions<DataContext> options) : base(options) { }
public DbSet<Hospital> Hospitals { get; set; }
public DbSet<Department> Departments { get; set; }
}
Above you can see that model Department has HospitalId to connect Hospital table.After join I want to get that Hospital Name where department belongs to.Result should be department ID,department Name and its Hospital Name .
My Final Try
public async Task<IEnumerable<Department>> GetDepartment(string input)
{
var departmentWithHospital = _context.Departments
.Where(d => d.Hospital.Id == d.HospitalId)
.Include(d => d.Hospital)
.Select(d => new {
departmentId = d.Id,
departmentName = d.Name,
hospitalName = d.Hospital.Name
});
return await departmentWithHospital;
// Compiler Error:doesnt contain a definition for GetAwaiter and no
//accesible extension for GetAwaiter....
}
Three points to note:
1.The await operator suspends evaluation of the enclosing async method until the asynchronous operation represented by its operand completes. like below:
var hospital =await _context.Hospitals.ToListAsync();
return hospital;
2.The relationships between Hospital and Department is one-to-many , you could refer to Relationships to design your model as follows:
public class Hospital
{
public int Id { get; set; }
public string Name { get; set; }
public string Location { get; set; }
}
public class Department
{
public int Id { get; set; }
public string Name { get; set; }
public int HospitalId { get; set; }
public Hospital Hospital { get; set; }
}
3.You want to return a new object list which contains department ID,department Name and its Hospital Name, but your return type of the method is IEnumerable<Department> .So you could directly return a Department collection or define a ViewModel with the properties you want
Return type :IEnumerable<Department>
var departmentWithHospital =await _context.Departments
.Include(d => d.Hospital)
.Where(d => d.HospitalId == hospitalId).ToListAsync();
return departmentWithHospital;
DepartmentWithHospital ViewModel
public class DepartmentWithHospital
{
public int departmentId { get; set; }
public string departmentName { get; set; }
public string hospitalName { get; set; }
}
public async Task<IEnumerable<DepartmentWithHospital>> GetDepartment(int hospitalId)
{
var departmentWithHospital =await _context.Departments
.Include(d => d.Hospital)
.Where(d => d.HospitalId == hospitalId)
.Select(d => new DepartmentWithHospital
{
departmentId = d.Id,
departmentName = d.Name,
hospitalName = d.Hospital.Name
}).ToListAsync();
return departmentWithHospital;
}
You need a Hospital in your Departments class, and a collection of Departments in your Hospital class.
public class Hospital
{
public int Id { get; set; }
public string Name { get; set; }
public string Location { get; set; }
public virtual ICollection<Department> Departments { get; set; }
}
public class Department
{
public int Id { get; set; }
public string Name { get; set; }
public int HospitalId { get; set; }
public Hospital Hospital { get; set; }
}
For the query, try this (Been awhile since I messed with EF, and this is for EF6). I can't remember if you need the include or not, but this should get you an anonymous object with the properties you requested.
This code is not tested.
var departmentWithHospital = context.Departments
.Where(d => d.Hospital.Id == hospitalId)
.Include(d => d.Hospital)
.Select(d => new {
departmentId = d.Id,
departmentName = d.DepartmentName,
hospitalName = d.Hospital.HospitalName
})
.ToList();
If I understood your question correctly, you are looking for this:
var departmentId = "123";
var result = from department in _context.Departments
join hospital in _context.Hospitals
on hospital.Id equals department.HospitalId
where department.Id == departmentId
select new
{
DepartmentID = departmentId,
DepartmentName = department.Name,
HospitalName = hospital.Name
};

DbContext is not giving any data from database

I have two sets of codes. The first one doesn't give me the list of data but the second on does. Please see codes below:
First Code:
Model
public class Student
{
[Key]
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
public string Gender { get; set; }
}
DataConnection
public class DataConnection : DbContext
{
public DataConnection()
: base("DefaultConnection")
{
}
public DbSet<Student> Students { get; set; }
}
Interface
public interface IStudent
{
List<Student> StudentList();
void InsertStudent(Student student);
void UpdateStudent(Student student);
Student GetStudentById(int id);
void DeleteStudent(int id);
}
Concrete
readonly DataConnection _context;
public StudentConcrete()
{
_context = new DataConnection();
}
public List<Student> StudentList()
{
var studentList = (from s in _context.Students select s).ToList();
return studentList;
}
Second Code
Concrete
readonly DataConnection _context;
public StudentConcrete()
{
_context = new DataConnection();
}
public List<Student> StudentList()
{
SqlConnection xxx = new SqlConnection(ConfigurationManager.ConnectionStrings["DefaultConnection"].ConnectionString);
var cmd = new SqlCommand("GetAllStudents", xxx);
var da = new SqlDataAdapter(cmd);
var ds = new DataSet();
da.Fill(ds);
if (ds.Tables[0].Rows.Count > 0)
{
return (from DataRow row in ds.Tables[0].Rows
select new Student()
{
Age = Convert.ToInt32(row["Age"]),
FirstName = row["FirstName"].ToString(),
Gender = row["Gender"].ToString(),
LastName = row["LastName"].ToString()
}).ToList();
}
else
{
return null;
}
}
I would like to get the data using the first code but I don't know where I get it wrong. My SP is just to get the students.
I suspected that maybe you are retrieving the records from another table somehow. Would you try to add Table attribute for Student entity.
using System.ComponentModel.DataAnnotations.Schema;
[Table("Students")]
public class Student
{
[Key]
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
public string Gender { get; set; }
}

Entity Framework Core (EF 7) many-to-many results always null

I have followed the instructions for the workaround for many-to-many described in Issue #1368 and the Docs Here... but when I try to navigate, it always returns null.
My Models:
public class Organization
{
public Guid OrganizationID { get; set; }
//...
public ICollection<OrganizationSubscriptionPlan> OrganizationSubscriptionPlans { get; set; }
}
public class SubscriptionPlan
{
public int SubscriptionPlanID { get; set; }
//...
public ICollection<OrganizationSubscriptionPlan> OrganizationSubscriptionPlans { get; set; }
public class OrganizationSubscriptionPlan
{
[ForeignKey("Organization")]
public Guid OrganizationID { get; set; }
public Organization Organization { get; set; }
[ForeignKey("SubscriptionPlan")]
public int SubscriptionPlanID { get; set; }
public SubscriptionPlan SubscriptionPlan { get; set; }
}
ApplicationDbContext:
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<OrganizationSubscriptionPlan>().HasKey(x => new { x.OrganizationID, x.SubscriptionPlanID });
builder.Entity<OrganizationSubscriptionPlan>().HasOne(x => x.Organization).WithMany(x => x.OrganizationSubscriptionPlans).HasForeignKey(x => x.OrganizationID);
builder.Entity<OrganizationSubscriptionPlan>().HasOne(x => x.SubscriptionPlan).WithMany(x => x.OrganizationSubscriptionPlans).HasForeignKey(x => x.SubscriptionPlanID);
}
And my Query:
var organizations = _context.Organizations
.Include(o => o.OrganizationSubscriptionPlans);
foreach (var organization in organizations)
{
//....
var subscriptions = organization.OrganizationSubscriptionPlans
.Select(s => s.SubscriptionPlan);
// ^^^^^^^^^^^ why is subscriptions always null?
}
The "organizations" query returns the results as expected, including the list of OrganizationSubscriptionPlans within each one, but when I try to navigate to them in the foreach loop the "subscriptions" query returns null every time. What am I doing wrong?
Turns out it's a Lazy Loading issue. You have to "Include" the joining entity and then "ThenInclude" the other entity.
var organizations = _context.Organizations
.Include(o => o.OrganizationSubscriptionPlans)
.ThenInclude(s => s.SubscriptionPlan);
ForeignKey attr is to decorate reference properties to indicate them what primitive property hold the FK value.
public class OrganizationSubscriptionPlan
{
public Guid OrganizationID { get; set; }
[ForeignKey("OrganizationID")]
public Organization Organization { get; set; }
public int SubscriptionPlanID { get; set; }
[ForeignKey("SubscriptionPlanID")]
public SubscriptionPlan SubscriptionPlan { get; set; }
}

EF6: Single relationship table for multiple related entities

I have a EF Model with many entities, like Nodes, Attributes, Tags, etc.
There is also an "Alias" entity, and pretty much every other entity else can have a many-to-many relationship with Aliases. One of the undesired things about this is the number of tables that are created to track these relationships (eg. NodeAlias, AttributeAlias, etc.).
Are there any design alternatives that could map an Alias to all of the other entities in a single table? I was thinking maybe something along these lines if it's possible:
+---------+--------+-------------+-----------+
| AliasId | NodeId | AttributeId | TagId |
+---------+--------+-------------+-----------+
| 1 | 1 | 2 | 3 |
+---------+--------+-------------+-----------+
I updated my solution to provide many-to-many relationships between aliases and every other entity.
I intentionally posted this as a separate answer so that my previous answer can also remain here if anyone would need it.
Step #1: I created extension methods for getting and setting property values using reflection in a convenient way:
public static class ObjectExtensions
{
public static TResult GetPropertyValue<TResult>(this object entity, string propertyName)
{
object propertyValue = entity?.GetType().GetProperty(propertyName)?.GetValue(entity);
try
{
return (TResult)propertyValue;
}
catch
{
return default(TResult);
}
}
public static void SetPropertyValue(this object entity, string propertyName, object value)
{
entity?.GetType().GetProperty(propertyName)?.SetValue(entity, value);
}
}
Step #2: I updated the models to provide many-to-many relationship.
public class Node
{
[Key]
public int NodeId { get; set; }
public string Name { get; set; }
public virtual ICollection<AliasMapping> AliasMappings { get; set; }
}
public class Attribute
{
[Key]
public int AttributeId { get; set; }
public string Name { get; set; }
public virtual ICollection<AliasMapping> AliasMappings { get; set; }
}
public class Tag
{
[Key]
public int TagId { get; set; }
public string Name { get; set; }
public virtual ICollection<AliasMapping> AliasMappings { get; set; }
}
public class Alias
{
[Key]
public int AliasId { get; set; }
public string Name { get; set; }
public virtual ICollection<AliasMapping> AliasMappings { get; set; }
}
public class AliasMapping
{
[Key]
public int Id { get; set; }
[ForeignKey("Alias")]
public int AliasId { get; set; }
public Alias Alias { get; set; }
[ForeignKey("Node")]
public int? NodeId { get; set; }
public virtual Node Node { get; set; }
[ForeignKey("Attribute")]
public int? AttributeId { get; set; }
public virtual Attribute Attribute { get; set; }
[ForeignKey("Tag")]
public int? TagId { get; set; }
public virtual Tag Tag { get; set; }
}
Step #3: Due to relationship changes the MyDbContext could have been simplified as the [ForeignKey] data annotations are enough.
public class MyDbContext : DbContext
{
public DbSet<Node> Nodes { get; set; }
public DbSet<Attribute> Attributes { get; set; }
public DbSet<Tag> Tags { get; set; }
public DbSet<Alias> Aliases { get; set; }
public DbSet<AliasMapping> AliasMappings { get; set; }
}
Step #4: I also updated the extension methods so that you can create and remove alias mappings.
public static class AliasExtensions
{
public static void CreateMapping(this MyDbContext context, object entity, Alias alias)
{
if (entity == null || alias == null)
{
return;
}
string mappingEntityPropertyName = entity.GetType().Name;
string entityKeyPropertyName = String.Concat(mappingEntityPropertyName, "Id");
int entityId = entity.GetPropertyValue<int>(entityKeyPropertyName);
AliasMapping[] mappings =
context
.AliasMappings
.Where(mapping => mapping.AliasId == alias.AliasId)
.ToArray();
if (mappings.Any(mapping => mapping.GetPropertyValue<int?>(entityKeyPropertyName) == entityId))
{
// We already have the mapping between the specified entity and alias.
return;
}
bool usableMappingExists = true;
var usableMapping = mappings.FirstOrDefault(mapping => mapping.GetPropertyValue<int?>(entityKeyPropertyName) == null);
if (usableMapping == null)
{
usableMappingExists = false;
usableMapping = new AliasMapping()
{
Alias = alias
};
}
usableMapping.SetPropertyValue(mappingEntityPropertyName, entity);
usableMapping.SetPropertyValue(entityKeyPropertyName, entityId);
if (!usableMappingExists)
{
context.AliasMappings.Add(usableMapping);
}
// This step is required here, I think due to using reflection.
context.SaveChanges();
}
public static void RemoveMapping(this MyDbContext context, object entity, Alias alias)
{
if (entity == null || alias == null)
{
return;
}
string mappingEntityPropertyName = entity.GetType().Name;
string entityKeyPropertyName = String.Concat(mappingEntityPropertyName, "Id");
int entityId = entity.GetPropertyValue<int>(entityKeyPropertyName);
AliasMapping[] mappings =
context
.AliasMappings
.Where(mapping => mapping.AliasId == alias.AliasId)
.ToArray();
AliasMapping currentMapping = mappings.FirstOrDefault(mapping => mapping.GetPropertyValue<int?>(entityKeyPropertyName) == entityId);
if (currentMapping == null)
{
// There is no mapping between the specified entity and alias.
return;
}
currentMapping.SetPropertyValue(mappingEntityPropertyName, null);
currentMapping.SetPropertyValue(entityKeyPropertyName, null);
// This step is required here, I think due to using reflection.
context.SaveChanges();
}
}
Step #5: Updated the console app steps to align it with the changes.
class Program
{
static void Main(string[] args)
{
// Consider specify the appropriate database initializer!
// I use DropCreateDatabaseAlways<> strategy only for this example.
Database.SetInitializer(new DropCreateDatabaseAlways<MyDbContext>());
var aliases =
Enumerable
.Range(1, 9)
.Select(index => new Alias() { Name = String.Format("Alias{0:00}", index) })
.ToList();
var attributes =
Enumerable
.Range(1, 5)
.Select(index => new Attribute() { Name = String.Format("Attribute{0:00}", index) })
.ToList();
var nodes =
Enumerable
.Range(1, 5)
.Select(index => new Node() { Name = String.Format("Node{0:00}", index) })
.ToList();
var tags =
Enumerable
.Range(1, 5)
.Select(index => new Tag() { Name = String.Format("Tag{0:00}", index) })
.ToList();
using (var context = new MyDbContext())
{
context.Aliases.AddRange(aliases);
context.Nodes.AddRange(nodes);
context.Attributes.AddRange(attributes);
context.Tags.AddRange(tags);
// Always save changes after adding an entity but before trying to create a mapping.
context.SaveChanges();
// One Alias To Many Entities
context.CreateMapping(nodes[0], aliases[0]);
context.CreateMapping(nodes[1], aliases[0]);
context.CreateMapping(nodes[2], aliases[0]);
context.CreateMapping(nodes[3], aliases[0]);
context.CreateMapping(attributes[0], aliases[0]);
context.CreateMapping(attributes[1], aliases[0]);
context.CreateMapping(attributes[2], aliases[0]);
context.CreateMapping(tags[0], aliases[0]);
context.CreateMapping(tags[1], aliases[0]);
// One Entity To Many Aliases
context.CreateMapping(nodes[4], aliases[0]);
context.CreateMapping(nodes[4], aliases[1]);
context.CreateMapping(nodes[4], aliases[2]);
context.CreateMapping(attributes[3], aliases[1]);
context.CreateMapping(attributes[3], aliases[3]);
context.CreateMapping(tags[2], aliases[2]);
context.CreateMapping(tags[2], aliases[3]);
// Remove mapping
context.RemoveMapping(nodes[4], aliases[0]);
// Not really needed here as both 'CreateMapping' and 'RemoveMapping' save the changes
context.SaveChanges();
}
Console.Write("Press any key to continue . . .");
Console.ReadKey(true);
}
}
Please note: RemoveMapping() will not delete an AliasMapping even if no entity is associated with it! But CreateMapping() will make use of it later if needed. E.g. look at the screenshot below and check AliasMapping where Id = 5.
Screenshot about the execution result:
You were talking about many-to-many relationship but reading your post I think it is more likely a "special one-to-many" relationship, actually "combined multiple one-to-one" relationship as I see that an Alias can be mapped to a single Node AND/OR to a single Attribute AND/OR to a single Tag.
I think I found a solution for this case.
If it's not the case and an Alias can be mapped to multiple Node AND/OR to multiple Attribute AND/OR to multiple Tag then I think this solution below needs only a small change. :)
Step #1 - These are my example models
public class Node
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public virtual AliasMapping AliasMapping { get; set; }
}
public class Attribute
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public virtual AliasMapping AliasMapping { get; set; }
}
public class Tag
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public virtual AliasMapping AliasMapping { get; set; }
}
public class Alias
{
[Key]
public int AliasId { get; set; }
public string Name { get; set; }
public virtual AliasMapping AliasMapping { get; set; }
}
Step #2 - Creating the custom mapping table
public class AliasMapping
{
[Key]
[ForeignKey("Alias")]
public int AliasId { get; set; }
public Alias Alias { get; set; }
[ForeignKey("Node")]
public int NodeId { get; set; }
public virtual Node Node { get; set; }
[ForeignKey("Attribute")]
public int AttributeId { get; set; }
public virtual Attribute Attribute { get; set; }
[ForeignKey("Tag")]
public int TagId { get; set; }
public virtual Tag Tag { get; set; }
}
Step #3 - Creating the DbContext
public class MyDbContext : DbContext
{
public DbSet<Node> Nodes { get; set; }
public DbSet<Attribute> Attributes { get; set; }
public DbSet<Tag> Tags { get; set; }
public DbSet<Alias> Aliases { get; set; }
public DbSet<AliasMapping> AliasMappings { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder
.Entity<AliasMapping>()
.HasOptional(mapping => mapping.Attribute)
.WithOptionalPrincipal(attribute => attribute.AliasMapping)
.Map(config => config.MapKey("AliasId"));
modelBuilder
.Entity<AliasMapping>()
.HasOptional(mapping => mapping.Node)
.WithOptionalPrincipal(node => node.AliasMapping)
.Map(config => config.MapKey("AliasId"));
modelBuilder
.Entity<AliasMapping>()
.HasOptional(mapping => mapping.Tag)
.WithOptionalPrincipal(tag => tag.AliasMapping)
.Map(config => config.MapKey("AliasId"));
}
}
Step #4 - Creating extension method so that creating a relationship will be easy
public static class AliasExtensions
{
public static void CreateMapping<TEntity>(this MyDbContext context, TEntity entity, Alias alias)
{
string mappingEntityPropertyName = typeof(TEntity).Name;
string entityKeyPropertyName = String.Concat(mappingEntityPropertyName, "Id");
bool entityExists = true;
var mapping = context.AliasMappings.Find(alias.AliasId);
if (mapping == null)
{
entityExists = false;
mapping = new AliasMapping()
{
Alias = alias
};
}
typeof(AliasMapping)
.GetProperty(mappingEntityPropertyName)
.SetValue(mapping, entity);
typeof(AliasMapping)
.GetProperty(entityKeyPropertyName)
.SetValue(mapping, typeof(TEntity).GetProperty("Id").GetValue(entity));
if (!entityExists)
{
context.AliasMappings.Add(mapping);
}
}
}
Step #5 - Created a console app to see this working
class Program
{
static readonly Random rnd = new Random(DateTime.Now.TimeOfDay.Milliseconds);
static void Main(string[] args)
{
Database.SetInitializer(new DropCreateDatabaseAlways<MyDbContext>());
var aliases =
Enumerable
.Range(1, 9)
.Select(index => new Alias() { Name = String.Format("Alias{0:00}", index) })
.ToList();
var attributes =
Enumerable
.Range(1, 5)
.Select(index => new Attribute() { Name = String.Format("Attribute{0:00}", index) })
.ToList();
var nodes =
Enumerable
.Range(1, 5)
.Select(index => new Node() { Name = String.Format("Node{0:00}", index) })
.ToList();
var tags =
Enumerable
.Range(1, 5)
.Select(index => new Tag() { Name = String.Format("Tag{0:00}", index) })
.ToList();
using (var context = new MyDbContext())
{
context.Aliases.AddRange(aliases);
context.Nodes.AddRange(nodes);
context.Attributes.AddRange(attributes);
context.Tags.AddRange(tags);
context.SaveChanges();
// Associate aliases to attributes
attributes.ForEach(attribute =>
{
var usableAliases = aliases.Where(alias => alias.AliasMapping?.Attribute == null).ToList();
var selectedAlias = usableAliases[rnd.Next(usableAliases.Count)];
context.CreateMapping(attribute, selectedAlias);
});
// Associate aliases to nodes
nodes.ForEach(node =>
{
var usableAliases = aliases.Where(alias => alias.AliasMapping?.Node == null).ToList();
var selectedAlias = usableAliases[rnd.Next(usableAliases.Count)];
context.CreateMapping(node, selectedAlias);
});
// Associate aliases to tags
tags.ForEach(tag =>
{
var usableAliases = aliases.Where(alias => alias.AliasMapping?.Tag == null).ToList();
var selectedAlias = usableAliases[rnd.Next(usableAliases.Count)];
context.CreateMapping(tag, selectedAlias);
});
context.SaveChanges();
}
Console.Write("Press any key to continue . . .");
Console.ReadKey(true);
}
}