Given the following entity models:
public class Workshop
{
public Guid Id { get; set; }
public string Name { get; set; }
public ICollection<QuoteRequest> QuoteRequests { get; set; }
}
public class QuoteRequest
{
public Guid Id { get; set; }
public Guid CustomerId { get; set; }
public Guid WorkshopId { get; set; }
public bool Responded { get; set; }
public decimal? Amount { get; set; }
public virtual Customer Customer { get; set; }
public virtual Workshop Workshop { get; set; }
}
and the following 2 view models:
public class WorkshopModel
{
public Guid Id { get; set; }
public string Name { get; set; }
public ICollection<QuoteRequestModel> QuoteRequests { get; set; }
}
public class QuoteRequestModel
{
public Guid Id { get; set; }
public Guid CustomerId { get; set; }
public Guid WorkshopId { get; set; }
public bool Responded { get; set; }
public decimal? Amount { get; set; }
public CustomerModel Customer { get; set; }
public WorkshopModel Workshop { get; set; }
}
Next, given the following query:
public async Task<Workshop> GetWorkshopAsync(Guid id, bool includeQuotes = false)
{
IQueryable<Workshop> query = _context.Workshops;
if (includeQuotes)
{
query = query.Include(w => w.QuoteRequests);
}
return await query.FirstOrDefaultAsync(w => w.Id == id);
}
No matter what I do, I cannot get EF to not give me a circular relationship when querying a Workshop. For instance, I query one Workshop that has 14 QuoteRequests, each of which has a Workshop, each of which has 14 QuoteRequests etc. etc. :
I do have the json serializer reference loophandling setting set to ignore, but that's not giving me the desired result
services.AddControllers()
.AddNewtonsoftJson(x => x.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore);
I want to thus strip out this circle in my mapping profile. I am using automapper. I have managed to break the circular reference with my mapping profile from the QuoteRequest side:
CreateMap<QuoteRequestModel, QuoteRequest>()
.ForMember(dest => dest.Customer, opt => opt.MapFrom(src => src.Customer))
.ForMember(dest => dest.Workshop, opt => opt.MapFrom(src => src.Workshop))
.ForPath(dest => dest.Customer.QuoteRequests, opt => opt.MapFrom(src => new List<QuoteRequest>()))
.ForPath(dest => dest.Workshop.QuoteRequests, opt => opt.MapFrom(src => new List<QuoteRequest>()));
CreateMap<QuoteRequest, QuoteRequestModel>()
.ForMember(dest => dest.Customer, opt => opt.MapFrom(src => src.Customer))
.ForMember(dest => dest.Workshop, opt => opt.MapFrom(src => src.Workshop))
.ForPath(dest => dest.Customer.QuoteRequests, opt => opt.MapFrom(src => new List<QuoteRequestModel>()))
.ForPath(dest => dest.Workshop.QuoteRequests, opt => opt.MapFrom(src => new List<QuoteRequestModel>()));
This might be a bit of a jank solution, but it's working for now when I query an individual QuoteRequest. What I want to figure out is how to do the same in the mapping profile, from the Workshop side:
CreateMap<WorkshopModel, Workshop>()
.ForMember(dest => dest.QuoteRequests, opt => opt.MapFrom(src => src.QuoteRequests));
CreateMap<Workshop, WorkshopModel>()
.ForMember(dest => dest.QuoteRequests, opt => opt.MapFrom(src => src.QuoteRequests));
I can't really target each iteration of the QuoteRequests to set the Workshop a default value.
This is not an issue. What you're seeing is EF's object fix-up. Because it already has these entities in its object cache, it automatically "fixes up" the relationships on each entity, without querying anything again.
The only time this might be an issue is during serialization, as serializing will attempt to recursively drill-down indefinitely. However, depending on the serialization method, there's different ways to prevent such recursive serialization. Additionally, you should really not serialize entities, directly, anyways. Instead, you should map them over into DTO classes, where you would then define a more basic structure that wouldn't suffer from the same recursive issues. Then, you would serialize the DTO, rather than the entity.
Related
I have three entities as shown here:
public class Application
{
[Key]
public int ApplicationId { get; set; }
public string Name { get; set; }
public virtual ICollection<User> Users { get; set; }
}
public class User
{
[Key]
public int UserId { get; set; }
public string UserName { get; set; }
public virtual ICollection<Application> Applications { get; set; }
}
Join entity
public class UserApplication
{
public int UserId { get; set; }
public int ApplicationId { get; set; }
[ForeignKey("UserId")]
public User User { get; set; }
[ForeignKey("ApplicationId")]
public Application Application { get; set; }
}
OnModelCreating section =>
modelBuilder.Entity<User>()
.HasMany(x => x.Applications)
.WithMany(x => x.Users)
.UsingEntity(ua => ua.ToTable("UserApplication"));
modelBuilder.Entity<UserApplication>()
.HasKey(a=> new { a.ApplicationId, a.UserId});
Running the code is causing an error
invalid object name => ApplicationUser.
Note - while OnModelCreating only entity with wrong name is there. DB Has table with name UserApplication
You are using mixture of explicit and implicit join entity. I'm afraid EF Core assumes 2 separate many-to-many relationships with 2 separate tables. Note that by convention the implicit join entity name is {Name1}{Name2} with names being in ascending order, which in your case is ApplicationUser.
What you need is to use the the generic overload of UsingEntity fluent API and pass the explicit join entity type as generic type argument. Also configure the join entity there instead of separately. e.g.
modelBuilder.Entity<User>()
.HasMany(x => x.Applications)
.WithMany(x => x.Users)
.UsingEntity<UserApplication>(
// ^^^
ua => ua.HasOne(e => e.Application).WithMany().HasForeignKey(e => e.ApplicationId),
ua => ua.HasOne(e => e.User).WithMany().HasForeignKey(e => e.UserId),
ua =>
{
ua.ToTable("UserApplication");
ua.HasKey(a => new { a.ApplicationId, a.UserId });
});
I basically have three tables that I need to query information to get PersonNotes. I am using Entity Framwork Core 3.
Person Table
PersonNote Table
PersonNoteAttachment Table
One person can have many personnotes and one personnote can contain many PersonNoteAttachment.
I need Person table to get the FirstName and LastName which is mapped to the AuthorName in the PersonNote User data model. You can see the mapping section which shows the mapping.
DataModels
namespace Genistar.Organisation.Models.DataModels
{
[Table(nameof(PersonNote), Schema = "common")]
public class PersonNote
{
public int Id { get; set; }
public int PersonId { get; set; }
[ForeignKey("PersonId")]
public Person Person { get; set; }
public string Note { get; set; }
public int AuthorId { get; set; }
[ForeignKey("AuthorId")]
public Person Author { get; set; }
public string CreatedBy { get; set; }
public DateTime Created { get; set; }
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public DateTime RecordStartDateTime { get; set; }
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public DateTime RecordEndDateTime { get; set; }
}
}
namespace Genistar.Organisation.Models.DataModels
{
[Table(nameof(PersonNoteAttachment), Schema = "common")]
public class PersonNoteAttachment
{
public int Id { get; set; }
public int PersonNoteId { get; set; }
[ForeignKey("PersonNoteId")]
public PersonNote PersonNote { get; set; }
public string Alias { get; set; }
public string FileName { get; set; }
public string MimeType { get; set; }
public int Deleted { get; set; }
public string CreatedBy { get; set; }
public DateTime Created { get; set; }
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public DateTime RecordStartDateTime { get; set; }
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public DateTime RecordEndDateTime { get; set; }
}
}
User Model - This is the model that I am returning to the client application
namespace Genistar.Organisation.Models.User
{
[Table(nameof(PersonNote), Schema = "common")]
public class PersonNote
{
public int Id { get; set; }
public int PersonId { get; set; }
public string Note { get; set; }
public int AuthorId { get; set; }
public string AuthorName { get; set; }
public string CreatedBy { get; set; }
public DateTime Created { get; set; }
}
}
Mapping
CreateMap<Genistar.Organisation.Models.DataModels.PersonNote, Genistar.Organisation.Models.User.PersonNote>()
.ForMember(t => t.Id, opt => opt.MapFrom(s => s.Id))
.ForMember(t => t.PersonId, opt => opt.MapFrom(s => s.PersonId))
.ForMember(t => t.AuthorName, opt => opt.MapFrom(s => s.Author.FirstName + " " + s.Author.LastName))
.ForMember(t => t.Note, opt => opt.MapFrom(s => s.Note))
.ForMember(t => t.AuthorId, opt => opt.MapFrom(s => s.AuthorId))
.ForMember(t => t.CreatedBy, opt => opt.MapFrom(s => s.CreatedBy))
.ForMember(t => t.Created, opt => opt.MapFrom(s => s.Created));
The following query works but is only pulling data from Person and PersonNote table. I am looking at getting the PersonNoteAttachment as well. How do I do that ? I would basically need FileName & MimeType
field populated in User.PersonNote model. If you see above I have created a PersonNoteAttachment data model
Repository
public IQueryable<PersonNote> GetPersonNotes(int personId)
{
var personNotes = _context.PersonNotes.Include(x => x.Person).Include(x=> x.Author).Where(p => p.PersonId == personId);
return personNotes;
}
API :
[FunctionName(nameof(GetPersonNote))]
[UsedImplicitly]
public Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "person-note/{id}")] HttpRequest req,
int id) => _helper.HandleAsync(async () =>
{
//await _helper.ValidateRequestAsync(req, SecurityPolicies.ViewNotes);
var personNotes = await _organisationRepository.GetPersonNotes(id).ProjectTo<PersonNote>(_mapper.ConfigurationProvider).ToListAsync();
return new OkObjectResult(personNotes);
});
My approach was to do it the following way in the repository but I need to return the PersonNote datamodel in the repository. I cannot add those additional fields in the model because it say invalid columns.How do I approach this ?
var personNotes = _context.PersonNotes
.Include(x => x.Person)
.Include(x => x.Author)
.Where(p => p.PersonId == personId)
.Join(_context.PersonNotesAttachments, c => c.Id, cm => cm.PersonNoteId, (c, cm) => new
{
cm.PersonNote.Id,
cm.PersonNote.PersonId,
cm.PersonNote.Person,
cm.PersonNote.Note,
cm.FileName,
cm.MimeType,
cm.Alias,
cm.PersonNote.AuthorId,
cm.PersonNote.CreatedBy,
cm.PersonNote.Created
});
I have resolved the issue
I just had to add the following line in PersonNote datamodel
public PersonNoteAttachment PersonNoteAttachment { get; set; }
Added the new fields to the PersonNote usermodel and did the following mapping
.ForMember(t => t.FileName, opt => opt.MapFrom(s => s.PersonNoteAttachment.FileName))
.ForMember(t => t.MimeType, opt => opt.MapFrom(s => s.PersonNoteAttachment.MimeType))
.ForMember(t => t.Alias, opt => opt.MapFrom(s => s.PersonNoteAttachment.Alias))
I have two entities and their relationship is many to many. The code is like below:
public class Post : Entity
{
//public int Id { get; set; }
public string Title { get; set; }
public string Body { get; set; }
public DateTime CreateTime { get; set; }
public DateTime LastModify { get; set; }
public ICollection<PostTag> PostTags { get; set; }
public int AuthorId { get; set; }
public Author Author { get; set; }
}
public class Tag :Entity
{
//public int Id { get; set; }
public string TagName { get; set; }
public ICollection<PostTag> PostTags { get; set; }
}
public class PostTag :Entity
{
//public int Id { get; set; }
public int PostId { get; set; }
public Post Post { get; set; }
public int TagId { get; set; }
public Tag Tag { get; set; }
}
public class PostViewModel
{
public string Title { get; set; }
public string Body { get; set; }
public DateTime CreateTime { get; set; }
public DateTime LastModify { get; set; }
public int AuthorId { get; set; }
public string AuthorName { get; set; }
public List<TagViewModel> Tags { get; set; }
}
Now, I had created the mapping configure from entity to viewmodel:
CreateMap<Post, PostViewModel>()
.ForMember(dest => dest.AuthorId, opt => opt.MapFrom(src => src.Author.Id))
.ForMember(dest => dest.Tags, opt => opt.MapFrom(src => src.PostTags))
.ForMember(dest => dest.AuthorName, opt => opt.MapFrom(src => src.Author.User.LastName))
.ForMember(dest => dest.Body, opt => opt.MapFrom(src => src.Body))
.ForMember(dest => dest.Title, opt => opt.MapFrom(src => src.Title))
.ForMember(dest => dest.CreateTime, opt => opt.MapFrom(src => src.CreateTime))
.ForMember(dest => dest.LastModify, opt => opt.MapFrom(src => src.LastModify))
.ForMember(dest => dest.Tags, opt => opt.MapFrom(src => src.PostTags.Select(y => y.Tag).ToList()));
My question is, when I receive a PostViewModel like:
public async Task<IActionResult> Post([FromBody] PostViewModel postViewModel)
{
}
how can I map the PostViewModel to Post? And I want to create an Entity from the PostViewModel and add them, which is post, tag(if not exist) and postTag relationship, into DbContext, what should I do?
I am pretty new to automapper and this is my first question, I hope I am doing right thing and I really appreciate your answer and guid for how to ask.
You have two choices I think.
1) Reverse mapping
You can just add .ReverseMap() to the end of your mapping config
CreateMap<Post, PostViewModel>()
.ForMember(dest => dest.AuthorId, opt => opt.MapFrom(src => src.Author.Id))
...
.ForMember(dest => dest.Tags, opt => opt.MapFrom(src => src.PostTags.Select(y => y.Tag).ToList()))
.ReverseMap();
2) You can define a new configuration for PostViewModel to Post
CreateMap<PostViewModel, Post>()
.ForMember(dest => dest.Author.Id, opt => opt.MapFrom(src => src.AuthorId))
...etc
Then Mapper.Map<Post>(postViewModel)
I have been getting error when I try to run add-migration script.
The error is:
Domain.DataAccessLayer.AllRoutines_Product: : Multiplicity conflicts with the referential constraint in Role 'AllRoutines_Product_Target' in relationship 'AllRoutines_Product'. Because all of the properties in the Dependent Role are non-nullable, multiplicity of the Principal Role must be '1'.
And I cannot figure what I am doing wrong. I have AllRoutines and Product entities. AllRoutines can have 0 or 1 Products. Here is my AllRoutines class (some code has been omitted for clarity):
public class AllRoutines
{
[Key]
public long Id { get; set; }
public string Name { get; set; }
public string Tags { get; set; }
public int? RoutineLevelId { get; set; }
public RoutineLevel RoutineLevel { get; set; }
public Guid? ProductId { get; set; }
public virtual Product Product { get; set; }
}
Here is FluetnApi mapping (again some code is omitted):
public void MapRoutine(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<RoutineLevel>().HasKey(r => r.RoutineLevelId);
modelBuilder.Entity<AllRoutines>()
.HasKey(r => r.Id)
.Property(r => r.Id)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
modelBuilder.Entity<AllRoutines>()
.HasOptional(r => r.RoutineLevel)
.WithMany()
.HasForeignKey(r => r.RoutineLevelId);
modelBuilder.Entity<AllRoutines>().HasOptional(c => c.Product)
.WithMany()
.HasForeignKey(c => c.ProductId)
.WillCascadeOnDelete(false);
}
Also I am not sure if this is important or not, but there is also class CustomRoutine which inherits AllRoutines and looks like this:
public class CustomRoutine : AllRoutines
{
public DateTime DateCreated { get; set; }
public string UserId { get; set; }
public User UserWhoCreatedRoutine { get; set; }
}
The inheritance approach was Table per Hierarchy.
I've tried to add to mapping configuration this:
modelBuilder.Entity<CustomRoutine>().HasOptional(c => c.Product)
.WithMany()
.HasForeignKey(c => c.ProductId)
.WillCascadeOnDelete(false);
But the error was same. I am not sure why this is happening, because, as you can see in the code same mapping was already done (without any problems) for RoutineLevel, also I have same mapping for Product and the other class, again with no problems.
EDIT
Here is also Product class:
public class Product
{
public Guid Id { get; protected set; }
public string Code { get; set; }
public string Name { get; set; }
public bool IsFree { get; set; }
public ICollection<SubscriptionProduct> SubscriptionProducts { get; set; }
}
And FluentAPI mapping:
modelBuilder.Entity<Product>()
.HasKey(p => p.Id);
modelBuilder.Entity<Product>()
.Property(p => p.Code)
.IsRequired()
.HasMaxLength(10);
modelBuilder.Entity<Product>()
.Property(p => p.Name)
.IsRequired()
.HasMaxLength(100);
modelBuilder.Entity<Product>()
.HasMany(p => p.SubscriptionProducts)
.WithRequired()
.HasForeignKey(p => p.ProductId)
.WillCascadeOnDelete(true);
I have the following classes that I would really like to map correctly in EF:
internal class Wallet : EntityFrameworkEntity
{
public Wallet()
{
this.Requests = new List<FinancialRequest>();
}
public string Name { get; set; }
public string Description { get; set; }
public decimal CurrentBalance { get; set; }
public decimal BlockedBalance { get; set; }
public virtual ICollection<Paper> Papers { get; set; }
public virtual ICollection<FinancialRequest> Requests { get; set; }
public virtual User Manager { get; set; }
}
internal class Request : EntityFrameworkEntity
{
public Int64 UserId { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public RequestStatus Status { get; set; }
public virtual User User { get; set; }
}
internal class FinancialRequest : Request
{
public DateTime ValidUntil { get; set; }
public FinancialRequestType RequestType { get; set; }
public int Quantity { get; set; }
public bool UseMarketValue { get; set; }
public decimal? Value { get; set; }
public virtual Wallet Source { get; set; }
public virtual Wallet Destination { get; set; }
public virtual Team Team { get; set; }
}
I'm using Code First, so this is my method that maps those classes:
modelBuilder.Entity<Wallet>()
.HasMany(x => x.Requests)
.WithOptional();
modelBuilder.Entity<Wallet>()
.HasMany(x => x.Papers)
.WithOptional(x => x.Owner)
.Map(configuration => configuration.MapKey("OwnerId"));
modelBuilder.Entity<Wallet>()
.HasMany(x => x.Requests)
.WithOptional();
modelBuilder.Entity<Request>().ToTable("Requests");
modelBuilder.Entity<FinancialRequest>().ToTable("FinancialRequests");
modelBuilder.Entity<FinancialRequest>()
.HasRequired(x => x.Team)
.WithOptional()
.Map(configuration => configuration.MapKey("TeamId"));
modelBuilder.Entity<FinancialRequest>()
.HasOptional(x => x.Destination)
.WithOptionalDependent()
.Map(configuration => configuration.MapKey("DestinationWalletId"));
modelBuilder.Entity<FinancialRequest>()
.HasRequired(x => x.Source)
.WithRequiredDependent()
.Map(configuration => configuration.MapKey("SourceWalletId"));
If I leave this mapping the way it's now, my database schema looks like this:
If you look carefully, you'll see that there's a column called "Wallet_Id" that it's not suposed to be there. This column only exists because the Wallet class has the "Requests" collection.
If I remove the collection from the the columns goes away, but I need this collection! It representes a importante relation between the classes. What I don't need is the 3rd column in the database wrongly generated.
Does anybody knows how can I avoid this? What am I doing wrong here?
The problem that causes the redundant foreign key column Wallet_Id is that EF doesn't know if the Wallet.Requests collection is the inverse navigation property of FinancialRequest.Source or FinancialRequest.Destination. Because it cannot decide between the two EF assumes that Wallet.Requests doesn't have an inverse navigation property at all. The result is a third redundant one-to-many relationship with the third FK.
Basically you have three options:
Remove the Wallet.Requests collection and the third relationship will disappear (as you already have noticed). But you don't want that.
Tell EF explicitly if Wallet.Requests has Source or Destination as inverse navigation property:
// Remove the modelBuilder.Entity<Wallet>().HasMany(x => x.Requests) mapping
modelBuilder.Entity<FinancialRequest>()
.HasOptional(x => x.Destination)
.WithMany(x => x.Requests)
.Map(config => config.MapKey("DestinationWalletId"));
modelBuilder.Entity<FinancialRequest>()
.HasRequired(x => x.Source)
.WithMany()
.Map(config => config.MapKey("SourceWalletId"));
Use WithMany(x => x.Requests) in one of the two (Destination in the example, it could also be Source), but not in both.
Introduce a second collection in Wallet and map the two collections to Source and Destination respectively:
internal class Wallet : EntityFrameworkEntity
{
public Wallet()
{
this.SourceRequests = new List<FinancialRequest>();
this.DestinationRequests = new List<FinancialRequest>();
}
// ...
public virtual ICollection<FinancialRequest> SourceRequests { get; set; }
public virtual ICollection<FinancialRequest> DestinationRequests { get; set; }
}
Mapping:
// Remove the modelBuilder.Entity<Wallet>().HasMany(x => x.Requests) mapping
modelBuilder.Entity<FinancialRequest>()
.HasOptional(x => x.Destination)
.WithMany(x => x.DestinationRequests)
.Map(config => config.MapKey("DestinationWalletId"));
modelBuilder.Entity<FinancialRequest>()
.HasRequired(x => x.Source)
.WithMany(x => x.SourceRequests)
.Map(config => config.MapKey("SourceWalletId"));
BTW: Shouldn't both Source and Destination be required? If yes, you can replace the HasOptional by HasRequired but you must append WillCascadeOnDelete(false) to at least one of the two mappings to avoid a multiple cascading delete path exception.