Automapper child collection loses foreign key id while updating - c#

I am trying to map entity and it's child collection. First I retrieve entity and child collection from database:
public async Task<int> Update(EmailTemplateModel model, int id)
{
var emailTemplate = _context.EmailTemplates.Include("EmailTemplateComponents").Include("Site").FirstOrDefault(x => x.Id == id);
_mapper.Map(model, emailTemplate);
return await _context.SaveChangesAsync();
}
then I use mapper, but in this case I lose EmailTemplate link in EmailTemplateComponents and I get an error:
"$id": "1",
"Message": "An error has occurred.",
"ExceptionMessage": "The operation failed: The relationship could not be changed because one or more of the foreign-key properties is
non-nullable. When a change is made to a relationship, the related
foreign-key property is set to a null value. If the foreign-key does
not support null values, a new relationship must be defined, the
foreign-key property must be assigned another non-null value, or the
unrelated object must be deleted.",
"ExceptionType": "System.InvalidOperationException",
My entities are this:
[Serializable]
public class EmailTemplate : EntityBase
{
[Key]
public int Id { get; set; }
public string Title { get; set; }
public string AuthorDisplayName { get; set; }
public int UserId { get; set; }
public bool IsDeleted { get; set; }
public virtual List<EmailTemplateComponent> EmailTemplateComponents { get; set; }
//Site
public int SiteId { get; set; }
[ForeignKey("SiteId")]
public virtual Site Site { get; set; }
}
[Serializable]
public class EmailTemplateComponent : EntityBase
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public bool IsTop { get; set; }
public bool IsBottom { get; set; }
public bool AlignRight { get; set; }
public string Text { get; set; }
public string Title { get; set; }
public string Url { get; set; }
public string ImageUrl { get; set; }
//Email template
public int EmailTemplateId { get; set; }
[ForeignKey("EmailTemplateId")]
public virtual EmailTemplate EmailTemplate { get; set; }
}
And mapping is this:
public class EmailTemplateProfile : Profile
{
protected override void Configure()
{
CreateMap<EmailTemplateModel, EmailTemplate>()
.ForMember(dest => dest.AuthorDisplayName, opts => opts.MapFrom(src => src.Author))
.ForMember(dest => dest.EmailTemplateComponents, opts => opts.MapFrom(src => src.Components))
.ForMember(dest => dest.Title, opts => opts.MapFrom(src => src.Title))
.IgnoreAllNonExisting()
.IgnoreAllNonExistingSource();
CreateMap<EmailTemplate, EmailTemplateModel> ()
.ForMember(dest => dest.Author, opts => opts.MapFrom(src => src.AuthorDisplayName))
.ForMember(dest => dest.Components, opts => opts.MapFrom(src => src.EmailTemplateComponents))
.ForMember(dest => dest.Title, opts => opts.MapFrom(src => src.Title))
.IgnoreAllNonExisting()
.IgnoreAllNonExistingSource();
CreateMap<EmailTemplateComponent, EmailTemplateComponentModel>()
.ForMember(dest => dest.Id, opts => opts.MapFrom(src => src.Id))
.ForMember(dest => dest.EmailTemplateId, opts => opts.MapFrom(src => src.EmailTemplateId))
.ForMember(dest => dest.Name, opts => opts.MapFrom(src => src.Name))
.ForMember(dest => dest.IsTop, opts => opts.MapFrom(src => src.IsTop))
.ForMember(dest => dest.IsBottom, opts => opts.MapFrom(src => src.IsBottom))
.ForMember(dest => dest.Text, opts => opts.MapFrom(src => src.Text))
.ForMember(dest => dest.Title, opts => opts.MapFrom(src => src.Title))
.F
orMember(dest => dest.Url, opts => opts.MapFrom(src => src.Url))
.ForMember(dest => dest.ImageUrl, opts => opts.MapFrom(src => src.ImageUrl))
.ForMember(dest => dest.AlignRight, opts => opts.MapFrom(src => src.AlignRight))
.ForMember(dest => dest.EmailTemplate, opts => opts.UseDestinationValue())
.IgnoreAllNonExisting()
.IgnoreAllNonExistingSource()
.ReverseMap();
}
I can't seem to figure out why it is replacing the values in child collection with nulls?

Related

AutoMapper - Mapping child properties

I have a view model that looked like this:
class PersonSectionViewModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
class PersonViewModel
{
public PersonSectionViewModel Main { get; set; }
public PersonSectionViewModel Partner { get; set; }
}
And I tried mapping the properties like this, but it's not working at all:
class PersonDTO
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string FirstName_2 { get; set; }
public string LastName_2 { get; set; }
}
CreateMap<PersonDTO, PersonViewModel>()
.ForMember(dest => dest.Main.FirstName,
opt => opt.MapFrom(src => src.FirstName))
.ForMember(dest => dest.Main.LastName,
opt => opt.MapFrom(src => src.LastName))
.ForMember(dest => dest.Partner.FirstName,
opt => opt.MapFrom(src => src.FirstName_2))
.ForMember(dest => dest.Partner.LastName,
opt => opt.MapFrom(src => src.LastName_2))
What's the right way of mapping this?
Thank you.
Working with .ForPath().
CreateMap<PersonDTO, PersonViewModel>()
.ForPath(dest => dest.Main.FirstName,
opt => opt.MapFrom(src => src.FirstName))
.ForPath(dest => dest.Main.LastName,
opt => opt.MapFrom(src => src.LastName))
.ForPath(dest => dest.Partner.FirstName,
opt => opt.MapFrom(src => src.FirstName_2))
.ForPath(dest => dest.Partner.LastName,
opt => opt.MapFrom(src => src.LastName_2));
Demo # .NET Fiddle
Reference
Use of .ForPath() | GitHub

