diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0d45997
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,11 @@
+obj
+bin
+_ReSharper.*
+*.csproj.user
+*.resharper.user
+*.resharper
+*.suo
+*.cache
+*~
+*.swp
+.svn
\ No newline at end of file
diff --git a/Divan.sln b/Divan.sln
new file mode 100644
index 0000000..25cde46
--- /dev/null
+++ b/Divan.sln
@@ -0,0 +1,26 @@
+
+Microsoft Visual Studio Solution File, Format Version 10.00
+# Visual Studio 2008
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Divan", "src\Divan.csproj", "{37AC0B66-5340-4B81-BC62-3EE80233A011}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Trivial", "samples\Trivial\Trivial.csproj", "{CDCC7924-F227-46DC-B2E6-2BBE06B84AF2}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {37AC0B66-5340-4B81-BC62-3EE80233A011}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {37AC0B66-5340-4B81-BC62-3EE80233A011}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {37AC0B66-5340-4B81-BC62-3EE80233A011}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {37AC0B66-5340-4B81-BC62-3EE80233A011}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CDCC7924-F227-46DC-B2E6-2BBE06B84AF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CDCC7924-F227-46DC-B2E6-2BBE06B84AF2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CDCC7924-F227-46DC-B2E6-2BBE06B84AF2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CDCC7924-F227-46DC-B2E6-2BBE06B84AF2}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+EndGlobal
diff --git a/README.rdoc b/README.rdoc
new file mode 100644
index 0000000..d6cd275
--- /dev/null
+++ b/README.rdoc
@@ -0,0 +1,92 @@
+= Divan, a C# library for CouchDB
+
+Divan is a C# library for using CouchDB (http://www.couchdb.org). It should be more or less API complete
+including bulk operations, attachments, views and design documents etc. It is quite fast and designed
+to be flexible but not bloated.
+
+Divan has been developed in-house at Foretagsplatsen AB (www.foretagsplatsen.se) and is being used in
+the new core system at Foretagsplatsen. It has unit tests (although could benefit from more) and at least
+one sample console project included.
+
+== Does it work under Mono?
+
+You bet. Foretagsplatsen uses mainly windows (Visual Studio 2008 and .Net 3.5) but Divan is meant to
+work fine in Mono too.
+
+== What about documentation?
+
+At the moment documentation is... this file! :) But there are unit tests in CouchTest.cs and there
+is at least one sample project showing basic usage. One more sample with more advanced usage is coming soon.
+
+== Dependencies
+
+The only dependencies and their tested versions are:
+
+* Newtonsoft.JSON (3.5 Beta 4), MIT-licensed fast library for JSON reading and writing, see: http://json.codeplex.com
+* NUnit (2.4.8). Unit testing framework, see: http://www.nunit.org
+* CouchDB (0.9.1, 0.9). Running on a server somewhere, see: http://www.couchdb.org
+
+The two neeed dlls are included in the lib directory.
+
+== Getting started
+
+Well... it goes something like this:
+
+1. First get CouchDB up and running on some box.
+
+2. Clone Divan and build it.
+
+3. Run the "Trivial" sample console app by pointing it at a running CouchDB server getting output similar to this:
+
+ C:\Divan\samples\Trivial>bin\Debug\Trivial.exe 192.168.9.205 5984
+ Using 192.168.9.205:5984
+ Created a CouchServer
+ Request: http://192.168.9.205:5984/trivial Method: HEAD
+ Request: http://192.168.9.205:5984/trivial Method: PUT
+ Created database 'trivial'
+ Request: http://192.168.9.205:5984/trivial/ Method: POST
+ Request: http://192.168.9.205:5984/trivial/ Method: POST
+ Request: http://192.168.9.205:5984/trivial/ Method: POST
+ Request: http://192.168.9.205:5984/trivial/ Method: POST
+ Request: http://192.168.9.205:5984/trivial/ Method: POST
+ Request: http://192.168.9.205:5984/trivial/ Method: POST
+ Request: http://192.168.9.205:5984/trivial/ Method: POST
+ Request: http://192.168.9.205:5984/trivial/ Method: POST
+ Request: http://192.168.9.205:5984/trivial/ Method: POST
+ Request: http://192.168.9.205:5984/trivial/ Method: POST
+ Saved 10 Cars with 170 hps each.
+ Request: http://192.168.9.205:5984/trivial/86a9d1ad306e204a037940c4fb0cbbe7 Method: PUT
+ Modified last Car with id 86a9d1ad306e204a037940c4fb0cbbe7
+ Request: http://192.168.9.205:5984/trivial/86a9d1ad306e204a037940c4fb0cbbe7 Method: GET
+ Loaded last Car Saab 93 now with 400hps.
+ Request: http://192.168.9.205:5984/trivial/_all_docs?include_docs=true Method: GET
+ Loaded all Cars: 10
+ Request: http://192.168.9.205:5984/trivial/1f6de464ae8034eb952b93105807f22c?rev1-208231211 Method: DELETE
+ Deleted car with id 1f6de464ae8034eb952b93105807f22c
+ Request: http://192.168.9.205:5984/trivial/44f9638877fc09e07bda6504a5bfd40d?rev1-345884075 Method: DELETE
+ Deleted car with id 44f9638877fc09e07bda6504a5bfd40d
+ Request: http://192.168.9.205:5984/trivial/591e0ef170154311aafa8a2a5fcbb310?rev1-2891194419 Method: DELETE
+ Deleted car with id 591e0ef170154311aafa8a2a5fcbb310
+ Request: http://192.168.9.205:5984/trivial/7124fada0f90ed93f88168dc6c1c8b4f?rev1-2479256166 Method: DELETE
+ Deleted car with id 7124fada0f90ed93f88168dc6c1c8b4f
+ Request: http://192.168.9.205:5984/trivial/73115c4dd3b2bcd512412813dd04b901?rev1-3993011972 Method: DELETE
+ Deleted car with id 73115c4dd3b2bcd512412813dd04b901
+ Request: http://192.168.9.205:5984/trivial/78a305543bff2f474cbcfe2ac667cc6d?rev1-1583012745 Method: DELETE
+ Deleted car with id 78a305543bff2f474cbcfe2ac667cc6d
+ Request: http://192.168.9.205:5984/trivial/86a9d1ad306e204a037940c4fb0cbbe7?rev2-1402499148 Method: DELETE
+ Deleted car with id 86a9d1ad306e204a037940c4fb0cbbe7
+ Request: http://192.168.9.205:5984/trivial/9e647b114b9ba1c8a170ed1c1951260c?rev1-30425501 Method: DELETE
+ Deleted car with id 9e647b114b9ba1c8a170ed1c1951260c
+ Request: http://192.168.9.205:5984/trivial/bad56664dec8f3d1fd30db70510f7ff2?rev1-1765374009 Method: DELETE
+ Deleted car with id bad56664dec8f3d1fd30db70510f7ff2
+ Request: http://192.168.9.205:5984/trivial/cbf0bb7f8ebc4aad02a2c90228d1076f?rev1-2425501185 Method: DELETE
+ Deleted car with id cbf0bb7f8ebc4aad02a2c90228d1076f
+ Request: http://192.168.9.205:5984/trivial Method: HEAD
+ Request: http://192.168.9.205:5984/trivial Method: DELETE
+ Deleted database
+
+
+3. Look at couchTest.cs and make sure the tests are green, you may need to edit CochServer.cs with different default server ip.
+
+4. Have fun!
+
diff --git a/lib/Newtonsoft.Json.dll b/lib/Newtonsoft.Json.dll
new file mode 100644
index 0000000..782e9c0
Binary files /dev/null and b/lib/Newtonsoft.Json.dll differ
diff --git a/lib/nunit.framework.dll b/lib/nunit.framework.dll
new file mode 100644
index 0000000..2a0a0aa
Binary files /dev/null and b/lib/nunit.framework.dll differ
diff --git a/samples/Trivial/Program.cs b/samples/Trivial/Program.cs
new file mode 100644
index 0000000..7f175f0
--- /dev/null
+++ b/samples/Trivial/Program.cs
@@ -0,0 +1,136 @@
+using System;
+using System.Diagnostics;
+using Divan;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Trivial
+{
+ ///
+ /// A trivial example of using Divan. Requires a running CouchDB on localhost!
+ ///
+ /// Run using:
+ ///
+ /// Trivial.exe
+ ///
+ ///
+ class Program
+ {
+ static void Main(string[] args) {
+ string host = "localhost";
+ int port = 5984;
+
+ // Lets you see all HTTP requests made by Divan
+ Trace.Listeners.Add(new ConsoleTraceListener());
+
+ // Trivial parse of args to get host and port
+ switch (args.Length) {
+ case 0:
+ Console.WriteLine("Using localhost:5984");
+ break;
+ case 1:
+ Console.WriteLine("Using " + args[0] + ":5984");
+ host = args[0];
+ break;
+ case 2:
+ Console.WriteLine("Using " + args[0] + ":" + args[1]);
+ host = args[0];
+ port = int.Parse(args[1]);
+ break;
+ }
+
+ // Get a server for default couch port 5984 on localhost
+ var server = new CouchServer(host, port);
+ Console.WriteLine("Created a CouchServer");
+
+ // Get (creates it if needed) a CouchDB database.
+ var db = server.GetDatabase("trivial");
+ Console.WriteLine("Created database 'trivial'");
+
+ // Create and save 10 Cars with automatically allocated Ids by Couch
+ Car car = null;
+ for (int i = 0; i < 10; i++)
+ {
+ car = new Car("Saab", "93", 170);
+ db.SaveDocument(car);
+ }
+ Console.WriteLine("Saved 10 Cars with 170 hps each.");
+
+ // Modify the last Car we saved...
+ car.HorsePowers = 400;
+
+ // ...and save the change.
+ // We could also have used WriteDocument if we knew it was an existing doc
+ db.SaveDocument(car);
+ Console.WriteLine("Modified last Car with id " + car.Id);
+
+ // Load a Car by known id, class to instantiate using generics
+ var sameCar = db.GetDocument(car.Id);
+ Console.WriteLine("Loaded last Car " + sameCar.Make + " " + sameCar.Model + " now with " + sameCar.HorsePowers + "hps.");
+
+ // Load all Cars, class to instantiate using generics
+ var cars = db.QueryAllDocuments().IncludeDocuments().GetResult().Documents();
+ Console.WriteLine("Loaded all Cars: " + cars.Count);
+
+ // Delete all Cars one by one
+ foreach (var eachCar in cars)
+ {
+ db.DeleteDocument(eachCar);
+ Console.WriteLine("Deleted car with id " + eachCar.Id);
+ }
+
+ // Delete the db itself
+ db.Delete();
+ Console.WriteLine("Deleted database");
+ }
+
+ ///
+ /// The simplest way to deal with domain objects is to subclass CouchDocument
+ /// and inherit members Id and Rev. You will need to implement WriteJson/ReadJson.
+ ///
+ private class Car : CouchDocument
+ {
+ public string Make;
+ public string Model;
+ public int HorsePowers;
+
+ public Car()
+ {
+ // This constructor is needed by Divan
+ }
+
+ public Car(string make, string model, int hps)
+ {
+ Make = make;
+ Model = model;
+ HorsePowers = hps;
+ }
+ #region CouchDocument Members
+
+ public override void WriteJson(JsonWriter writer)
+ {
+ // This will write id and rev
+ base.WriteJson(writer);
+
+ writer.WritePropertyName("Make");
+ writer.WriteValue(Make);
+ writer.WritePropertyName("Model");
+ writer.WriteValue(Model);
+ writer.WritePropertyName("Hps");
+ writer.WriteValue(HorsePowers);
+ }
+
+ public override void ReadJson(JObject obj)
+ {
+ // This will read id and rev
+ base.ReadJson(obj);
+
+ Make = obj["Make"].Value();
+ Model = obj["Model"].Value();
+ HorsePowers = obj["Hps"].Value();
+ }
+
+ #endregion
+ }
+ }
+}
diff --git a/samples/Trivial/Properties/AssemblyInfo.cs b/samples/Trivial/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..66856b0
--- /dev/null
+++ b/samples/Trivial/Properties/AssemblyInfo.cs
@@ -0,0 +1,36 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("Trivial")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("MSC Konsult AB")]
+[assembly: AssemblyProduct("Trivial")]
+[assembly: AssemblyCopyright("Copyright © MSC Konsult AB 2009")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("05c213a1-cf72-45ce-8607-7827b32a1699")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/samples/Trivial/Trivial.csproj b/samples/Trivial/Trivial.csproj
new file mode 100644
index 0000000..54b823d
--- /dev/null
+++ b/samples/Trivial/Trivial.csproj
@@ -0,0 +1,69 @@
+
+
+
+ Debug
+ AnyCPU
+ 9.0.30729
+ 2.0
+ {CDCC7924-F227-46DC-B2E6-2BBE06B84AF2}
+ Exe
+ Properties
+ Trivial
+ Trivial
+ v3.5
+ 512
+
+
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+ False
+ ..\..\lib\Newtonsoft.Json.dll
+
+
+
+ 3.5
+
+
+ 3.5
+
+
+ 3.5
+
+
+
+
+
+
+
+
+
+
+ {37AC0B66-5340-4B81-BC62-3EE80233A011}
+ Divan
+
+
+
+
+
\ No newline at end of file
diff --git a/src/CouchBulkDeleteDocuments.cs b/src/CouchBulkDeleteDocuments.cs
new file mode 100644
index 0000000..a6d6ae3
--- /dev/null
+++ b/src/CouchBulkDeleteDocuments.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Divan
+{
+ ///
+ /// Only used as pseudo doc when doing bulk updates/inserts.
+ ///
+ public class CouchBulkDeleteDocuments : CouchBulkDocuments
+ {
+ public CouchBulkDeleteDocuments(IList docs) : base(docs)
+ {
+ }
+
+ public override void WriteJson(JsonWriter writer)
+ {
+ writer.WritePropertyName("docs");
+ writer.WriteStartArray();
+ foreach (ICouchDocument doc in Docs)
+ {
+ writer.WriteStartObject();
+ CouchDocument.WriteIdAndRev(doc, writer);
+ writer.WritePropertyName("_deleted");
+ writer.WriteValue(true);
+ writer.WriteEndObject();
+ }
+ writer.WriteEndArray();
+ }
+
+ public override void ReadJson(JObject obj)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CouchBulkDocuments.cs b/src/CouchBulkDocuments.cs
new file mode 100644
index 0000000..dc95009
--- /dev/null
+++ b/src/CouchBulkDocuments.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Generic;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Divan
+{
+ ///
+ /// Only used as psuedo doc when doing bulk updates/inserts.
+ ///
+ public class CouchBulkDocuments : ICanJson
+ {
+ public CouchBulkDocuments(IList docs)
+ {
+ Docs = docs;
+ }
+
+ public IList Docs { get; private set; }
+
+ #region ICouchBulk Members
+
+ public int Count()
+ {
+ return Docs.Count;
+ }
+
+ public virtual void WriteJson(JsonWriter writer)
+ {
+ writer.WritePropertyName("docs");
+ writer.WriteStartArray();
+ foreach (ICouchDocument doc in Docs)
+ {
+ writer.WriteStartObject();
+ doc.WriteJson(writer);
+ writer.WriteEndObject();
+ }
+ writer.WriteEndArray();
+ }
+
+ public virtual void ReadJson(JObject obj)
+ {
+ throw new NotImplementedException();
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/src/CouchBulkKeys.cs b/src/CouchBulkKeys.cs
new file mode 100644
index 0000000..2ac3787
--- /dev/null
+++ b/src/CouchBulkKeys.cs
@@ -0,0 +1,55 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Divan
+{
+ ///
+ /// Only used as psuedo doc when doing bulk reads.
+ ///
+ public class CouchBulkKeys : ICanJson
+ {
+ public CouchBulkKeys(IEnumerable ids)
+ {
+ Ids = ids.ToArray();
+ }
+
+ public CouchBulkKeys()
+ {
+ }
+
+ public CouchBulkKeys(string[] ids)
+ {
+ Ids = ids;
+ }
+
+ public string[] Ids { get; set; }
+
+ #region ICouchBulk Members
+
+ public virtual void WriteJson(JsonWriter writer)
+ {
+ writer.WritePropertyName("keys");
+ writer.WriteStartArray();
+ foreach (string id in Ids)
+ {
+ writer.WriteValue(id);
+ }
+ writer.WriteEndArray();
+ }
+
+ public virtual void ReadJson(JObject obj)
+ {
+ throw new NotImplementedException();
+ }
+
+ public int Count()
+ {
+ return Ids.Count();
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/src/CouchConflictException.cs b/src/CouchConflictException.cs
new file mode 100644
index 0000000..0573066
--- /dev/null
+++ b/src/CouchConflictException.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace Divan
+{
+ ///
+ /// Represents a CouchDB HTTP 409 conflict.
+ ///
+ public class CouchConflictException : Exception
+ {
+ public CouchConflictException(string msg, Exception e) : base(msg, e)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CouchDatabase.cs b/src/CouchDatabase.cs
new file mode 100644
index 0000000..672a67b
--- /dev/null
+++ b/src/CouchDatabase.cs
@@ -0,0 +1,615 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
+using System.Linq;
+using System.Net;
+using Newtonsoft.Json.Linq;
+
+namespace Divan
+{
+ ///
+ /// A CouchDatabase corresponds to a named CouchDB database in a specific CouchServer.
+ /// This is the main API to work with CouchDB. One useful approach is to create your own subclasses
+ /// for your different databases.
+ ///
+ public class CouchDatabase
+ {
+ public CouchDatabase() : this("default", new CouchServer())
+ {
+ }
+
+ public CouchDatabase(string name) : this(name, new CouchServer())
+ {
+ }
+
+ public CouchDatabase(CouchServer server) : this("default", server)
+ {
+ }
+
+ public CouchDatabase(string name, CouchServer server)
+ {
+ Name = server.DatabasePrefix + name;
+ Server = server;
+ }
+
+ public CouchServer Server { get; set; }
+ public string Name { get; set; }
+
+ public CouchRequest Request()
+ {
+ return new CouchRequest(this);
+ }
+
+ public CouchRequest Request(string path)
+ {
+ return (new CouchRequest(this)).Path(path);
+ }
+
+ public int CountDocuments()
+ {
+ return (Request().Parse())["doc_count"].Value();
+ }
+
+ public CouchRequest RequestAllDocuments()
+ {
+ return Request("_all_docs");
+ }
+
+ ///
+ /// Read all documents in database.
+ ///
+ /// List of documents in database.
+ public IList GetAllDocuments()
+ {
+ var list = new List();
+ JObject json = Request("_all_docs").Parse();
+ foreach (JObject row in json["rows"])
+ {
+ list.Add(new CouchDocument(row["id"].ToString(), (row["value"])["rev"].ToString()));
+ }
+ return list;
+ }
+
+ ///
+ /// Initialize CouchDB database by loading design documents into it.
+ /// Override in subclasses.
+ ///
+ public virtual void Initialize()
+ {
+ // Nothing by default
+ }
+
+ public bool Exists()
+ {
+ return Server.HasDatabase(Name);
+ }
+
+ ///
+ /// Check first if database exists, and if it does not - create it and initialize it.
+ ///
+ public void Create()
+ {
+ if (!Exists())
+ {
+ Server.CreateDatabase(Name);
+ Initialize();
+ }
+ }
+
+ public void Delete()
+ {
+ if (Exists())
+ {
+ Server.DeleteDatabase(Name);
+ }
+ }
+
+ ///
+ /// Write a CouchDocument, it may already exist in db and will then be overwritten.
+ ///
+ /// Document as Json
+ /// Document identifier
+ /// A new CouchDocument
+ public ICouchDocument WriteDocument(string json, string documentId)
+ {
+ return WriteDocument(new CouchDocument(json, documentId));
+ }
+
+ ///
+ /// Write a CouchDocument or ICouchDocument, it may already exist in db and will then be overwritten.
+ ///
+ /// Couch document
+ /// Couch Document with new Rev set.
+ /// This relies on the document to already have an id.
+ public ICouchDocument
+ WriteDocument(ICouchDocument document)
+ {
+ return WriteDocument(document, false);
+ }
+
+ ///
+ /// This is a convenience method that creates or writes a ICouchDocument depending on if
+ /// it has an id or not. If it does not have an id we create the document and let CouchDB allocate
+ /// an id. If it has an id we use WriteDocument which will overwrite the document in CouchDB.
+ ///
+ /// Couch document
+ /// Couch Document with new Rev set and possibly an Id set.
+ public ICouchDocument SaveDocument(ICouchDocument document)
+ {
+ if (document.Id == null)
+ {
+ return CreateDocument(document);
+ }
+ return WriteDocument(document);
+ }
+
+ ///
+ /// Write a CouchDocument or ICouchDocument, it may already exist in db and will then be overwritten.
+ ///
+ /// Couch document
+ /// True if we don't want to wait for flush (commit).
+ /// Couch Document with new Rev set.
+ /// This relies on the document to already have an id.
+ public ICouchDocument WriteDocument(ICouchDocument document, bool batch)
+ {
+ if (document.Id == null)
+ {
+ throw CouchException.Create(
+ "Failed to write document using PUT because it lacks an id, use CreateDocument instead to let CouchDB generate an id");
+ }
+ JObject result =
+ Request(document.Id).Query(batch ? "?batch=ok" : null).Data(CouchDocument.WriteJson(document)).Put().Check("Failed to write document").Result();
+ document.Id = result["id"].Value(); // Not really neeed
+ document.Rev = result["rev"].Value();
+
+ return document;
+ }
+
+ ///
+ /// Add an attachment to an existing ICouchDocument, it may already exist in db and will then be overwritten.
+ ///
+ /// Couch document
+ /// Binary data as string
+ /// The MIME type for the attachment.
+ /// The document.
+ /// This relies on the document to already have an id.
+ public ICouchDocument WriteAttachment(ICouchDocument document, string attachment, string mimeType)
+ {
+ if (document.Id == null)
+ {
+ throw CouchException.Create(
+ "Failed to add attachment to document using PUT because it lacks an id");
+ }
+
+ JObject result =
+ Request(document.Id + "/attachment").Query("?rev=" + document.Rev).Data(attachment).MimeType(mimeType).Put().Check("Failed to write attachment")
+ .Result();
+ document.Id = result["id"].Value(); // Not really neeed
+ document.Rev = result["rev"].Value();
+
+ return document;
+ }
+
+ ///
+ /// Read a ICouchDocument with an id even if it has not changed revision.
+ ///
+ /// Document to fill.
+ public void ReadDocument(ICouchDocument document)
+ {
+ document.ReadJson(ReadDocument(document.Id));
+ }
+
+ ///
+ /// Read the attachment for an ICouchDocument.
+ ///
+ /// Document to read.
+ public string ReadAttachment(ICouchDocument document)
+ {
+ return ReadAttachment(document.Id);
+ }
+
+ ///
+ /// First use HEAD to see if it has indeed changed.
+ ///
+ /// Document to fill.
+ public void FetchDocumentIfChanged(ICouchDocument document)
+ {
+ if (HasDocumentChanged(document))
+ {
+ ReadDocument(document);
+ }
+ }
+
+ ///
+ /// Read a CouchDocument or ICouchDocument, this relies on the document to obviously have an id.
+ /// We also check the revision so that we can avoid parsing JSON if the document is unchanged.
+ ///
+ /// Document to fill.
+ public void ReadDocumentIfChanged(ICouchDocument document)
+ {
+ JObject result = Request(document.Id).Etag(document.Rev).Parse();
+ if (result == null)
+ {
+ return;
+ }
+ document.ReadJson(result);
+ }
+
+ ///
+ /// Read a couch document given an id, this method does not have enough information to do caching.
+ ///
+ /// Document identifier
+ /// Document Json as JObject
+ public JObject ReadDocument(string documentId)
+ {
+ try
+ {
+ return Request(documentId).Parse();
+ }
+ catch (WebException e)
+ {
+ throw CouchException.Create("Failed to read document", e);
+ }
+ }
+
+ ///
+ /// Read a couch document given an id, this method does not have enough information to do caching.
+ ///
+ /// Document identifier
+ /// Document Json as string
+ public string ReadDocumentString(string documentId)
+ {
+ try
+ {
+ return Request(documentId).String();
+ }
+ catch (WebException e)
+ {
+ throw CouchException.Create("Failed to read document: " + e.Message, e);
+ }
+ }
+
+ ///
+ /// Read a couch attachment given a document id, this method does not have enough information to do caching.
+ ///
+ /// Document identifier
+ /// Document attachment
+ public string ReadAttachment(string documentId)
+ {
+ try
+ {
+ return Request(documentId + "/attachment").String();
+ }
+ catch (WebException e)
+ {
+ throw CouchException.Create("Failed to read document: " + e.Message, e);
+ }
+ }
+
+ ///
+ /// Create a CouchDocument given JSON as a string. Uses POST and CouchDB will allocate a new id.
+ ///
+ /// Json data to store.
+ /// Couch document with data, id and rev set.
+ /// POST which may be problematic in some environments.
+ public CouchJsonDocument CreateDocument(string json)
+ {
+ return (CouchJsonDocument) CreateDocument(new CouchJsonDocument(json));
+ }
+
+ ///
+ /// Create a given ICouchDocument in CouchDB. Uses POST and CouchDB will allocate a new id and overwrite any existing id.
+ ///
+ /// Document to store.
+ /// Document with Id and Rev set.
+ /// POST which may be problematic in some environments.
+ public ICouchDocument CreateDocument(ICouchDocument document)
+ {
+ try
+ {
+ JObject result = Request().Data(CouchDocument.WriteJson(document)).Post().Check("Failed to create document").Result();
+ document.Id = result["id"].Value();
+ document.Rev = result["rev"].Value();
+ return document;
+ }
+ catch (WebException e)
+ {
+ throw CouchException.Create("Failed to create document", e);
+ }
+ }
+
+ ///
+ /// Create or update a list of ICouchDocuments in CouchDB. Uses POST and CouchDB will
+ /// allocate new ids if the documents lack them.
+ ///
+ /// List of documents to store.
+ /// POST may be problematic in some environments.
+ public void SaveDocuments(IList documents, bool allOrNothing)
+ {
+ var bulk = new CouchBulkDocuments(documents);
+ try
+ {
+ var result =
+ Request("_bulk_docs").Data(CouchDocument.WriteJson(bulk)).Query("?all_or_nothing=" + allOrNothing.ToString().ToLower()).PostJson().Parse
+ ();
+ for (int i = 0; i < documents.Count; i++)
+ {
+ documents[i].Id = (result[i])["id"].Value();
+ documents[i].Rev = (result[i])["rev"].Value();
+ }
+ }
+ catch (WebException e)
+ {
+ throw CouchException.Create("Failed to create bulk documents", e);
+ }
+ }
+
+ ///
+ /// Create or updates documents in bulk fashion, chunk wise. Optionally access given view
+ /// after each chunk to trigger reindexing.
+ ///
+ /// List of documents to store.
+ /// Number of documents to store per "POST"
+ /// List of views to touch per chunk.
+ public void SaveDocuments(IList documents, int chunkCount, List views, bool allOrNothing)
+ {
+ var chunk = new List(chunkCount);
+ int counter = 0;
+
+ foreach (ICouchDocument doc in documents)
+ {
+ // Do we have a chunk ready to create?
+ if (counter == chunkCount)
+ {
+ counter = 0;
+ SaveDocuments(chunk, allOrNothing);
+ TouchViews(views);
+ /* Skipping separate thread for now, ASP.Net goes bonkers...
+ (new Thread(
+ () => GetView(designDocumentName, viewName, ""))
+ {
+ Name = "View access in background", Priority = ThreadPriority.BelowNormal
+ }).Start(); */
+
+ chunk = new List(chunkCount);
+ }
+ counter++;
+ chunk.Add(doc);
+ }
+
+ SaveDocuments(chunk, allOrNothing);
+ TouchViews(views);
+ }
+
+ public void TouchViews(List views)
+ {
+ var timer = new Stopwatch();
+ if (views != null)
+ {
+ foreach (CouchViewDefinition view in views)
+ {
+ if (view != null)
+ {
+ timer.Reset();
+ timer.Start();
+ view.Touch();
+ timer.Stop();
+ Trace.WriteLine("Update view " + view.Path() + ":" + timer.ElapsedMilliseconds + " ms");
+ }
+ }
+ }
+ }
+
+ ///
+ /// Create documents in bulk fashion, chunk wise.
+ ///
+ /// List of documents to store.
+ /// Number of documents to store per "POST"
+ public void SaveDocuments(IList documents, int chunkCount, bool allOrNothing)
+ {
+ SaveDocuments(documents, chunkCount, null, allOrNothing);
+ }
+
+ ///
+ /// Get multiple documents.
+ ///
+ /// List of documents to get.
+ public IList GetDocuments(IList documentIds) where T : ICouchDocument, new()
+ {
+ return GetDocuments(documentIds.ToArray());
+ }
+
+ public IList GetDocuments(IList documentIds)
+ {
+ return GetDocuments(documentIds);
+ }
+
+ public IList GetDocuments(string[] documentIds)
+ {
+ return GetDocuments(documentIds);
+ }
+
+ public IList GetDocuments(string[] documentIds) where T : ICouchDocument, new()
+ {
+ var bulk = new CouchBulkKeys(documentIds);
+ return QueryAllDocuments().Data(CouchDocument.WriteJson(bulk)).IncludeDocuments().GetResult().Documents();
+ }
+
+ public T GetDocument(string documentId) where T : ICouchDocument, new()
+ {
+ var doc = new T {Id = documentId};
+ try
+ {
+ ReadDocument(doc);
+ }
+ catch (CouchNotFoundException)
+ {
+ return default(T);
+ }
+ return doc;
+ }
+
+ public CouchJsonDocument GetDocument(string documentId)
+ {
+ try
+ {
+ try
+ {
+ return new CouchJsonDocument(Request(documentId).Parse());
+ }
+ catch (WebException e)
+ {
+ throw CouchException.Create("Failed to get document", e);
+ }
+ }
+ catch (CouchNotFoundException)
+ {
+ return null;
+ }
+ }
+
+ public CouchQuery Query(string designName, string viewName)
+ {
+ return Query(new CouchViewDefinition(viewName, new DesignCouchDocument(designName, this)));
+ }
+
+ public CouchQuery Query(CouchViewDefinition view)
+ {
+ return new CouchQuery(view);
+ }
+
+ public CouchQuery QueryAllDocuments()
+ {
+ return Query(null, "_all_docs");
+ }
+
+ public void TouchView(string designDocumentId, string viewName)
+ {
+ Query(designDocumentId, viewName).Limit(0).GetResult();
+ }
+
+ public void DeleteDocument(ICouchDocument document)
+ {
+ DeleteDocument(document.Id, document.Rev);
+ }
+
+ public ICouchDocument DeleteAttachment(ICouchDocument document)
+ {
+ JObject result = Request(document.Id + "/attachment").Query("?rev=" + document.Rev).Delete().Check("Failed to delete attachment").Result();
+ document.Id = result["id"].Value(); // Not really neeed
+ document.Rev = result["rev"].Value();
+ return document;
+ }
+
+ public void DeleteAttachment(string id, string rev)
+ {
+ Request(id + "/attachment").Query("?rev=" + rev).Delete().Check("Failed to delete attachment");
+ }
+
+ public void DeleteDocument(string id, string rev)
+ {
+ Request(id).Query("?rev=" + rev).Delete().Check("Failed to delete document");
+ }
+
+ ///
+ /// Delete documents in bulk fashion.
+ ///
+ /// List of documents to delete.
+ public void DeleteDocuments(IList documents)
+ {
+ DeleteDocuments(documents.ToArray());
+ }
+
+ ///
+ /// Delete documents in key range. This method needs to retrieve
+ /// revisions and then use them to post a bulk delete. Couch can not
+ /// delete documents without being told about their revisions.
+ ///
+ public void DeleteDocuments(string startKey, string endKey)
+ {
+ IList docs = QueryAllDocuments().StartKey(startKey).EndKey(endKey).GetResult().RowDocuments();
+ DeleteDocuments(docs.ToArray());
+ }
+
+ ///
+ /// Delete documents in bulk fashion.
+ ///
+ /// Array of documents to delete.
+ public void DeleteDocuments(ICouchDocument[] documents)
+ {
+ DeleteDocuments(new CouchBulkDeleteDocuments(documents));
+ }
+
+ ///
+ /// Delete documents in bulk fashion.
+ ///
+ public void DeleteDocuments(ICanJson bulk)
+ {
+ try
+ {
+ var result = Request("_bulk_docs").Data(CouchDocument.WriteJson(bulk)).PostJson().Parse();
+ for (int i = 0; i < result.Count(); i++)
+ {
+ //documents[i].id = (result[i])["id"].Value();
+ //documents[i].rev = (result[i])["rev"].Value();
+ if ((result[i])["error"] != null)
+ {
+ throw CouchException.Create(string.Format(CultureInfo.InvariantCulture,
+ "Document with id {0} was not deleted: {1}: {2}",
+ (result[i])["id"].Value(), (result[i])["error"], (result[i])["reason"]));
+ }
+ }
+ }
+ catch (WebException e)
+ {
+ throw CouchException.Create("Failed to bulk delete documents", e);
+ }
+ }
+
+ public bool HasDocument(ICouchDocument document)
+ {
+ return HasDocument(document.Id);
+ }
+
+ public bool HasAttachment(ICouchDocument document)
+ {
+ return HasAttachment(document.Id);
+ }
+
+ public bool HasDocumentChanged(ICouchDocument document)
+ {
+ return HasDocumentChanged(document.Id, document.Rev);
+ }
+
+ public bool HasDocumentChanged(string documentId, string rev)
+ {
+ return Request(documentId).Head().Send().Etag() != rev;
+ }
+
+ public bool HasDocument(string documentId)
+ {
+ try
+ {
+ Request(documentId).Head().Send();
+ return true;
+ }
+ catch (WebException)
+ {
+ return false;
+ }
+ }
+
+ public bool HasAttachment(string documentId)
+ {
+ try
+ {
+ Request(documentId + "/attachment").Head().Send();
+ return true;
+ }
+ catch (WebException)
+ {
+ return false;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CouchDocument.cs b/src/CouchDocument.cs
new file mode 100644
index 0000000..279607d
--- /dev/null
+++ b/src/CouchDocument.cs
@@ -0,0 +1,114 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Text;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Divan
+{
+ ///
+ /// This is a base class that domain objects can inherit in order to get
+ /// Id and Rev instance variables. You can also implement ICouchDocument yourself if
+ /// you are not free to pick this class as your base. Some static methods to read and write
+ /// CouchDB documents are also kept here.
+ ///
+ /// See sample subclasses to understand how to use this class
+ ///
+ public class CouchDocument : ICouchDocument
+ {
+ public CouchDocument(string id, string rev)
+ {
+ Id = id;
+ Rev = rev;
+ }
+
+ public CouchDocument(string id)
+ {
+ Id = id;
+ }
+
+ public CouchDocument()
+ {
+ }
+
+ public CouchDocument(IDictionary doc)
+ : this(doc["_id"].Value(), doc["_rev"].Value())
+ {
+ }
+
+ #region ICouchDocument Members
+
+ public string Id { get; set; }
+ public string Rev { get; set; }
+
+ public virtual void WriteJson(JsonWriter writer)
+ {
+ WriteIdAndRev(this, writer);
+ }
+
+ public virtual void ReadJson(JObject obj)
+ {
+ ReadIdAndRev(this, obj);
+ }
+
+ #endregion
+
+ public void WriteJsonObject(JsonWriter writer)
+ {
+ writer.WriteStartObject();
+ WriteJson(writer);
+ writer.WriteEndObject();
+ }
+
+ public static string WriteJson(ICanJson doc)
+ {
+ var sb = new StringBuilder();
+ using (JsonWriter jsonWriter = new JsonTextWriter(new StringWriter(sb, CultureInfo.InvariantCulture)))
+ {
+ //jsonWriter.Formatting = Formatting.Indented;
+ jsonWriter.WriteStartObject();
+ doc.WriteJson(jsonWriter);
+ jsonWriter.WriteEndObject();
+ string result = sb.ToString();
+ return result;
+ }
+ }
+
+ public static void WriteIdAndRev(ICouchDocument doc, JsonWriter writer)
+ {
+ if (doc.Id != null)
+ {
+ writer.WritePropertyName("_id");
+ writer.WriteValue(doc.Id);
+ }
+ if (doc.Rev != null)
+ {
+ writer.WritePropertyName("_rev");
+ writer.WriteValue(doc.Rev);
+ }
+ }
+
+ public static void ReadIdAndRev(ICouchDocument doc, JObject obj)
+ {
+ doc.Id = obj["_id"].Value();
+ doc.Rev = obj["_rev"].Value();
+ }
+
+ public static void ReadIdAndRev(ICouchDocument doc, JsonReader reader)
+ {
+ reader.Read();
+ if (reader.TokenType == JsonToken.PropertyName && (reader.Value as string == "_id"))
+ {
+ reader.Read();
+ doc.Id = reader.Value as string;
+ }
+ reader.Read();
+ if (reader.TokenType == JsonToken.PropertyName && (reader.Value as string == "_rev"))
+ {
+ reader.Read();
+ doc.Rev = reader.Value as string;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CouchException.cs b/src/CouchException.cs
new file mode 100644
index 0000000..55c057c
--- /dev/null
+++ b/src/CouchException.cs
@@ -0,0 +1,62 @@
+using System;
+using System.Globalization;
+using System.Net;
+using System.Runtime.Serialization;
+
+namespace Divan
+{
+ ///
+ /// All Exceptions thrown inside Divan uses this class, MOST of these wrap a WebException
+ /// and we extract the HttpStatusCode to make it easily accessible.
+ ///
+ [Serializable]
+ public class CouchException : Exception
+ {
+ public HttpStatusCode StatusCode;
+
+ public CouchException()
+ {
+ }
+
+ public CouchException(string message)
+ : base(message)
+ {
+ }
+
+ public CouchException(string message, Exception innerException) : base(message, innerException)
+ {
+ }
+
+ protected CouchException(SerializationInfo info, StreamingContext context) : base(info, context)
+ {
+ }
+
+ public static Exception Create(string message)
+ {
+ return new CouchException(message);
+ }
+
+ public static Exception Create(string message, WebException e)
+ {
+ string msg = string.Format(CultureInfo.InvariantCulture, message + ": {0}", e.Message);
+ if (e.Response != null)
+ {
+ // Pick out status code
+ HttpStatusCode code = ((HttpWebResponse) e.Response).StatusCode;
+
+ // Create any specific exceptions we care to use
+ if (code == HttpStatusCode.Conflict)
+ {
+ return new CouchConflictException(msg, e);
+ }
+ if (code == HttpStatusCode.NotFound)
+ {
+ return new CouchNotFoundException(msg, e);
+ }
+ }
+
+ // Fall back on generic CouchException
+ return new CouchException(msg, e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CouchGenericViewResult.cs b/src/CouchGenericViewResult.cs
new file mode 100644
index 0000000..8af8c18
--- /dev/null
+++ b/src/CouchGenericViewResult.cs
@@ -0,0 +1,94 @@
+using System.Collections.Generic;
+using Newtonsoft.Json.Linq;
+
+namespace Divan
+{
+ ///
+ /// This is a view result from a CouchQuery that can return CouchDocuments for
+ /// resulting documents (include_docs) and/or ICanJson documents for the
+ /// result values. A value returned from a CouchDB view does not need to be
+ /// a CouchDocument.
+ ///
+ public class CouchGenericViewResult : CouchViewResult
+ {
+ ///
+ /// Return all found values as documents of given type
+ ///
+ /// Type of value.
+ /// All found values.
+ public IList ValueDocuments() where T : ICanJson, new()
+ {
+ return RetrieveDocuments("value");
+ }
+
+ ///
+ /// Return first value found as document of given type.
+ ///
+ /// Type of value
+ /// First value found or null if not found.
+ public T ValueDocument() where T : ICanJson, new()
+ {
+ return RetrieveDocument("value");
+ }
+
+ ///
+ /// Return all found docs as documents of given type
+ ///
+ /// Type of documents.
+ /// List of documents found.
+ public IList Documents() where T : ICouchDocument, new()
+ {
+ return RetrieveDocuments("doc");
+ }
+
+ ///
+ /// Return first document found as document of given type
+ ///
+ /// Type of document
+ /// First document found or null if not found.
+ public T Document() where T : ICouchDocument, new()
+ {
+ return RetrieveDocument("doc");
+ }
+
+ protected virtual IList RetrieveDocuments(string docOrValue) where T : ICanJson, new()
+ {
+ var list = new List();
+ foreach (JToken row in Rows())
+ {
+ var doc = new T();
+ doc.ReadJson(row[docOrValue].Value());
+ list.Add(doc);
+ }
+ return list;
+ }
+
+ protected virtual T RetrieveDocument(string docOrValue) where T : ICanJson, new()
+ {
+ foreach (JToken row in Rows())
+ {
+ var doc = new T();
+ doc.ReadJson(row[docOrValue].Value());
+ return doc;
+ }
+ return default(T);
+ }
+
+ public IList RowDocuments()
+ {
+ return RowDocuments();
+ }
+
+ public IList RowDocuments() where T : ICanJson, new()
+ {
+ var list = new List();
+ foreach (JObject row in Rows())
+ {
+ var doc = new T();
+ doc.ReadJson(row);
+ list.Add(doc);
+ }
+ return list;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CouchJSONDocument.cs b/src/CouchJSONDocument.cs
new file mode 100644
index 0000000..dae5662
--- /dev/null
+++ b/src/CouchJSONDocument.cs
@@ -0,0 +1,86 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Divan
+{
+ ///
+ /// A CouchDocument that holds its contents as a parsed JObject DOM which can be used
+ /// as a "light weight" base document instead of CouchDocument.
+ /// The _id and _rev are held inside the JObject.
+ ///
+ public class CouchJsonDocument : ICouchDocument
+ {
+ public CouchJsonDocument(string json, string id, string rev)
+ {
+ Obj = JObject.Parse(json);
+ Id = id;
+ Rev = rev;
+ }
+
+ public CouchJsonDocument(string json, string id)
+ {
+ Obj = JObject.Parse(json);
+ Id = id;
+ }
+
+ public CouchJsonDocument(string json)
+ {
+ Obj = JObject.Parse(json);
+ }
+
+ public CouchJsonDocument(JObject doc)
+ {
+ Obj = doc;
+ }
+
+ public CouchJsonDocument()
+ {
+ Obj = new JObject();
+ }
+
+ public JObject Obj { get; set; }
+
+ #region ICouchDocument Members
+
+ public void WriteJson(JsonWriter writer)
+ {
+ foreach (JToken token in Obj.Children())
+ {
+ token.WriteTo(writer);
+ }
+ }
+
+ // Presume that Obj has _id and _rev
+ public void ReadJson(JObject obj)
+ {
+ Obj = obj;
+ }
+
+ public string Rev
+ {
+ get
+ {
+ if (Obj["_rev"] == null)
+ {
+ return null;
+ }
+ return Obj["_rev"].Value();
+ }
+ set { Obj["_rev"] = JToken.FromObject(value); }
+ }
+ public string Id
+ {
+ get
+ {
+ if (Obj["_id"] == null)
+ {
+ return null;
+ }
+ return Obj["_id"].Value();
+ }
+ set { Obj["_id"] = JToken.FromObject(value); }
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/src/CouchNotFoundException.cs b/src/CouchNotFoundException.cs
new file mode 100644
index 0000000..63c9903
--- /dev/null
+++ b/src/CouchNotFoundException.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace Divan
+{
+ ///
+ /// Represents a HttpStatusCode of 404, document not found.
+ ///
+ public class CouchNotFoundException : Exception
+ {
+ public CouchNotFoundException(string msg, Exception e) : base(msg, e)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CouchPermanentViewResult.cs b/src/CouchPermanentViewResult.cs
new file mode 100644
index 0000000..66731b7
--- /dev/null
+++ b/src/CouchPermanentViewResult.cs
@@ -0,0 +1,9 @@
+namespace Divan
+{
+ ///
+ /// This is a view result from a CouchQuery on a permanent CouchDB view.
+ ///
+ public class CouchPermanentViewResult : CouchViewResult
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/CouchQuery.cs b/src/CouchQuery.cs
new file mode 100644
index 0000000..d7f9600
--- /dev/null
+++ b/src/CouchQuery.cs
@@ -0,0 +1,212 @@
+using System.Collections.Generic;
+using Newtonsoft.Json.Linq;
+
+namespace Divan
+{
+ ///
+ /// A view query with all its options. A CouchQuery is constructed to hold all query options that
+ /// CouchDB views support and to support ETag caching.
+ /// A CouchQuery object can be executed multiple times, holds the last result, the ETag for it,
+ /// and a reference to the CouchDatabase object used to perform the query.
+ ///
+ public class CouchQuery
+ {
+ public readonly CouchViewDefinition View;
+
+ // Special options
+ public bool checkETagUsingHead;
+ public Dictionary Options = new Dictionary();
+ public string postData;
+ public CouchViewResult Result;
+
+ public CouchQuery(CouchViewDefinition view)
+ {
+ View = view;
+ }
+
+ public void ClearOptions()
+ {
+ Options = new Dictionary();
+ }
+
+ public CouchQuery Data(string data)
+ {
+ postData = data;
+ return this;
+ }
+
+ public CouchQuery Key(string value)
+ {
+ Options["key"] = "\"" + value + "\"";
+ return this;
+ }
+
+ public CouchQuery Key(params object[] value)
+ {
+ Options["key"] = JArray.FromObject(value).ToString();
+ return this;
+ }
+
+ public CouchQuery StartKey(string value)
+ {
+ Options["startkey"] = "\"" + value + "\"";
+ return this;
+ }
+
+ public CouchQuery StartKey(params object[] value)
+ {
+ Options["startkey"] = JArray.FromObject(value).ToString();
+ return this;
+ }
+
+ public CouchQuery StartKeyDocumentId(string value)
+ {
+ Options["startkey_docid"] = value;
+ return this;
+ }
+
+ public CouchQuery EndKey(string value)
+ {
+ Options["endkey"] = "\"" + value + "\"";
+ return this;
+ }
+
+ public CouchQuery EndKey(params object[] value)
+ {
+ Options["endkey"] = JArray.FromObject(value).ToString();
+ return this;
+ }
+
+ public CouchQuery EndKeyDocumentId(string value)
+ {
+ Options["endkey_docid"] = value;
+ return this;
+ }
+
+ public CouchQuery Limit(int value)
+ {
+ Options["limit"] = value.ToString();
+ return this;
+ }
+
+ public CouchQuery Stale()
+ {
+ Options["stale"] = "ok";
+ return this;
+ }
+
+ public CouchQuery Descending()
+ {
+ Options["descending"] = "true";
+ return this;
+ }
+
+ public CouchQuery Skip(int value)
+ {
+ Options["skip"] = value.ToString();
+ return this;
+ }
+
+ public CouchQuery Group()
+ {
+ Options["group"] = "true";
+ return this;
+ }
+
+ public CouchQuery GroupLevel(int value)
+ {
+ Options["group_level"] = value.ToString();
+ return this;
+ }
+
+ public CouchQuery Reduce()
+ {
+ Options["reduce"] = "true";
+ return this;
+ }
+
+ public CouchQuery IncludeDocuments()
+ {
+ Options["include_docs"] = "true";
+ return this;
+ }
+
+ ///
+ /// Tell this query to do a HEAD request first to see
+ /// if ETag has changed and only then do the full request.
+ /// This is only interesting if you are reusing this query object.
+ ///
+ public CouchQuery CheckETagUsingHead()
+ {
+ checkETagUsingHead = true;
+ return this;
+ }
+
+ public CouchGenericViewResult GetResult()
+ {
+ return GetResult();
+ }
+
+ public bool IsCachedAndValid()
+ {
+ // If we do not have a result it is not cached
+ if (Result == null)
+ {
+ return false;
+ }
+ CouchRequest req = View.Request().QueryOptions(Options);
+ req.Etag(Result.etag);
+ return req.Head().Send().IsETagValid();
+ }
+
+ public string String()
+ {
+ CouchRequest req = View.Request().QueryOptions(Options);
+
+ if (postData != null)
+ {
+ req.Data(postData).Post();
+ }
+
+ return req.String();
+ }
+
+ public T GetResult() where T : CouchViewResult, new()
+ {
+ CouchRequest req = View.Request().QueryOptions(Options);
+
+ if (postData != null)
+ {
+ req.Data(postData).Post();
+ }
+
+ if (Result == null)
+ {
+ Result = new T();
+ }
+ else
+ {
+ // Tell the request what we already have
+ req.Etag(Result.etag);
+ if (checkETagUsingHead)
+ {
+ // Make a HEAD request to avoid transfer of data
+ if (req.Head().Send().IsETagValid())
+ {
+ return (T) Result;
+ }
+ // Set back to GET before proceeding below
+ req.Get();
+ }
+ }
+
+ JObject json = req.Parse();
+ if (json != null) // ETag did not match, view has changed
+ {
+ Result.Result(json);
+ Result.etag = req.Etag();
+ }
+ return (T) Result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CouchQueryDocument.cs b/src/CouchQueryDocument.cs
new file mode 100644
index 0000000..e14ffef
--- /dev/null
+++ b/src/CouchQueryDocument.cs
@@ -0,0 +1,19 @@
+using Newtonsoft.Json.Linq;
+
+namespace Divan
+{
+ ///
+ /// This is used to hold only metadata about a document retrieved from view queries.
+ ///
+ public class CouchQueryDocument : CouchDocument
+ {
+ public string Key { get; set; }
+
+ public override void ReadJson(JObject obj)
+ {
+ Id = obj["id"].Value();
+ Key = obj["key"].Value();
+ Rev = (obj["value"].Value())["rev"].Value();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CouchRequest.cs b/src/CouchRequest.cs
new file mode 100644
index 0000000..8aab17f
--- /dev/null
+++ b/src/CouchRequest.cs
@@ -0,0 +1,291 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Net;
+using System.Text;
+using System.Web;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Divan
+{
+ ///
+ /// A CouchDB HTTP request with all its options. This is where we do the actual HTTP requests to CouchDB.
+ ///
+ public class CouchRequest
+ {
+ private readonly CouchDatabase db;
+ private readonly CouchServer server;
+ private string etag, etagToCheck;
+ public Dictionary headers = new Dictionary();
+
+ // Query options
+ public string method = "GET"; // PUT, DELETE, POST, HEAD
+ public string mimeType;
+ public string path;
+ public string postData;
+ public string query;
+
+ public JToken result;
+
+ public CouchRequest(CouchServer server)
+ {
+ this.server = server;
+ }
+
+ public CouchRequest(CouchDatabase db)
+ {
+ server = db.Server;
+ this.db = db;
+ }
+
+ public CouchRequest Etag(string value)
+ {
+ etagToCheck = value;
+ headers["If-Modified"] = value;
+ return this;
+ }
+
+ public CouchRequest Path(string name)
+ {
+ path = name;
+ return this;
+ }
+
+ public CouchRequest Query(string name)
+ {
+ query = name;
+ return this;
+ }
+
+ public CouchRequest QueryOptions(ICollection> options)
+ {
+ if (options == null || options.Count == 0)
+ {
+ return this;
+ }
+
+ var sb = new StringBuilder();
+ sb.Append("?");
+ foreach (var q in options)
+ {
+ if (sb.Length > 1)
+ {
+ sb.Append("&");
+ }
+ sb.Append(HttpUtility.UrlEncode(q.Key));
+ sb.Append("=");
+ sb.Append(HttpUtility.UrlEncode(q.Value));
+ }
+
+ return Query(sb.ToString());
+ }
+
+ public CouchRequest Head()
+ {
+ method = "HEAD";
+ return this;
+ }
+
+ public CouchRequest PostJson()
+ {
+ MimeTypeJson();
+ return Post();
+ }
+
+ public CouchRequest Post()
+ {
+ method = "POST";
+ return this;
+ }
+
+ public CouchRequest Get()
+ {
+ method = "GET";
+ return this;
+ }
+
+ public CouchRequest Put()
+ {
+ method = "PUT";
+ return this;
+ }
+
+ public CouchRequest Delete()
+ {
+ method = "DELETE";
+ return this;
+ }
+
+ public CouchRequest Data(string data)
+ {
+ postData = data;
+ return this;
+ }
+
+ public CouchRequest MimeType(string type)
+ {
+ mimeType = type;
+ return this;
+ }
+
+ public CouchRequest MimeTypeJson()
+ {
+ MimeType("application/json");
+ return this;
+ }
+
+ public JObject Result()
+ {
+ return (JObject) result;
+ }
+
+ public T Result() where T : JToken
+ {
+ return (T) result;
+ }
+
+ public string Etag()
+ {
+ return etag;
+ }
+
+ public CouchRequest Check(string message)
+ {
+ try
+ {
+ if (result == null)
+ {
+ Parse();
+ }
+ if (!result["ok"].Value())
+ {
+ throw CouchException.Create(string.Format(CultureInfo.InvariantCulture, message + ": {0}", result));
+ }
+ return this;
+ }
+ catch (WebException e)
+ {
+ throw CouchException.Create(message, e);
+ }
+ }
+
+ private HttpWebRequest GetRequest()
+ {
+ Uri requestUri = new UriBuilder("http", server.Host, server.Port, ((db != null) ? db.Name + "/" : "") + path, query).Uri;
+ var request = WebRequest.Create(requestUri) as HttpWebRequest;
+ if (request == null)
+ {
+ throw CouchException.Create("Failed to create request");
+ }
+ request.Timeout = 3600000; // 1 hour. May use System.Threading.Timeout.Infinite;
+ request.Method = method;
+
+ if (mimeType != null)
+ {
+ request.ContentType = mimeType;
+ }
+
+ if (postData != null)
+ {
+ byte[] bytes = Encoding.UTF8.GetBytes(postData);
+ request.ContentLength = bytes.Length;
+ using (Stream ps = request.GetRequestStream())
+ {
+ ps.Write(bytes, 0, bytes.Length);
+ ps.Close();
+ }
+ }
+
+ Trace.WriteLine(string.Format(CultureInfo.InvariantCulture, "Request: {0} Method: {1}", requestUri, method));
+ return request;
+ }
+
+ public JObject Parse()
+ {
+ return Parse();
+ }
+
+ public T Parse() where T : JToken
+ {
+ //var timer = new Stopwatch();
+ //timer.Start();
+ using (WebResponse response = GetResponse())
+ {
+ using (Stream stream = response.GetResponseStream())
+ {
+ using (var reader = new StreamReader(stream))
+ {
+ using (var textReader = new JsonTextReader(reader))
+ {
+ PickETag(response);
+ if (etagToCheck != null)
+ {
+ if (IsETagValid())
+ {
+ return null;
+ }
+ }
+ result = JToken.ReadFrom(textReader); // We know it is a top level JSON JObject.
+ }
+ }
+ }
+ }
+ //timer.Stop();
+ //Trace.WriteLine("Time for Couch HTTP & JSON PARSE: " + timer.ElapsedMilliseconds);
+ return (T) result;
+ }
+
+ private void PickETag(WebResponse response)
+ {
+ etag = response.Headers["ETag"];
+ if (etag != null)
+ {
+ etag = etag.EndsWith("\"") ? etag.Substring(1, etag.Length - 2) : etag;
+ }
+ }
+
+ ///
+ /// Return the request as a plain string instead of trying to parse it.
+ ///
+ public string String()
+ {
+ using (WebResponse response = GetResponse())
+ {
+ using (var reader = new StreamReader(response.GetResponseStream()))
+ {
+ PickETag(response);
+ if (etagToCheck != null)
+ {
+ if (IsETagValid())
+ {
+ return null;
+ }
+ }
+ return reader.ReadToEnd();
+ }
+ }
+ }
+
+ private WebResponse GetResponse()
+ {
+ return GetRequest().GetResponse();
+ }
+
+ public CouchRequest Send()
+ {
+ using (WebResponse response = GetResponse())
+ {
+ PickETag(response);
+ return this;
+ }
+ }
+
+ public bool IsETagValid()
+ {
+ return etagToCheck == etag;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CouchServer.cs b/src/CouchServer.cs
new file mode 100644
index 0000000..561c6ed
--- /dev/null
+++ b/src/CouchServer.cs
@@ -0,0 +1,180 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Net;
+using System.Text.RegularExpressions;
+using Newtonsoft.Json;
+
+namespace Divan
+{
+ ///
+ /// A CouchServer is simply a communication end point holding a hostname and a port number to talk to.
+ /// It has an API to list, lookup, create or delete CouchDB "databases" in the CouchDB server.
+ /// One nice approach is to create a specific subclass that knows about its databases.
+ /// DatabasePrefix can be used to separate all databases created from other CouchDB databases.
+ ///
+ public class CouchServer
+ {
+ private const string DefaultHost = "192.168.9.205";
+ private const int DefaultPort = 5984;
+ private readonly JsonSerializer serializer = new JsonSerializer();
+
+ public readonly string Host;
+ public readonly int Port;
+
+ public string DatabasePrefix = ""; // Used by databases to prefix their names
+
+ public CouchServer(string host, int port)
+ {
+ Host = host;
+ Port = port;
+ }
+
+ public CouchServer(string host)
+ : this(host, DefaultPort)
+ {
+ }
+
+ public CouchServer()
+ : this(DefaultHost, DefaultPort)
+ {
+ }
+
+ public string ServerName
+ {
+ get { return Host + ":" + Port; }
+ }
+
+ public CouchRequest Request()
+ {
+ return new CouchRequest(this);
+ }
+
+ public bool HasDatabase(string name)
+ {
+ //return GetDatabaseNames().Contains(name); // This is too slow when we have thousands of dbs!!!
+ try
+ {
+ Request().Path(name).Head().Send();
+ return true;
+ }
+ catch (WebException)
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Get a CouchDatabase with given name. We create
+ /// the database if needed.
+ ///
+ public CouchDatabase GetDatabase(string name)
+ {
+ var db = new CouchDatabase(name, this);
+ db.Create();
+ return db;
+ }
+
+ ///
+ /// Get specialized subclass of CouchDatabase with given name.
+ /// We check if the database exists and delete it if it does,
+ /// then we recreate it.
+ ///
+ public CouchDatabase GetNewDatabase(string name)
+ {
+ var db = new CouchDatabase(name, this);
+ if (db.Exists())
+ {
+ db.Delete();
+ }
+ db.Create();
+ return db;
+ }
+
+ ///
+ /// Get specialized subclass of CouchDatabase. That class should
+ /// define its own database name. We presume it is already created.
+ ///
+ public T GetExistingDatabase() where T : CouchDatabase, new()
+ {
+ return new T {Server = this};
+ }
+
+ ///
+ /// Get specialized subclass of CouchDatabase with given name.
+ /// We presume it is already created.
+ ///
+ public T GetExistingDatabase(string name) where T : CouchDatabase, new()
+ {
+ return new T {Name = name, Server = this};
+ }
+
+ ///
+ /// Get specialized subclass of CouchDatabase. That class should
+ /// define its own database name. We ensure that it is created.
+ ///
+ public T GetDatabase() where T : CouchDatabase, new()
+ {
+ var db = GetExistingDatabase();
+ db.Create();
+ return db;
+ }
+
+ ///
+ /// Get specialized subclass of CouchDatabase with given name.
+ /// We create the database if needed.
+ ///
+ public T GetDatabase(string name) where T : CouchDatabase, new()
+ {
+ var db = GetExistingDatabase(name);
+ db.Create();
+ return db;
+ }
+
+ public void CreateDatabase(string name)
+ {
+ try
+ {
+ Request().Path(name).Put().Check("Failed to create database");
+ }
+ catch (WebException e)
+ {
+ throw CouchException.Create("Failed to create database", e);
+ }
+ }
+
+ public void DeleteAllDatabases()
+ {
+ DeleteDatabases(".*");
+ }
+
+ public void DeleteDatabases(string regExp)
+ {
+ var reg = new Regex(regExp);
+ foreach (string name in GetDatabaseNames())
+ {
+ if (reg.IsMatch(name))
+ {
+ DeleteDatabase(name);
+ }
+ }
+ }
+
+ public void DeleteDatabase(string name)
+ {
+ try
+ {
+ Request().Path(name).Delete().Check("Failed to delete database");
+ }
+ catch (WebException e)
+ {
+ throw new CouchException("Failed to delete database", e);
+ }
+ }
+
+ public IList GetDatabaseNames()
+ {
+ return (List) serializer.Deserialize(new JsonTextReader(new StringReader(Request().Path("_all_dbs").String())), typeof (List));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CouchTest.cs b/src/CouchTest.cs
new file mode 100644
index 0000000..3a4bbc3
--- /dev/null
+++ b/src/CouchTest.cs
@@ -0,0 +1,252 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Newtonsoft.Json.Linq;
+using NUnit.Framework;
+using NUnit.Framework.SyntaxHelpers;
+
+namespace Divan
+{
+ ///
+ /// Unit tests for Divan. Operates in a separate CouchDB database called divan_unit_tests.
+ ///
+ [TestFixture]
+ public class CouchTest
+ {
+ #region Setup/Teardown
+
+ [SetUp]
+ public void SetUp()
+ {
+ server = new CouchServer();
+ db = server.GetNewDatabase(DbName);
+ }
+
+ [TearDown]
+ public void TearDown()
+ {
+ db.Delete();
+ }
+
+ #endregion
+
+ private CouchServer server;
+ private CouchDatabase db;
+ private const string DbName = "divan_unit_tests";
+
+ [Test]
+ public void ShouldCheckChangedDocument()
+ {
+ CouchJsonDocument doc = db.CreateDocument("{\"CPU\": \"Intel\"}");
+ CouchJsonDocument doc2 = db.GetDocument(doc.Id);
+ Assert.That(db.HasDocumentChanged(doc), Is.False);
+ doc2.Obj["CPU"] = JToken.FromObject("AMD");
+ db.WriteDocument(doc2);
+ Assert.That(db.HasDocumentChanged(doc), Is.True);
+ }
+
+ [Test]
+ public void ShouldCountDocuments()
+ {
+ Assert.That(db.CountDocuments(), Is.EqualTo(0));
+ db.CreateDocument("{\"CPU\": \"Intel\"}");
+ Assert.That(db.CountDocuments(), Is.EqualTo(1));
+ }
+
+ [Test]
+ public void ShouldCreateDocument()
+ {
+ var doc = new CouchJsonDocument("{\"CPU\": \"Intel\"}");
+ ICouchDocument cd = db.CreateDocument(doc);
+ Assert.That(db.CountDocuments(), Is.EqualTo(1));
+ Assert.That(cd.Id, Is.Not.Null);
+ Assert.That(cd.Rev, Is.Not.Null);
+ }
+
+ [Test]
+ public void ShouldCreateDocuments()
+ {
+ const string doc = "{\"CPU\": \"Intel\"}";
+ var doc1 = new CouchJsonDocument(doc);
+ var doc2 = new CouchJsonDocument(doc);
+ IList list = new List {doc1, doc2};
+ db.SaveDocuments(list, true);
+ Assert.That(db.CountDocuments(), Is.EqualTo(2));
+ Assert.That(doc1.Id, Is.Not.Null);
+ Assert.That(doc1.Rev, Is.Not.Null);
+ Assert.That(doc2.Id, Is.Not.Null);
+ Assert.That(doc2.Rev, Is.Not.Null);
+ Assert.That(doc1.Id, Is.Not.EqualTo(doc2.Id));
+ }
+
+ [Test, ExpectedException(typeof (CouchNotFoundException))]
+ public void ShouldDeleteDatabase()
+ {
+ db.Delete();
+ Assert.That(server.HasDatabase(db.Name), Is.EqualTo(false));
+ server.DeleteDatabase(db.Name); // one more time should fail
+ }
+
+ [Test]
+ public void ShouldDeleteDocuments()
+ {
+ const string doc = "{\"CPU\": \"Intel\"}";
+ CouchJsonDocument doc1 = db.CreateDocument(doc);
+ CouchJsonDocument doc2 = db.CreateDocument(doc);
+ if (String.Compare(doc1.Id, doc2.Id) < 0)
+ {
+ db.DeleteDocuments(doc1.Id, doc2.Id);
+ }
+ else
+ {
+ db.DeleteDocuments(doc2.Id, doc1.Id);
+ }
+ Assert.That(db.HasDocument(doc1.Id), Is.False);
+ Assert.That(db.HasDocument(doc2.Id), Is.False);
+ }
+
+ [Test, ExpectedException(typeof (CouchException))]
+ public void ShouldFailCreateDatabase()
+ {
+ server.CreateDatabase(db.Name); // one more time should fail
+ }
+
+ [Test]
+ public void ShouldGetDatabaseNames()
+ {
+ bool result = server.GetDatabaseNames().Contains(db.Name);
+ Assert.That(result, Is.EqualTo(true));
+ }
+
+ [Test]
+ public void ShouldGetDocument()
+ {
+ const string doc = "{\"CPU\": \"Intel\"}";
+ CouchJsonDocument oldDoc = db.CreateDocument(doc);
+ CouchJsonDocument newDoc = db.GetDocument(oldDoc.Id);
+ Assert.That(oldDoc.Id, Is.EqualTo(newDoc.Id));
+ Assert.That(oldDoc.Rev, Is.EqualTo(newDoc.Rev));
+ }
+
+ [Test]
+ public void ShouldGetDocuments()
+ {
+ const string doc = "{\"CPU\": \"Intel\"}";
+ CouchJsonDocument doc1 = db.CreateDocument(doc);
+ CouchJsonDocument doc2 = db.CreateDocument(doc);
+ var ids = new List {doc1.Id, doc2.Id};
+ IList docs = db.GetDocuments(ids);
+ Assert.That(doc1.Id, Is.EqualTo(docs.First().Id));
+ Assert.That(doc2.Id, Is.EqualTo(docs.Last().Id));
+ }
+
+ [Test]
+ public void ShouldReturnNullWhenNotFound()
+ {
+ var doc = db.GetDocument("jadda");
+ Assert.That(doc, Is.Null);
+ CouchJsonDocument doc2 = db.GetDocument("jadda");
+ Assert.That(doc2, Is.Null);
+ }
+
+ [Test]
+ public void ShouldSaveDocumentWithId()
+ {
+ var doc = new CouchJsonDocument("{\"_id\":\"123\", \"CPU\": \"Intel\"}");
+ ICouchDocument cd = db.SaveDocument(doc);
+ Assert.That(db.CountDocuments(), Is.EqualTo(1));
+ Assert.That(cd.Id, Is.Not.Null);
+ Assert.That(cd.Rev, Is.Not.Null);
+ }
+
+ [Test]
+ public void ShouldSaveDocumentWithoutId()
+ {
+ var doc = new CouchJsonDocument("{\"CPU\": \"Intel\"}");
+ ICouchDocument cd = db.SaveDocument(doc);
+ Assert.That(db.CountDocuments(), Is.EqualTo(1));
+ Assert.That(cd.Id, Is.Not.Null);
+ Assert.That(cd.Rev, Is.Not.Null);
+ }
+
+ [Test]
+ public void ShouldStoreGetAndDeleteAttachment()
+ {
+ var doc = new CouchJsonDocument("{\"CPU\": \"Intel\"}");
+ ICouchDocument cd = db.CreateDocument(doc);
+ Assert.That(db.HasAttachment(cd), Is.False);
+ db.WriteAttachment(cd, "jabbadabba", "text/plain");
+ Assert.That(db.HasAttachment(cd), Is.True);
+ Assert.That(db.ReadAttachment(cd), Is.EqualTo("jabbadabba"));
+ db.WriteAttachment(cd, "jabbadabba-doo", "text/plain");
+ Assert.That(db.HasAttachment(cd), Is.True);
+ Assert.That(db.ReadAttachment(cd), Is.EqualTo("jabbadabba-doo"));
+ db.DeleteAttachment(cd);
+ Assert.That(db.HasAttachment(cd), Is.False);
+ }
+
+ [Test, ExpectedException(typeof (CouchConflictException))]
+ public void ShouldThrowConflictExceptionOnAlreadyExists()
+ {
+ const string doc = "{\"CPU\": \"Intel\"}";
+ CouchJsonDocument doc1 = db.CreateDocument(doc);
+ var doc2 = new CouchJsonDocument(doc) {Id = doc1.Id};
+ db.WriteDocument(doc2);
+ }
+
+ [Test, ExpectedException(typeof (CouchConflictException))]
+ public void ShouldThrowConflictExceptionOnStaleWrite()
+ {
+ const string doc = "{\"CPU\": \"Intel\"}";
+ CouchJsonDocument doc1 = db.CreateDocument(doc);
+ CouchJsonDocument doc2 = db.GetDocument(doc1.Id);
+ doc1.Obj["CPU"] = JToken.FromObject("AMD");
+ db.SaveDocument(doc1);
+ doc2.Obj["CPU"] = JToken.FromObject("Via");
+ db.SaveDocument(doc2);
+ }
+
+ [Test]
+ public void ShouldUseETagForView()
+ {
+ var design = new DesignCouchDocument("computers", db);
+ design.AddView("by_cpumake",
+ @"function(doc) {
+ emit(doc.CPU, doc);
+ }");
+ db.WriteDocument(design);
+
+ CouchJsonDocument doc1 = db.CreateDocument("{\"CPU\": \"Intel\"}");
+ db.CreateDocument("{\"CPU\": \"AMD\"}");
+ db.CreateDocument("{\"CPU\": \"Via\"}");
+ db.CreateDocument("{\"CPU\": \"Sparq\"}");
+
+ CouchQuery query = db.Query("computers", "by_cpumake").StartKey("Intel").EndKey("Via").CheckETagUsingHead();
+ // Query has no result yet so should not be cached
+ Assert.That(query.IsCachedAndValid(), Is.False);
+ query.GetResult();
+ // Now it is cached and should be valid
+ Assert.That(query.IsCachedAndValid(), Is.True);
+ // Make a change invalidating the view
+ db.SaveDocument(doc1);
+ // It should now be false
+ Assert.That(query.IsCachedAndValid(), Is.False);
+ query.GetResult();
+ // And now it should be cached again
+ Assert.That(query.IsCachedAndValid(), Is.True);
+ query.GetResult();
+ // Still cached of course
+ Assert.That(query.IsCachedAndValid(), Is.True);
+ }
+
+ [Test]
+ public void ShouldWriteDocument()
+ {
+ var doc = new CouchJsonDocument("{\"_id\":\"123\", \"CPU\": \"Intel\"}");
+ ICouchDocument cd = db.WriteDocument(doc);
+ Assert.That(db.CountDocuments(), Is.EqualTo(1));
+ Assert.That(cd.Id, Is.Not.Null);
+ Assert.That(cd.Rev, Is.Not.Null);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CouchViewDefinition.cs b/src/CouchViewDefinition.cs
new file mode 100644
index 0000000..769674f
--- /dev/null
+++ b/src/CouchViewDefinition.cs
@@ -0,0 +1,85 @@
+using Newtonsoft.Json;
+
+namespace Divan
+{
+ ///
+ /// A definition of a CouchDB view with a name, a map and a reduce function and a reference to the
+ /// owning DesignCouchDocument.
+ ///
+ public class CouchViewDefinition
+ {
+ ///
+ /// Constructor used to create "on the fly" definitions, like for example for "_all_docs".
+ ///
+ /// View name used in URI.
+ /// A design doc, can also be created on the fly.
+ public CouchViewDefinition(string name, DesignCouchDocument doc)
+ {
+ Doc = doc;
+ Name = name;
+ }
+
+ ///
+ /// Constructor used for permanent views, see CouchDesignDocument.
+ ///
+ /// View name.
+ /// Map function.
+ /// Optional reduce function.
+ /// Parent document.
+ public CouchViewDefinition(string name, string map, string reduce, DesignCouchDocument doc)
+ {
+ Doc = doc;
+ Name = name;
+ Map = map;
+ Reduce = reduce;
+ }
+
+ public DesignCouchDocument Doc { get; set; }
+ public string Name { get; set; }
+ public string Map { get; set; }
+ public string Reduce { get; set; }
+
+ public CouchRequest Request()
+ {
+ return Doc.Owner.Request(Path());
+ }
+
+ public CouchDatabase Db()
+ {
+ return Doc.Owner;
+ }
+
+ public void WriteJson(JsonWriter writer)
+ {
+ writer.WritePropertyName(Name);
+ writer.WriteStartObject();
+ writer.WritePropertyName("map");
+ writer.WriteValue(Map);
+ if (Reduce != null)
+ {
+ writer.WritePropertyName("reduce");
+ writer.WriteValue(Reduce);
+ }
+ writer.WriteEndObject();
+ }
+
+ public CouchQuery Query()
+ {
+ return Doc.Owner.Query(this);
+ }
+
+ public void Touch()
+ {
+ Query().Limit(0).GetResult();
+ }
+
+ public string Path()
+ {
+ if (Doc.Id == "_design/")
+ {
+ return Name;
+ }
+ return Doc.Id + "/_view/" + Name;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CouchViewResult.cs b/src/CouchViewResult.cs
new file mode 100644
index 0000000..b31ec5e
--- /dev/null
+++ b/src/CouchViewResult.cs
@@ -0,0 +1,36 @@
+using Newtonsoft.Json.Linq;
+
+namespace Divan
+{
+ ///
+ /// This is a view result from a CouchQuery. The result is returned as JSON
+ /// from CouchDB and parsed into a JObject by Newtonsoft.Json. A view result
+ /// also includes some meta information and this class has methods to access these.
+ /// Typically you use a subclass.
+ ///
+ public class CouchViewResult
+ {
+ public string etag;
+ public JObject result;
+
+ public void Result(JObject obj)
+ {
+ result = obj;
+ }
+
+ public int Count()
+ {
+ return result["total_rows"].Value();
+ }
+
+ public int Offset()
+ {
+ return result["offset"].Value();
+ }
+
+ public JEnumerable Rows()
+ {
+ return result["rows"].Children();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CustomDictionary.xml b/src/CustomDictionary.xml
new file mode 100644
index 0000000..9b13063
--- /dev/null
+++ b/src/CustomDictionary.xml
@@ -0,0 +1,468 @@
+
+
+
+
+
+ cb
+ ch
+ csc
+ elem
+ gt
+ idx
+ img
+ lg
+ multi
+ num
+ ps
+ pw
+ scp
+ si
+ sig
+ tk
+ tw
+ val
+
+
+
+ json
+ accessor
+ accessors
+ acos
+ aes
+ aptca
+ arg
+ args
+ asin
+ asm
+ aspx
+ async
+ atan
+ baml
+ bcl
+ bindable
+ bitrate
+ blittable
+ blog
+ bool
+ bootstrapper
+ bootstrappers
+ browsable
+ cacheability
+ callee
+ callees
+ canonicalize
+ cdecl
+ cdo
+ chtml
+ cim
+ cloneable
+ clr
+ clr's
+ cls
+ clsid
+ clsids
+ cmd
+ cmdlet
+ cmdlets
+ comparand
+ concat
+ config
+ contravariant
+ cookieless
+ cos
+ crm
+ css
+ cyclomatic
+ debuggable
+ decommission
+ deformatter
+ delegator
+ dequeue
+ dereferenced
+ des
+ deserialization
+ deserialize
+ deserialized
+ deserializing
+ dhcp
+ discardable
+ dll
+ dns
+ documentable
+ dsig
+ dtd
+ em
+ email
+ emails
+ emf
+ encodable
+ endian
+ enqueue
+ enum
+ enums
+ expando
+ finalizer
+ finalizers
+ fixup
+ fixups
+ formattable
+ func
+ guid
+ guids
+ hashtable
+ hashtables
+ hashtable's
+ hdc
+ hijri
+ href
+ iis
+ il
+ ime
+ initializer
+ initializers
+ int
+ interop
+ intrinsics
+ ipv
+ iterator
+ iterators
+ jit
+ ldap
+ linq
+ localhost
+ loopback
+ loopbacks
+ mapper
+ mappers
+ marshaler
+ marshalers
+ mdi
+ mergable
+ misc
+ miscased
+ monitorable
+ oks
+ mscoree
+ mscorlib
+ msh
+ multiline
+ multipanel
+ multipanels
+ multiview
+ multiviews
+ mutator
+ mutators
+ mutex
+ mutexes
+ ndpsec
+ nls
+ nop
+ ntfs
+ ntlm
+ nullable
+ obj
+ odbc
+ overridable
+ pageable
+ parameterless
+ pdb
+ persistable
+ playlist
+ pragma
+ prepend
+ prog
+ ptr
+ queryable
+ ras
+ rect
+ rects
+ recurse
+ refactor
+ reg
+ regex
+ remoted
+ remoting
+ representable
+ res
+ resolver
+ resolvers
+ rethrow
+ rethrows
+ rijndael
+ rpc
+ rtc
+ rva
+ sdl
+ searchspace
+ searchspaces
+ seekable
+ seq
+ serializable
+ serializer
+ serializers
+ smtp
+ specifier
+ specifiers
+ spline
+ sql
+ ssl
+ sta
+ stickies
+ struct
+ structs
+ subaddress
+ subaddresses
+ subclass
+ subclasses
+ subdirectories
+ subdirectory
+ subexpression
+ subexpressions
+ subitem
+ subitems
+ subkey
+ subkeys
+ submenu
+ submenus
+ subpath
+ subpaths
+ subsegment
+ subsegments
+ subtree
+ subtrees
+ tcp
+ templated
+ thunk
+ thunks
+ tlb
+ tuple
+ tuples
+ udp
+ udt
+ unboxing
+ uncategorize
+ unindent
+ uninitialize
+ uninitialized
+ uninstantiated
+ unmaintainable
+ unmarshal
+ unregister
+ unregistering
+ unregisters
+ unregistration
+ unrepresentable
+ unterminated
+ untrusted
+ uri
+ uris
+ url
+ urls
+ utc
+ utf
+ validator
+ vsa
+ weblog
+ wiki
+ wcf
+ wmf
+ wmi
+ wml
+ wpf
+ wql
+ wsdl
+ xaml
+ xhtml
+ xmlns
+ xor
+ xrml
+ xsd
+ xsi
+ xsl
+ xslt
+
+
+
+ complus
+ cancelled
+ indices
+ login
+ logout
+ signon
+ signoff
+ writeable
+ cant
+ arent
+ dont
+ doesnt
+ didnt
+ couldnt
+ wouldnt
+ shouldnt
+ wont
+ havent
+ hasnt
+ hadnt
+ isnt
+ wasnt
+ werent
+ flag
+ flags
+
+
+
+ datastore
+ datastores
+ dataset
+ datasets
+ textbox
+ textboxes
+ codepage
+ codepages
+ checkbox
+ checkboxes
+ pushbutton
+ pushbuttons
+ dropdown
+ dropdowns
+ toolbar
+ toolbars
+ scrollbar
+ scrollbars
+ bitflag
+ bitflags
+ filename
+ filenames
+ fileserver
+ fileservers
+ username
+ usernames
+ hostname
+ hostnames
+ fieldname
+ fieldnames
+ pathname
+ pathnames
+ whitespace
+ whitespaces
+ logon
+ logons
+ logoff
+ logoffs
+ signin
+ signins
+ signout
+ signouts
+ frontend
+ frontends
+ backend
+ backends
+ sitemap
+ sitemaps
+ datatype
+ datatypes
+ designtime
+ designtimes
+ readonly
+ truetype
+ netbios
+ autodetect
+ autodetects
+ autoscroll
+ autoscrolls
+ autocomplete
+ autocompletes
+ autosave
+ autosaves
+ javascript
+ jscript
+ voiceview
+ appletalk
+ mapinfo
+ newline
+ newlines
+ qword
+ qwords
+ keyset
+ keysets
+
+
+
+ onset
+ inset
+ byname
+ setout
+ countertype
+ editor
+ longtime
+ drawstring
+ hookup
+ cleanup
+ breakout
+ setline
+ maybe
+ nods
+ classis
+ gettable
+ inform
+ beset
+ settable
+ standalone
+ threadlike
+ infield
+ infields
+ meantime
+ mackey
+ jscript
+ ipv
+ tooltip
+ tooltips
+ indispose
+
+
+
+
+ Pi
+ Na
+ NESW
+ NWSE
+ Json
+
+
+
diff --git a/src/DesignCouchDocument.cs b/src/DesignCouchDocument.cs
new file mode 100644
index 0000000..6722026
--- /dev/null
+++ b/src/DesignCouchDocument.cs
@@ -0,0 +1,66 @@
+using System.Collections.Generic;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Divan
+{
+ ///
+ /// A named design document in CouchDB. Holds CouchViewDefinitions.
+ ///
+ public class DesignCouchDocument : CouchDocument
+ {
+ public IList Definitions = new List();
+ public string Language = "javascript";
+ public CouchDatabase Owner;
+
+ public DesignCouchDocument(string documentId, CouchDatabase owner)
+ : base("_design/" + documentId)
+ {
+ Owner = owner;
+ }
+
+ ///
+ /// Add view without a reduce function.
+ ///
+ /// Name of view
+ /// Map function
+ ///
+ public CouchViewDefinition AddView(string name, string map)
+ {
+ return AddView(name, map, null);
+ }
+
+ ///
+ /// Add view with a reduce function.
+ ///
+ /// Name of view
+ /// Map function
+ /// Reduce function
+ ///
+ public CouchViewDefinition AddView(string name, string map, string reduce)
+ {
+ var def = new CouchViewDefinition(name, map, reduce, this);
+ Definitions.Add(def);
+ return def;
+ }
+
+ public override void WriteJson(JsonWriter writer)
+ {
+ WriteIdAndRev(this, writer);
+ writer.WritePropertyName("language");
+ writer.WriteValue(Language);
+ writer.WritePropertyName("views");
+ writer.WriteStartObject();
+ foreach (CouchViewDefinition definition in Definitions)
+ {
+ definition.WriteJson(writer);
+ }
+ writer.WriteEndObject();
+ }
+
+ public override void ReadJson(JObject obj)
+ {
+ ReadIdAndRev(this, obj);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Divan.FxCop b/src/Divan.FxCop
new file mode 100644
index 0000000..a3875f0
--- /dev/null
+++ b/src/Divan.FxCop
@@ -0,0 +1,307 @@
+
+
+
+ True
+ c:\sandbox\monitor2\thirdparty\tools\fxcop\Xml\FxCopReport.xsl
+
+
+
+
+
+ True
+ True
+ True
+ 10
+ 1
+
+ False
+
+ False
+ 120
+ False
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ - 'Divan.dll'
+
+
+
+
+
+
+
+
+
+
+
+
+ - 'CouchDatabase.GetAllDocuments()'
+
+
+
+
+
+
+
+
+ - 'CouchDatabase.GetDocument<T>(string)'
+ - 'T'
+
+
+
+
+
+
+
+
+ - 'CouchDatabase.GetResultWithOptions<T>(string, string, Dictionary<string, string>)'
+ - 'T'
+
+
+
+
+
+
+
+
+ - 'CouchDatabase.GetView<T>(string, string)'
+ - 'T'
+
+
+
+
+
+
+
+
+ - 'CouchDatabase.GetView<T>(string, string, string)'
+ - 'T'
+
+
+
+
+
+
+
+
+ - 'CouchDatabase.GetView<T>(string, string, string, string)'
+ - 'T'
+
+
+
+
+
+
+
+
+
+
+
+
+ - 'CouchGenericViewResult.Document<T>()'
+ - 'T'
+
+
+
+
+
+
+
+
+ - 'CouchGenericViewResult.Documents<T>()'
+ - 'T'
+
+
+
+
+
+
+
+
+ - 'CouchGenericViewResult.RetrieveDocument<T>(string)'
+ - 'T'
+
+
+
+
+
+
+
+
+ - 'CouchGenericViewResult.RetrieveDocuments<T>(string)'
+ - 'T'
+
+
+
+
+
+
+
+
+ - 'CouchGenericViewResult.ValueDocument<T>()'
+ - 'T'
+
+
+
+
+
+
+
+
+ - 'CouchGenericViewResult.ValueDocuments<T>()'
+ - 'T'
+
+
+
+
+
+
+
+
+
+
+
+
+ - 'CouchQuery.GetResult<T>()'
+ - 'T'
+
+
+
+
+
+
+
+
+
+
+
+
+ - 'CouchServer.GetDatabaseNames()'
+
+
+
+
+
+
+
+
+ - 'url'
+ - 'CouchServer.Request(CouchDatabase, string, string)'
+
+
+
+
+
+
+
+
+ - 'url'
+ - 'CouchServer.Request(CouchDatabase, string, string, string, string)'
+
+
+
+
+
+
+
+
+ - 'url'
+ - 'CouchServer.RequestStream(CouchDatabase, string, string, string, string)'
+
+
+
+
+
+
+
+
+
+
+
+
+ - SetUp
+ - 'CouchTest.SetUp()'
+ - Setup
+
+
+
+
+
+
+
+
+
+
+
+
+
+ - TearDown
+ - 'CouchTest.TearDown()'
+ - Teardown
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ NUnit
+ NUnit
+
+
+
+
+ Sign {0} with a strong name key.
+
+
+ The compound word '{0}' in member name {1} exists as a discrete term. If your usage is intended to be single word, case it as '{2}' or strip the first token entirely if it represents any sort of Hungarian notation.
+
+
+ Consider a design where {0} doesn't require explicit type parameter {1} in any call to it.
+
+
+ Change the type of parameter {0} of method {1} from string to System.Uri, or provide an overload of {1}, that allows {0} to be passed as a System.Uri object.
+
+
+ Change {0} to a property if appropriate.
+
+
+
+
diff --git a/src/Divan.csproj b/src/Divan.csproj
new file mode 100644
index 0000000..fe5cc85
--- /dev/null
+++ b/src/Divan.csproj
@@ -0,0 +1,80 @@
+
+
+
+ Debug
+ AnyCPU
+ 9.0.30729
+ 2.0
+ {37AC0B66-5340-4B81-BC62-3EE80233A011}
+ Library
+ Properties
+ Divan
+ Divan
+ v3.5
+ 512
+
+
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+ False
+ ..\lib\Newtonsoft.Json.dll
+
+
+ False
+ ..\lib\nunit.framework.dll
+
+
+
+ 3.5
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/ICanJSON.cs b/src/ICanJSON.cs
new file mode 100644
index 0000000..02d7d4f
--- /dev/null
+++ b/src/ICanJSON.cs
@@ -0,0 +1,16 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Divan
+{
+ ///
+ /// Basic capability to write and read myself using Newtonsoft.JSON.
+ /// Writing is done using JsonWriter in a fast streaming fashion.
+ /// Reading is done using JObject "DOM style".
+ ///
+ public interface ICanJson
+ {
+ void WriteJson(JsonWriter writer);
+ void ReadJson(JObject obj);
+ }
+}
\ No newline at end of file
diff --git a/src/ICouchDocument.cs b/src/ICouchDocument.cs
new file mode 100644
index 0000000..d6b8999
--- /dev/null
+++ b/src/ICouchDocument.cs
@@ -0,0 +1,14 @@
+namespace Divan
+{
+ ///
+ /// An ICouchDocument needs to have a Rev and an Id. It also needs to implement ICanJson
+ /// which means it can read and write itself as JSON. Either you let your domain objects
+ /// that you want to store in CouchDB implement this interface or if you are free to pick
+ /// your own base class you can subclass from CouchDocument (or even CouchJsonDocument).
+ ///
+ public interface ICouchDocument : ICanJson
+ {
+ string Rev { get; set; }
+ string Id { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Properties/AssemblyInfo.cs b/src/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..dc865a7
--- /dev/null
+++ b/src/Properties/AssemblyInfo.cs
@@ -0,0 +1,39 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+
+[assembly: AssemblyTitle("Divan")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("Foretagsplatsen AB")]
+[assembly: AssemblyProduct("Divan")]
+[assembly: AssemblyCopyright("Copyright © Foretagsplatsen AB 2009")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+
+[assembly: Guid("83fd2c08-3765-4908-a901-a9ce4f6d05c0")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
\ No newline at end of file