Skip to content

Repository files navigation

harmony

NuGet version

A CRDT application library for C#, use it to build offline first applications.

Install

dotnet add package SIL.Harmony

It's expected that you use Harmony with the .Net IoC container (IoC intro) and with EF Core. If you're not familier with that you can take a look at the Host docs. If you're using ASP.NET Core you already have this setup for you.

Prerequisites:

Configure DbContext

EF Core needs to be told about the entities used by Harmony, for now these are just Commit, Snapshot, and ChangeEntitiy

publicclassAppDbContext:DbContext{protectedoverridevoidOnModelCreating(ModelBuildermodelBuilder){modelBuilder.UseCrdt(crdtConfig.Value);}}

Tip

SampleDbContext has a full example of how to setup the DbContext.

Register CRDT services

Harmony provides the DataModel class as the main way the application will interact with the CRDT model. You first need to register it with the IoC container.

varbuilder=Host.CreateApplicationBuilder(args);builder.Service.AddCrdtData<AppDbContext>(config =>{});

Note

the config callback passed into AddCrdtData is currently empty, we'll come back to that later.

Tip

Pay attention to the generic type when calling AddCrdtData, this will be the type of your application's DbContext.

Define CRDT objects

Now that you have the services setup, you need to define a CRDT object. Take a look the following examples

  • Word contains a reference to an Antonym Word.
  • Definition references the Word it belongs to. Notice that if the Word Reference is removed, the Definition deletes itself.
  • Example this one is special because it uses a YDoc to store the example text in a Yjs compatible format. This allows the example sentence to be edited by multiple users and have those changes merged using the yjs CRDT algorithm.

Once you have created your CRDT objects, you need to tell Harmony about them. Update the config callback passed into AddCrdtData

