Autofixture rule for properties - c#

I'm writing integrational test to my UserController. And currently testing User creation method using AutoFixture. CreateUser method accepts CreateUserDto
public class CreateUserDto : IMapWith<User>
{
public string Username { get; init; }
public string Name { get; init; }
public string Surname { get; init; }
public DateTime BirthDay { get; init; }
public UserTypeId UserTypeId { get; init; }
public Guid CityId { get; init; }
public string Email { get; init; }
public string PhoneNumber { get; init; }
public Guid OrientationId { get; init; }
public Guid GenderId { get; init; }
public void Mapping(Profile profile)
{
profile.CreateMap<CreateUserDto, User>();
}
}
And i want to set Email to have and email look and PhoneNumber to have only 11 digits.
For email i tried this but it's not working it still generates guid instead of email:
var fixture = GetFixture();
fixture.Customizations.Add(new MailAddressGenerator());
var userDto = fixture.Create<CreateUserDto>();
I don't even know how to customize number.
EDIT
I tried to create it with custom customization:
internal class EmailAndPhoneNumberCustomization : ICustomization
{
public void Customize(IFixture fixture)
{
fixture.Customize<CreateUserDto>(c => c
.With(u => u.Email, "test#test.com")
.With(u => u.PhoneNumber, "87777777777"));
}
}
And it's ignoring it.
Code where i use it:
private async Task<IEnumerable<UserEntity>> GenerateUsers()
{
var fixture = GetFixture();
var users = fixture.CreateMany<UserEntity>(3);
return users;
}
private static Fixture GetFixture()
{
var fixture = new Fixture();
fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList().ForEach(behavior => fixture.Behaviors.Remove(behavior));
fixture.Behaviors.Add(new OmitOnRecursionBehavior());
fixture.Customize(new EmailAndPhoneNumberCustomization());
return fixture;
}

