AngularJS, WebApi and DocumentDB
Here's the Visual Studio 2013 solution:
It's a simple solution, two projects. This allows for the single WebApi project to be consumed by any front end, whether it be a website, mobile site, or mobile app. As long as the clients can use REST, it will work.
The AngularJS Website
There is really nothing special about the way this AngularJS site is built, in fact it's fairly rudimentary compared to more advanced implementations.
It contains a single index.html file that has some Bootstrap markup and uses "ng-view" for partial HTML snippets. Being that this is a light-weight website with no server-side logic, everything is done via "api" calls to the WebApi service. This makes building the client-side code very quick once you know the endpoint.
This is a look at the folder structure for the Angular files:
The folders are partitioned to group entity types (such as golfer or golf club) and services. This helps with manageability of the code.
AngularJS controllers never communicate with the WebApi service directly, they always reference an AngularJS service, like "golferService", or "golfClubService". This helps with separation of concerns to abstract away the complexities of data access. The controllers only need to know that they communicate with an AngularJS service and it doesn't matter where the data is coming from. It could be from a SQL Server database, or a DocumentDB database, or some other provider, it doesn't matter.
AngularJS Services
The services in AngularJS are clean and simple. They handle the basic CRUD operations and any other custom operations that are needed. This is the GolfClubService:
01.
(
function
() {
02.
'use strict'
;
03.
04.
angular.module(
'golftracker'
)
05.
.service(
'golfClubService'
, [
"$http"
,
"mySettings"
,
06.
function
($http, mySettings) {
07.
var
url = mySettings.apiUriBase +
'api/golfclub'
;
08.
09.
this
.getGolfClubs =
function
() {
10.
return
$http.get(url)
11.
.then(
function
(response) {
12.
return
response.data;
13.
});
14.
};
15.
16.
this
.getGolfClub =
function
(id) {
17.
return
$http.get(url +
'/'
+ id);
18.
};
19.
20.
this
.insertGolfClub =
function
(golfclub) {
21.
return
$http.post(url, golfclub)
22.
.then(
function
(response) {
23.
return
response.data;
24.
});
25.
};
26.
27.
this
.updateGolfClub =
function
(golfclub) {
28.
return
$http.put(url +
'/'
+ golfclub.id, golfclub)
29.
.then(
function
(response) {
30.
return
response.data;
31.
});
32.
};
33.
34.
this
.deleteGolfClub =
function
(golfclub) {
35.
return
$http.
delete
(url +
'/'
+ golfclub.id)
36.
.then(
function
(response) {
37.
return
response.data;
38.
});
39.
};
40.
}]);
41.
})();
The service uses the built in $http provider for the Ajax calls to the WebApi service, allowing the AngularJS controllers to simply call standard methods on the service.
001.
(
function
() {
002.
'use strict'
;
003.
004.
angular.module(
'golftracker'
)
005.
.controller(
'golfClubController'
, [
"golfClubService"
,
006.
function
(golfClubService) {
007.
var
vm =
this
;
008.
009.
vm.golfclubs = [];
010.
vm.golfclub = {};
011.
vm.golfcourse = {};
012.
vm.tee = {};
013.
vm.teeIndex = -1;
014.
vm.courseIndex = -1;
015.
016.
vm.teeFormIsVisible =
false
;
017.
vm.golfClubFormIsVisible =
false
;
018.
vm.golfCourseFormIsVisible =
false
;
019.
vm.golfCoursesTableIsVisible =
false
;
020.
vm.showViewCoursesButton =
false
;
021.
022.
vm.isUpdate =
false
;
023.
vm.isDelete =
false
;
024.
025.
vm.dialogTitle =
"Add New"
;
026.
vm.teeFormTitle =
"Add"
;
027.
vm.courseFormTitle =
"Add"
;
028.
029.
// Load the golf clubs table when the page loads.
030.
golfClubService.getGolfClubs().then(
function
(data) {
031.
//console.log("data: " + JSON.stringify(data));
032.
vm.golfclubs = data;
033.
});
034.
035.
// Button to show or hide the "Add Golf Club Form"
036.
vm.showGolfClubForm =
function
() {
037.
hideAllForms()
038.
vm.dialogTitle =
"Add New"
;
039.
vm.golfClubFormIsVisible =
true
;
040.
vm.golfclub = {};
041.
if
(vm.golfClubFormIsVisible) {
042.
$(
"#golfClubName"
).attr(
"required"
,
"required"
);
043.
vm.golfCourseFormIsVisible =
false
;
044.
}
else
{
045.
$(
"#golfClubName"
).removeAttr(
"required"
);
046.
}
047.
};
048.
049.
// Insert or update the Golf Club and all nested documents.
050.
vm.insertOrUpdate =
function
(golfclub) {
051.
if
(golfclub.Name != undefined) {
052.
if
(golfclub.id ==
null
) {
053.
golfClubService.insertGolfClub(golfclub)
054.
.then(
function
(data) {
055.
vm.golfclubs.push(data.Result);
056.
vm.golfclub = {};
057.
vm.golfClubFormIsVisible =
false
;
058.
});
059.
}
else
{
060.
golfClubService.updateGolfClub(golfclub)
061.
.then(
function
(data) {
062.
vm.golfclub = {};
063.
vm.golfClubFormIsVisible =
false
;
064.
});
065.
}
066.
}
067.
};
068.
069.
// Get the selected golf club from the list and display the form.
070.
vm.update =
function
(idx) {
071.
vm.dialogTitle =
"Edit"
;
072.
vm.golfclub = vm.golfclubs[idx];
073.
vm.golfClubFormIsVisible =
true
;
074.
};
075.
076.
// Delete the selected golf club.
077.
vm.
delete
=
function
(idx) {
078.
if
(confirm(
"Are you sure you want to delete this golf club?"
)) {
079.
var
club = vm.golfclubs[idx];
080.
golfClubService.deleteGolfClub(club).then(
function
(data) {
081.
vm.golfclubs.splice(idx, 1);
082.
vm.golfCoursesTableIsVisible =
false
;
083.
});
084.
}
085.
};
086.
087.
// The cancel button on the Golf Club form.
088.
vm.cancel =
function
() {
089.
// I'm removing the "required" attribute before hiding the form.
090.
// This prevents a JavaScript exception in Chrome when an input
091.
// field is not focusable.
092.
$(
"#golfClubName"
).removeAttr(
"required"
);
093.
vm.golfclub = {};
094.
vm.golfClubFormIsVisible =
false
;
095.
};
096.
097.
// Display the Add Course form for the selected golf club.
098.
vm.showCourseForm =
function
(idx) {
099.
hideAllForms()
100.
vm.golfclub = vm.golfclubs[idx];
101.
vm.golfCourseFormIsVisible =
true
;
102.
vm.golfcourse = {};
103.
$(
"#golfCourseName"
).attr(
"required"
,
"required"
);
104.
};
105.
106.
// Add or edit the course to the golf club.
107.
vm.addOrUpdateCourse =
function
(id, courseObj) {
108.
// Validate the field.
109.
if
(courseObj.Name == undefined) {
110.
return
;
111.
}
112.
113.
// Get the golf club by the Id.
114.
var
c = getById(id, vm.golfclubs);
115.
116.
// Initialize the GolfCourses collection if it's null.
117.
if
(c.GolfCourses ===
null
) {
118.
c.GolfCourses = [];
119.
}
120.
121.
// Add the new GolfCourse object to the array.
122.
if
(vm.isUpdate) {
123.
// Replace the selected course information.
124.
c.GolfCourses.splice(vm.courseIndex, 1, courseObj);
125.
}
else
{
126.
// Add the new course to the array of golf courses.
127.
c.GolfCourses.push(courseObj);
128.
}
129.
130.
131.
// Update the document.
132.
golfClubService.updateGolfClub(c).then(
function
(data) {
133.
vm.golfCourseFormIsVisible =
false
;
134.
vm.golfcourse = {};
135.
});
136.
};
137.
138.
// Cancel the "Add Course" form.
139.
vm.cancelAddCourse =
function
() {
140.
$(
"#golfCourseName"
).removeAttr(
"required"
);
141.
vm.golfCourseFormIsVisible =
false
;
142.
};
143.
144.
vm.showGolfCoursesTable =
function
(idx) {
145.
hideAllForms();
146.
vm.golfclub = vm.golfclubs[idx];
147.
vm.golfCoursesTableIsVisible =
true
;
148.
};
149.
150.
vm.showTeeForm =
function
(id, idx) {
151.
// Get the golf club by the Id.
152.
var
club = getById(id, vm.golfclubs);
153.
vm.golfcourse = club.GolfCourses[idx];
154.
vm.tee = { Gender:
"Mens"
, Par: 72 };
155.
156.
vm.teeFormIsVisible =
true
;
157.
};
158.
159.
vm.cancelTeeForm =
function
() {
160.
$(
".tee-form input"
).removeAttr(
"required"
);
161.
vm.teeFormIsVisible =
false
;
162.
};
163.
164.
// Add or Edit the tee for the selected golf course.
165.
vm.submitTeeForm =
function
(isValid, id, course, tee) {
166.
if
(!isValid) {
167.
alert(
"This Tee form is not valid!"
);
168.
return
;
169.
}
170.
171.
// Get the golf club by the Id.
172.
var
club = getById(id, vm.golfclubs);
173.
174.
// Initialize the Tees array if it's null.
175.
if
(!vm.golfcourse.Tees) {
176.
vm.golfcourse.Tees = [];
177.
}
178.
179.
if
(vm.golfcourse.Tees ===
null
) {
180.
vm.golfcourse.Tees = [];
181.
}
182.
183.
// Add the new tee to the Tees array.
184.
// The 'club' object which represents the entire
185.
// golf club document is magically updated with x
186.
// Don't ask me how. I said, don't ask me how!
187.
//course.Tees.push(tee);
188.
if
(vm.isUpdate) {
189.
// Replace the old tee with the new tee
190.
vm.golfcourse.Tees.splice(vm.teeIndex, 1, tee);
191.
}
else
{
192.
// Insert new tee
193.
vm.golfcourse.Tees.push(tee);
194.
}
195.
196.
197.
// Update the entire document.
198.
golfClubService.updateGolfClub(club).then(
function
(data) {
199.
vm.teeFormIsVisible =
false
;
200.
vm.teeIndex = -1;
201.
vm.isUpdate =
false
;
202.
vm.isDelete =
false
;
203.
});
204.
};
205.
206.
// Show the Tee form for editing.
207.
vm.editTee =
function
(gc, idx) {
208.
vm.teeFormTitle =
"Edit"
;
209.
var
t = gc.Tees[idx];
210.
vm.teeIndex = idx;
211.
212.
vm.isUpdate =
true
;
213.
vm.golfcourse = gc;
214.
vm.tee = t;
215.
vm.teeFormIsVisible =
true
;
216.
};
217.
218.
// Identify the tee for deletion, then update the club.
219.
vm.deleteTee =
function
(club, course, idx) {
220.
if
(confirm(
"Are you sure you want to delete this tee?"
)) {
221.
course.Tees.splice(idx, 1);
222.
223.
golfClubService.updateGolfClub(club).then(
function
(data) {
224.
vm.teeFormIsVisible =
false
;
225.
vm.teeIndex = -1;
226.
vm.isUpdate =
false
;
227.
vm.isDelete =
false
;
228.
})
229.
}
230.
};
231.
232.
vm.closeCoursesPanel =
function
() {
233.
vm.golfCoursesTableIsVisible =
false
;
234.
};
235.
236.
vm.editCourse =
function
(club, gc, idx) {
237.
vm.courseFormTitle =
"Edit"
;
238.
hideAllForms();
239.
vm.isUpdate =
true
;
240.
vm.golfCourseFormIsVisible =
true
;
241.
242.
vm.courseIndex = idx;
243.
vm.golfclub = club;
244.
vm.golfcourse = gc;
245.
$(
"#golfCourseName"
).attr(
"required"
,
"required"
);
246.
};
247.
248.
vm.deleteCourse =
function
(club, gc, idx) {
249.
if
(confirm(
"Are you sure you want to delete this golf course?"
)) {
250.
club.GolfCourses.splice(idx, 1);
251.
252.
golfClubService.updateGolfClub(club).then(
function
(data) {
253.
vm.golfCoursesTableIsVisible =
false
;
254.
vm.courseIndex = -1;
255.
vm.isUpdate =
false
;
256.
vm.isDelete =
false
;
257.
})
258.
}
259.
};
260.
261.
262.
263.
264.
// Helper functions
265.
266.
function
hideAllForms() {
267.
vm.golfClubFormIsVisible =
false
;
268.
vm.golfCourseFormIsVisible =
false
;
269.
vm.golfCoursesTableIsVisible =
false
;
270.
}
271.
272.
function
getById(id, myArray) {
273.
return
myArray.filter(
function
(obj) {
274.
if
(obj.id == id) {
275.
return
obj
276.
}
277.
})[0]
278.
}
279.
280.
function
getByName(name, myArray) {
281.
return
myArray.filter(
function
(obj) {
282.
if
(obj.Name == name) {
283.
return
obj;
284.
}
285.
})[0]
286.
}
287.
288.
}]);
289.
})();
The HTML for the golf club contains various "dialogs" that only appear when specific buttons are clicked. Bootstrap is used to make everything nice looking and responsive.
<
h1
>Golf Clubs</
h1
>
<
p
>Enter golf club information here.</
p
>
<
div
class
=
"row"
>
<
div
class
=
"col-md-7"
>
<
button
class
=
"btn btn-success"
type
=
"button"
ng-click
=
"vm.showGolfClubForm()"
>
<
span
class
=
"glyphicon glyphicon-plus"
></
span
> Add Golf Club
</
button
>
<
table
class
=
"table"
>
<
thead
>
<
tr
>
<
th
>Golf Club</
th
>
<
th
>Location</
th
>
<
th
></
th
>
<
th
></
th
>
<
th
></
th
>
<
th
></
th
>
</
tr
>
</
thead
>
<
tbody
>
<
tr
ng-repeat
=
"gc in vm.golfclubs"
>
<
td
>{{ gc.Name }}</
td
>
<
td
>{{ gc.Location }}</
td
>
<
td
><
button
class
=
"glyphicon glyphicon-edit"
ng-click
=
"vm.update($index)"
title
=
"Edit {{ gc.Name }}"
></
button
></
td
>
<
td
><
button
class
=
"glyphicon glyphicon-trash"
ng-click
=
"vm.delete($index)"
title
=
"Delete {{ gc.Name }}"
></
button
></
td
>
<
td
><
button
ng-click
=
"vm.showCourseForm($index)"
>Add Course</
button
></
td
>
<
td
><
button
ng-click
=
"vm.showGolfCoursesTable($index)"
ng-if="gc.GolfCourses.length > 0">Courses <
span
class
=
"badge"
>{{ gc.GolfCourses.length }}</
span
></
button
></
td
>
</
tr
>
</
tbody
>
</
table
>
</
div
>
<
div
class
=
"col-md-5"
>
<!-- Form for managing Golf Clubs -->
<
div
class
=
"panel panel-primary"
ng-show
=
"vm.golfClubFormIsVisible"
>
<
div
class
=
"panel-heading"
>
{{ vm.dialogTitle }} Golf Club
</
div
>
<
div
class
=
"panel-body"
>
<
form
>
<
div
class
=
"form-group"
>
<
label
>Golf Club</
label
>
<
input
type
=
"text"
class
=
"form-control"
id
=
"golfClubName"
required
ng-model
=
"vm.golfclub.Name"
/>
</
div
>
<
div
class
=
"form-group"
>
<
label
>Location</
label
>
<
input
type
=
"text"
class
=
"form-control"
ng-model
=
"vm.golfclub.Location"
/>
</
div
>
<
button
class
=
"btn btn-primary"
ng-click
=
"vm.insertOrUpdate(vm.golfclub)"
>Save</
button
>
<
button
class
=
"btn btn-default"
ng-click
=
"vm.cancel()"
>Cancel</
button
>
</
form
>
</
div
>
</
div
>
<!-- Form for managing Golf Courses for the selected golf club. -->
<
div
class
=
"panel panel-primary"
ng-show
=
"vm.golfCourseFormIsVisible"
>
<
div
class
=
"panel-heading"
>
{{ vm.courseFormTitle }} Course - {{ vm.golfclub.Name }}
</
div
>
<
div
class
=
"panel-body"
>
<
form
>
<
div
class
=
"form-group"
>
<
label
>Course Name</
label
>
<
input
type
=
"text"
class
=
"form-control"
id
=
"golfCourseName"
required
ng-model
=
"vm.golfcourse.Name"
/>
</
div
>
<
button
class
=
"btn btn-primary"
ng-click
=
"vm.addOrUpdateCourse(vm.golfclub.id, vm.golfcourse)"
>{{ vm.courseFormTitle }} Course</
button
>
<
button
class
=
"btn btn-default"
ng-click
=
"vm.cancelAddCourse()"
>Cancel</
button
>
</
form
>
</
div
>
</
div
>
<!-- The Table showing the golf courses for the selected golf club, to add tees. -->
<
div
class
=
"panel panel-primary"
ng-show
=
"vm.golfCoursesTableIsVisible"
>
<
div
class
=
"panel-heading"
>
<
h3
class
=
"panel-title"
>Golf Courses for {{ vm.golfclub.Name }}</
h3
>
<
span
class
=
"glyphicon glyphicon-remove-sign panel-close-button"
ng-click
=
"vm.closeCoursesPanel()"
></
span
>
</
div
>
<
div
class
=
"panel-body"
>
<
div
ng-repeat
=
"gc in vm.golfclub.GolfCourses"
>
<
div
class
=
"row tee-row"
>
<
div
class
=
"col-md-9"
>
<
h3
>{{ gc.Name }} <
span
class
=
"glyphicon glyphicon-pencil small"
title
=
"Edit Course"
ng-click
=
"vm.editCourse(vm.golfclub, gc, $index)"
></
span
> <
span
class
=
"glyphicon glyphicon-remove small"
title
=
"Delete Course"
ng-click
=
"vm.deleteCourse(vm.golfclub, gc, $index)"
></
span
></
h3
>
</
div
>
<
div
class
=
"col-md-3 text-right"
>
</
div
>
</
div
>
<
div
class
=
"row"
>
<
div
class
=
"col-md-12"
>
<
table
class
=
"table"
>
<
thead
>
<
tr
>
<
th
>Name</
th
>
<
th
>Gender</
th
>
<
th
>Par</
th
>
<
th
>Length</
th
>
<
th
>Slope</
th
>
<
th
>Rating</
th
>
<
th
></
th
>
<
th
><
span
class
=
"glyphicon glyphicon-plus text-success"
title
=
"Add a new tee"
ng-click
=
"vm.showTeeForm(vm.golfclub.id, $index)"
></
span
></
th
>
</
tr
>
</
thead
>
<
tbody
>
<
tr
ng-repeat
=
"tee in gc.Tees"
>
<
td
>{{ tee.TeeName }}</
td
>
<
td
>{{ tee.Gender }}</
td
>
<
td
>{{ tee.Par }}</
td
>
<
td
>{{ tee.Length }}</
td
>
<
td
>{{ tee.Slope }}</
td
>
<
td
>{{ tee.Rating }}</
td
>
<
td
>
<
span
class
=
"glyphicon glyphicon-edit"
title
=
"Edit Tee"
ng-click
=
"vm.editTee(gc, $index)"
></
span
>
</
td
>
<
td
>
<
span
class
=
"glyphicon glyphicon-trash"
title
=
"Delete Tee"
ng-click
=
"vm.deleteTee(vm.golfclub, gc, $index)"
></
span
>
</
td
>
</
tr
>
</
tbody
>
</
table
>
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
<!-- The form for adding a Tee to the selected golf course. -->
<
div
class
=
"panel panel-danger"
ng-show
=
"vm.teeFormIsVisible"
>
<
div
class
=
"panel-heading"
>
<
h3
class
=
"panel-title"
>{{ vm.teeFormTitle }} Tee for {{ vm.golfclub.Name }} - {{ vm.golfcourse.Name }}</
h3
>
</
div
>
<
div
class
=
"panel-body"
>
<
form
class
=
"tee-form"
name
=
"teeform"
novalidate
ng-submit
=
"vm.submitTeeForm(teeform.$valid, vm.golfclub.id, vm.golfcourse, vm.tee)"
>
<
div
class
=
"row"
>
<
div
class
=
"form-group col-md-6"
>
<
label
>Name</
label
>
<
input
type
=
"text"
name
=
"Name"
class
=
"form-control"
required
ng-model
=
"vm.tee.TeeName"
/>
</
div
>
<
div
class
=
"form-group col-md-6"
>
<
label
>Gender</
label
>
<
select
class
=
"form-control"
name
=
"Gender"
ng-model
=
"vm.tee.Gender"
>
<
option
value
=
"Mens"
>Men's</
option
>
<
option
value
=
"Ladies"
>Ladies</
option
>
</
select
>
</
div
>
</
div
>
<
div
class
=
"row"
>
<
div
class
=
"form-group col-md-6"
>
<
label
>Length</
label
>
<
input
type
=
"number"
name
=
"Length"
class
=
"form-control"
required
ng-model
=
"vm.tee.Length"
/>
</
div
>
<
div
class
=
"form-group col-md-6"
>
<
label
>Par</
label
>
<
input
type
=
"number"
name
=
"Par"
class
=
"form-control"
required
ng-model
=
"vm.tee.Par"
/>
</
div
>
</
div
>
<
div
class
=
"row"
>
<
div
class
=
"form-group col-md-6"
>
<
label
>Slope</
label
>
<
input
type
=
"number"
name
=
"Slope"
class
=
"form-control"
required
ng-model
=
"vm.tee.Slope"
/>
</
div
>
<
div
class
=
"form-group col-md-6"
>
<
label
>Rating</
label
>
<
input
type
=
"text"
name
=
"Rating"
class
=
"form-control"
ng-model
=
"vm.tee.Rating"
/>
</
div
>
</
div
>
<
button
type
=
"submit"
ng-disabled
=
"teeform.$invalid"
class
=
"btn btn-primary"
>{{ vm.teeFormTitle }} Tee</
button
>
<
button
type
=
"button"
class
=
"btn btn-default"
ng-click
=
"vm.cancelTeeForm()"
>Cancel</
button
>
</
form
>
</
div
>
</
div
>
</
div
>
</
div
>
When the client is built, it just needs to have data to populate the fields in the HTML and we get that from the WebApi service.
WebApi and Azure DocumentDB
For this section I will assume you know about WebApi and how to use it. I will just be describing how I integrated DocumentDB into WebApi for use as the data store.
In general, this WebApi looks like a standard service from the top down, so looking at the apiControllers will look very familiar and standard. They just take a repository in the constructor and are used within standard CRUD methods. And the repository class has DocumentDB specific methods for the CRUD operations which make implementation a breeze.
[RoutePrefix(
"api/golfer"
)]
public
class
GolferController : ApiController
{
#region ctors
private
readonly
IGolferRepository _repo;
public
GolferController(IGolferRepository repo)
{
this
._repo = repo;
}
#endregion
#region Standard CRUD
[AllowAnonymous]
public
IEnumerable<Golfer> Get()
{
var result = _repo.Get();
return
result;
}
public
Task<Golfer> GetById(
string
id)
{
return
_repo.GetById(id);
}
[Authorize]
public
async Task<IHttpActionResult> Post([FromBody]Golfer entity)
{
var result = await _repo.CreateDocumentAsync(entity);
var id = result.Resource.Id;
var model = _repo.GetById(id);
return
Ok(model);
}
[Authorize]
public
async Task<IHttpActionResult> Put(
string
id, [FromBody]Golfer entity)
{
await _repo.UpdateDocumentAsync(entity);
var model = _repo.GetById(id);
return
Ok(model);
}
[Authorize]
public
async Task<IHttpActionResult> Delete(
string
id)
{
await _repo.DeleteDocumentAsync(id);
return
Ok();
}
#endregion
}
Important Note about Document Updates: as of May 2015 DocumentDB doesn't have the concept of partial document updates, so updates are an entire document replacement. I haven't found this to be an issue, but I thought I would point it out.
The GolferRepository class is very simple and clean.
01.
public
class
GolferRepository :
02.
RepositoryBase<Golfer>, IGolferRepository
03.
{
04.
public
GolferRepository():
base
(
"golfer"
,
05.
AppSettingsConfig.Db,
"GolfCollection"
)
06.
{
07.
08.
}
09.
}
Make note of the "golfer" string in the base constructor on line 4. I'll address that in a moment, but that is crucial to making this Single Collection Concept work. The other parameters for the base constructor is the database name, and the collection name. This pattern allows for flexibility in using the same repository base and pointing to a different database and collection.
The class inherits from the generic RepositoryBase<T> and passes in the Golfer type, and implements the IGolferRepository interface.
1.
/// <summary>
2.
/// Add custom members here.
3.
/// </summary>
4.
public
interface
IGolferRepository : IRepository<Golfer>
5.
{
6.
}
For this application as it stands now, it's empty. There is no need for any custom members. But if you had some you wanted to add, they would be included in this interface. The CRUD members are in the generic IRepository<T> interface.
01.
public
interface
IRepository<T>
02.
where T : EntityBase
03.
{
04.
System.Collections.Generic.IEnumerable<T> Get(System.Linq.Expressions.Expression<Func<T,
bool
>> predicate =
null
);
05.
System.Threading.Tasks.Task<T> GetById(
string
id);
06.
07.
System.Threading.Tasks.Task<Microsoft.Azure.Documents.Client.ResourceResponse<Microsoft.Azure.Documents.Document>> CreateDocumentAsync(T entity);
08.
System.Threading.Tasks.Task<Microsoft.Azure.Documents.Client.ResourceResponse<Microsoft.Azure.Documents.Document>> DeleteDocumentAsync(
string
id);
09.
System.Threading.Tasks.Task<Microsoft.Azure.Documents.Client.ResourceResponse<Microsoft.Azure.Documents.Document>> UpdateDocumentAsync(T entity);
10.
}
RepositoryBase<T>
This is the work-horse of the operation. It's the class that implements the CRUD operations for DocumentDB, and it contains the "sugar" for managing queries for you based on the "type" property.
001.
/// <summary>
002.
/// All repository classes must inherit from this base class. This base class
003.
/// contains all the basic CRUD operations.
004.
/// </summary>
005.
/// <typeparam name="T">The entity type used for the repository.</typeparam>
006.
public
class
RepositoryBase<T> :
007.
DocumentDbClient, IRepository<T> where T : EntityBase
008.
{
009.
#region ctors
010.
011.
private
Expression<Func<T,
bool
>> _typePredicate =
null
;
012.
013.
/// <summary>
014.
/// All Repository classes must inherit this base class.
015.
/// </summary>
016.
/// <param name="type">The name of the entity (T), which is the same as
017.
/// the name passed into the model (lowercase).</param>
018.
/// <param name="dbName">The name of the database.</param>
019.
/// <param name="collectionName">The name of the collection.</param>
020.
public
RepositoryBase(
string
type,
string
dbName,
string
collectionName)
021.
:
base
(dbName, collectionName)
022.
{
023.
if
(
string
.IsNullOrEmpty(type))
024.
{
025.
throw
new
ArgumentNullException(
"type"
);
026.
}
027.
028.
_typePredicate = v => v.type == type;
029.
}
030.
031.
#endregion
032.
033.
#region Public Methods
034.
035.
/// <summary>
036.
/// Get a list of T, with an optional predicate.
037.
/// </summary>
038.
/// <param name="predicate">The linq expression Where clause.</param>
039.
/// <returns>An IEnumerable of T.</returns>
040.
public
IEnumerable<T> Get(Expression<Func<T,
bool
>> predicate =
null
)
041.
{
042.
var query = Client.CreateDocumentQuery<T>(Collection.DocumentsLink)
043.
.Where(_typePredicate)
044.
.AsQueryable();
045.
046.
if
(predicate !=
null
)
047.
{
048.
query = query.Where(predicate);
049.
}
050.
051.
return
query;
052.
}
053.
054.
public
Task<T> GetById(
string
id)
055.
{
056.
return
Task<T>.Run(() =>
057.
Client.CreateDocumentQuery<T>(Collection.DocumentsLink)
058.
.Where(_typePredicate)
059.
.Where(p => p.Id == id)
060.
.AsEnumerable()
061.
.FirstOrDefault());
062.
}
063.
064.
public
async Task<ResourceResponse<Document>> CreateDocumentAsync(T entity)
065.
{
066.
return
await Client.CreateDocumentAsync(Collection.DocumentsLink, entity);
067.
}
068.
069.
public
async Task<ResourceResponse<Document>> UpdateDocumentAsync(T entity)
070.
{
071.
var doc = GetDocument(entity.Id);
072.
073.
return
await Client.ReplaceDocumentAsync(doc.SelfLink, entity);
074.
}
075.
076.
public
async Task<ResourceResponse<Document>> DeleteDocumentAsync(
string
id)
077.
{
078.
var doc = GetDocument(id);
079.
080.
return
await Client.DeleteDocumentAsync(doc.SelfLink);
081.
}
082.
083.
084.
#endregion
085.
086.
#region Private Methods
087.
088.
private
Document GetDocument(
string
id)
089.
{
090.
var doc = Client.CreateDocumentQuery<Document>(Collection.DocumentsLink)
091.
.Where(d => d.Id == id)
092.
.AsEnumerable()
093.
.FirstOrDefault();
094.
return
doc;
095.
}
096.
097.
098.
#endregion
099.
}
The Single Collection Concept works because the "type" value is automatically passed into the constructor from the Repository class. And on line 11 a Linq Expression is prepared and created inside the constructor on line 28. It's a Lambda expression that will be passed into the two Get methods into Where clauses.
So whenever a Get is called, there is a Where clause that contains the expression for the "type" property. This is what makes the Single Collection Concept work without your intervention in your daily operations.
You'll see lines 43 and 58 contain the Where clauses that take the "typePredicate". From there all we need to do is make sure we are getting a single instance of the DocumentDB Client. And we get that from the "DocumentDBClient" class that the RepositoryBase class inherits.
DocumentDBClient
The DocumentDBClient class contains all the connection glue for instantiating an instance of DocumentDB.
001.
/// <summary>
002.
/// This is the DocumentDB client class that the RepositoryBase class
003.
/// will inherit to consume the properties.
004.
/// </summary>
005.
public
class
DocumentDbClient: Disposable
006.
{
007.
#region ctors
008.
009.
private
Database _database;
010.
011.
private
readonly
string
_dbName;
012.
013.
private
readonly
string
_collectionName;
014.
015.
public
DocumentDbClient(
string
dbName,
string
collectionName)
016.
{
017.
this
._dbName = dbName;
018.
this
._collectionName = collectionName;
019.
}
020.
021.
#endregion
022.
023.
#region Public Properties
024.
025.
private
DocumentClient _client;
026.
public
DocumentClient Client
027.
{
028.
get
029.
{
030.
if
(_client ==
null
)
031.
{
032.
// This could be handled differently.
033.
string
endpoint = AppSettingsConfig.EndPoint;
034.
string
authkey = AppSettingsConfig.AuthKey;
035.
036.
Uri endpointUri =
new
Uri(endpoint);
037.
_client =
new
DocumentClient(endpointUri, authkey);
038.
}
039.
return
_client;
040.
}
041.
}
042.
043.
#endregion
044.
045.
#region Private Methods
046.
047.
private
Database SetupDatabase()
048.
{
049.
var db = Client.CreateDatabaseQuery()
050.
.Where(d => d.Id == _dbName)
051.
.AsEnumerable()
052.
.FirstOrDefault();
053.
054.
if
(db ==
null
)
055.
{
056.
db = Client
057.
.CreateDatabaseAsync(
058.
new
Database { Id = _dbName }).Result;
059.
}
060.
return
db;
061.
}
062.
063.
private
async Task SetupCollection(
string
databaseLink)
064.
{
065.
_collection = Client
066.
.CreateDocumentCollectionQuery(databaseLink)
067.
.Where(c => c.Id == _collectionName)
068.
.AsEnumerable()
069.
.FirstOrDefault();
070.
071.
if
(_collection ==
null
)
072.
{
073.
var collectionSpec =
074.
new
DocumentCollection { Id = _collectionName };
075.
_collection = await Client
076.
.CreateDocumentCollectionAsync(
077.
databaseLink, collectionSpec);
078.
}
079.
}
080.
081.
protected
Database Database
082.
{
083.
get
084.
{
085.
if
(_database ==
null
)
086.
{
087.
_database = SetupDatabase();
088.
}
089.
090.
return
_database;
091.
}
092.
}
093.
094.
private
DocumentCollection _collection;
095.
protected
DocumentCollection Collection
096.
{
097.
get
098.
{
099.
if
(_collection ==
null
)
100.
{
101.
SetupCollection(Database.SelfLink).Wait();
102.
}
103.
return
_collection;
104.
}
105.
}
106.
107.
#endregion
108.
109.
#region Overrides
110.
111.
protected
override
void
DisposeCore()
112.
{
113.
if
(_client !=
null
)
114.
{
115.
_client.Dispose();
116.
}
117.
}
118.
119.
#endregion
120.
}
Cross Origin Resource Sharing (CORS)
The benefit of having a stand-alone WebApi project is that it can be consumed by any client. You probably know that you can have WebApi enabled inside an ASP.NET MVC application, but if you plan on using the WebApi for other clients, it's probably best to have it stand alone so any changes to make to it, doesn't affect the usage of the MVC application.
But when we have a stand-alone api service, web clients can have trouble communicating from other domains. This is a security feature preventing unauthorized access to your api. But you can easily allow specific domains, or all domains access to your api with some simple steps.
By installing the CORS NuGet package you can enable CORS in the application. Open the NuGet Packages dialog and search for "microsoft.aspnet.cors".
This installs the necessary assemblies into your project. From there you can enable CORS in a variety of ways. I've chosen to create a factory that can enable it application-wide, but you can chose a simpler method.
To see the various ways to enable CORS, take a look at this article.
Summary
This article has shown you how to create a WebApi project to use Azure's DocumentDB to perform common REST functions, and how to implement the Single Collection Concept to store documents of various shapes in the same collection.
This technique was employed primarily for budgetary purposes since DocumentDB charges you based on the number of Collections you create. So if you want to use DocumentDB and have various documents of different shapes that your application uses, you can easily store them all in the same collection following this method.
You will no doubt come up with ways to refine this, but this is a good starting point.
Credits:
I want to make sure I give credit to Richard Leopold for creating a github project that uses DocumentDB with WebApi and AngularJS. His project was the basis for my project and I embellished it for my own use. But his project contained much of the AngularJS code that I employed.
https://github.com/rleopold/DocDbWebApi