
During a consulting by a customer, which commissioned us the development of an e-commerce platform, we were asked if and how it was possible to allow users to customize their own interface and modify pages styles and themes, and to save them in a data source, maybe with a version number, in order to allow file recovery.
For this reason, we compiled a list of different problems to deal with:
- versioning: define a files versioning mode, perhaps considering not definitive versions too (draft);
- editing inline: files modification through syntax coloring, if possible;
- comparing: comparison of files with previous versions;
- provisioning: define a files recovery mode.
The e.commerce platform was based on the framework ASP.NET MVC 5, then we search for solutions in the .NET environment.
For the file editing, we opted for Monaco Editor, a tool that powers Visual Studio Code too: it’s supported by main browsers and supplies some supports, as the text coloring based on the used language, the intellisense and the files diff.
Monaco Editor is easy to install and to configure, since it needs the link to a JS library and, thanks to documented API (API Monaco Editor), it is possible to obtain an instance ready to be used. As example, the instruction:
monaco.editor.create(document.getElementById("container"), {
value: "function hello() {\n\t alert('Hello world!');\n}",
language: "javascript"
});
on the page:
<div id="container" style="height:100%;"></div>
generates the screen:

In our case, we used the editor both to modify and to compare different files versions.
To define those files, that can be modified, we created the class EditableContent:
public class EditableContent
{
[Key]
public int Id { get; set; }
[Required]
public string Path { get; set; }
public string Type { get; set; }
[DefaultValue(false)]
public bool Disabled { get; set; }
}
The attribute Type is used to rightly display the file on Monaco Editor, since, as I previously wrote, it is possible to define the file type, in order to obtain the text coloring and the error reporting.
For already modified files, we have the class StoredContent instead:
public class StoredContent
{
[Key]
public int Id { get; set; }
[Required]
public string Path { get; set; }
[Required]
public string Content { get; set; }
[Required]
public int Version { get; set; }
[DefaultValue(false)]
public bool Draft { get; set; }
}
In StoredContent both the version number and the draft attribute are present. This last indicates if we are dealing with a definitive version.
For CRUD (Create, Read, Update e Delete) operations of files, we created the API controller ContentsController, that has following methods:
- IEnumerable<string> GetEditableContents(): it is used to obtain the list of adjustable contents;
- IEnumerable<StoredContentVersionModel> GetContentAvailableVersions(BaseContentModel baseContent): it obtains the list of available versions for the specified path;
- string GetContent(StoredContentModel storedContent): it obtains the file content starting from the path and the version number;
- StoredContentModel GetLatestContent(BaseContentModel baseContent): it obtains the file content starting from path and relative to the last version number available;
- int CreateStoredContent(StoredContentModel storedContent): it creates a new file version;
- int RecoverStoredContentVersion([FromBody] StoredContentModel storedContent): it recovers a previously saved version and imports it as the last one.
BaseContentModel, StoredContentModel, StoredContentVersionModel are viewmodel, used for the data exchange with the views, and they are defined as:
public class BaseContentModel
{
public string Path { get; set; }
}
public class StoredContentVersionModel
{
public int Version { get; set; }
public bool Draft { get; set; }
}
public class StoredContentModel : BaseContentModel
{
public string Content { get; set; }
public string Type { get; set; }
public bool? Draft { get; set; }
public int Version { get; set; }
}
In particular, the implementation has some points to be defined better, as, for example, GetLatestContent:
public StoredContentModel GetLatestContent([FromBody] BaseContentModel baseContent)
{
var storedContent = _context.StoredContents
.Where(sc => sc.Path == baseContent.Path)
.OrderByDescending(sc => sc.Version).FirstOrDefault();
if (storedContent == null)
{
var webRoot = _env.WebRootPath;
var file = System.IO.Path.Combine(webRoot, baseContent.Path);
storedContent = new StoredContent()
{
Path = baseContent.Path,
Content = System.IO.File.ReadAllText(file, System.Text.Encoding.Default),
Version = 0,
Draft = false
};
_context.StoredContents.Add(storedContent);
_context.SaveChanges();
}
return new StoredContentModel()
{
[...]
};
}
When there’s no corresponding file on the db, a version 0 is created and it contains the content of the file on the file system.
GetLatestContent is invoked by an Ajax call through jquery:
function loadPage(path) {
$.ajax({
url: "/api/contents/GetLatestContent",
type: "POST",
data: JSON.stringify({Path: path }),
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function (data) {
setMonacoEditor(data.content, data.type, data.path);
}
})
}
where setMonacoEditor() launches the editor in reading mode and modifies the single file:
function setMonacoEditor(text, mode, path) {
if (!editor) {
$('#editor').empty();
editor = monaco.editor.create(document.getElementById('editor'), {
model: null,
automaticLayout: true
});
}
var oldModel = editor.getModel();
var newModel = monaco.editor.createModel(text, mode);
editor.setModel(newModel);
if (oldModel) {
oldModel.dispose();
}
//[...]
}
To save the new content, there’s the method CreateStoredContent which creates a new file version:
public int CreateStoredContent([FromBody] StoredContentModel storedContent)
{
var storedContentToCreate = new StoredContent()
{
Content = storedContent.Content,
Path = storedContent.Path,
Draft = storedContent.Draft.GetValueOrDefault(false)
};
var latestStoredContent = _context.StoredContents
.Where(sc => sc.Path == storedContent.Path)
.OrderByDescending(sc => sc.Version).FirstOrDefault();
if (latestStoredContent == null){
storedContentToCreate.Version = 1;
}
else if (latestStoredContent.Draft){
storedContentToCreate.Version = latestStoredContent.Version;
}
else {
storedContentToCreate.Version = ++latestStoredContent.Version;
}
_context.StoredContents.Add(storedContentToCreate);
_context.SaveChanges();
return storedContentToCreate.Id;
}
The management of the version number, depending on the Draft flag set, is to be noted. If the last version present on the db is a draft, then the version number doesn’t increment, on the contrary it depends on the draft flag passed to the service.
In our example, in which we set the file site.css as adjustable, the windows appears as follows:

