跳到主要内容

教程:从零创建一个业务模块

本教程创建一个最小的 Notice 模块,并完整接入当前主宿主。示例遵循仓库正在使用的边界:

  • FreeKit.Notice:实体、Manager、Application Service、契约和 Core Startup。
  • FreeKit.Notice.HttpApi:Controller 和 HttpApi Startup。
  • FreeKit.DI:后台 Host 可复用的 Core 模块清单。
  • FreeKit.Host:同时装配 Core 和 HttpApi。

最终接口为 POST /api/notice/notices,归入现有 Swagger v1 文档。

1. 创建两个项目

目录:

src/Services/Notice/
├── FreeKit.Notice/
│ ├── Application/Notices/
│ ├── Contracts/
│ ├── Domain/Notices/
│ ├── FreeKit.Notice.csproj
│ └── NoticeModuleStartup.cs
└── FreeKit.Notice.HttpApi/
├── Controllers/
├── FreeKit.Notice.HttpApi.csproj
└── NoticeHttpApiModuleStartup.cs

Core 项目文件:

src/Services/Notice/FreeKit.Notice/FreeKit.Notice.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
</PropertyGroup>

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<ProjectReference Include="..\..\..\BuildingBlocks\IGeekFan.FreeKit.Web\IGeekFan.FreeKit.Web.csproj" />
</ItemGroup>
</Project>

HttpApi 项目文件:

src/Services/Notice/FreeKit.Notice.HttpApi/FreeKit.Notice.HttpApi.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
</PropertyGroup>

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<ProjectReference Include="..\FreeKit.Notice\FreeKit.Notice.csproj" />
</ItemGroup>
</Project>

将两个项目加入根解决方案:

dotnet sln FreeKitModules.slnx add src/Services/Notice/FreeKit.Notice/FreeKit.Notice.csproj
dotnet sln FreeKitModules.slnx add src/Services/Notice/FreeKit.Notice.HttpApi/FreeKit.Notice.HttpApi.csproj

2. 定义实体

src/Services/Notice/FreeKit.Notice/Domain/Notices/NoticeItem.cs
using IGeekFan.FreeKit.Extras.AuditEntity;

namespace FreeKit.Notice.Domain.Notices;

