Story Details for articles

4 - DocumentDB - Golf Tracker Article - Visual Studio Deep Dive

kahanu
Author:
Version:
Views:
1232
Date Posted:
5/26/2015 9:34:04 PM
Date Updated:
5/26/2015 9:34:04 PM
Rating:
0/0 votes
Framework:
DocumentDB, WebApi, AngularJS
Platform:
Windows
Programming Language:
C#, JavaScript
Technologies:
DocumentDB, WebApi, AngularJS
Tags:
documentdb, webapi, angularjs
Demo site:
Home Page:
https://github.com/kahanu/GolfTracker.DocumentDB
Share:

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.}
You'll notice that T is of type EntityBase and this is important too because this class contains the "Id" property and the "type" property responsible for making document type queries possible.

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.}
Creating a DocumentDB client this way is best since you should use the same instance of the client for all requests, and implement IDisposable.

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

Comments

    No comments yet.

 

User Name:
(Required)
Email:
(Required)