For the diff of files, we recall another ContentsController method, GetContent, and we pass the path and number version in the body:
public string GetContent([FromBody] StoredContentModel storedContent)
{
var _storedContent = _context.StoredContents.FirstOrDefault(sc => sc.Path == storedContent.Path && sc.Version == storedContent.Version);
if (_storedContent != null)
return _storedContent.Content;
else
return "";
}
On the front-end side, we find the javascript method loadPageDiff:
function loadPageDiff(path, newversion, oldversion, mode) {
var onError = function () {
$('.loading.diff-editor').fadeOut({ duration: 200 });
$('#diff-editor').append('<p class="alert alert-error">Failed to load diff editor sample</p>');
};
$('.loading.diff-editor').show();
var lhsData = null, rhsData = null, jsMode = null;
$.ajax({
url: "/api/contents/GetContent",
type: "POST",
data: JSON.stringify({ Path: path, Version: newversion }),
contentType: "application/json; charset=utf-8",
dataType: "text",
success: function (data) {
lhsData = data;
onProgress();
}
})
$.ajax({
url: "/api/contents/GetContent",
type: "POST",
data: JSON.stringify({ Path: path, Version: oldversion }),
contentType: "application/json; charset=utf-8",
dataType: "text",
success: function (data) {
rhsData = data;
onProgress();
}
})
function onProgress() {
// set diff environment
}
}
that recalls GetContent two times and positions the versions in tiled windows, in order to allow user to verify modifications made to the file and to check them with previous versions.
The recovery functionality is guaranteed by the method RecoverStoredContentVersion
public int RecoverStoredContentVersion([FromBody] StoredContentModel storedContent)
{
var versionedStoredContent = _context.StoredContents
.Where(sc => sc.Path == storedContent.Path && sc.Version == storedContent.Version)
.FirstOrDefault();
var latestStoredContentVersion = _context.StoredContents
.Where(sc => sc.Path == storedContent.Path)
.Max(sc => sc.Version);
var storedContentToCreate = new StoredContent()
{
Path = storedContent.Path,
Content = versionedStoredContent.Content,
Version = ++latestStoredContentVersion,
Draft = false
};
_context.StoredContents.Add(storedContentToCreate);
_context.SaveChanges();
return storedContentToCreate.Id;
}
In our example, the diff window is shown as follows:

Files recovery is not the subject of this article, but we would like to introduce two important classes. On .NET Framework you can find the VirtualPathProvider class (here), in the library System. Web. Hosting. That class describes how to create a virtual file system by several types of data and allows to define customized recovery and caching logics by overriding following methods:
- VirtualFile GetFile(string virtualPath): it allows to recover a file both from file system and from virtual path;
- bool FileExists(string virtualPath): it allows to verify if a virtual file exists;
- CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart): it manages virtual file caching;
- String GetFileHash(String virtualPath, IEnumerable virtualPathDependencies): it returns an hash of specified file.
On .NET Core you can find IfileProvider interface (here) that, as for VirtualPathProvider, defines file recovery logic from several sources, by implementing following methods:
- IDirectoryContents GetDirectoryContents(string subpath): it retirns the directory contents (folders and files);
- IFileInfo GetFileInfo(string subpath): it returns a FileInfo object (here you have to define the business logic);
- IChangeToken Watch(string filter): it checks if file has changed since latest use (here you might define the business logic in file access).
You can find an example of IFileProvider, and all the code used in this article, at this link.