Story Details for articles

Golf Tracker - Ep. 3 - Adding Classes

kahanu
Author:
Version:
Views:
2924
Date Posted:
8/24/2011 7:49:08 PM
Date Updated:
8/24/2011 7:49:08 PM
Rating:
0/0 votes
Framework:
ASP.NET MVC 2
Platform:
Windows
Programming Language:
C#
Technologies:
jQuery, Telerik Extensions for MVC, Linq-to-Sql
Tags:
jquery, golf, golf tracker, C# classes, telerik
Demo site:
Home Page:
Share:

Golf Tracker - Adding Classes

In this episode I'll be adding classes to the many projects that I created in the last episode.  Since this is a quite time consuming process, I've created most of these classes offline to show you what I'm doing, but so you don't have to watch me hand code everything from scratch.

Business Objects

I'll begin with the business objects since they are going to be passed around all layers.  As I mentioned before, the business objects are going to be the classes that are transported between layers, not the Linq-To-Sql entity classes.  In addition to these classes working as Data Transfer Objects (DTO), I want them to also work in the views with validation.  So I'll decorate them with DataAnnotations attributes.

The Course business object looks like this:

01.using System;
02.using System.ComponentModel;
03.using System.ComponentModel.DataAnnotations;
04. 
05.namespace GolfTracker.BusinessObjects
06.{
07.    public class Course
08.    {
09.        [ScaffoldColumn(false)]
10.        public Guid ID { get; set; }
11. 
12.        [Required(ErrorMessage = "Course Name is required.")]
13.        [DisplayName("Course Name")]
14.        public string CourseName { get; set; }
15.         
16.        public string Location { get; set; }
17. 
18.        public int? VoteCount { get; set; }
19.        public int? VoteTotal { get; set; }
20.        public string rowversion { get; set; }
21.    }
22.}

It's a simple class that simply contains properties to carry data around.  Keeping the classes as simple and clean as possible is a priority for me.  This makes them easy to refactor and manage in the future.  Now I'll show you how they are used in the DataObjects layer.

DataObjects Layer

Here's a look at the DataObjects project and the various interfaces and classes I've added to make this work.

data objects folders and classes

The classes and interfaces are separated to make them easy to identify and maintain. 

For this episode I'm just creating a single vertical, meaning I'm just going to make a single database entity such as Course work from the data layer to the presentation layer.

I begin with the interfaces.  You'll see that there are two interfaces, ICourseRepository, and ICRUDRepository.  The ICourseRepository is what the concrete CourseRepository class will implement, so let's take a look at it.

01.using System;
02.using System.Collections.Generic;
03.using GolfTracker.BusinessObjects;
04. 
05.namespace GolfTracker.DataObjects.Interfaces
06.{
07.    public interface ICourseRepository : ICRUDRepository<GolfTracker.BusinessObjects.Course>
08.    {
09. 
10.    }
11.}

You'll see that the interface is empty, it has no members of it's own.  But it's implementing the ICRUDRepository interface and it's passing in the Course business object.  Let's see what that looks like.

01.using System;
02.using System.Linq;
03. 
04.namespace GolfTracker.DataObjects.Interfaces
05.{
06.    public interface ICRUDRepository<T>
07.    {
08.        IQueryable<T> GetAll();
09.        T GetById(Guid id);
10.        Guid Insert(T model);
11.        Guid Update(T model);
12.        void Delete(T model);
13.    }
14.}

This has several methods and we'll see that they are just CRUD methods, nothing at all custom.  The reason for this is to make management of your code easy.  This is especially helpful when you have a code generator generating your code.

When you create the CourseRepository concrete class, and implement the ICourseRepository interface, this will be the result.

