I am trying to learn asp.NetCore 2.2. I am trying to setup a simple one page site. I have run into a problem with Automapper where manual Mappinng using forMember() is working at a top level for CreateMap<Listing, ListingSearchResultsDto>().ForMember(ListingPhotosUrl) but not at a lower level. I have another mapping CreateMap<User, UserDetailsDto>() where user contains an object Mylistings of type Listing. Mylistings is correctly auto mapped to ListingSearchResultsDto but manual configuration CreateMap<Listing, ListingSearchResultsDto>().ForMember(ListingPhotosUrl) is not applied.
I Have tried CreateMap<User, UserDetailsDto>().Formember(dest.Mylistings.ListingPhotosUrl,src.Mylistings.Photos.Url) but it seems that is not possible.
I Also tried this-> But no luck
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<User, UserDetailsDto>();
cfg.CreateMap<Listing, ListingSearchResultsDto>()
.ForMember(dest => dest.ListingPhotosUrl, opt =>
{
opt.MapFrom(src => src.Photos.FirstOrDefault(p => p.IsMain).Url);
});
});
var mapper = config.CreateMapper();
The Code:
AutoMappperProfiles
public AutoMapperProfiles()
{
CreateMap<Listing, ListingSearchResultsDto>()
.ForMember(dest => dest.ListingPhotosUrl, opt =>
{
opt.MapFrom(src => src.Photos.FirstOrDefault(p => p.IsMain).Url);
});
CreateMap<User, UserDetailsDto>();
CreateMap<ListingPhoto, ListingPhotosDetailedDto>();
}
User
public class User
{
public int Id { get; set; }
public string Username { get; set; }
public ICollection<Listing> MyListings { get; set; }
}
UserDetailsDto
public class UserDetailsDto
{
public int Id { get; set; }
public string Username { get; set; }
public ICollection<ListingSearchResultsDto> MyListings { get; set;}
}
Listing
public int id { get; set; }
public string Category { get; set; }
public string Title { get; set; }
public ICollection<ListingPhoto> Photos { get; set; }
ListingSearchResultsDto
public class ListingSearchResultsDto
{
public int id { get; set; }
public string Title { get; set; }
public string ListingPhotosUrl { get; set; }
}
I am using CreateMap<Listing, ListingSearchResultsDto>().Formember(des,src) to manually map a destination property ListingPhotosUrl. I have another mapping CreateMap<User, UserDetailsDto>(). Inside User & UsedetailsDto classes i have a objects called MyListings of types ICollection<Listing> and ICollection<ListingSearchResultsDto> respectively. MyListings object is auto mapped correctly but ListingPhotosUrl manual mapping is not being applied. CreateMap<Listing,ListingSearchResultsDto>.Formember(des,src)) manual mapping is working at top level, but not at deeper level inside CreateMap<User, UserDetailsDto>(), is there anyway to fix this? thanks
FIXED - Automapper was working fine. Issue in Entity Framework DbContext. I did not include the photos as related data in the EF Core method for loading USER data GETUSER(). It was working with EF Core method for loading LISTING GetListing() because i had an include for photos Include(p => p.Photos).
After adding .ThenInclude(p => p.Photos) in GetUser(), the photos were returned with USER data and automapper successfully mapped User data and ListingPhotosUrl manual mapping was applied successfully.
Entity Framework Core DbContext:
public async Task<User> GetUser(int id)
{
var user = await _context.Users
.Include(a => a.Avatar)
.Include(l => l.MyListings)
.ThenInclude(p => p.Photos)
.FirstOrDefaultAsync(u => u.Id == id);
return user;
}
public async Task<Listing> GetListing(int id)
{
var listing = await _context.Listings
.Include(p => p.Photos)
.FirstOrDefaultAsync(l => l.id == id);
return listing;
}
Related
I want to make PUT request in ASP .NET CORE that user can make to update his note.
It's my UpdateNoteDTO:
public class UpdateNoteDto
{
[MaxLength(50)]
public string NoteTitle { get; set; }
[MaxLength(500)]
public string NoteBody { get; set; }
public int Color { get; set; }
}
and it's my Update method:
public void Update(UpdateNoteDto dto, int noteID)
{
var note = _dbContext
.Notes
.FirstOrDefault(r => r.Id == noteID);
if (note is null)
{
throw new NotFoundException("Note not found");
}
note.NoteTitle = dto.NoteTitle == string.Empty
|| dto.NoteTitle is null
? note.NoteTitle : dto.NoteTitle;
note.NoteBody = dto.NoteBody == string.Empty
|| dto.NoteBody is null
? note.NoteBody : dto.NoteBody;
note.Color = dto.Color == 1 ? note.Color : dto.Color;
_dbContext.SaveChanges();
}
I want to make request that user can make to change single field without need for declare all of them. This code what i wrote is working but i bet there is better solution for this :D
The way you are doing it looks fine but I would suggest two changes,
If you plan to use the endpoint for updating one or some values it's better to change request type to PATCH, the HTTP PATCH request method applies partial modifications to a resource whereas HTTP PUT changes whole resource.
To easily add conditions for mapping DTOs to entity models I would rather use Automapper which allows user to declare mapping conditions in a single spot and use IMapper interface to map the models.
For example if you have two classes:
public class UpdateNoteDto
{
[MaxLength(50)]
public string NoteTitle { get; set; }
[MaxLength(500)]
public string NoteBody { get; set; }
public int Color { get; set; }
}
public class Note
{
public int Id { get; set; }
public string NoteTitle { get; set; }
public string NoteBody { get; set; }
public int Color { get; set; }
}
you can map them by creating a mapping profile with conditions instead of using if, switch or any other way of comparing whether to map or not
mapping Profile could look like this:
public class NoteProfile: Profile
{
public NoteProfile()
{
CreateMap<UpdateNoteDto, Note>()
.ForMember(dest => dest.NoteTitle, opt => opt.Condition(src => !string.IsNullOrEmpty(src.NoteTitle)))
.ForMember(dest => dest.NoteBody, opt => opt.Condition(src => !string.IsNullOrEmpty(src.NoteBody)))
.ForMember(dest => dest.Color, opt => opt.Condition(src => src.Color != default))
.ForMember(dest => dest.Id, opt => opt.Ignore());
}
}
and update the Update(UpdateNoteDto dto, int noteID) function correspondingly:
public void Update(UpdateNoteDto dto, int noteID)
{
var noteToUpdate = _dbContext
.Notes
.FirstOrDefault(r => r.Id == noteID);
if (note is null)
{
throw new NotFoundException("Note not found");
}
var note = _mapper.Map(dto, note);
_dbContext.Update(note);
_dbContext.SaveChanges();
}
client should handled this issue
Or
if you want update an entity, it is better to use PATCH
If the client wants the previous data not to be edited
It should send you the previous data
Maybe, for example, the user wants to field the "NoteBody" value as empty In which case it cannot do this
So I am having an issue using AutoMapper to resolve my Cart items from the database where they are stored as a string. I am using a passing a CustomerCartDto to the front-end. When I manually map the properties everything works but I can't seem to get AutoMapper to recognize how to map the the items.
This is what I have in the controller. I have commented out the manual mapping below that works. The mapper function above that code block is what throws the error.
public async Task<ActionResult<CustomerCartDto>> GetCurrentUserCart()
{
var user = await _userManager.FindUserByClaimsPrinciple(HttpContext.User);
var cart = await _unitOfWork.Carts.GetCartAsync(user);
await _unitOfWork.Complete();
var returnedCart = _mapper.Map<CustomerCart, CustomerCartDto>(cart);
// var returnedCart = new CustomerCartDto
// {
// Id = cart.Id,
// Username = cart.AppUser.UserName,
// Email = cart.AppUser.Email,
// Items = JsonConvert.DeserializeObject<List<CartItemDto>>(cart.Items)
//};
return Ok(returnedCart);
}
I have the mapping profiles pulled out into another file here:
CreateMap<CustomerCart, CustomerCartDto>()
.ForMember(d => d.Items, o => o.MapFrom<CartItemsFromJsonResolver>())
.ForMember(d => d.Username, o => o.MapFrom(d => d.AppUser.UserName))
.ForMember(d => d.Username, o => o.MapFrom(d => d.AppUser.Email));
Because I am mapping from a JSON string to a class I have the actually resolver in another function here:
public class CartItemsFromJsonResolver: IValueResolver<CustomerCart, CustomerCartDto, List<CartItemDto>>
{
public CartItemsFromJsonResolver()
{
//
}
public List<CartItemDto> Resolve(CustomerCart source, CustomerCartDto destination, List<CartItemDto> destMember, ResolutionContext context)
{
if (!string.IsNullOrEmpty(source.Items))
{
return JsonConvert.DeserializeObject<List<CartItemDto>>(source.Items);
}
return null;
}
}
As I said if I manually map the properties in my controller and don't use AutoMapper it works fine without any issues but I would like to keep my controller as skinny as possible. Here is the error that gets thrown out in Postman.
Image of Postman Error
Edit : Here are the class being used:
public class CustomerCartDto
{
public CustomerCartDto(AppUser appUser, List<CartItemDto> items)
{
Items = items;
Username = appUser.UserName;
}
public CustomerCartDto(AppUser appUser, List<CartItemDto> items, string id)
{
Id = id;
Items = items;
Username = appUser.UserName;
}
public CustomerCartDto()
{
//
}
public string Id { get; set; }
public string Username { get; set; }
public string Email { get; set; }
public List<CartItemDto> Items { get; set; }
}
And also how the model stored in the DB -
public class CustomerCart
{
public string Id { get; set; }
public AppUser AppUser { get; set; }
public string Items { get; set; }
// public float Subtotal { get; set; }
}
I don't know how IValueResolver is supposed to work in AutoMapper mapping configuration, but since your manual mapping works just fine, I'm assuming this line -
Items = JsonConvert.DeserializeObject<List<CartItemDto>>(cart.Items)
in your manual mapping is doing it's job as expected.
If so, then changing your current mapping configuration to following should solve your issue -
CreateMap<CustomerCart, CustomerCartDto>()
.ForMember(d => d.Username, o => o.MapFrom(s => s.AppUser.UserName))
.ForMember(d => d.Username, o => o.MapFrom(s => s.AppUser.Email))
.AfterMap((s, d) =>
{
d.Items = JsonConverter.DeserializeObject<List<CartItemDto>>(s.Items);
});
Let us know if your issue still remains.
Edit :
And also, rename Items property to something else in either CustomerCart or CustomerCartDto class. That's because when a map is not defined for a property, AutoMapper will look for a identically named property in the source model and automatically map from it.
I'm using Entity Framework Core.
My model classes are:
public class Material
{
public long Id { get; set; }
public string Name { get; set; }
public double Weight { get; set; }
public long? ParentId { get; set; }
public Material Parent { get; set; }
public State State { get; set; }
}
public class State
{
public long Id { get; set; }
public string Name { get; set; }
}
My problem is that EF will load Parent entity even I don't include it in my query.
public IEnumerable<Material> GetAllMaterials()
{
return _context.Materials
.Include(m => m.State)
.ToList();
}
Related configuration is this.
builder.Entity<Material>()
.HasOne(m => m.Parent)
.WithMany()
.HasForeignKey(m => m.ParentId);
builder.Entity<Material>()
.HasOne(m => m.State)
.WithMany();
Is there away that I can prevent this happens? I need only parentId, not the whole entity.
I have tried the [NotMapped] attribute and virtual for Parent property.
Here is example json what my controller is returning http://paste.dy.fi/5xa?clr=json&style=default&row=1&tabsize=0
EDIT:
More researching: If I make this query:
_context.Materials
.Include(m=>m.Location)
.Include(m => m.State)
.Include(m => m.Status)
.Include(m => m.RemovalMethod)
.Where( m =>m.Parent!= null)
.ToList();
I will not get parents. And they are not coming with child entities. JSON result is this http://paste.dy.fi/Ozk
Here is one more example how this is working:
_context.Materials
.Include(m=>m.Location)
.Include(m => m.State)
.Include(m => m.Status)
.Include(m => m.RemovalMethod)
.Include(m => m.Parent)
.Where( m =>m.Parent!= null)
.ToList();
JSON result is this http://paste.dy.fi/Oie
This last one make sence because I include Parent.
Using Automapper I am trying to map one object to another. One property is a class called Task containing a list of customers. The other class is called Result and contains a count of customers as well as another list of customers.
This is my current approach which fills information into the order properties correctly, but fails in result, which is still null. How can I get the List into result? How do i need to change the maps and do i need to create a map in both directions, or this this not necessary?
Mapper.Initialize(cfg =>
{
cfg.CreateMap<CustomerPost.RootObject, Customers.RootObject>();
cfg.CreateMap<CustomerPost.Order, Customers.Order>();
cfg.CreateMap<Customers.Result, CustomerPost.Task>();
cfg.CreateMap<CustomerPost.Task, Customers.Result>()
.ForMember(x => x.customerscount, opt => opt.Ignore())
.ForMember(x => x.customerstotalcount, opt => opt.Ignore());
});
try
{
Mapper.AssertConfigurationIsValid();
}
catch (AutoMapperConfigurationException ex)
{
//TODO: Handle this
throw ex;
}
var customer = Mapper.Map<CustomerPost.RootObject, Customers.RootObject>(input);
here are my current classes (Customer):
public class Result
{
public int customerstotalcount { get; set; }
public int customerscount { get; set; }
public List<Customer> customers { get; set; }
}
public class RootObject
{
public Status status { get; set; }
public Order order { get; set; }
public Result result { get; set; }
}
CustomerPost:
public class Task
{
public List<Customer> customers { get; set; }
}
public class RootObject
{
public Order order { get; set; }
public List<Task> tasks { get; set; }
}
Okay so the solution to my problem was that my mapping didn't find "result" so i've just mapped my RootObject like this:
Mapper.Initialize(cfg =>
{
cfg.CreateMap<CustomerPost.RootObject, Customers.RootObject>()
.ForMember(x => x.status, opt => opt.Ignore())
.ForMember(x => x.order, opt => opt.Ignore())
.ForMember(dest => dest.result, src => src.MapFrom(opt => opt.tasks.FirstOrDefault()));
then i went ahead and just mapped the result like this:
var result = Mapper.Map<CustomerPost.Task, Customers.Result>(input.tasks.FirstOrDefault());
var customer = new Customers.Customer();
customer = result.customers.FirstOrDefault();
and just bound it to a new Customerobject. Then all my information got transmitted as expected
I'm developing a library with Entity Framework 6.1.0 Code First, .NET Framework 4.5, C# and SQL Server.
I have these two classes:
public class User
{
public int UserId { get; set; }
public string UserName { get; set; }
public ICollection<Group> Groups { get; set; }
public ICollection<Message> MessagesSent { get; set; }
public ICollection<Message> MessagesReceived { get; set; }
}
public class Message
{
public int MessageId { get; set; }
public string Body { get; set; }
public DateTime DateSent { get; set; }
public int UserId { get; set; }
public User Sender { get; set; }
public ICollection<User> Recipients { get; set; }
}
And these configurations file:
public class UserConfiguration : EntityTypeConfiguration<User>
{
public UserConfiguration()
{
ToTable("User");
Property(u => u.UserName).IsRequired();
Property(u => u.UserName).HasMaxLength(50);
HasMany(u => u.Groups).
WithMany(g => g.Members).
Map(ug =>
{
ug.MapLeftKey("UserId");
ug.MapRightKey("GroupId");
ug.ToTable("UserGroup");
});
}
}
public class MessageConfiguration : EntityTypeConfiguration<Message>
{
public MessageConfiguration()
{
ToTable("Message");
Property(m => m.Body).IsRequired();
Property(m => m.Body).IsMaxLength();
Property(m => m.DateSent).IsRequired();
HasRequired(m => m.Sender).
WithMany(u => u.MessagesSent).
HasForeignKey(m => m.UserId);
HasMany(r => r.Recipients).
WithMany(m => m.MessagesReceived).
Map(mr =>
{
mr.ToTable("MessageRecipient");
mr.MapLeftKey("MessageId");
mr.MapRightKey("UserId");
});
}
}
The first time I run this project I get the following message:
Introducing FOREIGN KEY constraint 'FK_dbo.MessageRecipient_dbo.User_UserId' on table
'MessageRecipient' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION
or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
Could not create constraint. See previous errors.
Users won't be deleted from my database, but I don't know how to fix this problem.
I think the problem is that a message has a sender (an user) and also has one or more recipients (user). This creates different cascade paths (which is not allowed with SQL Server).
How can I disable those cascade paths? Maybe on Message.Sender.
I have solved my problem.
Before I had on MessageConfiguration:
HasRequired(m => m.Sender).
WithMany(u => u.MessagesSent).
HasForeignKey(m => m.UserId);
Now I have:
HasRequired(m => m.Sender).
WithMany(u => u.MessagesSent).
HasForeignKey(m => m.UserId).
WillCascadeOnDelete(false);
The problem is that SQL Server doesn't allow multiple cascade paths. If you delete an user you could have two delete cascade on Message: one on Message.Sender and another one on Message.Recipients.