Stackoverflow exception automapper

So a little insight into my problem, i have 4 classes employee, employeedto, request and requestdto as shown below, they have circular references, i have created mapping configuration for them but when i try to map list of employees to employeedtos i get stackoverflow exception even though i am using MaxDepth(1).
public class Employee
{
public string Name { get; set; }
public List<Request> Requests { get; set; }
}
public class EmployeeDto
{
public string Name { get; set; }
public List<RequestDto> RequestsDto { get; set; }
}
public class Request
{
public int Id { get; set; }
public Employee CreatedBy { get; set; }
}
public class RequestDto
{
public int Id { get; set; }
public EmployeeDto CreatedBy { get; set; }
}
The mapping configuration:
cfg.CreateMap<Employee, EmployeeDto>().MaxDepth(1)
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Name))
.AfterMap((src, dest, context) =>
{
dest.RequestsDto = context.Mapper.Map<List<RequestDto>>(src.Requests);
});
cfg.CreateMap<EmployeeDto, Employee>().MaxDepth(1)
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Name))
.AfterMap((src, dest, context) =>
{
dest.Requests = context.Mapper.Map<List<Request>>(src.RequestsDto);
});
cfg.CreateMap<Request, RequestDto>().MaxDepth(1)
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id))
.AfterMap((src, dest, context) =>
{
dest.CreatedBy = context.Mapper.Map<EmployeeDto>(src.CreatedBy);
});
cfg.CreateMap<RequestDto, Request>().MaxDepth(1)
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id))
.AfterMap((src, dest, context) =>
{
dest.CreatedBy = context.Mapper.Map<Employee>(src.CreatedBy);
});
The exception occurs on this line when i try to map employees to employedtos.
var mapped = _mapper.Map<List<EmployeeDto>>(employees);

How can i map a child object with AutoMapper?

I am not sure if i am over thinking this or not, but i cannot sus this out.
I have a parent object here called Template
public Template()
{
public long Id { get; set; }
public Scoring SubProperty { get; set; }
}
Here is my Scoring object which is a child property of Template
public enum MyEnum : short
{
Basic = 0
}
public Scoring()
{
public MyEnum Type { get; set; }
public string Text { get; set; }
}
I have a TemplateModel defined, like so, which i want to convert to
public TemplateModel()
{
public long Id { get; set; }
public string Type { get; set; }
public string Text { get; set; }
}
In my AutoMapper Profile, i have set this up like so, to covert Template to TemplateModel.
public class TemplateProfile : Profile
{
protected override void Configure()
{
// converters
this.CreateMap<TemplateType, string>().ConvertUsing(new TemplateTypeConverter());
// models
this.CreateMap<Template, TemplateModel>()
.ForMember(dest => dest.Type, opt => opt.MapFrom(src => src.Scoring.Type))
.ForMember(dest => dest.Text, opt => opt.MapFrom(src => src.Scoring.Text));
}
/// <summary>
/// Convert TemplateType to string
/// </summary>
private class TemplateTypeConverter : ITypeConverter<TemplateType, string>
{
public string Convert(ResolutionContext context)
{
return context.SourceValue.ToString().ToLower();
}
}
}
How can i convert TemplateModel back into Template?
If i add the following, i get an exception, because dest.Scoring.Type is not a root property.
this.CreateMap<TemplateModel, Template>()
.ForMember(dest => dest.Scoring.Type, opt => opt.MapFrom(src => src.Type))
.ForMember(dest => dest.Scoring.Text, opt => opt.MapFrom(src => src.Text));
Any help much appreciated. In this case a Template must always have a Scoring object, but in other cases i have optional properties. If someone could help me with both that would be great.
this.CreateMap<TemplateModel, Template>()
.ForMember(dest => dest.SubProperty, opt => opt.MapFrom(src => src));
this.CreateMap<TemplateModel, Scoring>()
.ForMember(dest => dest.Type, opt => opt.MapFrom(src => src.Type))
.ForMember(dest => dest.Text, opt => opt.MapFrom(src => src.Text));

