-
Notifications
You must be signed in to change notification settings - Fork 393
05 feature dev guide
This guide uses the Student entity as an example to systematically explain the standard workflow for implementing complete CRUD (Create, Read, Update, Delete) functionality in an ADNC project. It covers key stages from Entity definition to the Controller layer, ensuring code consistency and maintainability.
Create a new Student entity in the Repository\Entities directory:
- The entity must directly or indirectly inherit from
EfEntity. - If the entity contains
CreateBy,CreateByTime,ModifyBy, orModifyTimefields, inherit fromEfFullAuditEntity. - If it only contains
CreateByandCreateByTime, inherit fromEfBasicAuditEntity. - If it has no audit fields, inherit from
EfEntity. - For soft delete support, implement the
ISoftDeleteinterface. - For optimistic locking (concurrency control), implement the
IConcurrencyinterface.
namespace Adnc.Demo.Admin.Repository.Entities;
/// <summary>
/// Student Entity
/// </summary>
public class Student : EfBasicAuditEntity, ISoftDelete
{
public static readonly int Name_MaxLength = 50;
/// <summary>
/// Student Name
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Soft Delete Flag
/// </summary>
public bool IsDeleted { get; set; }
}Register it in EntityInfo.cs:
modelBuilder.Entity<Student>().ToTable("sch_student");Create a StudentConfig class in Repository\Entities\Config:
namespace Adnc.Demo.Admin.Repository.Entities.Config;
public class StudentConfig : AbstractEntityTypeConfiguration<Student>
{
public override void Configure(EntityTypeBuilder<Student> builder)
{
base.Configure(builder);
builder.Property(x => x.Name).HasMaxLength(Student.Name_MaxLength);
}
}After defining the entity, perform the migration:
cd src
# Add migration
dotnet ef migrations add Student --project Repository\Adnc.Demo.Admin.Repository.csproj --startup-project Api\Adnc.Demo.Admin.Api.csproj
# Update database
dotnet ef database update --project Repository\Adnc.Demo.Admin.Repository.csproj --startup-project Api\Adnc.Demo.Admin.Api.csprojAdd mapping configurations in Application\AutoMapper\MapperProfile.cs:
CreateMap<Student, StudentDto>();
CreateMap<StudentCreationDto, Student>();Create DTOs in Application\Contracts\Dtos\Student:
- If the DTO includes audit fields, inherit from
OutputFullAuditInfoDtoorOutputBaseAuditDto. - Otherwise, inherit from
InputDtoorOutputDto. - Paged search DTOs must inherit from
SearchPagedDto.
namespace Adnc.Demo.Admin.Application.Contracts.Dtos.Student;
public class StudentCreationDto : InputDto
{
public string Name { get; set; } = string.Empty;
}
public class StudentUpdationDto : StudentCreationDto { }
public class StudentSearchPagedDto : SearchPagedDto { }
[Serializable]
public class StudentDto : OutputBaseAuditDto
{
public string Name { get; set; } = string.Empty;
}Create validator classes in Application\Contracts\Dtos\Student\Validators:
using Adnc.Demo.Admin.Application.Contracts.Dtos.Student;
namespace Adnc.Demo.Admin.Application.Validators;
public class StudentCreationDtoValidator : AbstractValidator<StudentCreationDto>
{
public StudentCreationDtoValidator()
{
RuleFor(x => x.Name).NotEmpty().MaximumLength(Student.Name_MaxLength);
}
}
public class StudentUpdationDtoValidator : AbstractValidator<StudentUpdationDto>
{
public StudentUpdationDtoValidator()
{
Include(new StudentCreationDtoValidator());
}
}Define the interface in Application\Contracts\Interfaces:
- Must inherit from
IAppService. - All write operations must return
ServiceResultorServiceResult<T>. - For transactions, use the
[UnitOfWork]attribute.
namespace Adnc.Demo.Admin.Application.Contracts.Interfaces;
public interface IStudentService : IAppService
{
[OperateLog(LogName = "Create Student")]
Task<ServiceResult<IdDto>> CreateAsync(StudentCreationDto input);
[OperateLog(LogName = "Update Student")]
Task<ServiceResult> UpdateAsync(long id, StudentUpdationDto input);
[OperateLog(LogName = "Delete Student")]
Task<ServiceResult> DeleteAsync(long[] ids);
Task<StudentDto?> GetAsync(long id);
Task<PageModelDto<StudentDto>> GetPagedAsync(StudentSearchPagedDto input);
}Implement the service in Application\Services:
- Must inherit from
AbstractAppService. - Return
Problemfor business validation failures instead of throwing exceptions.
namespace Adnc.Demo.Admin.Application.Services;
public class StudentService(IEfRepository<Student> studentRepo)
: AbstractAppService, IStudentService
{
public async Task<ServiceResult<IdDto>> CreateAsync(StudentCreationDto input)
{
input.TrimStringFields();
var nameExists = await studentRepo.AnyAsync(x => x.Name == input.Name);
if (nameExists) return Problem(HttpStatusCode.BadRequest, "Name already exists");
var entity = Mapper.Map<Student>(input, IdGenerater.GetNextId());
await studentRepo.InsertAsync(entity);
return new IdDto(entity.Id);
}
public async Task<ServiceResult> UpdateAsync(long id, StudentUpdationDto input)
{
input.TrimStringFields();
var entity = await studentRepo.FetchAsync(x => x.Id == id, noTracking: false);
if (entity is null) return Problem(HttpStatusCode.NotFound, "Student not found");
var nameExists = await studentRepo.AnyAsync(x => x.Name == input.Name && x.Id != id);
if (nameExists) return Problem(HttpStatusCode.BadRequest, "Name already exists");
Mapper.Map(input, entity);
await studentRepo.UpdateAsync(entity);
return ServiceResult();
}
public async Task<ServiceResult> DeleteAsync(long[] ids)
{
await studentRepo.ExecuteDeleteAsync(x => ids.Contains(x.Id));
return ServiceResult();
}
public async Task<StudentDto?> GetAsync(long id)
{
var entity = await studentRepo.FetchAsync(x => x.Id == id);
return entity is null ? null : Mapper.Map<StudentDto>(entity);
}
public async Task<PageModelDto<StudentDto>> GetPagedAsync(StudentSearchPagedDto input)
{
input.TrimStringFields();
var whereExpr = ExpressionCreator.New<Student>()
.AndIf(input.Keywords.IsNotNullOrWhiteSpace(), x => EF.Functions.Like(x.Name, $"{input.Keywords}%"));
var total = await studentRepo.CountAsync(whereExpr);
if (total == 0) return new PageModelDto<StudentDto>(input);
var entities = await studentRepo.Where(whereExpr)
.OrderByDescending(x => x.Id)
.Skip(input.SkipRows())
.Take(input.PageSize)
.ToListAsync();
return new PageModelDto<StudentDto>(input, Mapper.Map<List<StudentDto>>(entities), total);
}
}Define permission codes in Api\Consts.cs:
public static class Student
{
public const string Create = "student-create";
public const string Update = "student-update";
public const string Delete = "student-delete";
public const string Search = "student-search";
public const string Get = "student-get";
}Create the Controller in Api\Controllers:
namespace Adnc.Demo.Admin.Api.Controllers;
[Route($"{RouteConsts.AdminRoot}/students")]
[ApiController]
public class StudentController(IStudentService studentService) : AdncControllerBase
{
[HttpPost]
[AdncAuthorize(PermissionConsts.Student.Create)]
public async Task<ActionResult<IdDto>> CreateAsync([FromBody] StudentCreationDto input)
=> CreatedResult(await studentService.CreateAsync(input));
[HttpPut("{id}")]
[AdncAuthorize(PermissionConsts.Student.Update)]
public async Task<ActionResult<long>> UpdateAsync([FromRoute] long id, [FromBody] StudentUpdationDto input)
=> Result(await studentService.UpdateAsync(id, input));
[HttpDelete("{ids}")]
[AdncAuthorize(PermissionConsts.Student.Delete)]
public async Task<ActionResult> DeleteAsync([FromRoute] string ids)
{
var idArr = ids.Split(',').Select(long.Parse).ToArray();
return Result(await studentService.DeleteAsync(idArr));
}
[HttpGet("page")]
[AdncAuthorize(PermissionConsts.Student.Search)]
public async Task<ActionResult<PageModelDto<StudentDto>>> GetPagedAsync([FromQuery] StudentSearchPagedDto input)
=> await studentService.GetPagedAsync(input);
[HttpGet("{id}")]
[AdncAuthorize([PermissionConsts.Student.Get, PermissionConsts.Student.Update])]
public async Task<ActionResult<StudentDto>> GetAsync([FromRoute] long id)
{
var dto = await studentService.GetAsync(id);
return dto is null ? NotFound() : dto;
}
}By following this workflow, you can implement features efficiently and consistently within the ADNC architecture. Key takeaways: always return Problem for business errors, keep controllers "thin," and ensure all cross-cutting concerns (logging, auth, validation) are handled via framework-level abstractions.
- Official Website: https://aspdotnetcore.net
- Online Demo: https://online.aspdotnetcore.net
- Documentation: https://docs.aspdotnetcore.net
- Code Generator: https://code.aspdotnetcore.net