N2.Core.Entity 1.3.6

dotnet add package N2.Core.Entity --version 1.3.6
                    
NuGet\Install-Package N2.Core.Entity -Version 1.3.6
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="N2.Core.Entity" Version="1.3.6" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="N2.Core.Entity" Version="1.3.6" />
                    
Directory.Packages.props
<PackageReference Include="N2.Core.Entity" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add N2.Core.Entity --version 1.3.6
                    
#r "nuget: N2.Core.Entity, 1.3.6"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package N2.Core.Entity@1.3.6
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=N2.Core.Entity&version=1.3.6
                    
Install as a Cake Addin
#tool nuget:?package=N2.Core.Entity&version=1.3.6
                    
Install as a Cake Tool

N2.Core.Entity

N2.Core.Entity

Overview

This library extends Entity Framework Core by providing standardized base models, change tracking, connection services, and useful query extensions for data contexts. It is designed to simplify and unify common patterns when working with Entity Framework in .NET 8 and .NET 9 projects.

Classes

ChangeLog : IChangeLog

The class ChangeLog implements the interface IChangeLog and represents a change log entry. It is used to store information about changes made to an entity. The structure of the model can be used as a template for other record definitions. It features:

- `Id` : `int` : The unique identifier of the change log entry.
- `LogRecordId` : `Guid` : A unique identifier for external references.
- `ReferenceId` : `Guid` : The unique identifier of the entity that was changed.
- Other columns to store information about the change.
- A static BuildModel method to define the structure of the model.

Define the ChangeLog in the data context where you want to implement change tracking. If you define a datacontext usig the CoreDataContext class, the ChangeLog class is added by default.

public class MyDataContext(DbContextOptions options) : DbContext(options)
{
    public virtual DbSet<ChangeLog> ChangeLog { get; set; }

	 protected override void OnModelCreating([NotNull] ModelBuilder modelBuilder)
	 {
		 base.OnModelCreating(modelBuilder);

		 // Add the ChangeLog model(s) to the data context.
		 ChangeLogBuilder.BuildModel(modelBuilder);
	 }
}

ChangeLogBuilder

The ChangeLogBuilder class provides a static method to define the structure of the ChangeLog model. For a set of tables that have some common goal, a static builder class should be defined to set up the data context. The BuildModel method is used to define the structure of the model. Call this method from the OnModelCreating method of the data context.

ChangeLogBuilder

The ChangeLogBuilder class provides a static method to define the structure of the ChangeLog model. For a set of tables that have some common goal, a static builder class should be defined to set up the data context. The BuildModel method is used to define the structure of the model. Call this method from the OnModelCreating method of the data context.

DbBaseModel : IDbBaseModel

The DbBaseModel class implements the IDbBaseModel interface and represents a base model for all entity classes. It is used to store common properties that are shared by all entities. The structure of the DbBaseModel contains basic tracking information. A few things to note:

- `Id` : This is the primary key, and it is automatically assigned by the database. It is an integer

as this is the type that is easily sorted and indexed. - PublicId : This is a unique identifier that is used to identify the entity in a public context. This is useful when the entity is exposed to the public, as the internal identifier should not be. - RowVersion : This is a timestamp that is used to track changes to the entity. It is used to prevent concurrent updates to the same entity. Using a timestamp is more efficient than using a date-timé values and is more reliable than using a version number.A bonus is that Entity Framework Core recognizes a timeatamp as a concurrency token. - The BuildModel method is used to define the structure of the model. It should be called from the buildmodel method of any derived class.

CoreDataContext : ICoreDataContext

The CoreDataContext is an abstract base class that extends DbContext and implements the ICoreDataContext interface. It provides a standardized foundation for building data contexts with built-in support for:

  • Change tracking via the ChangeLog entity
  • CRUD operations with standardized response handling
  • Automatic management of tracking fields (Created, Modified, Removed dates)
  • Soft delete functionality for IDbBaseModel entities
Key Features
  • Built-in ChangeLog: Automatically includes the ChangeLog table in your database
  • Simplified Record Management: Methods for adding, finding, and deleting records
  • Standardized Responses: All operations return consistent response status codes and messages
  • Soft Delete Support: Entities implementing IDbBaseModel are marked as removed rather than deleted
  • PublicId-based Queries: Find records using their PublicId (GUID) rather than internal IDs
Adding Tables to CoreDataContext

When creating a data context that inherits from CoreDataContext, follow these best practices for adding table resources:

  1. Define DbSet Properties
