To decouple UI I use a DataTransferObject pattern so that my services layer or BAL is the only layer then using Domain and Repository.
DTO's are just poco that only contain the information for the object your are transmitting and that is it, nothing more.
To map data from domain object to dto I then use AutoMapper (a god send)
Your UI layer whatever it may be will then only ever deal with dto's relevant to the work being done at that point in time. Whether it be from a Web Services layer or the service library itself.
Domain Layer
Altered your sample above to show how it works a bit better and added concurrency field to show how to handle that also with the dto's and when its needed.
public class Person : IEntity
{
[key]
public int Id { get; set; }
[Required]
[StringLength(20)]
public string FirstName { get; set; }
[StringLength(50)]
public string Surname { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
public Person() { }
public Person(string firstName, string surname)
{
FirstName = firstName;
Surname = surname;
}
}
DTO Layer
In one namespace for easier viewing, i usually separate every dto class into its own file as per normal, and also create folders to store relevant dto in. ie. CreatePersonDto will go in the Create folder along with all other Create Dto's, ListPersonDto in the List folder, QueryPersonDto in the Query fodler within the project, adds extra using references to your class files but that's nothing.
namespace Panda.DataTransferObjects
{
public class PersonDto
{
public int? Id { get; set; }
public string FirstName { get; set; }
public string Surname { get; set; }
public byte[] RowVersion { get; set; }
}
public class CreatePersonDto
{
public string FirstName { get; set; }
public string Surname { get; set; }
}
public class EditPersonDto
{
public int Id { get; set; }
public string FirstName { get; set; }
public string Surname { get; set; }
public byte[] RowVersion { get; set; }
// user context info, i would usually use a separate ServiceContextDto to do
// this, if you need to store whom changed what and when, and how etc
// ie. log other information of whats going on and by whom.
// Needed in Create and Edit DTO's only
public string ChangedBy { get; set; }
}
public class ListPersonDto
{
public string Name { get; set; }
}
public class QueryPersonDto
{
public int? Id { get; set; }
public string FirstName { get; set; }
public string Surname { get; set; }
}
}
BAL or Services Layer
Add to your stuff above
Firstly a Create person method. You UI layer will create a dto setup the info and call the create method below. Create dto's do not need to contain Id, timestamp (rowversion) for concurrency etc, as none of that information is needed when creating a new object all you need is the members of the object that can be added by the repo. In this example FirstName and Surname only. Other data like user context data etc could also be passed through within these objects, but you don't need anything else.
public int CreatePerson(CreatePersonDto dto)
{
//checks to ensure dto is valid
var instance = new Person(dto.FirstName, dto.Surname);
// do your stuff to persist your instance of person. ie. save it
return instance.Id;
}
Secondly a Get of the Person instance. You pass it the id of the person instance your after and it retrieves it and returns a PersonDto. Firstly you need to obtain your Person object from the persistence layer via your repository, then you need to convert that object to the Dto for return to the client. For the mapping i use AutoMapper, it will help enormously with this type of pattern, your doing a lot of mapping from one object to another and that is what it is for.
public PersonDto Get(int id) {
Person instance = // repo stuff to get person from store/db
//Manual way to map data from one object to the other.
var personDto = new PersonDto();
personDto.Id = instance.Id;
personDto.FirstName = instance.firstName;
personDto.Surname = instance.Surname;
personDto.RowVersion = instance.RowVersion;
return personDto;
// As mentioned I use AutoMapper for this, so the above becomes a 1 liner.
// **Beware** there is some configuration for this to work in this case you
// would have the following in a separate automapper config class.
// AutoMapper.CreateMap<Person, PersonDto>();
// Using AutoMapper all the above 6 lines done for you in this 1.
return Mapper.Map<Person, PersonDto>(instance);
}
ListPersonDto
As previously stated use AutoMapper for this and things like object conversion in a query becomes painless.
public IEnumerable<ListPersonDto> ListOfPersons(QueryPersonDto dto = null)
{
// check dto and setup and querying needed
// i wont go into that
// Using link object mapping from the Person to ListPersonDto is even easier
var listOfPersons = _personRep.Where(p => p.Surname == dto.Surname).Select(Mapper.Map<Person, ListPersonDto>).ToList();
return listOfPersons;
}
Just for completeness the automapper signature for the above would look something like the following, considering ListPersonDto only contains name.
AutoMapper.Mapper.CreateMap<Person, ListPersonDto>()
.ForMember(dest => dest.Name, opt => opt.ResolveUsing(src => { return string.Format("{0} {1}", src.FirstName, src.Surname); } ))
So your app will then need to only see the BAL & dto layer.
public class Program
{
static void Main(string[] args)
{
Logic lol = new Logic();
CreatePersonDto dto = new CreatePersonDto { FirstName = "Joe", Surname = "Bloggs" };
var newPersonId = lol.Create(dto);
foreach (var item in lol.ListOfPersons())
{
Console.WriteLine(item.Name);
}
//or to narrow down list of people
QueryPersonDto queryDto = new QueryPersonDto { Surname = "Bloggs" }
foreach (var item in lol.ListOfPersons(queryDto))
{
Console.WriteLine(item.Name);
}
}
}
It adds extra work, but unfortunately there is no easy way to do it, and using a pattern like above will help separate things and make it easier to track down issues. Your dto should only ever have whats necessary for the operation, so it make it easier to see where things are left out or not included. AutoMapper is a must have for this scenario, it makes life much easier, and reduces your typing, there are plenty good examples of using automapper around.
Hope this helps in getting closer to a solution.