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.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.
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.