Automapper one-to-many relationship - c#

I have a method that creates a Course. The Course retains the Teacher and their id. But after adding the Course, TeacherID has the value but Teacher has null. I think the problem is in the mapping. CourseAddRequest only has a teacherID, how can I add a Teacher?
AddCourse:
public CourseResponse AddCourse(CourseAddRequest courseAddRequest, Guid teacherId)
{
var teacher = _uniDbContext.Teachers
.Include(t => t.Courses)
.SingleOrDefault(t => t.Id == teacherId);
if (teacher == null)
throw new Exception("User doesn't exist");
var course = _mapper.Map<Course>(courseAddRequest);
teacher.Courses.Add(course);
_uniDbContext.Teachers.Update(teacher);
_uniDbContext.Courses.Update(course);
_uniDbContext.Courses.Add(course);
_uniDbContext.SaveChanges();
return _mapper.Map<CourseResponse>(course);
}
Course:
public class Course : BaseEntity
{
public string Header { get; set; }
public string Description { get; set; }
public Guid TeacherId { get; set; }
public Teacher Teacher { get; set; }
public List<Student> StudentsOnCourse { get; set; } = new List<Student>();
}
CourseResponse:
public class CourseResponse
{
public Guid Id { get; set; }
public string Header { get; set; }
public string Description { get; set; }
public TeacherResponse Teacher { get; set; }
public Guid TeacherId { get; set; }
public IEnumerable<StudentResponse> Students { get; set; }
}
CourseAddRequest:
public class CourseAddRequest
{
public string Header { get; set; }
public string Description { get; set; }
public Guid TeacherId { get; set; }
}
CourseProfile:
public class CourseProfile : Profile
{
public CourseProfile()
{
CreateMap<CourseAddRequest, Course>();
CreateMap<Course, CourseResponse>();
}
}
TeacherProfile:
public class TeacherProfile : Profile
{
public TeacherProfile()
{
CreateMap<TeacherAddRequest, Teacher>();
CreateMap<Teacher, TeacherResponse>();
}
}

You guessed right. When you execute the mapping from CourseAddRequest to Course it has no Teacher Therefore the Course teach will be null.
var course = _mapper.Map<Course>(courseAddRequest);
Assuming you're using EntityFramework or another ORM it'll be able to do the insertion correctly due to the existence of the Teacher that you've referenced in the Course via TeacherId property.
And while you add the Course to the teacher's in line 11 of your method, This still leaves the Teacher property null in course. As a result, when you map it to CourseResponse you get null.
There's two way to fix this, First, you can add the teach to your course object So the mapper finds the teacher before mapping to CourseResponse in the return statement.
course.Teacher = teacher;
Or map the teacher object directly to the response.
var courseResponse = _mapper.Map<CourseResponse>(course);
courseResponse.Teacher = _mapper.Map<TeacherResponse>(teacher);
return courseResponse;

Related

Handling Nested Objects in Entity Framework

I am struggling a bit to wrap my head around Entity Framework and It's driving me crazy. I have an target object that I'd like to populate:
public class ApiInvitationModel
{
public int Id { get; set; }
public EventModel Event { get; set; }
public UserModel InvitationSentTo { get; set; }
public UserModel AttendingUser { get; set; }
}
The schemas of the above models are:
public class EventModel {
public int Id? { get; set; }
public string Name { get; set; }
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set }
public OrganizationModel HostingOrganization { get; set; }
public Venue Venue { get; set; }
public string Price { get; set; }
}
public class UserModel {
public int Id? { get; set; }
public string Name { get; set; }
public string PhoneNumber { get; set; }
public string MobileNumber { get; set; }
public List<OrganizationModel> Organizations { get; set; }
}
public class OrganizationModel {
public int Id? { get; set; }
public stirng Name { get; set; }
public string Address { get; set; }
public UserModel PrimaryContact { get; set; }
}
The above schemas are simplified for the purpose of the question and are the models we intend to return via API.
The problem is the origin schemas in the database is very different and I'm trying to map the database objects to these objects via Entity Framework 6.
My attempted solution was to try and nest the models via a query but that didn't work and I'm not sure where to go from here besides making numerous calls to the database.
public List<ApiInvitationModel> GetInvitations(int userId) {
using (var entities = new Entities()) {
return entities.EventInvitations
.Join(entities.Users, invitation => invitiation.userId, user => user.id, (invitation, user) => new {invitation, user})
.Join(entities.Events, model => model.invitation.eventId, ev => ev.id, (model, ev) => new {model.invitation, model.user, ev})
.Join(entities.organization, model => model.user.organizationId, organization => organization.id, (model, organization) => new ApiInvitationModel
{
Id = model.invitation.id,
Event = new EventModel {
Id = model.event.id,
Name = model.event.name,
StartDate = model.event.startDate,
EndDate = model.event.endDate,
HostingOrganization = new OrganizationModel {
Id = model.invitation.hostingId,
Name = model.event.venueName,
Address = model.event.address,
PrimaryContact = new UserModel {
Name = model.event.contactName,
PhoneNumber = model.event.contactNumber,
}
}
...
},
InvitedUser = {
}
}
).ToList();
}
}
As you can see above, there's quite a bit of nesting going on but this doesn't work in Entity Framework 6 as far as I am aware. I keep getting the following errors:
"The type 'Entities.Models.API.UserModel' appears in two structurally incompatible initializations within a single LINQ to Entities query. A type can be initialized in two places in the same query, but only if the same properties are set in both places and those properties are set in the same order.",
Based on the above error, I assumed that each of the model initiatilizations would need to be the same (i.e. initializing the values as the same ApiInvitationModel in each join in the same order) but that produces the same error.
What would be the best approach to handling this, keepign in mind the source database doesn't have foreign keys implemented?

