Understanding the Generated Types and Methods
Sample Entity Declaration
publicpartialclassEmployee:IBkEntity<Employee,ApplicationDbContext>
{
publicvoidBkConfiguration(IBkEntityDesigner<Employee,ApplicationDbContext>designer)
{
designer.DefaultCatalogTextProperty(a=>$"{a.FirstName} {a.LastName}");
designer.HasIndex(a=>a.Email,true);
designer.EntityDisplayInfo("Employee","Employees are the people who work for the company");
designer.DefaultOrderBy(a=>a.FirstName);
designer.HasIndex(a=>a.FirstName);
designer.Authorization.RestrictDeleteToRoles(["Admin","Manager"],"You are not allowed to delete this employee");
designer.Properties.AddTextProperty<string>("FirstName",c=>
{
c.Label("First Name");
c.PlaceHolder("First Name");
c.ToolTip("Employee First Name");
c.MaxLength(50);
c.MinLength(1);
c.TabIndex(1);
c.AutoComplete("given-name");
});
designer.Properties.AddTextProperty<string?>("MiddleName",c=>
{
c.Label("Middle Name");
c.PlaceHolder("Middle Name");
c.ToolTip("Employee Middle Name");
c.MaxLength(50);
c.TabIndex(2);
});
designer.Properties.AddTextProperty<string>("LastName",c=>
{
c.Label("Last Name");
c.PlaceHolder("Last Name");
c.ToolTip("Employee Last Name");
c.MaxLength(50);
c.TabIndex(3);
c.MinLength(1);
});
designer.Properties.AddTextProperty<string>("Email",c=>
{
c.Label("Email");
c.PlaceHolder("Email");
c.ToolTip("Employee Email");
c.MaxLength(150);
c.MinLength(6);
c.TabIndex(4);
});
designer.Properties.AddOneEntity<Department>("Department",c=>
{
c.Label("Department");
c.NameOfMany("Employees");
c.TabIndex(5);
c.UseDropDown();
});
}
}
To use the generated types and methods correctly and safely, there are several important concepts and behaviors you should understand. These rules are designed to help you maintain a clean and maintainable architecture, especially when your application grows.
- Entity configuration must be done only inside the
BkConfiguration
method of the partial class for each entity. This method is located in your own code, inside the partial entity definition. You should not try to configure the entity elsewhere. - Generated code updates automatically. Every time you make a change to your partial entity class (e.g., add or remove properties), the framework will regenerate the corresponding code to reflect that change. This ensures that your entity stays in sync with its model and underlying logic.
- New properties are automatically mirrored. When you add a new property to your entity, the same property will be added to the generated partial entity class and to the generated EntityModel, using the same name and type.
- Generated properties in the entity are immutable by design. This means you cannot change their values directly. This is intentional and helps enforce a clean domain model. Any changes or logic that affect the entity must be declared in the entity itself — not scattered across the application. This makes it much easier to reason about your codebase and perform maintenance in the future.
- The generated EntityModel (named by convention
{EntityName}Model
) acts as a bridge between the UI and the entity itself. It has public setters for all properties so you can bind and modify data in forms. But remember — it's just a transport layer, not the real domain logic. - The
EntityModel
includes two key methods:Create()
andUpdate()
. These methods are responsible for transforming the model into a real entity or updating an existing one. On the entity side, there are also correspondingCreate
andUpdate
methods which the model uses under the hood. These are tightly coupled and designed to work together — do not bypass them. - Non-editable (functional) properties are managed automatically by the framework.
You don't need to bind them in the UI or pass them to
Create
orUpdate
methods. These might include timestamps, versioning fields, user tracking, etc. - You can add custom methods inside the entity class if you need to perform logic that’s not handled through the EntityModel.
For example, if you need to update only one property based on a complex rule, you can create a method in the entity and call it when using the
DbContext
. - If you implement custom editing logic (outside of
Create
/Update
), it is your responsibility to validate the entity before saving. You must call the entity'sIsValid()
method before callingSaveChanges
. If you skip this, invalid entities may be saved to the database. - If you bypass the generated
Create
,Update
, orDelete
methods, you are also responsible for publishing the correct events. These events are essential for triggering any registered event handlers (such as those for caching, syncing, or audit logs). If you skip publishing them, none of the automation will happen — and your system may become inconsistent. - If you use the default caching system, you must set the
CacheKey
in your custom edit methods. Otherwise, the framework will not be able to invalidate the cache after changes, and your UI may show outdated data.
Summary
BlazorForKids provides a robust and safe foundation for working with entities, models, and the database — but it's important to understand how the generated code works and what your responsibilities are when you step outside of the default paths.
The system is designed to centralize business logic inside the entity itself and provide clean communication between the UI and the backend via the EntityModel
.
When you follow this pattern, your code will be easier to maintain, debug, and extend.
Below is a full example showing:
Sample: The generated partial entity
- Note: The framework automatically applies essential attributes such as
[Required]
,[StringLength]
, and[Display]
to your properties based on the configuration you define inBkConfiguration
. You don’t need to add them manually. - Note: The framework also generates the necessary Entity Type Configuration for EF Core behind the scenes, so you don’t need to manually configure entity mapping, relationships, or constraints.
- Note: The parameters included in the generated
Create
andUpdate
methods are only the ones that are actually required to construct or update the entity. Optional and framework-managed properties are excluded automatically.
publicpartialclassEmployee:IEmployeeEditModel,IBkEntityKey<EmployeeId>,IBkEditModel<EmployeeModel>,IBkEntityDefaultOrderBy<Employee>
{
[Required]
[Display(Name="First Name",Prompt="First Name",Description="Employee First Name")]
[StringLength(50,MinimumLength=1)]
publicstringFirstName{get;privateset;}=string.Empty;
[Display(Name="Middle Name",Prompt="Middle Name",Description="Employee Middle Name")]
[StringLength(50)]
publicstring?MiddleName{get;privateset;}
[Required]
[Display(Name="Last Name",Prompt="Last Name",Description="Employee Last Name")]
[StringLength(50,MinimumLength=1)]
publicstringLastName{get;privateset;}=string.Empty;
[Required]
[Display(Name="Email",Prompt="Email",Description="Employee Email")]
[StringLength(150,MinimumLength=6)]
publicstringEmail{get;privateset;}=string.Empty;
publicDepartmentDepartment{get;privateset;}=default!;
[Key]
publicEmployeeIdId{get;privateset;}
[Display(Name="Department")]
[Required]
publicDepartmentIdDepartmentId{get;privateset;}
publicstaticEmployeeCreate(stringfirstName,string?middleName,stringlastName,stringemail,DepartmentIddepartmentId)
{
returnnewEmployee
{
FirstName=firstName,
MiddleName=middleName,
LastName=lastName,
Email=email,
Id=EmployeeId.NewId,
DepartmentId=departmentId
};
}
publicvoidUpdate(stringfirstName,string?middleName,stringlastName,stringemail,DepartmentIddepartmentId)
{
FirstName=firstName;
MiddleName=middleName;
LastName=lastName;
Email=email;
DepartmentId=departmentId;
}
publicBkValidationResultIsValid(IServiceProvider?serviceProvider=null)
{
returnBkValidationResult.CreateValidationResult(this,serviceProvider);
}
publicstaticEmployeeMapFrom(IEmployeeEditModelitem)
{
returnnewEmployee()
{
FirstName=item.FirstName,
MiddleName=item.MiddleName,
LastName=item.LastName,
Email=item.Email,
Id=item.Id,
DepartmentId=item.DepartmentId
};
}
publicstaticExpression<Func<Employee,BkCatalogItem>>ProjectToDefaultCatalog=>a=>newBkCatalogItem(a.Id,$"{a.FirstName} {a.LastName}");
publicstaticExpression<Func<Employee,object>>DefaultOrderBy=>a=>a.FirstName;
publicclassConfiguration:IEntityTypeConfiguration<Employee>
{
publicvoidConfigure(EntityTypeBuilder<Employee>builder)
{
builder.Property(bk=>bk.FirstName).IsRequired(true).HasMaxLength(50);
builder.Property(bk=>bk.MiddleName).IsRequired(false).HasMaxLength(50);
builder.Property(bk=>bk.LastName).IsRequired(true).HasMaxLength(50);
builder.Property(bk=>bk.Email).IsRequired(true).HasMaxLength(150);
builder.HasOne<Department>(a=>a.Department).WithMany(a=>a.Employees).HasForeignKey(a=>a.DepartmentId).IsRequired(true).OnDelete(DeleteBehavior.Restrict);
builder.HasKey(bk=>bk.Id);
builder.Property(bk=>bk.Id).HasConversion(bk=>bk.Value,bk=>newEmployeeId(bk)).ValueGeneratedNever();
builder.HasIndex(a=>a.Email).IsUnique();
builder.HasIndex(a=>a.FirstName);
}
}
}
Sample: The generated entity model
publicclassEmployeeModel:IEmployeeEditModel,IBkCloneable<EmployeeModel>,IBkEntityDefaultOrderBy<EmployeeModel>,IBkEntityKey<EmployeeId>,IBkProjectFromEntity<EmployeeModel,Employee,ApplicationDbContext>,IBkEntityModel<Employee,ApplicationDbContext>,IBkEntityDto<Employee>
{
[Required]
[Display(Name="First Name",Prompt="First Name",Description="Employee First Name")]
[StringLength(50,MinimumLength=1)]
[BkFieldAttribute(BkFieldType.Text)]
[BkTabIndexAttribute(1)]
[BkAutoCompleteAttribute("given-name")]
publicstringFirstName{get;set;}=string.Empty;
[Display(Name="Middle Name",Prompt="Middle Name",Description="Employee Middle Name")]
[StringLength(50)]
[BkFieldAttribute(BkFieldType.Text)]
[BkTabIndexAttribute(2)]
publicstring?MiddleName{get;set;}
[Required]
[Display(Name="Last Name",Prompt="Last Name",Description="Employee Last Name")]
[StringLength(50,MinimumLength=1)]
[BkFieldAttribute(BkFieldType.Text)]
[BkTabIndexAttribute(3)]
publicstringLastName{get;set;}=string.Empty;
[Required]
[Display(Name="Email",Prompt="Email",Description="Employee Email")]
[StringLength(150,MinimumLength=6)]
[BkFieldAttribute(BkFieldType.Text)]
[BkTabIndexAttribute(4)]
publicstringEmail{get;set;}=string.Empty;
[Key]
publicEmployeeIdId{get;set;}
[Display(Name="Department")]
[BkFieldAttribute(BkFieldType.DropDown)]
[Required]
[BkTabIndexAttribute(5)]
publicDepartmentIdDepartmentId{get;set;}
publicEmployeeCreate()
{
varentity=Employee.Create(FirstName,MiddleName,LastName,Email,DepartmentId);
Id=entity.Id;
returnentity;
}
publicvoidUpdate(Employeeemployee)
{
employee.Update(FirstName,MiddleName,LastName,Email,DepartmentId);
}
publicEmployeeModelClone()
{
returnnewEmployeeModel()
{
FirstName=FirstName,
MiddleName=MiddleName,
LastName=LastName,
Email=Email,
Id=Id,
DepartmentId=DepartmentId
};
}
publicBkValidationResultIsValid(IServiceProvider?serviceProvider=null)
{
returnBkValidationResult.CreateValidationResult(this,serviceProvider);
}
publicstaticExpression<Func<Employee,EmployeeModel>>ProjectFromEntity=>entity=>newEmployeeModel()
{
FirstName=entity.FirstName,
MiddleName=entity.MiddleName,
LastName=entity.LastName,
Email=entity.Email,
Id=entity.Id,
DepartmentId=entity.DepartmentId
};
publicstaticExpression<Func<EmployeeModel,object>>DefaultOrderBy=>a=>a.FirstName;
publicEmployeeMapToEntity()
{
returnEmployee.MapFrom(this);
}
}
- Note: The
EntityModel
includes only the properties that are necessary for mapping data to and from the actual entity. It is designed to mirror the entity’s structure while keeping the focus on editable fields. - Note: In addition to data properties, the
EntityModel
also includes metadata used by the framework to generate the appropriate UI editors (such as labels, tooltips, placeholders, and validation rules). - Note: The
EntityModel
provides helper methods such asClone()
and mappings likeCreate()
orUpdate()
, which simplify UI-to-entity transformations and are commonly used when working with forms and temporary copies.
Using Generated Methods in a Custom Service
The following EmployeeService
class is provided as an example to demonstrate how you can work with the generated types and methods in BlazorForKids.
While you are free to implement services like this in your own projects, you do not need to create a custom service manually to perform basic CRUD operations — the framework already provides built-in functionality for creating, updating, deleting, and querying entities.
This example is meant to show how the EntityModel
, validation methods, and the mediator-based query system can be used in a real-world service class.
It also illustrates best practices such as:
- How to call the
Create()
andUpdate()
methods of theEntityModel
. - How to validate entities using the
IsValid()
method before saving changes. - How to work with the
ApplyChangesWithResultAsync()
extension to persist data and return a result object. - How to filter and paginate query results using
BkFilterCollection
.
Notes:
- Validation: Every time you create or update an entity manually, you must call the
IsValid()
method. This ensures that all validation rules defined in your entity are checked before attempting to save. If you skip this step, the entity could be saved in an invalid state. - Event Handling: The
ApplyChangesWithResultAsync()
method also handles publishing any required domain events (such as when caching or event listeners are involved). This helps maintain consistency and ensures side effects are triggered as expected. - Queries: The method
GetEmployeesAsync()
demonstrates how to useIBkMediatorDomain
to run pre-generated queries. While you can call the query method directly, be aware that retrieving all records from the database (without filters) may impact performance if the table grows large. Filtering and pagination should always be considered in production scenarios. - DbContext Access: This example uses
IDbContextFactory<T>
to create DbContext instances. This is the recommended way when working outside of scoped services or when you need precise control over context lifetime.
The sample code below can serve as a reference when you need to build custom business workflows or wrap generated logic inside your own service layer. Remember that this is just a sample and not a real service.
publicclassEmployeeService
{
publicasyncTask<IBkCommandResult>CreateEmployeeAsync(IDbContextFactory<ApplicationDbContext>dbContextFactory,IServiceProviderserviceProvider,EmployeeModelemployeeModel)
{
awaitusingvarcontext=awaitdbContextFactory.CreateDbContextAsync();
// Create a new Employee entity varemployee=employeeModel.Create();
// check if the entity is valid varisEmployeeEntityValid=employee.IsValid(serviceProvider);
if(isEmployeeEntityValid==false)
{
returnisEmployeeEntityValid.CommandResult();
}
// Note: The DbSet
// Note: ApplyChangesWithResultAsync is an extension method that applies changes to the context and returns a result returnawaitcontext.ApplyChangesWithResultAsync(CancellationToken.None);
}
publicasyncTask<IBkCommandResult>UpdateEmployeeAsync(IDbContextFactory<ApplicationDbContext>dbContextFactory,IServiceProviderserviceProvider,EmployeeModelemployeeModel)
{
awaitusingvarcontext=awaitdbContextFactory.CreateDbContextAsync();
// Find the existing Employee entity varemployee=awaitcontext.Employee.FindAsync(employeeModel.Id);
if(employee==null)
{
returnBkCommandResult.Error("Employee not found");
}
// Update the Employee entity employeeModel.Update(employee);
// check if the entity is valid varisEmployeeEntityValid=employee.IsValid(serviceProvider);
if(isEmployeeEntityValid==false)
{
returnisEmployeeEntityValid.CommandResult();
}
// Note: ApplyChangesWithResultAsync is an extension method that applies changes to the context and returns a result returnawaitcontext.ApplyChangesWithResultAsync(CancellationToken.None);
}
publicasyncTask<IBkCommandResult>DeleteEmployeeAsync(IDbContextFactory<ApplicationDbContext>dbContextFactory,EmployeeIdemployeeId)
{
awaitusingvarcontext=awaitdbContextFactory.CreateDbContextAsync();
// Find the existing Employee entity varemployee=awaitcontext.Employee.FindAsync(employeeId);
if(employee==null)
{
returnBkCommandResult.Error("Employee not found");
}
// Remove the Employee entity context.Employee.Remove(employee);
// Note: ApplyChangesWithResultAsync is an extension method that applies changes to the context and returns a result returnawaitcontext.ApplyChangesWithResultAsync(CancellationToken.None);
}
// Learn to use the BkFilterCollection to filter the employees publicasyncTask<List<EmployeeModel>>GetEmployeesAsync(IBkMediatorDomainmediator)
{
// Get all employees using the mediator, no filters applied// Calling EmployeeQuery() without any filters will return all employees from database, this can be a performance issue if the database has a lot of records varallEmployeesResult=awaitmediator.EmployeeQuery();
List<EmployeeModel>allEmployees=allEmployeesResult.Items;
intallEmployeesCount=allEmployeesResult.TotalItems;
// Get all employees with filters applied varemployeesFilters=newBkFilterCollection<Employee>();
employeesFilters.AddDefaultFilter(a=>a.MiddleName!=null);
// Set the page size to 10, this will limit the number of records returned from the database employeesFilters.Paginator.SetPageSize(10);
varemployeesWithMiddleNameResult=awaitmediator.EmployeeQuery(employeesFilters);
// the result has 2 properties, Items and TotalItems List<EmployeeModel>employeesWithMiddleName=employeesWithMiddleNameResult.Items;
intemployeesWithMiddleNameCount=employeesWithMiddleNameResult.TotalItems;
returnemployeesWithMiddleName.Any()?employeesWithMiddleName:allEmployees;
}
}