0

I have troubles mapping database entities to WPF models and back. My application works with WPF model, that implements INotifyPropertyChanged interface. Everytime I need to store it in database, I need to map my WPF model to database model. I've created a complicated mapper to do so, but it end up having cycle dependancy. How i can efficently map my WPF model to database model? My database model:

public class DbUser
{
    [Key]
    public Guid UserId { get; set; }
    public List<DbSeries> UserSeries { get; set; }
}

public class DbSeries
{
    [Key]
    public Guid SeriesId { get; set; }

    public List<DbDropPhoto> DropPhotosSeries { get; set; }
    public virtual DbReferencePhoto ReferencePhotoForSeries { get; set; }

    public Guid CurrentUserId { get; set; }
    public DbUser CurrentUser { get; set; }
}

public class DbReferencePhoto
{
    [Key]
    public Guid ReferencePhotoId { get; set; }

    public virtual DbSimpleLine SimpleLine { get; set; }

    public virtual DbSeries Series { get; set; }
}

public class DbDropPhoto
{
    [Key]
    public Guid DropPhotoId { get; set; }

    public virtual DbSimpleLine SimpleHorizontalLine { get; set; }
    public virtual DbSimpleLine SimpleVerticalLine { get; set; }

    public virtual DbDrop Drop { get; set; }

    public Guid CurrentSeriesId { get; set; }
    public DbSeries CurrentSeries { get; set; }
}

public class DbDrop
{
    [Key]
    public Guid DropId { get; set; }

    public virtual DbDropPhoto DropPhoto { get; set; }
}

public class DbSimpleLine
{
    [Key]
    public Guid SimpleLineId { get; set; }

    public virtual DbReferencePhoto ReferencePhoto { get; set; }
    public virtual DbDropPhoto DropPhotoHorizontalLine { get; set; }
    public virtual DbDropPhoto DropPhotoVerticalLine { get; set; }
}

fluent api configuration goes like this:

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<DbUser>()
            .HasMany(s => s.UserSeries)
            .WithRequired(g => g.CurrentUser)
            .HasForeignKey(s => s.CurrentUserId);

        modelBuilder.Entity<DbSeries>()
            .HasMany(s => s.DropPhotosSeries)
            .WithRequired(g => g.CurrentSeries)
            .HasForeignKey(s => s.CurrentSeriesId)
            .WillCascadeOnDelete();

        modelBuilder.Entity<DbSeries>()
            .HasRequired(s => s.ReferencePhotoForSeries)
            .WithRequiredPrincipal(ad => ad.Series);

        modelBuilder.Entity<DbDropPhoto>()
            .HasRequired(s => s.Drop)
            .WithRequiredPrincipal(ad => ad.DropPhoto);

        modelBuilder.Entity<DbDropPhoto>()
            .HasRequired(s => s.SimpleHorizontalLine)
            .WithRequiredPrincipal(ad => ad.DropPhotoHorizontalLine);

        modelBuilder.Entity<DbDropPhoto>()
            .HasRequired(s => s.SimpleVerticalLine)
            .WithRequiredPrincipal(ad => ad.DropPhotoVerticalLine);

        modelBuilder.Entity<DbReferencePhoto>()
            .HasRequired(s => s.SimpleLine)
            .WithRequiredPrincipal(ad => ad.ReferencePhoto);
    }

my WPF model:

public class User : INotifyPropertyChanged
{
    public User()
    {
        _userSeries = new ObservableCollection<Series>();
        _userSeries.CollectionChanged += _userSeries_CollectionChanged;
    }

