Using DTOs with NHibernate

For a better understanding of what Data Transfer Objects (DTOs) are, I suggest these nice articles from Big Mike and Oracle. My simplistic definition of a DTO:

A stripped down version of your data layer entity that lacks some of the fields and validation mechanisms the true entity would have, and which is exposed for encapsulation needs (in this case, hiding the internal representation of your entities).

Out of the box, NHibernate has support for DTOs, using what they call a ResulTransformer.

For my tests, I used an Author object as my data layer entity

public class Author:Person {
 public virtual string Biography { get; set; }
 public virtual IList<Book> PublishedBooks { get; set; }
 public Author() {
 PublishedBooks = new List<Book>();
 }
 }

I also created a AuthorDTO that has just a few of the properties the original data layer entity has.

public class AuthorDTO {
    public string Prefix { get; set; }
    public string LastName { get; set; }
    public string Biography { get; set; }
    public List<BookDTO> PublishedBooks { get; set; }
    public AuthorDTO() {
      PublishedBooks = new List<BookDTO>();
    }
  }

The repository code I used is below. The only methods of interest for this post are the RetrieveAndConvertToDTO methods.

public interface IEntityRepository<T> where T : class {
 bool AddItem(T item);
 bool RemoveItem(T item);
 bool RemoveItemById(long Id);
 bool UpdateItem(T item);
 T RetrieveItemById(long Id);
 IList<T> RetrieveAllItems();
 IEnumerable<T> RetrieveEntities(Func<T, bool> lambda);
 IEnumerable<G> RetrieveAndConvertToDTO<G, T>(Func<G, bool> lambda, params string[] projections);
 IEnumerable<G> RetrieveAndConvertToDTO<G, T>(IList<AbstractCriterion> criterions, params string[] projections);
 }

 public class EntityRepository<T> : IEntityRepository<T> where T : BaseEntity {

 public virtual bool AddItem(T item) {
 if (RetrieveItemById(item.Id) != null && !item.Validate()) {
 return false;
 }
 using (ISession session = SessionManager.Instance.OpenSession())
 using (ITransaction transaction = session.BeginTransaction()) {
 session.Save(item);
 transaction.Commit();
 return transaction.WasCommitted;
 }
 }

 public IEnumerable<G> RetrieveAndConvertToDTO<G, T>(IList<AbstractCriterion> criterions, params string[] projections) {
 if (projections.Length > 0) {
 var proj = Projections.ProjectionList();
 foreach (var item in projections) {
 proj.Add(Projections.Property(item), item);
 }

 using (ISession session = SessionManager.Instance.OpenSession()) {
 var criteria = DetachedCriteria.For<T>();
 foreach (var item in criterions) {
 criteria.Add(item);
 }
 return criteria.GetExecutableCriteria(session)
 .SetProjection(proj)
 .SetResultTransformer(Transformers.AliasToBean<G>())
 .List<G>();
 }
 }
 else {
 return null;
 }
 }

 //This is slick...but it will likely be too slow
 public IEnumerable<G> RetrieveAndConvertToDTO<G, T>(Func<G, bool> lambda, params string[] projections) {
 if (projections.Length > 0) {
 var proj = Projections.ProjectionList();
 foreach (var item in projections) {
 proj.Add(Projections.Property(item), item);
 }

 using (ISession session = SessionManager.Instance.OpenSession()) {
 return DetachedCriteria.For<T>()
 .GetExecutableCriteria(session)
 .SetProjection(proj)
 .SetResultTransformer(Transformers.AliasToBean<G>())
 .List<G>()
 .Where(lambda);
 }
 }
 else {
 return null;
 }
 }

 public bool RemoveItem(T item) {
 using (ISession session = SessionManager.Instance.OpenSession())
 using (ITransaction transaction = session.BeginTransaction()) {
 session.Delete(item);
 transaction.Commit();
 return transaction.WasCommitted;
 }
 }

 public IEnumerable<T> RetrieveEntities(Func<T, bool> lambda) {
 using (ISession session = SessionManager.Instance.OpenSession()) {
 return session.CreateCriteria(typeof(T)).List<T>().Where(lambda);//very bad, don't do this in actual operational code. This was just a spike
 }
 }

 public bool UpdateItem(T item) {
 if (!item.Validate()) return false;

 using (ISession session = SessionManager.Instance.OpenSession())
 using (ITransaction transaction = session.BeginTransaction()) {
 session.Update(item);
 transaction.Commit();
 return transaction.WasCommitted;
 }
 }

 public T RetrieveItemById(long Id) {
 using (ISession session = SessionManager.Instance.OpenSession()) {
 return session.CreateCriteria(typeof(T))
 .Add(Restrictions.Eq("Id", Id))
 .UniqueResult<T>();
 }
 }

 public IList<T> RetrieveAllItems() {
 using (ISession session = SessionManager.Instance.OpenSession()) {
 return session.CreateCriteria(typeof(T))
 .List<T>();
 }
 }

 public bool RemoveItemById(long Id) {
 var toDelete = RetrieveItemById(Id);
 if (toDelete != null) return RemoveItem(toDelete);
 else return false;
 }
 }