As mentioned in the comments the generic type in CreateMany<>(3) is wrong, should be the customized type.
As for making it easier, depending on your testing framework you can make use of data attributes and inject the values into your test method.
To make data attribute creation easier, you can encapsulate the customization using the ICustomization interface.
The example below demonstrates how to encapsulate the customization into reusable classes and how to generate mail addresses and phone numbers.
/* recursion omitter customization */
public class OmitRecursionCustomization : ICustomization
{
public void Customize(IFixture fixture)
{
fixture.Behaviors.OfType<ThrowingRecursionBehavior>()
.ToList().ForEach(behavior => fixture.Behaviors.Remove(behavior));
fixture.Behaviors.Add(new OmitOnRecursionBehavior());
}
}
/* your DTO customizations */
public class DtoCustomization : ICustomization
{
public void Customize(IFixture fixture)
{
fixture.Customize<CreateUserDto>(c => c
.With(u => u.Email, fixture.Create<MailAddress>().Address)
.With(u => u.PhoneNumber, fixture.Create(
new RegularExpressionRequest("[1-9]{11}"),
new SpecimenContext(fixture))));
}
}
/* The data attribute customization */
public class DomainDataAttribute : AutoDataAttribute
{
public DomainDataAttribute()
: base(() => new Fixture().Customize(
new CompositeCustomization(
new OmitRecursionCustomization(),
new DtoCustomization())))
{
}
}
/* The test */
[Theory, DomainData]
public void Foo(List<CreateUserDto> users)
{
Assert.Equal(3, users.Count);
Assert.All(users, user => Assert.Matches(#"\#example\.(com|org|net)", user.Email));
Assert.All(users, user => Assert.Matches("[1-9]{11}", user.PhoneNumber));
}

Related

Is there a better way to test this ASP.NET MVC application?

I have created mocks for both IUserService and IDataResult. The test works fine but instead of pulling from the database, I created a user object to test this getcustomerlogin method. Is there a better way to test this case? Can we test this method with actual data from the database?
This is the testing code:
namespace UnitTesting
{
public class Tests
{
[Test]
public void login_unit_test()
{
// arrange
var userinput = new UserForLogin()
{
email = "testmail#mail.com",
password = "123456"
};
var userobject = new User()
{
Email= "testmail#mail.com",
Password = "123456"
};
var mockIdataResult = new Mock<IDataResult<User>>();
mockIdataResult.Setup(i => i.Success).Returns(true);
mockIdataResult.Setup(i => i.Data).Returns(userobject);
var mockIUserService = new Mock<IUserService>();
mockIUserService.Setup(i => i.getByEmail(userinput)).Returns(mockIdataResult.Object);
var authscontroller = new AuthsController(mockIUserService.Object);
// action
IActionResult result = authscontroller.getcustomerlogin(userinput);
var okResult = result as OkObjectResult;
// assert
Assert.AreEqual(200, okResult.StatusCode);
}
}
}
This is the login function we are trying to test.
namespace WEBAPII.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class AuthsController : ControllerBase
{
IUserService _userService;
public AuthsController(IUserService userService)
{
_userService = userService;
}
[HttpPost("login")]
public IActionResult getcustomerlogin(UserForLogin userForLogin)
{
var user = _userService.getByEmail(userForLogin);
if (user.Success)
{
if (!(user.Data.Email == userForLogin.email &&
user.Data.Password == userForLogin.password))
{
return BadRequest(user);
}
return Ok(user);
}
return BadRequest(user);
}
[HttpPost("logindadmin")]
public IActionResult adminlogin(UserForLogin userForLogin)
{
var admin = _userService.getAdmin(userForLogin);
if (admin.Success)
{
return Ok(admin);
}
return BadRequest(admin);
}
}
}
This is the IUserService interface that is set inside Authscontroller
namespace Business.Abstract
{
public interface IUserService
{
List<User> GetAll();
User GetById(int userId);
void Add(User user);
IDataResult<User> getByEmail(UserForLogin userForLogin);
IDataResult<User> getAdmin(UserForLogin userForLogin);
}
}
This is UserForLogin class that takes user information parameters.
public class UserForLogin
{
public string email { get; set; }
public string password { get; set; }
}
This is the User class that we store our information:
namespace Entities.Concrete
{
public class User : IEntity
{
public int id { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime CreatedAt { get; set; }
public string roles { get; set; }
}
}
To test your functionality with actual data from the database, you need to implement an integration test instead of a unit test.
You can create a new test database or a replica of the existing production server and populate some test data into it.
Use EF or ADO.Net for DB operations in place of macking.

How to configure an owned entity in order to have one to many relationship with an abstract class ef core .net 6

EF Core version: 6.0.6
I have a class User which owns many class Post. The class posts own many class Comment and now I would like to handle a relationship between comments and reactions(reaction is an abstract class which can be like, star etc). As I read in the official doc I cannot implement reactions as owned because it has an inheritance hierarchy.
public class User
{
private readonly List<Post> _posts;
public long Id{ get; }
public string Username{ get; }
public DateTime CreatedAt { get; }
public IReadOnlyCollection<Post> Posts => _posts.AsReadOnly();
private User() : base(default, default)
{
CreatedAt = DateTime.UtcNow;
UpdatedAt = DateTime.UtcNow;
_posts= new List<Post>();
}
//bla bla public constructor etc
}
public class Post
{
private readonly List<Comment> _comments;
public long PostId { get; }
public DateTime CreatedAt { get; }
public IReadOnlyCollection<Comment> Comments => _comments.AsReadOnly();
private Post()
{
CreatedAt = DateTime.UtcNow;
_comments = new List<Comment>();
}
//bla bla public constructor etc
}
public class Comment
{
private readonly List<Reaction> _reactions;
public long CommentId { get; }
public string Text { get; }
public DateTime CreatedAt { get; }
public IReadOnlyCollection<Reaction> Reactions => _reactions.AsReadOnly();
private Comment()
{
CreatedAt = DateTime.UtcNow;
_reactions = new List<Reaction>();
}
//bla bla public ctor
}
and finally the class Reaction
public abstract partial class Reaction
{
public long Id { get; }
protected Reaction() { }
protected Reaction(long id)
{
Id = id;
}
}
which has two child classes
the class Star
public abstract partial class Reaction
{
public class Star : Reaction
{
private readonly List<Caption> _captions;
public decimal Value { get; private set; }
public IReadOnlyCollection<Caption> Captions => _captions.AsReadOnly();
public Star(long id, decimal value, IReadOnlyCollection<Caption> captions) : base(id)
{
Value = value;
_captions = captions.ToList();
}
}
}
and the class Like
public abstract partial class Reaction
{
public class Like : Reaction
{
private readonly List<Caption> _captions;
public IReadOnlyCollection<Caption> Captions => _captions.AsReadOnly();
public Like(long id, Caption[] captions) : base(id)
{
_captions = captions.ToList();
}
}
}
Finally the class
public class Caption
{
public int Id { get; }
public string Text { get; }
public Caption(int id, string text)
{
Id = id;
Text = text;
}
}
I have stacked how to configure the database schema.
Comments cannot own many reactions due to an owned entity cannot have an inheritance hierarchy.
I tried to implement a table-per-type solution for reactions which is abstract class.
Here is the configuration
public void Configure(EntityTypeBuilder<User> builder)
{
builder.ToTable("Users");
builder.HasKey(x => x.Id);
builder.Property(x => x.Id).ValueGeneratedNever().IsRequired();
builder.Property(x => x.Username).IsRequired();
builder.Property(x => x.CreatedAt).IsRequired();
builder.OwnsMany(x => x.Posts, post =>
{
post.ToTable("Posts");
post.HasKey(PostsId);
post.Property(x => x.PostsId).ValueGeneratedNever().IsRequired();
post.Property(x => x.CreatedAt).IsRequired();
post.OwnsMany(x => x.Comments, comment =>
{
comment.ToTable("Comments");
comment.HasKey(CommentId);
comment.Property(x => x.CommentId).ValueGeneratedNever().IsRequired();
comment.Property(x => x.Text).HasMaxLength(100).IsRequired();
comment.Property(x => x.CreatedAt).IsRequired();
//how to continue the configuration of reactions and captions???
});
});
}
}
I don't know how to configure a relationship between comments and reactions.A comment could have many reactions(one to many) and a reaction could have many captions(one to many)

How to create a reusable mapping profile with Mapster?

I have a .Net 5 Web Api project and want to use
Mapster v7.2.0
to avoid mapping objects manually. The following code shows a sample scenario
setup a mapping configuration
map from multiple sources
map to fields with different names
.
[ApiController]
[Route("[controller]")]
public class MyController : ControllerBase
{
[HttpGet]
public ActionResult<UsernameWithTodoTitle> Get()
{
TypeAdapterConfig<(User, Todo), UsernameWithTodoTitle>
.NewConfig()
.Map(dest => dest, src => src.Item1) // map everything from user
.Map(dest => dest, src => src.Item2) // map everything from todo
.Map(dest => dest.TodoTitle, src => src.Item2.Title); // map the special fields from todo
var user = new User { Username = "foo", FieldFromUser = "x" };
var todo = new Todo { Title = "bar", FieldFromTodo = "y" };
var usernameWithTodoTitle = (user, todo).Adapt<(User, Todo), UsernameWithTodoTitle>();
return Ok(usernameWithTodoTitle);
}
}
public class User
{
public string Username { get; set; }
public string FieldFromUser { get; set; }
}
public class Todo
{
public string Title { get; set; } // !! map this one to the TodoTitle field !!
public string FieldFromTodo { get; set; }
}
public class UsernameWithTodoTitle
{
public string Username { get; set; }
public string TodoTitle { get; set; } // !! this one is special, is has a different name !!
public string FieldFromUser { get; set; }
public string FieldFromTodo { get; set; }
}
When running the app the mapping seems to work fine this way
I had to setup the configuration this way, other ways didn't work for me. But there are 3 things left to be solved
The configuration looks wrong to me. It maps everything from the todo and maps the special field again ... so it might loop through multiple times? This might get expensive, if there are multiple fields with different names
I created the configuration inside the controller. How can I create a reusable mapping profile class registered once globally?
When having a mapping profile this line var usernameWithTodoTitle = (user, todo).Adapt<(User, Todo), UsernameWithTodoTitle>(); looks quite messy to me. Better would be var usernameWithTodoTitle = UsernameWithTodoTitle.Adapt((user, todo)) /* pass in as a tuple */ because based on the parameter type it chooses the correct mapping profile
Do you guys have any ideas how to create such a mapping profile?
Updated: Couldn't find way to do what you are trying to do with Mapster, but here is an example of it working with Automapper.
using AutoMapper;
using System;
namespace ConsoleApp5
{
class A { public string FirstName { get; set; } }
public class B { public string Address1 { get; set; } }
public class C
{
public string FirstName { get; set; }
public string Address1 { get; set; }
}
public class DemoProfile : Profile
{
public DemoProfile()
{
CreateMap<(A, B), C>()
.ForMember(dest=> dest.FirstName, opts => opts.MapFrom(src => src.Item1.FirstName))
.ForMember(dest => dest.Address1, opts => opts.MapFrom(src => src.Item2.Address1));
}
}
class Program
{
static void Main(string[] args)
{
var config = new MapperConfiguration(cfg => {
cfg.AddProfile<DemoProfile>();
});
var mapper = config.CreateMapper();
var destination = mapper.Map<C>((new A { FirstName = "Test" }, new B { Address1 = "Addr" }));
Console.ReadKey();
}
}
}
Hey I haven't used Mapster before till now but here is what I gather. It is very specific about the type of tuple you use Tuple<T1,T2> over (T1,T2) but aside from that minor thing I was able to get it running and mapping without issues. Here is a small console example as example.
using Mapster;
using System;
namespace ConsoleApp5
{
class A { public string FirstName { get; set; } }
public class B { public string Address1 { get; set; } }
public class C
{
public string FirstName { get; set; }
public string Address1 { get; set; }
}
class Program
{
static void Main(string[] args)
{
// Option 1
TypeAdapterConfig<Tuple<A, B>, C>.NewConfig()
.Map(dest => dest.FirstName, src => src.Item1.FirstName)
.Map(dest => dest.Address1, src => src.Item2.Address1);
var destObject = new Tuple<A, B>(new A { FirstName = "Test" }, new B { Address1 = "Address 1" })
.Adapt<Tuple<A, B>, C>();
// Option 2
TypeAdapterConfig<(A, B), C>.NewConfig()
.Map(dest => dest.FirstName, src => src.Item1.FirstName)
.Map(dest => dest.Address1, src => src.Item2.Address1);
var destObject2 = (new A { FirstName = "Test" }, new B { Address1 = "Address 1" })
.Adapt<(A, B), C>();
Console.ReadKey();
}
}
}
I managed to do it with Mapster. What I did was
in Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// Some other magical code
// Tell Mapster to scan this assambly searching for the Mapster.IRegister
// classes and execute them
TypeAdapterConfig.GlobalSettings.Scan(Assembly.GetExecutingAssembly());
}
Create another class like this
using Mapster;
namespace Your.Cool.Namespace
{
public class MappingConfig : IRegister
{
public void Register(TypeAdapterConfig config)
{
// Put your mapping logic here
config
.NewConfig<MySourceType, MyDestinyType>()
.Map(dest => dest.PropA, src => src.PropB);
}
}
}
The key part is using TypeAdapterConfig.GlobalSettings, which is a static public singleton used by Mapster to hold the mappig config. If you do what Jack suggests, it will be a complety new TypeAdapterConfig and not the actual one being used by Mapster and won't work (at least it didn't for me).
On your unit tests remember to load the mapping profile too
[AssemblyInitialize] // Magic part 1 ~(˘▾˘~)
public static void AssemblyInitialization(TestContext testContext)
{
// Magic part 2 (~˘▾˘)~
TypeAdapterConfig.GlobalSettings.Scan(AppDomain.CurrentDomain.GetAssemblies());
}
You can use next:
var config = new TypeAdapterConfig()
{
RequireExplicitMapping = true,
RequireDestinationMemberSource = true,
Compiler = exp => exp.CompileFast()
};
config.Scan("Your assembly");
services.AddSingleton(config);
services.AddTransient<IMapper, ServiceMapper>();
public class RegisterConfig : IRegister
{
public void Register(TypeAdapterConfig config)
{
config.NewConfig<TSource, TDestination>();
}
}
Where services is IServiceCollection
Based on #Felipe Ramos answer I wasn't able to solve it with Mapster but with Automapper. This is my solution just for the sake of completeness. Please let me know if there is a solution for Mapster!
I installed the packages
AutoMapper v10.1.1
AutoMapper.Extensions.Microsoft.DependencyInjection v8.1.1
Inside the method Startup.ConfigureServices I added the line services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
The whole code then looks like
[ApiController]
[Route("[controller]")]
public class MyController : ControllerBase
{
private readonly IMapper _mapper;
public MyController(IMapper mapper)
{
_mapper = mapper;
}
[HttpGet]
public ActionResult<UsernameWithTodoTitle> Get()
{
var user = new User { Username = "foo", FieldFromUser = "x" };
var todo = new Todo { Title = "bar", FieldFromTodo = "y" };
var usernameWithTodoTitle = _mapper.Map<UsernameWithTodoTitle>((user, todo));
return Ok(usernameWithTodoTitle);
}
}
public class User
{
public string Username { get; set; }
public string FieldFromUser { get; set; }
}
public class Todo
{
public string Title { get; set; } // !! map this one to the TodoTitle field !!
public string FieldFromTodo { get; set; }
}
public class UsernameWithTodoTitle
{
public string Username { get; set; }
public string TodoTitle { get; set; } // !! this one is special, is has a different name !!
public string FieldFromUser { get; set; }
public string FieldFromTodo { get; set; }
}
public class UsernameWithTodoTitleMappingProfile : Profile
{
public UsernameWithTodoTitleMappingProfile()
{
CreateMap<(User, Todo), UsernameWithTodoTitle>()
.ForMember(
destination => destination.Username,
memberOptions => memberOptions.MapFrom(source => source.Item1.Username))
.ForMember(
destination => destination.TodoTitle,
memberOptions => memberOptions.MapFrom(source => source.Item2.Title))
.ForMember(
destination => destination.FieldFromUser,
memberOptions => memberOptions.MapFrom(source => source.Item1.FieldFromUser))
.ForMember(
destination => destination.FieldFromTodo,
memberOptions => memberOptions.MapFrom(source => source.Item2.FieldFromTodo));
}
}