01.using System;
02.using System.Linq;
03.using GolfTracker.DataObjects.Interfaces;
04. 
05.namespace GolfTracker.DataObjects.Repositories
06.{
07.    public class CourseRepository : ICourseRepository
08.    {
09. 
10.        #region ICRUDRepository<Course> Members
11. 
12.        public IQueryable<GolfTracker.BusinessObjects.Course> GetAll()
13.        {
14.            throw new NotImplementedException();
15.        }
16. 
17.        public GolfTracker.BusinessObjects.Course GetById(Guid id)
18.        {
19.            throw new NotImplementedException();
20.        }
21. 
22.        public Guid Insert(GolfTracker.BusinessObjects.Course model)
23.        {
24.            throw new NotImplementedException();
25.        }
26. 
27.        public Guid Update(GolfTracker.BusinessObjects.Course model)
28.        {
29.            throw new NotImplementedException();
30.        }
31. 
32.        public void Delete(GolfTracker.BusinessObjects.Course model)
33.        {
34.            throw new NotImplementedException();
35.        }
36. 
37.        #endregion
38.    }
39.}

All of the CRUDRepository interface members are stubbed out.  Now all I need to do is write the code that will be used for each of these methods.

Here is the code for the CourseRepository class.

001.using System;
002.using System.Collections.Generic;
003.using System.Data.Linq;
004.using System.Linq;
005.using GolfTracker.BusinessObjects;
006.using GolfTracker.DataObjects.Interfaces;
007.using GolfTracker.DataObjects.LinqToSql;
008. 
009.namespace GolfTracker.DataObjects.Repositories
010.{
011.    public class CourseRepository : ICourseRepository
012.    {
013.        #region ICRUDRepository<Course> Members
014. 
015.        public IQueryable<GolfTracker.BusinessObjects.Course> GetAll()
016.        {
017.            Database db = DataContextFactory.CreateContext();
018. 
019.            return db.Courses
020.                .Select(c => new GolfTracker.BusinessObjects.Course()
021.                {
022.                    ID = c.ID,
023.                    CourseName = c.CourseName,
024.                    VoteCount = c.VoteCount,
025.                    VoteTotal = c.VoteTotal,
026.                    rowversion = VersionConverter.ToString(c.rowversion)
027.                });
028.        }
029. 
030.        public GolfTracker.BusinessObjects.Course GetById(Guid id)
031.        {
032.            using (Database db = DataContextFactory.CreateContext())
033.            {
034.                return db.Courses.Where(c => c.ID == id)
035.                    .Select(c => new GolfTracker.BusinessObjects.Course()
036.                    {
037.                        ID = c.ID,
038.                        CourseName = c.CourseName,
039.                        VoteCount = c.VoteCount,
040.                        VoteTotal = c.VoteTotal,
041.                        rowversion = VersionConverter.ToString(c.rowversion)
042.                    }).SingleOrDefault();
043.            }
044.        }
045. 
046.        public Guid Insert(GolfTracker.BusinessObjects.Course model)
047.        {
048.            GolfTracker.DataObjects.LinqToSql.Course entity = new GolfTracker.DataObjects.LinqToSql.Course();
049.            entity.ID = model.ID;
050.            entity.CourseName = model.CourseName;
051.            entity.VoteCount = model.VoteCount;
052.            entity.VoteTotal = model.VoteTotal;
053.            entity.rowversion = VersionConverter.ToBinary(model.rowversion);
054. 
055.            using (Database db = DataContextFactory.CreateContext())
056.            {
057.                try
058.                {
059.                    db.Courses.InsertOnSubmit(entity);
060.                    db.SubmitChanges();
061.                    db.SubmitChanges();
062. 
063.                    model.ID = entity.ID;
064.                    model.rowversion = VersionConverter.ToString(entity.rowversion);
065.                    return entity.ID;
066.                }
067.                catch (ChangeConflictException)
068.                {
069.                    foreach (ObjectChangeConflict conflict in db.ChangeConflicts)
070.                    {
071.                        conflict.Resolve(RefreshMode.KeepCurrentValues);
072.                    }
073.                    try
074.                    {
075.                        db.SubmitChanges();
076.                    }
077.                    catch (ChangeConflictException)
078.                    {
079.                        throw new Exception("A concurrency error occurred!");
080.                    }
081.                    return entity.ID;
082.                }
083.                catch (Exception)
084.                {
085.                    throw new Exception("There was an error inserting the record!");
086.                }
087.            }
088.        }
089. 
090.        public Guid Update(GolfTracker.BusinessObjects.Course model)
091.        {
092.            GolfTracker.DataObjects.LinqToSql.Course entity = new GolfTracker.DataObjects.LinqToSql.Course();
093.            entity.ID = model.ID;
094.            entity.CourseName = model.CourseName;
095.            entity.VoteCount = model.VoteCount;
096.            entity.VoteTotal = model.VoteTotal;
097.            entity.rowversion = VersionConverter.ToBinary(model.rowversion);
098. 
099.            using (Database db = DataContextFactory.CreateContext())
100.            {
101.                try
102.                {
103.                    db.Courses.Attach(entity, true);
104.                    db.SubmitChanges();
105. 
106.                    model.ID = entity.ID;
107.                    model.rowversion = VersionConverter.ToString(entity.rowversion);
108.                    return entity.ID;
109.                }
110.                catch (ChangeConflictException)
111.                {
112.                    foreach (ObjectChangeConflict conflict in db.ChangeConflicts)
113.                    {
114.                        conflict.Resolve(RefreshMode.KeepCurrentValues);
115.                    }
116.                    try
117.                    {
118.                        db.SubmitChanges();
119.                    }
120.                    catch (ChangeConflictException)
121.                    {
122.                        throw new Exception("A concurrency error occurred!");
123.                    }
124.                    return entity.ID;
125.                }
126.                catch (Exception)
127.                {
128.                    throw new Exception("There was an error updating the record!");
129.                }
130.            }
131.        }
132. 
133.        public void Delete(GolfTracker.BusinessObjects.Course model)
134.        {
135.            GolfTracker.DataObjects.LinqToSql.Course entity = new GolfTracker.DataObjects.LinqToSql.Course();
136.            entity.ID = model.ID;
137.            entity.CourseName = model.CourseName;
138.            entity.VoteCount = model.VoteCount;
139.            entity.VoteTotal = model.VoteTotal;
140.            entity.rowversion = VersionConverter.ToBinary(model.rowversion);
141. 
142.            using (Database db = DataContextFactory.CreateContext())
143.            {
144.                try
145.                {
146.                    db.Courses.Attach(entity, false);
147.                    db.Courses.DeleteOnSubmit(entity);
148.                    db.SubmitChanges();
149.                }
150.                catch (ChangeConflictException)
151.                {
152.                    foreach (ObjectChangeConflict conflict in db.ChangeConflicts)
153.                    {
154.                        conflict.Resolve(RefreshMode.KeepCurrentValues);
155.                    }
156.                    try
157.                    {
158.                        db.SubmitChanges();
159.                    }
160.                    catch (ChangeConflictException)
161.                    {
162.                        throw new Exception("A concurrency error occurred!");
163.                    }
164.                }
165.                catch (Exception ex)
166.                {
167.                    throw new Exception("There was an error updating the record! " + ex.Message);
168.                }
169.            }
170.        }
171. 
172.        #endregion
173. 
174.    }
175.}