As you can see, this requires you to use a DetachedCriteria, although you can also do the same if you execute a spql query e.g.

session.CreateSQLQuery(yourSqlQuery).SetResulTransformer(Transformers.AliasToBean<G>());

To ensure that my code worked, i wrote the tests below

[TestFixture]
 public class DTO_Entity_Tests {
 EntityRepository<Author> authors_repo;
 Author auth1;
 Author auth2;
 Book book;

[TestFixtureSetUp]
 public void SetupDatabase() {
 SessionManager.Instance.CreateNewDatabaseSessionFactory();
 authors_repo = new EntityRepository<Author>();
 auth1 = new Author {
 Biography = "blablabal",
 FirstName = "James",
 LastName = "Bond",
 Prefix = "agent"
 };

book = new Book {
 Currency = new Currency { ExchangeRate = 0.9, Name = "USD" },
 Price = 90.2,
 Rating = 5.0,
 Title = "Art of Con"
 };

new EntityRepository<Book>().AddItem(book);

auth1.PublishedBooks.Add(book);

auth2 = new Author {
 Biography = "blablue",
 FirstName = "Janette",
 LastName = "Jackson",
 Prefix = "Mr"
 };

authors_repo.AddItem(auth1);
 authors_repo.AddItem(auth2);

}

[Test]
 public void Author_EntityRepository_Can_Convert_To_DTO_With_Projection_Restricted() {

var dtos = authors_repo.RetrieveAndConvertToDTO<AuthorDTO, Author>(e => true, "Biography", "LastName");

Assert.NotNull(dtos);
 Assert.AreEqual(2, dtos.Count());
 Assert.IsNull(dtos.ElementAt(0).Prefix);
 Assert.IsNull(dtos.ElementAt(1).Prefix);

var lastNames = dtos.Select(e => e.LastName);
 Assert.IsTrue(lastNames.Contains(auth1.LastName));
 Assert.IsTrue(lastNames.Contains(auth2.LastName));
 }

[Test]
 public void Author_EntityRepository_Can_Convert_To_DTO_With_Projection_And_Lambda() {

var dtos = authors_repo.RetrieveAndConvertToDTO<AuthorDTO, Author>(e => e.LastName==auth1.LastName, "Biography", "LastName");

Assert.NotNull(dtos);
 Assert.AreEqual(1, dtos.Count());

var lastNames = dtos.Select(e => e.LastName);
 Assert.IsTrue(lastNames.Contains(auth1.LastName));

}

[Test]
 public void Author_EntityRepository_Can_Convert_To_DTO_With_Projection_And_Criterions() {

var criterions = new List<AbstractCriterion>();
 criterions.Add(Expression.Eq("LastName", auth1.LastName));
 var dtos = authors_repo.RetrieveAndConvertToDTO<AuthorDTO, Author>(criterions, "Biography", "LastName");

Assert.NotNull(dtos);
 Assert.AreEqual(1, dtos.Count());

var lastNames = dtos.Select(e => e.LastName);
 Assert.IsTrue(lastNames.Contains(auth1.LastName));

}

[Test]
 public void Author_EntityRepository_Can_Convert_To_DTO_With_Projection_And_Criterions_And_Link_Relationship() {

var criterions = new List<AbstractCriterion>();
 criterions.Add(Expression.Eq("LastName", auth1.LastName));
 var dtos = authors_repo.RetrieveAndConvertToDTO<AuthorDTO, Author>(criterions, "Biography", "LastName", "PublishedBooks");

Assert.NotNull(dtos);
 Assert.AreEqual(1, dtos.Count());

var author = dtos.ElementAt(0);
 Assert.AreEqual(auth1.LastName, author.LastName);
 Assert.NotNull(author.PublishedBooks);

}

[TestFixtureTearDown]
 public void CleanupDB() {
 SessionManager.Instance.CreateNewDatabaseSessionFactory();
 }
 }

All of my tests passed, with the exception of the last, which I expected. To handle such cases, you would need to write a custom ResultTransformer. As you can see, using DTOs with NHibernate is pretty straightforward.

In some instances, using DTOs is almost mandatory. If you execute a query through NHibernate that only returns a few columns (e.g. select Id, Name from Employees)  and you specify the return type to be the Employee class through the “AliasToBeans” concept, it will blow up. You would need to return all the columns for it to work, i.e. select * from Employees. In such circumstances, creating another object that will map the properties that you need would be the best solution….unless you decide to create a custom result transformer which will make use of reflection to assign the properties. While this is not particularly hard to to do it, you need to keep in mind that every time you use reflection you lose in performance.

The source code is available here.

Advertisements
Using DTOs with NHibernate

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s