Checking conditions using Automapper

I am using Automapper. In that, I mapped the DTO with the database table. In that one, I need to check one condition and then take the value.
CreatedBy = mapper.Map<UserProperties>((from createdByUser in context.persons.Where(x => x.IsActive && x.Id == notes.CreatedBy) select createdByUser).FirstOrDefault())
This is my code.
User Properties Class:
public string DisplayName { get; set; }
public int Id { get; set; }
public bool IsUser { get; set; }
public int NotesCount {get;set;}
Persons
public string DisplayName { get; set; }
public int Id { get; set; }
public int RoleId{ get; set; }
public int NotesCount {get;set;}
public string Notes{get;set;}
public string Comments {get;set;}
The below code is the automapper configuration in the start up file.
Mapping Profile Class
In persons, have the field roleId. I need to assign the values for the IsUser field in the User properties class by checking the condition like RoleId field in Persons is equal to 2.
How to Check the condition using the automapper?
Automapper Version: 9.0.0
You need to add a ForMember clause to your mapping to add the condition - here's a working example (which took longer than it should have, because you posted an image of your code instead of the actual code. This is why on SO you should always post code, not images.)
void Main()
{
var mapperConfig =
new MapperConfiguration(mc => mc.AddProfile<MappingProfile>());
var mapper = mapperConfig.CreateMapper();
var notAUser = new Persons { RoleId = 1};
var isAUser = new Persons { RoleId = 2};
var shouldBeNotAUser = mapper.Map<UserProperties>(notAUser);
var shouldBeAUser = mapper.Map<UserProperties>(isAUser);
Console.WriteLine(shouldBeNotAUser.IsUser);
Console.WriteLine(shouldBeAUser.IsUser);
}
public class MappingProfile : Profile
{
public MappingProfile()
{
CreateMap<Persons, UserProperties>()
.ForMember(destination => destination.IsUser,
options => options.MapFrom(src => src.RoleId == 2));
}
}
class UserProperties
{
public string DisplayName { get; set; }
public int Id { get; set; }
public bool IsUser { get; set; }
public int NotesCount { get; set; }
}
class Persons
{
public string DisplayName { get; set; }
public int Id { get; set; }
public int RoleId { get; set; }
public int NotesCount { get; set; }
public string Notes { get; set; }
public string Comments { get; set; }
}
Output:
False
True
However
Your mapping configuration code should not have to 'know' about what RoleID indicates a user. Your Person class should be where that knowledge is held, so that should have either an IsUser() method or a get-only IsUser property (with the NotMapped attribute) which returns RoleId == 2: in the former case you would still need ForMember but in the latter case you wouldn't, though if you do map back from UserProperties to Persons you would need something there to handle it - again, this should be in the Persons class and not in the mapper config. Maybe SetAsUser() that sets the RoleId.

EF Core returns only first record of a list unless FK entities are reset