I've found that the more you build applications, the more you find yourself wishing there was a better and easier way of doing things.  Such was my problem when ever I needed to refactor a database table.  If I ended up adding a new column to a table, I then needed to add a property to the business object and then also modify all the properties in my repository.

Take a look at the code above and you'll see that every method contains model mapping information to go from either entity to business object or vice versa.  While this works, it's a nightmare to maintain.

To follow my example in the paragraph before, let's say I add a new column to the database table, let's see what I have to refactor.  I need to obviously refactor the Course business object and add the new property, that's no big deal, and I have to modify the Linq-To-Sql classes, no huge deal there either.

But look at all the places I'll need to modify in the CourseRepository class.  And this is only with the basic CRUD methods.  There are no custom methods, of which there will obviously be!

Is there a solution, yes - DataMappers!

DataMappers are separate classes that handle a single thing, mapping the properties back and forth between the business objects and the entities.

Here's the CourseMapper class.

01.using GolfTracker.DataObjects.LinqToSql;
02. 
03.namespace GolfTracker.DataObjects.Mappers
04.{
05.    public class CourseMapper
06.    {
07.        public static GolfTracker.BusinessObjects.Course ToBusinessObject(GolfTracker.DataObjects.LinqToSql.Course entity)
08.        {
09.            if (entity == null) return null;
10. 
11.            return new GolfTracker.BusinessObjects.Course()
12.            {
13.                ID = entity.ID,
14.                CourseName = entity.CourseName,
15.                Location = entity.Location,
16.                VoteCount = entity.VoteCount ?? 0,
17.                VoteTotal = entity.VoteTotal ?? 0,
18.                rowversion = VersionConverter.ToString(entity.rowversion)
19.            };
20.        }
21. 
22.        public static GolfTracker.DataObjects.LinqToSql.Course ToEntity(GolfTracker.BusinessObjects.Course model)
23.        {
24.            if (model == null) return null;
25. 
26.            return new GolfTracker.DataObjects.LinqToSql.Course()
27.            {
28.                ID = model.ID,
29.                CourseName = model.CourseName,
30.                Location = model.Location,
31.                VoteTotal = model.VoteTotal,
32.                VoteCount = model.VoteCount,
33.                rowversion = VersionConverter.ToBinary(model.rowversion)
34.            };
35.        }
36.    }
37.}

