Browse files

initial commit

  • Loading branch information...
0 parents commit acfb937246493dacfaf5a63005ae862c33da666a @ftgp ftgp committed Aug 11, 2009
11 .gitignore
@@ -0,0 +1,11 @@
+obj
+bin
+_ReSharper.*
+*.csproj.user
+*.resharper.user
+*.resharper
+*.suo
+*.cache
+*~
+*.swp
+.svn
26 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
92 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!
+
BIN lib/Newtonsoft.Json.dll
Binary file not shown.
BIN lib/nunit.framework.dll
Binary file not shown.
136 samples/Trivial/Program.cs
@@ -0,0 +1,136 @@
+using System;
+using System.Diagnostics;
+using Divan;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Trivial
+{
+ /// <summary>
+ /// A trivial example of using Divan. Requires a running CouchDB on localhost!
+ ///
+ /// Run using:
+ ///
+ /// Trivial.exe <host> <port>
+ ///
+ /// </summary>
+ 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>(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<Car>();
+ 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");
+ }
+
+ /// <summary>
+ /// 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.
+ /// </summary>
+ 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<string>();
+ Model = obj["Model"].Value<string>();
+ HorsePowers = obj["Hps"].Value<int>();
+ }
+
+ #endregion
+ }
+ }
+}
36 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")]
69 samples/Trivial/Trivial.csproj
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="3.5" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProductVersion>9.0.30729</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{CDCC7924-F227-46DC-B2E6-2BBE06B84AF2}</ProjectGuid>
+ <OutputType>Exe</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>Trivial</RootNamespace>
+ <AssemblyName>Trivial</AssemblyName>
+ <TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
+ <FileAlignment>512</FileAlignment>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>bin\Debug\</OutputPath>
+ <DefineConstants>DEBUG;TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>bin\Release\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="Newtonsoft.Json, Version=3.5.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\..\lib\Newtonsoft.Json.dll</HintPath>
+ </Reference>
+ <Reference Include="System" />
+ <Reference Include="System.Core">
+ <RequiredTargetFramework>3.5</RequiredTargetFramework>
+ </Reference>
+ <Reference Include="System.Xml.Linq">
+ <RequiredTargetFramework>3.5</RequiredTargetFramework>
+ </Reference>
+ <Reference Include="System.Data.DataSetExtensions">
+ <RequiredTargetFramework>3.5</RequiredTargetFramework>
+ </Reference>
+ <Reference Include="System.Data" />
+ <Reference Include="System.Xml" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="Program.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Divan.csproj">
+ <Project>{37AC0B66-5340-4B81-BC62-3EE80233A011}</Project>
+ <Name>Divan</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+ <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
+ Other similar extension points exist, see Microsoft.Common.targets.
+ <Target Name="BeforeBuild">
+ </Target>
+ <Target Name="AfterBuild">
+ </Target>
+ -->
+</Project>
37 src/CouchBulkDeleteDocuments.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Divan
+{
+ /// <summary>
+ /// Only used as pseudo doc when doing bulk updates/inserts.
+ /// </summary>
+ public class CouchBulkDeleteDocuments : CouchBulkDocuments
+ {
+ public CouchBulkDeleteDocuments(IList<ICouchDocument> 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();
+ }
+ }
+}
47 src/CouchBulkDocuments.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Generic;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Divan
+{
+ /// <summary>
+ /// Only used as psuedo doc when doing bulk updates/inserts.
+ /// </summary>
+ public class CouchBulkDocuments : ICanJson
+ {
+ public CouchBulkDocuments(IList<ICouchDocument> docs)
+ {
+ Docs = docs;
+ }
+
+ public IList<ICouchDocument> 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
+ }
+}
55 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
+{
+ /// <summary>
+ /// Only used as psuedo doc when doing bulk reads.
+ /// </summary>
+ public class CouchBulkKeys : ICanJson
+ {
+ public CouchBulkKeys(IEnumerable<string> 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
+ }
+}
14 src/CouchConflictException.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace Divan
+{
+ /// <summary>
+ /// Represents a CouchDB HTTP 409 conflict.
+ /// </summary>
+ public class CouchConflictException : Exception
+ {
+ public CouchConflictException(string msg, Exception e) : base(msg, e)
+ {
+ }
+ }
+}
615 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
+{
+ /// <summary>
+ /// 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.
+ /// </summary>
+ 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<int>();
+ }
+
+ public CouchRequest RequestAllDocuments()
+ {
+ return Request("_all_docs");
+ }
+
+ /// <summary>
+ /// Read all documents in database.
+ /// </summary>
+ /// <returns>List of documents in database.</returns>
+ public IList<CouchDocument> GetAllDocuments()
+ {
+ var list = new List<CouchDocument>();
+ 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;
+ }
+
+ /// <summary>
+ /// Initialize CouchDB database by loading design documents into it.
+ /// Override in subclasses.
+ /// </summary>
+ public virtual void Initialize()
+ {
+ // Nothing by default
+ }
+
+ public bool Exists()
+ {
+ return Server.HasDatabase(Name);
+ }
+
+ /// <summary>
+ /// Check first if database exists, and if it does not - create it and initialize it.
+ /// </summary>
+ public void Create()
+ {
+ if (!Exists())
+ {
+ Server.CreateDatabase(Name);
+ Initialize();
+ }
+ }
+
+ public void Delete()
+ {
+ if (Exists())
+ {
+ Server.DeleteDatabase(Name);
+ }
+ }
+
+ /// <summary>
+ /// Write a CouchDocument, it may already exist in db and will then be overwritten.
+ /// </summary>
+ /// <param name="json">Document as Json</param>
+ /// <param name="documentId">Document identifier</param>
+ /// <returns>A new CouchDocument</returns>
+ public ICouchDocument WriteDocument(string json, string documentId)
+ {
+ return WriteDocument(new CouchDocument(json, documentId));
+ }
+
+ /// <summary>
+ /// Write a CouchDocument or ICouchDocument, it may already exist in db and will then be overwritten.
+ /// </summary>
+ /// <param name="document">Couch document</param>
+ /// <returns>Couch Document with new Rev set.</returns>
+ /// <remarks>This relies on the document to already have an id.</remarks>
+ public ICouchDocument
+ WriteDocument(ICouchDocument document)
+ {
+ return WriteDocument(document, false);
+ }
+
+ /// <summary>
+ /// 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.
+ /// </summary>
+ /// <param name="document">Couch document</param>
+ /// <returns>Couch Document with new Rev set and possibly an Id set.</returns>
+ public ICouchDocument SaveDocument(ICouchDocument document)
+ {
+ if (document.Id == null)
+ {
+ return CreateDocument(document);
+ }
+ return WriteDocument(document);
+ }
+
+ /// <summary>
+ /// Write a CouchDocument or ICouchDocument, it may already exist in db and will then be overwritten.
+ /// </summary>
+ /// <param name="document">Couch document</param>
+ /// <param name="batch">True if we don't want to wait for flush (commit).</param>
+ /// <returns>Couch Document with new Rev set.</returns>
+ /// <remarks>This relies on the document to already have an id.</remarks>
+ 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<string>(); // Not really neeed
+ document.Rev = result["rev"].Value<string>();
+
+ return document;
+ }
+
+ /// <summary>
+ /// Add an attachment to an existing ICouchDocument, it may already exist in db and will then be overwritten.
+ /// </summary>
+ /// <param name="document">Couch document</param>
+ /// <param name="attachment">Binary data as string</param>
+ /// <param name="mimeType">The MIME type for the attachment.</param>
+ /// <returns>The document.</returns>
+ /// <remarks>This relies on the document to already have an id.</remarks>
+ 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<string>(); // Not really neeed
+ document.Rev = result["rev"].Value<string>();
+
+ return document;
+ }
+
+ /// <summary>
+ /// Read a ICouchDocument with an id even if it has not changed revision.
+ /// </summary>
+ /// <param name="document">Document to fill.</param>
+ public void ReadDocument(ICouchDocument document)
+ {
+ document.ReadJson(ReadDocument(document.Id));
+ }
+
+ /// <summary>
+ /// Read the attachment for an ICouchDocument.
+ /// </summary>
+ /// <param name="document">Document to read.</param>
+ public string ReadAttachment(ICouchDocument document)
+ {
+ return ReadAttachment(document.Id);
+ }
+
+ /// <summary>
+ /// First use HEAD to see if it has indeed changed.
+ /// </summary>
+ /// <param name="document">Document to fill.</param>
+ public void FetchDocumentIfChanged(ICouchDocument document)
+ {
+ if (HasDocumentChanged(document))
+ {
+ ReadDocument(document);
+ }
+ }
+
+ /// <summary>
+ /// 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.
+ /// </summary>
+ /// <param name="document">Document to fill.</param>
+ public void ReadDocumentIfChanged(ICouchDocument document)
+ {
+ JObject result = Request(document.Id).Etag(document.Rev).Parse();
+ if (result == null)
+ {
+ return;
+ }
+ document.ReadJson(result);
+ }
+
+ /// <summary>
+ /// Read a couch document given an id, this method does not have enough information to do caching.
+ /// </summary>
+ /// <param name="documentId">Document identifier</param>
+ /// <returns>Document Json as JObject</returns>
+ public JObject ReadDocument(string documentId)
+ {
+ try
+ {
+ return Request(documentId).Parse();
+ }
+ catch (WebException e)
+ {
+ throw CouchException.Create("Failed to read document", e);
+ }
+ }
+
+ /// <summary>
+ /// Read a couch document given an id, this method does not have enough information to do caching.
+ /// </summary>
+ /// <param name="documentId">Document identifier</param>
+ /// <returns>Document Json as string</returns>
+ public string ReadDocumentString(string documentId)
+ {
+ try
+ {
+ return Request(documentId).String();
+ }
+ catch (WebException e)
+ {
+ throw CouchException.Create("Failed to read document: " + e.Message, e);
+ }
+ }
+
+ /// <summary>
+ /// Read a couch attachment given a document id, this method does not have enough information to do caching.
+ /// </summary>
+ /// <param name="documentId">Document identifier</param>
+ /// <returns>Document attachment</returns>
+ public string ReadAttachment(string documentId)
+ {
+ try
+ {
+ return Request(documentId + "/attachment").String();
+ }
+ catch (WebException e)
+ {
+ throw CouchException.Create("Failed to read document: " + e.Message, e);
+ }
+ }
+
+ /// <summary>
+ /// Create a CouchDocument given JSON as a string. Uses POST and CouchDB will allocate a new id.
+ /// </summary>
+ /// <param name="json">Json data to store.</param>
+ /// <returns>Couch document with data, id and rev set.</returns>
+ /// <remarks>POST which may be problematic in some environments.</remarks>
+ public CouchJsonDocument CreateDocument(string json)
+ {
+ return (CouchJsonDocument) CreateDocument(new CouchJsonDocument(json));
+ }
+
+ /// <summary>
+ /// Create a given ICouchDocument in CouchDB. Uses POST and CouchDB will allocate a new id and overwrite any existing id.
+ /// </summary>
+ /// <param name="document">Document to store.</param>
+ /// <returns>Document with Id and Rev set.</returns>
+ /// <remarks>POST which may be problematic in some environments.</remarks>
+ 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<string>();
+ document.Rev = result["rev"].Value<string>();
+ return document;
+ }
+ catch (WebException e)
+ {
+ throw CouchException.Create("Failed to create document", e);
+ }
+ }
+
+ /// <summary>
+ /// Create or update a list of ICouchDocuments in CouchDB. Uses POST and CouchDB will
+ /// allocate new ids if the documents lack them.
+ /// </summary>
+ /// <param name="documents">List of documents to store.</param>
+ /// <remarks>POST may be problematic in some environments.</remarks>
+ public void SaveDocuments(IList<ICouchDocument> 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
+ <JArray>();
+ for (int i = 0; i < documents.Count; i++)
+ {
+ documents[i].Id = (result[i])["id"].Value<string>();
+ documents[i].Rev = (result[i])["rev"].Value<string>();
+ }
+ }
+ catch (WebException e)
+ {
+ throw CouchException.Create("Failed to create bulk documents", e);
+ }
+ }
+
+ /// <summary>
+ /// Create or updates documents in bulk fashion, chunk wise. Optionally access given view
+ /// after each chunk to trigger reindexing.
+ /// </summary>
+ /// <param name="documents">List of documents to store.</param>
+ /// <param name="chunkCount">Number of documents to store per "POST"</param>
+ /// <param name="views">List of views to touch per chunk.</param>
+ public void SaveDocuments(IList<ICouchDocument> documents, int chunkCount, List<CouchViewDefinition> views, bool allOrNothing)
+ {
+ var chunk = new List<ICouchDocument>(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<CouchPermanentViewResult>(designDocumentName, viewName, ""))
+ {
+ Name = "View access in background", Priority = ThreadPriority.BelowNormal
+ }).Start(); */
+
+ chunk = new List<ICouchDocument>(chunkCount);
+ }
+ counter++;
+ chunk.Add(doc);
+ }
+
+ SaveDocuments(chunk, allOrNothing);
+ TouchViews(views);
+ }
+
+ public void TouchViews(List<CouchViewDefinition> 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");
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Create documents in bulk fashion, chunk wise.
+ /// </summary>
+ /// <param name="documents">List of documents to store.</param>
+ /// <param name="chunkCount">Number of documents to store per "POST"</param>
+ public void SaveDocuments(IList<ICouchDocument> documents, int chunkCount, bool allOrNothing)
+ {
+ SaveDocuments(documents, chunkCount, null, allOrNothing);
+ }
+
+ /// <summary>
+ /// Get multiple documents.
+ /// </summary>
+ /// <param name="documentIds">List of documents to get.</param>
+ public IList<T> GetDocuments<T>(IList<string> documentIds) where T : ICouchDocument, new()
+ {
+ return GetDocuments<T>(documentIds.ToArray());
+ }
+
+ public IList<CouchJsonDocument> GetDocuments(IList<string> documentIds)
+ {
+ return GetDocuments<CouchJsonDocument>(documentIds);
+ }
+
+ public IList<CouchJsonDocument> GetDocuments(string[] documentIds)
+ {
+ return GetDocuments<CouchJsonDocument>(documentIds);
+ }
+
+ public IList<T> GetDocuments<T>(string[] documentIds) where T : ICouchDocument, new()
+ {
+ var bulk = new CouchBulkKeys(documentIds);
+ return QueryAllDocuments().Data(CouchDocument.WriteJson(bulk)).IncludeDocuments().GetResult().Documents<T>();
+ }
+
+ public T GetDocument<T>(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<string>(); // Not really neeed
+ document.Rev = result["rev"].Value<string>();
+ 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");
+ }
+
+ /// <summary>
+ /// Delete documents in bulk fashion.
+ /// </summary>
+ /// <param name="documents">List of documents to delete.</param>
+ public void DeleteDocuments(IList<ICouchDocument> documents)
+ {
+ DeleteDocuments(documents.ToArray());
+ }
+
+ /// <summary>
+ /// 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.
+ /// </summary>
+ public void DeleteDocuments(string startKey, string endKey)
+ {
+ IList<CouchQueryDocument> docs = QueryAllDocuments().StartKey(startKey).EndKey(endKey).GetResult().RowDocuments();
+ DeleteDocuments(docs.ToArray());
+ }
+
+ /// <summary>
+ /// Delete documents in bulk fashion.
+ /// </summary>
+ /// <param name="documents">Array of documents to delete.</param>
+ public void DeleteDocuments(ICouchDocument[] documents)
+ {
+ DeleteDocuments(new CouchBulkDeleteDocuments(documents));
+ }
+
+ /// <summary>
+ /// Delete documents in bulk fashion.
+ /// </summary>
+ public void DeleteDocuments(ICanJson bulk)
+ {
+ try
+ {
+ var result = Request("_bulk_docs").Data(CouchDocument.WriteJson(bulk)).PostJson().Parse<JArray>();
+ for (int i = 0; i < result.Count(); i++)
+ {
+ //documents[i].id = (result[i])["id"].Value<string>();
+ //documents[i].rev = (result[i])["rev"].Value<string>();
+ 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<string>(), (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;
+ }
+ }
+ }
+}
114 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
+{
+ /// <summary>
+ /// 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
+ /// </summary>
+ 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<string, JToken> doc)
+ : this(doc["_id"].Value<string>(), doc["_rev"].Value<string>())
+ {
+ }
+
+ #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<string>();
+ doc.Rev = obj["_rev"].Value<string>();
+ }
+
+ 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;
+ }
+ }
+ }
+}
62 src/CouchException.cs
@@ -0,0 +1,62 @@
+using System;
+using System.Globalization;
+using System.Net;
+using System.Runtime.Serialization;
+
+namespace Divan
+{
+ /// <summary>
+ /// All Exceptions thrown inside Divan uses this class, MOST of these wrap a WebException
+ /// and we extract the HttpStatusCode to make it easily accessible.
+ /// </summary>
+ [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);
+ }
+ }
+}
94 src/CouchGenericViewResult.cs
@@ -0,0 +1,94 @@
+using System.Collections.Generic;
+using Newtonsoft.Json.Linq;
+
+namespace Divan
+{
+ /// <summary>
+ /// 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.
+ /// </summary>
+ public class CouchGenericViewResult : CouchViewResult
+ {
+ /// <summary>
+ /// Return all found values as documents of given type
+ /// </summary>
+ /// <typeparam name="T">Type of value.</typeparam>
+ /// <returns>All found values.</returns>
+ public IList<T> ValueDocuments<T>() where T : ICanJson, new()
+ {
+ return RetrieveDocuments<T>("value");
+ }
+
+ /// <summary>
+ /// Return first value found as document of given type.
+ /// </summary>
+ /// <typeparam name="T">Type of value</typeparam>
+ /// <returns>First value found or null if not found.</returns>
+ public T ValueDocument<T>() where T : ICanJson, new()
+ {
+ return RetrieveDocument<T>("value");
+ }
+
+ /// <summary>
+ /// Return all found docs as documents of given type
+ /// </summary>
+ /// <typeparam name="T">Type of documents.</typeparam>
+ /// <returns>List of documents found.</returns>
+ public IList<T> Documents<T>() where T : ICouchDocument, new()
+ {
+ return RetrieveDocuments<T>("doc");
+ }
+
+ /// <summary>
+ /// Return first document found as document of given type
+ /// </summary>
+ /// <typeparam name="T">Type of document</typeparam>
+ /// <returns>First document found or null if not found.</returns>
+ public T Document<T>() where T : ICouchDocument, new()
+ {
+ return RetrieveDocument<T>("doc");
+ }
+
+ protected virtual IList<T> RetrieveDocuments<T>(string docOrValue) where T : ICanJson, new()
+ {
+ var list = new List<T>();
+ foreach (JToken row in Rows())
+ {
+ var doc = new T();
+ doc.ReadJson(row[docOrValue].Value<JObject>());
+ list.Add(doc);
+ }
+ return list;
+ }
+
+ protected virtual T RetrieveDocument<T>(string docOrValue) where T : ICanJson, new()
+ {
+ foreach (JToken row in Rows())
+ {
+ var doc = new T();
+ doc.ReadJson(row[docOrValue].Value<JObject>());
+ return doc;
+ }
+ return default(T);
+ }
+
+ public IList<CouchQueryDocument> RowDocuments()
+ {
+ return RowDocuments<CouchQueryDocument>();
+ }
+
+ public IList<T> RowDocuments<T>() where T : ICanJson, new()
+ {
+ var list = new List<T>();
+ foreach (JObject row in Rows())
+ {
+ var doc = new T();
+ doc.ReadJson(row);
+ list.Add(doc);
+ }
+ return list;
+ }
+ }
+}
86 src/CouchJSONDocument.cs
@@ -0,0 +1,86 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Divan
+{
+ /// <summary>
+ /// 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.
+ /// </summary>
+ 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<string>();
+ }
+ set { Obj["_rev"] = JToken.FromObject(value); }
+ }
+ public string Id
+ {
+ get
+ {
+ if (Obj["_id"] == null)
+ {
+ return null;
+ }
+ return Obj["_id"].Value<string>();
+ }
+ set { Obj["_id"] = JToken.FromObject(value); }
+ }
+
+ #endregion
+ }
+}
14 src/CouchNotFoundException.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace Divan
+{
+ /// <summary>
+ /// Represents a HttpStatusCode of 404, document not found.
+ /// </summary>
+ public class CouchNotFoundException : Exception
+ {
+ public CouchNotFoundException(string msg, Exception e) : base(msg, e)
+ {
+ }
+ }
+}
9 src/CouchPermanentViewResult.cs
@@ -0,0 +1,9 @@
+namespace Divan
+{
+ /// <summary>
+ /// This is a view result from a CouchQuery on a permanent CouchDB view.
+ /// </summary>
+ public class CouchPermanentViewResult : CouchViewResult
+ {
+ }
+}
212 src/CouchQuery.cs
@@ -0,0 +1,212 @@
+using System.Collections.Generic;
+using Newtonsoft.Json.Linq;
+
+namespace Divan
+{
+ /// <summary>
+ /// 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.
+ /// </summary>
+ public class CouchQuery
+ {
+ public readonly CouchViewDefinition View;
+
+ // Special options
+ public bool checkETagUsingHead;
+ public Dictionary<string, string> Options = new Dictionary<string, string>();
+ public string postData;
+ public CouchViewResult Result;
+
+ public CouchQuery(CouchViewDefinition view)
+ {
+ View = view;
+ }
+
+ public void ClearOptions()
+ {
+ Options = new Dictionary<string, string>();
+ }
+
+ 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;
+ }
+
+ /// <summary>
+ /// 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.
+ /// </summary>
+ public CouchQuery CheckETagUsingHead()
+ {
+ checkETagUsingHead = true;
+ return this;
+ }
+
+ public CouchGenericViewResult GetResult()
+ {
+ return GetResult<CouchGenericViewResult>();
+ }
+
+ 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<T>() 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;
+ }
+ }
+}
19 src/CouchQueryDocument.cs
@@ -0,0 +1,19 @@
+using Newtonsoft.Json.Linq;
+
+namespace Divan
+{
+ /// <summary>
+ /// This is used to hold only metadata about a document retrieved from view queries.
+ /// </summary>
+ public class CouchQueryDocument : CouchDocument
+ {
+ public string Key { get; set; }
+
+ public override void ReadJson(JObject obj)
+ {
+ Id = obj["id"].Value<string>();
+ Key = obj["key"].Value<string>();
+ Rev = (obj["value"].Value<JObject>())["rev"].Value<string>();
+ }
+ }
+}
291 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
+{
+ /// <summary>
+ /// A CouchDB HTTP request with all its options. This is where we do the actual HTTP requests to CouchDB.
+ /// </summary>
+ public class CouchRequest
+ {
+ private readonly CouchDatabase db;
+ private readonly CouchServer server;
+ private string etag, etagToCheck;
+ public Dictionary<string, string> headers = new Dictionary<string, string>();
+
+ // 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<KeyValuePair<string, string>> 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<T>() 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<bool>())
+ {
+ 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<JObject>();
+ }
+
+ public T Parse<T>() 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;
+ }
+ }
+
+ /// <summary>
+ /// Return the request as a plain string instead of trying to parse it.
+ /// </summary>
+ 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;
+ }
+ }
+}
180 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
+{
+ /// <summary>
+ /// 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.
+ /// </summary>
+ 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;
+ }
+ }
+
+ /// <summary>
+ /// Get a CouchDatabase with given name. We create
+ /// the database if needed.
+ /// </summary>
+ public CouchDatabase GetDatabase(string name)
+ {
+ var db = new CouchDatabase(name, this);
+ db.Create();
+ return db;
+ }
+
+ /// <summary>
+ /// Get specialized subclass of CouchDatabase with given name.
+ /// We check if the database exists and delete it if it does,
+ /// then we recreate it.
+ /// </summary>
+ public CouchDatabase GetNewDatabase(string name)
+ {
+ var db = new CouchDatabase(name, this);
+ if (db.Exists())
+ {
+ db.Delete();
+ }
+ db.Create();
+ return db;
+ }
+
+ /// <summary>
+ /// Get specialized subclass of CouchDatabase. That class should
+ /// define its own database name. We presume it is already created.
+ /// </summary>
+ public T GetExistingDatabase<T>() where T : CouchDatabase, new()
+ {
+ return new T {Server = this};
+ }
+
+ /// <summary>
+ /// Get specialized subclass of CouchDatabase with given name.
+ /// We presume it is already created.
+ /// </summary>
+ public T GetExistingDatabase<T>(string name) where T : CouchDatabase, new()
+ {
+ return new T {Name = name, Server = this};
+ }
+
+ /// <summary>
+ /// Get specialized subclass of CouchDatabase. That class should
+ /// define its own database name. We ensure that it is created.
+ /// </summary>
+ public T GetDatabase<T>() where T : CouchDatabase, new()
+ {
+ var db = GetExistingDatabase<T>();
+ db.Create();
+ return db;
+ }
+
+ /// <summary>
+ /// Get specialized subclass of CouchDatabase with given name.
+ /// We create the database if needed.
+ /// </summary>
+ public T GetDatabase<T>(string name) where T : CouchDatabase, new()
+ {
+ var db = GetExistingDatabase<T>(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<string> GetDatabaseNames()
+ {
+ return (List<string>) serializer.Deserialize(new JsonTextReader(new StringReader(Request().Path("_all_dbs").String())), typeof (List<string>));
+ }
+ }
+}
252 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
+{
+ /// <summary>
+ /// Unit tests for Divan. Operates in a separate CouchDB database called divan_unit_tests.
+ /// </summary>
+ [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<ICouchDocument> list = new List<ICouchDocument> {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<string> {doc1.Id, doc2.Id};
+ IList<CouchJsonDocument> 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<CouchJsonDocument>("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);