I am facing the same issue as described in this question. Problem: my method GetAllConferences() returns correctly all the conferences from the DB, but when I return the result to the View from the controller return Ok(tripListVm) inly the first collection item is returned to the client. On the otehr side, by setting to null all the FK references (as pointed out in the SO question above) I can return correctly all the entities to the client, however this does not seem to me the proper way of proceeding.
EDIT: the solution was much simpler than I though. In the code below (I leave it in its original form for others to see it) I was not mapping the FK entities inside the ViewModel to Dto objects, but returning the model entity itself. That was the reason why I needed to null those inner references to make it work. By returning all Dtos objects, it works properly.
I have three entities involved with 1-many relationships:
public class Conference
{
public int Id { get; set; }
[Required]
[MaxLength(50)]
public string Name { get; set; }
public ICollection<Venue> Venues { get; set; }
public int? LocationId { get; set; }
public Location Location { get; set; }
}
public class Venue
{
public int Id { get; set; }
[Required]
[MaxLength(50)]
public string Name { get; set; }
public int? ConferenceId { get; set; }
public Trip Conference { get; set; }
public int? LocationId { get; set; }
public City City { get; set; }
}
public class City
{
public int Id { get; set; }
[Required]
[MaxLength(50)]
public string Name { get; set; }
public ICollection<Conference> Conferences { get; set; }
public ICollection<Venue> Venues { get; set; }
}
In the repository, I have a method that returns the conferences and the related entities (City and Venues):
public IEnumerable<Conference> GetAllConferences()
{
return _context.Conferences
.Include(t => t.Venues)
.Include(t => t.City)
.ToList();
}
In the controller I need to use the following code to return all the results:
var conferences = _repository.GetAllConferences();
if (conferences.Any())
{
var conferenceListVm = trips.ToConferenceVmList();
//Without setting the FK references to null, I can return only the first result of the collection
foreach (var vm in conferenceListVm)
{
foreach (var pm in vm.PoinOfInterests)
{
pm.Trip = null;
}
vm.Location.Conferences = null;
vm.Location.Venues = null;
}
return Ok(conferenceListVm);
}
public static ConferenceViewModel ToConferenceVm(this Conference conference)
{
var confVm = new ConferenceViewModel();
confVm.Name = conference.Name;
confVm.City = conference.City;
confVm.Venues = conference.Venues;
return tripVm;
}
public static IEnumerable<ConferenceViewModel> ToConferenceVmList(this IEnumerable<Conference> conferences)
{
return conferences.Select(c => c.ToConferenceVm()).ToList();
}

What's the best practice for associating many-to-many table with another table?

I'm running into this scenario where I'm not sure exactly what the proper way to solve it is. I have a many-to-many table.
ProductsTable <--------> DrugsTable
ProductsTable
|----- ProductID
|----- ProductName
DrugsTable
|----- DrugID
|----- DrugStrength
|----- GCNSeqNumber
The idea is that a ProductName can have many DrugStrength or GCNSequNumber, and one GCNSeqNumber can have many ProductName.
So, to create this, I created the Model class as follows:
public class ProductsTable
{
public int ProductID { get;set; }
public string ProductName { get; set; }
public ICollection<DrugsTable> Drugs { get; set; }
public ProductsTable()
{
this.Drugs = new HashSet<DrugsTable>();
}
}
public partial class DrugsTable
{
public int DrugID { get; set; }
public string Strength { get; set; }
public string GCNSeqNo { get; set; }
public virtual ICollection<ProductsTable> Products { get; set; }
}
And then I mapped it:
modelBuilder.Entity<ProductsTable>()
.HasMany(c => c.Drugs).WithMany(i => i.Products)
.Map(t => t.MapLeftKey("ProductID")
.MapRightKey("DrugID")
.ToTable("RxTable"));
So far so good. .NET creates another table called RxTable and stores the IDs from DrugsTable and ProductsTable. Now the issue is, the RxTable doesn't have an id field. I need to have another table that contains the basic information on the claims (the date, person, as such). What's the best practice of associating another table with these many-to-many table? If I create the RxTable manually, would .NET be able to populate the table automatically? How?
Edit made in response to Luca Ghersi's response:
RxTable Rx = new RxTable();
ProductsTable Product = db.Products.Where(x => x.ProductName == productName).FirstOrDefault();
DrugsTable Drug = db.Drugs.Where(x => x.GCNSeqnumber == gcnSeqnumber).FirstOrDefault();
if (Product == null)
{
Product = AddProduct(productName);
}
if(Drug == null)
{
Drug = AddDrug(strength, gcnSeqnumber);
}
Rx = (RxTable)db.RxTable.Where(x => x.ProductId == Product.ProductId && x.DrugId == Drug.DrugId).FirstOrDefault();
if (Rx == null)
{
Rx.Drugs = Drug;
Rx.Products = Product;
db.RxTable.Add(Rx);
}
return Rx;
Actually you can simply add a autogenerated ID column to the RxTable (I mean, you can create the table by yourself on db with the ID column) an use this column as you please. EF will be able to "ignore" that column. Obviously, if you are going to put a FK on it, EF will ignore that and will simply crash if you are going to delete a record from that table without removing the linked records first.
What about creating a three key mapping table?
Like:
public class ProductsTable
{
public int ProductID { get; set; }
public string ProductName { get; set; }
public ICollection<RxTable> RxTable { get; set; }
}
public partial class DrugsTable
{
public int DrugID { get; set; }
public string Strength { get; set; }
public string GCNSeqNo { get; set; }
public virtual ICollection<RxTable> RxTable { get; set; }
}
public class Person
{
public int PersonID { get; set; }
public virtual ICollection<RxTable> RxTable { get; set; }
}
public class RxTable
{
[Key, Column(Order = 0)]
public int ProductID { get; set; }
[Key, Column(Order = 1)]
public int DrugID { get; set; }
[Key, Column(Order = 2)]
public int PersonID { get; set; }
public virtual ProductsTable ProductsTable { get; set; }
public virtual DrugsTable DrugsTable { get; set; }
public virtual Person Person { get; set; }
}
It's definitively harder to manage, but it's the best way to have everything under control in the code, instead on relying on out-of-EF-control tricks on SQL Server.
Hope it helps!

