I'm using Fluent NHibernate in an attempt to improve testability and maintainability on a web application that is using a legacy database and I'm having some trouble mapping this structure properly:
I have two tables that really represent one entity in the domain, and so I'm using a join to map them as such, and a third table that represents a second entity.
DB Tables:
[eACCT]
ACCT_ID
ACCT_NAME
[eREPORT_TYPE]
ACCT_ID
REPORT_NO
[eREPORT_TYPE_DESC]
REPORT_NO
REPORT_TYPE
Entities:
public class Account
{
public virtual string AccountID { get; set; }
public virtual string AccountName { get; set; }
public virtual ReportType ReportType { get; set; }
}
public class ReportType
{
public virtual int Number { get; set; }
public virtual string Type { get; set; }
}
Mapping:
public AccountMap()
{
Table("eACCT");
Id(x => x.AccountID, "ACCT_ID");
Map(x => x.AccountName, "ACCT_NAME");
Join("eREPORT_TYPE", m =>
{
m.KeyColumn("ACCT_ID");
m.References(x => x.ReportType)
.Cascade.None()
.Column("REPORT_NO");
});
}
public ReportTypeMap()
{
Table("eREPORT_TYPE_DESC");
Id(x => x.Number)
.Column("REPORT_NO")
.GeneratedBy.Assigned();
Map(x => x.Type, "REPORT_TYPE");
}
This works fine for my Gets, but when I modify Account.ReportType.Number and then SaveOrUpdate() Account, I get the error: 'identifier of an instance of DataTest.Model.ReportType was altered from (old_value) to (new_value)'.
All I want to do is modify Account's reference to ReportType and I thought that by setting the Cascade.None() property on the reference to ReportType, NHibernate wouldn't attempt to save the ReportType instance as well, but I must be misunderstanding how that works. I've tried making ReportType ReadOnly(), making the reference to ReportType ReadOnly(), etc and nothing seems to help.
Any ideas?
Finally solved this problem. Turns out I wasn't thinking about this in an NHibernate way. In my mind I had a new ReportType.Number, so that's what I needed to update. In reality, what I needed to do was get the ReportType with the new ReportType.Number and set the Account.ReportType. Doing it this way worked as expected.
Related
I have created the following classes:
public class Character
{
public int ID { get; set; }
public string Title { get; set; }
public ICollection<Relationship> RelatedTo { get; set; }
public ICollection<Relationship> RelatedFrom { get; set; }
}
public class Relationship
{
public int ToID { get; set; }
public int FromID { get; set; }
public Character CharacterFrom { get; set; }
public Character CharacterTo { get; set; }
public string Details { get; set; }
}
In my Context I have this:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Relationship>()
.HasKey(r => new { r.ToID, r.FromID });
modelBuilder.Entity<Relationship>()
.HasOne(r => r.CharacterFrom)
.WithMany(r => r.RelatedTo)
.HasForeignKey(r => r.FromID)
.OnDelete(DeleteBehavior.ClientSetNull);
modelBuilder.Entity<Relationship>()
.HasOne(r => r.CharacterTo)
.WithMany(r => r.RelatedFrom)
.HasForeignKey(r => r.ToID)
.OnDelete(DeleteBehavior.ClientSetNull);
}
I think that it is right but I cannot apply the migration due to the following error:
Cannot create the foreign key "FK_Relationship_Character_FromID" with the SET NULL referential action, because one or more referencing columns are not nullable.
I've tried every combination of DeleteBehaviour for OnDelete. None of them work. I don't believe I can make the ICollections nullable and it doesn't seem right that I'd want to. I've spent two hours on this searching for answers. Every tutorial or explanation on EF Core that I've tried to follow seems to take a slightly different approach and be subtly incompatible with every other one. Please help!
The error is telling you that you cannot use DeleteBehavior.ClientSetNull (or DeleteBehavior.SetNull) because the corresponding FK property is not nullable - both ToID and FromID are of type int, hence does not allow setting to null (neither client nor server).
To turn off the cascade delete (in order to break the multiple cascade paths I guess) for required FK relationships, use DeleteBehavior.Restrict instead.
I've worked with databases for a long time now but am new to Entity Framework. I handle both the aspects of programming and database development. As a db developer, I try to keep it clean so this structure that I came up with works well for me but I'm not sure if Entity Framework even supports it for I've tried for several days, using different scenarios, Data Annotations as well as Fluent API but couldn't get this to work.
What I'm trying to do might be a bit unconventional but what I'm trying to avoid is having to duplicate a file table for each area hence I define 1 file table that can be used by multiple areas using a Relationship. Thus, what I have is: one [company, employee, or project] can have many files (one to many). Similarly, the file table can be sourced by any area (many to many, in this case, it's not the data but rather the structure, hopefully that makes sense). The file records are related to only 1 area [company, employee, or project] (many to one).
The obvious advantage to this method is that I can avoiding having to manage 3 file tables but it doesn't end there. As you can see from the FileAccess table, instead of having multiple tables here or multiple fields to represent pointers to the multiple tables, I only need to manage 1 table for file access. The key is in the RelationTable and RelationId rather than the specific File.Id.
Below is a simplified example of the structure I'm trying to accomplish. Can it be done in Entity Framework?
public class Company
{
public Guid Id { get; set; }
public string Name { get; set; }
public virtual ICollection<File> Files { get; set; }
}
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<File> Files { get; set; }
}
public class Project
{
public int Id { get; set; }
public Guid? CompanyId { get; set; }
public string ProjectNo {get; set; }
public virtual ICollection<File> Files { get; set; }
}
public class File
{
public int Id { get; set; }
public Int16 RelationTable { get; set; } 0=Company, 1=Employee, 2=Project
public string RelationId { get; set; } Company.Id, Employee.Id, Project.Id
public string FileName { get; set; }
}
public class FileAccess
{
public int Id { get; set; }
public int EmployeeId { get; set; }
public Int16 RelationTable { get; set; } 0=Company, 1=Employee, 2=Project
public string RelationId { get; set; } Company.Id, Employee.Id, Project.Id
public string AccessType
}
As Ivan pointed out, EF doesn't support this due to the foreign key limitations but I was able to come up with a working solution. However, I must warn you that I'm only on my 3rd week of EF so I don't know what ramifications this may cause but this is what I did, for those who may be interested.
As it turns out (through trial and error), EF just needs the OnModelCreating to wire up the relationship between the objects, it doesn't really need the FK to be created thus I defined the relationship this way:
modelBuilder.Entity<File>()
.HasIndex(k => new { k.RelationTable, k.RelationId }); //for performance
modelBuilder.Entity<FileAccess>()
.HasMany(fa => fa.Files)
.WithOne(f => f.FileAccess)
.HasForeignKey(k => new { k.RelationTable, k.RelationId })
.HasPrincipalKey(k => new { k.RelationTable, k.RelationId });
//Using enumerations to control
relationships and adding PERSISTED so it doesn't need to be maintained plus it
won't break the Add-Migration with the "non persistent error"
modelBuilder.Entity<Project>()
.Property(f => f.RelationTable)
.HasComputedColumnSql((int)NTGE.Database.Shared.eFileRelTable.Projects + " PERSISTED") //This injects the value so we don't have to store it
.HasDefaultValue(123); //This doesn't really matter, we just need it so EF doesn't try to insert a value when saving, which will cause an error
modelBuilder.Entity<Project>()
.HasMany(p => p.Files)
.WithOne(f => f.Project)
.HasForeignKey(k => new { k.RelationTable, k.RelationId })
.HasPrincipalKey(k => new { k.RelationTable, k.Id });
When you add the above codes and run the Add-Migration, it'll cause it to add the below codes, which will break the Update-Database command so you'll need to comment it out in the Up function.
//migrationBuilder.AddForeignKey(
// name: "FK_Files_Projects_RelationTable_RelationId",
// table: "Files",
// columns: new[] { "RelationTable", "RelationId" },
// principalTable: "Projects",
// principalColumns: new[] { "RelationTable", "Id" },
// onDelete: ReferentialAction.Cascade);
//migrationBuilder.AddForeignKey(
// name: "FK_Files_FileAccess_RelationTable_RelationId",
// table: "Files",
// columns: new[] { "RelationTable", "RelationId" },
// principalTable: "FileAccess",
// principalColumns: new[] { "RelationTable", "RelationId" },
// onDelete: ReferentialAction.Cascade);
You'll need to do the same with the Down function else you won't be able to roll back your changes.
//migrationBuilder.DropForeignKey(
// name: "FK_Files_Projects_RelationTable_RelationId",
// table: "Files");
//migrationBuilder.DropForeignKey(
// name: "FK_Files_FileAccess_RelationTable_RelationId",
// table: "Files");
Now you can do an Update-Database and it should run just fine. Running the app works perfectly fine as well. I'm able to use the EF method to get the Project with the associated files and worked with the FileAccess object as well. However, keep in mind that this is a hack and future versions of EF might not support it. Cheers!
I have a problem, I have to create a model where we have two entities which CAN be linked together but can also exist as stand alone entities.
The model currently looks like this:
public class Authorisation
{
public int AuthorisationID { get; set; }
public virtual Change Change { get; set; }
}
public class Change
{
public int ChangeID{ get; set; }
public virtual Authorisation Authorisation { get; set; }
public int? AuthorisationID{ get; set; }
}
The reason for this is that we can have an authorization record independent of a change, and some changes require authorisation and some dont, so neither side of the relationship is required.
I can configure this with the fluent API like so:
modelBuilder.Entity<Authorisation>()
.HasOptional(t => t.Change)
.WithOptionalPrincipal(t => t.Authorisation);
And alls well, Except that the migration that it creates looks like this
CreateTable(
"dbo.Changes",
c => new
{
ChangeID = c.Int(nullable: false, identity: true),
AuthorisationID = c.Int(),
Authorisation_AuthorisationID = c.Int(),
})
.PrimaryKey(t => t.ChangeID)
.ForeignKey("dbo.Authorisations", t => t.Authorisation_AuthorisationID)
.Index(t => t.Authorisation_AuthorisationID);
EF is deciding that its going to add a new column (Authorisation_AuthorisationID) for me to use as the FK between the two entities, what I really want to be able to do is to use the change.AuthorisationID property as the FK onto the Authorisation, I cannot find a way to configure this at all (Please note that I need the FK to be in the model - for consistency with the rest of the app more than anything else).
To sum up I need to be able to create a relationship between two entities where both sides of the relationship are optional and if possible I want to be able to define the FK column myself.
Am I just approaching this wrong? Ive been staring at the same block of code for so long I could be missing something simple.
Looks like explicit foreign key property is not supported for one-to-one relationships - there is no HasForeignKey Fluent API and also if you put ForeignKey attribute on the navigation property you get exception saying that multiplicity must be *.
So the only choice you have is to remove the explicit Change.AuthorisationID property and work only with navigation properties:
Model:
public class Authorisation
{
public int AuthorisationID { get; set; }
public virtual Change Change { get; set; }
}
public class Change
{
public int ChangeID{ get; set; }
public virtual Authorisation Authorisation { get; set; }
}
Configuration:
modelBuilder.Entity<Authorisation>()
.HasOptional(t => t.Change)
.WithOptionalPrincipal(t => t.Authorisation)
.Map(a => a.MapKey("AuthorisationID"));
To skip pain, go to EDIT below.
I'm using EF with AutoMapper IQueryableExtensions.
Two of my models are as follows:
public class Article
{
public int Key { get; set; }
public DateTimeOffset Created { get; set; }
public int? SemesterKey { get; set; }
public virtual Semester Semester { get; set; }
}
public class Semester
{
public int Key { get; set; }
public string Name { get; set; }
public virtual ICollection<Article> Articles { get; set; }
// Other relationships
public virtual ICollection<Subject> Subjects { get; set; }
public virtual ICollection<AppUser> Users { get; set; }
}
And I have the following DTOs:
public class ArticleDto
{
public int Key { get; set; }
public DateTimeOffset Created { get; set; }
// If I remove this (or ignore it), everything works.
public SemesterDto Semester { get; set; }
}
public class SemesterDto
{
public int Key { get; set; }
public string Name { get; set; }
}
Configuration:
Mapper.CreateMap<Semester, SemesterDto>().MaxDepth(1);
Mapper.CreateMap<Article, ArticleDto>().MaxDepth(1);
The query:
context.Articles.Include(a => a.Semester)
.OrderByDescending(a => a.Created)
.Take(5)
.ProjectTo<ArticleDto>()
.ToList();
Executing the query yields the following strange exception:
System.ArgumentException: Property 'NS.Models.Semester Semester' is not defined for type 'NS.Models.Semester'
This is some of the stack trace:
System.Linq.Expressions.Expression.Property(Expression expression, PropertyInfo property)
System.Linq.Expressions.Expression.MakeMemberAccess(Expression expression, MemberInfo member)
....
AutoMapper.MappingEngine.CreateMapExpression(ExpressionRequest request, Expression instanceParameter, IDictionary`2 typePairCount)
It seems as if AutoMapper generated an expression that is looking for a Semester property (in response to ArticleDto.Semester it seems) on the NS.Models.Semester type which of course doesn't exist.
I can get it working if I do the following:
Mapper.CreateMap<Article, ArticleDto>().MaxDepth(1)
.ForMember(a => a.Semester, c => c.MapFrom(a => a.Semester == null ? null : new SemesterDto() {
Key = a.Semester.Key,
Year = a.Semester.Year }));
But this is just the thing I'm trying to avoid writing by using AutoMapper!
It's probably something wrong on my side but I can't find anything suspicious.
EDIT:
I have the following ctors on SemesterDto:
public SemesterDto()
{
}
public SemesterDto(int key, string name)
{
Key = key;
Name = name;
}
When I remove the second one everything works. Seems this is it, this is really strange behavior though. I never thought the ctor would make a problem so I didn't include it for clarity, everything is possible isn't it. Sorry about that.
So, is this a bug from AutoMapper or is there something else I misunderstood?
EDIT 2:
Stripping this more, I tried mapping a Semester to SemesterDto directly and this is the result:
context.Semesters.ProjectTo<SemesterDto>().FirstOrDefault();
NotSupportedException: Only parameterless constructors and initializers are supported in LINQ to Entities.
This strengthens the idea that the ctor is causing strange behavior.
I can reproduce the issue in your second edit. Somehow (I don't know AutoMapper well enough to see why), the parametrized constructor always takes precedence over the parameterless one in AutoMapper. And EF doesn't support parametrized constructors in its expression tree.
Anyway, you can fix this issue by using ConstructProjectionUsing:
Mapper.CreateMap<Semester, SemesterDto>().MaxDepth(1)
.ConstructProjectionUsing(sem => new SemesterDto());
This has been confirmed as a bug, see this github issue. For now, I guess I'll just avoid ctors in my DTOs until the promised next release.
If you have v4.1.0 or above, then this should've been fixed.
Mapper.CreateMap<Article, Dto.Article>()
.ForMember(d => d.Semester, o => o.MapFrom(s => Mapper.Map<Dto.Semester>(s));
You need to tell it to use the Semester -> Dto.Semester mapping.
I have two models, One ApplicationUser which holds all users in the system and I have a Quotation model which will hold all Quotations made. now I want to store two mappings to ApplicationUser inside Quotations. So that I can map to created User as well as cancelled User. My model looks like this
public class Quotation
{
public int QuotationID { get; set; }
public DateTime QuotationDate { get; set; }
public DateTime QuotationCancelDate { get; set; }
public int ApplicationUserID { get; set; }
public virtual ApplicationUser CreatedUser { get; set; }
[ForeignKey("ApplicationUserID")]
public ApplicationUser CancelledUser { get; set; }
}
But this throws an error
Quotation_CancelledUser_Target_Quotation_CancelledUser_Source: : The types of all properties in the Dependent Role of a referential constraint must be the same as the corresponding property types in the Principal Role. The type of property 'ApplicationUserID' on entity 'Quotation' does not match the type of property 'Id' on entity 'ApplicationUser' in the referential constraint 'Quotation_CancelledUser'.
So I guess , The approach I am taking is wrong. Can anyone point out the correct way to achieve this?
The problem you are observing is called "Multiple Cascade Path". A Multiple Cascade Path happens when a cascade path goes from column col1 in table A to table B and also from column col2 in table A to table B.
The exception is caused by SQL Server when code first attempted to add table that has columns appearing more than once of another table.
In SQL Server, a table cannot appear more than one time in a list of all the cascading referential actions that are started by either a DELETE or an UPDATE statement. For example, the tree of cascading referential actions must only have one path to a particular table on the cascading referential actions tree.
You will need to use FluentAPI to configure the relationship. I am using EF5 currently and do not know if this can be accomplished in EF6/7.
So modifying your code sample, it would look like:
public class Quotation
{
public int QuotationID { get; set; }
public DateTime QuotationDate { get; set; }
public DateTime QuotationCancelDate { get; set; }
public int CreatedUserID { get; set; }
// Navigation property
public virtual ApplicationUser CreatedUser { get; set; }
public int CancelledUserID { get; set; }
// Navigation property
public virtual ApplicationUser CancelledUser { get; set; }
}
// Created a simple class for example
public class ApplicationUser
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
}
Now in you context class you can write:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
// Disable the default PluralizingTableNameConvention
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
// Add configuration here
modelBuilder.Entity<Quotation>()
.HasKey(e => e.QuotationID);
modelBuilder.Entity<ApplicationUser>()
.HasKey(e => e.Id);
modelBuilder.Entity<Quotation>()
.HasRequired(a => a.CreatedUser)
.WithMany()
.HasForeignKey(u => u.CreatedUserID);
modelBuilder.Entity<Quotation>()
.HasRequired(a => a.CancelledUser)
.WithMany()
.HasForeignKey(u => u.CancelledUserID);
}
For more information with example refer this link.