FluentValidator. Add _validateService for all models

I need to creat fluent _validator where I will not use only one model. If I need more models in my controller? I will create for each model _validator{modelName}?
My code
public class UserDtoValidator : AbstractValidator<UserDTO>
{
public UserDtoValidator()
{
RuleFor(p => p.Email).NotEmpty().EmailAddress()
.WithMessage("{PropertyName} should be not empty.");
RuleFor(p => p.Password).NotNull().Length(5, 30)
.WithMessage("{PropertyName} should be not empty.");
RuleFor(p => p.PasswordConfirm).Equal(p => p.Password);
}
}
Controller.cs
private readonly IValidator<UserDTO> _validator;
public UserController(IValidator<UserDTO> validator)
{
_validator = validator;
}
HttpPost
var validResult = _validator.Validate(model);
if (validResult.IsValid)
{
}
Startup
services.AddControllers().AddFluentValidation();
services.AddSingleton<IValidator<UserDTO>, UserDtoValidator>();
It is work good. But I need multiple models in my _validator.
I don't think it makes much sense to make fluent validator universal, because all models may have different attributes and require different validations to validate attributes.
Unless these models of yours inherit a common class, then you can use a BaseValidator to share a validator method.
public class Teacher
{
public string Name { get; set; }
public int Age { get; set; }
public string Gender { get; set; }
}
public class UserDTO
{
public string Email { get; set; }
public string Password { get; set; }
public string PasswordConfirm { get; set; }
}
public class BaseValidator<T> : AbstractValidator<T>
{
public BaseValidator()
{
}
}
public class TeacherValidator : BaseValidator<Teacher>
{
public TeacherValidator()
{
RuleFor(p => p.Name).NotEmpty().WithMessage("{PropertyName} should be not empty.");
RuleFor(p => p.Age).InclusiveBetween(18, 60)
.WithMessage("{PropertyName} should between 18-60.");
RuleFor(p => p.Gender).Must(x => new string[] { "Male", "Female" }.Contains(x)).WithMessage("Then gender can only be male or female");
}
}
public class UserDtoValidator : BaseValidator<UserDTO>
{
public UserDtoValidator()
{
RuleFor(p => p.Email).NotEmpty().EmailAddress()
.WithMessage("{PropertyName} should be not empty.");
RuleFor(p => p.Password).NotNull().Length(5, 30)
.WithMessage("{PropertyName} should be not empty.");
RuleFor(p => p.PasswordConfirm).Equal(p => p.Password);
}
}
Or you can have a look for this.

