JsonStatham
JsonStatham

Reputation: 10364

AutoMapper referencing a map within a map

Really struggling with how I might accomplish this, I'm basically trying to feed my Index view a list of ClubViewModel but to do this I need to match up my list of Club. I'm using AutoMapper and everything is working well apart from the CalendarEntry stuff which is a List in my database model and a flattened class in my view model.

I don't really want to change my view model around as I have the Create view in the way I want it (bools for each day and associated start/finish times).

First off here is the controller which is successfully mapping most of the class:

    public ActionResult Index()
    {   
        var clubsList = _service.GetAllClubs();
        IList<ClubViewModel> clubsViewModelList = new List<ClubViewModel>();

        foreach (var item in clubsList)
        {
            clubsViewModelList.Add(AutoMapper.Mapper.Map<ClubViewModel>(item));                
        }               
        return View(clubsViewModelList);
    }

Here are my Repository Models and Enums and also my Web ViewModels

   public class Club
    {        
        public int ClubId { get; set; }        
        public string Organisation { get; set; }
        public List<CalendarEntry> MeetingDays { get; set; }        
    }

    public class CalendarEntry
    {
        public int CalendarEntryId { get; set; }
        public int ClubId { get; set; }
        public Days Day { get; set; }
        public DateTime From { get; set; }
        public DateTime To { get; set; }      
    }

    public enum Days
    {
        Monday,
        Tuesday,
        Wednesday,
        Thursday,
        Friday,
        Saturday,
        Sunday
    }

    public class ClubViewModel
    {
        public int ClubId { get; set; }
        public string Organisation { get; set; }
        public CalendarEntryViewModel CalendarEntryViewModel { get; set; }      
    }

    public class CalendarEntryViewModel
    { 
        public bool IsMonday { get; set; }
        public bool IsTuesday { get; set; }
        public bool IsWednesday { get; set; }
        public bool IsThursday { get; set; }
        public bool IsFriday { get; set; }     
        public DateTime? MondayFrom { get; set; }        
        public DateTime? MondayTo { get; set; }        
        public DateTime? TuesdayFrom { get; set; }        
        public DateTime? TuesdayTo { get; set; }        
        public DateTime? WednesdayFrom { get; set; }        
        public DateTime? WednesdayTo { get; set; }        
        public DateTime? ThursdayFrom { get; set; }        
        public DateTime? ThursdayTo { get; set; }        
        public DateTime? FridayFrom { get; set; }        
        public DateTime? FridayTo { get; set; }
    }

Finally here is my AutoMapper configuration method:

public static void RegisterMappings()
        {
            Mapper.CreateMap<Club, ClubViewModel>()                

                .ForMember(dest => dest.CalendarEntryViewModel,
                opts => opts.Ignore());

                Mapper.CreateMap<CalendarEntry, CalendarEntryViewModel>()

                    .ForMember(dest => dest.IsMonday, opts => opts.Equals(true))
                    .ForMember(dest => dest.IsTuesday, opts => opts.Equals(true))
                    .ForMember(dest => dest.IsWednesday, opts => opts.Equals(true))
                    .ForMember(dest => dest.IsThursday, opts => opts.Equals(true))
                    .ForMember(dest => dest.IsFriday, opts => opts.Equals(true))

                    .ForMember(dest => dest.MondayFrom, opts => opts.Equals(DateTime.Now))
                    .ForMember(dest => dest.MondayTo, opts => opts.Equals(DateTime.Now))
                    .ForMember(dest => dest.TuesdayFrom, opts => opts.Equals(DateTime.Now))
                    .ForMember(dest => dest.TuesdayTo, opts => opts.Equals(DateTime.Now))
                    .ForMember(dest => dest.WednesdayFrom, opts => opts.Equals(DateTime.Now))
                    .ForMember(dest => dest.WednesdayTo, opts => opts.Equals(DateTime.Now))
                    .ForMember(dest => dest.ThursdayFrom, opts => opts.Equals(DateTime.Now))
                    .ForMember(dest => dest.ThursdayTo, opts => opts.Equals(DateTime.Now))
                    .ForMember(dest => dest.FridayFrom, opts => opts.Equals(DateTime.Now))
                    .ForMember(dest => dest.FridayTo, opts => opts.Equals(DateTime.Now));

                Mapper.AssertConfigurationIsValid();
        }

For the time being I have just hardcoded the values of the bools/dates whilst I try and get things working.

Is there anyway I can somehow add the second CalendarEntry to CalendarEntryViewModel Map to the Club to ClubViewModel Map as an additional member or am I doing this completely wrong?

Upvotes: 4

Views: 300

Answers (1)

Simon Whitehead
Simon Whitehead

Reputation: 65079

You should unpack the CalendarEntryViewModel so that it more closely represents a calendar entry in your domain. You're currently trying to shove a list into a flattened object .. which will set you up for even more pain later.

Before I get to that, I have to mention:

foreach (var item in clubsList)
{
    clubsViewModelList.Add(AutoMapper.Mapper.Map<ClubViewModel>(item));                
}

Automapper can handle lists ... so that can be:

var clubsViewModelList = Mapper.Map<IList<Club>, IList<ClubViewModel>>(clubsList);

Unpack the view model

I know this is what you said you didn't want to do .. but you really should. It will make your mapping much simpler between your domain and your view. I am all for keeping them separate for many many reasons, but I think you're not doing yourself any favours by trying to flatten this sort of domain model into a single view model. So unpack it to multiple view models:

public class ClubViewModel
{
    public int ClubId { get; set; }
    public string Organisation { get; set; }
    public IList<CalendarEntryViewModel> CalendarEntries { get; set; } // <--- a list of them      
}

Your map then becomes:

.ForMember(dest => dest.IsMonday, opt => opt.MapFrom(src => src.Day == Days.Monday))
.ForMember(dest => dest.IsTuesday, opt => opt.MapFrom(src => src.Day == Days.Tuesday))
/* etc... */

Then, remove all of the nullable dates and just represent this entry with two fields:

public DateTime? From { get; set; }        
public DateTime? To { get; set; }

You can then order them by the day:

foreach (var club in clubsViewModelList) {
    club.CalendarEntries = club.CalendarEntries.OrderBy(x => x.Day).ToList();
}

Then in your view .. just render them in order:

@foreach (var calendarEntry in club.CalendarEntries) {
    Day: @calendarEntry.Day.ToString()
    From: @calendarEntry.From.ToString("D")
}

..etc.

When it comes time to create.. you just render your entries as required:

public class ClubCalendarEntryCreateViewModel {
    /* other properties here */
    public IList<CalendarEntryViewModel> Days { get; set; }
}

Then in your controller:

viewModel.CalendarEntries = new ClubCalendarEntryCreateViewModel {
    Days = new List<CalendarEntryViewModel>() {
        new CalendarEntryViewModel {
            IsMonday = true
        },
        new CalendarEntryViewModel {
            IsTuesday = true
        },
        /* etc... */
    }
};

Then you just render those and accept it back in your POST method. Your mapping on that method becomes:

club.CalendarEntries = Mapper.Map<IList<CalendarEntryViewModel>, IList<CalendarEntry>>(viewModel.CalendarEntries.Days);

I left out a little bit here (like how you would remodel your CalendarEntry domain object) .. hopefully you can fill in the blanks and understand what I mean. I know that seems like a lot of work - but you will be better for it later on. Your mappings won't be your enemy like they are now (they shouldn't ever be your enemy) and your code will become much clearer.

I hope that helps .. its probably not what you want to hear, but its definitely how I would do it (or similar, given more requirements).

Upvotes: 2

Related Questions