In this recipe, we are going to look at a non-SQL-oriented solution. There are times when you might not need a full blown SQL server backend or you might just not have access to one. In those times, storing the data on the filesystem might be more appropriate. For that reason we will take a look at how to remove SQL server from the picture and plug in an XML data store in its place. We will see that even with this drastically different infrastructure on the backend, we can still work within the boundaries of our existing solution and easily plug in this new data store.
The only thing we need to do to get started with this recipe is prepare our new data storage. We want to swap out our existing SQL server database and make an XML data store. To do this, all we need to do is create an XML folder inside of the App_Data
folder. This will be the location we will write our XML documents to.
With that out of the way, we next need to discuss how these XML documents will look. In this recipe, we will write one XML file per Post
. This Post
document will contain a collection of Comments
inside of it. The reason for this is that a Post
is important on its own. A Comment
by itself means nothing without its parent Post
. When talking about document stores, especially where the item being stored is as simple as our Comment
, we can store our value types (Comment
) with our entities (Post
) directly.
Working with .NET like we are, creating XML directly from our objects is as easy as serializing them directly to the filesystem. When we need to read the XML back into our program, we simply reverse the process and de-serialize our file back into an object. In order to locate the appropriate post, we will use the Post's Id, which is a Guid
as the filename.
Keep in mind that the system that we are creating in this recipe is not an absolute best practice. It is prone to several issues, such as file locking and write-access restrictions. Also, finding a Post
is easy enough, as we named the file with the Post's identifier. But locating other data, such as a specific Comment
, becomes problematic (we will look at this shortly). Having said that, this solution may be just the one you need! Just make sure you weigh the pros and cons prior to choosing the filesystem for your data storage mechanism.
App_Data
folder. Create a new folder inside there called XML.
ConfigurationService
class called GetXmlConnectionString.
This method will determine the file path to the root of our application. It will then append App_DataXml
to that path, so that we can get to our XML store.Models/ConfigurationService.cs:
... public string GetXmlConnectionString() { string path = HttpContext.Current .Request.PhysicalApplicationPath + "App_Data\Xml\"; return path; } ...
Connection
object. Instead we need something to manage access to the filesystem. When dealing with files, we don't want more than one thread accessing a file at a time (as that will throw an exception). In that case, we need to create a class that controls the number of allowed instances that are created across our entire application. We will use the singleton pattern to do this. Create a new class called XmlStore
in a new Models/Repository/XmlRepository
directory. Create a private constructor to ensure that no instances can be created directly. Also, create a static property to control the instance that is initially created and create a lock to allow us to check and create our instance.Models/Repository/XmlRepository/XmlStore.cs:
public sealed class XmlStore { static XmlStore instance = new XmlStore(); static readonly object padlock = new object(); XmlStore() { } public static XmlStore Instance { get { lock (padlock) { return instance; } } } }
Delete
method, which will take the ID of the Post
to be deleted.Models/Repository/XmlRepository/XmlStore.cs:
... public void Delete(Guid postId) { string file = new ConfigurationService().GetXmlConnectionString() + postId + ".xml"; if (File.Exists(file)) { File.Delete(file); } } ...
Write
method. This method will create a serializer to handle our Post
object. Then we will attempt to serialize the object to the filesystem using a TextWriter.Models/Repository/XmlRepository/XmlStore.cs:
public void Write(Post post) { string file = new ConfigurationService().GetXmlConnectionString() + post.PostID + ".xml"; // Force Domain Model to check for comments first. var comments = post.Comments; var s = new XmlSerializer(typeof(Post)); var fs = new FileStream(file, FileMode.Create); TextWriter w = new StreamWriter(fs, new UTF8Encoding()); try { s.Serialize(w, post); } catch (Exception e) { Console.WriteLine(e.Message); } finally { w.Close(); }}
Read
method is basically Write
in reverse. We create a FileStream
to consume the XML file and an XmlSerializer
to deserialize the resulting stream into our Post
object.Models/Repository/XmlRepository/XmlStore.cs:
public Post Read(Guid id) { var file = new ConfigurationService().GetXmlConnectionString() + id.ToString() + ".xml"; // Assume Comments precache if (!File.Exists(file)) return new Post { PostID = id, Comments = new List<Comment>() }; var stream = new FileStream(file, FileMode.Open); Post result; try { result = (Post)new XmlSerializer(typeof(Post)).Deserialize(stream); } catch (Exception e) { result = new Post(); Console.WriteLine(e.Message); } finally { stream.Close(); stream.Dispose(); stream = null; } return result; }
ReadAll
method will simply iterate through all the files in the directory and reconstitute each of them as Post
objects.Models/Repository/XmlRepository/XmlStore.cs:
public List<Post> ReadAll() { List<Post> result = new List<Post>(); string path = new ConfigurationService().GetXmlConnectionString(); DirectoryInfo di = new DirectoryInfo(path); FileInfo[] rgFiles = di.GetFiles("*.xml"); foreach (FileInfo fi in rgFiles) { result.Add(Read(Guid.Parse(fi.Name.Split('.')[0]))); } return result; }
Lucene
index, which would allow us to search for a CommentID
and map it to our PostID
, thereby allowing us to load the specific file that contains the comment we are interested in. But as this is a simple demo, we will instead need to iterate through each Post
and search the collection of comments within the Post
. If we find the comment, we then return it.Models/Repository/XmlRepository/XmlStore.cs:
public Comment SearchForComment(Guid commentId) { Comment result = null; string path = new ConfigurationService().GetXmlConnectionString(); DirectoryInfo di = new DirectoryInfo(path); FileInfo[] rgFiles = di.GetFiles("*.xml"); foreach (FileInfo fi in rgFiles) { Post post = Read(Guid.Parse(fi.Name.Split('.')[0])); result = post.Comments .Where(c => c.CommentID == commentId).FirstOrDefault(); if (result != null) return result; } return result; }
Post
and Comment
repositories. Start by creating the XmlPostRepository
and XmlCommentRepository
classes. Next, configure those classes to implement the IPostRepository
and ICommentRepository
interfaces. XmlRepositoryConfiguration
file and set it to implement the IRepositoryConfiguration
interface. Then generate the appropriate methods by making the XmlRepositoryConfiguration
class implement the interface. XmlRepository
, we now need to configure StructureMap, so that it is aware of our new repository. Do this by creating a new class called XmlRepositoryRegistry
class in the Models/StructureMap
directory. In this class, we will configure all the mappings that are needed for our host application.Models/StructureMap/XmlRepositoryRegistry.cs:
public class XmlRepositoryRegistry : Registry { public XmlRepositoryRegistry() { For<ICommentRepository>().Use<XmlCommentRepository>(); For<IPostRepository>().Use<XmlPostRepository>(); For<IRepositoryConfiguration>() .Use<XmlRepositoryConfiguration>(); } public static void Register() { ObjectFactory.Initialize(x => x.AddRegistry( new XmlRepositoryRegistry())); } }
Global.asax.cs:
protected void Application_Start() { AreaRegistration.RegisterAllAreas(); RegisterRoutes(RouteTable.Routes); XmlRepositoryRegistry.Register(); //run any data access configuration that might be needed ObjectFactory.GetInstance<IRepositoryConfiguration>() .Configure(); }
XmlStore
, the actual code in our repository classes is on the light side.Models/Repository/XmlRepository/XmlPostRepository.cs:
public class XmlPostRepository : IPostRepository { private XmlStore _xmlStore; public XmlPostRepository() { _xmlStore = XmlStore.Instance; } public void AddPost(Post post) { post.PostID = Guid.NewGuid(); post.CreateDate = DateTime.Now; _xmlStore.Write(post); } public void UpdatePost(Post post) { _xmlStore.Write(post); } public void DeletePost(Post post) { _xmlStore.Delete(post.PostID); } public Post GetPost(Guid postId) { return _xmlStore.Read(postId); ; } public List<Post> GetPosts() { return _xmlStore.ReadAll(); } }
XmlCommentRepository
. Oddly enough, there is more actual work to do in the comment repository, as we need to find the right post file to perform the appropriate actions on a comment.Models/Repository/XmlRepository/XmlCommentRepository.cs:
public class XmlCommentRepository : ICommentRepository { private IPostRepository _postRepository; private XmlStore _xmlStore; public XmlCommentRepository(IPostRepository postRepository) { _postRepository = postRepository; _xmlStore = XmlStore.Instance; } public void AddComment(Comment comment) { Post post = _postRepository.GetPost(comment.PostID); List<Comment> comments = post.Comments; comments.Add(comment); post.Comments = comments; _postRepository.UpdatePost(post); } public void DeleteComment(Comment comment) { Post post = _postRepository.GetPost(comment.PostID); List<Comment> comments = post.Comments; comments = comments.Where(c => c.CommentID != comment.CommentID).ToList(); post.Comments = comments; _postRepository.UpdatePost(post); } public Comment GetComment(Guid commentId) { return _xmlStore.SearchForComment(commentId); } public List<Comment> GetCommentsByPostID(Guid postId) { Post post = _postRepository.GetPost(postId); if (post != null) return post.Comments; return new List<Comment>(); } public void DeleteComments(Guid postId) { Post post = _postRepository.GetPost(postId); post.Comments = new List<Comment>(); _postRepository.UpdatePost(post); } }
DeleteComment
view exposing the PostId
in the form post.Views/Home/DeleteComment.aspx:
... <% using (Html.BeginForm()) { %> <p> <%= Html.HiddenFor(m=>m.PostID) %> <input type="submit" value="Delete" /> | <%: Html.ActionLink("Back to List", "Index") %> </p> <% } %> ...
Controllers/HomeController.cs:
[HttpPost] public ActionResult DeleteComment(Guid id, Guid postId) { ObjectFactory.GetInstance<ICommentRepository>(). DeleteComment(new Comment { CommentID = id, PostID = postId }); return RedirectToAction("Index"); }