MongoDB.Entities Documentation

repository·master·Indexed 20 days ago

https://github.com/dj-nitehawk/mongodb.entities

A lightweight .NET Standard library providing an abstraction layer over the official MongoDB driver. It features an async-only API (as of version 20), automatic audit field tracking via ModifiedBy, and a watcher registry for MongoDB change-streams. The library simplifies data access with a human-friendly API for entity persistence, targeted updates, LINQ queries, and relationship management.

Tokens
31.8K
Snippets
135
Records
149
Agent score
69%

What's inside MongoDB.Entities

  1. Overview of MongoDB.Entities

    master

    MongoDB.Entities is a lightweight .NET Standard 2.1 library designed to simplify MongoDB access by abstracting the official MongoDB driver. It provides an elegant, human-friendly API surface with minimal overhead, focusing on scalable application development through an async-only API.

    Key capabilities include:

    • Relationship Management: Built-in support for One-To-One, One-To-Many, and Many-To-Many relationships.
    • Querying: Support for LINQ, lambda expressions, filters, and aggregation pipelines, including sorting, paging, and projection.
    • Data Management: Simple data migration framework (similar to EntityFramework), programmatic index management, and easy bulk operations.
    • Advanced Search: Full-text search (with fuzzy matching), GeoSpatial search, and change-stream support.
    • Developer Productivity: Avoids manual handling of ObjectIds, BsonDocuments, and magic strings; provides easy audit fields, global filters, and multi-document transaction support.
    • File Handling: Ability to stream files in chunks to and from MongoDB (as a GridFS alternative).
  2. How FuzzyString works internally

    master

    When a FuzzyString is stored, the resulting MongoDB document contains both the original text and a Hash field. The Hash consists of Double Metaphone key codes for each word.

    Example document structure:

    {
      "AuthorName": {
          "Value": "Eckhart Tolle",
          "Hash": "AKRT TL"
      }
    }

    During a fuzzy search, your search term is converted into Double Metaphone key codes on the fly and matched against the stored Hash using MongoDB's standard full-text search functionality.

  3. Use String Templates to compose MongoDB queries

    master

    The Template<T> class allows you to write raw MongoDB queries (as strings) and safely inject C# values or property paths using a tag-based replacement system. This avoids manual string concatenation and keeps your queries coupled to your C# entity schema.

    How it works

    1. Mark Tags: In your raw query string, wrap the parts you want to replace with < and > (e.g., <PropertyName> or <tag_name>).
    2. Map Paths: Use .Path(expression) to automatically resolve a C# member expression into a MongoDB 'dotted' path string and replace the corresponding <Tag> in the query.
    3. Inject Values: Use .Tag("tagName", value) to replace a specific tag with a literal value. Note that when using .Tag(), you do not include the < or > characters in the tag name.

    Error Scenarios

    The system throws an exception if:

    • The input text contains no < and > tags.
    • You have tags in the query that were not assigned a replacement.
    • You provided a replacement via .Tag() or .Path() that does not exist in the query.
    var query = new Template<Book>(
                    """
                    {
                      <Title> : '<book_name>',
                      <Price> : <book_price>
                    }
                    """
                )
                .Path(b => b.Title)
                .Path(b => b.Price)
                .Tag("book_name", "The Power Of Now")
                .Tag("book_price", "10.95");
    
    var result = await db.Find<Book>()
                         .Match(query)
                         .ExecuteAsync();
  4. How global filters work in MongoDB.Entities

    master

    Global filters allow you to define a set of criteria that are automatically applied to all retrieval, update, and delete operations performed by a DB instance. This prevents the need to repeat common criteria (like soft-delete checks) in every single operation.

    To use them, you must create a custom class that derives from DB and register the filters within its constructor using SetGlobalFilter or its variants.

    public class MyDatabase : DB
    {
        public MyDatabase(string dbName) : base(Instance(dbName))
        {
            SetGlobalFilter<Book>(b => b.IsDeleted == false);
        }
    }
    
    // Usage
    var db = new MyDatabase("DatabaseName");
  5. Entity Deletion in Referenced Relationships

    master

    When an entity is deleted, the library handles the cleanup of references automatically based on the relationship type:

    • One-to-One: References pointing to the deleted entity become invalid. Calling .ToEntityAsync(db) on those references will return null.
    • One-to-Many / Many-to-Many: All join records (the links in the join collections) associated with the deleted entity are automatically removed. The other entities in the relationship remain intact but no longer point to the deleted entity.
  6. How the migration system works

    master

    The migration system allows you to transform database content and schema to match your C# entity models, similar to Entity Framework. You define migration classes that implement the IMigration interface. The library tracks which migrations have already been executed and runs any new ones in numerical order based on the class name prefix.

    public class _001_migration_name : IMigration 
    {
        public async Task UpgradeAsync() 
        {
            // Migration logic goes here
        }
    }
  7. Use the 'Date' type for improved precision and querying

    master

    Instead of using the standard System.DateTime type, you can use the specialized Date type in your entities. This type is designed to preserve date/time precision, allow for querying via Ticks, and supports extensibility through inheritance.

    When saving an entity containing a Date property, you can assign a standard DateTime value directly to it.

    // define the entity
    public class Book : Entity
    {
        public Date PublishedOn { get; set; }
    }
    
    // save the entity using a standard DateTime
    new Book
    {
        PublishedOn = DateTime.UtcNow
    }
    .Save();
  8. Use auto-managed ICreatedOn and IModifiedOn properties

    master

    Implement the ICreatedOn and IModifiedOn interfaces to enable automatic timestamp management. When these interfaces are present, the library automatically sets the appropriate values for creation and modification timestamps, which is useful for sorting and querying.

    public class Book : Entity, ICreatedOn, IModifiedOn
    {
        public string Title { get; set; }
        public DateTime CreatedOn { get; set; }
        public DateTime ModifiedOn { get; set; }
    }
  9. Map C# properties to MongoDB paths using Template.Path

    master

    The .Path() method (part of the Prop class functionality) converts a C# lambda/member expression into a MongoDB 'dotted' path string. This is used to replace tags in a template that represent property names.

    ExpressionResulting path
    x => x.Authors[0].Books[0].TitleAuthors.Books.Title

    If your template contains a tag like <Authors.Books.Title>, calling .Path(x => x.Authors[0].Books[0].Title) will perform the replacement.

  10. Customize ID formats and generators

    master

    The library uses the MongoDB driver's IIdGenerator to create IDs. By default, string IDs use ObjectId-formatted strings, while ObjectId and Guid use their respective types.

    Registering ID Generators

    Registration should be done at application startup, before calling DB.InitAsync().

    1. Entity-specific generator: Use DB.RegisterIdGenerator<T>(generator) to target a specific entity type. This takes precedence over other registrations.
    2. Type-specific generator: Use BsonSerializer.RegisterIdGenerator(typeof(T), generator) to affect all entities using that specific CLR type.
    // Entity-specific
    DB.RegisterIdGenerator<Book>(new MyCustomIdGenerator());
    
    // Type-specific
    BsonSerializer.RegisterIdGenerator(typeof(long), new MySequentialLongIdGenerator());

    Important Notes

    • ID Types: You can use any type serializable by the MongoDB driver (e.g., Guid, long, ObjectId).
    • Relationships: Many<> relationships support any ID type. For One<TEntity> one-to-one references, if you are not using a string ID, you must use the typed One<TEntity, TIdentity> version.
    • Guid Warning: If using Guid, ensure you register a Guid serializer (e.g., BsonSerializer.RegisterSerializer(new GuidSerializer(GuidRepresentation.Standard));) before initializing the library. Be aware that Guid carries a risk of duplicates during partial entity saving (upserts), which could lead to data loss.
    • Recommendation: It is highly recommended to use ObjectId to minimize the risk of duplicate IDs.
  11. Understand Async-only API requirements

    master

    As of version 20, MongoDB.Entities has removed all synchronous operations to avoid the performance penalties and thread-pool starvation associated with the official MongoDB driver's 'sync-over-async' implementation.

    To ensure scalability and performance, especially in server environments, applications should be built using async/await patterns from top to bottom for all I/O bound work.

  12. Define Referenced Relationships

    master

    Referenced relationships in MongoDB.Entities are defined using One<T> for one-to-one relationships and Many<TChild, TParent> for one-to-many and many-to-many relationships.

    To use these, you must initialize the Many<TChild, TParent> properties in the entity's constructor using InitOneToMany or InitManyToMany:

    • InitOneToMany(() => Property): Used for one-to-many relationships. Takes a single lambda pointing to the property.
    • InitManyToMany(() => Property, otherSide => otherSide.Property): Used for many-to-many relationships. Takes a lambda for the local property and a second lambda pointing to the corresponding property on the related entity.

    For many-to-many relationships, use the [OwnerSide] attribute on the property in the parent entity and the [InverseSide] attribute on the property in the related entity to define the relationship direction.

    public class Book : Entity
    {
        public One<Author> MainAuthor { get; set; }
        public Many<Author, Book> CoAuthors { get; set; }
    
        [OwnerSide]
        public Many<Genre, Book> Genres { get; set; }
    
        public Book()
        {
            this.InitOneToMany(() => CoAuthors);
            this.InitManyToMany(() => Genres, genre => genre.Books);
        }
    }
    
    public class Genre : Entity
    {
        [InverseSide]
        public Many<Book, Genre> Books { get; set; }
    
        public Genre()
        {
            this.InitManyToMany(() => Books, book => book.Genres);
        }
    }