Automapper complex objects

I've begun implementing this;
Automapper, mapping to a complex object but figured there must be a better way.
So I created this;
Mapper.CreateMap<StoreTransportWindow, CSVWindow>()
.ForMember(dest => dest.DC, opt => opt.ResolveUsing(fa => fa.DC.number))
.ForMember(dest => dest.Type, opt => opt.ResolveUsing(fa => fa.Type))
;
Mapper.CreateMap<Store, CSVStore>()
.ForMember(dest => dest.StoreName, opt => opt.ResolveUsing(fa => fa.name))
.ForMember(dest => dest.StoreNumber, opt => opt.ResolveUsing(fa => fa.number))
;
Now I'd like to use the above mappings in the primary map;
Mapper.CreateMap<Store, CSVLineObject>()
.ForMember( dest => dest.store, opt => opt.ResolveUsing(/* This is where I'd like to use the above Store to CSVStore mapping */))
;
Is this possible?
edit
public class CSVStore
{
public string StoreNumber { get; set; }
public string StoreName { get; set; }
}
public class CSVWindow
{
public string Type { get; set; }
public string DC { get; set; }
public string TPC { get; set; }
public class CSVLineObject
{
public CSVStore store { get; set; }
public List<CSVWindow> storeWindows { get; set; }
As mentioned in the comment, the initial mappings should probably be more like:
Mapper.CreateMap<StoreTransportWindow, CSVWindow>()
.ForMember(dest => dest.DC, opt => opt.MapFrom(src => src.DC.number));
// Mapping for property Type not required
Mapper.CreateMap<Store, CSVStore>()
.ForMember(dest => dest.StoreName, opt => opt.MapFrom(src => src.name))
.ForMember(dest => dest.StoreNumber, opt => opt.MapFrom(src => src.number));
Now say you have the following:
public class Source
{
public Store Store { get; set; }
}
public class Destination
{
public CSVStore Store { get; set; }
}
Then the following mapping will suffice (as you've already defined the nested mapping Store to CSVStore):
Mapper.CreateMap<Source, Destination>();
However if Destination was more like this:
public class Destination
{
public CSVStore CSVStore { get; set; }
}
Then you'll need to explicitly define the properties to be mapped:
Mapper.CreateMap<Source, Destination>()
.ForMember(dest => dest.CSVStore, opt => opt.MapFrom(src => src.Store));
(Note that the mapping from Store to CVStore is applied automatically.)
If for some reason you do need to explicitly define a nested mapping, you can do something like this:
Mapper.CreateMap<Source, Destination>()
.ForMember(dest => dest.destproperty,
opt => opt.MapFrom(
src => Mapper.Map<SrcType, DestType>(src.srcproperty));
I have needed to use that at times, but not very often as the default functionality takes care of it for you automatically.
I can provide more details if required if you can expand on your requirements.

Mapping DTO back to model with AutoMapper

I have a Fixture model :
public partial class Fixture
{
public int FixtureId { get; set; }
public string Season { get; set; }
public byte Week { get; set; }
//foreign key
public int AwayTeamId { get; set; }
//navigation properties
public virtual Team AwayTeam { get; set; }
//foreign key
public int HomeTeamId { get; set; }
//navigation properties
public virtual Team HomeTeam { get; set; }
public byte? AwayTeamScore { get; set; }
public byte? HomeTeamScore { get; set; }
}
And a Fixture DTO :
public class FixtureDTO
{
public int Id { get; set; }
public string Season { get; set; }
public byte Week { get; set; }
public string AwayTeamName { get; set; }
public string HomeTeamName { get; set; }
public byte? AwayTeamScore { get; set; }
public byte? HomeTeamScore { get; set; }
}
I am using AutoMapper for the mapping and this is my first attempt using it. Here is my mapping :
CreateMap<Fixture, FixtureDTO>()
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.FixtureId))
.ForMember(dest => dest.AwayTeamName, opt => opt.MapFrom(src => src.AwayTeam.TeamName))
.ForMember(dest => dest.HomeTeamName, opt => opt.MapFrom(src => src.HomeTeam.TeamName));
CreateMap<FixtureDTO, Fixture>();
It works fine in taking the Fixture and mapping it to the FixtureDTO which I use to display the data. But when I want to update the data and pass the FixtureDTO back to map it back to Fixture I get an error.
public HttpResponseMessage PutFixture(int id, FixtureDTO fixture)
{
if (ModelState.IsValid && id == fixture.Id)
{
//do mapping manually here?
var updated = _repository.UpdateFixture(Mapper.Map<Fixture>(fixture));
return Request.CreateResponse(updated ? HttpStatusCode.OK : HttpStatusCode.NotFound);
}
return Request.CreateResponse(HttpStatusCode.BadRequest);
}
This is the error I get :
Store update, insert, or delete statement affected an unexpected number of rows (0). Entities may have been modified or deleted since entities were loaded. Refresh ObjectStateManager entries.
Can anybody help with this?
EDIT : Reverse mapping :
CreateMap<Fixture, FixtureDTO>()
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.FixtureId))
.ForMember(dest => dest.AwayTeamName, opt => opt.MapFrom(src => src.AwayTeam.TeamName))
.ForMember(dest => dest.HomeTeamName, opt => opt.MapFrom(src => src.HomeTeam.TeamName));
CreateMap<FixtureDTO, Fixture>()
.ForMember(dest => dest.FixtureId, opt => opt.MapFrom(src => src.Id))
.ForMember(dest => dest.AwayTeam.TeamName, opt => opt.MapFrom(src => src.AwayTeamName))
.ForMember(dest => dest.HomeTeam.TeamName, opt => opt.MapFrom(src => src.HomeTeamName));
Extension to #lazyberezovsky's answer:
This is an (untested) example of the reverse mapping you might require:
CreateMap<FixtureDTO, Fixture>()
.ForMember(dest => dest.FixtureId,
opt => opt.MapFrom(src => src.Id))
.ForMember(dest => dest.AwayTeam,
opt => opt.MapFrom(src => new Team
{
TeamName = src.AwayTeamName
}))
.ForMember(dest => dest.HomeTeam,
opt => opt.MapFrom(src => new Team
{
TeamName = src.HomeTeamName
}));
I've used this format in the past and its worked fine, but alternatively you could also create specific mappings for team. Eg:
CreateMap<string, Team>()....
Also, I presume you are doing it, but assert that your configuration is valid. Eg:
[Test]
public void AutoMapper_Configuration_IsValid()
{
Mapper.Initialize(m => m.AddProfile<MyProfile>());
Mapper.AssertConfigurationIsValid();
}
First, you need to create reverse mapping from DTO to entity (provide custom member mappings, if needed):
Mapper.CreateMap<FixtureDTO, Fixture>();
And second - retrieve, map, and update existing entity
if (ModelState.IsValid && id == fixture.Id)
{
Fixture entity = _repository.FindById(fixture.Id);
Mapper.Map(fixture, entity); // Use this mapping method!
var updated = _repository.UpdateFixture(entity);
// etc
}
Generally that exception is thrown by EF when it detects one of the following:
Opimistic Concurrency Violation: This usually occurs when the entity you are trying to edit was modified else where during the time you loaded, edited and saved it. (see: Entity Framework: "Store update, insert, or delete statement affected an unexpected number of rows (0).")
An Incorrectly set ID: No ID set for Pks or FKs. I've also seen an exception like this when I mistakenly set an Entity's FK to a value and set the associated object to an object with a different ID.
Most likely this exception is being thrown because of some code in your repository. If you post the code in your repository we might get a better idea of what is causing the exception.

Categories

Resources