How can I tell what subclass was saved?

I have a model with 2 subclasses:
public class User
{
public string eRaiderUsername { get; set; }
public int AllowedSpaces { get; set; }
public ContactInformation ContactInformation { get; set; }
public Ethnicity Ethnicity { get; set; }
public Classification Classification { get; set; }
public Living Living { get; set; }
}
public class Student : User
{
public Student()
{
AllowedSpaces = AppSettings.AllowedStudentSpaces;
}
}
public class OrganizationRepresentative : User
{
public Organization Organization { get; set; }
public OrganizationRepresentative()
{
AllowedSpaces = AppSettings.AllowedOrganizationSpaces;
}
}
public class ContactInformation
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string EmailAddress { get; set; }
public string CellPhoneNumber { get; set; }
}
public enum Ethnicity
{
AfricanAmerican,
AmericanIndian,
Asian,
Black,
Hispanic,
NativeHawaiian,
NonResidentAlien,
White
}
public enum Classification
{
Freshman,
Sophomore,
Junior,
Senior,
GraduateStudent
}
public enum Living
{
OnCompus,
OffCampus
}
This is (mostly) saving fine using these initializers:
var students = new List<Student>
{
new Student{ eRaiderUsername="somestudent", ContactInformation=new ContactInformation{FirstName="Some", LastName="Student", EmailAddress="student#example.com", CellPhoneNumber="1234567890"}, Classification=Classification.Junior, Ethnicity=Ethnicity.Hispanic, Living=Living.OffCampus }
};
students.ForEach(s => context.Users.Add(s));
context.SaveChanges();
var orgReps = new List<OrganizationRepresentative>
{
new OrganizationRepresentative{ eRaiderUsername="somerep", ContactInformation=new ContactInformation{FirstName="Some", LastName="Representative", EmailAddress="orgrep#example.com", CellPhoneNumber="0987654321"}, Classification=Classification.Freshman, Ethnicity=Ethnicity.White, Living=Living.OnCompus, Organization=context.Organizations.Find(1) }
};
orgReps.ForEach(o => context.Users.Add(o));
context.SaveChanges();
None of the enums are saving (advice on this would be awesome too). But everything else is saving fine.
I have noticed Entity has added a Discriminator column with the subclass names. How do I use this to query only students, only organization reps, or just tell if the current object is a student or organization rep in a controller or view?
The discriminator column is used internally by EF to determine the type of object to instantiate.
For example you could query for a student directly. context.Set<Student>.Find(id). The same is true for an org rep. Or you could query for any user context.Set<User>.Find(id).
If you query for a student, but pass an org rep's ID, then EF will return null, because the ID doesn't belong to a student.

Categories