My repository is returning back a user object which has this definition where the user will only ever have 1 email address.
User
public class User
{
[Key]
public string UserName { get; set; }
[Required]
[MaxLength(50)]
public string DisplayName { get; set; }
[Required]
[MaxLength(50)]
public string Email { get; set; }
}
Then I am mapping that to a UserDTO object for transmission to an environment where they expect the Email field to be an array of Email objects. So I created this Email object per what the receiving system needs and it looks like this below. We could set Type to a string with a value of "work" and Primary to boolean true;
public class Email
{
public string Value { get; set; }
public string Type { get; set; }
public bool Primary { get; set; }
}
And then I have my UserDTO that looks like this:
public class UserReadDto
{
public string schemas { get; set; }
public string UserName { get; set; }
public string externalId { get; set; }
// this should be an array of names, this is a name object.
public Name name { get; set; }
public string DisplayName { get; set; }
public Email[] Emails { get; set; }
}
Is it possible to have Automapper map the email string, such as test#test.com to an Email object array that only has one Email object in it for the destination?
You can create a function that returns the list for you, something like this:
public static Email[] GetList(User x)
{
return new List<Email>
{
new Email()
{
Value = x.Address
}
}.ToArray()
}
And then you can put this in your mapping configuration:
var configuration = new MapperConfiguration(cfg =>
{
cfg.CreateMap<User, UserReadDto>()
.ForMember(d => d.Emails, src =>
{
src.MapFrom(x => GetList(x));
});
});
You can put the GetList() method inside your User model, or anywhere else really, as long as you can access it in your mapping configuration.
More info on the docs page of automapper here.
Related
I'm writting web application in ASP.NET Web API and working on method to modify restaurant informations. The core entity is Restaurant that looks like this:
public class Restaurant
{
public int Id { get; set; }
public string Name { get; set; }
public string? Description { get; set; }
public string Category { get; set; }
public bool HasDelivery { get; set; }
public string ContactEmail { get; set; }
public string ContactNumber { get; set; }
public int? CreatedById { get; set; }
public virtual User CreatedBy { get; set; }
public int AddressId { get; set; }
public virtual Address Address { get; set; }
public virtual List<Dish> Dishes { get; set; }
}
What is the most important in here - the HasDelivery property must be not-nullable. It has to take one of true or false value.
Next, I have ModifyRestaurantDto class which is given as a request from body while the app working. Just like the following:
public class ModifyRestaurantDto
{
[MaxLength(25)]
public string? Name { get; set; }
[MaxLength(100)]
public string? Description { get; set; }
public bool? HasDelivery { get; set; }
}
To simplify I've just given a few of properties that are allowed to be changed. Note that all of them are nullable types.
I also have a service method called UpdateAsync, as follows:
public async Task UpdateAsync(int id, ModifyRestaurantDto modifyRestaurantDto)
{
var restaurant = await _dbContext
.Restaurants
.FirstOrDefaultAsync(r => r.Id == id)
?? throw new NotFoundException("Restaurant not found...");
var authorizationResult = _authorizationService
.AuthorizeAsync(
_userContextService.User,
restaurant,
new ResourceOperationRequirement(ResourceOperation.Update))
.Result;
if (!authorizationResult.Succeeded)
throw new ForbidException();
_mapper.Map(modifyRestaurantDto, restaurant);
await _dbContext.SaveChangesAsync();
}
What I want to achieve is to change only values which has been given in request body (in ModifyRestaurantDto). E.g. if my json body was looking like this
{
"name": "Foo"
}
I wouldn't want Description and HasDelivery props to change.
Now, I've created AutoMapper profile and configured it inside Program.cs of course.
public class RestaurantMappingProfile : Profile
{
public RestaurantMappingProfile()
{
CreateMap<ModifyRestaurantDto, Restaurant>()
.ForAllMembers(opts => opts.Condition((src, dest, value) => value is not null));
}
}
And while given types are string everything works correctly. The problem is that nullable bool is always converted to false anyways. I'm using .NET 6.0 and have enabled Nullable and ImplicitUsings in .csproj.
Do you have some idea why only nullable booleans are not overlooked by AutoMapper?
It seems like a bug to me. A quick fix would be adding mapping from bool? to bool:
CreateMap<bool?, bool>().ConvertUsing((src, dest) => src ?? dest);
CreateMap<ModifyRestaurantDto, Restaurant>()
.ForAllMembers(opts => opts.Condition((_, _, v) => v is not null));
So I have something like this.
public interface IFoundUserDataResponse
{
string UserName { get; }
string DisplayName { get; }
string Email { get; }
ICollection<INotifications> Notifications { get; }
}
My class looks like this
public class User
{
public int Id { get; set; }
public string UserName { get; set; }
public string DisplayName{ get; set; }
public string Email{ get; set; }
public ICollection<Notification> Notifications { get; set; }
}
And in MassTransit response I have this, everything is mapped except of Notifications. I always get null in response no matter what i try.
public async Task Consume(ConsumeContext<IProfileDataRequest> context)
{
var user = await _userService.GetProfileByUsernameAsync(context.Message.UserName);
await context.RespondAsync<IFoundUserDataResponse>(new
{
user.UserName,
user.DisplayName,
user.Email,
user.Notifications
});
}
I guess I need to map List<Notification> to List<INotification> but how ???
You might change the message contract to either IList<INotification> or IEnumerable<INotification>, or even an array INotification[].
The message initializer should convert the collection types as needed. As long as Notification is structurally compatible with INotification, the properties of the element type should be converted over as well.
I tried several answered questions related to my problem but I can't seem to resolve the issue.
Here is the JSON object I am sending to my API:
{
"userName": "Test_06",
"password": "#00a00B00c",
"firstName": "Test",
"lastName": "Test",
"address": {
"houseNumber": 1,
"appartementBus": "A bus 34",
"street": "Teststreet",
"ZIP": "0000",
"country": "Test"
}
}
And I am trying to map the address object into the Address POCO:
using System.Collections.Generic;
namespace Kubex.Models
{
public class Address
{
public int Id { get; set; }
public int HouseNumber { get; set; }
public string AppartementBus { get; set; }
public int StreetId { get; set; }
public virtual Street Street { get; set; }
public int ZIPId { get; set; }
public virtual ZIP ZIP { get; set; }
public int CountryId { get; set; }
public virtual Country Country { get; set; }
public virtual ICollection<User> Users { get; set; }
public virtual ICollection<Company> Companies { get; set; }
public virtual ICollection<Post> Posts { get; set; }
}
}
The following types are basically the same, example of Country looks like
using System.Collections.Generic;
namespace Kubex.Models
{
public class Country
{
public byte Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Address> Addresses { get; set; }
}
}
The error I am getting back from Postman is the following:
{
"errors": {
"address.ZIP": [
"Error converting value \"0000\" to type 'Kubex.Models.ZIP'. Path 'address.ZIP', line 10, position 18."
],
"address.street": [
"Error converting value \"Teststreet\" to type 'Kubex.Models.Street'. Path 'address.street', line 9, position 27."
],
"address.country": [
"Error converting value \"Test\" to type 'Kubex.Models.Country'. Path 'address.country', line 11, position 22."
]
},
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "|cca8afa7-463e762ee0ed670d."
}
I tried to create mappings for every Type, from string to Type. but I keep getting the errors.
Looking like this: Also tried ConstructUsing.
CreateMap<string, Country>()
.ConvertUsing(s => new Country { Name = s });
CreateMap<string, ZIP>()
.ConvertUsing(s => new ZIP { Code = s });
CreateMap<string, Street>()
.ConvertUsing(s => new Street { Name = s });
Do I need to create a map for Address too? If so, how should I do that because I don't know how it sees the object, and how I should create a map for it.
Many thanks in advance.
EDIT:
This is part of the DTO I am sending up the API endpoint to register a user,
This is the mapping used:
CreateMap<UserRegisterDTO, User>();
And this is how the DTO looks like:
using Kubex.Models;
namespace Kubex.DTO
{
public class UserRegisterDTO
{
public string UserName { get; set; }
public string Password { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public Address Address { get; set; }
}
}
This is how the User class looks like:
using System.Collections.Generic;
using Microsoft.AspNetCore.Identity;
namespace Kubex.Models
{
public class User : IdentityUser
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string EmployeeNumber { get; set; }
public int? AddressId { get; set; }
public virtual Address Address { get; set; }
public virtual ICollection<License> Licenses { get; set; }
public virtual ICollection<Contact> Contacts { get; set; }
public virtual ICollection<Post> Posts { get; set; }
}
}
In my service, I use this line to do the mapping:
var newUser = _mapper.Map<User>(dto);
If I understand correctly, Address class used in UserRegisterDTO is the same as in User. Not saying that this is a directly an issue, but from architecture standpoint it is, as you're mixing your DTO with database model (I guess), DTO should reference only simple types or other DTO's.
Hence, I would rather have something like this:
public class UserRegisterDTO
{
public string UserName { get; set; }
public string Password { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public AddressDTO Address { get; set; }
}
public class AddressDTO {
public string HouseNumber { get;set; }
public string AppartementBus { get;set; }
public string Street { get;set; }
public string ZIP { get;set; }
public string Country { get;set; }
}
and may be then mapping from AddressDTO to Address:
CreateMap<AddressDTO, Address>()
.ForMember(d => d.Country, d => d.MapFrom(s => new Country { Name = s.Country }))
// cut for brevity
But I still don't like this. As this mapping would create country instance with each request and depending on your setup it might even create new row in database. May be better for it would be to lookup country within existing ones?
You can pass list of country instances through mapping context and use in ResolveUsing, but I personally prefer not to have complex logic in AutoMapper mappings, as that scatters business logic in at least two places and complicates unit testing.
Automapper is great for doing simple things, but if you feel that it is pushed to its limits, it is better to do at least some parts of the mapping manually in my opinion.
In my application I have a model User
public class User
{
private ICollection<User> friends;
private ICollection<Album> albums;
public User()
{
this.albums = new HashSet<Album>();
this.friends = new HashSet<User>();
}
public int Id { get; set; }
public UserInfo UserInfo { get; set; }
public int BornTownId { get; set; }
public Town BornTown { get; set; }
public int CurrentLivingTownId { get; set; }
public Town CurrentLivingTown { get; set; }
public virtual ICollection<User> Friends
{
get
{
return this.friends;
}
set
{
this.friends = value;
}
}
public virtual ICollection<Album> Albums
{
get
{
return this.albums;
}
set
{
this.albums = value;
}
}
}
Here, UserInfo is a complex type which looks like this:
[ComplexType]
public class UserInfo
{
[Required]
[MaxLength(30)]
[MinLength(2)]
[Column("FirstName",TypeName = "varchar")]
public string FirstName { get; set; }
[Required]
[MaxLength(30)]
[MinLength(2)]
[Column("LastName", TypeName = "varchar")]
public string LastName { get; set; }
[NotMapped]
public string FullName => this.FirstName + " " + this.LastName;
[Required]
[MaxLength(30)]
[MinLength(4)]
[Column("Username", TypeName = "varchar")]
public string Username { get; set; }
[Required]
[MinLength(6)]
[MaxLength(50)]
[Column("Password", TypeName = "varchar")]
[PasswordValidation]
public string Password { get; set; }
[Required]
[Column("Email", TypeName = "varchar")]
[DataType(DataType.EmailAddress)]
[EmailValidation]
public string Email { get; set; }
[Column("RegisteredOn")]
public DateTime RegisteredOn { get; set; }
[Column("LastTimeLoggedIn")]
public DateTime LastTimeLoggedIn { get; set; }
[Range(1, 120)]
[Column("Age")]
public int Age { get; set; }
[Column("IsDeleted")]
public bool IsDeleted { get; set; }
}
When I try to seed data for the Users table with context.Users.AddOrUpdate(u => u.UserInfo.Username, user);,
I get the following error message:
Message=The properties expression 'u => u.UserInfo.Username' is not valid. The expression should represent a property: C#: 't => t.MyProperty'.
My question(which is as complex as the target of the problem) is:
What is the correct syntactic way to use u.UserInfo.Username as identifier in AddOrUpdate, should I add any tag to UserInfo class or property or make changes with the FluentApi and is what I am trying to do a proper way to use AddOrUpdate.
I realize that I can take Username out of UserInfo and put it directly in User but I am aiming for more "broad" solution.
Any help will be much appreciated. Thanks.
The only way you'll get this to work is by moving UserName property out to User class, as you said.
AddOrUpdate requires that you pass either a simple property of the target type in order to execute properly or an anonymous object in case that you want to match your entities using more than one property, but it'll complain if you pass any other kind of expression function to it.
So, I'll recommend you to declare you Username property on the User class.
Additional advice: remember to always initialize your complex types:
public class User
{
private ICollection<User> friends;
private ICollection<Album> albums;
public User()
{
this.albums = new HashSet<Album>();
this.friends = new HashSet<User>();
//Good practice: always initialize your complex type properties
UserInfo = new UserInfo();
}
//the rest of your class members here
}
Hope this helps!
The only solution i could think of without transferring Username to User is to check first if a user with given UserInfo.Username exists in the Users DbSet:
var user = context.Users.FirstOrDefault(u => u.UserInfo.Username == username)
?? new User
{
UserInfo = new UserInfo()
{
FirstName = firstName,
LastName = lastName,
Username = username,
Password = password,
Email = email,
RegisteredOn = registeredOnDate.AddDays(2),
LastTimeLoggedIn = lastDateLoggedOn.AddDays(10),
IsDeleted = isDeleted
}
};
Then use AddOrUpdate with the params TEntity[] entities overload, which does not employ the "culprit" System.Linq.Expressions.Expression<Func<User, object>> expression that leads to the InvalidOperationException.
context.Users.AddOrUpdate(user);
EF updates the values correctly and so this works.
I am still sad that while you can make queries with complex type you can not use complex type in AddOrUpdate extension method to update database data.
I have an Attendee Class and An AttendeeViewModel
The datetime field on the Attendee Model gets set to the default .NET Datetime when i map it from AttendeeViewModel instead of the value that is already existing in the Attendee Model
Here's my AttendeeViewModel
public class AttendeeViewModel
{
public int Id { get; set; }
[Required]
[EmailAddress]
public string Email { get; set; }
[Required]
[Display(Name = "First Name")]
public string FirstName { get; set; }
[Required]
[Display(Name = "Last Name")]
public string LastName { get; set; }
public int FEventId { get; set; }
public string DisplayName
{
get { return string.Format("{0}, {1}", FirstName, LastName); }
}
}
Here's my Base AttendeeModel
public class Attendee
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Required]
public string Email { get; set; }
[Required]
public string FirstName { get; set; }
[Required]
public string LastName { get; set; }
public DateTime CreatedAt { get; set; }
public bool IsActive { get; set; }
public int FEventId { get; set; }
public virtual ApplicationUser CreatedBy { get; set; }
public virtual FEvent FEvent { get; set; }
public ICollection<ProjectPledge> ProjectPledges { get; set; }
}
Here's My mapping configuration
public static void Configure()
{
Mapper.CreateMap<AttendeeViewModel, Attendee>().ForMember(dest=>dest.CreatedAt , opt=>opt.Ignore());
}
And heres's the Controller Action
[HttpPost]
[ValidateAntiForgeryToken]
public virtual ActionResult Edit(AttendeeViewModel attendee)
{
if (!_attendeeService.CanAddAttendee(attendee.Email, attendee.FilanthropyEventId))
{
AddEmailModelError();
}
if (ModelState.IsValid)
{
var mappedAttendee = _attendeeService.GetById(attendee.Id);
mappedAttendee = Mapper.Map<AttendeeViewModel, Attendee>(attendee);
_attendeeService.AddOrUpdate(mappedAttendee);
return RedirectToAction(MVC.Attendee.Index(mappedAttendee.FilanthropyEventId));
}
return View(attendee);
}
if I set the configuration to be this insetad of opt.Ignore()
Mapper.CreateMap<AttendeeViewModel, Attendee>().ForMember(dest=>dest.CreatedAt , opt=>opt.UseDestinationValue());
The Mapping fails giving this exception
Missing type map configuration or unsupported mapping.
Mapping types:
AttendeeViewModel -> DateTime
MyProject.Web.ViewModels.AttendeeViewModel -> System.DateTime
Destination path:
Attendee.CreatedAt.CreatedAt
Source value:
MyProject.Web.ViewModels.AttendeeViewModel
Any ideas on how i can resolve this?
If you want to map onto an existing object you need to use the overload that takes the existing destination:
Mapper.Map<Source, Destination>(source, destination);
that should do the trick.
Have you tried removing the ".ForMember" section and just let AutoMapper ignore it? In order to help you any more it would be helpful to see the two models for comparison.
Update: after lookind at your models I would suggest the following should solve the issue you are having...
Mapper.CreateMap <attendeeviewmodel, attendee>.ForMember (x => x.CreatedAt, opt => opt.MapFrom (src => datetime.utcnow));