public sealed class NoticeItem : FullAuditEntity
{
public string Title { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public DateTime PublishedAt { get; set; }
}

FullAuditEntity 使用 Guid 主键并包含创建、修改、删除审计和软删除字段。PublishedAt 是业务时间,后面显式写入 DateTime.UtcNow

3. 定义契约和权限

src/Services/Notice/FreeKit.Notice/Contracts/NoticeContracts.cs
using System.ComponentModel.DataAnnotations;
using IGeekFan.FreeKit.Infrastructure.Application;

namespace FreeKit.Notice.Contracts;

public sealed class PublishNoticeRequest
{
[Required]
[MaxLength(200)]
public string Title { get; set; } = string.Empty;

[Required]
public string Content { get; set; } = string.Empty;
}

public sealed class NoticeDto
{
public Guid Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public DateTime PublishedAt { get; set; }
}

public interface INoticeService : IApplicationService
{
Task<NoticeDto> PublishAsync(PublishNoticeRequest request);
}

public static class NoticePermissions
{
public const string GroupName = "Notice";

public static class Notices
{
public const string Default = GroupName + ".Notices";
public const string Create = Default + ".Create";
}
}

公开 DTO 不使用 [JsonPropertyName],因为主 API 边界由 Newtonsoft.Json 处理。

4. 实现 Domain Manager

src/Services/Notice/FreeKit.Notice/Domain/Notices/NoticeManager.cs
using IGeekFan.FreeKit.Extras.FreeSql;
using IGeekFan.FreeKit.Infrastructure.DDD;
using IGeekFan.FreeKit.Infrastructure.Exceptions;

namespace FreeKit.Notice.Domain.Notices;

public interface INoticeManager : IDomainService
{
Task<NoticeItem> PublishAsync(
string title,
string content);
}

public sealed class NoticeManager(
IAuditBaseRepository<NoticeItem> repository)
: DomainService, INoticeManager
{
public async Task<NoticeItem> PublishAsync(
string title,
string content)
{
if (string.IsNullOrWhiteSpace(title))
{
throw new BusinessException("公告标题不能为空");
}

var notice = new NoticeItem
{
Id = Guid.NewGuid(),
Title = title.Trim(),
Content = content,
PublishedAt = DateTime.UtcNow
};

return await repository.InsertAsync(notice);
}
}

Manager 通过 IAuditBaseRepository<NoticeItem> 持久化,不向 Application 或 Controller 暴露 IFreeSql

5. 实现 Application Service

src/Services/Notice/FreeKit.Notice/Application/Notices/NoticeService.cs
using FreeKit.Notice.Contracts;
using FreeKit.Notice.Domain.Notices;
using IGeekFan.FreeKit.Infrastructure.Application;

namespace FreeKit.Notice.Application.Notices;

public sealed class NoticeService(INoticeManager manager)
: ApplicationService, INoticeService
{
public async Task<NoticeDto> PublishAsync(
PublishNoticeRequest request)
{
NoticeItem notice = await manager.PublishAsync(
request.Title,
request.Content);

return new NoticeDto
{
Id = notice.Id,
Title = notice.Title,
Content = notice.Content,
PublishedAt = notice.PublishedAt
};
}
}

Application Service 负责用例编排和 DTO 转换,Controller 不直接调用 Manager。

6. 创建 Core Startup

src/Services/Notice/FreeKit.Notice/NoticeModuleStartup.cs
using FreeKit.Notice.Application.Notices;
using FreeKit.Notice.Contracts;
using FreeKit.Notice.Domain.Notices;
using FreeSql;
using IGeekFan.FreeKit.Modularity;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace FreeKit.Notice;

public sealed class NoticeModuleStartup : IModuleStartup
{
public void ConfigureServices(
IServiceCollection services,
IConfiguration configuration)
{
services.AddScoped<INoticeManager, NoticeManager>();
services.AddScoped<INoticeService, NoticeService>();
}

public void Configure(
WebApplication app,
IWebHostEnvironment env)
{
#if DEBUG
IFreeSql freeSql = app.Services.GetRequiredService<IFreeSql>();
freeSql.CodeFirst.SyncStructure<NoticeItem>();
#endif
}
}

两个接口方法都必须实现。示例在 DEBUG 下同步表结构,Release 部署必须使用正式迁移或预建表,不能依赖这段代码。

开放泛型 IAuditBaseRepository<> 已由 Web BuildingBlock 注册,不存在也不需要调用 AddFreeRepository(typeof(NoticeItem).Assembly)

7. 创建 Controller

src/Services/Notice/FreeKit.Notice.HttpApi/Controllers/NoticeController.cs
using FreeKit.Notice.Contracts;
using IGeekFan.FreeKit.Infrastructure.Application;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace FreeKit.Notice.HttpApi.Controllers;

[ApiExplorerSettings(GroupName = "v1")]
[Route("api/notice/notices")]
[ApiController]
public sealed class NoticeController(INoticeService service)
: FreeKitController
{
[HttpPost]
[Authorize(NoticePermissions.Notices.Create)]
public Task<NoticeDto> PublishAsync(
[FromBody] PublishNoticeRequest request)
{
return service.PublishAsync(request);
}
}

路由显式写成 /api/notice/noticesFreeKitController 默认要求登录,Action 再要求 Notice.Notices.Create 权限。

主 Host 当前只在 DEBUG 下执行权限扫描同步;生产环境必须把新权限纳入发布初始化流程,否则动态 policy 会正确拒绝尚未入库的权限。

8. 创建 HttpApi Startup

src/Services/Notice/FreeKit.Notice.HttpApi/NoticeHttpApiModuleStartup.cs
using IGeekFan.FreeKit.Modularity;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace FreeKit.Notice.HttpApi;

public sealed class NoticeHttpApiModuleStartup : IModuleStartup
{
public void ConfigureServices(
IServiceCollection services,
IConfiguration configuration)
{
}

public void Configure(
WebApplication app,
IWebHostEnvironment env)
{
}
}

即使当前为空,这个 Startup 也不能省略:AddModule 根据 Startup 所在程序集添加 MVC ApplicationPart,Controller 才会被发现。未来的 Hub、HTTP Client 或 HTTP 专属替换也应放在这里。

9. 接入 FreeKit.DI

FreeKit.DI 必须能编译引用 Core Startup。在 src/Services/Host/FreeKit.DI/FreeKit.DI.csproj 增加:

<ProjectReference Include="..\..\Notice\FreeKit.Notice\FreeKit.Notice.csproj" />

然后修改 src/Services/Host/FreeKit.DI/E.cs

using FreeKit.Notice;

public static readonly Dictionary<string, Type> Modules =
new(StringComparer.OrdinalIgnoreCase)
{
// 现有模块……
["notice"] = typeof(NoticeModuleStartup)
};

这里登记 Core Startup,使 Job、Message 等后台 Host 可以引用业务服务而不暴露 Controller。

10. 接入主 Host

首先在 src/Services/Host/FreeKit.Host/FreeKit.Host.csproj 引用 HttpApi:

<ProjectReference Include="..\..\Notice\FreeKit.Notice.HttpApi\FreeKit.Notice.HttpApi.csproj" />

然后在 src/Services/Host/FreeKit.Host/Program.cs 增加命名空间:

using FreeKit.Notice;
using FreeKit.Notice.HttpApi;

与其他业务模块一样,先直接注册 Core:

builder.Services.AddModule<NoticeModuleStartup>(
"notice",
builder.Configuration);

再在 hostModules 初始化器中把 Core 替换成 HttpApi:

var hostModules = new Dictionary<string, Type>(
E.Modules,
StringComparer.OrdinalIgnoreCase)
{
// 现有替换……
["notice"] = typeof(NoticeHttpApiModuleStartup)
};

最后把 Core 程序集加入 extraAssemblies

builder.UseFreeKit(
moduleTypeMap: hostModules,
extraAssemblies:
[
// 现有程序集……
typeof(NoticeModuleStartup).Assembly
]);

不要再调用第二次 UseFreeKit。现有 app.ConfigureModules().Init() 会同时执行 Notice Core 和 HttpApi 的 Configure

当前 Job 和 Message Host 虽然使用 E.Modules 注册 Core,但没有调用 ConfigureModules()。如果 Notice 的后台能力依赖 Configure 初始化,需要先在相应 Host 明确补齐该生命周期,不能假设它已经执行。

11. 构建和验证

dotnet restore FreeKitModules.slnx
dotnet build FreeKitModules.slnx -c Release
dotnet test FreeKitModules.slnx
dotnet run --project src/Services/Host/FreeKit.Host

开发环境打开:

  • Swagger:https://localhost:7000/kit_api/swagger
  • 接口:POST https://localhost:7000/kit_api/api/notice/notices

在 Swagger 中先完成认证,并确保当前用户拥有 Notice.Notices.Create。请求示例:

{
"title": "系统维护通知",
"content": "今晚 23:00 开始维护。"
}

接入检查表

  • Core 和 HttpApi 都是 net10.0,HttpApi 引用 Core。
  • Core/HttpApi Startup 都完整实现 ConfigureServicesConfigure
  • FreeKit.DI.csproj 引用 Core,E.Modules 登记 Core Startup。
  • FreeKit.Host.csproj 引用 HttpApi。
  • 主 Host 直接注册 Core,并把同一 key 替换成 HttpApi Startup。
  • Core 程序集已加入 extraAssemblies
  • Controller 使用真实 Swagger 分组、显式路由和权限常量。
  • Release 有数据库迁移与权限初始化方案。
  • 异步方法以 Async 结尾,持久化时间使用 DateTime.UtcNow

相关文档