PUT request with without every field from the DTO model - c#

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

Related

AutoMapper - Assign a value manually depending on the value of the source

I would like to assign a value manually to a DTO property in the Profile of the AutoMapper depending on the value I have in my entity.
Below you can see my code, but it doesn't work as expected because the .AfterMap is independent from the .Condition, as a matter of fact every dest.Sent is mapped with true:
.ForMember(
dest => dest.Sent,
opts => opts.Condition(src => src.Status == 2 && src.Status != 1)
)
.AfterMap((notification, dto) =>
dto.Sent = true);
My DTO
public class NotificationItemDTO
{
public long Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string Type { get; set; }
public bool Sent { get; set; }
}
My Source
public class Notification
{
public long Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string Type { get; set; }
public int Status { get; set; }
}
I wish to assign a value to dest.Sent which is a bool depending on src.Status which is an int. So if src.Status == 1, my dest.Sent = false, if src.Status == 2, my dest.Sent = true. Is it possible to achieve this?
You have misunderstood the usage of .Condition().
AutoMapper allows you to add conditions to properties that must be met before that property will be mapped.
Hence, from your scenario, you are trying to assign a different value to the destination based on condition but not block the mapping from source to destination by condition which is the main purpose of .Condition().
While .AfterMap() is the last action to be executed after the mapping definition logic is executed, hence the value will be overridden.
Using .MapFrom() and defining the logic to set the destination's Sent is true only when the source's Status is 2.
CreateMap<Notification, NotificationItemDTO>()
.ForMember(
dest => dest.Sent,
opts => opts.MapFrom(src => src.Status == 2)
);
Demo # .NET Fiddle

AutoMapper cannot resolve Object from JSON string

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.

Automapper - how to apply ForMember configuration at a deep level

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;
}

Replacing empty strings with nulls with AutoMapper

I am using AutoMapper to map DTOs to entities. Also, my WCF services are being consumed by SAP.
The issue is that SAP sends me empty strings instead of nulls (that is, "" instead of null).
So I basically need to go through every field of the DTO I am receiving, and replace empty strings by nulls. Is there an easy way to accomplish this with AutoMapper?
Consider value transform construction for mapper profile
CreateMap<Source, Destination>()
.AddTransform<string>(s => string.IsNullOrEmpty(s) ? null : s);
This construction will transform all 'string' type members and if they null or empty replace with null
Depends on what you are after - if there are string fields for which you would like to preserve the empty string and not convert to null, or you want to threat all of them the same. The provided solution is if you need to threat them all the same. If you want to specify individual properties for which the empty to null conversion should happen, use ForMemeber() instead of ForAllMembers.
Convert all solution:
namespace Stackoverflow
{
using AutoMapper;
using SharpTestsEx;
using NUnit.Framework;
[TestFixture]
public class MapperTest
{
public class Dto
{
public int Int { get; set; }
public string StrEmpty { get; set; }
public string StrNull { get; set; }
public string StrAny { get; set; }
}
public class Model
{
public int Int { get; set; }
public string StrEmpty { get; set; }
public string StrNull { get; set; }
public string StrAny { get; set; }
}
[Test]
public void MapWithNulls()
{
var dto = new Dto
{
Int = 100,
StrNull = null,
StrEmpty = string.Empty,
StrAny = "any"
};
Mapper.CreateMap<Dto, Model>()
.ForAllMembers(m => m.Condition(ctx =>
ctx.SourceType != typeof (string)
|| ctx.SourceValue != string.Empty));
var model = Mapper.Map<Dto, Model>(dto);
model.Satisfy(m =>
m.Int == dto.Int
&& m.StrNull == null
&& m.StrEmpty == null
&& m.StrAny == dto.StrAny);
}
}
}
You can just define string mapping like this:
cfg.CreateMap<string, string>()
.ConvertUsing(s => string.IsNullOrWhiteSpace(s) ? null : s);
You can be specific with the properties too.
cfg.CreateMap<Source, Dest>()()
.ForMember(destination => destination.Value, opt => opt.NullSubstitute(string.Empty)));
Remember if you are using ReverseMap() place it in the very last something like following,
cfg.CreateMap<Source, Dest>()()
.ForMember(destination => destination.Value, opt => opt.NullSubstitute(string.Empty)))
.ReverseMap();;

Need to speed up automapper...It takes 32 seconds to do 113 objects