public class MyAppContext : CoreDataContext { public MyAppContext(DbContextOptions options) : base(options) { }

// Define entity tables as DbSet properties
public virtual DbSet<Customer> Customers { get; set; }
public virtual DbSet<Order> Orders { get; set; }

// Required implementation of abstract method
public override async Task<List<KeyValuePair<string, string>>> GetSelectListAsync(string tableName)
{
    // Implementation to return key-value pairs for dropdowns/selects
    return tableName switch
    {
        "Customers" => await Customers
            .Where(x => !x.IsRemoved)
            .Select(x => new KeyValuePair<string, string>(x.PublicId.ToString(), x.Name))
            .ToListAsync(),
		"Orders" => await Orders
            .Where(x => !x.IsRemoved)
            .Select(x => new KeyValuePair<string, string>(x.PublicId.ToString(), x.Name))
            .ToListAsync(),
        // Other tables...
        _ => new List<KeyValuePair<string, string>>()
    };
}
  1. Extend OnModelCreating

The preferred way to configure entity models is by creating builder classes for each entity and calling them from OnModelCreating:

protected override void OnModelCreating(ModelBuilder modelBuilder) { 
	// Always call the base implementation first to ensure ChangeLog is configured 
	base.OnModelCreating(modelBuilder);

	// Call the static BuildModel methods of your entity builders
	CustomerBuilder.BuildModel(modelBuilder);
	OrderBuilder.BuildModel(modelBuilder);
}
  1. Create Entity Builders

Following the pattern of ChangeLogBuilder, create builder classes for each entity:

public static class CustomerBuilder { 

    public static void BuildModel(ModelBuilder modelBuilder) { 
        modelBuilder.Entity<Customer>(entity => { 
        
        // Configure entity... 
        entity.ToTable("Customers"); 
        entity.HasKey(e => e.Id); 
        entity.HasIndex(e => e.PublicId).IsUnique();

        // Configure properties
        entity.Property(e => e.Name).HasMaxLength(200).IsRequired();
        
        // Configure relationships
        entity.HasMany(e => e.Orders)
              .WithOne(e => e.Customer)
              .HasForeignKey(e => e.CustomerId);
    });
}
Working with Change Logs

Use the provided methods to track changes to entities:


// In your service layer or repository class: 

public async Task UpdateCustomer(Customer customer, Guid userId, string userName) { 
    // Validate the customer entity and other business logic here
    
    // Update the entity 
    _context.Update(customer);

    // Add change log with specific message (optional)
    _context.AddChangeLog<Customer>(
        customer.PublicId,
        "Customer details updated: Name changed to " + customer.Name,
        userId,
        userName
    );

    // Save changes
    await _context.SaveChangesAsync();
}

When adding multiple records, optimize performance by using AddRange and then calling SaveChangesAsync once, instead of saving after each record. The changelog should reflect the batch operation as a single entry, or you can log each change individually based on your requirements.

// When importing many records
public async Task ImportCustomers(List<Customer> customers, Guid userId, string userName)
{
    // Add all customers
    _context.AddRange(customers);
    
    // Add a single summary log instead of one per customer
    _context.AddChangeLog<Customer>(
        Guid.Empty, // No specific entity
        $"Bulk import: Added {customers.Count} customer records",
        userId,
        userName
    );
    
    await _context.SaveChangesAsync();
}

Consider Retention Policies

For the ChangeLog table, consider implementing a retention policy to manage the size of the log. You can periodically archive or delete old entries based on your application's requirements.

public async Task PruneChangeLogsAsync(int daysToRetain)
{
    DateTime cutoffDate = DateTime.UtcNow.AddDays(-daysToRetain);
    
    await _context.Database.ExecuteSqlRawAsync(
        "DELETE FROM ChangeLog WHERE Created < {0}",
        cutoffDate);
}

By following these patterns, you'll maintain a consistent structure across your data access layer while leveraging the built-in capabilities of CoreDataContext.

Other Considerations

  • Never log changes without user context (Throw InvalidOperationException when the userId or userName is empty)
  • Sanitize sensitive information before logging

Automatic Change Tracking

You can implement an IEntityChangedListener pattern that automatically logs changes based on EF's change tracker, by overriding the SaveChangesAsyc method in CoreDataContext. This will automatically log changes for all entities implementing IDbBaseModel without needing to manually call AddChangeLog in your service methods. But be cautious with performance and ensure that the change log does not grow too large without pruning.