    private void _userSeries_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsAnySelectedSeriesCanDrawPlot)));
    }

    public Guid UserId { get; set; }

    private ObservableCollection<Series> _userSeries;
    public ObservableCollection<Series> UserSeries
    {
        get
        {
            return _userSeries;
        }
        set
        {
            _userSeries = value;
            OnPropertyChanged(new PropertyChangedEventArgs("UserSeries"));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    public void OnPropertyChanged(PropertyChangedEventArgs e)
    {
        PropertyChanged?.Invoke(this, e);
    }
}

public class Series : INotifyPropertyChanged
{
    private Guid _currentUserId;
    public Guid CurrentUserId
    {
        get
        {
            return _currentUserId;
        }
        set
        {
            _currentUserId = value;
            OnPropertyChanged(new PropertyChangedEventArgs("CurrentUserId"));
        }
    }

    private User _currentUser;
    public User CurrentUser
    {
        get
        {
            return _currentUser;
        }
        set
        {
            _currentUser = value;
            OnPropertyChanged(new PropertyChangedEventArgs("CurrentUser"));
        }
    }

    public Series()
    {
        _dropPhotosSeries = new ObservableCollection<DropPhoto>();
        _dropPhotosSeries.CollectionChanged += _dropPhotosSeries_CollectionChanged;
    }

    private void _dropPhotosSeries_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        OnPropertyChanged(new PropertyChangedEventArgs(nameof(CanDrawPlot)));
        CurrentUser.OnPropertyChanged(new PropertyChangedEventArgs(nameof(User.IsAnySelectedSeriesCanDrawPlot)));
    }

    public Guid SeriesId { get; set; }

    private ObservableCollection<DropPhoto> _dropPhotosSeries;
    public ObservableCollection<DropPhoto> DropPhotosSeries
    {
        get
        {
            return _dropPhotosSeries;
        }
        set
        {
            _dropPhotosSeries = value;
            OnPropertyChanged(new PropertyChangedEventArgs("DropPhotosSeries"));
        }
    }

    private ReferencePhoto _referencePhotoForSeries;
    public ReferencePhoto ReferencePhotoForSeries
    {
        get
        {
            return _referencePhotoForSeries;
        }
        set
        {
            _referencePhotoForSeries = value;
            OnPropertyChanged(new PropertyChangedEventArgs("ReferencePhotoForSeries"));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    public void OnPropertyChanged(PropertyChangedEventArgs e)
    {
        if (e.PropertyName == nameof(IntervalBetweenPhotos))
            OnPropertyChanged(new PropertyChangedEventArgs(nameof(CanDrawPlot)));

        if (e.PropertyName == nameof(IsChecked))
            CurrentUser.OnPropertyChanged(new PropertyChangedEventArgs(nameof(User.IsAnySelectedSeriesCanDrawPlot))); ;

        PropertyChanged?.Invoke(this, e);
    }
}

public class ReferencePhoto : INotifyPropertyChanged
{
    private Series _series;
    public Series Series
    {
        get
        {
            return _series;
        }
        set
        {
            _series = value;
            OnPropertyChanged(new PropertyChangedEventArgs("Series"));
        }
    }

    public Guid ReferencePhotoId { get; set; }

    private SimpleLine _simpleLine;
    public SimpleLine SimpleLine
    {
        get
        {
            return _simpleLine;
        }
        set
        {
            _simpleLine = value;
            OnPropertyChanged(new PropertyChangedEventArgs("SimpleLine"));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    public void OnPropertyChanged(PropertyChangedEventArgs e)
    {
        PropertyChanged?.Invoke(this, e);
    }
}

public class DropPhoto : INotifyPropertyChanged
{
    private Guid _currentSeriesId;
    public Guid CurrentSeriesId
    {
        get
        {
            return _currentSeriesId;
        }
        set
        {
            _currentSeriesId = value;
            OnPropertyChanged(new PropertyChangedEventArgs("CurrentSeriesId"));
        }
    }

    private Series _currentSeries;
    public Series CurrentSeries
    {
        get
        {
            return _currentSeries;
        }
        set
        {
            _currentSeries = value;
            OnPropertyChanged(new PropertyChangedEventArgs("CurrentSeries"));
        }
    }

    public Guid DropPhotoId { get; set; }

    private SimpleLine _simpleHorizontalLine;
    public SimpleLine SimpleHorizontalLine
    {
        get
        {
            return _simpleHorizontalLine;
        }
        set
        {
            _simpleHorizontalLine = value;
            OnPropertyChanged(new PropertyChangedEventArgs("SimpleHorizontalLine"));
        }
    }

    private SimpleLine _simpleVerticalLine;
    public SimpleLine SimpleVerticalLine
    {
        get
        {
            return _simpleVerticalLine;
        }
        set
        {
            _simpleVerticalLine = value;
            OnPropertyChanged(new PropertyChangedEventArgs("SimpleVerticalLine"));
        }
    }

    private Drop _drop;
    public Drop Drop
    {
        get
        {
            return _drop;
        }
        set
        {
            _drop = value;
            OnPropertyChanged(new PropertyChangedEventArgs("Drop"));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    public void OnPropertyChanged(PropertyChangedEventArgs e)
    {
        PropertyChanged?.Invoke(this, e);
    }
}

public class SimpleLine
{
    public Guid SimpleLineId { get; set; }

    public ReferencePhoto ReferencePhoto { get; set; }
    public DropPhoto DropPhotoHorizontalLine { get; set; }
    public DropPhoto DropPhotoVerticalLine { get; set; }
}

For example i need to create a new Series. My application implements repository pattern. Series creation method goes like this:

    public async Task CreateSeries(DbSeries series)
    {
        using (var context = new DDropContext())
        {
            var createdSeries = context.Series.Add(series);

            await context.SaveChangesAsync();
        }
    }

At first I create new Series:

            Series seriesToAdd = new Series()
            {
                SeriesId = Guid.NewGuid(),
                Title = seriesTitle,                    
                AddedDate = DateTime.Now.ToString("dd/MM/yyyy HH:mm:ss"),
                ReferencePhotoForSeries = new ReferencePhoto()
                {
                    ReferencePhotoId = Guid.NewGuid(),
                    Series = CurrentSeries,
                    SimpleLine = new SimpleLine { SimpleLineId = Guid.NewGuid()}
                },
                CurrentUser = User,
                CurrentUserId = User.UserId
            };

Then i need to map it to DbSeries and create new DbSeries:

            try
            {

                await _dDropRepository.CreateSeries(DDropDbEntitiesMapper.SingleSeriesToSingleDbSeries(seriesToAdd, User));

            }
            catch (Exception)
            {

            }   

To map Series to DbSeries i use very complicated mapper, which doesn't work properly:

    public static DbSeries SingleSeriesToSingleDbSeries(Series userSeries, User user)
    {
        DbSeries singleSeries = new DbSeries();
        List<DbDropPhoto> dropPhotosSeries = new List<DbDropPhoto>();

        foreach (var dropPhoto in userSeries.DropPhotosSeries)
        {
            DbDropPhoto newDbDropPhoto = new DbDropPhoto()
            {
                Name = dropPhoto.Name,
                Content = dropPhoto.Content,
                AddedDate = dropPhoto.AddedDate,
                DropPhotoId = dropPhoto.DropPhotoId,
                XDiameterInPixels = dropPhoto.XDiameterInPixels,
                YDiameterInPixels = dropPhoto.YDiameterInPixels,
                ZDiameterInPixels = dropPhoto.ZDiameterInPixels,
                CurrentSeries = SingleSeriesToSingleDbSeries(userSeries, user),
                CurrentSeriesId = userSeries.SeriesId,

            };

            DbSimpleLine newHorizontalDbSimpleLine = new DbSimpleLine
            {
                X1 = dropPhoto.SimpleHorizontalLine.X1,
                X2 = dropPhoto.SimpleHorizontalLine.X2,
                Y1 = dropPhoto.SimpleHorizontalLine.Y1,
                Y2 = dropPhoto.SimpleHorizontalLine.Y2,
                DropPhotoHorizontalLine = newDbDropPhoto,
                SimpleLineId = dropPhoto.SimpleHorizontalLine.SimpleLineId,
            };

            DbSimpleLine newVerticalDbSimpleLine = new DbSimpleLine
            {
                X1 = dropPhoto.SimpleVerticalLine.X1,
                X2 = dropPhoto.SimpleVerticalLine.X2,
                Y1 = dropPhoto.SimpleVerticalLine.Y1,
                Y2 = dropPhoto.SimpleVerticalLine.Y2,
                DropPhotoVerticalLine = newDbDropPhoto,
                SimpleLineId = dropPhoto.SimpleHorizontalLine.SimpleLineId,
            };

            DbDrop newDbDrop = new DbDrop()
            {
                DropId = dropPhoto.Drop.DropId,
                RadiusInMeters = dropPhoto.Drop.RadiusInMeters,
                VolumeInCubicalMeters = dropPhoto.Drop.VolumeInCubicalMeters,
                XDiameterInMeters = dropPhoto.Drop.XDiameterInMeters,
                YDiameterInMeters = dropPhoto.Drop.YDiameterInMeters,
                ZDiameterInMeters = dropPhoto.Drop.ZDiameterInMeters,
                DropPhoto = newDbDropPhoto,
            };

            newDbDropPhoto.Drop = newDbDrop;
            newDbDropPhoto.SimpleHorizontalLine = newHorizontalDbSimpleLine;
            newDbDropPhoto.SimpleVerticalLine = newVerticalDbSimpleLine;

            dropPhotosSeries.Add(newDbDropPhoto);
        }

        if (userSeries.ReferencePhotoForSeries != null)
        {
            var referencePhoto = new DbReferencePhoto
            {
                Content = userSeries.ReferencePhotoForSeries.Content,
                Name = userSeries.ReferencePhotoForSeries.Name,
                PixelsInMillimeter = userSeries.ReferencePhotoForSeries.PixelsInMillimeter,
                ReferencePhotoId = userSeries.ReferencePhotoForSeries.ReferencePhotoId,
                Series = singleSeries,
            };

            var simpleLineForReferencePhoto = new DbSimpleLine
            {
                X1 = userSeries.ReferencePhotoForSeries.SimpleLine.X1,
                X2 = userSeries.ReferencePhotoForSeries.SimpleLine.X2,
                Y1 = userSeries.ReferencePhotoForSeries.SimpleLine.Y1,
                Y2 = userSeries.ReferencePhotoForSeries.SimpleLine.Y2,
                ReferencePhoto = referencePhoto,
                SimpleLineId = userSeries.ReferencePhotoForSeries.SimpleLine.SimpleLineId,
            };

            referencePhoto.SimpleLine = simpleLineForReferencePhoto;

            singleSeries.ReferencePhotoForSeries = referencePhoto;
        }

        singleSeries.DropPhotosSeries = dropPhotosSeries;
        singleSeries.IntervalBetweenPhotos = userSeries.IntervalBetweenPhotos;
        singleSeries.AddedDate = userSeries.AddedDate;
        singleSeries.SeriesId = userSeries.SeriesId;
        singleSeries.Title = userSeries.Title;
        singleSeries.CurrentUser = UserToDbUser(user);
        singleSeries.CurrentUserId = user.UserId;

        return singleSeries;
    }

The main proplem is that DbSeries has in it public DbUser CurrentUser, so when i map Series to DbSeries i need to fill it with User, that i convert to DbUser, which results stackoverflow exception (method SingleSeriesToSingleDbSeries calls itself) Is there any better way to achieve my goal?

1 Answer 1

1

From what I can see your approach feels a little bit backwards. Typically I will start with the domain models (Entities, mapped to DB tables) then from that I will define the view models for the view. The important thing here is that the view models do not generally map 1-to-1 to the domain models. View models serve the needs of the view, so while I can take a 1-to-1 representation of an entity (set up with NotifyPropertyChanged etc.) and bind the controls and such to that hierarchy of entities, it's better if I simply define only the fields the view needs (irregardless of where in the entity model they come from) and let the mapper transpose those values from the entities to the view model. This reduces the payload sent from server to client (and back) and keeps from exposing more about your domain than your view needs to know about. (Visible in debugging tools)

Automapper is definitely the tool of choice for doing this largely due to it's ProjectTo method which can work with EF's IQueryable to compose queries to pull back only the fields from the entity graph that the view model, and any related view models need. In some cases you will need to set up some configuration for non-conventional mappings, but the call to perform the mapping is a 1-liner.

When it comes to editing entities, these will be more of a 1-to-1 representation between the entity and the view model, but for the most part I treat editing at a single level basis. For instance if I have an Order with associated order lines, products, customer, etc. then a create order view model will likely contain a whole hierarchy of related details (order lines with product ID, quantity, etc.) An Edit Order action would not necessarily. Depending on what actions a user can perform, the view model would just contain the relevant fields that can be updated as opposed to trying to pass back and forth a representation of the complete order. The issue with passing a complete order back & forth is transmission packet size considerations, as well as the opportunity for data that should not be updated to be updated due to tampering. (Why passing entities back & forth and using Attach/Update should be heavily discouraged.) Automapper can assist with mapping data back to entities, but I tend to hand-write the update mappings myself since I'm going to be validating and inspecting the values anyways. I don't often use Automapper for mapping back to Entities because I don't want to trust things like User IDs/Customer IDs or such coming back from a client. These should be loaded based on session state. Though provided you have validation in place reliably then Automapper can easily help out with Insert type mapping back to Entities.

Sign up to request clarification or add additional context in comments.

3 Comments

So automapper could solve my issue? My model have some properties, that doesn't correspond with db model, but for the most part i just want to work with it in a way, that i can easily map whole view model to db model, and my main problem is that i cant map series to db series, because db series and series contains user, and user contains series and series contains user and so on, i end up going in cycle when mapping
This is why modelling for what your view actually needs is important. If you are inspecting a Series, you may want to display some information about the User associated to that series, but do you really need every other series associated to that user in that specific case? Views rarely need to cascade that deeply, and if they do that should be something that can be fetched asynchronously on demand, such as from switching from a Series view to a User view, or having a call to "expand" a user, returning another view model suited to the details needed.
Automapper can handle many things by convention, even resolving something like SeriesViewModel.UserName from Series.User.Name, but you can configure mappings how you like including ignoring properties in the ViewModel that have no corresponding data fields. My only criticism of Automapper is the documentation. It's fairly good for describing basic scenarios, but finding details on specific methods and arguments etc. is somewhat lacking. Still, there are a lot of examples out there in StackOverflow, CodeProject and other resources.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.