You can see that it easily maps all the entity properties and it's in one class that can easily be maintained.  So I'm going to refactor the CourseRepository class to use the DataMappers instead.

This is the newly refactored code for the CourseRepository class.

001.using GolfTracker.DataObjects.Interfaces;
002.using GolfTracker.DataObjects.LinqToSql;
003.using GolfTracker.DataObjects.Mappers;
004.using System.Linq;
005.using System;
006.using System.Data.Linq;
007. 
008.namespace GolfTracker.DataObjects.Repositories
009.{
010.    public class CoursesRepository : ICourseRepository
011.    {
012. 
013.        #region ICRUDRepository<Course> Members
014. 
015.        public IQueryable<GolfTracker.BusinessObjects.Course> GetAll()
016.        {
017.            Database db = DataContextFactory.CreateContext();
018. 
019.            return db.Courses.Select(c => CourseMapper.ToBusinessObject(c));
020.        }
021. 
022.        public GolfTracker.BusinessObjects.Course GetById(Guid id)
023.        {
024.            using (Database db = DataContextFactory.CreateContext())
025.            {
026.                return CourseMapper.ToBusinessObject(
027.                    db.Courses.Where(c => c.ID == id)
028.                    .SingleOrDefault());
029.            }
030.        }
031. 
032.        public Guid Insert(GolfTracker.BusinessObjects.Course model)
033.        {
034.            GolfTracker.DataObjects.LinqToSql.Course entity = CourseMapper.ToEntity(model);
035. 
036.            using (Database db = DataContextFactory.CreateContext())
037.            {
038.                try
039.                {
040.                    db.Courses.InsertOnSubmit(entity);
041.                    db.SubmitChanges();
042. 
043.                    model.ID = entity.ID;
044.                    model.rowversion = VersionConverter.ToString(entity.rowversion);
045.                    return entity.ID;
046.                }
047.                catch (ChangeConflictException)
048.                {
049.                    foreach (ObjectChangeConflict conflict in db.ChangeConflicts)
050.                    {
051.                        conflict.Resolve(RefreshMode.KeepCurrentValues);
052.                    }
053.                    try
054.                    {
055.                        db.SubmitChanges();
056.                    }
057.                    catch (ChangeConflictException)
058.                    {
059.                        throw new Exception("A concurrency error occurred!");
060.                    }
061.                    return entity.ID;
062.                }
063.                catch (Exception)
064.                {
065.                    throw new Exception("There was an error inserting the record!");
066.                }
067.            }
068.        }
069. 
070.        public Guid Update(GolfTracker.BusinessObjects.Course model)
071.        {
072.            GolfTracker.DataObjects.LinqToSql.Course entity = CourseMapper.ToEntity(model);
073. 
074.            using (Database db = DataContextFactory.CreateContext())
075.            {
076.                try
077.                {
078.                    db.Courses.Attach(entity, true);
079.                    db.SubmitChanges();
080. 
081.                    model.ID = entity.ID;
082.                    model.rowversion = VersionConverter.ToString(entity.rowversion);
083.                    return entity.ID;
084.                }
085.                catch (ChangeConflictException)
086.                {
087.                    foreach (ObjectChangeConflict conflict in db.ChangeConflicts)
088.                    {
089.                        conflict.Resolve(RefreshMode.KeepCurrentValues);
090.                    }
091.                    try
092.                    {
093.                        db.SubmitChanges();
094.                    }
095.                    catch (ChangeConflictException)
096.                    {
097.                        throw new Exception("A concurrency error occurred!");
098.                    }
099.                    return entity.ID;
100.                }
101.                catch (Exception)
102.                {
103.                    throw new Exception("There was an error updating the record!");
104.                }
105.            }
106.        }
107. 
108.        public void Delete(GolfTracker.BusinessObjects.Course model)
109.        {
110.            GolfTracker.DataObjects.LinqToSql.Course entity = CourseMapper.ToEntity(model);
111. 
112.            using (Database db = DataContextFactory.CreateContext())
113.            {
114.                try
115.                {
116.                    db.Courses.Attach(entity, false);
117.                    db.Courses.DeleteOnSubmit(entity);
118.                    db.SubmitChanges();
119.                }
120.                catch (ChangeConflictException)
121.                {
122.                    foreach (ObjectChangeConflict conflict in db.ChangeConflicts)
123.                    {
124.                        conflict.Resolve(RefreshMode.KeepCurrentValues);
125.                    }
126.                    try
127.                    {
128.                        db.SubmitChanges();
129.                    }
130.                    catch (ChangeConflictException)
131.                    {
132.                        throw new Exception("A concurrency error occurred!");
133.                    }
134.                }
135.                catch (Exception ex)
136.                {
137.                    throw new Exception("There was an error updating the record! " + ex.Message);
138.                }
139.            }
140.        }
141. 
142.        #endregion
143.    }
144.}

