When using DTOs, Automapper & Nhibernate reflectin

2019-03-16 21:38发布

问题:

I'm not massively familiar with this design but I am hoping to get some guidance.

I have a backend service that sends out DTOs to a WPF smart client. On the WPF smart client the user will change,delete and modify items and then the changes are sent back (client --> server). As an example, currently I am working on the Customer details form and the user has the ability to add,remove and change categories belonging to a customer in a datagrid. When the DTO is sent back to the server I would like to load in the domain object that is related to the ID in the DTO and apply the changes made on the DTO to the domain object, including all the child collections.

I have made an attempt at doing something similar to this in the code below with the UpdateCustomer method. However, I think I am way off the mark. When the code runs instead of ending up with a list of {Individual,Company,NGO,Government} I end up with a list of {Individual,B2B,Company,NGO,Government} as it has clearly not deleted the B2B entry from the original list.

One option that has occurred to me is to loop through the DTO collection and compare it to the collection from the domain object and add, remove and update dependent on what has been modified. However, this seemed really cumbersome.

What do I need to do to apply the changes from the DTO to the child collections in my domiain object?

Thank you very much for any assistance it will be thoroughly appreciated

Alex

    public class Customer
        {
            public virtual int Id { get; set; }
            public virtual IList<Category> Categories { get; private set; }
            public virtual string Code { get; set; }
            public virtual string Description { get; set; }

            public Customer()
            {
                Categories = new List<Category>();
            }

            public virtual void AddCategory(string categoryName)
            {
                Categories.Add(new Category(categoryName));
            }
        }

        public class Category
        {

            public virtual string CategoryName { get; private set; }
            public virtual Customer Customer {get;set;}
            public virtual int Id { get; set; }

            protected Category(){}

            public Category(string name)
            {
                CategoryName = name;
            }
        }
    }

    public void SetUpAutoMapper()
    {
        Mapper.CreateMap<Category, CategoryDto>();
        Mapper.CreateMap<Customer, CustomerDto>();
        Mapper.CreateMap<CategoryDto, Category>();
        Mapper.CreateMap<CustomerDto, Customer>();
        Mapper.AssertConfigurationIsValid();
    }
       public void SaveCustomer()
        {
            var customer = new Customer{Code="TESTCUST",Description="TEST CUSTOMER"};
             customer.AddCategory("Individual");
             customer.AddCategory("B2B");
             customer.AddCategory("Healthcare");
             customer.AddCategory("NGO");
             repository.Save(customer);
        }

     public CustomerDto GetCustomer(int customerId)
     { 
        var customer = repository.GetCustomer(customerId);
        var customerDto = Mapper.Map<Customer,CustomerDto>(customer);
        return customerDto;
     }

    public void UpateCustomer(CustomerDto customerToUpdate)
    {
        /*imagine that the dto incoming has had the following operations performed on it
        -----add new category----
        customerToUpdate.Categories.Add(new CategoryDto {CategoryName = "Government"});
        ---update existing category---
        customerToUpdate.Categories[2].CategoryName = "Company";
        ---remove category---
        customerToUpdate.Categories.RemoveAt(1);*/

        var customer = repository.GetCustomer(customerToUpdate.Id);
        /* How in this bit do I ensure that the child collection changes are
        propogated into the underlying customer object retrieved from the database*/
        var customer = Mapper.Map<CustomerDto,Customer>(customerToUpdate);
        repository.Save(customer);
    }

public class CustomerDto
    {
        public int Id { get; set; }
        public string Code { get; set; }
        public string Description { get; set; }
        public List<CategoryDto> Categories { get; set; }
    }

    public class CategoryDto
    {
        public int Id { get; set; }
        public string CategoryName { get; set; }
    }

    <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2">
      <class name="Customer" table="Customer">
        <id name="Id" column="CustomerId">
          <generator class="native"/>
        </id>
        <property name="Code" />
        <property name="Description" />
        <bag name="Categories" table="Categories" cascade="all" inverse="false">
          <key column="FK_CustomerID" />
          <one-to-many class="Category"/>
        </bag>
      </class>

      <class name="Category" table="Categories">
        <id name="Id" column="CategoryId">
          <generator class="native"/>
        </id>
        <many-to-one name="Customer" column="FK_CustomerId" not-null="true" class="Customer"></many-to-one>
        <property name="CategoryName" />
      </class>
    </hibernate-mapping>

回答1:

I recently did something similar but with EF as the datatier. I don't know nhibernate to know if the same approach would work.

Basic steps were

  • Ensure the destination collection is loaded from db and attached to the object graph for change tracking
  • .ForMember(dest => dest.Categories, opt => opt.UseDestinationValue())
  • Then create a custom IObjectMapper for mapping IList<> to IList<T> where T : Entity
  • The custom IObject mapper used some code from http://groups.google.com/group/automapper-users/browse_thread/thread/8c7896fbc3f72514

    foreach (var child in source.ChildCollection)
    { 
        var targetChild = target.ChildCollection.SingleOrDefault(c => c.Equals(child)); // overwrite Equals or replace comparison with an Id comparison
        if (targetChild == null)
        { 
            target.ChildCollection.Add(Mapper.Map<SourceChildType, TargetChildType>(child));
        } 
        else
        { 
            Mapper.Map(child, targetChild);
        } 
    } 
    
  • Finally one last piece of logic to check all Id's in targetCollection exist in sourceCollection and delete them if they don't.

It wasn't all that much code in the end and is reusable in other actions.



回答2:

Mapper.CreateMap<Customer, CustomerDto>()
    .ForMember(dest => dest.Categories, opt => opt.MapFrom(src =>src.Categories));

or

Mapper.CreateMap<IList<Category>, IList<CategoryDto>>();

something like this to tell automapper to map the list, too.