Building WCF Services using Data Transfer Objects
If the design of your distributed system is intending to provide a loose coupling between client and service by sharing contracts then you will want to build your system using Data Transfer Objects to enforce a clean separation across service boundaries. You will then want to be able to map your domain entities to and from these Data Transfer Objects. If you are intending to use a tight coupling where you share the details of your domain model across the service boundary then you should review the associated section above on Distributed Entities.
The design of your contracts is beyond the scope of this document and is not specifically a role which LightSpeed provides. However, we do provide assistance around importing and exporting your entities.
Here is a basic example which is based around this LightSpeed model containing relationships between Contributions, Comments and Members of a system.
Given this model we might wish to represent a data transfer object which provides the details of a member to form part of our published service contract.
Example of a Data Transfer object which would represent a member |
[DataContract] |
We need to achieve several steps to deal with data transfer objects within our system.
· We need to project LightSpeed Member entities into ApplicationMember instances so we can send these objects out of our system and over the wire.
· We need to allow inbound ApplicationMember to be mapped back to Member entities.
· We need to support both creating new instances and updating existing instances.
The first step can be achieved by either creating a mapping function or by using LINQ to project the data as required. Assuming you are able to use LINQ then this would be the preferred approach.
Example of projecting Member entities to ApplicationMember instances |
var members = unitOfWork.Members.Select(m => new ApplicationMember() |
The second and third steps can be achieved by using IUnitOfWork.Import which provides functionality for mapping arbitrary data transfer objects back against the entity store. It will conditionally create new entities or update existing entities based on the value of the Id property held on the data transfer object.
Calling IUnitOfWork.Import |
members.ForEach(m => unitOfWork.Import<Member>(m)); |
In our example the call to the Import method returns the actual Member entity instance that was attached to the UnitOfWork. As with the example above we do not make use of this, but if you need to perform any subsequent processing against the entity then it is available for use.
The default mapping approach used by the Import method comes with two caveats.
1. We expect that the data transfer object will have an Id property which we can use to check if there is already an entity with that Id in the database, and load it if that is the case so we can perform an update. If no Id property is present then that aspect of the mapper will be ignored and you will always be dealing with new entities.
2. The mapper will use reflection to assign properties of the data transfer object which exactly match the value fields on the entity. In the example you will notice that ApplicationMember has a case sensitive match with the Member entity for the property UserName. If the property does not exactly match, then assignment will not happen.
Providing Custom Mapping for IUnitOfWork.Import
The default mapping approach provided with IUnitOfWork.Import will work well for one to one style mapping and constrains you to the use of a key property to ensure you get conditional insert/update behaviour. Lastly associations are also not currently traversed as part of the mapping so in the earlier example if an ApplicationMember held Contribution and Comments collections then these would not be imported back into the UnitOfWork.
If you need finer control over the behaviour of the mapping then IUnitOfWork.Import provides an overload which allows you to provide a custom function which allows you specifically control how mapping occurs.
The mapping function takes two arguments, the generic type argument and the source object we are passing to be imported.
An example of a custom mapping function used with IUnitOfWork.Import |
members.ForEach(m => unitOfWork.Import<Member>(m, (t, o) => |
Compatibility with LightSpeed 3 Generated DTOs
As part of LightSpeed 3 we offered code generation templates which would generate you some 1:1 style DTO’s and associated mapping helper methods. These have been deprecated however the code generation will still occur for these but the code itself now relies on the presence of a compiler conditional to be included. If you wish to continue using these templates then you must set the LS3_DTOS compiler conditional in the project containing your model file.
Samples
If you wish to review a practical sample which uses Data Transfer Objects with LightSpeed then you should review the ATM sample within which the ATMClient console application project makes use of several service calls which rely on DTOs which have been defined in the Contracts project.