First, add a new model class called DiaryEntry
with the contents from Listing 13.
Listing 13: DiaryEntry.cs
using System; namespace MembaseTutorial.Models { [Serializable] public class DiaryEntry { public Guid DiaryEntryId { get; set; } public Guid UserId { get; set; } public String Title { get; set; } public String Text { get; set; } public DateTime CreationDate { get; set; } } }
Next add MakeEntry action methods (Listing 14)
in HomeController.
Listing 14: MakeEntry actions.
/// <summary> /// GET: /Home/MakeEntry /// </summary> /// <returns></returns> public ActionResult MakeEntry() { var client = MvcApplication.MembaseClient; User user; if (!IsAuthenticated(client, out user)) return RedirectToAction("Login"); return View(); } /// <summary> /// POST: /Home/MakeEntry /// </summary> /// <param name="collection"></param> /// <returns></returns> [HttpPost] public ActionResult MakeEntry(FormCollection collection) { var client = MvcApplication.MembaseClient; User user; if (!IsAuthenticated(client, out user)) RedirectToAction("Login"); var title = collection["EntryTitle"]; var text = collection["EntryText"]; // Persist these in the database var entry = new DiaryEntry { DiaryEntryId = Guid.NewGuid(), Text = text, Title = title, CreationDate = DateTime.Now, UserId = user.UserId }; client.Store(StoreMode.Set, entry.DiaryEntryId.ToString(), entry); // Now we need to keep track of the entries a user has. var entriesKey = user.UserId + ".DiaryEntries"; AddToListWithCas(client, entriesKey, entry.DiaryEntryId); return RedirectToAction("Index"); }
You can see that this method uses another method called
AddToListWithCas to make sure that the list of
diary entries for each user is added to the database. Add this
method to HomeController.cs from Listing 15.
Listing 15: AddToListWithCas
/// <summary> /// Adds a value to a list of type T associated with the given Key using a /// CAS operation and retries. /// </summary> /// <typeparam name="T"></typeparam> /// <param name="client"></param> /// <param name="key"></param> /// <param name="entry"></param> private static void AddToListWithCas<T>(MembaseClient client, string key, T entry) { CasResult<bool> casResult; do { var result = client.GetWithCas<List<T>>(key); var list = result.Result ?? new List<T>(); var cas = result.Cas; // Avoid duplicates if (list.Contains(entry)) return; list.Add(entry); casResult = client.Cas(StoreMode.Set, key, list, cas); } while (casResult.Result == false); }
A CAS (Check And Set) operation is the correct way to ensure that
operations happen without data loss without requiring distributed
locking protocols or transactions to be handled. The way it works
is you get the value of something with its cas. This is a
monotonically increasing number that can be thought of as the
version number of the object in the database. You mutate the
object (change it in some way) and use a Cas
operation to write it back to the database. This will fail (a
casResult whose Result property is set to
false) if some other client had modified the value in the database
in the meantime. Notice the use of the '??'
operator. This says assign to the list variable
result.Result if it is not null; otherwise
create an empty list.
So, in this case, the loop is repeated until the operation succeeds. This is a simplified example of how to do this. You may want to limit the number of retries performed before failing.
Also note that in this case it is highly unlikely that another user will be modifying the same list, so the Cas operation is probably overkill, but we will need this operation later in situations where the database could indeed be modified at the same time by another user.
Next add the form for creating a new diary entry by right clicking on the Home folder under Views, and choosing Add | View and naming it MakeEntry. See Listing 16 for the contents of this file.
Listing 16: MakeEntry.cshtml
@{ ViewBag.Title = "Diary Entry"; } <h2>Diary Entry</h2> @using (Html.BeginForm()) { @Html.ValidationSummary(true) <fieldset> <legend>Enter your diary entry:</legend> <p> Title: @Html.TextBox("EntryTitle") </p> <p> @Html.TextArea("EntryText", "", 20, 80, null) </p> <p> <input type="submit" value="Done" /> </p> </fieldset> } <div> @Html.ActionLink("Go back...", "Index") </div>
You will need a place to display the diary entries too. We'll just display them as a bulleted list in the Index view. Edit Index.cshtml and add the content in Listing 17 before the Logout link.
Listing 17, an update to Index.cshtml
<p>@Html.ActionLink("Make diary entry...", "MakeEntry")</p> @if (ViewData.ContainsKey("DiaryEntries")) { <ul> @foreach (var entry in ((IList<DiaryEntry>)ViewData["DiaryEntries"])) { <li>@entry.CreationDate.ToShortDateString() @entry.CreationDate.ToShortTimeString() - @entry.Title <p>@entry.Text</p> </li> } </ul> }
Finally you need to make an update (Listing 18) to the Index action in HomeController.cs to generate the view data that the view is trying to display. Insert these lines just before the return at the end of the method.
Listing 18: Update Index method of
HomeController.cs
var entriesKey = user.UserId + ".DiaryEntries"; var result = client.Get<List<Guid>>(entriesKey); if (result != null) { // First we want to display all of a user's diary entries // Convert all of the Guids to strings to do a multi-get with var keys = result.Select(key => key.ToString()).ToList(); var entryDictionary = client.Get(keys); var diaryEntries = new List<DiaryEntry>( entryDictionary.Values.Cast<DiaryEntry>()); diaryEntries.Sort((a, b) => a.CreationDate.CompareTo(b.CreationDate)); ViewData["DiaryEntries"] = diaryEntries; }
The list of diary entry keys is retrieved from the database first.
If the list exists, we convert these to a list of strings to use
in a multi-get operation. The multi-get operation returns an entry
dictionary but we are only interested in the fact that the values
are all DiaryEntry objects, so we typecast them
all using a quick LINQ expression. Finally the entries are sorted
by creation date, and stored into the ViewData
under the DiaryEntries key.
Now you can compile the application and have some fun writing diary entries. You will also be able to log out, create other users, and make more diary entries. Don't get too carried away.
That's a lot of fun, but what if we tried to make this a more social operation? Imagine if you could be told the names of the top three users who are making diary entries containing many of the same words that you are. The final stage of this tutorial will discuss doing just that.