services.AddCrdtData<SampleDbContext>(config =>{// add the following linesconfig.ObjectTypeListBuilder.Add<Word>().Add<Definition>().Add<Example>();});

Define CRDT Changes

Now that you've defined your objects, you need to define your changes. These record user intent when making changes to objects. How detailed and specific you make your changes will directly impact how changes get merged between clients and how often users 'lose' changes that they made.

Example SetWordTextChange

publicclassSetWordTextChange(GuidentityId,stringtext):Change<Word>(entityId),ISelfNamedType<SetWordTextChange>{publicstringText{get;}=text;publicoverrideValueTask<IObjectBase>NewEntity(Commitcommit,IChangeContextcontext){returnnew(newWord(){Id=EntityId,Text=Text});}publicoverrideValueTaskApplyChange(Wordentity,IChangeContextcontext){entity.Text=Text;returnValueTask.CompletedTask;}}

This is a fairly simple change, it can either create a new Word entry, or if the entityId passed in matches an object that has previously been created, then it will just set the Text field on the Word entry matching the Id.

Note

Changes will be serialized and stored forever. Try to keep the amount of data stored as small as possible.

This change can either create, or update an object. Most changes will probably be either an update, or a create. In those cases you should inherit from EditChange<T> or CreateChange<T>.

Tip

The Sample project contain a number of reference changes which are good examples for a couple different change types. There are also a built in DeleteChange<T>

Once you have created your change types, you need to tell Harmony about them. Again update the config callback passed into AddCrdtData

services.AddCrdtData<SampleDbContext>(config =>{// add the following lineconfig.ChangeTypeListBuilder.Add<SetWordTextChange>();config.ObjectTypeListBuilder.Add<Word>().Add<Definition>().Add<Example>();});

Use change objects to author changes to CRDT objects

Either via DI, or directly from the IoC container get an instance of DataModel and call AddChange

GuidclientId= ... get a stable Guid representing the application instance
Guid objectId=Guid.NewGuid();awaitdataModel.AddChange(clientId,newSetWordTextChange(objectId,"Hello World"));varword=awaitdataModel.GetLatest<Word>(objectId);Console.WriteLine(word.Text);

Important

The ClientId should be consistent for a project per computer/device. It is used to determine what changes should be synced between clients with the assumption that each client produces changes sequentially. So if a project is on 2 different computers, each copy should have a unique client Id. If they had the same Id, then they would not sync changes properly.

How the ClientId is stored is left up to the application. In FW Lite we created a table to store the ClientId. It's generated automatically when the project is downloaded or created the first time and it should never change after that.

In case of an online web app there could be one ClientId to represent the server. However, if users can author changes offline and sync them later, then each browser would need it's own ClientId.

Warning

If you were to regenerate the ClientId for each change or on application start, that would eventually result in poor sync performance, as the sync process checks for new changes to sync per ClientId.

Usage

Queries

DataModel is the primary class for both making changes and getting data. Above you saw an example of making changes, now we'll start querying data.

Query Word objects starting with the letter "A"

DataModeldataModel;//get from IoC, probably via DIvarwordsStartingWithA=awaitdataModel.GetLatestObjects<Word>().Where(w =>w.Text.StartsWith("a")).ToArrayAsync();

Harmony uses EF Core queries under the covers, you can read more about them here.

Submitting Changes

Changes are the only way to modify CRDT data. Here's another example of a change

DataModeldataModel;GuidclientId;//get a stable Guid representing the application instancevardefinitionId=Guid.NewGuid();GuidwordId;//get the word Id this definition is related to.awaitdataModel.AddChange(clientId,newNewDefinitionChange(definitionId){WordId=wordId,Text="Hello",PartOfSpeech=partOfSpeech,Order=order});

Warning

You can modify data returned by EF Core, and issue updates and inserts yourself, but that data will be lost, and will not sync properly. Do not directly modify the tables produced by Harmony otherwise you risk losing data.

Syncing data

Syncing is primarily done using the DataModel class, however the implementation of the server side is left up to you. You can find the Lexbox implementation here. The sync works by having 2 instances of the ISyncable interface. The local one is implemented by DataModel and the remote implementation depends on your server side. The FW Lite implementation can be found here. You will need to scope the instance to the project as well as deal with authentication.

Once you have a remote representation of the ISyncable interface you just call it like this

DataModeldataModel;ISyncableremoteModel;awaitdataModel.SyncWith(remoteModel);

It's that easy. All the heavy lifting is done by the interface which is fairly simple to implement.

Development

SemVer commit messages

NuGet package versions are calculated from a combination of tags and commit messages. First, the most recent Git tag matching the pattern v\d+.\d+.\d+ is located. If that is the commit being built, then that version number is used. If there have been any commits since then, the version number will be bumped by looking for one of the following patterns in the commit messages:

  • +semver: major or +semver: breaking - update major version number, reset others to 0 (so 2.3.1 would become 3.0.0)
  • +semver: minor or +semver: feature - update minor version number, reset patch to 0 (so 2.3.1 would become 2.4.0)
  • Anything else, including no +semver lines at all - update patch version number (so 2.3.1 would become 2.3.2)
    • If you want to include +semver lines, then +semver: patch or +semver: fix are the standard ways to increment a patch version bump, but the patch version will be bumped regardless as long as there is at least one commit since the most recent tag.

About

C# CRDT Library for building offline first apps

Topics

Resources

Stars

13 stars

Watchers

2 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
GitHub - sillsdev/harmony: C# CRDT Library for building offline first apps · GitHub
Skip to content

Repository files navigation

harmony

NuGet version

A CRDT application library for C#, use it to build offline first applications.

Install

dotnet add package SIL.Harmony

It's expected that you use Harmony with the .Net IoC container (IoC intro) and with EF Core. If you're not familier with that you can take a look at the Host docs. If you're using ASP.NET Core you already have this setup for you.

Prerequisites:

Configure DbContext

EF Core needs to be told about the entities used by Harmony, for now these are just Commit, Snapshot, and ChangeEntitiy

publicclassAppDbContext:DbContext{protectedoverridevoidOnModelCreating(ModelBuildermodelBuilder){modelBuilder.UseCrdt(crdtConfig.Value);}}

Tip

SampleDbContext has a full example of how to setup the DbContext.

Register CRDT services

Harmony provides the DataModel class as the main way the application will interact with the CRDT model. You first need to register it with the IoC container.

varbuilder=Host.CreateApplicationBuilder(args);builder.Service.AddCrdtData<AppDbContext>(config =>{});

Note

the config callback passed into AddCrdtData is currently empty, we'll come back to that later.

Tip

Pay attention to the generic type when calling AddCrdtData, this will be the type of your application's DbContext.

Define CRDT objects

Now that you have the services setup, you need to define a CRDT object. Take a look the following examples

  • Word contains a reference to an Antonym Word.
  • Definition references the Word it belongs to. Notice that if the Word Reference is removed, the Definition deletes itself.
  • Example this one is special because it uses a YDoc to store the example text in a Yjs compatible format. This allows the example sentence to be edited by multiple users and have those changes merged using the yjs CRDT algorithm.

Once you have created your CRDT objects, you need to tell Harmony about them. Update the config callback passed into AddCrdtData

services.AddCrdtData<SampleDbContext>(config =>{// add the following linesconfig.ObjectTypeListBuilder.Add<Word>().Add<Definition>().Add<Example>();});

Define CRDT Changes

Now that you've defined your objects, you need to define your changes. These record user intent when making changes to objects. How detailed and specific you make your changes will directly impact how changes get merged between clients and how often users 'lose' changes that they made.

Example SetWordTextChange

publicclassSetWordTextChange(GuidentityId,stringtext):Change<Word>(entityId),ISelfNamedType<SetWordTextChange>{publicstringText{get;}=text;publicoverrideValueTask<IObjectBase>NewEntity(Commitcommit,IChangeContextcontext){returnnew(newWord(){Id=EntityId,Text=Text});}publicoverrideValueTaskApplyChange(Wordentity,IChangeContextcontext){entity.Text=Text;returnValueTask.CompletedTask;}}

This is a fairly simple change, it can either create a new Word entry, or if the entityId passed in matches an object that has previously been created, then it will just set the Text field on the Word entry matching the Id.

Note

Changes will be serialized and stored forever. Try to keep the amount of data stored as small as possible.

This change can either create, or update an object. Most changes will probably be either an update, or a create. In those cases you should inherit from EditChange<T> or CreateChange<T>.

Tip

The Sample project contain a number of reference changes which are good examples for a couple different change types. There are also a built in DeleteChange<T>

Once you have created your change types, you need to tell Harmony about them. Again update the config callback passed into AddCrdtData

services.AddCrdtData<SampleDbContext>(config =>{// add the following lineconfig.ChangeTypeListBuilder.Add<SetWordTextChange>();config.ObjectTypeListBuilder.Add<Word>().Add<Definition>().Add<Example>();});

Use change objects to author changes to CRDT objects

Either via DI, or directly from the IoC container get an instance of DataModel and call AddChange

GuidclientId= ... get a stable Guid representing the application instance
Guid objectId=Guid.NewGuid();awaitdataModel.AddChange(clientId,newSetWordTextChange(objectId,"Hello World"));varword=awaitdataModel.GetLatest<Word>(objectId);Console.WriteLine(word.Text);

Important

The ClientId should be consistent for a project per computer/device. It is used to determine what changes should be synced between clients with the assumption that each client produces changes sequentially. So if a project is on 2 different computers, each copy should have a unique client Id. If they had the same Id, then they would not sync changes properly.

How the ClientId is stored is left up to the application. In FW Lite we created a table to store the ClientId. It's generated automatically when the project is downloaded or created the first time and it should never change after that.

In case of an online web app there could be one ClientId to represent the server. However, if users can author changes offline and sync them later, then each browser would need it's own ClientId.

Warning

If you were to regenerate the ClientId for each change or on application start, that would eventually result in poor sync performance, as the sync process checks for new changes to sync per ClientId.

Usage

Queries

DataModel is the primary class for both making changes and getting data. Above you saw an example of making changes, now we'll start querying data.

Query Word objects starting with the letter "A"

DataModeldataModel;//get from IoC, probably via DIvarwordsStartingWithA=awaitdataModel.GetLatestObjects<Word>().Where(w =>w.Text.StartsWith("a")).ToArrayAsync();

Harmony uses EF Core queries under the covers, you can read more about them here.

Submitting Changes

Changes are the only way to modify CRDT data. Here's another example of a change

DataModeldataModel;GuidclientId;//get a stable Guid representing the application instancevardefinitionId=Guid.NewGuid();GuidwordId;//get the word Id this definition is related to.awaitdataModel.AddChange(clientId,newNewDefinitionChange(definitionId){WordId=wordId,Text="Hello",PartOfSpeech=partOfSpeech,Order=order});

Warning

You can modify data returned by EF Core, and issue updates and inserts yourself, but that data will be lost, and will not sync properly. Do not directly modify the tables produced by Harmony otherwise you risk losing data.

Syncing data

Syncing is primarily done using the DataModel class, however the implementation of the server side is left up to you. You can find the Lexbox implementation here. The sync works by having 2 instances of the ISyncable interface. The local one is implemented by DataModel and the remote implementation depends on your server side. The FW Lite implementation can be found here. You will need to scope the instance to the project as well as deal with authentication.

Once you have a remote representation of the ISyncable interface you just call it like this

DataModeldataModel;ISyncableremoteModel;awaitdataModel.SyncWith(remoteModel);

It's that easy. All the heavy lifting is done by the interface which is fairly simple to implement.

Development

SemVer commit messages

NuGet package versions are calculated from a combination of tags and commit messages. First, the most recent Git tag matching the pattern v\d+.\d+.\d+ is located. If that is the commit being built, then that version number is used. If there have been any commits since then, the version number will be bumped by looking for one of the following patterns in the commit messages:

  • +semver: major or +semver: breaking - update major version number, reset others to 0 (so 2.3.1 would become 3.0.0)
  • +semver: minor or +semver: feature - update minor version number, reset patch to 0 (so 2.3.1 would become 2.4.0)
  • Anything else, including no +semver lines at all - update patch version number (so 2.3.1 would become 2.3.2)
    • If you want to include +semver lines, then +semver: patch or +semver: fix are the standard ways to increment a patch version bump, but the patch version will be bumped regardless as long as there is at least one commit since the most recent tag.

About

C# CRDT Library for building offline first apps

Topics

Resources

Stars

13 stars

Watchers

2 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - sillsdev/harmony: C# CRDT Library for building offline first apps · GitHub
Skip to content

Repository files navigation

harmony

NuGet version

A CRDT application library for C#, use it to build offline first applications.

Install

dotnet add package SIL.Harmony

It's expected that you use Harmony with the .Net IoC container (IoC intro) and with EF Core. If you're not familier with that you can take a look at the Host docs. If you're using ASP.NET Core you already have this setup for you.

Prerequisites:

Configure DbContext

EF Core needs to be told about the entities used by Harmony, for now these are just Commit, Snapshot, and ChangeEntitiy

publicclassAppDbContext:DbContext{protectedoverridevoidOnModelCreating(ModelBuildermodelBuilder){modelBuilder.UseCrdt(crdtConfig.Value);}}

Tip

SampleDbContext has a full example of how to setup the DbContext.

Register CRDT services

Harmony provides the DataModel class as the main way the application will interact with the CRDT model. You first need to register it with the IoC container.

varbuilder=Host.CreateApplicationBuilder(args);builder.Service.AddCrdtData<AppDbContext>(config =>{});

Note

the config callback passed into AddCrdtData is currently empty, we'll come back to that later.

Tip

Pay attention to the generic type when calling AddCrdtData, this will be the type of your application's DbContext.

Define CRDT objects

Now that you have the services setup, you need to define a CRDT object. Take a look the following examples

  • Word contains a reference to an Antonym Word.
  • Definition references the Word it belongs to. Notice that if the Word Reference is removed, the Definition deletes itself.
  • Example this one is special because it uses a YDoc to store the example text in a Yjs compatible format. This allows the example sentence to be edited by multiple users and have those changes merged using the yjs CRDT algorithm.

Once you have created your CRDT objects, you need to tell Harmony about them. Update the config callback passed into AddCrdtData

services.AddCrdtData<SampleDbContext>(config =>{// add the following linesconfig.ObjectTypeListBuilder.Add<Word>().Add<Definition>().Add<Example>();});

Define CRDT Changes

Now that you've defined your objects, you need to define your changes. These record user intent when making changes to objects. How detailed and specific you make your changes will directly impact how changes get merged between clients and how often users 'lose' changes that they made.

Example SetWordTextChange

publicclassSetWordTextChange(GuidentityId,stringtext):Change<Word>(entityId),ISelfNamedType<SetWordTextChange>{publicstringText{get;}=text;publicoverrideValueTask<IObjectBase>NewEntity(Commitcommit,IChangeContextcontext){returnnew(newWord(){Id=EntityId,Text=Text});}publicoverrideValueTaskApplyChange(Wordentity,IChangeContextcontext){entity.Text=Text;returnValueTask.CompletedTask;}}

This is a fairly simple change, it can either create a new Word entry, or if the entityId passed in matches an object that has previously been created, then it will just set the Text field on the Word entry matching the Id.

Note

Changes will be serialized and stored forever. Try to keep the amount of data stored as small as possible.

This change can either create, or update an object. Most changes will probably be either an update, or a create. In those cases you should inherit from EditChange<T> or CreateChange<T>.

Tip

The Sample project contain a number of reference changes which are good examples for a couple different change types. There are also a built in DeleteChange<T>

Once you have created your change types, you need to tell Harmony about them. Again update the config callback passed into AddCrdtData

services.AddCrdtData<SampleDbContext>(config =>{// add the following lineconfig.ChangeTypeListBuilder.Add<SetWordTextChange>();config.ObjectTypeListBuilder.Add<Word>().Add<Definition>().Add<Example>();});

Use change objects to author changes to CRDT objects

Either via DI, or directly from the IoC container get an instance of DataModel and call AddChange

GuidclientId= ... get a stable Guid representing the application instance
Guid objectId=Guid.NewGuid();awaitdataModel.AddChange(clientId,newSetWordTextChange(objectId,"Hello World"));varword=awaitdataModel.GetLatest<Word>(objectId);Console.WriteLine(word.Text);

Important

The ClientId should be consistent for a project per computer/device. It is used to determine what changes should be synced between clients with the assumption that each client produces changes sequentially. So if a project is on 2 different computers, each copy should have a unique client Id. If they had the same Id, then they would not sync changes properly.

How the ClientId is stored is left up to the application. In FW Lite we created a table to store the ClientId. It's generated automatically when the project is downloaded or created the first time and it should never change after that.

In case of an online web app there could be one ClientId to represent the server. However, if users can author changes offline and sync them later, then each browser would need it's own ClientId.

Warning

If you were to regenerate the ClientId for each change or on application start, that would eventually result in poor sync performance, as the sync process checks for new changes to sync per ClientId.

Usage

Queries

DataModel is the primary class for both making changes and getting data. Above you saw an example of making changes, now we'll start querying data.

Query Word objects starting with the letter "A"

DataModeldataModel;//get from IoC, probably via DIvarwordsStartingWithA=awaitdataModel.GetLatestObjects<Word>().Where(w =>w.Text.StartsWith("a")).ToArrayAsync();

Harmony uses EF Core queries under the covers, you can read more about them here.

Submitting Changes

Changes are the only way to modify CRDT data. Here's another example of a change

DataModeldataModel;GuidclientId;//get a stable Guid representing the application instancevardefinitionId=Guid.NewGuid();GuidwordId;//get the word Id this definition is related to.awaitdataModel.AddChange(clientId,newNewDefinitionChange(definitionId){WordId=wordId,Text="Hello",PartOfSpeech=partOfSpeech,Order=order});

Warning

You can modify data returned by EF Core, and issue updates and inserts yourself, but that data will be lost, and will not sync properly. Do not directly modify the tables produced by Harmony otherwise you risk losing data.

Syncing data

Syncing is primarily done using the DataModel class, however the implementation of the server side is left up to you. You can find the Lexbox implementation here. The sync works by having 2 instances of the ISyncable interface. The local one is implemented by DataModel and the remote implementation depends on your server side. The FW Lite implementation can be found here. You will need to scope the instance to the project as well as deal with authentication.

Once you have a remote representation of the ISyncable interface you just call it like this

DataModeldataModel;ISyncableremoteModel;awaitdataModel.SyncWith(remoteModel);

It's that easy. All the heavy lifting is done by the interface which is fairly simple to implement.

Development

SemVer commit messages

NuGet package versions are calculated from a combination of tags and commit messages. First, the most recent Git tag matching the pattern v\d+.\d+.\d+ is located. If that is the commit being built, then that version number is used. If there have been any commits since then, the version number will be bumped by looking for one of the following patterns in the commit messages:

  • +semver: major or +semver: breaking - update major version number, reset others to 0 (so 2.3.1 would become 3.0.0)
  • +semver: minor or +semver: feature - update minor version number, reset patch to 0 (so 2.3.1 would become 2.4.0)
  • Anything else, including no +semver lines at all - update patch version number (so 2.3.1 would become 2.3.2)
    • If you want to include +semver lines, then +semver: patch or +semver: fix are the standard ways to increment a patch version bump, but the patch version will be bumped regardless as long as there is at least one commit since the most recent tag.

About

C# CRDT Library for building offline first apps

Topics

Resources

Stars

13 stars

Watchers

2 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - sillsdev/harmony: C# CRDT Library for building offline first apps · GitHub
Skip to content

Repository files navigation

harmony

NuGet version

A CRDT application library for C#, use it to build offline first applications.

Install

dotnet add package SIL.Harmony

It's expected that you use Harmony with the .Net IoC container (IoC intro) and with EF Core. If you're not familier with that you can take a look at the Host docs. If you're using ASP.NET Core you already have this setup for you.

Prerequisites:

Configure DbContext

EF Core needs to be told about the entities used by Harmony, for now these are just Commit, Snapshot, and ChangeEntitiy

publicclassAppDbContext:DbContext{protectedoverridevoidOnModelCreating(ModelBuildermodelBuilder){modelBuilder.UseCrdt(crdtConfig.Value);}}

Tip

SampleDbContext has a full example of how to setup the DbContext.

Register CRDT services

Harmony provides the DataModel class as the main way the application will interact with the CRDT model. You first need to register it with the IoC container.

varbuilder=Host.CreateApplicationBuilder(args);builder.Service.AddCrdtData<AppDbContext>(config =>{});

Note

the config callback passed into AddCrdtData is currently empty, we'll come back to that later.

Tip

Pay attention to the generic type when calling AddCrdtData, this will be the type of your application's DbContext.

Define CRDT objects

Now that you have the services setup, you need to define a CRDT object. Take a look the following examples

  • Word contains a reference to an Antonym Word.
  • Definition references the Word it belongs to. Notice that if the Word Reference is removed, the Definition deletes itself.
  • Example this one is special because it uses a YDoc to store the example text in a Yjs compatible format. This allows the example sentence to be edited by multiple users and have those changes merged using the yjs CRDT algorithm.

Once you have created your CRDT objects, you need to tell Harmony about them. Update the config callback passed into AddCrdtData

services.AddCrdtData<SampleDbContext>(config =>{// add the following linesconfig.ObjectTypeListBuilder.Add<Word>().Add<Definition>().Add<Example>();});

Define CRDT Changes

Now that you've defined your objects, you need to define your changes. These record user intent when making changes to objects. How detailed and specific you make your changes will directly impact how changes get merged between clients and how often users 'lose' changes that they made.

Example SetWordTextChange

publicclassSetWordTextChange(GuidentityId,stringtext):Change<Word>(entityId),ISelfNamedType<SetWordTextChange>{publicstringText{get;}=text;publicoverrideValueTask<IObjectBase>NewEntity(Commitcommit,IChangeContextcontext){returnnew(newWord(){Id=EntityId,Text=Text});}publicoverrideValueTaskApplyChange(Wordentity,IChangeContextcontext){entity.Text=Text;returnValueTask.CompletedTask;}}

This is a fairly simple change, it can either create a new Word entry, or if the entityId passed in matches an object that has previously been created, then it will just set the Text field on the Word entry matching the Id.

Note

Changes will be serialized and stored forever. Try to keep the amount of data stored as small as possible.

This change can either create, or update an object. Most changes will probably be either an update, or a create. In those cases you should inherit from EditChange<T> or CreateChange<T>.

Tip

The Sample project contain a number of reference changes which are good examples for a couple different change types. There are also a built in DeleteChange<T>

Once you have created your change types, you need to tell Harmony about them. Again update the config callback passed into AddCrdtData

services.AddCrdtData<SampleDbContext>(config =>{// add the following lineconfig.ChangeTypeListBuilder.Add<SetWordTextChange>();config.ObjectTypeListBuilder.Add<Word>().Add<Definition>().Add<Example>();});

Use change objects to author changes to CRDT objects

Either via DI, or directly from the IoC container get an instance of DataModel and call AddChange

GuidclientId= ... get a stable Guid representing the application instance
Guid objectId=Guid.NewGuid();awaitdataModel.AddChange(clientId,newSetWordTextChange(objectId,"Hello World"));varword=awaitdataModel.GetLatest<Word>(objectId);Console.WriteLine(word.Text);

Important

The ClientId should be consistent for a project per computer/device. It is used to determine what changes should be synced between clients with the assumption that each client produces changes sequentially. So if a project is on 2 different computers, each copy should have a unique client Id. If they had the same Id, then they would not sync changes properly.

How the ClientId is stored is left up to the application. In FW Lite we created a table to store the ClientId. It's generated automatically when the project is downloaded or created the first time and it should never change after that.

In case of an online web app there could be one ClientId to represent the server. However, if users can author changes offline and sync them later, then each browser would need it's own ClientId.

Warning

If you were to regenerate the ClientId for each change or on application start, that would eventually result in poor sync performance, as the sync process checks for new changes to sync per ClientId.

Usage

Queries

DataModel is the primary class for both making changes and getting data. Above you saw an example of making changes, now we'll start querying data.

Query Word objects starting with the letter "A"

DataModeldataModel;//get from IoC, probably via DIvarwordsStartingWithA=awaitdataModel.GetLatestObjects<Word>().Where(w =>w.Text.StartsWith("a")).ToArrayAsync();

Harmony uses EF Core queries under the covers, you can read more about them here.

Submitting Changes

Changes are the only way to modify CRDT data. Here's another example of a change

DataModeldataModel;GuidclientId;//get a stable Guid representing the application instancevardefinitionId=Guid.NewGuid();GuidwordId;//get the word Id this definition is related to.awaitdataModel.AddChange(clientId,newNewDefinitionChange(definitionId){WordId=wordId,Text="Hello",PartOfSpeech=partOfSpeech,Order=order});

Warning

You can modify data returned by EF Core, and issue updates and inserts yourself, but that data will be lost, and will not sync properly. Do not directly modify the tables produced by Harmony otherwise you risk losing data.

Syncing data

Syncing is primarily done using the DataModel class, however the implementation of the server side is left up to you. You can find the Lexbox implementation here. The sync works by having 2 instances of the ISyncable interface. The local one is implemented by DataModel and the remote implementation depends on your server side. The FW Lite implementation can be found here. You will need to scope the instance to the project as well as deal with authentication.

Once you have a remote representation of the ISyncable interface you just call it like this

DataModeldataModel;ISyncableremoteModel;awaitdataModel.SyncWith(remoteModel);

It's that easy. All the heavy lifting is done by the interface which is fairly simple to implement.

Development

SemVer commit messages

NuGet package versions are calculated from a combination of tags and commit messages. First, the most recent Git tag matching the pattern v\d+.\d+.\d+ is located. If that is the commit being built, then that version number is used. If there have been any commits since then, the version number will be bumped by looking for one of the following patterns in the commit messages:

  • +semver: major or +semver: breaking - update major version number, reset others to 0 (so 2.3.1 would become 3.0.0)
  • +semver: minor or +semver: feature - update minor version number, reset patch to 0 (so 2.3.1 would become 2.4.0)
  • Anything else, including no +semver lines at all - update patch version number (so 2.3.1 would become 2.3.2)
    • If you want to include +semver lines, then +semver: patch or +semver: fix are the standard ways to increment a patch version bump, but the patch version will be bumped regardless as long as there is at least one commit since the most recent tag.

About

C# CRDT Library for building offline first apps

Topics

Resources

Stars

13 stars

Watchers

2 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' GitHub - sillsdev/harmony: C# CRDT Library for building offline first apps · GitHub
Skip to content

Repository files navigation

harmony

NuGet version

A CRDT application library for C#, use it to build offline first applications.

Install

dotnet add package SIL.Harmony

It's expected that you use Harmony with the .Net IoC container (IoC intro) and with EF Core. If you're not familier with that you can take a look at the Host docs. If you're using ASP.NET Core you already have this setup for you.

Prerequisites:

Configure DbContext

EF Core needs to be told about the entities used by Harmony, for now these are just Commit, Snapshot, and ChangeEntitiy

publicclassAppDbContext:DbContext{protectedoverridevoidOnModelCreating(ModelBuildermodelBuilder){modelBuilder.UseCrdt(crdtConfig.Value);}}

Tip

SampleDbContext has a full example of how to setup the DbContext.

Register CRDT services

Harmony provides the DataModel class as the main way the application will interact with the CRDT model. You first need to register it with the IoC container.

varbuilder=Host.CreateApplicationBuilder(args);builder.Service.AddCrdtData<AppDbContext>(config =>{});

Note

the config callback passed into AddCrdtData is currently empty, we'll come back to that later.

Tip

Pay attention to the generic type when calling AddCrdtData, this will be the type of your application's DbContext.

Define CRDT objects

Now that you have the services setup, you need to define a CRDT object. Take a look the following examples

  • Word contains a reference to an Antonym Word.
  • Definition references the Word it belongs to. Notice that if the Word Reference is removed, the Definition deletes itself.
  • Example this one is special because it uses a YDoc to store the example text in a Yjs compatible format. This allows the example sentence to be edited by multiple users and have those changes merged using the yjs CRDT algorithm.

Once you have created your CRDT objects, you need to tell Harmony about them. Update the config callback passed into AddCrdtData

services.AddCrdtData<SampleDbContext>(config =>{// add the following linesconfig.ObjectTypeListBuilder.Add<Word>().Add<Definition>().Add<Example>();});

Define CRDT Changes

Now that you've defined your objects, you need to define your changes. These record user intent when making changes to objects. How detailed and specific you make your changes will directly impact how changes get merged between clients and how often users 'lose' changes that they made.

Example SetWordTextChange

publicclassSetWordTextChange(GuidentityId,stringtext):Change<Word>(entityId),ISelfNamedType<SetWordTextChange>{publicstringText{get;}=text;publicoverrideValueTask<IObjectBase>NewEntity(Commitcommit,IChangeContextcontext){returnnew(newWord(){Id=EntityId,Text=Text});}publicoverrideValueTaskApplyChange(Wordentity,IChangeContextcontext){entity.Text=Text;returnValueTask.CompletedTask;}}

This is a fairly simple change, it can either create a new Word entry, or if the entityId passed in matches an object that has previously been created, then it will just set the Text field on the Word entry matching the Id.

Note

Changes will be serialized and stored forever. Try to keep the amount of data stored as small as possible.

This change can either create, or update an object. Most changes will probably be either an update, or a create. In those cases you should inherit from EditChange<T> or CreateChange<T>.

Tip

The Sample project contain a number of reference changes which are good examples for a couple different change types. There are also a built in DeleteChange<T>

Once you have created your change types, you need to tell Harmony about them. Again update the config callback passed into AddCrdtData

services.AddCrdtData<SampleDbContext>(config =>{// add the following lineconfig.ChangeTypeListBuilder.Add<SetWordTextChange>();config.ObjectTypeListBuilder.Add<Word>().Add<Definition>().Add<Example>();});

Use change objects to author changes to CRDT objects

Either via DI, or directly from the IoC container get an instance of DataModel and call AddChange

GuidclientId= ... get a stable Guid representing the application instance
Guid objectId=Guid.NewGuid();awaitdataModel.AddChange(clientId,newSetWordTextChange(objectId,"Hello World"));varword=awaitdataModel.GetLatest<Word>(objectId);Console.WriteLine(word.Text);

Important

The ClientId should be consistent for a project per computer/device. It is used to determine what changes should be synced between clients with the assumption that each client produces changes sequentially. So if a project is on 2 different computers, each copy should have a unique client Id. If they had the same Id, then they would not sync changes properly.

How the ClientId is stored is left up to the application. In FW Lite we created a table to store the ClientId. It's generated automatically when the project is downloaded or created the first time and it should never change after that.

In case of an online web app there could be one ClientId to represent the server. However, if users can author changes offline and sync them later, then each browser would need it's own ClientId.

Warning

If you were to regenerate the ClientId for each change or on application start, that would eventually result in poor sync performance, as the sync process checks for new changes to sync per ClientId.

Usage

Queries

DataModel is the primary class for both making changes and getting data. Above you saw an example of making changes, now we'll start querying data.

Query Word objects starting with the letter "A"

DataModeldataModel;//get from IoC, probably via DIvarwordsStartingWithA=awaitdataModel.GetLatestObjects<Word>().Where(w =>w.Text.StartsWith("a")).ToArrayAsync();

Harmony uses EF Core queries under the covers, you can read more about them here.

Submitting Changes

Changes are the only way to modify CRDT data. Here's another example of a change

DataModeldataModel;GuidclientId;//get a stable Guid representing the application instancevardefinitionId=Guid.NewGuid();GuidwordId;//get the word Id this definition is related to.awaitdataModel.AddChange(clientId,newNewDefinitionChange(definitionId){WordId=wordId,Text="Hello",PartOfSpeech=partOfSpeech,Order=order});

Warning

You can modify data returned by EF Core, and issue updates and inserts yourself, but that data will be lost, and will not sync properly. Do not directly modify the tables produced by Harmony otherwise you risk losing data.

Syncing data

Syncing is primarily done using the DataModel class, however the implementation of the server side is left up to you. You can find the Lexbox implementation here. The sync works by having 2 instances of the ISyncable interface. The local one is implemented by DataModel and the remote implementation depends on your server side. The FW Lite implementation can be found here. You will need to scope the instance to the project as well as deal with authentication.

Once you have a remote representation of the ISyncable interface you just call it like this

DataModeldataModel;ISyncableremoteModel;awaitdataModel.SyncWith(remoteModel);

It's that easy. All the heavy lifting is done by the interface which is fairly simple to implement.

Development

SemVer commit messages

NuGet package versions are calculated from a combination of tags and commit messages. First, the most recent Git tag matching the pattern v\d+.\d+.\d+ is located. If that is the commit being built, then that version number is used. If there have been any commits since then, the version number will be bumped by looking for one of the following patterns in the commit messages:

  • +semver: major or +semver: breaking - update major version number, reset others to 0 (so 2.3.1 would become 3.0.0)
  • +semver: minor or +semver: feature - update minor version number, reset patch to 0 (so 2.3.1 would become 2.4.0)
  • Anything else, including no +semver lines at all - update patch version number (so 2.3.1 would become 2.3.2)
    • If you want to include +semver lines, then +semver: patch or +semver: fix are the standard ways to increment a patch version bump, but the patch version will be bumped regardless as long as there is at least one commit since the most recent tag.

About

C# CRDT Library for building offline first apps

Topics

Resources

Stars

13 stars

Watchers

2 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - sillsdev/harmony: C# CRDT Library for building offline first apps · GitHub
Skip to content

Repository files navigation

harmony

NuGet version

A CRDT application library for C#, use it to build offline first applications.

Install

dotnet add package SIL.Harmony

It's expected that you use Harmony with the .Net IoC container (IoC intro) and with EF Core. If you're not familier with that you can take a look at the Host docs. If you're using ASP.NET Core you already have this setup for you.

Prerequisites:

Configure DbContext

EF Core needs to be told about the entities used by Harmony, for now these are just Commit, Snapshot, and ChangeEntitiy

publicclassAppDbContext:DbContext{protectedoverridevoidOnModelCreating(ModelBuildermodelBuilder){modelBuilder.UseCrdt(crdtConfig.Value);}}

Tip

SampleDbContext has a full example of how to setup the DbContext.

Register CRDT services

Harmony provides the DataModel class as the main way the application will interact with the CRDT model. You first need to register it with the IoC container.

varbuilder=Host.CreateApplicationBuilder(args);builder.Service.AddCrdtData<AppDbContext>(config =>{});

Note

the config callback passed into AddCrdtData is currently empty, we'll come back to that later.

Tip

Pay attention to the generic type when calling AddCrdtData, this will be the type of your application's DbContext.

Define CRDT objects

Now that you have the services setup, you need to define a CRDT object. Take a look the following examples

  • Word contains a reference to an Antonym Word.
  • Definition references the Word it belongs to. Notice that if the Word Reference is removed, the Definition deletes itself.
  • Example this one is special because it uses a YDoc to store the example text in a Yjs compatible format. This allows the example sentence to be edited by multiple users and have those changes merged using the yjs CRDT algorithm.

Once you have created your CRDT objects, you need to tell Harmony about them. Update the config callback passed into AddCrdtData

services.AddCrdtData<SampleDbContext>(config =>{// add the following linesconfig.ObjectTypeListBuilder.Add<Word>().Add<Definition>().Add<Example>();});

Define CRDT Changes

Now that you've defined your objects, you need to define your changes. These record user intent when making changes to objects. How detailed and specific you make your changes will directly impact how changes get merged between clients and how often users 'lose' changes that they made.

Example SetWordTextChange

publicclassSetWordTextChange(GuidentityId,stringtext):Change<Word>(entityId),ISelfNamedType<SetWordTextChange>{publicstringText{get;}=text;publicoverrideValueTask<IObjectBase>NewEntity(Commitcommit,IChangeContextcontext){returnnew(newWord(){Id=EntityId,Text=Text});}publicoverrideValueTaskApplyChange(Wordentity,IChangeContextcontext){entity.Text=Text;returnValueTask.CompletedTask;}}

This is a fairly simple change, it can either create a new Word entry, or if the entityId passed in matches an object that has previously been created, then it will just set the Text field on the Word entry matching the Id.

Note

Changes will be serialized and stored forever. Try to keep the amount of data stored as small as possible.

This change can either create, or update an object. Most changes will probably be either an update, or a create. In those cases you should inherit from EditChange<T> or CreateChange<T>.

Tip

The Sample project contain a number of reference changes which are good examples for a couple different change types. There are also a built in DeleteChange<T>

Once you have created your change types, you need to tell Harmony about them. Again update the config callback passed into AddCrdtData

services.AddCrdtData<SampleDbContext>(config =>{// add the following lineconfig.ChangeTypeListBuilder.Add<SetWordTextChange>();config.ObjectTypeListBuilder.Add<Word>().Add<Definition>().Add<Example>();});

Use change objects to author changes to CRDT objects

Either via DI, or directly from the IoC container get an instance of DataModel and call AddChange

GuidclientId= ... get a stable Guid representing the application instance
Guid objectId=Guid.NewGuid();awaitdataModel.AddChange(clientId,newSetWordTextChange(objectId,"Hello World"));varword=awaitdataModel.GetLatest<Word>(objectId);Console.WriteLine(word.Text);

Important

The ClientId should be consistent for a project per computer/device. It is used to determine what changes should be synced between clients with the assumption that each client produces changes sequentially. So if a project is on 2 different computers, each copy should have a unique client Id. If they had the same Id, then they would not sync changes properly.

How the ClientId is stored is left up to the application. In FW Lite we created a table to store the ClientId. It's generated automatically when the project is downloaded or created the first time and it should never change after that.

In case of an online web app there could be one ClientId to represent the server. However, if users can author changes offline and sync them later, then each browser would need it's own ClientId.

Warning

If you were to regenerate the ClientId for each change or on application start, that would eventually result in poor sync performance, as the sync process checks for new changes to sync per ClientId.

Usage

Queries

DataModel is the primary class for both making changes and getting data. Above you saw an example of making changes, now we'll start querying data.

Query Word objects starting with the letter "A"

DataModeldataModel;//get from IoC, probably via DIvarwordsStartingWithA=awaitdataModel.GetLatestObjects<Word>().Where(w =>w.Text.StartsWith("a")).ToArrayAsync();

Harmony uses EF Core queries under the covers, you can read more about them here.

Submitting Changes

Changes are the only way to modify CRDT data. Here's another example of a change

DataModeldataModel;GuidclientId;//get a stable Guid representing the application instancevardefinitionId=Guid.NewGuid();GuidwordId;//get the word Id this definition is related to.awaitdataModel.AddChange(clientId,newNewDefinitionChange(definitionId){WordId=wordId,Text="Hello",PartOfSpeech=partOfSpeech,Order=order});

Warning

You can modify data returned by EF Core, and issue updates and inserts yourself, but that data will be lost, and will not sync properly. Do not directly modify the tables produced by Harmony otherwise you risk losing data.

Syncing data

Syncing is primarily done using the DataModel class, however the implementation of the server side is left up to you. You can find the Lexbox implementation here. The sync works by having 2 instances of the ISyncable interface. The local one is implemented by DataModel and the remote implementation depends on your server side. The FW Lite implementation can be found here. You will need to scope the instance to the project as well as deal with authentication.

Once you have a remote representation of the ISyncable interface you just call it like this

DataModeldataModel;ISyncableremoteModel;awaitdataModel.SyncWith(remoteModel);

It's that easy. All the heavy lifting is done by the interface which is fairly simple to implement.

Development

SemVer commit messages

NuGet package versions are calculated from a combination of tags and commit messages. First, the most recent Git tag matching the pattern v\d+.\d+.\d+ is located. If that is the commit being built, then that version number is used. If there have been any commits since then, the version number will be bumped by looking for one of the following patterns in the commit messages:

  • +semver: major or +semver: breaking - update major version number, reset others to 0 (so 2.3.1 would become 3.0.0)
  • +semver: minor or +semver: feature - update minor version number, reset patch to 0 (so 2.3.1 would become 2.4.0)
  • Anything else, including no +semver lines at all - update patch version number (so 2.3.1 would become 2.3.2)
    • If you want to include +semver lines, then +semver: patch or +semver: fix are the standard ways to increment a patch version bump, but the patch version will be bumped regardless as long as there is at least one commit since the most recent tag.

About

C# CRDT Library for building offline first apps

Topics

Resources

Stars

13 stars

Watchers

2 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - sillsdev/harmony: C# CRDT Library for building offline first apps · GitHub
Skip to content

Repository files navigation

harmony

NuGet version

A CRDT application library for C#, use it to build offline first applications.

Install

dotnet add package SIL.Harmony

It's expected that you use Harmony with the .Net IoC container (IoC intro) and with EF Core. If you're not familier with that you can take a look at the Host docs. If you're using ASP.NET Core you already have this setup for you.

Prerequisites:

Configure DbContext

EF Core needs to be told about the entities used by Harmony, for now these are just Commit, Snapshot, and ChangeEntitiy

publicclassAppDbContext:DbContext{protectedoverridevoidOnModelCreating(ModelBuildermodelBuilder){modelBuilder.UseCrdt(crdtConfig.Value);}}

Tip

SampleDbContext has a full example of how to setup the DbContext.

Register CRDT services

Harmony provides the DataModel class as the main way the application will interact with the CRDT model. You first need to register it with the IoC container.

varbuilder=Host.CreateApplicationBuilder(args);builder.Service.AddCrdtData<AppDbContext>(config =>{});

Note

the config callback passed into AddCrdtData is currently empty, we'll come back to that later.

Tip

Pay attention to the generic type when calling AddCrdtData, this will be the type of your application's DbContext.

Define CRDT objects

Now that you have the services setup, you need to define a CRDT object. Take a look the following examples

  • Word contains a reference to an Antonym Word.
  • Definition references the Word it belongs to. Notice that if the Word Reference is removed, the Definition deletes itself.
  • Example this one is special because it uses a YDoc to store the example text in a Yjs compatible format. This allows the example sentence to be edited by multiple users and have those changes merged using the yjs CRDT algorithm.

Once you have created your CRDT objects, you need to tell Harmony about them. Update the config callback passed into AddCrdtData

services.AddCrdtData<SampleDbContext>(config =>{// add the following linesconfig.ObjectTypeListBuilder.Add<Word>().Add<Definition>().Add<Example>();});

Define CRDT Changes

Now that you've defined your objects, you need to define your changes. These record user intent when making changes to objects. How detailed and specific you make your changes will directly impact how changes get merged between clients and how often users 'lose' changes that they made.

Example SetWordTextChange

publicclassSetWordTextChange(GuidentityId,stringtext):Change<Word>(entityId),ISelfNamedType<SetWordTextChange>{publicstringText{get;}=text;publicoverrideValueTask<IObjectBase>NewEntity(Commitcommit,IChangeContextcontext){returnnew(newWord(){Id=EntityId,Text=Text});}publicoverrideValueTaskApplyChange(Wordentity,IChangeContextcontext){entity.Text=Text;returnValueTask.CompletedTask;}}

This is a fairly simple change, it can either create a new Word entry, or if the entityId passed in matches an object that has previously been created, then it will just set the Text field on the Word entry matching the Id.

Note

Changes will be serialized and stored forever. Try to keep the amount of data stored as small as possible.

This change can either create, or update an object. Most changes will probably be either an update, or a create. In those cases you should inherit from EditChange<T> or CreateChange<T>.

Tip

The Sample project contain a number of reference changes which are good examples for a couple different change types. There are also a built in DeleteChange<T>

Once you have created your change types, you need to tell Harmony about them. Again update the config callback passed into AddCrdtData

services.AddCrdtData<SampleDbContext>(config =>{// add the following lineconfig.ChangeTypeListBuilder.Add<SetWordTextChange>();config.ObjectTypeListBuilder.Add<Word>().Add<Definition>().Add<Example>();});

Use change objects to author changes to CRDT objects

Either via DI, or directly from the IoC container get an instance of DataModel and call AddChange

GuidclientId= ... get a stable Guid representing the application instance
Guid objectId=Guid.NewGuid();awaitdataModel.AddChange(clientId,newSetWordTextChange(objectId,"Hello World"));varword=awaitdataModel.GetLatest<Word>(objectId);Console.WriteLine(word.Text);

Important

The ClientId should be consistent for a project per computer/device. It is used to determine what changes should be synced between clients with the assumption that each client produces changes sequentially. So if a project is on 2 different computers, each copy should have a unique client Id. If they had the same Id, then they would not sync changes properly.

How the ClientId is stored is left up to the application. In FW Lite we created a table to store the ClientId. It's generated automatically when the project is downloaded or created the first time and it should never change after that.

In case of an online web app there could be one ClientId to represent the server. However, if users can author changes offline and sync them later, then each browser would need it's own ClientId.

Warning

If you were to regenerate the ClientId for each change or on application start, that would eventually result in poor sync performance, as the sync process checks for new changes to sync per ClientId.

Usage

Queries

DataModel is the primary class for both making changes and getting data. Above you saw an example of making changes, now we'll start querying data.

Query Word objects starting with the letter "A"

DataModeldataModel;//get from IoC, probably via DIvarwordsStartingWithA=awaitdataModel.GetLatestObjects<Word>().Where(w =>w.Text.StartsWith("a")).ToArrayAsync();

Harmony uses EF Core queries under the covers, you can read more about them here.

Submitting Changes

Changes are the only way to modify CRDT data. Here's another example of a change

DataModeldataModel;GuidclientId;//get a stable Guid representing the application instancevardefinitionId=Guid.NewGuid();GuidwordId;//get the word Id this definition is related to.awaitdataModel.AddChange(clientId,newNewDefinitionChange(definitionId){WordId=wordId,Text="Hello",PartOfSpeech=partOfSpeech,Order=order});

Warning

You can modify data returned by EF Core, and issue updates and inserts yourself, but that data will be lost, and will not sync properly. Do not directly modify the tables produced by Harmony otherwise you risk losing data.

Syncing data

Syncing is primarily done using the DataModel class, however the implementation of the server side is left up to you. You can find the Lexbox implementation here. The sync works by having 2 instances of the ISyncable interface. The local one is implemented by DataModel and the remote implementation depends on your server side. The FW Lite implementation can be found here. You will need to scope the instance to the project as well as deal with authentication.

Once you have a remote representation of the ISyncable interface you just call it like this

DataModeldataModel;ISyncableremoteModel;awaitdataModel.SyncWith(remoteModel);

It's that easy. All the heavy lifting is done by the interface which is fairly simple to implement.

Development

SemVer commit messages

NuGet package versions are calculated from a combination of tags and commit messages. First, the most recent Git tag matching the pattern v\d+.\d+.\d+ is located. If that is the commit being built, then that version number is used. If there have been any commits since then, the version number will be bumped by looking for one of the following patterns in the commit messages:

  • +semver: major or +semver: breaking - update major version number, reset others to 0 (so 2.3.1 would become 3.0.0)
  • +semver: minor or +semver: feature - update minor version number, reset patch to 0 (so 2.3.1 would become 2.4.0)
  • Anything else, including no +semver lines at all - update patch version number (so 2.3.1 would become 2.3.2)
    • If you want to include +semver lines, then +semver: patch or +semver: fix are the standard ways to increment a patch version bump, but the patch version will be bumped regardless as long as there is at least one commit since the most recent tag.

About

C# CRDT Library for building offline first apps

Topics

Resources

Stars

13 stars

Watchers

2 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); GitHub - sillsdev/harmony: C# CRDT Library for building offline first apps · GitHub
Skip to content

Repository files navigation

harmony

NuGet version

A CRDT application library for C#, use it to build offline first applications.

Install

dotnet add package SIL.Harmony

It's expected that you use Harmony with the .Net IoC container (IoC intro) and with EF Core. If you're not familier with that you can take a look at the Host docs. If you're using ASP.NET Core you already have this setup for you.

Prerequisites:

Configure DbContext

EF Core needs to be told about the entities used by Harmony, for now these are just Commit, Snapshot, and ChangeEntitiy

publicclassAppDbContext:DbContext{protectedoverridevoidOnModelCreating(ModelBuildermodelBuilder){modelBuilder.UseCrdt(crdtConfig.Value);}}

Tip

SampleDbContext has a full example of how to setup the DbContext.

Register CRDT services

Harmony provides the DataModel class as the main way the application will interact with the CRDT model. You first need to register it with the IoC container.

varbuilder=Host.CreateApplicationBuilder(args);builder.Service.AddCrdtData<AppDbContext>(config =>{});

Note

the config callback passed into AddCrdtData is currently empty, we'll come back to that later.

Tip

Pay attention to the generic type when calling AddCrdtData, this will be the type of your application's DbContext.

Define CRDT objects

Now that you have the services setup, you need to define a CRDT object. Take a look the following examples

  • Word contains a reference to an Antonym Word.
  • Definition references the Word it belongs to. Notice that if the Word Reference is removed, the Definition deletes itself.
  • Example this one is special because it uses a YDoc to store the example text in a Yjs compatible format. This allows the example sentence to be edited by multiple users and have those changes merged using the yjs CRDT algorithm.

Once you have created your CRDT objects, you need to tell Harmony about them. Update the config callback passed into AddCrdtData

services.AddCrdtData<SampleDbContext>(config =>{// add the following linesconfig.ObjectTypeListBuilder.Add<Word>().Add<Definition>().Add<Example>();});

Define CRDT Changes

Now that you've defined your objects, you need to define your changes. These record user intent when making changes to objects. How detailed and specific you make your changes will directly impact how changes get merged between clients and how often users 'lose' changes that they made.

Example SetWordTextChange

publicclassSetWordTextChange(GuidentityId,stringtext):Change<Word>(entityId),ISelfNamedType<SetWordTextChange>{publicstringText{get;}=text;publicoverrideValueTask<IObjectBase>NewEntity(Commitcommit,IChangeContextcontext){returnnew(newWord(){Id=EntityId,Text=Text});}publicoverrideValueTaskApplyChange(Wordentity,IChangeContextcontext){entity.Text=Text;returnValueTask.CompletedTask;}}

This is a fairly simple change, it can either create a new Word entry, or if the entityId passed in matches an object that has previously been created, then it will just set the Text field on the Word entry matching the Id.

Note

Changes will be serialized and stored forever. Try to keep the amount of data stored as small as possible.

This change can either create, or update an object. Most changes will probably be either an update, or a create. In those cases you should inherit from EditChange<T> or CreateChange<T>.

Tip

The Sample project contain a number of reference changes which are good examples for a couple different change types. There are also a built in DeleteChange<T>

Once you have created your change types, you need to tell Harmony about them. Again update the config callback passed into AddCrdtData

services.AddCrdtData<SampleDbContext>(config =>{// add the following lineconfig.ChangeTypeListBuilder.Add<SetWordTextChange>();config.ObjectTypeListBuilder.Add<Word>().Add<Definition>().Add<Example>();});

Use change objects to author changes to CRDT objects

Either via DI, or directly from the IoC container get an instance of DataModel and call AddChange

GuidclientId= ... get a stable Guid representing the application instance
Guid objectId=Guid.NewGuid();awaitdataModel.AddChange(clientId,newSetWordTextChange(objectId,"Hello World"));varword=awaitdataModel.GetLatest<Word>(objectId);Console.WriteLine(word.Text);

Important

The ClientId should be consistent for a project per computer/device. It is used to determine what changes should be synced between clients with the assumption that each client produces changes sequentially. So if a project is on 2 different computers, each copy should have a unique client Id. If they had the same Id, then they would not sync changes properly.

How the ClientId is stored is left up to the application. In FW Lite we created a table to store the ClientId. It's generated automatically when the project is downloaded or created the first time and it should never change after that.

In case of an online web app there could be one ClientId to represent the server. However, if users can author changes offline and sync them later, then each browser would need it's own ClientId.

Warning

If you were to regenerate the ClientId for each change or on application start, that would eventually result in poor sync performance, as the sync process checks for new changes to sync per ClientId.

Usage

Queries

DataModel is the primary class for both making changes and getting data. Above you saw an example of making changes, now we'll start querying data.

Query Word objects starting with the letter "A"

DataModeldataModel;//get from IoC, probably via DIvarwordsStartingWithA=awaitdataModel.GetLatestObjects<Word>().Where(w =>w.Text.StartsWith("a")).ToArrayAsync();

Harmony uses EF Core queries under the covers, you can read more about them here.

Submitting Changes

Changes are the only way to modify CRDT data. Here's another example of a change

DataModeldataModel;GuidclientId;//get a stable Guid representing the application instancevardefinitionId=Guid.NewGuid();GuidwordId;//get the word Id this definition is related to.awaitdataModel.AddChange(clientId,newNewDefinitionChange(definitionId){WordId=wordId,Text="Hello",PartOfSpeech=partOfSpeech,Order=order});

Warning

You can modify data returned by EF Core, and issue updates and inserts yourself, but that data will be lost, and will not sync properly. Do not directly modify the tables produced by Harmony otherwise you risk losing data.

Syncing data

Syncing is primarily done using the DataModel class, however the implementation of the server side is left up to you. You can find the Lexbox implementation here. The sync works by having 2 instances of the ISyncable interface. The local one is implemented by DataModel and the remote implementation depends on your server side. The FW Lite implementation can be found here. You will need to scope the instance to the project as well as deal with authentication.

Once you have a remote representation of the ISyncable interface you just call it like this

DataModeldataModel;ISyncableremoteModel;awaitdataModel.SyncWith(remoteModel);

It's that easy. All the heavy lifting is done by the interface which is fairly simple to implement.

Development

SemVer commit messages

NuGet package versions are calculated from a combination of tags and commit messages. First, the most recent Git tag matching the pattern v\d+.\d+.\d+ is located. If that is the commit being built, then that version number is used. If there have been any commits since then, the version number will be bumped by looking for one of the following patterns in the commit messages:

  • +semver: major or +semver: breaking - update major version number, reset others to 0 (so 2.3.1 would become 3.0.0)
  • +semver: minor or +semver: feature - update minor version number, reset patch to 0 (so 2.3.1 would become 2.4.0)
  • Anything else, including no +semver lines at all - update patch version number (so 2.3.1 would become 2.3.2)
    • If you want to include +semver lines, then +semver: patch or +semver: fix are the standard ways to increment a patch version bump, but the patch version will be bumped regardless as long as there is at least one commit since the most recent tag.

About

C# CRDT Library for building offline first apps

Topics

Resources

Stars

13 stars

Watchers

2 watching

Forks

Releases

Used by

Contributors

Languages