You can probably see that it looks like a lot less code now, which is true, but more importantly the code is now easier to maintain.

If I now need to add a new column to the database, all I need to do with the data layer is modify the CourseMapper class, and I'm done!  I don't even need to touch the CourseRepository class.  That's a huge time saver.

But since I'm using a DataMapper it means that I need to handle my POST methods differently.  The POST methods are Insert, Update and Delete. 

Notice that each of those methods don't simply call db.SubmitChanges().  That's because the entity that is being created in those methods by the DataMapper class, is not part of the data context.  You'll see that they are created outside the creation of the DataContext.  And since they are created outside the data context, they have to be attached to the context is some manner in order for Linq-To-Sql to process it correctly.

In the case of an Insert, I need to call the InsertOnSubmit(entity) method and pass in the local entity.  This ends up validating the manually created entity and attaches it to the context and then executes the proper action, in this case Insert.  Then I can call the db.SubmitChanges() method.

Also, because there is this abstraction of the data entity as it never leaves the data layer, we need to have that rowversion column in each table that will contain a timestamp to track the concurrency.  If we were doing all this data mapping and we didn't have any rowversion columns, none of this would work.

Building out the Service Layer

So now I'll move up to the service layer and show you what I've done with the interfaces and concrete classes.

The first I need to do is add some assembly references.  I'll add the references to the GolfTracker projects that I need here and some assemblies to help with dependency injection.

service layer references

The service layer interfaces are similar in functionality to the data repository interfaces in that there is on ICRUDService interface that contains members for standard CRUD methods, and the custom ICourseService interface implements that interface.

Here's the ICRUDService interface.

01.using System;
02.using System.Collections.Generic;
03. 
04.namespace GolfTracker.Services.Interfaces
05.{
06.    public interface ICRUDService<T>
07.    {
08.        List<T> GetAll();
09.        T GetById(Guid id);
10.        Guid Insert(T model);
11.        Guid Update(T model);
12.        void Delete(T model);
13.    }
14.}

And the ICourseService interface.

01.using System;
02.using System.Collections.Generic;
03. 
04.namespace GolfTracker.Services.Interfaces
05.{
06.    public interface ICourseService : ICRUDService<GolfTracker.BusinessObjects.Course>
07.    {
08. 
09.    }
10.}

Now the service classes can be called from the MVC controllers.

Before I want to go much further, I should probably do some unit test to make sure everything is working as it should.

Stay tuned.

Comments

    No comments yet.

 

User Name:
(Required)
Email:
(Required)