The MVC scaffolding that created the Razor template to list breweries also included links to create, show, edit and delete breweries. Using more scaffolding, these CRUD features are easily implemented.
Create and Update methods require a bit of effort to encapsulate.
One decision to make is whether to use the detailed result
ExecuteStore method or the
Boolean> Store method of the Client.
ExecuteStore returns an instance of an
IStoreOperationResult, which contains a success
status and error message properties, among others.
Since it is likely important to know whether operations succeeded,
ExecuteStore will be used in our
RepositoryBase. However, that interface will be
hidden from the application and instead an int will be returned by
each method. The int will be the status code returned by Couchbase
Server for each operation.
public virtual int Create(T value){} public virtual int Update(T value) {} public virtual int Save(T value) {}
There are other implementation details that need to be considered when implementing these methods, namely key creation and JSON serialization.
CRUD operations in Couchbase are performed using a key/value API. The key that is used for these operations may be either meaningful (i.e., human readable) or arbitrary (e.g., a GUID). When made human readable, your application may be able to make use of predictable keys to perform key/value get operations (as opposed to secondary indexes by way of view operations).
A common pattern for creating readable keys is to take a unique
property, such as Brewery.Name, and replace its
spaces, possibly normalizing to lowercase. So “Thomas Hooker
Brewery” becomes “thomas_hooker_brewery.”
Add the following BuildKey method to the
RepositoryBase to allow for default key
creation based on the Id property.
protected virtual string BuildKey(T model) { if (string.IsNullOrEmpty(model.Id)) { return Guid.NewGuid().ToString(); } return model.Id.InflectTo().Underscored; }
BuildKey will default to a
GUID string when no Id is provided. It's also
virtual so that subclasses are able to override the default
behavior. The BreweryRepository needs to
override the default behavior to provide a key based on brewery
name.
protected override string BuildKey(Brewery model) { return model.Name.InflectTo().Underscored; }
When storing a Brewery instance in Couchbase
Server, it first has to be serialized into a JSON string. An
important consideration is how to map the properties of the
Brewery to properties of the JSON document.
JSON.NET (from Newtonsoft.Json) will by default serialize all
properties. However, ModelBase objects all have
an Id property that shouldn't be serialized into the stored JSON.
That Id is already being used as the document's key (in the
key/value operations), so it would be redundant to store it in the
JSON.
JSON.NET supports various serialization settings, including which
properties should be included in serialization. In
RepositoryBase, create a
serializAndIgnoreId method and a private
DocumentIdContractResolver class as shown
below.
private string serializeAndIgnoreId(T obj) { var json = JsonConvert.SerializeObject(obj, new JsonSerializerSettings() { ContractResolver = new DocumentIdContractResolver(), }); return json; } private class DocumentIdContractResolver : CamelCasePropertyNamesContractResolver { protected override List<MemberInfo> GetSerializableMembers(Type objectType) { return base.GetSerializableMembers(objectType).Where(o => o.Name != "Id").ToList(); } }
The DocumentIdContractResolver will prevent the
Id property from being saved into the JSON. It also extends
CamelCasePropertyNamesContractResolver to
provide camel-cased properties in the JSON output.
Note that there is a JsonIgnore attribute that
could be added to properties that should be omitted from the
serialized JSON, however it is less global in its application. For
example, if a class overrides the Id property
of ModelBase, it would have to add the
attribute.
With this new plumbing in place, it's now possible to complete the
Create, Update and
Save methods. Exceptions are caught and wrapped
in the IStoreOperationResult's Exception property. If an exception
is detected, it will be thrown up to the caller. These new methods
also have an optional durability argument, which will block until
a document has been written to disk, or the operation times out.
By default, there is no durability requirement imposed.
public virtual int Create(T value, PersistTo persistTo = PersistTo.Zero) { var result = _Client.ExecuteStore(StoreMode.Add, BuildKey(value), serializeAndIgnoreId(value), persistTo); if (result.Exception != null) throw result.Exception; return result.StatusCode.Value; } public virtual int Update(T value, PersistTo persistTo = PersistTo.Zero) { var result = _Client.ExecuteStore(StoreMode.Replace, value.Id, serializeAndIgnoreId(value), persistTo); if (result.Exception != null) throw result.Exception; return result.StatusCode.Value; } public virtual int Save(T value, PersistTo persistTo = PersistTo.Zero) { var key = string.IsNullOrEmpty(value.Id) ? BuildKey(value) : value.Id; var result = _Client.ExecuteStore(StoreMode.Set, key, serializeAndIgnoreId(value), persistTo); if (result.Exception != null) throw result.Exception; return result.StatusCode.Value; }
The Get method of
RepositoryBase requires similar considerations.
CouchbaseClient.ExecuteGet returns an
IGetOperationResult. To be consistent with the
goal of not exposing Couchbase SDK plumbing to the app,
Get will return the object or null if not
found, while throwing a swallowed exception. Notice also that the
Id property of the model is set to the value of
the key, since it's not being stored in the JSON.
public virtual T Get(string key) { var result = _Client.ExecuteGet<string>(key); if (result.Exception != null) throw result.Exception; if (result.Value == null) { return null; } var model = JsonConvert.DeserializeObject<T>(result.Value); model.Id = key; //Id is not serialized into the JSON document on store, so need to set it before returning return model; }
Completing the CRUD operations is the Delete
method. Delete will also hide its SDK result
data structure (IRemoveOperationResult) and
return a status code, while throwing swallowed exceptions. Delete
also supports the durability requirement overload.
public virtual int Delete(string key, PersistTo persistTo = PersistTo.Zero) { var result = _Client.ExecuteRemove(key, persistTo); if (result.Exception != null) throw result.Exception; return result.StatusCode.HasValue ? result.StatusCode.Value : 0; }