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
<PackageReference Include="N2.Core.Entity" Version="1.3.6" />
<PackageVersion Include="N2.Core.Entity" Version="1.3.6" />
<PackageReference Include="N2.Core.Entity" />
paket add N2.Core.Entity --version 1.3.6
#r "nuget: N2.Core.Entity, 1.3.6"
#:package N2.Core.Entity@1.3.6
#addin nuget:?package=N2.Core.Entity&version=1.3.6
#tool nuget:?package=N2.Core.Entity&version=1.3.6
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:
- 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>>()
};
}
- 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);
}
- 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 theuserId
oruserName
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 implementsICoreDataContext
TData
: The entity model that implementsIDbBaseModel
TView
: A simplified view model for list displays (implementsIBasicListModel
)TDetail
: A detailed view model for editing (implementsIBasicListModel
)
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 | Versions 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. |
-
net8.0
- Microsoft.EntityFrameworkCore (>= 9.0.5)
- Microsoft.EntityFrameworkCore.Analyzers (>= 9.0.5)
- Microsoft.EntityFrameworkCore.Relational (>= 9.0.5)
- Microsoft.EntityFrameworkCore.Tools (>= 9.0.5)
- Microsoft.Extensions.Configuration (>= 9.0.5)
- Microsoft.Extensions.Configuration.Abstractions (>= 9.0.5)
- Microsoft.Extensions.Configuration.Binder (>= 9.0.5)
- Microsoft.Extensions.Configuration.Json (>= 9.0.5)
- Microsoft.Extensions.Configuration.UserSecrets (>= 9.0.5)
- N2.Core.Abstractions (>= 1.3.6)
- System.Identitymodel.Tokens.Jwt (>= 8.12.0)
- System.IO.Abstractions (>= 22.0.14)
-
net9.0
- Microsoft.EntityFrameworkCore (>= 9.0.5)
- Microsoft.EntityFrameworkCore.Analyzers (>= 9.0.5)
- Microsoft.EntityFrameworkCore.Relational (>= 9.0.5)
- Microsoft.EntityFrameworkCore.Tools (>= 9.0.5)
- Microsoft.Extensions.Configuration (>= 9.0.5)
- Microsoft.Extensions.Configuration.Abstractions (>= 9.0.5)
- Microsoft.Extensions.Configuration.Binder (>= 9.0.5)
- Microsoft.Extensions.Configuration.Json (>= 9.0.5)
- Microsoft.Extensions.Configuration.UserSecrets (>= 9.0.5)
- N2.Core.Abstractions (>= 1.3.6)
- System.Identitymodel.Tokens.Jwt (>= 8.12.0)
- System.IO.Abstractions (>= 22.0.14)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.