Hi I have some major problems with auto mapper and it being slow. I am not sure how to speed it up.
I am using nhibernate,fluent nhibernate and asp.net mvc 3.0
[Serializable()]
public class Test
{
public virtual int Id { get; private set; }
public virtual string Name { get; set; }
public virtual string Description { get; set; }
public virtual DateTimeDate { get; set; }
public virtual IList<Reminder> Reminders { get; set; }
public virtual IList<Reminder2> Reminders2 { get; set; }
public virtual Test2 Test2 { get; set; }
public Test()
{
Reminders = new List<Reminders>();
Reminders2 = new List<Reminders2>();
}
}
So as you can see I got some properties, Some other classes as in my database I have references between them.
I then do this
var a = // get all items (returns a collection of Test2)
var List<MyViewModel> collection = new List<MyViewModel>();
foreach (Test2 t in a)
{
MyViewModel vm = Mapper.Map<Test2, MyViewModel>(t);
vm.SetDateFormat(t.DateTimeDate, DateFilters.All.ToString());
collection.Add(vm);
}
// view model
public class MyViewModel
{
public int Id { get; private set; }
public string Name { get; set; }
public string Description { get; set; }
public DateTime DateTimeDate { get; set; }
public string FormatedDueDate { get; set; }
public string Test2Prefix { get; set; }
public string Test2BackgroundColor { get; set; }
public string SelectedDateFilter { get; set; }
public bool DescState { get; set; }
public bool AlertState { get; set; }
/// <summary>
/// Constructor
/// </summary>
public MyViewModel()
{
// Default values
SelectedDateFilter = "All";
DescState = false;
AlertState = false;
}
/// <summary>
/// Sets the date formatter string used
/// </summary>
/// <param name="dateFormat"></param>
public void SetDateFormat(DateTime dueDate, string dateFilter)
{
// simple if statement to format date.
}
}
// mapping
Mapper.CreateMap<Test2,MyViewModel>().ForMember(dest => dest.DescState, opt =>
opt.ResolveUsing<DescStateResolver>())
.ForMember(dest => dest.AlertState, opt =>
opt.ResolveUsing<AlertStateResolver>());
// resolvers
public class AlertStateResolver : ValueResolver<Task, bool>
{
protected override bool ResolveCore(Task source)
{
if (source.Reminders.Count > 0 || source.Reminders2.Count > 0)
{
return true;
}
else
{
return false;
}
}
}
public class DescStateResolver : ValueResolver<Task,bool>
{
protected override bool ResolveCore(Task source)
{
if (String.IsNullOrEmpty(source.Description))
{
return false;
}
else
{
return true;
}
}
}
Ignore the weird names and any typos my real object works just fine and makes sense.
So I used the stop watch and did this
Stopwatch a = new Stopwatch()
foreach (Test2 t in a)
{
a.Start()
MyViewModel vm = Mapper.Map<Test2, MyViewModel>(t);
a.Stop()
vm.SetDateFormat(t.DateTimeDate, DateFilters.All.ToString());
collection.Add(vm);
}
var b = a.Elapsed; // comes back with 32 seconds.
I need to optimized this very badly.
Instead of:
var a = // get all items (returns a collection of Test2)
List<MyViewModel> collection = new List<MyViewModel>();
foreach (Test2 t in a)
{
MyViewModel vm = Mapper.Map<Test2, MyViewModel>(t);
vm.SetDateFormat(t.DateTimeDate, DateFilters.All.ToString());
collection.Add(vm);
}
Try:
var a = // get all items (returns a collection of Test2)
List<MyViewModel> collection = Mapper.Map<IEnumerable<Test2>, IEnumerable<MyViewModel>>(a);
which is equivalent to the first except the SetDateFormat call which you could do at your mapping definition. It might also be faster.
If you have a mapping defined between Test2 => MyViewModel AutoMapper automatically provides one for IEnumerable<Test2> => IEnumerable<MyViewModel> so that you don't need to loop.
Also you have mentioned NHibernate in your question. Make sure that your source object along with its collections is eagerly loaded from the database before passing it to the mapping layer or you cannot blame AutoMapper for being slow because when it tries to map one of the collections of your source object it hits the database because NHibernate didn't fetch this collection.
Another thing to look for is mapping code that throws exceptions. AutoMapper will catch these silently, but catching exceptions in this way impacts performance.
So if SomethingThatMightBeNull is often null, then this mapping will perform poorly due to the NullreferenceExceptions :
.ForMember(dest => dest.Blah, c.MapFrom(src=>src.SomethingThatMightBeNull.SomeProperty))
I've found making a change like this will more than half the time the mapping takes:
.ForMember(dest => dest.Blah, c.MapFrom(src=> (src.SomethingThatMightBeNull == null
? null : src.SomethingThatMightBeNull.SomeProperty)))
Update: C# 6 syntax
.ForMember(dest => dest.Blah, c.MapFrom(src => (src.SomethingThatMightBeNull?.SomeProperty)))
Was Able to improve Launch Time when added this
.ForAllMembers(options => options.Condition(prop => prop.SourceValue != null));
at end of Each
.CreateMap<..,..>()
If your subcollections are large you might benefit from using "Any()" instead of the "Count > 1". The Any function will only have to iterate once while the Count might need to iterate the entmes collection (depending on the implementation).
Not sure if this is causing any issues in your case, but beware of serializing auto-implemented properties.
Each time your code is compiled, the name of each (anonymous) backing field is picked at random by the compiler. So you may see some surprising exceptions if you serialize data with a progran that is compiled at one time and de-serialize it with a different program.
I fixed the same issue as yours. It also costs me 32s for mapping only one object.
So, I use opts.Ignore() to deal with some customized object as below:
CreateMap<SiteConfiguration, Site>()
.ForMember(x => x.SubSystems, opts => opts.Ignore())
.ForMember(x => x.PointInformations, opts => opts.Ignore())
.ForMember(x => x.Schedules, opts => opts.Ignore())
.ForMember(x => x.EquipmentDefinitions, opts => opts.Ignore());
After that, it just cost a few milliseconds.
A good tip is to optimize the configuration of AutoMapper, use Ignore for the properties of the ViewModels, and make the method call to validate the mappings "Mapper.AssertConfigurationIsValid()".
Mapper.Initialize(cfg =>
{
cfg.ValidateInlineMaps = true;
cfg.AllowNullCollections = false;
cfg.AllowNullDestinationValues = true;
cfg.DisableConstructorMapping(); // <= In the case of my project, I do not use builders, I had a performance gain.
cfg.AddProfile<DomainToViewModelMappingProfile>();
cfg.AddProfile<ViewModelToDomainMappingProfile>();
});
Mapper.AssertConfigurationIsValid();

Categories