Map nested elements to related lists using AutoMapper

I have two sets of objects
Objects that I use in C# client application:
public class EmployeeClient
{
public int Id { get; set; }
public int DepartmentId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string MiddleName { get; set; }
}
public class DepartmentClient
{
public int Id { get; set; }
public string Name { get; set; }
}
public class OrganizationClient
{
public int Id { get; set; }
public string Name { get; set; }
public List<DepartmentClient> Departments { get; set; }
public List<EmployeeClient> Employees { get; set; }
}
And DTOs:
public class EmployeeDto
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string MiddleName { get; set; }
}
public class DepartmentDto
{
public int Id { get; set; }
public string Name { get; set; }
public List<EmployeeDto> Employees { get; set; }
}
public class OrganizationDto
{
public int Id { get; set; }
public string Name { get; set; }
public List<DepartmentDto> Departments { get; set; }
}
I use AutoMapper and I need to configure mapping Client -> DTOs and DTOs -> Client.
I implemented mapping DTOs->Client like this:
public class DtoToClientMappingProfile: Profile
{
public DtoToClientMappingProfile()
{
CreateMap<EmployeeDto, EmployeeClient>();
CreateMap<DepartmentDto, DepartmentClient>();
CreateMap<OrganizationDto, OrganizationClient>()
.ForMember(dest => dest.Employees, opt => opt.ResolveUsing(src => src.Departments.SelectMany(d => d.Employees)))
.AfterMap(AfterMap);
}
private void AfterMap(OrganizationDto dto, OrganizationClient client)
{
foreach (var department in dto.Departments)
{
foreach (var employee in department.Employees)
{
var clientEmployee = client.Employees.First(e => e.Id == employee.Id);
clientEmployee.DepartmentId = department.Id;
}
}
}
}
It is not universal solution, but is works for me.
I've found only one option how mapping Client->DTOs could be implemented:
public class ClientToDtosMappingProfile : Profile
{
public ClientToDtosMappingProfile()
{
CreateMap<EmployeeClient, EmployeeDto>();
CreateMap<DepartmentClient, DepartmentDto>();
CreateMap<OrganizationClient, OrganizationDto>()
.AfterMap(AfterMap);
}
private void AfterMap(OrganizationClient client, OrganizationDto dto)
{
foreach (var employee in client.Employees)
{
var departmentDto = dto.Departments.First(d => d.Id == employee.DepartmentId);
if (departmentDto.Employees == null)
{
departmentDto.Employees = new List<EmployeeDto>();
}
var configuration = (IConfigurationProvider)new MapperConfiguration(cfg =>
{
cfg.AddProfiles(typeof(ClientToDtosMappingProfile));
});
var mapper = (IMapper)new Mapper(configuration);
var employeeDto = mapper.Map<EmployeeDto>(employee);
departmentDto.Employees.Add(employeeDto);
}
}
}
It works, but I do not like this solution because I should create instance of new Mapper every time I map objects. In my real code Employee has a lot of nested elements and mapping is configured in multiple profiles.
Any ideas how it could be implemented better?
I made my code a bit better using ResolutionContext. It allows not to create mappers in AfterMap function.
DtoToClientMappingProfile:
public class DtoToClientMappingProfile: Profile
{
public DtoToClientMappingProfile()
{
CreateMap<EmployeeDto, EmployeeClient>();
CreateMap<DepartmentDto, DepartmentClient>();
CreateMap<OrganizationDto, OrganizationClient>()
.ForMember(dest => dest.Employees, opt => opt.Ignore())
.AfterMap(AfterMap);
}
private void AfterMap(OrganizationDto dto, OrganizationClient client, ResolutionContext resolutionContext)
{
if (dto.Departments == null)
{
return;
}
client.Departments = new List<DepartmentClient>();
foreach (var department in dto.Departments)
{
var departmentClient = resolutionContext.Mapper.Map<DepartmentClient>(department);
client.Departments.Add(departmentClient);
if (department.Employees == null)
{
continue;
}
if (client.Employees == null)
{
client.Employees = new List<EmployeeClient>();
}
foreach (var employee in department.Employees)
{
var employeeClient = resolutionContext.Mapper.Map<EmployeeClient>(employee);
employeeClient.DepartmentId = department.Id;
client.Employees.Add(employeeClient);
}
}
}
ClientToDtosMappingProfile:
public class ClientToDtosMappingProfile : Profile
{
public ClientToDtosMappingProfile()
{
CreateMap<EmployeeClient, EmployeeDto>();
CreateMap<DepartmentClient, DepartmentDto>();
CreateMap<OrganizationClient, OrganizationDto>()
.AfterMap(AfterMap);
}
private void AfterMap(OrganizationClient client, OrganizationDto dto, ResolutionContext resolutionContext)
{
if (client.Employees == null)
{
return;
}
foreach (var employee in client.Employees)
{
var departmentDto = dto.Departments.First(d => d.Id == employee.DepartmentId);
if (departmentDto.Employees == null)
{
departmentDto.Employees = new List<EmployeeDto>();
}
var employeeDto = resolutionContext.Mapper.Map<EmployeeDto>(employee);
departmentDto.Employees.Add(employeeDto);
}
}
}
If you call AssertConfigurationIsValid, AM will complain about what it doesn't know how to map.
The problem seems to be that you don't have the information needed to fill the destination object in the source object.
You will need to add a resolver for each property AM complains about, like the ResolveUsing you already have, for example.
You also need to pass the extra information that's needed.
The result may not look good eventually because AM cannot rely on uniform objects to do its job, you have to tell it what to do.
Another way to go about it is to do the high level mapping in your own code and rely on AM only when the mapping is simple enough so AM can do it by itself. The more you customize AM, the less value you get from it.

Categories

Resources