 // Override SaveChangesAsync in your context
public override async Task<(ResponseStatus status, string message)> SaveChangesAsync()
{
    // Get modified entities
    var modifiedEntities = ChangeTracker.Entries()
        .Where(e => e.State == EntityState.Added || 
                   e.State == EntityState.Modified ||
                   e.State == EntityState.Deleted);
                   
    foreach (var entry in modifiedEntities)
    {
        if (entry.Entity is IDbBaseModel model)
        {
            var action = entry.State switch
            {
                EntityState.Added => "Added",
                EntityState.Modified => "Updated",
                EntityState.Deleted => "Deleted",
                _ => "Modified"
            };
            
            AddChangeLog(
                model.GetType().Name,
                model.PublicId,
                $"{action} {model.GetType().Name} record",
                _currentUserContext.PublicId,
                _currentUserContext.Name
            );
        }
    }
    
    return await base.SaveChangesAsync();
}

CoreDesignComponent

The CoreDesignComponent is an abstract generic class that bridges the gap between your data layer and presentation layer. It's designed for implementing a component-based architecture for CRUD operations, especially in admin or design interfaces where users need to manage entity data.

Purpose and Key Features
  • Service Layer Pattern: Acts as a mediator between your UI and data access layer
  • Generic Implementation: Works with any data context, entity model, and view models
  • Built-in Authorization: Validates user permissions for each operation
  • Change Tracking: Automatically creates changelog entries for data modifications
  • Mapping Support: Provides structured patterns for mapping between entities and view models
  • Pagination and Sorting: Includes ready-to-use functionality for listing data with filtering
Generic Type Parameters
  • TContext: Your data context that implements ICoreDataContext
  • TData: The entity model that implements IDbBaseModel
  • TView: A simplified view model for list displays (implements IBasicListModel)
  • TDetail: A detailed view model for editing (implements IBasicListModel)
Core Operations

The component implements standard CRUD operations:

  • Create: Initialize and save new entities with proper tracking
  • Read: Retrieve entities as view models with mapped properties
  • Update: Modify existing entities while preserving history
  • Delete: Perform soft deletes with automatic change logging
Implementation Example

To use CoreDesignComponent, create a concrete implementation for your specific entity:

public class CustomerDesignComponent : CoreDesignComponent<MyAppContext, Customer, CustomerListViewModel, CustomerDetailViewModel> { 
    public CustomerDesignComponent( ICoreDataContextFactory<MyAppContext> contextFactory, IUserContextAccessor userContextAccessor) : 
        base( dataContextFactory: contextFactory, 
        recordListView: context => context.Customers.Include(c => c.Orders), 
        initializeView: () => new CustomerDetailViewModel(), 
        mapRecordToList: data => new CustomerListViewModel { 
            PublicId = data.PublicId, 
            Name = data.Name, 
            OrderCount = data.Orders.Count }, 
        mapRecordToView: async data => new CustomerDetailViewModel { 
            PublicId = data.PublicId, 
            Name = data.Name, 
            Email = data.Email, 
            // Other properties }, 
        mapViewToRecord: async (view, data) => { 
            data.Name = view.Name; 
            data.Email = view.Email; 
            // Map other properties 
            return data; }) { } }
Usage in Controllers or Pages

The component can be used in controllers or pages to handle data operations:

public class CustomerController : Controller { 

    private readonly CustomerDesignComponent _designComponent; 
    private readonly IUserContextAccessor _userAccessor;

    public CustomerController(
        CustomerDesignComponent designComponent,
        IUserContextAccessor userAccessor)
        {
            _designComponent = designComponent;
            _userAccessor = userAccessor;
        }

    [HttpGet]
    public async Task<IActionResult> Index(PagingInfo pagingInfo)
    {
        await _designComponent.LoadItemsAsync(pagingInfo, _userAccessor.UserContext);
        return View(_designComponent.Items);
    }

    [HttpGet]
    public async Task<IActionResult> Edit(string id)
    {
        var model = await _designComponent.InitializeModelAsync(id, _userAccessor.UserContext);
        if (model == null)
        {
            return NotFound();
        }
        return View(model);
    }

    [HttpPost]
    public async Task<IActionResult> Save(CustomerDetailViewModel model)
    {
        if (!ModelState.IsValid)
        {
            return View("Edit", model);
        }

        var result = await _designComponent.SaveItemAsync(model, _userAccessor.UserContext);
        if (result.Status.IsSuccess())
        {
            return RedirectToAction("Index");
        }

        ModelState.AddModelError("", result.Message);
        return View("Edit", model);
    }
}
Benefits of Using CoreDesignComponent
  • Consistent Implementation: Standardizes how CRUD operations are implemented
  • Reduced Boilerplate: Eliminates repetitive code for loading, saving, and mapping entities
  • Automatic Tracking: Integrates with the changelog system for audit trails
  • Separation of Concerns: Cleanly separates data access, business logic, and presentation
  • Type Safety: Leverages generics for compile-time type checking

By using the CoreDesignComponent, you can rapidly develop administrative interfaces that follow consistent patterns while maintaining a clean separation between your data models and view models.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.3.6 172 6/4/2025
1.0.9 398 8/5/2024 1.0.9 is deprecated.
1.0.4 309 5/13/2024 1.0.4 is deprecated.
1.0.3 313 5/8/2024 1.0.3 is deprecated.